| name | tdd-permanent-fn |
| description | Scaffold a Tier-1 testthat test file for a permanent (non-legacy) devPTIpack function per arch-03/arch-02.01 — uses bundled fixtures (ukr_shp, ukr_mtdt_full) and the helper-test-data.R harness, pins known behavioural quirks instead of skipping them. |
Skill: tdd-permanent-fn
Scaffold a Tier-1 testthat test file for permanent devPTIpack functions
(GitHub issue #10).
When to invoke
The user (or a parent task) names one or more functions to test. The function
must appear in the "Permanent Functions" registry in
.github/docs/arch-01-cleanup.md.
If the function is in a cleanup batch, stop and warn — we do not write
tests for code scheduled for deletion.
Inputs (collect before scaffolding)
- Target function(s) and their source file(s) under
R/.
- Whether the function belongs to:
Procedure
- Confirm permanence. Search arch-01 § "Permanent Functions" for the
function name. If absent, halt with a warning.
- Locate the existing test file under
tests/testthat/ matching the
per-function map in arch-03 § "Per-Function Test Map". If a test file
exists, read it first and append — never overwrite.
- Author cases. Use the case tables in arch-02.01 (calc pipeline) or
arch-03 (everything else) verbatim. For each row:
- One
test_that() block per row.
- Inputs sourced from
helper-test-data.R or tests/testthat/fixtures/.
- Assertions: prefer
expect_equal, expect_s3_class, expect_named,
expect_error, expect_warning over generic expect_true.
- Pin known quirks. Encode these as explicit assertions, never
skip():
- Lexicographic admin sort in
get_adm_levels (fails for admin10+).
get_scores_data leaves 1-row groups as NA, not 0.
pivot_pti_dta over-matches names containing "administrative".
expand_adm_levels extracts only the first digit for level comparison.
See arch-03 §"Known Issues to Pin".
Codex handoff (default when codex:codex-rescue is available)
Step 3 ("Author cases") is the right place to delegate. By the time you
reach it you have: the target function name + path, the test file path
(existing or new), the case table from arch-02.01 or arch-03, and the
fixture names from helper-test-data.R. Brief Codex with all four,
plus the "Skeleton (copy-and-adapt)" block from this skill. Codex
writes the test_that() blocks; Claude reviews the diff against the
case table before running step 5. See .claude/CLAUDE.md §"Codex
delegation policy".
- Run.
Rscript -e 'devtools::test(filter = "<basename>")' from repo
root. All tests must pass against the current (uncleaned) codebase
before the test file lands.
- Update changelog. Append a row to
.github/docs/changelog.md under
today's date — Scope Tests. (The Stop hook will draft this; refine.)
- Sync
PLAN.md. Tick the relevant box(es) under §4.1 (e.g. 1d,
1e), add new sub-bullets if the PR introduced work not previously
enumerated, and update the "Progress log" with the PR number. See
.claude/CLAUDE.md § "PLAN.md Sync (COMPULSORY)".
Hard rules
- Never test legacy functions slated for deletion (arch-01 batches).
- Never use external network access in tests.
- Examples and fixtures use only
ukr_shp, ukr_mtdt_full, or generated
fixtures committed under tests/testthat/fixtures/.
- Tests must pass cleanly — no
skip_on_*, no commented assertions.
- Test runtimes must keep
devtools::test() under 2 minutes total.
Skeleton (copy-and-adapt)
test_that("<function>: <case-from-arch-table>", {
input <- test_pivoted
out <- pivot_pti_dta(ukr_mtdt_full, test_indicators)
expect_s3_class(out, "tbl_df")
expect_named(out, c("admin_level", "var_code", "value"), ignore.order = TRUE)
})
Tier-2 patterns (shiny::testServer)
When the target is a Shiny module server (e.g. mod_*_server),
follow the patterns established in
tests/testthat/test-mod-calc-pti2.R — the canonical Tier-2
template — instead of starting from scratch.
Key gotchas (don't re-discover):
-
Attach shiny at the top of the test file:
suppressPackageStartupMessages(library(shiny))
Module servers use unnamespaced reactive, observeEvent, etc.
pkgload::load_all() loads but does not attach Imports.
-
Drive observers with session$flushReact() before inspecting
the returned reactive. Many modules wrap their inputs in an
observeEvent that copies into an internal reactiveVal; without
a flush, the eventReactive body runs once with the initial NULL
and yields an empty result.
testServer(mod_X_server, args = list(...), expr = {
session$flushReact()
out <- session$getReturned()()
})
-
Use a .build_*() helper to centralise the input-list shape so
per-test variations only override the fields they care about.
See .build_wt_dta() in test-mod-calc-pti2.R.
-
Reactive inputs change via reactiveVal() in the calling
scope, not by re-passing args. Capture the rv outside the
args = list(...) block and update it from inside expr = {...},
then session$flushReact() again.
-
Skip cases that depend on bugged behaviour elsewhere in the
pipeline — pick fixture data that avoids the bug rather than
pinning a Tier-2 failure for a Tier-1 issue. Reference the bug in
PLAN.md §12 from a comment.
Per-module test cases live in arch-03 §2.1–§2.7. PR one module per
sub-branch (tests/mod-<name>) to keep review velocity high — see
PLAN.md §9 for the open-question rationale.
After authoring
- If tests fail against current code, do not weaken the assertion to
match — investigate. Either the case in arch-03 is wrong (escalate via PR
comment / changelog note) or the function actually misbehaves (pin the
current behaviour and open a follow-up issue tagged
bug).
- Cross-check the per-function map in arch-03 § "Per-Function Test Map" and
tick off the corresponding row in the issue #10 acceptance checklist.