Skip to content

Audit the window the vacuous unit mutation gate left unmutated, and kill what escaped - #1013

Merged
thekevinscott merged 4 commits into
mainfrom
claude/tackle-975-n8mc2h
Aug 21, 2026
Merged

thekevinscott merged 4 commits into
mainfrom
claude/tackle-975-n8mc2h

Conversation

@thekevinbot

@thekevinbot thekevinbot commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator

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 .gitignore nit. The nit was already fixed on main in eb9443f; this PR is the other two.

A note on notation: GitHub's sanitizer strips angle-bracket placeholders even inside code spans (it ate every one of them in the first version of this description), so BASE, PREFIX, FILE, PKG and friends below stand in for them.

The python and typescript arms cannot fail this way

Read at upstream 9077875. measure_rust is the only arm that materializes a diff file and delegates the filtering to its engine: write_base_diff runs git diff --relative BASE...HEAD -- PREFIX at the cargo workspace root so the paths reach cargo mutants --in-diff workspace-relative, and rebase_report_paths strips 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_typescript and measure_python both call patch_coverage::changed_lines(SCAN_ROOT, base), a git diff --unified=0 --relative run inside the scan dir whose paths come out scan-relative already, and hand the engine explicit scoping (Stryker --mutate FILE:START-END; cosmic-ray --module plus a changed-line post-filter). No diff file, no workspace root, no rebase, and it is the same helper the changed-lines unit coverage check 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 no outcomes.json it re-lists the crate's mutants and bails if any sits on a line the diff inserted — plus a count == 0 branch that now prints the engine found no mutants to test. Neither string is in the shipped binary: strings on @testing-conventions/x86_64-unknown-linux-gnu at 0.0.86 and 0.0.91 finds only the two old messages, and 0.0.91 is the npm latest that @v0 + npx resolves. #980's canary remains the only in-repo signal that doesn't wait on an upstream release.

The audit

Base 6bc91e2 — the last main commit on or before 2026-07-27, the day upstream closed #672 with a green proof — through 6f9e34a. 22 files, +7413/-2411, 470 mutants.

Two scopings, because the docs and CI disagree about which one the rust gate uses. -C --lib is the documented rule ("fails on any mutant no unit test kills"); CI's actual invocation is mutants_argv's cargo mutants --output OUT --in-diff DIFF --features cli with no target scoping, so the whole cargo test suite judges each mutant and an integration test counts as a kill.

result
unit scope (-C --lib) 330 caught, 61 survivors, 75 unviable, 4 timeouts
CI scope, over those 61 40 killed by the integration tier, 3 timeout, 18 escape every tier

The 18 escapes split three ways:

  • 7 were real gaps and are closed here. Six operator mutations in matcher.rs's placeholder_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. Plus lib.rs's prepare_persist, which computes meta_current as compatible && 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.
  • 7 are equivalent mutants, proved rather than argued. | → ^ over disjoint bit fields is always a no-op: function_flags' SQLITE_UTF8|SQLITE_DETERMINISTIC (bits 0x1 and 0x800), 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). In placeholder_spans, j += 1 → j *= 1 is re-consumed by the following while, and i = j + 1 → i = j * 1 / i = j - 1 land 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.
  • 4 sit in effectful code no tier reaches — RootedFs::hash (two), cli/run.rs's load_state guard and wait_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: integrate unit 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 pinning placeholder_spans's grammar, its resume-after-} scan, and an unclosed brace ahead of a real placeholder
  • packages/rust/src/lib.rs — one test pinning prepare_persist's incompatible-meta-of-equal-length case
  • agents/reference/testing-gates.md — the three findings above plus the recipe to redo the audit, including two sandbox traps that cost real time: -j4 unscoped exhausts the disk allowance and cargo-mutants records the resulting No space left on device as Unviable (a wrong verdict that looks real — -j2 is the ceiling), and passing -- --lib on top of -C --lib makes cargo reject the duplicated flag so the run dies with cargo test failed in an unmutated tree, which reads exactly like vtab permission test fails for any root user, so the Rust suite is never clean in the hosted sandbox #977 but is an argument error
  • AGENTS.md — one clause correcting the summary: the rust arm is the exception to unit, since the whole suite judges its mutants

Manual exercise

Beyond the suites, the audit itself is the manual exercise — cargo mutants was 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

  • Ran e2e suites locally for every affected SDK
  • Python SDK e2e: pass (32 passed, 1 skipped)
  • TypeScript SDK e2e: pass (5 files, 16 tests)
  • Rust core e2e (if applicable): pass — cargo test --features cli whole suite green, including the integration tier
  • packages/python/e2e-attestations/BRANCH.json receipt written if packages/python changed (claude-tackle-975-n8mc2h.json)
  • packages/ts/e2e-attestations/BRANCH.json receipt written if packages/ts changed (claude-tackle-975-n8mc2h.json)
  • Command(s) run: cd packages/python && uv run python -m pytest tests/e2e/ -x -q; cd packages/ts && pnpm build && pnpm test:e2e; receipts via uvx testing-conventions e2e attest
  • Result summary: all green. e2e verify confirmed for both packages with their real --extra-scope sets. 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

  • Changelog fragment added under ROOT/PKG/changelog.d/ for each changed package (or: skip-changelog trailer on a commit with reason) — skip-changelog: tests and docs only, no observable behavior change
  • Migration fragment added under ROOT/PKG/migrations.d/ (or: not required — additive/bugfix only) — not required, no API or behavior change

PARITY.md is untouched: no SDK public API changes, so neither drift introduced nor restored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DmQ9jX8oRurgmLGWMwpH4s

claude added 4 commits August 21, 2026 04:27
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

Copy link
Copy Markdown
Collaborator Author

Worth flagging before anyone reads it as the bug recurring: this PR's own rust / Unit mutation — changed lines (rust) job logs 0 mutant(s) tested, and that is correct here. Every line the diff adds sits inside a #[cfg(test)] mod tests, and cargo-mutants does not mutate test modules. Verified directly against the PR's own diff:

$ git diff --relative 9b23473...HEAD -- packages/rust > /tmp/pr.diff
$ cargo mutants --features cli --in-diff /tmp/pr.diff --list
INFO No mutants to filter
(0 mutants)

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 zero_mutant_verdict is shaped as re-list the crate's mutants and compare them against the diff's inserted lines instead. Under that guard this PR still passes, because none of the crate's mutants sits on a line this diff inserted. The 83s runtime is provisioning (Cache not found for input keys: cargo-mutants-provision-Linux), not a short-circuit.

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 matcher.rs and lib.rs lines the new tests target.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants