Nothing
# Tests for v0.2.0 Target-2 grass_report() / grass_card class.
#
# Phase 4A scope: the new headline `grass_report(ratings = Y)` flow plus the
# non-plot S3 methods (print, summary, as.data.frame, format). Plotting is
# tested separately by Phase 4B.
# ---- Shared fixtures ------------------------------------------------------
# `.simulate_op_strong_panel()` builds the §4 divergent worked-example case
# from the merged paper: k=5 raters with alternating bias direction
# (R1, R3, R5 favor sensitivity at Se=0.95/Sp=0.75; R2, R4 favor specificity
# at Se=0.75/Sp=0.95), F = logit-normal(pi_target=0.50, tau2=0.25), N=1000.
# This DGP is the `op_strong` profile from paper2/code/21_asym_grid.R at
# q = 0.85. seed = 6 produces the divergent panel cited in §4
# (delta_hat ~ 18 pp; AC1 separates from kappa-family on the surface).
.simulate_op_strong_panel <- function(seed = 6L) {
set.seed(seed)
N <- 1000L; k <- 5L
Se <- c(0.95, 0.75, 0.95, 0.75, 0.95)
Sp <- c(0.75, 0.95, 0.75, 0.95, 0.75)
mu_logit <- qlogis(0.50)
p_i <- plogis(rnorm(N, mu_logit, sqrt(0.25)))
C <- rbinom(N, 1, p_i)
Y <- matrix(0L, N, k)
for (j in seq_len(k)) {
Y[, j] <- rbinom(N, 1, ifelse(C == 1L, Se[j], 1 - Sp[j]))
}
Y
}
# ---- Test 1 ---------------------------------------------------------------
# Basic call returns a grass_card with the documented top-level structure.
test_that("grass_report returns a grass_card with all top-level fields", {
set.seed(1)
Y <- matrix(rbinom(500, 1, 0.3), nrow = 100, ncol = 5)
card <- grass_report(Y, bootstrap_B = 50)
expect_s3_class(card, "grass_card")
expect_type(card, "list")
expected_fields <- c("sample", "coefficient", "delta", "panel", "per_rater",
"surface", "call", "grass_version", "timestamp",
"inputs", "notes")
for (f in expected_fields) {
expect_true(f %in% names(card),
info = paste("missing field:", f))
}
# sample fields
expect_true(all(c("k", "N", "pi_hat", "tau2_hat", "axis") %in%
names(card$sample)))
expect_equal(card$sample$k, 5L)
expect_equal(card$sample$N, 100L)
expect_true(card$sample$pi_hat > 0 && card$sample$pi_hat < 1)
expect_equal(card$sample$axis, "inter")
# coefficient fields (v0.7.1: `qualifier` retired; `band` is a rendered
# consistency-band string, plus percentile_basis / consistency_band / sweep).
expect_true(all(c("primary", "observed_value", "surface_percentile",
"band", "q_hat", "consistency_band") %in%
names(card$coefficient)))
expect_false("qualifier" %in% names(card$coefficient))
# delta fields
expect_true(all(c("delta_hat", "flag", "thresholds") %in%
names(card$delta)))
expect_true(card$delta$flag %in% c("aligned", "caution", "divergent"))
expect_named(card$delta$thresholds, c("caution", "divergent"))
# panel data.frame structure (v0.7.1 columns: consistency-band endpoints
# instead of a modal band string / qualifier).
expect_s3_class(card$panel, "data.frame")
expect_true(nrow(card$panel) >= 2)
expect_true(all(c("coefficient", "observed_value", "surface_percentile",
"band_lo", "band_hi", "band_open_low", "band_open_high",
"q_hat", "se_q_hat", "clamped", "reference_used",
"in_delta_hat") %in% names(card$panel)))
expect_false("qualifier" %in% names(card$panel))
})
# ---- Test 2 ---------------------------------------------------------------
# Paper §4 divergent worked-example reproduction (load-bearing integration
# test). Uses the op_strong asym-grid profile at q=0.85, pi_target=0.50,
# N=1000, seed=6 — see .simulate_op_strong_panel() at top of file.
# Heterogeneous per-rater bias produces genuine cross-coefficient
# discordance; the divergent flag is real (not an ICC-clamp artifact).
test_that("grass_report reproduces the paper §4 divergent worked example", {
skip_if_not_installed("lme4") # the fixture pins the 4-row panel (ICC row)
Y <- .simulate_op_strong_panel(seed = 6L)
card <- grass_report(Y, bootstrap_B = 200)
# Re-pinned 2026-07-06 against the stage-6B delta-B null: divergent at
# the >= 99th percentile of the matched null (delta ~0.25 quality pp).
expect_equal(card$delta$flag, "divergent")
expect_gte(card$delta$delta_percentile, 99)
# The divergent flag must come from real cross-coefficient disagreement,
# not an ICC-clamp artifact: the non-ICC pooled percentiles also spread
# (observed ~2 pp at re-pin; aligned panels stay well under 0.5 pp).
nonicc <- card$panel$surface_percentile[card$panel$coefficient != "icc"]
expect_true(diff(range(nonicc, na.rm = TRUE)) >= 1,
info = sprintf("non-ICC pooled-pct range = %.2f pp; expected >= 1",
diff(range(nonicc, na.rm = TRUE))))
# Four-coefficient panel (PABAK, AC1, Fleiss kappa, ICC; alpha removed
# from the panel at v0.6.0).
expect_equal(nrow(card$panel), 4L)
expect_true("clamped" %in% names(card$panel))
# Per-rater data.frame with five rows of finite Se/Sp + CIs.
expect_s3_class(card$per_rater, "data.frame")
expect_equal(nrow(card$per_rater), 5L)
expect_true(all(is.finite(card$per_rater$se_hat)))
expect_true(all(is.finite(card$per_rater$sp_hat)))
expect_true(all(is.finite(card$per_rater$se_lower)))
expect_true(all(is.finite(card$per_rater$se_upper)))
expect_true(all(is.finite(card$per_rater$sp_lower)))
expect_true(all(is.finite(card$per_rater$sp_upper)))
# Per-rater point estimates separate Se-favoring vs Sp-favoring raters
# (R1, R3, R5 should have higher Se than Sp; R2, R4 the reverse).
pr <- card$per_rater
for (j in c(1L, 3L, 5L)) {
expect_gt(pr$se_hat[j], pr$sp_hat[j],
label = sprintf("R%d should be Se-favoring", j))
}
for (j in c(2L, 4L)) {
expect_gt(pr$sp_hat[j], pr$se_hat[j],
label = sprintf("R%d should be Sp-favoring", j))
}
# Headline coefficient band suppressed under the divergent flag.
expect_equal(card$coefficient$band, "suppressed")
})
# ---- Test 3 ---------------------------------------------------------------
# Paper §4 k=2 symmetric counter-example: 200 subjects, prevalence 0.05,
# Se = Sp = 0.94. Should resolve as `aligned`, primary band Strong /
# Excellent, no per-rater table.
test_that("grass_report handles the §4 k=2 symmetric counter-example", {
set.seed(1)
N <- 200; k <- 2
pi <- 0.05
Se <- 0.94; Sp <- 0.94
C <- rbinom(N, 1, pi)
Y <- matrix(0L, N, k)
for (j in seq_len(k)) {
Y[, j] <- rbinom(N, 1, ifelse(C == 1, Se, 1 - Sp))
}
card <- grass_report(Y, bootstrap_B = 50)
# v0.7.1 delta-B: at k = 2 the two-coefficient family implies identical
# quality by construction, so delta_hat is structurally uninformative
# and the flag reports not_applicable (never aligned/caution/divergent).
expect_equal(card$delta$flag, "not_applicable")
expect_equal(card$delta$thresholds_source, "not_applicable_k2")
# v0.7.1: the primary `band` is a rendered consistency-band string on
# quality (not the retired Strong/Excellent adjective), and is only
# "suppressed" under the divergent flag.
expect_type(card$coefficient$band, "character")
expect_false(identical(card$coefficient$band, "suppressed"))
expect_match(card$coefficient$band, "quality")
expect_true(is.list(card$coefficient$consistency_band))
expect_null(card$per_rater)
})
# ---- Test 4 ---------------------------------------------------------------
# print() emits "GRASS Report Card" header and shape varies with flag.
.make_align_card_k2 <- function() {
set.seed(1)
N <- 200; k <- 2
pi <- 0.05
Se <- 0.94; Sp <- 0.94
C <- rbinom(N, 1, pi)
Y <- matrix(0L, N, k)
for (j in seq_len(k)) {
Y[, j] <- rbinom(N, 1, ifelse(C == 1, Se, 1 - Sp))
}
grass_report(Y, bootstrap_B = 0)
}
test_that("print.grass_card renders the aligned card header, gloss and delta", {
card_align <- .make_align_card_k2()
expect_output(print(card_align), "GRASS Report Card")
out_align <- capture.output(print(card_align))
txt_align <- paste(out_align, collapse = "\n")
# k = 2 card: delta is not_applicable (v0.7.1) with the n/a matched-null
# line routing to the pairwise/bounds path.
expect_match(txt_align, "not_applicable", fixed = TRUE)
expect_match(txt_align, "n/a at k = 2", fixed = TRUE)
# v0.7.1: the aligned card carries a plain-language "read:" gloss line.
expect_match(txt_align, "read:", fixed = TRUE)
# The primary coefficient (PABAK) and its consistency band on quality show.
expect_match(txt_align, "PABAK", fixed = TRUE)
expect_match(txt_align, "percentile", fixed = TRUE)
# Debug-grade notes (glmer / F_key provenance) stay off the headline card.
expect_false(grepl("F_key picked via glmer", txt_align, fixed = TRUE))
})
test_that("print.grass_card renders the divergent card (all coefficients + suppressed)", {
# Re-pinned 2026-07-06: op_strong fires divergent on the delta-B null.
Y_div <- .simulate_op_strong_panel(seed = 6L)
card_div <- grass_report(Y_div, bootstrap_B = 50)
expect_output(print(card_div), "GRASS Report Card")
out_div <- capture.output(print(card_div))
txt_div <- paste(out_div, collapse = "\n")
expect_match(txt_div, "PABAK", fixed = TRUE)
expect_match(txt_div, "AC1", fixed = TRUE)
expect_match(txt_div, "Fleiss kappa", fixed = TRUE)
expect_match(txt_div, "suppressed", fixed = TRUE)
expect_match(txt_div, "divergent", fixed = TRUE)
})
# ---- Test 5 ---------------------------------------------------------------
# summary() returns a summary.grass_card object, prints multi-section block.
test_that("summary.grass_card returns the documented structure", {
Y <- .simulate_op_strong_panel(seed = 6L)
card <- grass_report(Y, bootstrap_B = 50)
s <- summary(card)
expect_s3_class(s, "summary.grass_card")
expect_true("panel" %in% names(s))
expect_s3_class(s$panel, "data.frame")
expect_output(print(s), "panel")
})
# ---- Test 6 ---------------------------------------------------------------
# as.data.frame() returns a data.frame whatever the flag; on divergent cards
# the per-rater table rides along as an attribute. The panel rows always have
# an `is_primary` logical column.
test_that("as.data.frame.grass_card has an is_primary column", {
set.seed(1)
N <- 200; k <- 2
pi <- 0.05
Se <- 0.94; Sp <- 0.94
C <- rbinom(N, 1, pi)
Y <- matrix(0L, N, k)
for (j in seq_len(k)) {
Y[, j] <- rbinom(N, 1, ifelse(C == 1, Se, 1 - Sp))
}
card_align <- grass_report(Y, bootstrap_B = 50)
df_align <- as.data.frame(card_align)
expect_s3_class(df_align, "data.frame")
expect_true("is_primary" %in% names(df_align))
expect_true(any(df_align$is_primary))
})
test_that("as.data.frame.grass_card stays a data.frame on divergent cards, per_rater as attribute", {
# 2026-09-05: a divergent card used to come back as a list, which broke
# binding many studies' cards into one table (usability test finding).
Y <- .simulate_op_strong_panel(seed = 6L)
card_div <- grass_report(Y, bootstrap_B = 50)
df_div <- as.data.frame(card_div)
expect_s3_class(df_div, "data.frame")
expect_true("is_primary" %in% names(df_div))
expect_s3_class(attr(df_div, "per_rater"), "data.frame")
expect_true(nrow(attr(df_div, "per_rater")) >= 2L)
})
# ---- Test 7 ---------------------------------------------------------------
# verbose = TRUE emits at least one progress message.
test_that("grass_report with verbose = TRUE emits a progress message", {
set.seed(1)
Y <- matrix(rbinom(500, 1, 0.3), nrow = 100, ncol = 5)
expect_message(grass_report(Y, bootstrap_B = 25, verbose = TRUE),
regexp = NULL)
})
# ---- v0.2.3 auto-trigger of pairwise_agreement under divergent flag -------
# Per paper §3.3, divergent panels populate `card$pairwise` with the
# pairwise PABAK matrix + per-rater pooled-reference table; aligned panels
# do not. This is the load-bearing wiring for the divergent recovery path.
test_that("grass_report auto-populates card$pairwise on divergent panels", {
# Re-pinned 2026-07-06: op_strong fires divergent on the delta-B null.
Y <- .simulate_op_strong_panel(seed = 6L)
card <- grass_report(Y, bootstrap_B = 50)
expect_equal(card$delta$flag, "divergent")
expect_s3_class(card$pairwise, "grass_pairwise")
expect_equal(dim(card$pairwise$pabak_matrix), c(5L, 5L))
expect_equal(nrow(card$pairwise$pooled_per_rater), 5L)
})
test_that("grass_report leaves card$pairwise NULL when flag is not divergent", {
set.seed(2)
Y <- matrix(rbinom(500, 1, 0.3), nrow = 100, ncol = 5)
card <- grass_report(Y, bootstrap_B = 25)
expect_false(card$delta$flag == "divergent")
expect_null(card$pairwise)
})
test_that("exported percentiles carry one decimal, no more", {
set.seed(11)
Y <- sapply(1:5, function(j) rbinom(300, 1, 0.3))
d <- as.data.frame(grass_report(Y, verbose = FALSE))
expect_equal(d$surface_percentile, round(d$surface_percentile, 1))
p <- as.data.frame(position_on_surface(0.56, "pabak", pi_hat = 0.3, k = 5, N = 300))
expect_equal(p$percentile, round(p$percentile, 3))
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.