test_that("ai_group_disparity computes demographic parity correctly", { outcome <- c(1, 1, 0, 0, 1, 0) group <- c("A", "A", "A", "B", "B", "B") res <- ai_group_disparity(outcome, group) expect_equal(round(res$group_means[["A"]], 3), round(2/3, 3)) expect_equal(round(res$group_means[["B"]], 3), round(1/3, 3)) expect_equal(round(res$demographic_parity_diff, 3), round(1/3, 3)) }) test_that("ai_group_disparity rejects probabilities passed as predicted_class", { outcome <- c(1, 0, 1, 0) group <- c("A", "A", "B", "B") bad_pred <- c(0.2, 0.8, 0.4, 0.9) expect_error(ai_group_disparity(outcome, group, predicted_class = bad_pred), "must be binary") }) test_that("ai_dif_mh requires exactly two groups", { expect_error( ai_dif_mh(c(1,0,1,0,1,0), c("A","B","C","A","B","C"), c(1,2,3,4,5,6)), "exactly 2 groups" ) }) test_that("ai_dif_mh runs and returns expected fields", { set.seed(1) n <- 200 total <- rnorm(n) group <- sample(c("ref", "focal"), n, replace = TRUE) item <- rbinom(n, 1, plogis(total)) res <- ai_dif_mh(item, group, total, n_strata = 4) expect_true(all(c("mh_or", "mh_delta", "chisq", "p_value") %in% names(res))) expect_true(res$mh_or > 0) }) test_that("ai_fairness warns rather than errors when aiDIF is not installed", { skip_if(requireNamespace("aiDIF", quietly = TRUE), "aiDIF is installed; see 'dispatches to aiDIF' test instead") outcome <- c(1,0,1,0); group <- c("A","A","B","B") expect_warning( ai_fairness(outcome, group, human_mle = list(), ai_mle = list()), "aiDIF" ) }) test_that("ai_fairness dispatches to aiDIF::fit_aidif with valid data when installed", { skip_if_not_installed("aiDIF") outcome <- c(1,0,1,0); group <- c("A","A","B","B") dat <- aiDIF::simulate_aidif_data(n_items = 6, seed = 1) res <- ai_fairness(outcome, group, human_mle = dat$human, ai_mle = dat$ai) expect_true(!is.null(res$item_level_dasb)) expect_s3_class(res$item_level_dasb, "aidif") }) test_that("ai_fairness's malformed-mle error from aiDIF is informative, not swallowed", { skip_if_not_installed("aiDIF") outcome <- c(1,0,1,0); group <- c("A","A","B","B") # empty lists are invalid mle input -- aiDIF's own validation should # surface a clear error, not be silently caught by aiEvalR expect_error( ai_fairness(outcome, group, human_mle = list(), ai_mle = list()), "est.*var.cov|var.cov.*est" ) }) ## ---- additional coverage: chi-square, equalized odds, dashboard ---- test_that("ai_dif_mh returns chi-square and p-value with valid structure", { set.seed(40) n <- 300 total <- rnorm(n) group <- sample(c("ref", "focal"), n, replace = TRUE) item <- rbinom(n, 1, plogis(total)) res <- ai_dif_mh(item, group, total, n_strata = 5) expect_true(all(c("mh_or", "mh_delta", "chisq", "p_value", "n_strata_used") %in% names(res))) expect_gte(res$chisq, 0) expect_gte(res$p_value, 0) expect_lte(res$p_value, 1) }) test_that("ai_dif_mh detects strong planted DIF with a small p-value", { set.seed(41) n <- 600 total <- rnorm(n) group <- sample(c("ref", "focal"), n, replace = TRUE) shift <- ifelse(group == "focal", 1.5, 0) # strong DIF item <- rbinom(n, 1, plogis(total + shift)) res <- ai_dif_mh(item, group, total, n_strata = 5) expect_lt(res$p_value, 0.05) }) test_that("ai_group_disparity computes equalized-odds gaps for binary decisions", { set.seed(42) outcome <- rbinom(200, 1, 0.5) group <- sample(c("A", "B"), 200, replace = TRUE) predicted <- rbinom(200, 1, 0.5) out <- ai_group_disparity(outcome, group, predicted_class = predicted) expect_true("equalized_odds" %in% names(out)) expect_true(all(c("tpr_gap", "fpr_gap") %in% names(out$equalized_odds))) }) test_that("ai_fairness computes calibration-by-group when probabilities given", { set.seed(43) outcome <- rbinom(200, 1, 0.4) group <- sample(c("A", "B"), 200, replace = TRUE) prob <- pmin(pmax(outcome * 0.5 + runif(200, 0, 0.5), 0), 1) out <- ai_fairness(outcome, group, predicted_probability = prob) expect_true("calibration_by_group" %in% names(out)) }) test_that("fairness_dashboard prints a summary without error", { outcome <- c(1, 1, 0, 0, 1, 0) group <- c("A", "A", "A", "B", "B", "B") fobj <- ai_fairness(outcome, group) expect_output(fairness_dashboard(fobj), "Fairness Summary") }) test_that("fairness_dashboard rejects non-fairness objects", { expect_error(fairness_dashboard(list(a = 1)), "aiEvalR_fairness") })