Repository navigation
Audit the window the vacuous unit mutation gate left unmutated, and kill what escaped - #1013
Conversation
The rust `unit mutation` gate has been vacuous since at least 2026-07-27 (#975), so every `packages/rust/src` line merged since then went unmutated. Re-mutating that window by hand (470 mutants over `6bc91e2...HEAD`) turned up two survivors that no test at any tier kills: - `placeholder_spans` accepted no coverage of its own grammar. Four operator mutations survive: two `<` -> `<=` that run the index past the pattern, an `&&` -> `||` that does the same, and an `==` -> `!=` that reads `{1a}` as a placeholder. The new tests pin the counter-examples the function's own doc comment names. - `prepare_persist` computes `meta_current` as `compatible && lengths match`, and `&&` -> `||` survives: nothing exercised an incompatible meta block that happens to have the same key count, which would skip the meta rewrite and leave a stale glob hash on disk. Also correct AGENTS.md: the rust arm hands cargo-mutants no target scoping, so the whole `cargo test` suite judges each mutant there, unlike the python and typescript arms. skip-changelog: tests and docs only, no observable behavior change Refs #975 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmQ9jX8oRurgmLGWMwpH4s
Three things a future agent needs and cannot re-derive cheaply: - the defect is structurally Rust-only. `measure_rust` is the only arm that materializes a diff file and delegates filtering to its engine across a cargo-workspace-prefix round-trip; the python and typescript arms compute changed lines themselves inside the scan dir and hand the engine explicit scoping, so they cannot fail this way. - upstream's fix exists but is unpublished. `zero_mutant_verdict` and the corrected zero-count message are at upstream HEAD and in neither 0.0.86 nor 0.0.91, and 0.0.91 is the npm `latest` that `@v0` + `npx` resolves. - what the audit found, and the recipe to redo it: two scopings (the docs and CI disagree about which one the rust gate uses), the `-j2` disk ceiling, and the `-- --lib` argument clash that masquerades as #977. Also drop the direct `std::fs::write` from the new `prepare_persist` test -- `unit lint`'s no-out-of-module-call rule rejects it, and the assertion it fed was not what kills the mutant. skip-changelog: tests and docs only, no observable behavior change Refs #975 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmQ9jX8oRurgmLGWMwpH4s
|
Worth flagging before anyone reads it as the bug recurring: this PR's own That makes this PR a live example of the case #672 was worried about — the reason a guard keyed on "0 is always a bug" was rejected, and the reason upstream's The gate's own coverage of these changes came from running cargo-mutants by hand over the affected sites, which is reported in the PR body: 14 mutants killed at the Generated by Claude Code |
Closes #975.
#975's own task list had been split: the upstream root cause went to testing-conventions#481 and the in-repo fixture canary to #980. Three items stayed here — confirm whether the python/typescript arms share the defect, re-mutate the Rust source that merged while the gate was blind and fix any survivors, and a
.gitignorenit. The nit was already fixed onmainineb9443f; this PR is the other two.The python and typescript arms cannot fail this way
Read at upstream
9077875.measure_rustis the only arm that materializes a diff file and delegates the filtering to its engine:write_base_diffrunsgit diff --relative BASE...HEAD -- PREFIXat the cargo workspace root so the paths reachcargo mutants --in-diffworkspace-relative, andrebase_report_pathsstrips the prefix back off the report. That workspace-prefix round-trip is where #659, #672 and #975 have all lived, and it exists nowhere else —measure_typescriptandmeasure_pythonboth callpatch_coverage::changed_lines(SCAN_ROOT, base), agit diff --unified=0 --relativerun inside the scan dir whose paths come out scan-relative already, and hand the engine explicit scoping (Stryker--mutate FILE:START-END; cosmic-ray--moduleplus a changed-line post-filter). No diff file, no workspace root, no rebase, and it is the same helper the changed-linesunit coveragecheck exercises on every PR.What all three arms do share is the reporting: each can return
Measurement::Tested { count: 0 }, which the published CLI prints as the all-caught success line.Upstream's fix is written but unpublished, so CI is still blind
Upstream HEAD has
zero_mutant_verdict— when cargo-mutants writes nooutcomes.jsonit re-lists the crate's mutants and bails if any sits on a line the diff inserted — plus acount == 0branch that now printsthe engine found no mutants to test. Neither string is in the shipped binary:stringson@testing-conventions/x86_64-unknown-linux-gnuat 0.0.86 and 0.0.91 finds only the two old messages, and 0.0.91 is the npmlatestthat@v0+npxresolves. #980's canary remains the only in-repo signal that doesn't wait on an upstream release.The audit
Base
6bc91e2— the lastmaincommit on or before 2026-07-27, the day upstream closed #672 with a green proof — through6f9e34a. 22 files, +7413/-2411, 470 mutants.Two scopings, because the docs and CI disagree about which one the rust gate uses.
-C --libis the documented rule ("fails on any mutant no unit test kills"); CI's actual invocation ismutants_argv'scargo mutants --output OUT --in-diff DIFF --features cliwith no target scoping, so the wholecargo testsuite judges each mutant and an integration test counts as a kill.-C --lib)The 18 escapes split three ways:
matcher.rs'splaceholder_spans— its own documented grammar counter-examples ({},{1a},{a-b}, an unclosed{) had no assertions, so two<→<=that run the index past the pattern, an&&→||that does the same, and an==→!=that reads{1a}as a placeholder all survived. Pluslib.rs'sprepare_persist, which computesmeta_currentascompatible && lengths match:&&→||survived, and it would claim a stale glob hash is current and skip the meta rewrite. Verified by re-running cargo-mutants over those sites: the five new tests kill 14 mutants, leaving only the equivalents below.|→^over disjoint bit fields is always a no-op:function_flags'SQLITE_UTF8|SQLITE_DETERMINISTIC(bits0x1and0x800),base64_encode's three shifted bytes (exhaustive over all 16,777,216 inputs),base64_decode's(triple << 6) | value_of(b)(all 67,108,864). Inplaceholder_spans,j += 1→j *= 1is re-consumed by the followingwhile, andi = j + 1→i = j * 1/i = j - 1land on the closing brace or a name byte, never a{— zero differing results over all 335,923 strings up to length 7 on{ } a 1 _ -. No exemptions added: the gate is diff-scoped so these only surface if someone edits those lines, and the exemption count stays zero.RootedFs::hash(two),cli/run.rs'sload_stateguard andwait_for_shutdown. Tracked in Four mutants in effectful core survive every test tier (found by the #975 blind-window audit) #1012 rather than re-litigating testing-conventions: integrateunit lint(isolation) #233 here.The headline: nothing merged behind the blind gate was a silent behavior regression. What it hid was test debt in two pure functions.
Changes
packages/rust/src/matcher.rs— four tests pinningplaceholder_spans's grammar, its resume-after-}scan, and an unclosed brace ahead of a real placeholderpackages/rust/src/lib.rs— one test pinningprepare_persist's incompatible-meta-of-equal-length caseagents/reference/testing-gates.md— the three findings above plus the recipe to redo the audit, including two sandbox traps that cost real time:-j4unscoped exhausts the disk allowance and cargo-mutants records the resultingNo space left on deviceasUnviable(a wrong verdict that looks real —-j2is the ceiling), and passing-- --libon top of-C --libmakes cargo reject the duplicated flag so the run dies withcargo test failed in an unmutated tree, which reads exactly likevtabpermission test fails for any root user, so the Rust suite is never clean in the hosted sandbox #977 but is an argument errorAGENTS.md— one clause correcting the summary: the rust arm is the exception to unit, since the whole suite judges its mutantsManual exercise
Beyond the suites, the audit itself is the manual exercise —
cargo mutantswas driven by hand over three scopings (470 unit-scoped, 80 + 34 CI-scoped, 17 verifying the new tests), and the equivalence claims were checked by two standalone Rust programs run to completion rather than reasoned about. The rust coverage floor was re-measured on the final tree under the pinned nightly: regions 92.97 / functions 92.36 / lines 93.74 / branch 79.79 against floors 91 / 91 / 92 / 72, so no floor needed nudging.E2E Verification
cargo test --features cliwhole suite green, including the integration tierpackages/python/e2e-attestations/BRANCH.jsonreceipt written ifpackages/pythonchanged (claude-tackle-975-n8mc2h.json)packages/ts/e2e-attestations/BRANCH.jsonreceipt written ifpackages/tschanged (claude-tackle-975-n8mc2h.json)cd packages/python && uv run python -m pytest tests/e2e/ -x -q;cd packages/ts && pnpm build && pnpm test:e2e; receipts viauvx testing-conventions e2e atteste2e verifyconfirmed for both packages with their real--extra-scopesets. Gates run locally: rust unit tests (773 passed),colocated-test,unit lint,integration lint,unit coverage,clippy -D warnings,cargo fmt --check,changelog-gate.Changelog / Migrations
ROOT/PKG/changelog.d/for each changed package (or:skip-changelogtrailer on a commit with reason) —skip-changelog: tests and docs only, no observable behavior changeROOT/PKG/migrations.d/(or: not required — additive/bugfix only) — not required, no API or behavior changePARITY.mdis untouched: no SDK public API changes, so neither drift introduced nor restored.🤖 Generated with Claude Code
https://claude.ai/code/session_01DmQ9jX8oRurgmLGWMwpH4s