Files
uscogdata/tests/testthat/test-recipes.R
jaredandClaude Opus 5 77074621d8 revert: drop the I3(b) suppression pre-check gate (#9)
Scoped re-review measured .needs_suppression_query() against the fixture
and found it doesn't pay for itself: it skips the round trip on ~3% of
healthy candidate-bearing calls, ~0% of the multi-govid batch shape
(cog_geographic_rollup()/cog_peer_compare()) it was meant to help, and
reaching the gate costs an unconditional metadata query that on its own
roughly cancels the expected saving -- net slower on the fixture. The
gate was also correct (0 unsound skips) but left an untested exactness
invariant (result$codes_included and the anti-join sharing the harmonized
item_code space) whose silent violation would kill signposting, which is
the exact failure class uscogdata#9 exists to prevent.

Owner's call: revert it and keep the code simple. A batch-aware
optimization, if warranted, is a separate issue.

Removes .needs_suppression_query() entirely (function, roxygen, call
site, comp_rows/flow_components), restoring .build_suggestions() to call
.suppressed_components() directly -- unchanged from f77adb6 except that
it still threads flow_prefixes through (I1, kept). Also removes the two
tests that existed solely to exercise the gate (the five-branch synthetic
test and the local_mocked_bindings call-counter test); no test asserting
real signposting behavior was touched.

I1 (flow_prefixes filter), I2 (schema wording + ig_recipe_id required),
I3(a) (restated NOT EXISTS literals for partition pruning), and M4
(reworded scope claims) are all untouched by this revert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 07:59:19 -04:00

469 lines
20 KiB
R

# 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"),
govid = "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"),
govid = "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", govid = "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"),
govid = "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: cog_revenue never fabricates suppressed dollars for an expenditure-only recipe", {
# uscogdata#9 review, finding I1: Corrections is an expenditure-only
# category (E04/E05). cog_revenue() naturally returns zero rows for it, so
# corrections_combined still fires as an empty_year suggestion (its own
# generic join finds real E04/E05 data for this government) -- but before
# the flow_prefixes fix, .suppressed_components() measured E04/E05 against
# cog_revenue()'s OWN view (which can never contain an E-coded row by
# construction) and reported the full $3,631,945,000 as "suppressed",
# when cog_spending() for the same gov/years/category actually returns
# $3,691,029,000 -- nothing was suppressed 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))
expect_true("corrections_combined" %in% ids)
hit <- sugg[[which(ids == "corrections_combined")]]
expect_equal(hit$suppressed_amount, 0)
expect_equal(hit$suppressed_years, integer(0))
expect_equal(hit$suppressed_codes, character(0))
# And cog_spending() for the identical gov/years/category is unaffected --
# it actually finds the E04/E05 dollars the buggy measurement claimed were
# excluded.
sp <- suppressMessages(
cog_spending("061037123085", years = 2019:2020, category = "Corrections"))
expect_equal(sum(sp$amt_nominal), 3691029000)
})
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"))
})