test(785): mutation proofs for the three unproven ranked guards #810
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
55a7df226e |
test(785): pin the workflow's TRIGGERS, and sweep the retracted figure by subject
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 21s
PR Gates / Docs update reminder (pull_request) Successful in 19s
PR Gates / decisions lifecycle (pull_request) Successful in 30s
review-verdict/h10 Review-verdict: MERGEABLE @ 55a7df2 (base: main)
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 18s
Review verdict / Set review-verdict status (pull_request_target) Successful in 19s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m48s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m51s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Fifth cold review returned MERGEABLE, having reproduced all five of the previous commit's
measurements exactly and verified by execution that all twelve parametrised cases decide for
their labelled reason rather than incidentally. Three residual items, none of which was
coverage claimed and not delivered.
The `on:` trigger was an uncovered channel. `_active_runs` started at `jobs:`, so a workflow
whose triggers no longer include `pull_request` left a perfectly healthy `decisions-guard`
job gating nothing while the proof stayed green. Nothing claimed to cover it, which is why
it was LOW rather than a false coverage claim — but it is two lines and it is now the ninth
disablement case.
`on` parses to the BOOLEAN True in YAML 1.1, not the string `"on"`, so the key is looked up
both ways. Verified which branch is live rather than assuming: pyyaml produces `True` here,
so a lookup of `"on"` alone would have yielded an empty trigger set and fired the assertion
on a correct workflow — a false red that the anti-vacuity check (the unmodified workflow
must still hold) is what catches.
`script.replace("python3", sys.executable)` was unbounded, having been `count=1` on a single
extracted line before the withdrawal. It rewrote every occurrence including inside a path, so
a step spelled `/usr/bin/python3` would have become nonsense and failed with a red blaming
the catalog guard. Now substituted only where `python3` is a bare command word.
And the retracted "1,488 records" figure survived in a second place — the leak guard's
docstring — because the previous sweep went by file rather than by subject. Replaced with the
reproduction and the figure that survives it, the same wording the inventory carries.
`stale-comments-sweep-by-subject-not-verb`.
Two channels remain uncovered and are deliberately not guessed at: a job `if:` whose
expression is false only at run time, and `continue-on-error` as a non-literal expression.
`_falsey`'s docstring already declines these as undecidable outside the runner, and no row,
test name or docstring claims them.
Refs: #785
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
52ed319c8a |
fix(785): withdraw the line-level workflow heuristic; land the doc fix I said I had landed
Fourth cold review, scoped to the previous commit. Three findings, one of which is the
worst kind.
HIGH — a doc fix asserted as landed was not in the diff. `e4a14e282`'s message says the
`test_hook_fire_log.py` "TWO clauses" correction was "verified against the files"; the file
still said TWO. Cause: both doc edits were applied in one script that wrote once at the
end, the second edit's `assert` fired, and `write_text` never ran — so the first edit was
discarded silently while I reported it as done. Every edit here is applied, written and
re-read individually.
The correction itself was right and is now landed: that file proves THREE clauses —
coverage, transparency, placement — and the row's Blocks column already claims two of them;
the single Proof ref is a column limit, not a coverage statement. All four cited test names
verified to exist. Also fixed a second instance of the same stale subject: the ranked entry
still said "Three clauses were disarmed" for the worktree pair, and listed three, after the
fourth was added two commits ago.
MEDIUM — the line-level extractor was round three, and review found round four's defect on
schedule:
run: |
cat <<'EOF' > /dev/null
PYTHONPATH=. python3 scripts/build_decisions_catalog.py --check
EOF
The matched line is heredoc DATA. The extractor reported the guard as running and the proof
executed a command CI does not — the exact class the previous commit claimed to close, one
level deeper. Deciding which lines of a shell script execute requires parsing shell, and
this repo has lost that argument repeatedly.
So the line-level heuristic is WITHDRAWN rather than patched a fourth time. The step's WHOLE
`run` script is handed to bash, as the runner does. The heredoc then runs, writes to
/dev/null, checks nothing and exits 0 — so the stale-catalog case fails to redden and the
proof reports the defect instead of stepping around it. Ambiguity resolves by execution.
That also removed a false RED the reviewer found and two I had not: a backslash
continuation, a leading `set -euo pipefail`, and the command wrapped in an `if` block are
all accepted now, and are pinned as parametrised negative controls. The disablement cases
are eight, and assert on the whole guarantee (fresh passes AND stale fails) rather than on
whether extraction raises, because the failure surfaces in different places.
MEDIUM — `if: ${{ false }}` read as wired. `_falsey` compared literals only. The unwrap is
a regex over the whole value, not `.strip("${{ }}")`, which strips a character SET and would
turn `"false}"` into `"false"` while reading as though it removed a wrapper.
MEDIUM — the leak guard's docstring claimed it "cannot pass while anything in this file logs
to the production dir, whatever the mechanism". False, and review demonstrated it: add a
second launcher beside `_run()` that passes a stale environment and point the mutation tests
at it — 14 passed, 18 records leaked. I reproduced that before correcting. It guards the
launch path it drives; every hook in the file goes through one helper today, which is what
makes it sufficient there and not a general property. Both the docstring and the inventory
now say so, and #809 carries the general form.
Two figures in the previous message were also wrong. "13 tests pass and 58 records leak"
conflates two runs — the pre-fix 13-test file leaks 54, and 58 is the 14-test file at head.
And "four sibling suites already parse this workflow" is two: the other two parse
docker-build.yml.
Refs: #785
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b2b651779a |
fix(785): close the re-review's BLOCKER — the leak guard pinned the FIX, not the property
Third cold review, this one scoped to the previous two fix commits. Its central finding is the one worth reading, because the guard it faults is the guard I added to stop exactly this class of thing. BLOCKER — `test_the_subprocess_env_CARRIES_the_isolated_hook_fire_log_dir` asserted on `_env()`'s RETURN VALUE. `_env()` can be perfectly correct while a call site passes something else, and that is the reintroduction a future tidy-up actually produces. Demonstrated: leave `_env()` exactly as shipped, restore a module-level snapshot and point one `env=` at it — 13 tests pass and 58 records leak. I reproduced it before acting. The guard was pinned to the SHAPE OF THE FIX rather than to the property, which is `verify-against-the-REAL-predecessor`: it was witnessed red against a hand-written revert, so "pins it" and #809's "witnessed red by reintroducing the snapshot" were both overclaims. `test_driving_a_hook_LANDS_its_records_in_the_ISOLATED_dir` asserts the EFFECT instead — drive a real hook through the real `_run()`, records must land in the fixture's dir — with an anti-vacuity check that the hook reached a decision first. Measured: green at head, RED under the call-site reintroduction that the old guard passed. MEDIUM x3 — the workflow extractor was a text scanner and review broke it three ways in one pass. A `run: |` block scalar was invisible, so reformatting the step to the form its own sibling in that job already uses produced a FALSE RED whose message asserts a CI regression that has not happened. A job or step switched off with `if: false` or `continue-on-error: true` still read as wired — 7 green with the guard not gating at all. And `run:` inside block-scalar TEXT was extracted and executed, so an `echo we no longer run: PYTHONPATH=. python3 ... --check` left the subprocess proof green while the real step was deleted. Two of the three are false greens. `fixing-a-parser-bug-introduces-the-next-one` says the second round of a string predicate is not the last. The structure is available — pyyaml is installed by the `script-tests` job and four sibling suites already parse this workflow with it — so the scanner is replaced by `yaml.safe_load`, and "would actually execute" is now three explicit conditions: the job exists, the job is not disabled, the step is neither disabled nor allowed to fail. The six ways it can stop gating are a parametrised test, and the block-scalar REFORMAT is a negative control, because a detector that cannot tell a reformat from a removal trains its readers to ignore it. LOW — `_hook_calls` skipped whole-line comments but not inline ones, and broadening the marker in the previous commit is what made that reachable: a trailing `# ... .claude/hooks/removed-helper.sh` reddened a correct `pre-push`. The fix for one over-match introduced another. Verified both directions. NIT — a section header still said "three clauses" after the fourth mutation was added. Docs. Two accuracy fixes, both verified against the files rather than reasoned about: `test_hook_fire_log.py` proves THREE clauses (coverage, transparency, placement), not two, and its row's Blocks column already claims two of them — the single `Proof ref` is a column limit, not a coverage statement. And the "1,488 stray records" figure was unreproducible in a durable doc, the same commit having deleted the evidence. Replaced with the reproduction and a figure that survives it: 58 records per run, measured identically on macOS/git 2.55 and Linux/git 2.47.3. Also corrects a count in 88832047b's own message: it says the inversion reddens "8 of 12"; that file had 13 tests at that commit. Third mislabelled figure in this branch, all three in messages about mislabelled figures. Refs: #785 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a01def3f10 |
fix(785): the new leak guard's third assertion was a tautology
`test_the_subprocess_env_CARRIES_the_isolated_hook_fire_log_dir` shipped with
assert "hook-fire" not in str(Path.home() / ".cache") or isolated != str(...)
Evaluated: `str(Path.home() / ".cache")` is `/Users/timothy/.cache`, which never contains
"hook-fire", so the left operand is always True and the `or` never reaches the comparison.
The assertion could not fail. It was decoration inside a guard added specifically to stop
vacuous checks — the recurring shape this milestone exists to close, committed by its own
fix.
The first two assertions in that test were doing the real work throughout, so nothing that
was proved is now unproved. The third now compares the resolved paths, with the production
path DERIVED the way `hook-fire-log.sh` derives it (`${HOME:-/tmp}/.cache/ersatztv/
hook-fire`) rather than restated as a literal that would drift from it.
Verified reachable: with `isolated` set to the production path the comparison evaluates
False, i.e. the assertion fires. Previously that same input passed.
Found by re-reading the fix commit rather than by the reviews — which is the argument for
re-reviewing the FIX and not only the original diff.
Refs: #785
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4822003004 |
fix(785): stop the tests writing to the PRODUCTION hook-fire log, and correct a mislabelled count
Two independent cold reviews, both against the real subjects rather than the diff. Five
findings; the first is the serious one.
HIGH — `test_worktree_ownership_guard.py` was defeating conftest's isolation. `_ENV` was a
module-level `{**os.environ, ...}` snapshot, taken at IMPORT time, which is before the
autouse `isolate_hook_fire_log` fixture monkeypatches `ETV_HOOK_FIRE_LOG_DIR`. So every
hook subprocess logged to the real `$HOME/.cache/ersatztv/hook-fire/`. Measured: 1,488
records for the two synthetic session ids, and one further run added 28 more.
That is #776's defect reintroduced inside the file meant to prove #776's hooks, and it
corrupts the surface `docs/guard-inventory.md` cites as the observability claim for every
guard still graded NONE. Nothing surfaced it because the fire-log library is fail-open by
design — a logging failure must leave the hook behaving exactly as an uninstrumented one.
The env is now built per call. Measured after: a full run of the file adds 0 records. The
stray records were deleted; `s1`/`unknown-session` were left alone, not being ours.
`test_the_subprocess_env_CARRIES_the_isolated_hook_fire_log_dir` pins it, witnessed red by
reintroducing the snapshot. It is per-file, which is the same narrowness as the guard that
should have caught this: `test_hook_fire_log.py::test_the_suite_does_not_write_to_the_
PRODUCTION_log` says conftest "must isolate every test, not just this file's" but checks
only the running test and its own sandbox. Recorded in the inventory and tracked in #809,
including why the obvious general check (diff the log dir per test) is racy against a real
session's hooks firing concurrently.
MEDIUM — a9433d5cb's commit message mislabels a measurement. It reports the ownership
comparison mutation as "2 failed, 8 passed (inverted)". Reproduced both: INVERTING the
clause reddens 8 of 12 at today's head; 2 failed / 8 passed is what DELETING it produces
(2 of 10 at that commit's head). The number is real but belongs to the mutation the
parenthetical says was rejected. The shipped test is unaffected — inversion genuinely
reddens `test_MUTATION_inverting_the_OWNERSHIP_COMPARISON_blocks_the_owner`. Corrected
here rather than by amending, since the history is the record.
LOW — `docs/guard-inventory.md` described the unproven set as "every PreToolUse hook
except ..." two paragraphs above a standing list of eight hooks spanning four events. Both
sentences named the same set. `design-sync-reminder.sh` is registered on BOTH PreToolUse
and Stop in `.claude/settings.json`, so any "every X hook" phrasing double-counts it; the
set is now named by enumeration.
LOW — a truncated sentence ("`test_hook_fire_log.py` asserts TWO clauses", no verb, no
closing bold) orphaned the following paragraph's "That unproven set". Completed with both
clauses and their proofs.
NIT — `_hook_calls` matched comment lines and only the `./` invocation form. A commented
ghost hook would have reddened a correct tree; a `bash .claude/hooks/x.sh` form would have
gone unstubbed and exited 127, which two tests read as "drift caught". Both verified.
Refs: #785
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
fe01982a97 |
test(785): close two findings from the independent cold review
Both are the same species the milestone is about — a coverage claim resting on source-text presence rather than on the guard actually running or the clause actually being reached. MEDIUM, `test_build_catalog_check_path.py`. `_ci_check_command()` grepped the whole workflow for the command text. So it matched a step that had been COMMENTED OUT, and it was not scoped to the `decisions-guard` job at all — a match anywhere in the file would do. Commenting out a step is the ordinary way a check gets switched off during an incident and never switched back, and the subprocess proof would have stayed green over a guard CI no longer runs. `guard-ships-with-mutation-proof`: the guard being WIRED is not the guard RUNNING. Now the extractor finds the job by key, stops at the next top-level job, strips comment lines before matching, and asserts exactly one active step. `test_the_extractor_rejects_a_ COMMENTED_OUT_step` pins it. Witnessed against the real workflow: commenting the step out reddens 2 of 7, deleting it reddens the same 2. LOW, `test_worktree_ownership_guard.py`. The docstring claimed the three mutations covered every clause the pair hangs on. They did not: the detection clause is `(commit|merge)`, and every payload in the file used `commit`, so `merge` could be dropped and the file stayed green. That is the more damaging half to lose — the mechanism exists for the plumbing-merge path (`process.foreign-worktree-plumbing-merge`). Adds a merge deny case and a fourth mutation, narrow by construction: the mutant must stop denying a merge WHILE STILL denying a commit, so a mutation that merely broke the regex cannot pass for it. Witnessed: dropping `merge` from the real hook reddens exactly those 2. The docstring now also states which clauses are NOT mutated (the `git -C`/`cd` redirection extraction, the marker hook's argument parsing) rather than implying the file is exhaustive. The review's third finding — the inventory calling both husky files thin dispatchers — was raised against ad0bed458..7bd7b4c8 and had already been corrected in fdc91056f. Refs: #785 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
77317e5c71 |
test(785): pin the pre-push premise to both platforms, measured not assumed
The docstring said "git 2.55, macOS; the same holds on Linux". The second half was an assertion, and this repo's canonical disaster (#775, the BOM guard) is precisely a guard that behaved differently on the platform nobody measured — macOS had `xxd`, the Linux runner did not, and it was fail-open there for months. So it was measured on the runner host (Linux, git 2.47.3) rather than reasoned about: * git exports GIT_DIR=<repo>/.git/worktrees/<wt> to pre-push on a push from a worktree * `git rev-parse --show-toplevel` from web/ with that GIT_DIR answers `<wt>/web` * the nested `git diff --exit-code` reports exit 0 on a drifted file and all three new suites were run there: 25 passed. Disarming the `unset` on that host reddens 4 of the 9, the same four as on macOS. Refs: #785 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9ebf8023f9 |
docs(785): correct three false claims in the remaining-NONE groups
Self-review of the prose added in the previous commit found three statements that are
not true of the files they describe:
* `.husky/commit-msg` was called a thin dispatcher. It dispatches to nothing — it is one
`grep -q '^Co-Authored-By:'` plus a `MERGE_HEAD` exemption, and nothing tests that the
exemption fires only for merges.
* `.husky/pre-commit` was called one too. It dispatches to `lint-staged` and
`decisions-guard.sh`, but the root-level-`*.png` refusal and the
`dotnet format whitespace --verify-no-changes` block are its own clauses.
* eight hooks were grouped as "the `PreToolUse` advisory hooks". Only four are
`PreToolUse`; two are pre-push, one pre-commit, one `Stop`.
The first two mattered most: "it dispatches to guards" was being used to argue those rows
need no proof of their own, which is the relabelling error this file's withdrawn-guard
table already exists to prevent — committed by the paragraph explaining the grades. Both
are now stated as backlogs.
Membership was verified by deriving the GUARD+NONE set from the table: 16 rows, 8 + 2 + 6,
matching the three groups exactly.
Refs: #785
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3651b06c3c |
docs(785): record what each mutation established, and why every remaining row is NONE
The ranked list at the bottom of the inventory named four gaps; all four now carry
clause-level proofs. It keeps the ranking — that is the reusable part — and states what
each mutation actually established, because two of the four turned out worse than the
ranking predicted (the BOM guard was fail-open on every non-vim host; the catalog guard's
wiring can drop a failure while still printing it).
Two additions the table cannot express:
* what a file-level MUTATION grade does NOT claim. Three of these four rows are
multi-purpose files. Per `testing.guard-ships-with-mutation-proof` the grade covers
the clause its cited case mutates, not every line in the file: `.husky/pre-push` runs
four other things and its row asserts only that line 11 is load-bearing. Reading it as
"pre-push is tested" is the relabelling error the withdrawn-guard table above already
exists to prevent.
* why each remaining guard is still NONE — the third `## Done-when` box. They split
three ways, and only the first is a backlog: the PreToolUse advisory hooks (unproven,
now observable via #776, which is the weaker claim); the two husky dispatchers, whose
mutable clauses live in the guards they call and whose dispatch is already covered by
`test_every_hook_file_is_actually_WIRED`; and six that each need their own harness
rather than a proof written to clear a row.
That list is prose and nothing checks it. Stated as such in the file rather than left to
be discovered — a guard moving between groups will not redden anything.
Refs: #785
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f2240b0205 |
test(785): drive build_decisions_catalog's --check path, and its __main__ wiring
`--check` is what the decisions-guard job runs; the existing tests call `render_catalog()`
directly and never call `main()` at all. So neither the stale-detection comparison nor the
`raise SystemExit(main())` wiring had any coverage.
Two mutations, both witnessed red against the real script:
* disarming `want.strip() != have.strip()` alone -> 3 failed, 3 passed
* replacing `raise SystemExit(main())` with a bare `main()` -> 1 failed, 5 passed, and
only the subprocess case reddens. That mutant prints
`build_decisions_catalog: docs/decisions/README.md is stale` on stderr and exits 0.
The workflow step reads nothing but the exit code, so CI would report green over a
stale catalog — the #751/#719 shape the decision record names.
The subprocess case runs the command DERIVED from pr-checks.yml rather than a restated
copy of it, and runs it twice: once on an untouched copy of the corpus, which must exit 0.
Without that half, any failure of the harness — an import error, a missing file in the
copy — would also exit non-zero and read as the guard firing.
Refs: #785
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d05d6623d1 |
test(785): pin .husky/pre-push's unset GIT_DIR ..., and measure why it matters
The one-line fix at pre-push:11 had nothing pinning it, and #785 ranks it third because the regression is a relocation rather than a deletion — a tidy-up that reads as a no-op. What the investigation established, since the comment above it asserts the mechanism without stating when it applies (git 2.55): * git exports GIT_DIR to pre-push when the push comes FROM A WORKTREE, and exports nothing from the main tree. `process.shared-tree-readonly` makes the worktree the mandated way to work here, so the exposed path is the normal one, not an edge case. * with GIT_DIR set and GIT_WORK_TREE unset, git stops discovering the repo and treats the CURRENT DIRECTORY as the work tree: `git rev-parse --show-toplevel` from web/ answers `<wt>/web`. * so `cd web && npm run check:api`'s `git diff --exit-code` compares against index paths that do not exist there and reports NO DIFF, exit 0. Generated-API drift ships and the gate prints success. The test drives the real pre-push file unedited, in a real worktree, with that exact environment; the hook calls are stubs (DERIVED from the file, so a new hook cannot leave the harness dying at a missing script and calling that a red) and `npm` is a stand-in whose `run check:api` performs the nested diff. Both mutations witnessed red against the real file: deletion -> 4 failed, 5 passed; relocation to the end -> 5 failed, 4 passed. The relocation mutation was wrong in the first draft. Appending the `unset` at the end also makes it the script's last statement, and a bare `unset` succeeds — so a `returncode == 0` assertion held for a reason unrelated to the nested git call. The stand-in npm now records the nested diff's OWN exit code and the mutations assert on that. Verified by making the mutation a no-op: the test fails, as it must. Refs: #785 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
629ab0b173 |
test(785): prove the worktree-ownership PAIR, including the cross-file seam
`pretooluse-worktree-guard.sh` and `posttooluse-worktree-marker.sh` are one mechanism in two files and neither had a test. #785 ranks them second because a regression in either half is invisible: both hooks are deliberately fail-open, so a marker hook that writes nothing and a guard that reads nothing both produce "the commit is allowed" — identical to a correct fail-open. Three clauses are disarmed and each was witnessed red against the REAL hooks in place, not only against the isolated copies the tests build: * the guard's marker read -> 5 failed, 5 passed * the guard's ownership comparison -> 2 failed, 8 passed (inverted, because disarming it the other way only makes the guard deny MORE and every deny assertion stays green) * the marker hook's write -> 6 failed, 4 passed The third is the one that could not exist while the halves were tested apart: the clause is in the marker hook, the assertion is about the guard's decision. The first attempt at that third demonstration used a `sed` that silently matched nothing and reported 10 passed. Re-run with an asserted replacement, hence the numbers above. Refs: #785 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |