Skip to content

fail-pattern: abort when it matches the baseline, and ship known-good patterns per harness - #22

Open
thedavidmeister wants to merge 3 commits into
2026-08-12-mutation-probe-binfrom
issue-13-fail-pattern
Open

fail-pattern: abort when it matches the baseline, and ship known-good patterns per harness#22
thedavidmeister wants to merge 3 commits into
2026-08-12-mutation-probe-binfrom
issue-13-fail-pattern

Conversation

@thedavidmeister

Copy link
Copy Markdown
Contributor

Closes #13. Stacked on #10mutation-probe-rs/ only exists on that branch, so this targets 2026-08-12-mutation-probe-bin.

The killer column is the part of a mutation matrix a reader can check. suite.fail-pattern produces it, is optional, has no default, and the only worked example in --help was cargo-shaped — so every Solidity campaign authored its own. It carries two opposite defects, and neither of them shows up as a failing run, because the VERDICT is read from the suite's pass/fail tally and never from this pattern. One campaign on rainlanguage/rain.sol.codegen hit both.

Too wide. '\] (test\w+)\(' matches [PASS] testFoo( exactly as readily as [FAIL: …] testFoo(captures_iter runs unanchored over the whole output and nothing there distinguishes a pass from a failure. A guard mutant inside applyMigration came back killed by testHeadGenesisIsNotZero, a pure constant assertion. The matrix read exactly like a correct one while naming tests that pass under the mutant.

Too narrow, and silent. '\[FAIL.*?\] (test\w+)\(' fixes that and then names nobody: . does not cross newlines, and forge puts multi-line assertion messages inside the brackets. The issue records 87 of 95 kills unattributed in one pass and 7 of 18 in another — all correct KILLED verdicts, with an empty killer column.

Three changes, one per direction plus the one that stops the field being authored at all:

direction how it is caught now
too wide the green baseline is a free oracle — baseline_defect has already established that ZERO tests failed there, so anything the pattern captures out of baseline output is a PASSING test. mutation-probe aborts (exit 2) with the offending captures in the message, the way a red baseline already aborts
too narrow invisible to that check (a pattern matching nothing matches nothing at baseline either), so each such kill prints killer NOT NAMED instead of shipping a blank cell
authored at all harness = "forge" | "cargo" supplies a known-good proof and fail-pattern, selected by the same signal that already picks the suite command

Both patterns describe a harness's OUTPUT FORMAT — a property of forge or cargo, not of any repo — so authoring them per campaign re-derives the same two mistakes every time. Each shipped pattern is pinned by a test to real captured output of that harness under fixtures/, green and red, across two forge versions.

(?s) is not the fix, and that is measured rather than reasoned

The issue proposes (?s) as the one-line fix for the narrow direction and flags it unverified. It was verified, against real forge output from two versions, and it is refuted — the_s_flag_fixes_multiline_messages_and_then_misattributes_invariants holds both halves:

  • It is the reason the narrow pattern names nobody. Under (?s), testGeneratedSourceMatchesSnapshot — the multi-line assertEq case — is named again.
  • And it reintroduces defect 1 from the other direction. A forge invariant failure has no ] name( of its own: the name is printed on a later line, after a [Sequence] block. (?s) runs on past the end of that entry and stops at the next ] name( in the output — which in both fixtures belongs to a [PASS] line. invariant_NeverIncrements drops out of the killers and testCounterStartsAtZero, which passed under the mutant, is named in its place.

Trading a blank cell for a wrong one is the worse half of the trade, so the shipped patterns are line-anchored instead of dot-all.

Decisions worth disagreeing with

  • An explicit proof or fail-pattern overrides the harness's, not the other way round. A repo whose suite wraps its harness — a build.sh that reformats output — has to be able to say so, and silently preferring the shipped pattern over what the config asked for would be its own wrong matrix.
  • The wide direction aborts rather than warns. [PASS] testFoo( and [FAIL: …] testFoo( differ by a few characters, and every artefact downstream of the wide pattern looks correct. A warning on a pass whose whole output is a matrix is a warning nobody reads.
  • A missing killer is announced per kill, and only when a fail-pattern was configured. A config that never asked for attribution has no blank column to complain about, and nagging on every kill would train the reader to skip the line that matters when a pattern is configured.
  • killedBy is deduplicated, then capped. Forge prints every failing test twice — once inline, once under Failing tests: — so an undeduped list spends its cap of 5 on two names printed twice and drops the rest.
  • The fixtures are captured output, verbatim, from two forge versions and not hand-written samples. A pattern that only reads the newest build is not a known-good pattern for the org's repos, and the shapes that break these patterns (a multi-line assertEq of generated Solidity, a fuzz counterexample with ] inside the message, an invariant name printed after a [Sequence] block, a -vvv trace line) are not shapes anyone writes from memory correctly.

What it does not do

The issue's stronger fix — capture the set of test names that PASS at baseline and at each mutant, take the difference, and delete the regex entirely — is not here. It needs structured output (forge test --json) rather than scraped text, which is a different change to a different part of the bin: it replaces how the suite is run and read, not how its text is matched. Worth filing on its own; this PR closes the class the two shipped defects came from without pre-empting it.

QA

  • Discriminating tests: 17 new — 12 unit (shipped_forge_proof_reads_forges_own_tally, shipped_forge_fail_pattern_names_every_failure_and_no_passing_test, shipped_cargo_patterns_read_real_cargo_output, the_incidents_wide_pattern_is_caught_by_the_green_baseline, a_sound_fail_pattern_captures_nothing_at_a_green_baseline, the_s_flag_fixes_multiline_messages_and_then_misattributes_invariants, killed_by_is_distinct_and_capped, a_named_harness_supplies_both_patterns, explicit_patterns_override_the_harness, an_unknown_harness_names_the_ones_that_exist, without_a_harness_proof_is_still_required, every_shipped_pattern_compiles_with_the_groups_the_probe_reads) and 5 integration against the toy repo (a_fail_pattern_matching_the_baseline_aborts_before_probing, a_fail_pattern_that_names_nobody_says_so_per_kill, a_pass_with_no_fail_pattern_at_all_is_not_nagged, a_named_harness_replaces_the_hand_written_patterns, an_unknown_harness_is_a_config_abort). Suite is 39 green (27 unit + 12 integration), up from 22. The integration five drive the real bin end to end: the wide pattern aborts the pass at exit 2 with its baseline captures in the message and writes no report at all; the narrow one still scores KILLED and says killer NOT NAMED; harness = "cargo" alone, with neither regex written out, drives a full pass and names the killer.

  • Mutations applied: 14 self-mutants over the new logic and the shipped patterns, run BY mutation-probe itself (suite = nix develop -c cargo test --offline --no-fail-fast, baseline asserted green at 39 passed before probing). 14/14 killed, every one naming the test that kills it. tests:: prefixes are unit tests, bare names integration:

    mutant killed by
    M01 baseline fail-pattern check never fires tests::a_sound_fail_pattern_captures_nothing_at_a_green_baseline, tests::the_incidents_wide_pattern_is_caught_by_the_green_baseline, +3
    M02 the baseline check is computed and thrown away a_fail_pattern_matching_the_baseline_aborts_before_probing
    M03 killedBy stops deduplicating tests::killed_by_is_distinct_and_capped, tests::shipped_forge_fail_pattern_names_every_failure_and_no_passing_test, +1
    M04 killedBy cap lifted tests::killed_by_is_distinct_and_capped
    M05 harness stops supplying proof tests::a_named_harness_supplies_both_patterns, a_named_harness_replaces_the_hand_written_patterns
    M06 harness stops supplying fail-pattern tests::a_named_harness_supplies_both_patterns, a_named_harness_replaces_the_hand_written_patterns
    M07 the harness wins over an explicit pattern tests::explicit_patterns_override_the_harness
    M08 any harness name resolves to the first one tests::an_unknown_harness_names_the_ones_that_exist, an_unknown_harness_is_a_config_abort, +2
    M09 forge pattern loses its invariant branch tests::shipped_forge_fail_pattern_names_every_failure_and_no_passing_test, tests::the_s_flag_fixes_multiline_messages_and_then_misattributes_invariants
    M10 forge pattern admits any bracketed line, PASS included tests::shipped_forge_fail_pattern_names_every_failure_and_no_passing_test, tests::the_s_flag_fixes_multiline_messages_and_then_misattributes_invariants
    M11 forge pattern drops the trailing gas/runs anchor tests::shipped_forge_fail_pattern_names_every_failure_and_no_passing_test
    M12 forge proof only counts green suites tests::shipped_forge_proof_reads_forges_own_tally
    M13 cargo pattern takes a non-space name tests::shipped_cargo_patterns_read_real_cargo_output
    M14 NOT NAMED printed even with no fail-pattern configured a_pass_with_no_fail_pattern_at_all_is_not_nagged
  • The survivor this pass found, and what closed it. M11 SURVIVED the first run: deleting the trailing \([^\n]*\) \((?:gas|runs): from the shipped forge pattern changed nothing any fixture could see, so the anchor was undocumented and unpinned. The shape that discriminates it is one campaigns hit constantly — at -vvv forge prints call traces, and a reverting contract's custom error comes out as └─ ← [Revert] Custom(1, 2), which is a ] name( with no metrics after it. Without the anchor the pattern captures Custom and names a Solidity error as the killing test. Closed with the same red run re-captured at -vvv on both pinned forge versions (*-red-traces.txt), shipped_forge_fail_pattern_names_every_failure_and_no_passing_test strengthened in place to walk all four fixtures, and the anchor's job written into the pattern's comment. Confirmed by re-running the whole 14 on the fix: 14/14.

  • Oracle: issue fail-pattern silently misattributes killers: abort when it matches the baseline's own output, and ship known-good patterns #13's two observed defects and the harnesses' real output, never the code. Every shipped-pattern expectation is captured_names(fixture) == the failures the run's own summary reports — forge says "4 tests passed, 7 failed" and the test asserts exactly those seven names, once each, from output where forge printed each of them twice. The (?s) hypothesis was treated the same way: run, not reasoned about, and recorded as refuted with the fixture that refutes it.

  • Category check: covers both defects the issue names, not just the one that produced the incident, plus the authoring step that generates them (harness). The issue's own (?s) suggestion was tested rather than adopted, and its "stronger fix" is left explicitly unbuilt rather than silently dropped. No skills/ change, so no version bump — version-hygiene CI requires one only when skills/ moves.

claude and others added 3 commits August 16, 2026 16:48
… patterns

Closes #13.

Two independent defects hit `suite.fail-pattern` in one campaign, neither a
property of the repo it happened in.

TOO WIDE. `'\] (test\w+)\('` matches forge's `[PASS] testFoo(` as readily as
`[FAIL: ...] testFoo(`, so mutants were credited to tests that pass under
them — a pure-constant assertion named as the killer of a guard mutant. The
green baseline is a free oracle for this: nothing failed there, so anything
the pattern captures out of baseline output is a passing test. `mutation-probe`
now aborts on that, with the offending captures in the message, the way a red
baseline already aborts.

TOO NARROW, AND SILENT. `'\[FAIL.*?\] (test\w+)\('` fixes the first and then
names nobody, because `.` does not cross the newlines forge puts inside a
multi-line assertion message. The verdict is unaffected (it comes from the
tally), so the only symptom is an empty killer column. The baseline check
cannot see this one — a pattern that matches nothing matches nothing at
baseline either — so each such kill now prints "killer NOT NAMED".

And `harness = "forge" | "cargo"` supplies both patterns, so a campaign stops
authoring the field that carries both mistakes. Each shipped pattern is pinned
by a test to real captured output of that harness under fixtures/, green and
red, across two forge versions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cargo fixture had no FAILING doctest, so nothing discriminated `(.+)` from
`(\S+)` in the cargo fail-pattern — the comment claimed a property the tests did
not hold it to. And no end-to-end run killed a mutant with no fail-pattern
configured at all, so the guard on the 'killer NOT NAMED' line was free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mutation pass survived a mutant that deletes `\([^\n]*\) \((?:gas|runs):`
from the shipped forge fail-pattern: nothing in the fixtures discriminated a
name on a RESULT line from any other `] name(` in forge's output.

Real output has one, and campaigns hit it routinely: at `-vvv` forge prints
call traces, and a reverting contract's custom error comes out as
`└─ ← [Revert] Custom(1, 2)`. Without the anchor the pattern captures `Custom`
and names it as the test that killed the mutant.

Same red run re-captured at `-vvv` on both pinned forge versions, and the
existing test strengthened to walk all four fixtures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Aug 16, 2026
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ca2111ba-d681-470d-a140-b063b03c73c8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
vet-protocol 4
lens source@b2d7e41057a8c3dea9464f299b6c3f336551a7ff + audit skill invoked at pr:22
Reviewed b2d7e41: design — Does #13 accept this PR as its close? The coverage gate cannot be applied: the stacked base (2026-08-12-mutation-probe-bin) means GitHub links no issue — pr_context returns closes:[] and no #13 payload — so the body's textual "Closes #13" is unfalsifiable on this surface, while the body itself says #13's "stronger fix" (forge --json structured output replacing the regex) is deliberately left unbuilt; if the stack later retargets to main the linkage goes live at this same head. The code passed audit (all body claims verified against the fixtures; only LOW/INFO in-diff findings) — the ruling needed is whether Closes #13 stands or must read Refs #13 with the stronger fix filed separately.
cost 617 — regex attribution logic across harnesses

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

Labels

ai:design AI vetter: raises a design question

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants