Files
ersatztv/scripts/tests/test_mutation_harness.py
T
timothyandClaude Fable 5.1 a7d91bf15a
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 35s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 57s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 37s
PR Gates / Docs update reminder (pull_request) Successful in 1m0s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Review-verdict: MERGEABLE @ a7d91bf (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 45s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m27s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m4s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
fix(876): sweep session narrative out of hooks, workflows, scripts, tests and code comments; grow the detector to the process corpus
`docs.no-session-narrative` reaches every durable artifact, but its detector scanned only
`docs/**/*.md` and root markdown, and nothing had ever swept the rest. The issue named four sites
from one grep and called them a floor. Deriving the population instead — a whitespace-joined sweep
over every tracked file outside the detector, for the detector's own phrasings plus the attribution
and review-round class #812 found — gave 453 sites in 108 files at `fb5592971`, and a second pass
for phrasings the first list missed (hyphenated `round-N`, "an earlier version", "the reviewer
proved") added residuals in the same files. Every site was classified with #812's three
dispositions (CUT / SEVER / KEEP with its sub-kind) under the who-benefits test; the per-site
manifests are on the PR. The rejected designs, tested-and-rejected fixtures, measurements and
traps stay; the attribution of who found them and the round in which they were found go.

The detector's population grows to `.claude/`, `.gitea/`, `.husky/` and `scripts/` regardless
of extension, minus the detector and its own test (whose fixtures ARE the phrasings) and minus
`scripts/tests/fixtures/` (test data, including decision-record copies — the same reasoning as
the records' own exemption, and what keeps the record's depth measurement true), and `--all`
lists tracked REGULAR files only — a symlink's content is its target and a gitlink has none. The #812
argument for leaving `docs/superpowers/**` in the population runs the other way here: `--diff`
sees only ADDED lines, and 287 of the 453 sites were under 30 days old — this corpus is where
narrative is being added, so the advisory nudge has reach. Density agrees: 56 line-mode hits over
the 113 regular files the predicate admits, against 9 over 66 docs files before #812. `web/` and C# stay out on the same
measurement (3 of 74 PATTERNS-matching sites, ~4,600 files). The predicate did not grow: PATTERNS
matched 74 of 453 sites, and widening the word list to the attribution class is the treadmill
the withdrawn parity test ran on. The population oracle is restated over segments with the new
arms, the synthetic cross product gains the process heads and non-markdown extensions, a fixture
witnesses that a tracked symlink is neither scanned nor counted, a `.py.bak` axis separates a
by-name exemption from a `startswith` over the same tuple, and eight mutants (drop the process
arm, drop the by-name exemption, exempt by `startswith`, drop or add a prefix, drop the fixtures
exemption, list only markdown, drop the symlink filter, test the mode per row instead of per
path) each
redden it. A pre-existing silent drop in `--diff` goes with it: git tab-terminates a `+++`
filename that contains a space, and the kept tab made `is_scanned_path` refuse the file with no
notice — fixed, with a positive control and its own mutant.

Code is unchanged by construction, measured per file type against `origin/main`: Python modules
are AST-equal with docstrings stripped, except `#` lines inside the embedded fixture programs
(string literals) of three test modules; workflows differ only in `#` lines inside `run:` block
scalars; shell, C#, TypeScript and jq are equal with comment lines stripped. The stated
exceptions: the detector and its test, 26 vitest titles that carried review-round or severity
labels or a reviewer attribution (call sites whose title changed — every changed title line
walked back to its `it(` / `it.each(...)(` anchor, so a `' + '` concatenation counts once), two
registry note strings and the mutation manifest's prose fields. scripts/tests: 1565 passed.
Web: lint, typecheck, 1319 tests green. Closes #876.

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEcBoFw7ctrf3Nb7R7x7wk
2026-09-03 20:51:39 +02:00

503 lines
27 KiB
Python

"""Every `MUTATION` row of `docs/guard-inventory.md` is EXECUTED, not asserted (ersatztv#790).
The `MUTATION` grade means "a clause-level mutation was executed and this named test was witnessed
red". Witnessed once, by hand, that is evidence about the day the row was written and nothing else:
it decays as soon as the guard is edited, and a wrong grade has no way to announce itself.
So this file re-runs every one of them: for each declared mutation in `mutation_manifest.py`, apply
it to an isolated copy of this repository and require the row's OWN named test to go red.
WHAT A GREEN RUN HERE DOES AND DOES NOT PROVE — stated because a mutation harness that overclaims is
the same defect one level up:
* It proves the recorded proof ref names a test that EXISTS, still collects, and still reacts to
the declared clause. That is precisely the decay #790 was filed about.
* It proves the declared clause still occurs, exactly once, in the entry's declared `target` —
which is not always the guard's own file. A clause that has been reworded fails here rather than
silently mutating nothing.
* Combined with the positive control below, it proves every named proof test is GREEN on the
unmutated sandbox — so "the mutation was noticed" cannot be confused with "the test was already
red". For the proof tests that perform a disarm of their own, that control also runs the disarm;
the two named tests that are plain production set-equality checks have no disarm of their own,
and this harness supplies theirs.
* It does NOT prove the declared clause is the ONLY thing the guard hangs on. Some entries redden
through the proof test's own "the clause has moved, RETARGET this" assertion rather than through
changed behaviour. That is the intended reading, not a hole: those tests perform their
behavioural disarm themselves on every green run, and what they could not do is notice their own
clause reference going stale. Which entries those are is a dated measurement and lives in
`docs/decisions/records/testing/mutation-claims-are-executed.md`, not here.
The `granularity` column is where this file refuses to flatter itself. See `Mutation` in
`mutation_harness_lib.py`: a `DETECTOR` entry — one whose detector accumulates faults from arms that
a single mutation trips several of at once — must CARRY the finer mutation that survived, re-run
every suite and required to keep surviving. EVERY entry is `CLAUSE` today and the survivor set is
EMPTY, so that machinery is currently unexercised; `test_the_DETECTOR_survivor_set_is_empty_ON_PURPOSE`
says so out loud rather than leaving it as a lone SKIPPED placeholder nobody reads.
"""
from __future__ import annotations
import importlib.util
import re
import shutil
import sys
from dataclasses import replace
from pathlib import Path
import pytest
from scripts.tests.mutation_harness_lib import (
_BASELINES,
Mutation,
_git,
build_sandbox,
reset_sandbox,
run_pytest,
verify_mutation,
)
from scripts.tests.mutation_manifest import MUTATIONS, UNDECLARED
REPO_ROOT = Path(__file__).resolve().parents[2]
INVENTORY = REPO_ROOT / "docs" / "guard-inventory.md"
# `| `guard` | blocks | kind | proof | proof ref |` — the five-column inventory row. The column
# count is what excludes the two-column tables elsewhere in the file; `test_the_inventory_rows_parse`
# below is the floor that catches the shape changing under it.
_ROW = re.compile(r"^\|\s*`([^`]+)`\s*\|([^|]*)\|\s*(\w[\w-]*)\s*\|\s*([A-Z-]+)\s*\|\s*(.*?)\s*\|\s*$", re.M)
def _inventory_rows() -> list[tuple[str, str, str, str]]:
"""(guard, kind, proof grade, proof ref) for every inventory row.
Derived from the document the grades live in, never hand-listed: the whole point is that a row
cannot change its grade without this file noticing.
"""
rows = [(m.group(1), m.group(3), m.group(4), m.group(5).strip("`")) for m in _ROW.finditer(INVENTORY.read_text())]
guards = [g for g, _k, _p, _r in rows]
duplicates = sorted({g for g in guards if guards.count(g) > 1})
assert not duplicates, (
f"the inventory lists {duplicates} more than once. Every comparison below reduces rows "
"through a set or a dict, so a duplicate row is INVISIBLE to this file — it would report all "
"entries verified over a table that contradicts itself. `test_guard_inventory.py` catches "
"this too, but only when the whole suite runs, and this file is routinely run alone."
)
assert len(rows) >= 50, (
f"only parsed {len(rows)} inventory rows — the table's shape has changed and this regex now "
"reads a fraction of it. Every set comparison below would then pass over a population that "
"is mostly missing, which is the vacuous-completeness failure the inventory itself exists to "
"prevent."
)
return rows
# ------------------------------------------------------------------------------------------------
# THE MANIFEST IS PINNED TO THE INVENTORY — a grade cannot change without this file changing
# ------------------------------------------------------------------------------------------------
def test_the_manifest_covers_exactly_the_MUTATION_rows():
"""Set equality, both directions, because they are different defects.
A `MUTATION` row with no manifest entry is a claim nothing checks — the state #790 was filed
about. A manifest entry for a row that is no longer graded `MUTATION` is a check whose subject
has moved out from under it, and it would keep passing.
"""
graded = {guard for guard, _kind, grade, _ref in _inventory_rows() if grade == "MUTATION"}
declared = {m.guard for m in MUTATIONS}
assert graded, "no row in the inventory is graded MUTATION, so this whole file would prove nothing"
assert declared - graded == set(), (
f"declared mutations for rows that are not graded MUTATION: {sorted(declared - graded)}"
)
assert graded - declared == set(), (
f"these rows claim a MUTATION proof with nothing executing it: {sorted(graded - declared)}. "
"Declare the clause in mutation_manifest.py, or regrade the row."
)
assert len(MUTATIONS) == len(declared), "two manifest entries name the same guard"
def test_every_declared_mutation_names_the_row_s_OWN_proof_ref():
"""The manifest may not point at a different test than the row does.
Without this, the inventory could keep citing a stale proof while the harness quietly exercised
a healthier one, and the row would read as verified.
"""
refs = {guard: ref for guard, _kind, grade, ref in _inventory_rows() if grade == "MUTATION"}
wrong = [(m.guard, m.proof, refs.get(m.guard)) for m in MUTATIONS if refs.get(m.guard) != m.proof]
assert not wrong, f"manifest proof ref disagrees with the inventory row: {wrong}"
def test_every_GUARD_row_is_either_DECLARED_or_STATED_here():
"""`Done-when`: guards whose mutation cannot be declared are STATED, not silently skipped.
Set equality against the inventory's GUARD rows, both directions, keyed on the guard. Keying on
the row's GRADE instead would be cheaper and tautological — a new guard graded NONE would inherit
a reason automatically and nobody would look at it — and a COUNT moves only on net change, so one
guard arriving as another is promoted leaves it unchanged. This is the same hand-maintained,
machine-checked shape as `docs/guard-inventory.md` itself, which is what makes it safe.
The partition covers `Kind == GUARD` rows only. `TOOLING` asserts nothing and `PROOF` files exist
to prove other guards, so neither carries a mutation claim to verify — a rule about the Kind
column rather than a list anyone maintains.
"""
guards = {guard for guard, kind, _grade, _ref in _inventory_rows() if kind == "GUARD"}
declared = {m.guard for m in MUTATIONS}
stated = set(UNDECLARED)
assert not (declared & stated), (
f"these guards are both declared and stated as undeclared: {sorted(declared & stated)}"
)
unaccounted = sorted(guards - declared - stated)
assert not unaccounted, (
f"these guards are neither declared nor stated: {unaccounted}. Declare the clause in "
"mutation_manifest.py, or write a line in UNDECLARED saying what a proof would need — "
"silence is the one option that is not available."
)
orphaned = sorted(stated - guards)
assert not orphaned, (
f"UNDECLARED names rows that are not GUARD-kind rows any more: {orphaned}. A reason for a "
"guard that no longer exists reads as coverage."
)
thin = [g for g, reason in UNDECLARED.items() if len(reason.strip()) < 60]
assert not thin, f"these UNDECLARED entries say nothing a reader could act on: {thin}"
def test_every_entry_declares_a_known_granularity_and_DETECTOR_entries_CARRY_their_survivor():
"""`DETECTOR` is an admission, and an unevidenced one would be a grading curve.
#790's complaint about `pin_population_faults` — that neutering a whole helper is "coarse enough
that a single surviving clause would not be noticed" — applies to every entry graded here. So an
entry may only claim `DETECTOR` while carrying the finer mutation that was tried, as data rather
than as a sentence: `test_every_SURVIVING_clause_mutation_still_does` then runs it.
"""
bad = [m.guard for m in MUTATIONS if m.granularity not in (Mutation.CLAUSE, Mutation.DETECTOR)]
assert not bad, f"unknown granularity on {bad}; allowed: CLAUSE, DETECTOR"
assert all(m.why.strip() for m in MUTATIONS), "every declared mutation must say what its clause does"
unevidenced = [m.guard for m in MUTATIONS if m.granularity == Mutation.DETECTOR and not m.survived_clause]
assert not unevidenced, (
f"these entries claim DETECTOR granularity while naming no finer mutation that was tried: "
f"{unevidenced}. Carry the survivor, or declare the finer clause instead."
)
misplaced = [m.guard for m in MUTATIONS if m.granularity == Mutation.CLAUSE and m.survived_clause]
assert not misplaced, (
f"these entries are graded CLAUSE but carry a surviving finer mutation: {misplaced}. If a "
"finer clause exists and survives, the grade is DETECTOR."
)
half = [m.guard for m in MUTATIONS if bool(m.survived_clause) != bool(m.survived_replacement)]
assert not half, f"a survivor needs both a clause and a replacement: {half}"
def test_every_entry_declares_a_SPECIFIC_diagnostic_it_must_redden_with():
"""`expect` is what stops exit code 1 from being the whole verdict, so it cannot be a token.
An empty or near-empty expectation matches any output and hands the verdict straight back to the
exit status — the state this field exists to leave. It must also be a substring of no other
entry's, or two rows could be satisfied by one another's diagnostic.
"""
vague = [(m.guard, m.expect) for m in MUTATIONS if len(m.expect.strip()) < 20]
assert not vague, f"these expectations are too weak to distinguish one red from another: {vague}"
for m in MUTATIONS:
clashes = [o.guard for o in MUTATIONS if o is not m and m.expect in o.expect]
assert not clashes, f"{m.guard}'s expectation is contained in {clashes}'s — neither is specific"
# ------------------------------------------------------------------------------------------------
# THE EXECUTION — one isolated repository, reused, reset between mutations
# ------------------------------------------------------------------------------------------------
@pytest.fixture(scope="session")
def sandbox(tmp_path_factory):
"""An isolated copy of this repository, with the POSITIVE CONTROL already run in it.
The control is inside the fixture rather than in a test of its own so ordering is a dependency
rather than a convention: no mutation can be judged before every proof test has been shown green
on the unmutated tree. Without it, "the named test went red" is satisfied just as well by a proof
test that was already broken, and reporting that as a verified mutation is the failure mode this
whole file exists to remove.
"""
sb = build_sandbox(tmp_path_factory.mktemp("mutation-sandbox"))
# The sandbox holds what GIT TRACKS, so a proof test in a file that has never been `git add`ed is
# simply absent there and the run below reports "file or directory not found" — accurate, and
# unreadable as a diagnosis. Named here instead, because this is what a developer adding a guard
# hits first.
missing = sorted({m.node_id.split("::")[0] for m in MUTATIONS if not (sb / m.node_id.split("::")[0]).is_file()})
assert not missing, (
f"these proof files are not in the sandbox: {missing}. The sandbox is derived from "
"`git ls-files`, so an unstaged new file is not in it — `git add` it and re-run."
)
result = run_pytest(sb, [m.node_id for m in MUTATIONS])
assert result.returncode == 0, (
"the proof tests named by the inventory are NOT green on an unmutated copy of this "
"repository, so nothing below can distinguish 'the mutation was noticed' from 'the test was "
f"already red'. Fix them first.\n{result.stdout[-4000:]}{result.stderr[-2000:]}"
)
assert "passed" in result.stdout, f"the control run collected nothing: {result.stdout!r}"
try:
yield sb
finally:
# ~115 MiB of tracked files plus its own git objects. pytest keeps the last three sessions'
# tmp dirs by default, so leaving it costs a third of a gigabyte on a developer machine that
# runs this a few times.
shutil.rmtree(sb, ignore_errors=True)
def test_the_reset_restores_the_BASELINE_even_after_a_proof_COMMITS(sandbox):
"""One sandbox serves every mutation, so the reset has to be a reset to a fixed point.
`git reset --hard` with no argument resets to whatever HEAD currently is. A proof test that
commits inside the sandbox — several drive `git commit` for real — moves HEAD onto a commit
carrying whatever was in the tree at the time, and every later reset would faithfully restore
THAT. The contamination surfaces as an unrelated red several mutations further on, which is the
hardest kind of harness defect to attribute.
"""
subject = sandbox / "docs" / "guard-inventory.md"
baseline = subject.read_text()
subject.write_text(baseline + "\n<!-- planted, then COMMITTED -->\n")
_git(sandbox, "add", "-A")
_git(sandbox, "commit", "-qm", "a proof test committing inside the sandbox")
assert subject.read_text() != baseline, "the planted change did not land, so this proves nothing"
reset_sandbox(sandbox)
assert subject.read_text() == baseline, (
"the reset restored the sandbox to a commit made DURING a mutation rather than to the "
"pristine baseline, so every later verdict is computed against a contaminated tree"
)
# Through `_git`, not a raw `subprocess.run`: an ambient `GIT_DIR` — which a git hook exports,
# and this suite runs from one — would resolve this against the REAL repository and compare two
# commits that have nothing to do with the sandbox. Asserting isolation with an unisolated call
# is the defect this harness exists to catch, one level up.
head = _git(sandbox, "rev-parse", "HEAD").stdout.decode().strip()
assert head == _BASELINES[str(sandbox.resolve())], f"HEAD was left off the recorded baseline commit: {head}"
@pytest.mark.parametrize("mutation", MUTATIONS, ids=lambda m: m.guard)
def test_MUTATION_the_declared_clause_reddens_the_named_proof(sandbox, mutation):
reset_sandbox(sandbox)
verdict = verify_mutation(sandbox, mutation)
assert verdict.ok, f"{mutation.guard}: {verdict.reason}"
_SURVIVORS = tuple(m for m in MUTATIONS if m.granularity == Mutation.DETECTOR)
def test_the_DETECTOR_survivor_set_is_empty_ON_PURPOSE():
"""ANTI-VACUITY for the parametrized proof below, which currently runs over NOTHING.
`@parametrize` over an empty tuple does not fail and does not collect a real case: with pytest's
default `empty_parameter_set_mark=skip` (and this repo sets no ini), it collects ONE placeholder
reported as `SKIPPED … got empty parameter set`. Measured. In a 1500-test run that is one `s`
among the dots — not a false green, but indistinguishable from coverage unless someone reads the
skip list, which is the same reason `test_the_husky_launched_population_is_not_empty` exists in
`test_hook_fire_log.py`.
`_SURVIVORS` went empty when ersatztv#891 regraded the last `DETECTOR` row to `CLAUSE`. TWO
things are consequently unexercised, enumerated rather than counted:
1. the survivor runner below, `test_every_SURVIVING_clause_mutation_still_does`; and
2. the `unevidenced` arm of
`test_every_entry_declares_a_known_granularity_and_DETECTOR_entries_CARRY_their_survivor`,
which filters on `granularity == DETECTOR`.
Both self-arm the moment an entry is graded `DETECTOR`. `verify_mutation`'s "still PASSED"
verdict path is NOT in this list: it is not DETECTOR-gated and fires for any declared mutation
that fails to redden, and
`test_an_INERT_mutation_is_REPORTED_rather_than_passed` drives it every run — measured by
retargeting its diagnostic string and watching that test go red.
This test states the expected size, so the day someone adds a `DETECTOR` entry it goes red and
names the lines to update; the empty set becomes a decision rather than a skip nobody reads.
"""
assert len(_SURVIVORS) == 0, (
f"a DETECTOR entry exists again ({[m.guard for m in _SURVIVORS]}), so the survivor runner "
"below is no longer unexercised. Update this expectation — and note that the machinery it "
"drives has had no coverage since ersatztv#891, so exercise it deliberately rather than "
"assuming it still works."
)
@pytest.mark.parametrize("mutation", _SURVIVORS, ids=lambda m: m.guard)
def test_every_SURVIVING_clause_mutation_still_does(sandbox, mutation):
"""The DETECTOR grade, executed rather than recited.
A finer mutation that has since STARTED reddening the proof test means the guard now admits
clause-level proof and the entry should be regraded — the coarse grade would otherwise persist as
an excuse long after the reason for it went away. Failing here is therefore good news; it just
has to be acted on.
"""
reset_sandbox(sandbox)
finer = replace(
mutation,
clause=mutation.survived_clause,
replacement=mutation.survived_replacement,
survived_clause="",
survived_replacement="",
)
verdict = verify_mutation(sandbox, finer)
assert not verdict.ok, (
f"{mutation.guard} is graded DETECTOR because {mutation.survived_clause!r} was tried and left "
"the proof test green — but it reddens it now. Regrade the entry to CLAUSE with that mutation."
)
assert "still PASSED" in verdict.reason, (
f"the survivor did not survive for the recorded reason — it failed with: {verdict.reason}"
)
# ------------------------------------------------------------------------------------------------
# THIS GUARD'S OWN MUTATION PROOF — the redness clause, disarmed, on a sandbox of two files
# ------------------------------------------------------------------------------------------------
EXIT_STATUS_CLAUSE = " if result.returncode != 1:"
DIAGNOSTIC_CLAUSE = " if mutation.expect not in diagnostic:"
LIB = REPO_ROOT / "scripts" / "tests" / "mutation_harness_lib.py"
def _inert_sandbox(tmp_path: Path, body: str = " assert True\n") -> tuple[Path, Mutation]:
"""A minimal synthetic sandbox holding one test file, whose outcome is fixed by `body`.
Deliberately not a copy of the repo: the subject here is `verify_mutation`'s verdict, and a real
sandbox would cost four seconds to prove something about two lines of control flow.
"""
tests = tmp_path / "scripts" / "tests"
tests.mkdir(parents=True)
(tests / "test_inert.py").write_text(f"# INERT MARKER\n\n\ndef test_ok():\n{body}")
return tmp_path, Mutation(
guard="inert",
target="scripts/tests/test_inert.py",
clause="# INERT MARKER",
replacement="# INERT MARKER, CHANGED",
proof="test_inert.py::test_ok",
granularity=Mutation.CLAUSE,
# A legal expectation, not the empty string: an empty one is a shape the manifest forbids,
# so a fixture relying on it would be proving something about a configuration that cannot
# ship. Each caller that needs a different one passes it through `replace`.
expect="a diagnostic no run of this fixture produces",
why="an inert mutation: the named test cannot notice it",
)
def _lib_with(tmp_path: Path, clause: str, replacement: str, label: str):
"""Import a copy of the library with one clause replaced.
The copy is registered in `sys.modules` before execution: `@dataclass` resolves a string
annotation through `sys.modules[cls.__module__]`, so an unregistered module raises AttributeError
on the first dataclass it defines rather than on anything to do with the clause.
"""
source = LIB.read_text()
assert source.count(clause) == 1, (
f"the {label} has moved or been reworded; RETARGET this mutation rather than loosening the "
"match — and update the mutation_manifest entry for this file, which names the same string"
)
path = tmp_path / f"mutant_{label.replace(' ', '_')}.py"
path.write_text(source.replace(clause, replacement, 1))
spec = importlib.util.spec_from_file_location(path.stem, path)
mutant = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = mutant
try:
spec.loader.exec_module(mutant)
finally:
sys.modules.pop(spec.name, None)
return mutant
def test_an_INERT_mutation_is_REPORTED_rather_than_passed(tmp_path):
"""The behavioural half. A harness that cannot tell a real disarm from a comment edit would
report every row verified on any tree at all."""
sb, inert = _inert_sandbox(tmp_path)
verdict = verify_mutation(sb, inert)
assert not verdict.ok, "a mutation the named test cannot possibly notice was reported as verified"
assert "still PASSED" in verdict.reason, verdict.reason
def test_a_red_for_the_WRONG_REASON_is_not_accepted(tmp_path):
"""The gate `expect` exists for. Pytest reports an ordinary exception exactly as it reports a
failed assertion, so a mutation that CRASHES the proof test looks identical to one it detected.
A verdict that cannot tell those apart certifies rows on evidence about nothing."""
sb, inert = _inert_sandbox(tmp_path, " raise RuntimeError('an unrelated crash')\n")
crashing = replace(inert, expect="the diagnostic this row is supposed to produce")
verdict = verify_mutation(sb, crashing)
assert not verdict.ok, "a red with nothing to do with the declared diagnostic was accepted"
assert "NOT with the declared diagnostic" in verdict.reason, verdict.reason
def test_MUTATION_disarming_the_DIAGNOSTIC_gate_accepts_a_red_for_the_wrong_reason(tmp_path):
"""The clause-level proof, on the newer of the two gates every verdict passes through.
Disarm the check that the failure carries the declared diagnostic, and the crashing case above
must start reporting as verified. If it does not, that rejection is coming from somewhere other
than the clause, and the test above proves nothing about it.
"""
sb, inert = _inert_sandbox(tmp_path, " raise RuntimeError('an unrelated crash')\n")
crashing = replace(inert, expect="the diagnostic this row is supposed to produce")
assert not verify_mutation(sb, crashing).ok, (
"the UNMUTATED verdict already accepted it, so the mutant proves nothing"
)
mutant = _lib_with(tmp_path, DIAGNOSTIC_CLAUSE, " if False:", "diagnostic gate")
assert mutant.verify_mutation(sb, crashing).ok, (
"the diagnostic gate was replaced with a constant and a red for an unrelated reason was "
"STILL rejected, so the verdict does not hang on the clause that reads it"
)
def test_MUTATION_disarming_the_EXIT_STATUS_gate_accepts_a_run_that_NEVER_RAN_A_TEST(tmp_path):
"""The same proof for the older gate, and it has to be built carefully to isolate it.
A GREEN run cannot serve: it produces no exception output, so only an EMPTY expectation would
reach the status gate — and an empty expectation is a shape the manifest forbids, which would
make this a proof about a configuration that cannot ship. Instead the sandbox's test file fails
at IMPORT: pytest exits non-1 (nothing was collected, so nothing ran) while still printing the
exception, so a legal non-empty expectation matches and the exit status is the ONLY thing
rejecting it. That is the case this gate exists for — a proof ref that no longer names a
collectable test must not read as a guard going red.
"""
boom = "a deliberate import-time failure, which is not a test result"
sb, inert = _inert_sandbox(tmp_path)
(sb / "scripts" / "tests" / "test_inert.py").write_text(f"# INERT MARKER\nraise RuntimeError({boom!r})\n")
uncollectable = replace(inert, expect=boom)
verdict = verify_mutation(sb, uncollectable)
assert not verdict.ok, "a run in which no test executed was accepted as a guard going red"
assert len(uncollectable.expect) >= 20, "the expectation must be one the manifest would accept"
mutant = _lib_with(tmp_path, EXIT_STATUS_CLAUSE, " if False:", "exit status gate")
assert mutant.verify_mutation(sb, uncollectable).ok, (
"the exit-status gate was replaced with a constant and a run that never executed a test was "
"still rejected, so the verdict does not hang on the status it reads"
)
def test_a_clause_that_no_longer_OCCURS_ONCE_is_reported_rather_than_applied(tmp_path):
"""The failure paths, driven directly, because this is where a harness quietly stops harnessing.
A clause that has been reworded away, or that now matches a second site, must produce a verdict
naming the problem. Silently replacing nothing — or replacing the wrong site — would leave every
row in this file reporting verified while mutating something nobody declared.
"""
sb, inert = _inert_sandbox(tmp_path)
gone = replace(inert, clause="# A CLAUSE THAT IS NOT THERE")
assert not verify_mutation(sb, gone).ok
assert "occurs 0 times" in verify_mutation(sb, gone).reason
(sb / "scripts" / "tests" / "test_inert.py").write_text(
"# INERT MARKER\n# INERT MARKER\n\n\ndef test_ok():\n assert True\n"
)
assert "occurs 2 times" in verify_mutation(sb, inert).reason
absent = replace(inert, target="scripts/tests/no_such_file.py")
assert "does not exist in the sandbox" in verify_mutation(sb, absent).reason
def test_the_sandbox_is_left_UNCHANGED_by_a_verdict(tmp_path):
"""One sandbox serves every mutation, so a verdict that leaves its edit behind would make each
result a function of the ones before it."""
sb, inert = _inert_sandbox(tmp_path)
subject = sb / "scripts" / "tests" / "test_inert.py"
before = subject.read_text()
verify_mutation(sb, inert)
assert subject.read_text() == before, "verify_mutation left its mutation in the sandbox"