From aba7ffbac2be817ca6aad96e0c2661d979a218f6 Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Mon, 27 Jul 2026 12:06:22 -0400 Subject: [PATCH] test: cover C1 direct-suppressed handling, C2 corpus guard, and I2 candidate filter Adds regression coverage for the three preceding fixes: - "total" on a legacy aggregate-only family (AL Corrections 2011) now fires recipe suggestions, flags provenance$expenditure_concept_direct_ suppressed, and names a recovering recipe in the affected row's notes, plus a contrast test confirming the flag stays FALSE when the Direct leg is present. - expenditure_concept = "total" aborts with class uscogdata_ig_categories_unsupported against a corpus whose summary_categories carries no M/L rows (new with_corpus_missing_ig_categories() fixture helper), is unaffected for "direct" on the same corpus, and still works on a corpus that does carry M/L rows. - an M/L recipe (corrections_ig_local_combined) no longer appears as a raw suggestion for a Direct-flavored cog_spending() call. --- tests/testthat/test-expenditure-concept.R | 112 ++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/testthat/test-expenditure-concept.R b/tests/testthat/test-expenditure-concept.R index cd473a1..b19f374 100644 --- a/tests/testthat/test-expenditure-concept.R +++ b/tests/testthat/test-expenditure-concept.R @@ -309,6 +309,118 @@ test_that("a mis-scoped cog_spending() call never attaches an M/L counterpart to expect_length(ig, 0L) }) +test_that("C1: 'total' on a legacy aggregate-only family reports the IG-only figure honestly, not as Direct + IG", { + # AL state government, Corrections, 2011. Measured pre-fix: 'total' + # returned $31,358,000 (the IG leg alone, on an aggregate-flagged M04/M05 + # row) with 0 suggestions (the surviving IG row made the gap-detection + # machinery think the Direct leg was covered) and a note asserting + # "Total = Direct + intergovernmental" with no caveat. True Direct (via + # recipe = "corrections_combined") is $521,651,000 -- the IG-only figure + # is ~6% of it. + gov <- "010000226085" + + d <- cog_spending(gov, years = 2011, category = "Corrections", + expenditure_concept = "direct") + expect_equal(nrow(d), 0L) + + t <- suppressMessages(cog_spending( + gov, years = 2011, category = "Corrections", expenditure_concept = "total" + )) + expect_equal(nrow(t), 1L) + expect_equal(t$spend_subtype, "intergovernmental") + expect_equal(t$amt_nominal, 31358000) + + r <- cog_spending(gov, years = 2011, recipe = "corrections_combined") + expect_equal(r$amt_nominal, 521651000) + + # C1(a): the recipe hints must fire for "total" exactly as they do for + # "direct" -- the surviving IG row must not be mistaken for Direct + # coverage. + prov <- attr(t, "provenance") + expect_gt(length(prov$suggestions), 0L) + ids <- vapply(prov$suggestions, function(s) s$recipe_id %||% "", character(1)) + expect_true("corrections_combined" %in% ids) + + # C1(b): the affected row's notes name a recovering recipe rather than + # staying silent, and the provenance carries a flag a downstream consumer + # (e.g. cog-api, which passes provenance through verbatim) can test. + expect_true(nzchar(t$notes)) + expect_match(t$notes, "unavailable", fixed = TRUE) + expect_match(t$notes, "corrections_combined", fixed = TRUE) + expect_true(prov$expenditure_concept_direct_suppressed) + + # The base "Total = Direct + IG" note must NOT stand unqualified when that + # arithmetic didn't actually happen for this row. + expect_match(prov$expenditure_concept_note, "NOTE", fixed = TRUE) + expect_match(prov$expenditure_concept_note, + "expenditure_concept_direct_suppressed", fixed = TRUE) +}) + +test_that("C1(b): expenditure_concept_direct_suppressed is FALSE when the Direct leg is present", { + d <- cog_spending("010000226085", years = 2019, category = "Police", + expenditure_concept = "direct") + t <- cog_spending("010000226085", years = 2019, category = "Police", + expenditure_concept = "total") + expect_false(isTRUE(attr(d, "provenance")$expenditure_concept_direct_suppressed)) + expect_false(isTRUE(attr(t, "provenance")$expenditure_concept_direct_suppressed)) + expect_false(any(nzchar(t$notes[t$spend_subtype == "intergovernmental"]) & + grepl("unavailable", t$notes[t$spend_subtype == "intergovernmental"]))) +}) + +test_that("C2: expenditure_concept = 'total' aborts on a corpus with no intergovernmental category rows", { + with_corpus_missing_ig_categories({ + con <- uscogdata:::.ensure_session() + n <- DBI::dbGetQuery(con, + "SELECT COUNT(*) AS n FROM summary_categories WHERE LEFT(item_code, 1) IN ('M', 'L')" + )$n + expect_equal(n, 0) + + err <- tryCatch( + cog_spending("010000226085", years = 2019, category = "Police", + expenditure_concept = "total"), + condition = function(e) e + ) + expect_s3_class(err, "uscogdata_ig_categories_unsupported") + msg <- conditionMessage(err) + expect_match(msg, "PR #59|predates", perl = TRUE) + }) + + # 'direct' is unaffected on the same corpus -- the guard is scoped to + # expenditure_concept = "total" only. + with_corpus_missing_ig_categories({ + expect_no_error( + cog_spending("010000226085", years = 2019, category = "Police", + expenditure_concept = "direct") + ) + }) +}) + +test_that("C2: expenditure_concept = 'total' still works on a corpus that DOES carry M/L category rows", { + expect_no_error( + cog_spending("010000226085", years = 2019, category = "Police", + expenditure_concept = "total") + ) +}) + +test_that("I2: an intergovernmental (M/L) recipe never appears as its own top-level suggestion", { + # Task 1's M04/M05 category rows share the "Corrections" summary_categories + # category with the Direct-flavored E04/E05, so `corrections_ig_local_ + # combined` (entirely M-prefixed) becomes a raw *candidate* in + # .build_suggestions()'s component_code-driven query. Following a + # "re-run with recipe = 'corrections_ig_local_combined'" hint on a plain + # cog_spending() call would silently return intergovernmental dollars + # under provenance$expenditure_concept = "direct". Task 6's gate + # (.attach_ig_counterparts()) already protects the *counterpart* lookup; + # this exercises that the candidate list itself is filtered too. + r <- suppressMessages( + cog_spending("010000226085", years = c(2005, 2011), category = "Corrections") + ) + sugg <- attr(r, "provenance")$suggestions + ids <- vapply(sugg, function(s) s$recipe_id %||% "", character(1)) + expect_true("corrections_combined" %in% ids) + expect_false("corrections_ig_local_combined" %in% ids) +}) + test_that(".attach_ig_counterparts() never pairs a revenue-side recipe with its coincidental M/L suffix twin", { # Broader version of the case above, run at the matching-helper level # (the same level code review's pairwise enumeration was done at) rather