Files
improvise/context/test-evidence/tooling.md
T
Edward Langley 3542d6f717 fix(tooling): a leaf is not an identity, and a convenience flag was a bypass
Both blockers from the adversarial review of #83, both in the mode this PR
rewrote, both producing a pass while the gate did not do what its prose said.

1. cargo_test_names reduced every listed path to its last segment. Six leaf
names collide in this workspace and two are cited in evidence, so a heading was
satisfiable by a test in another module answering another question -- renaming
ui::project::tests::a_load_that_fails_is_not_a_rebase, the one its entry
describes, left the checker at exit 0 with the ui::host namesake answering for
it. Paths are kept whole; a bare leaf resolves only when unique, and an
ambiguous one is REFUSED with both candidates named. _RUST_NAME accepts `::`, so
a qualified heading is a claim rather than prose, and a citation is matched on
the leaf so qualifying a heading cannot orphan its own test. The two colliding
entries are qualified. Neither half suffices alone: rename one of a colliding
pair and the leaf becomes unique again, which is why qualification is the fix
and the refusal is what forces it.

2. --rust-tests called cargo_test_names with one argument, so the --ignored
subtraction never ran, and it printed a byte-identical success line. It now
requires --rust-tests-ignored (one alone exits 2) and the line names its source.

Also, from the same review: a missing context/test-evidence/ exited 0 while 170
citations pointed at nothing (early return, now gone); the doc-site "runs last"
check accepted a trailing `echo` that merely NAMED the generator, so it asks for
an invocation; bundle_freedom gains an arm for a transitive `import "../app.js"`,
which reaches the bundle while naming nothing forbidden; and two prose overclaims
are corrected -- the prose-heading count is a convenience rather than a guard
(git diff is the mechanism), and the ratio rule says "above the run MEDIAN",
which is defensible where "above the others" is false against second place.

scripts/check-ci-graph.py makes claim 6's rule a gate: pages-deploy must `needs`
every other job in ci.yml. The rule was stated in the last commit and a
zz-new-check: job added during review was still ungated with nothing noticing.
README now states the rule and lists no jobs, the second copy being what went
stale twice.

Every fault re-injected and observed; figures in context/test-evidence/tooling.md
rather than only here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:42:07 -07:00

8.3 KiB

Test evidence — the repository's own gates

Evidence for the checks in scripts/ and for the tests that hold CI's shape. The arguments behind the checkers themselves are in context/code/tooling.md; this file records what fault each test was watched to catch.

Note what is deliberately absent. scripts/check-comment-runs.py --self-test, scripts/kb-test.sh and scripts/assemble-stamp-test.sh are check-the-checker suites whose cases are already injected faults, and none of their case names is a Rust fn or a JS test( the evidence checker could resolve — so their evidence is the case list itself plus context/code/tooling.md, and inventing headings for them here would be headings naming nothing.

"no test in web/tests/ reaches for a built bundle"

The js CI job runs node --test web/tests/*.test.js and installs nothing; the browser job runs web/tests/bundle/*.test.js after three wasm-pack builds. The moment a flat web/tests/ file reads web/pkg/, the fast job inherits the slow one's dependency — and the likely repair is to add a build step to it, silently spending the ~4m30s the split exists to avoid. Until improvise-4ktt that constraint was a directory convention and a glob, checked by nothing, and it failed in the direction that looks like ordinary breakage: a confusing ENOENT in CI on a machine where the bundle happens not to be built.

Fault injected, 2026-09-21: appended const injected = readFileSync("../pkg/improvise_wasm_client.js"); to web/tests/grid_plan.test.js. The test failed naming that file and pointing at web/tests/bundle/. Reverted; green again.

Two things the test does that a simpler one would not. It asserts its own filename is among the files it scans, so it cannot become the one place a violation may hide. And it strips comments before matching, because this file's own header and the test's header both name the directory in prose — a lexical grep over raw text would have had to exempt them, and an exemption is where the next violation goes.

What it cannot see, stated because the check itself uses the evasion: a path assembled at runtime ("../" + "p" + "kg") is invisible to a lexical match, and the test's own FORBIDDEN constant is built exactly that way to stay out of its own scan. That is a deliberate, disclosed hole — closing it needs a parser, and the failure it would catch is one somebody has to write on purpose, where this gate exists for the one written by accident.

"the subdirectory that MAY use a bundle is not the one being checked"

The companion, and the one that stops the first from passing vacuously. If web/tests/bundle/ were emptied or its files stopped touching the bundle, the first test would still be green while having nothing to be scoped against — "no flat test uses the bundle" is uninteresting if no test anywhere does. This asserts the other half of the split exists and really does what the flat half is forbidden to do.

Fault injected, 2026-09-21: moved web/tests/bundle/wasm_surface.test.js out of the tree. The test failed with "web/tests/bundle/ is empty — the split lost its other half" rather than passing. Restored; green again.

"no test in web/tests/ imports a module that reaches the bundle"

The companion to the lexical check above, and the reason that one is not enough: import "../app.js" needs the bundle transitively and names nothing forbidden, so the js job breaks loudly but unattributed. app.js and worker.js are the two entry points that load the bundle; shell.js and serial.js do not, and serial.test.js imports one legitimately.

Fault injected, 2026-09-21: appended import "../app.js"; to web/tests/grid_plan.test.js. The lexical test stayed green; this one failed naming the file. Reverted.

Note — the checker's own blockers, found by review of PR #83

Two gates in scripts/check-comment-runs.py produced a pass while not doing what their prose said. Both were in the mode the same PR rewrote, which is the part worth keeping: writing the doctrine down does not exempt the writer.

A leaf name is not an identity. Six leaf names collide in this workspace and two were cited in evidence. Renaming ui::project::tests::a_load_that_fails_is_not_a_rebase — the one its entry describes — left the checker printing all name a test that runs, exit 0, with the heading answered by the ui::host::tests namesake. Now an ambiguous leaf is refused with both candidates named, and the two entries are qualified. Re-verified both ways, 2026-09-21: the refusal fires on the unqualified headings and names exactly those two; with the heading qualified, the same rename is reported as an orphan rather than absorbed.

--rust-tests was strictly more permissive and said the same words. It called cargo_test_names with one argument, so the --ignored subtraction never ran, and the success line was byte-identical to the honest path's. Injecting ### eyeball_the_rendered_hints_and_pages orphaned on the default path and passed on the flag's. Now the flag requires --rust-tests-ignored (one alone exits 2) and the success line names its source.

A missing evidence directory exited 0. The early return preceded the citation scan, so deleting context/test-evidence/ reported nothing while 170 citations pointed at a directory that was not there. Verified 2026-09-21 by moving the directory aside: exit 1, 170 tests cite context/test-evidence and have no entry there.

Note — the stamp gate's discrimination, measured

scripts/assemble-stamp-test.sh separates three outcomes rather than two, which is what makes it evidence rather than a smoke test. Measured 2026-09-21 over its nine cases: the real script passes 9; deleting the final test -z "$missing" (the PR #69 injection, which the inline version survived at exit 0) passes 4 and fails 5; a script that does nothing passes 2; one that always fails passes 0.

The PR body first cited "4 of 9" for the middle row, from a run taken when the suite had eight cases. A figure is what the next reader checks, so it is recorded here where it can be re-measured rather than only in a message nothing can audit (design-principles §6).

Note — the stamp gate matches literally

scripts/assemble-stamp.sh greps with -F. As a regex, the . in improvise_wasm_client.js?v=abc matches any character, so a stamp corrupted in exactly that position reads as present — a pass in the one direction a guard must not fail in. Inherited from the inline version this was extracted from, and found by reading the extraction rather than by anything failing.

Fault injected, 2026-09-21: dropping the -F makes improvise_wasm_clientXjs?v=abc satisfy the pattern, and a pattern is matched literally, not as a regex goes from pass to fail.

Note — the doc site's derived checks

scripts/build-doc-site.mjs carries two gates that no test names, because the generator is the test: it runs on every assembly and exits non-zero.

Faults injected, 2026-09-21, each observed:

injected reported
a new context/code/newarea.md context/code/newarea.md is neither published nor a named omission
context/plan.md moved out of the tree context/plan.md is listed as omitted but does not exist
a line appended below the generator in web/assemble.sh web/assemble.sh must invoke this generator LAST, naming the offending command
a trailing echo "see scripts/build-doc-site.mjs" the same, once the check asks for an INVOCATION rather than a mention — .includes had accepted it (review, PR #83)

scripts/check-ci-graph.py is the third, and it is a script rather than a stanza because its own refusals need checking: adding a zz-new-check: job to ci.yml reports pages-deploy does not need zz-new-check and exits 1, where before the rule was prose and nothing noticed. Its five self-test graphs include the one a naive parse gets wrong — another job's needs: read as the deploy's.

The second arm is the one that would not get written first, and it is the point: without it, deleting or renaming an omitted file leaves an entry asserting a reason for a file nobody can find — the same failure as a dead ticket id, which AGENTS.md already calls worse than no id.

The first arm was filed as latent and was live when it was written: context/code/kb-tool.md had been unpublished, unargued and unnoticed since it was added.