# tests/testthat/test-recipes.R # # cog_recipes(), recipe = in cog_spending()/cog_revenue(), and the # recipe-component-driven signposting in prov$suggestions (Phase R2 / # Task 11, schema_version 5). test_that("cog_recipes lists the curated catalog including corrections_combined", { skip_if_no_corpus() r <- cog_recipes() expect_s3_class(r, "tbl_df") expect_equal(names(r), c("recipe_id", "label", "n_components", "year_min", "year_max")) expect_equal(nrow(r), 24L) expect_true("corrections_combined" %in% r$recipe_id) expect_true("t19_selective_sales_wide" %in% r$recipe_id) expect_true("ig_federal_b89_wide" %in% r$recipe_id) expect_true("rents_royalties_u4_wide" %in% r$recipe_id) expect_true("higher_ed_e18_wide" %in% r$recipe_id) expect_true("cash_securities_z77_wide" %in% r$recipe_id) # Superseded id from the pre-curation brief text must NOT be present. expect_false("corrections_judicial_combined" %in% r$recipe_id) }) test_that("cog_recipes(pattern=) filters by recipe_id or label", { skip_if_no_corpus() r <- cog_recipes("corrections") expect_true(nrow(r) >= 1L) expect_true(all(grepl("corrections", r$recipe_id, ignore.case = TRUE) | grepl("corrections", r$label, ignore.case = TRUE))) }) test_that("cog_recipes requires schema_version >= 5", { skip_if_no_corpus() with_doctored_schema_version(4L, { expect_error(cog_recipes(), class = "uscogdata_schema_unsupported") }) }) # --- recipe = : generic join, no is_aggregate filter ----------------------- test_that("recipe = 'corrections_combined' is continuous across the 2011->2012 seam", { skip_if_no_corpus() r <- cog_spending("121011212191", years = c(2011L, 2012L), recipe = "corrections_combined") expect_equal(nrow(r), 2L) expect_true(all(c("year", "canonical_govid", "gov_name", "spend_subtype", "category", "amt_nominal", "codes_included", "aggregate_fallback", "notes") %in% names(r))) expect_equal(unique(r$spend_subtype), "recipe") expect_equal(unique(r$category), "Corrections (functions 04+05 combined)") expect_false(any(r$aggregate_fallback)) r2011 <- r$amt_nominal[r$year == 2011L] r2012 <- r$amt_nominal[r$year == 2012L] # 2011: E05 only exists as a wide-era AGGREGATE row (is_aggregate = TRUE) # for Broward -- data-verified $216,088,000. Since .run_recipe() does NOT # filter is_aggregate (amendment: the recipe join must not, because these # families exist ONLY as aggregate rows in the wide era), the recipe # correctly picks this up. expect_equal(r2011, 216088000) # 2012: modern E04 leaf ($213,056,000); Broward reports no E05 leaf that # year, so the recipe total equals E04 alone -- still continuous with the # 2011 aggregate, proving the wide-aggregate -> modern-leaf handoff. expect_equal(r2012, 213056000) expect_true(all(grepl("E04|E05", r$codes_included))) }) test_that("recipe result carries a recipe provenance block with component rows", { skip_if_no_corpus() r <- cog_spending("121011212191", years = c(2011L, 2012L), recipe = "corrections_combined") prov <- attr(r, "provenance") expect_equal(prov$basis, "recipe") expect_equal(prov$category, "Corrections (functions 04+05 combined)") expect_type(prov$recipe, "list") expect_equal(prov$recipe$recipe_id, "corrections_combined") expect_equal(prov$recipe$label, "Corrections (functions 04+05 combined)") expect_length(prov$recipe$components, 2L) comp_codes <- vapply(prov$recipe$components, function(x) x$component_code, character(1)) expect_setequal(comp_codes, c("E04", "E05")) # A recipe query resolves its own coverage; it should never also carry # suggestions for itself. expect_length(prov$suggestions, 0L) }) test_that("recipe results report an unambiguous basis/harmonization, ignoring basis=", { skip_if_no_corpus() # A recipe query bypasses spending_annotated(_harmonized) entirely -- # .run_recipe() joins `long` directly -- so `basis` must never read # "harmonized"/"raw" (which would describe a code path this query never # took) regardless of what the caller passed for `basis`. Task 12 # consumes provenance verbatim, so this needs to be unambiguous. r_default <- cog_spending("121011212191", years = c(2011L, 2012L), recipe = "corrections_combined") r_raw <- cog_spending("121011212191", years = c(2011L, 2012L), recipe = "corrections_combined", basis = "raw") r_harm <- cog_spending("121011212191", years = c(2011L, 2012L), recipe = "corrections_combined", basis = "harmonized") for (r in list(r_default, r_raw, r_harm)) { prov <- attr(r, "provenance") expect_equal(prov$basis, "recipe") expect_true(is.na(prov$basis_note)) expect_false(prov$harmonization$applied) expect_equal(prov$harmonization$na_rows_excluded, 0L) expect_match(prov$harmonization$note, "recipe", ignore.case = TRUE) } # basis= truly has zero effect on a recipe query's actual numbers. expect_equal(r_raw$amt_nominal, r_harm$amt_nominal) expect_equal(r_default$amt_nominal, r_raw$amt_nominal) }) test_that("recipe = 't19_selective_sales_wide' sums the local T11/T14 legs when present", { skip_if_no_corpus() # Westminster City, CA (canonical_govid 082001211654): T11 = 0 in 2011, # T11 = 568 (T14 = 0/absent) in 2012 -- a real, data-verified equality/ # inequality pair inside the amended fixture window (2011-2012), standing # in for the brief's original 2004/2005 example (out of scope per the # amended fixture years; the underlying local-tax-split boundary is # nationally FY2005, but this government's own T11 reporting activates # within our 2011-2012 window). r <- cog_revenue("082001211654", years = c(2011L, 2012L), recipe = "t19_selective_sales_wide") # Raw, single-code T19 total (not the "Other Taxes" category total, which # would also sum in T11/T14/T21/T23/T27/T29/T53/T99 -- queried directly to # isolate exactly the code the brief's equality/inequality check is about). con <- uscogdata:::.ensure_session() raw_t19 <- DBI::dbGetQuery(con, " SELECT year, SUM(amt) * 1000.0 AS amt FROM revenue_long WHERE canonical_govid = '082001211654' AND item_code = 'T19' AND year IN (2011, 2012) GROUP BY year ORDER BY year ") raw_t19_2011 <- raw_t19$amt[raw_t19$year == 2011L] raw_t19_2012 <- raw_t19$amt[raw_t19$year == 2012L] expect_equal(raw_t19_2011, 2231000) expect_equal(raw_t19_2012, 2365000) recipe_2011 <- r$amt_nominal[r$year == 2011L] recipe_2012 <- r$amt_nominal[r$year == 2012L] expect_equal(recipe_2011, raw_t19_2011) # equality: no local T11/T14 yet expect_gt(recipe_2012, raw_t19_2012) # inequality: local T11 joins in expect_equal(recipe_2012, raw_t19_2012 + 568000) }) test_that("recipe = and category = together aborts", { skip_if_no_corpus() expect_error( cog_spending("121011212191", 2020L, category = "Corrections", recipe = "corrections_combined"), class = "uscogdata_recipe_category_conflict" ) }) test_that("unknown recipe id aborts and lists valid ids", { skip_if_no_corpus() err <- tryCatch( cog_spending("121011212191", 2020L, recipe = "does_not_exist"), error = identity ) expect_s3_class(err, "uscogdata_unknown_recipe") expect_match(conditionMessage(err), "corrections_combined") }) test_that("recipe = requires schema_version >= 5", { skip_if_no_corpus() with_doctored_schema_version(4L, { expect_error( cog_spending("121011212191", 2020L, recipe = "corrections_combined"), class = "uscogdata_schema_unsupported" ) }) }) # --- signposting ------------------------------------------------------- test_that("signposting suggests corrections_combined across the 2011->2012 gap", { skip_if_no_corpus() expect_message( r <- cog_spending("121011212191", years = c(2011L, 2012L), category = "Corrections"), "recipe" ) prov <- attr(r, "provenance") expect_true(length(prov$suggestions) >= 1L) ids <- vapply(prov$suggestions, function(s) s$recipe_id, character(1)) expect_true("corrections_combined" %in% ids) hit <- prov$suggestions[[which(ids == "corrections_combined")]] expect_equal(hit$hint, "re-run with recipe = 'corrections_combined'") expect_equal(hit$available_years, c(1967L, 2023L)) }) test_that("no signposting when the result already has full year coverage", { skip_if_no_corpus() r <- cog_spending("121011212191", years = 2019:2020, category = "Corrections") prov <- attr(r, "provenance") expect_length(prov$suggestions, 0L) }) test_that("no signposting when category is NULL (unscoped query)", { skip_if_no_corpus() r <- cog_spending("121011212191", years = c(2011L, 2012L)) prov <- attr(r, "provenance") expect_length(prov$suggestions, 0L) }) test_that("no signposting under basis = 'raw'", { skip_if_no_corpus() r <- cog_spending("121011212191", years = c(2011L, 2012L), category = "Corrections", basis = "raw") prov <- attr(r, "provenance") expect_length(prov$suggestions, 0L) }) # --- uscogdata#9: partial-coverage signposting ------------------------------ test_that("no recipe component is ever renamed by harmonization", { # The suppression trigger anti-joins the verb's long view on item_code. # That is only sound because harmonization never rewrites a recipe # component's code -- every component whose harmonized_code differs has # harmonized_code IS NULL (and is aggregate-flagged). If this ever fails, # .suppressed_components() would report reachable dollars as suppressed. skip_if_no_corpus() con <- uscogdata:::.ensure_session() n <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS renamed FROM long WHERE item_code IN (SELECT DISTINCT component_code FROM harmonization_recipes) AND harmonized_code IS NOT NULL AND harmonized_code <> item_code")$renamed expect_equal(as.integer(n), 0L) }) test_that(".select_long_view maps annotated view bases to their long views", { expect_equal( uscogdata:::.select_long_view("spending_annotated", "harmonized"), "spending_long_harmonized") expect_equal( uscogdata:::.select_long_view("revenue_annotated", "harmonized"), "revenue_long_harmonized") expect_equal( uscogdata:::.select_long_view("spending_annotated", "raw"), "spending_long") }) test_that(".suppressed_components measures the E67/E68 dollars Public Welfare drops", { skip_if_no_corpus() con <- uscogdata:::.ensure_session() s <- uscogdata:::.suppressed_components( con, candidates = c("welfare_cash_e67_wide", "welfare_cash_e68_wide"), cohort = uscogdata:::.make_cohort("061037123085"), years = 2011L, long_view = "spending_long_harmonized", flow_prefixes = c("E", "F", "G")) expect_s3_class(s, "tbl_df") expect_equal(nrow(s), 2L) s <- s[order(s$recipe_id), ] expect_equal(s$recipe_id, c("welfare_cash_e67_wide", "welfare_cash_e68_wide")) expect_equal(s$suppressed_amount, c(1803872000, 271589000)) expect_equal(s$suppressed_codes, c("E67", "E68")) }) test_that(".suppressed_components finds nothing in a modern year", { skip_if_no_corpus() con <- uscogdata:::.ensure_session() s <- uscogdata:::.suppressed_components( con, candidates = c("welfare_cash_e67_wide", "welfare_cash_e68_wide"), cohort = uscogdata:::.make_cohort("061037123085"), years = 2019L, long_view = "spending_long_harmonized", flow_prefixes = c("E", "F", "G")) expect_equal(nrow(s), 0L) }) test_that(".suppressed_components rejects a long_view outside the allowlist", { skip_if_no_corpus() con <- uscogdata:::.ensure_session() expect_error( uscogdata:::.suppressed_components( con, candidates = "welfare_cash_e67_wide", cohort = uscogdata:::.make_cohort("061037123085"), years = 2011L, long_view = "long; DROP TABLE x", flow_prefixes = c("E", "F", "G")), class = "uscogdata_internal_error") }) test_that(".suppressed_components never measures a component from the other flow family (I1)", { # uscogdata#9 review, finding I1: without the flow_prefixes filter, a # candidate recipe entirely outside the calling verb's own flow family is # ALWAYS absent from that verb's view (by construction), so it was always # reported as "suppressed" -- fabricating a dollar claim. E67/E68 are # Public Welfare EXPENDITURE codes; scoping the measurement to revenue's # own flow_prefixes must find nothing for them. skip_if_no_corpus() con <- uscogdata:::.ensure_session() s <- uscogdata:::.suppressed_components( con, candidates = c("welfare_cash_e67_wide", "welfare_cash_e68_wide"), cohort = uscogdata:::.make_cohort("061037123085"), years = 2011L, long_view = "revenue_long_harmonized", flow_prefixes = c("T", "A", "U", "B", "C", "D")) expect_equal(nrow(s), 0L) }) test_that("uscogdata#9: Public Welfare signposts its suppressed E67/E68 dollars", { # The bug: E74/E79 return rows for FY2011, so there is no row-absence gap, # so nothing fired -- while E67 ($1,803,872,000) and E68 ($271,589,000) were # dropped for being aggregate-published. LA County reports $3,185,943,000 # and omits $2,075,461,000, a 39% understatement, silently. skip_if_no_corpus() r <- suppressMessages( cog_spending("061037123085", years = 2011L, category = "Public Welfare")) sugg <- attr(r, "provenance")$suggestions expect_length(sugg, 2L) ids <- vapply(sugg, function(s) s$recipe_id, character(1)) expect_setequal(ids, c("welfare_cash_e67_wide", "welfare_cash_e68_wide")) e67 <- sugg[[which(ids == "welfare_cash_e67_wide")]] expect_equal(e67$trigger, "suppressed_component") expect_equal(e67$suppressed_amount, 1803872000) expect_equal(e67$suppressed_years, 2011L) expect_equal(e67$suppressed_codes, "E67") expect_equal(e67$hint, "re-run with recipe = 'welfare_cash_e67_wide'") e68 <- sugg[[which(ids == "welfare_cash_e68_wide")]] expect_equal(e68$trigger, "suppressed_component") expect_equal(e68$suppressed_amount, 271589000) expect_equal(e68$suppressed_codes, "E68") }) test_that("uscogdata#9: an empty_year fire keeps its trigger and gains the dollars", { # Corrections is the case that already worked: zero rows in FY2011, so the # row-absence path fires. It must keep firing, keep trigger = "empty_year", # keep its IG counterpart -- and now also report what was suppressed. skip_if_no_corpus() r <- suppressMessages( cog_spending("061037123085", years = 2011L, category = "Corrections")) sugg <- attr(r, "provenance")$suggestions expect_length(sugg, 3L) ids <- vapply(sugg, function(s) s$recipe_id, character(1)) expect_setequal(ids, c("corrections_combined", "corrections_capital_combined", "corrections_other_capital_combined")) expect_true(all(vapply(sugg, function(s) s$trigger, character(1)) == "empty_year")) cc <- sugg[[which(ids == "corrections_combined")]] expect_equal(cc$suppressed_amount, 1371460000) expect_equal(cc$suppressed_codes, "E05") expect_equal(cc$ig_recipe_id, "corrections_ig_local_combined") }) test_that("uscogdata#9: the revenue verb inherits the same trigger", { # Alaska state FY2011 Miscellaneous Revenue reports $943,842,000 from # U11/U20/U30 while dropping $1,899,995,000 of aggregate-published `U4-` # rents and royalties -- the omission is LARGER than the reported figure. skip_if_no_corpus() r <- suppressMessages( cog_revenue("020000227749", years = 2011L, category = "Miscellaneous Revenue")) sugg <- attr(r, "provenance")$suggestions expect_length(sugg, 1L) expect_equal(sugg[[1]]$recipe_id, "rents_royalties_u4_wide") expect_equal(sugg[[1]]$trigger, "suppressed_component") expect_equal(sugg[[1]]$suppressed_amount, 1899995000) expect_equal(sugg[[1]]$suppressed_codes, "U4-") # A revenue recipe must never be handed an M/L expenditure counterpart. expect_null(sugg[[1]]$ig_recipe_id) }) test_that("I1 + #34: cog_revenue never suggests expenditure-only recipes", { # uscogdata#9 review, finding I1: Corrections is an expenditure-only # category (E04/E05). Before the flow_prefixes fix (#9), .suppressed_components() # measured E04/E05 against cog_revenue()'s OWN view and reported $3.6B as # "suppressed" -- nothing was suppressed at all. # # Issue #34 builds on that: the candidate query now also filters by # category_type ('revenue'), so expenditure-only recipes like corrections_combined # (whose components E04/E05 are classified as 'expenditure' in summary_categories) # are never even considered for a revenue verb. This is stronger than just # suppressing the dollar claim -- it prevents the suggestion from firing at all. skip_if_no_corpus() r <- suppressMessages( cog_revenue("061037123085", years = 2019:2020, category = "Corrections")) sugg <- attr(r, "provenance")$suggestions ids <- vapply(sugg, function(s) s$recipe_id %||% "", character(1)) # corrections_combined should NOT appear -- its components are expenditure-only. expect_false("corrections_combined" %in% ids) }) test_that(".query_candidate_recipes() scopes candidates by category_type (#34)", { # Direct assertion on the mechanism the two tests above exercise # end-to-end: corrections_combined's own components (E04/E05) are # category_type = 'expenditure' in summary_categories, so an # expenditure-flavored flow_prefixes call must surface it and a # revenue-flavored one must not. This queries only summary_categories/ # harmonization_recipes (no government data), so it runs against the # bundled fixture with no skip_if_no_corpus() needed. con <- uscogdata:::.ensure_session() expenditure <- uscogdata:::.query_candidate_recipes( con, category = "Corrections", flow_prefixes = c("E", "F", "G")) expect_true("corrections_combined" %in% expenditure) revenue <- uscogdata:::.query_candidate_recipes( con, category = "Corrections", flow_prefixes = c("T", "A", "U", "B", "C", "D")) expect_false("corrections_combined" %in% revenue) }) test_that("uscogdata#9: no partial-coverage fire in a modern year", { skip_if_no_corpus() r <- cog_spending("061037123085", years = 2019L, category = "Public Welfare") expect_length(attr(r, "provenance")$suggestions, 0L) }) test_that("uscogdata#9: leaf-and-classified wide-era families never fire", { # higher_ed_e18_wide and general_gov_e89_wide are the control group: their # components (E16/E18, E85/E89) are ordinary classified leaves even in the # wide era, so widening the trigger must leave them silent. This is the # measurement that refutes "it would fire on every category in every legacy # year" -- corpus-wide on the fixture, these two produce zero suppressed rows. skip_if_no_corpus() con <- uscogdata:::.ensure_session() n <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM long l JOIN harmonization_recipes r ON l.item_code = r.component_code AND l.year BETWEEN r.year_min AND r.year_max WHERE r.recipe_id IN ('higher_ed_e18_wide', 'general_gov_e89_wide') AND l.amt <> 0 AND NOT EXISTS ( SELECT 1 FROM spending_long_harmonized v WHERE v.canonical_govid = l.canonical_govid AND v.year = l.year AND v.item_code = l.item_code)")$n expect_equal(as.integer(n), 0L) }) test_that("uscogdata#9: the cli message reports the suppressed dollars", { skip_if_no_corpus() expect_message( cog_spending("061037123085", years = 2011L, category = "Public Welfare"), "1,803,872,000", fixed = TRUE) expect_message( cog_spending("061037123085", years = 2011L, category = "Public Welfare"), "FY2011", fixed = TRUE) expect_message( cog_spending("061037123085", years = 2011L, category = "Public Welfare"), "E67", fixed = TRUE) }) test_that("uscogdata#9: cog_explain() reports the suppressed dollars", { # cog_explain()'s whole "print" output -- including the Suggestions # section built from cli::cli_ul() -- is emitted on the message stream # (verified empirically 2026-08-04: capture.output(..., type = "output") # returns character(0) for this call; testthat::capture_messages() is what # actually carries it), so that is the stream this test captures. skip_if_no_corpus() r <- suppressMessages( cog_spending("061037123085", years = 2011L, category = "Public Welfare")) out <- paste(testthat::capture_messages(cog_explain(r)), collapse = "") expect_match(out, "271,589,000", fixed = TRUE) }) test_that("the provenance schema documents the suggestion trigger fields", { sch <- jsonlite::fromJSON( system.file("schemas", "provenance-v1.json", package = "uscogdata"), simplifyVector = FALSE) props <- sch$properties$suggestions$items$properties expect_true(all(c("trigger", "suppressed_amount", "suppressed_years", "suppressed_codes") %in% names(props))) expect_setequal(unlist(props$trigger$enum), c("empty_year", "suppressed_component")) })