Close three fail-open gaps in the fixture-layout controls - #720
Merged
Merged
Conversation
committed_fixture_refs() is the oracle both bill gates consult, and neither can see it fail. An empty answer leaves test_no_source_reaches_into_bills_for_a_committed_fixture with no offenders and test_download_only_versions_are_genuinely_uncommitted with nothing still-committed, so both go green over a tree nothing polices. Measured before this commit: returning set() and planting a real stale fixture path in a scanned product module left the entire non-browser suite at 3818 passed. With this control the same mutation reddens. The sibling test claims to close this and structurally cannot, because it consults the same function -- the same oracle, so it fails the same way. The cross-check here is against git ls-files, which reads the index while committed_fixture_refs() walks the working tree, so the two can disagree. Only the index-knows-it/oracle-lost-it direction is asserted. The reverse is test_every_fixture_file_is_tracked_by_git's subject and is not restated, to keep one mutation from reddening two controls. Verified: 29 passed in the module, 3819 in the full non-browser suite, and 1 failed under the C1 mutation that previously passed silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwVE7osYo39KaUe7CEitFJ
test_every_exemption_names_a_file_the_scan_reaches checks that a key still RESOLVES, never that it is DESERVED. Those failures point opposite ways: a dead key has stopped applying, an over-broad one applies perfectly to a file that should never have had it. Measured before this commit: adding src/deltatrack/similarity.py to _DOWNLOAD_TIER_FILES and planting a real committed-fixture path in that module left this module at 29 passed and the suite green. Exempting product code was a one-line, silent, permanent hole, and the cheapest way to defeat every bill rule in the file. The two bounds differ in shape because the sets carry different risk. _DOWNLOAD_TIER_FILES switches off every bill rule, so it is bounded by tier (tools/, tests/): a new fetcher test gets in without ceremony while product code is unrepresentable. _DOWNLOAD_ROOT_NAMERS is enumerated, because one legitimate member IS product code (diff_bill.py's --bills-dir default, ADR 0013 / #152) and no prefix admits it without admitting the whole engine. Both bounds are literals in the assertion rather than constants beside the sets, per the reasoning in test_exclusions_stay_within_the_research_tree: a second configuration value lets the policy certify itself by widening both together. Verified: 30 passed in the module, and both C2 and C3 redden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwVE7osYo39KaUe7CEitFJ
test_fixture_tree_is_not_gitignored gave false assurance two ways.
1. Missing --no-index. Without it, git check-ignore reports what git
would DO with the path, and it does nothing to a TRACKED file whatever
.gitignore says. Measured: appending tests/corpus/**/*.xml to
.gitignore left the control at rc=1 ("not ignored") and the module at
28 passed. With the flag the same mutation reports rc=0 and the
control reddens.
2. The probe crossed bills/. That tree is commonly a SYMLINK -- the
.gitignore comment on /bills exists to support exactly that setup --
and git refuses a pathspec "beyond a symbolic link" with 128. The old
code inferred work-tree-ness from the check-ignore exit code, so 128
became a skip claiming "not a git work tree" while standing in one.
Measured with bills/ symlinked: 27 passed, 1 skipped. The control
silently did not run on the setup it most needed to cover.
Work-tree-ness is now asked directly with rev-parse --is-inside-work-tree
and is the only thing that may skip, so any other git failure is a
failure rather than a silent pass. The ignored-probe moved to
tests/data/extract_cache/, which is inside tests/ and crosses no symlink.
Verified: 30 passed; 1 failed under A5; 30 passed with bills/ symlinked
(no skip); 1 failed under A5 with bills/ symlinked; 3820 in the full
non-browser suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwVE7osYo39KaUe7CEitFJ
The G3 repair moved the ignored-probe off the download tree, which left DOWNLOADS_DIR imported and unused (F401). Every remaining mention in the module is a string, a regex, a comment or a monkeypatch target, so the import goes. Also wraps the probe's argument list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwVE7osYo39KaUe7CEitFJ
Review found both new ceilings were themselves fail-open. Each mutation below was applied, run, and reverted. G2 -- the bound admitted all of tests/ and tools/, which hold ordinary product and test code. Exempting tests/test_diff_bill.py, or a new non-fetch tools/verify_manifest.py, and then addressing a committed fixture through bills/ in it, left this module at 30 passed both times. _DOWNLOAD_TIER_FILES switches off EVERY bill rule for a file, so the prefix form let any test or tool bypass the lot. Membership is now enumerated: there is no rule separating a fetcher from its neighbours -- "is acquisition code" is a judgement, not a path shape -- so a list is the only honest bound. Both mutations now fail. G3 -- the control probed ONE xml file and the corpus holds 111 fixtures, 58 xml and 53 pdf. Appending *.pdf left it at 30 passed while git reported every committed PDF as ignored. It now derives the set from `git ls-files tests/corpus` and asks git about all of it in one check-ignore --no-index --stdin call, so a third format is covered the day it is committed. *.pdf now names all 53; tests/corpus/**/*.xml names all 58. An exit code that is neither 0 nor 1 is a failure rather than a verdict, instead of being inferred as "not ignored". The positive control keeps its purpose and still probes tests/data/extract_cache/, which crosses no symlink. test_download_only_versions_are_genuinely_uncommitted is DELETED. Its detection set is test_no_source_reaches_into_bills_for_a_committed_fixture's minus the tmp_path skip, and that difference is exactly the false positive the audit documented -- a synthetic tree naming a committed fixture is legal per the module's own docstring. The guarantee it claimed is held by test_the_committed_fixture_set_is_not_vacuous against a second oracle. No meta-test was added to replace it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwVE7osYo39KaUe7CEitFJ
willhea
force-pushed
the
fixture-layout-audit-and-gap-repairs
branch
from
September 11, 2026 15:12
43e0b7b to
3b3b366
Compare
The three added controls documented their own discovery: mutation counts, a prefix bound that was tried and rejected, corpus snapshots, the deleted sibling's reasoning. That is review history, and it belongs in the pull request rather than in the source. Kept: why the exemption allowlists are literals independent of the sets they bound; that _DOWNLOAD_ROOT_NAMERS waives one rule rather than all of them; that git's index is a second oracle for the filesystem-derived fixture set; that the reverse comparison is owned by test_every_fixture_file_is_tracked_by_git; why --no-index is required for tracked paths; and git check-ignore's exit codes with the purpose of the positive control. Removed: mutation histories and pass counts, the rejected prefix design, "58 XML, 53 PDF", the deleted test's justification, and the group labels above lists whose filenames already say what they are. Behaviour unchanged -- assertion logic is identical, only messages and prose are shorter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwVE7osYo39KaUe7CEitFJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Repairs the three places
tests/test_fixture_layout.pyfails open — where a real defect is planted and the suite stays green. One file changed, test-only, no production code.Found by mutation-testing the module's 28 controls. The evidence is below rather than in a committed report: 37 mutations were applied to the checkout, run, and reverted, and every number here is a measurement, not a reading.
The three gaps
Each verified red-then-green against the mutation that previously passed silently.
committed_fixture_refs()returnsset()+ a real stale path planted in a scanned moduletests/test_diff_bill.py, then address a committed fixture throughbills/in ittools/verify_manifest.py, same reference*.pdfto.gitignoretests/corpus/**/*.xmlG1 — the oracle nothing could see fail
committed_fixture_refs()decides what counts as an offence, and the gate consulting it cannot tell an empty answer from "no offences". Planting a genuine stale fixture path alongside a vacuous oracle left the entire non-browser suite green.The new control asserts non-emptiness and cross-checks
git ls-files tests/corpus— the index, against the working tree the oracle walks. That is a second oracle rather than the same one twice, which is precisely why the test that previously claimed this guarantee could not deliver it (below).G2 — bounded by enumeration, not by prefix
A
tools//tests/prefix bound was written first and was itself fail-open: both directories hold ordinary product and test code. Membership of_DOWNLOAD_TIER_FILESswitches off every bill rule for a file, so the prefix form let any test or tool address a committed fixture throughbills/forever.Membership is now enumerated. There is no rule that separates a fetcher from its neighbours — "is acquisition code" is a judgement, not a path shape — so a list is the only honest bound. A genuinely new fetcher costs a line in the test as well as in the set; that friction is the point, because exempting a file is the cheapest way to defeat the whole module.
G3 — three defects, not one
--no-index. Without itgit check-ignorereports what git would do with a path, and it does nothing to a tracked file whatever.gitignoresays. The control returned "not ignored" on a fixture tree that was fully ignored.bills/, which this repo's own.gitignoresays is commonly a symlink. Git refuses a pathspec "beyond a symbolic link" with 128, and the old code inferred work-tree-ness from that exit code — so it skipped, claiming "not a git work tree" while standing in one (measured withbills/symlinked: 27 passed, 1 skipped). Any code that is neither 0 nor 1 is now a failure, not a verdict.git ls-files tests/corpusand asks git about all of it in a singlecheck-ignore --no-index --stdincall, so a third format is covered the day one is committed, with no edit here.One control deleted
test_download_only_versions_are_genuinely_uncommittedis removed, taking the module from 30 controls to 29. No meta-test was added to replace it.Its detection set is
test_no_source_reaches_into_bills_for_a_committed_fixture's minus thetmp_pathskip — and that difference is a false positive: a synthetic tree naming a committed fixture is legal, per the module's own docstring. The guarantee it claimed (that the committed set has not quietly shrunk) is exactly what it could not deliver, because it consulted the samecommitted_fixture_refs()that had gone vacuous. G1's control holds it now, against git.What the audit found and this PR deliberately does not do
A prior pass proposed deleting two controls as redundant. Attacking those findings before acting on them withdrew both — each is the sole detector of a failure nothing else in the suite sees.
test_conftest_refuses_a_foreign_enginewas said to be dominated by its sibling, which asserts the same two things on a harder input. They actually differ in how their stand-in engine dies. Demoting the foreignness check into theexcept ModuleNotFoundErrorhandler reddens it alone across the whole suite, because the sibling's partial engine raisesModuleNotFoundError(still caught) while this one's fuller engine dies on a plainImportError(not caught). Against a complete engine copied out of another checkout:--collect-onlyRuntimeError: … DIFFERENT tree's sourceThat is a silent green about another checkout's source.
test_the_known_bad_source_is_reported_end_to_endwas said to add nothing over the discovery and rule tests. Givingfind_stale_fixture_pathsa directory roster reddens it alone: discovery still returns everything, and the rule still fires on every input the can-fire tests feed it. Every other input to that rule is keyed under a real top-level directory, so any roster written from today's tree spares them by construction.Net: the cluster has no measured removal candidates. The count moves 28 → 29 only because of the G1/G2 additions and the G3-sibling deletion.
A limitation worth stating
Mutation testing measures detection, never worth. A sole detector of an irrelevant behaviour is still deletable, and nothing here establishes relevance. Roughly half these controls exist only to make the static text scan trustworthy; their value is inherited from that scan, so they cannot be judged one at a time.
Relatedly, the audit also asked whether the scan could shrink to the files the suite never executes. That question is unresolved and out of scope here. The coverage measurement behind it does not carry it: file-level execution does not establish that a file's fixture-path lines ran, and a skip ceiling sees only skips that actually occur. Answering it would need line- or branch-level coverage of the path-bearing code plus mutations proving a replacement boundary catches stale paths after the scan is removed. The scan's shape is untouched by this PR.
Verification
tests/test_fixture_layout.pyruff check/ruff format --checkgit diff --statvsdevelopEvery mutation applied, run, and reverted; the worktree carries no mutation or generated artifact. The mutation harness is deliberately uncommitted — its bodies contain
bills/<id>/<file>literals that the very gates under audit would flag.The audit ran on a machine with the bill corpus fetched (32 bills, 17 shared with
tests/corpus/), which an earlier CI-shaped pass did not have. Two findings changed as a result.Follow-up in #721, stacked on this branch: the runtime skip ceiling widened suite-wide, five live CWD-dependent defects, and a
cwd-independenceCI job. Also code-only.🤖 Generated with Claude Code
https://claude.ai/code/session_01NwVE7osYo39KaUe7CEitFJ