Files
uscogdata/tests/testthat/test-categories.R
T
jared da726a61f6
R-CMD-check / check (pull_request) Successful in 3m39s
R-CMD-check / check (push) Successful in 3m39s
fix: cog_categories() surfaces balance subtypes and accepts type = "balance"
The balance work added category_type = "balance" rows to the corpus and
cog_balances() to read them, but left cog_categories() -- the discovery
surface -- unable to describe them:

- subtype COALESCEd only spend_subtype and revenue_subtype, so every balance
  row came back with subtype = NA
- type rejected "balance", so there was no way to ask for the holdings
  taxonomy at all

Both matter downstream: cog-api derives its subtype vocabulary from
cog_categories(), so an NA subtype becomes an unusable API parameter. Found
while implementing cog-api#26.

Note cog_balances() itself still takes no subtype argument -- for holdings
category is a strict coarsening of balance_subtype -- but the value belongs
in the discovery surface regardless.

Tests read the expected subtype set independently from the crosswalk parquet
rather than from the function under test.
2026-08-03 12:02:26 -04:00

133 lines
5.4 KiB
R

test_that("cog_categories returns all categories grouped by subtype", {
skip_if_no_corpus()
r <- cog_categories()
expect_s3_class(r, "tbl_df")
expected <- c("category", "category_type", "subtype",
"n_codes", "item_codes")
expect_true(all(expected %in% names(r)))
expect_gt(nrow(r), 10L)
# corpus preserves Census-native "expenditure" vocabulary; the API takes
# "spending" as a friendlier alias.
#
# `balance` joined as a third category_type with the cash-and-security
# holding codes (pipeline#76). `cog_categories()` is a CATALOGUE verb, not a
# money verb, so it surfaces every category_type the corpus carries -- the
# stock/flow guard belongs on cog_spending()/cog_revenue(), which must never
# return a balance row.
expect_setequal(unique(r$category_type),
c("expenditure", "revenue", "balance"))
})
test_that("cog_categories(type = 'spending') returns only expenditure rows", {
skip_if_no_corpus()
r <- cog_categories(type = "spending")
expect_true(all(r$category_type == "expenditure"))
# "assistance" (the J-prefix aid/benefit codes) joined the vocabulary with
# the crosswalk completion in cog_pipeline#60/#65 -- every flow code
# carrying dollars now maps to a category.
# `interest` (I89, I91-I94) and `insurance_benefits` (Y05/Y06/Y14/Y53)
# joined with the I/Q/Y flow batch -- the last two characters of Census's
# expenditure taxonomy. `interest` is what makes the three-concept model
# computable: primary = direct minus debt service.
expect_true(all(r$subtype %in%
c("operations", "capital", "intergovernmental", "assistance",
"interest", "insurance_benefits")))
})
test_that("cog_categories surfaces the intergovernmental spending subtype", {
skip_if_no_corpus()
r <- cog_categories(type = "spending")
expect_true("intergovernmental" %in% r$subtype)
# IG rows reuse the existing functional categories -- they add a subtype,
# not new category values.
ig_cats <- sort(unique(r$category[r$subtype == "intergovernmental"]))
direct_cats <- sort(unique(r$category[r$subtype != "intergovernmental"]))
expect_true(all(ig_cats %in% c(direct_cats, "Other Education")))
})
test_that("cog_categories(type = 'revenue') returns only revenue rows", {
skip_if_no_corpus()
r <- cog_categories(type = "revenue")
expect_true(all(r$category_type == "revenue"))
# The four non-general subtypes are deliberately NOT own_source: Census's
# General Revenue excludes insurance trust (Y01 alone is $1.31T corpus-wide,
# plus the employee-retirement X codes), utility (A91-A94) and liquor store
# (A90) revenue by definition, which is what makes both of its published
# revenue concepts computable -- see `revenue_concept` in `?cog_revenue`.
expect_true(all(r$subtype %in%
c("own_source", "federal", "state", "local_aid",
"insurance_trust", "utility", "liquor_store")))
})
test_that("cog_categories(pattern = ...) filters case-insensitively", {
skip_if_no_corpus()
r <- cog_categories(pattern = "police")
expect_gt(nrow(r), 0L)
expect_true(all(grepl("Police", r$category, ignore.case = TRUE)))
})
test_that("cog_categories has one row per (category, subtype)", {
skip_if_no_corpus()
r <- cog_categories()
key <- paste(r$category, r$subtype, sep = "|")
expect_equal(length(key), length(unique(key)))
})
test_that("cog_categories item_codes is non-empty comma-separated string", {
skip_if_no_corpus()
r <- cog_categories()
expect_true(all(nzchar(r$item_codes)))
expect_true(all(r$n_codes >= 1L))
# n_codes should equal count of commas + 1
expect_equal(r$n_codes,
vapply(strsplit(r$item_codes, ","), length, integer(1)))
})
test_that("cog_categories sorted by category_type, category, subtype", {
skip_if_no_corpus()
r <- cog_categories()
sorted <- r[order(r$category_type, r$category, r$subtype), ]
expect_identical(r, sorted)
})
test_that("cog_categories rejects invalid type", {
expect_error(cog_categories(type = "both"), "type")
})
test_that("cog_categories() surfaces balance subtypes", {
skip_if_no_corpus()
with_fixture_corpus({
cc <- cog_categories()
b <- cc[cc$category_type == "balance", ]
expect_true(nrow(b) > 0L)
# Every balance row must carry its subtype. Before the COALESCE included
# balance_subtype these were all NA, which silently made the balance
# taxonomy undiscoverable -- cog-api derives its subtype vocabulary from
# this function, so an NA here becomes an unusable API parameter.
expect_false(any(is.na(b$subtype)))
# The exact set, read independently from the crosswalk rather than from
# the function under test.
con2 <- DBI::dbConnect(duckdb::duckdb())
on.exit(DBI::dbDisconnect(con2, shutdown = TRUE), add = TRUE)
p <- file.path(fixture_corpus_path(), "data", "summary_categories.parquet")
want <- DBI::dbGetQuery(con2, sprintf(
"SELECT DISTINCT balance_subtype FROM read_parquet(%s)
WHERE category_type = 'balance' AND balance_subtype IS NOT NULL
ORDER BY 1", uscogdata:::.sql_lit_chr(p)))$balance_subtype
expect_true(length(want) > 1L)
expect_identical(sort(unique(b$subtype)), sort(want))
})
})
test_that('cog_categories(type = "balance") filters to holdings', {
skip_if_no_corpus()
with_fixture_corpus({
b <- cog_categories(type = "balance")
expect_true(nrow(b) > 0L)
expect_identical(unique(b$category_type), "balance")
expect_false(any(is.na(b$subtype)))
})
})