fix(887): two defects the FIX introduced, found by attacking it rather than reading it

Both were measured, not reasoned, and both are the shape this repo keeps recording — the
fix round introducing an adjacent defect, and a unit test using a simpler input shape
than the real file has.

`npm test -- --run && echo ok || true` reported NO suppression. `&&`/`||` chain across a
whole list, so when the suite fails the `&&` right-hand side is skipped and the `||`
right-hand side runs: the list exits 0 and the suite's failure is swallowed even though
the `||` is not adjacent to it. The detector looked only at the separator IMMEDIATELY
after the suite segment. It is now scoped to the `;`-delimited list, which also catches a
backgrounded `npm test &` (status never awaited) and `( npm test ) || true`. A `;` ends
the list and resets, so `npm test; other || true` stays clean — that `||` is about the
other command.

`--exclude 2 > log` reported `['--exclude']`, losing the filter's own value: stripping
redirections as a PRE-PASS let the file-descriptor rule claim the `2` before the flag
could. Redirections are now consumed inside the walk, after flag values are taken.

The mutant battery grew from 17 to 24 and is 0-missed. The `docs/guard-inventory.md` row
now states the count and, explicitly, the grading: exactly ONE of the 24 is declared in
`mutation_manifest.py` and re-executed every suite; the other 23 were witnessed by hand
and are NOT standing. That is the same footing `pageSizeCallSites.guard.test.ts` states
for its nine, and saying so is the difference between evidence for the reach and a claim
of a per-run proof.

Refs: #887
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
This commit is contained in:
2026-08-30 13:37:58 +02:00
co-authored by Claude Opus 5
parent a4df8f7958
commit 090a29db3d
2 changed files with 49 additions and 36 deletions
@@ -461,35 +461,14 @@ def suite_invocations(text: str, scripts: Mapping[str, list[str]] | None = None)
return found
def _strip_redirections(args: list[str]) -> list[str]:
"""Drop redirection operators, their targets, and any file descriptor written before them.
`2>&1` tokenises as `['2', '>&', '1']`, so the descriptor and the target are bare tokens that
would otherwise both be reported as positional spec filters.
"""
kept: list[str] = []
index = 0
while index < len(args):
argument = args[index]
if _REDIRECTION.match(argument):
index += 2 # the operator and its target
continue
if (
argument.isdigit()
and index + 1 < len(args)
and _REDIRECTION.match(args[index + 1])
and not args[index + 1][:1].isdigit()
):
index += 3 # descriptor, operator, target
continue
kept.append(argument)
index += 1
return kept
def _narrowing_in(args: list[str]) -> list[str]:
"""The subset of `args` that restricts WHICH specs run."""
args = _strip_redirections(args)
"""The subset of `args` that restricts WHICH specs run.
Redirections are consumed HERE rather than in a pre-pass, so a flag's value is claimed before the
file-descriptor rule can see it. As a pre-pass, `--exclude 2 > log` lost the `2`: the digit was
read as the descriptor of the following `>` and dropped with it, and the fault message then named
a filter without its value. Measured.
"""
found: list[str] = []
index = 0
while index < len(args):
@@ -497,6 +476,17 @@ def _narrowing_in(args: list[str]) -> list[str]:
index += 1
if argument == "--":
continue
if _REDIRECTION.match(argument):
index += 1 # the operator's target
continue
if (
argument.isdigit()
and index < len(args)
and _REDIRECTION.match(args[index])
and not args[index][:1].isdigit()
):
index += 2 # the operator and its target; `argument` was its descriptor
continue
base = argument.split("=", 1)[0]
if base in NARROWING_FLAGS:
found.append(argument)
@@ -551,6 +541,13 @@ def failure_suppressions(command: str, scripts: Mapping[str, list[str]], depth:
tokens = tokens[1:] if tokens[0] == "RUN" else []
current: list[str] = []
# Whether the SUITE has run earlier in this `;`-delimited list. `&&`/`||` chain across a
# whole list, so `npm test && echo ok || true` runs `true` when the suite fails and exits 0 —
# the suite's failure is swallowed even though the `||` does not sit immediately after it.
# Looking only at the NEXT separator missed that; measured. A `;` (or a newline, since each
# line is walked separately) ends the list and resets, because a `||` in the next list is
# about the next command.
suite_in_list = False
for word in [*tokens, ";"]:
if word not in SEPARATORS:
current.append(word)
@@ -559,17 +556,21 @@ def failure_suppressions(command: str, scripts: Mapping[str, list[str]], depth:
head = _strip_prefixes(current)
if head[:2] == ["set", "+e"]:
found.append("`set +e` disarms the step's own `set -e`")
if suite_args(current, scripts) is not None and word in {"||", "|"}:
found.append(
"a trailing or-true swallows the exit status"
if word == "||"
else "a pipeline reports the LAST command's status"
)
if suite_args(current, scripts) is not None:
suite_in_list = True
if word == "|":
found.append("a pipeline reports the LAST command's status")
if len(head) >= 3 and head[0] in SHELLS:
for offset, inner in enumerate(head[1:-1], start=1):
if inner.startswith("-") and not inner.startswith("--") and "c" in inner.lstrip("-"):
found.extend(failure_suppressions(head[offset + 1], scripts, depth + 1))
break
if word == "||" and suite_in_list:
found.append("an `||` later in the same list swallows the exit status")
if word == "&" and suite_in_list:
found.append("`&` backgrounds the command, so its status is never awaited")
if word == ";":
suite_in_list = False
current = []
return found
@@ -1041,10 +1042,19 @@ def test_a_SUPPRESSED_failure_is_advisory_by_another_spelling() -> None:
assert failure_suppressions("npm test -- --run || true", scripts)
assert failure_suppressions("npm test -- --run | tee /tmp/spa.log", scripts)
assert failure_suppressions("set +e\nnpm test -- --run", scripts)
# A `||` LATER in the same list still swallows it: if the suite fails, the `&&` right-hand side
# is skipped and the `||` right-hand side runs, so the list exits 0. Measured missed before the
# detector became list-scoped rather than next-separator-scoped.
assert failure_suppressions("npm test -- --run && echo ok || true", scripts)
assert failure_suppressions("npm test -- --run && npm run build || true", scripts)
assert failure_suppressions("npm test -- --run &", scripts)
assert failure_suppressions("( npm test -- --run ) || true", scripts)
# An ordinary run, and a `||` on something that is NOT the suite, are both clean.
assert failure_suppressions(marker + "npm test -- --run", scripts) == []
assert failure_suppressions("npm run lint && npm test -- --run", scripts) == []
assert failure_suppressions("npm run lint || true\nnpm test -- --run", scripts) == []
# A `;` ends the list, so a `||` after it is about the NEXT command, not the suite.
assert failure_suppressions("npm test -- --run; npm run other || true", scripts) == []
def test_a_REDIRECTION_is_not_a_spec_filter() -> None:
@@ -1059,8 +1069,11 @@ def test_a_REDIRECTION_is_not_a_spec_filter() -> None:
assert narrowing_arguments("npm test -- --run >> test.log", scripts) == []
assert narrowing_arguments("npm test -- --run 2>&1", scripts) == []
assert narrowing_arguments("npm test -- --run > test.log 2>&1", scripts) == []
# A real filter beside a redirection is still reported.
# A real filter beside a redirection is still reported, INCLUDING a numeric value: stripping
# redirections as a pre-pass ate the `2` in `--exclude 2 > log` by reading it as a descriptor.
assert narrowing_arguments("npm test -- --run --exclude a.ts > log", scripts) == ["--exclude", "a.ts"]
assert narrowing_arguments("npm test -- --run --exclude 2 > log", scripts) == ["--exclude", "2"]
assert narrowing_arguments("npm test -- --run 2 --exclude a.ts", scripts) == ["2", "--exclude", "a.ts"]
def test_the_flags_that_move_the_COLLECTED_SET_count_as_filters() -> None: