fix(876): sweep session narrative out of hooks, workflows, scripts, tests and code comments; grow the detector to the process corpus
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
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
`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
This commit is contained in:
@@ -12,9 +12,9 @@
|
||||
# no forcing function was the one that got defaulted. A check that runs beats a rule you must remember
|
||||
# (the same reasoning as pretooluse-bom-guard.sh).
|
||||
#
|
||||
# SCOPE — gate EVERY dispatch that names no model, not just implementer-looking ones. The first cut
|
||||
# tried to be clever: it fired only when the prompt text matched implementer signals (`git commit`,
|
||||
# `worktree`, `fixes #`…). Review of that version (#583) confirmed the heuristic both over- and
|
||||
# SCOPE — gate EVERY dispatch that names no model, not just implementer-looking ones. A NARROWER
|
||||
# cut was TRIED AND REJECTED: it fired only when the prompt text matched implementer signals (`git
|
||||
# commit`, `worktree`, `fixes #`…). Measured (#583), the heuristic both over- and
|
||||
# under-fired — a read-only recon brief mentioning "worktree" nagged, while "author the change and
|
||||
# open a PR", "land this on the branch" and "make the changes and commit them" all sailed through
|
||||
# silently, i.e. it missed the exact case it existed to catch. Prompt prose is not a reliable signal
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
# "re-review the fix commit, not just the initial PR diff").
|
||||
#
|
||||
# EVERY ONE OF THOSE IS A SNAPSHOT, taken when the merge tool is called. The window is SMALL for an
|
||||
# immediate merge and UNBOUNDED for a scheduled one. Small is not zero, and this comment used to say
|
||||
# "sound", which is the overclaim ersatztv#778 removed: this hook returns `allow` and a SEPARATE call
|
||||
# immediate merge and UNBOUNDED for a scheduled one. Small is not zero, and calling this gate
|
||||
# "sound" is the overclaim ersatztv#778 removed: this hook returns `allow` and a SEPARATE call
|
||||
# performs the merge, so a push can still land in between. The merge API accepts an optional
|
||||
# `head_commit_id` that would make that call a true compare-and-set; a PreToolUse hook cannot add an
|
||||
# argument, only refuse without one. With merge_when_checks_succeed, Gitea merges
|
||||
@@ -186,11 +186,11 @@ fi
|
||||
# posted before ersatztv#632 and gets NO opinion, rather than denying every in-flight PR the day
|
||||
# this lands. The window closes on its own — verdicts are per-head and short-lived, so every verdict
|
||||
# posted after this carries the field.
|
||||
# "Could not check" is a THIRD outcome, distinct from both "matches" and "no base recorded". Cold
|
||||
# review found the first draft collapsing it into the latter: an unreadable status response yielded
|
||||
# an empty `recorded_base`, which took the graceful-adoption path and skipped validation silently —
|
||||
# "Could not check" is a THIRD outcome, distinct from both "matches" and "no base recorded".
|
||||
# Collapsing it into the latter is a false-open: an unreadable status response yields
|
||||
# an empty `recorded_base`, which takes the graceful-adoption path and skips validation silently —
|
||||
# after which a later, successful status read could still auto-grant. A transient failure would then
|
||||
# have produced a "merge gate: satisfied" message for a comparison that never happened. Every
|
||||
# produce a "merge gate: satisfied" message for a comparison that never happened. Every
|
||||
# unreadable input here therefore falls through to a human (`ask`), never to silence.
|
||||
# RE-READ THE BASE HERE, ONCE, FOR EVERY PATH BELOW (ersatztv#778).
|
||||
#
|
||||
@@ -207,8 +207,8 @@ fi
|
||||
# of `process.check-and-use-pins-a-version`, so the guard enforcing that rule must not break it.
|
||||
#
|
||||
# This re-read first landed inside the scheduled-auto-merge branch only, which fixed the branch-
|
||||
# protection lookup and left the #632 retarget DETECTION below still reading the stale snapshot. Cold
|
||||
# review demonstrated the consequence with this repo's own fixture: scheduled+retarget denied, while
|
||||
# protection lookup and left the #632 retarget DETECTION below still reading the stale snapshot.
|
||||
# Measured on this repo's own fixture: scheduled+retarget denied, while
|
||||
# immediate+retarget auto-GRANTED. That is the twin-missed shape — a fix applied to the path where it
|
||||
# was noticed — so the re-read is hoisted above every consumer rather than duplicated into each.
|
||||
prjson_now=$(gq "repos/$owner/$repo/pulls/$pr")
|
||||
@@ -501,7 +501,7 @@ else
|
||||
# performs no matching and knows nothing about precedence, so a 200 from it means only "a rule
|
||||
# with this NAME exists and lists this context", never "this context is required on this branch".
|
||||
#
|
||||
# It was used first, with the list consulted only on a 404, and cold review found what that left
|
||||
# It was used first, with the list consulted only on a 404, and that design left a false-open
|
||||
# behind: the precedence argument below guarded the 404 path while the 200 path — the one this
|
||||
# repo actually takes — granted without it. Given a rule `main` requiring `review-verdict/h10` and
|
||||
# a rule `m*` with better Priority that does not, Gitea applies `m*`, and the by-name hit on
|
||||
@@ -585,8 +585,8 @@ else
|
||||
undecidable) decide ask "H6/H10 merge gate: no branch-protection rule on this repo governs '$base_ref' decidably — a GLOB rule could govern it, or two rule names fold-equal, or a name is non-ASCII. This hook deliberately does not reimplement Gitea's glob matcher, so whether 'review-verdict/h10' is required on this base cannot be derived here (ersatztv#778). Confirm it in the repo's branch-protection settings, or merge immediately instead of scheduling." ;;
|
||||
none) bp_code=nomatch; bp="" ;;
|
||||
# A DECLARED class of the classifier's contract (ersatztv#859), with its OWN sentinel — not
|
||||
# merely its own arm. The first draft gave it an arm that set `unreadable-rules`, the same value
|
||||
# the catch-all sets, and that arm was measured to be a no-op: deleting it left the WHOLE suite
|
||||
# merely its own arm. Giving it an arm that set `unreadable-rules`, the same value
|
||||
# the catch-all sets, was measured to be a no-op: deleting that arm left the WHOLE suite
|
||||
# green, because nothing downstream could tell the two apart. An arm no observation can
|
||||
# distinguish is not a fix, it is a comment with syntax. (The invariant is "no test reddens",
|
||||
# not a test count — a count goes stale the next time anyone adds one.)
|
||||
@@ -770,7 +770,7 @@ esac
|
||||
# `else` (mwcs = true) and never executes on an immediate merge, which is the common case; hanging
|
||||
# the freshness check off it would fire it only when an auto-merge is armed. This file already
|
||||
# records that exact defect one section up — the base re-read "first landed inside the
|
||||
# scheduled-auto-merge branch only", and cold review found scheduled+retarget denied while
|
||||
# scheduled-auto-merge branch only", with scheduled+retarget denied while
|
||||
# immediate+retarget auto-GRANTED. Same shape, so it is not repeated here.
|
||||
#
|
||||
# It reads `main` (the branch the snapshot names), NOT `$base_ref`. That is a DIFFERENT question
|
||||
|
||||
@@ -128,8 +128,8 @@ env:
|
||||
# serves only its eight `actions/checkout` steps. Note this file needs no `packages:` unit for that
|
||||
# same reason: the `container:` blocks carry explicit `credentials:`.
|
||||
# (Sites above are named by JOB, not by line number: this file is ~1150 lines, so any edit above a
|
||||
# citation silently invalidates it — which is how the first version of this comment went stale two
|
||||
# lines after it was written.)
|
||||
# citation silently invalidates it — a line-number citation here has gone stale within two lines
|
||||
# of being written.)
|
||||
permissions:
|
||||
code: read
|
||||
|
||||
@@ -727,9 +727,9 @@ jobs:
|
||||
# runs at all — the image is not built, let alone pushed. Fail-closed by dependency, not by
|
||||
# assertion.
|
||||
#
|
||||
# WHY IT RUNS THE REAL PYTEST rather than a bespoke scanner. The first cut of #767 hand-parsed the
|
||||
# workflow YAML in stdlib Python, to avoid provisioning PyYAML on `build`'s bare runner. Two
|
||||
# independent reviews found ~10 false NEGATIVES in that parser within one round (flow mappings
|
||||
# WHY IT RUNS THE REAL PYTEST rather than a bespoke scanner. A stdlib hand-parser of the workflow
|
||||
# YAML was TRIED AND REJECTED in #767 (its appeal: no PyYAML to provision on `build`'s bare
|
||||
# runner). That parser had ~10 false NEGATIVES, all found at once (flow mappings
|
||||
# `{run: …}`, a quoted `"run":` key, aliases, multiline quoted scalars) — i.e. it was strictly
|
||||
# WEAKER than the check it was meant to backstop, in the one direction that matters for a security
|
||||
# gate. Running the existing PyYAML-based test needs no second implementation of "what is a `run:`
|
||||
@@ -785,16 +785,16 @@ jobs:
|
||||
"${GITHUB_WORKSPACE:-.}/scripts/ci-step-ran.sh" mark ban
|
||||
PYTHONPATH=. python3 -m pytest scripts/tests/test_ci_dropped_step_guard.py scripts/tests/test_ci_release_path_scan_job.py -q
|
||||
# THE POSITIVE CONTROL, and it is deliberately NOT a test (ersatztv#767). The step above proves
|
||||
# the ban HOLDS; it cannot prove the ban would NOTICE. Review disarmed the entire gate with one
|
||||
# repo-root `pytest.ini` (`addopts = -k "not delimiter_banned"`) or `conftest.py`
|
||||
# (`pytest_collection_modifyitems`), which deselects the ban test and every test guarding it,
|
||||
# the ban HOLDS; it cannot prove the ban would NOTICE. DEMONSTRATED: ONE repo-root `pytest.ini`
|
||||
# (`addopts = -k "not delimiter_banned"`) or `conftest.py` (`pytest_collection_modifyitems`)
|
||||
# disarms the entire gate, deselecting the ban test and every test guarding it,
|
||||
# leaving all jobs green with a delimiter sitting in `Smoke`. Nothing inside pytest can be
|
||||
# trusted to catch that, because pytest's own configuration outranks it.
|
||||
#
|
||||
# So this poisons the checked-out workflow, re-runs the SAME command, and fails the job if it
|
||||
# PASSES. It runs in the real checkout — an isolated copy does not inherit the repo-root config
|
||||
# a disarm would live in, which made the first version of this script report healthy while the
|
||||
# job's real invocation was deselected. The workflow file is restored by an EXIT trap.
|
||||
# a disarm would live in, so a check run in a copy reports healthy while the
|
||||
# job's real invocation is deselected. The workflow file is restored by an EXIT trap.
|
||||
- name: Prove the ban would DETECT a delimiter (ersatztv#767)
|
||||
run: |
|
||||
"${GITHUB_WORKSPACE:-.}/scripts/ci-step-ran.sh" mark selfcheck
|
||||
|
||||
@@ -540,9 +540,9 @@ jobs:
|
||||
# is installed, but the one-shot WRITE path `migrate_decisions_split.py` uses PyYAML by
|
||||
# design — and `test_migration_equivalence.py` imports that module, so the suite needs it.
|
||||
# `pytest` and `yaml` are the complete third-party set, established by an AST import scan over
|
||||
# all of scripts/ rather than by reading the files that seemed relevant: the first cut of this
|
||||
# job claimed "pure stdlib", passed locally on a machine that happened to have PyYAML, and
|
||||
# went red in CI on a collection error.
|
||||
# all of scripts/ rather than by reading the files that seemed relevant — reading only those
|
||||
# yields "pure stdlib", a claim that passes locally on a machine that happens to have PyYAML
|
||||
# and goes red in CI on a collection error.
|
||||
- name: Install test dependencies
|
||||
run: python3 -m pip install --disable-pip-version-check --quiet pytest pyyaml
|
||||
# jq gets its OWN step because its VERSION, not merely its presence, is load-bearing
|
||||
|
||||
@@ -89,8 +89,8 @@ name: Review verdict
|
||||
# What IS established on 1.27.1, and stated no wider than it was measured: `pull_request_target`
|
||||
# fires for THIS workflow continuously — 210 such runs across 189 distinct head shas, counting only
|
||||
# runs started strictly after the 2026-08-05 upgrade (measured 2026-09-02). The WINDOW is part of
|
||||
# the claim, and an earlier draft got it wrong by citing the all-time REPO-WIDE figure: 258 runs /
|
||||
# 233 shas = 44 before the upgrade date + 4 ON it + 210 after. Citing 258 here would have imported
|
||||
# the claim, and the all-time REPO-WIDE figure is the WRONG window: 258 runs /
|
||||
# 233 shas = 44 before the upgrade date + 4 ON it + 210 after. Citing 258 here would import
|
||||
# 1.25.4 evidence into a 1.27.1 sentence. This workflow's own all-time count is 250; the difference
|
||||
# is 8 runs from three since-deleted probe workflows, all pre-upgrade — which is why every one of
|
||||
# the 210 post-upgrade runs is this file.
|
||||
@@ -285,7 +285,7 @@ jobs:
|
||||
# exactly one event — the next run saw a machine-written `pending`, re-derived it, and
|
||||
# posted `success` again, with its own freshly-taken high-water mark now ABOVE the human
|
||||
# row, so the post-write check stayed silent and the rejection went green a second time.
|
||||
# Found by cold review. Refusing here can only ever withhold an exemption, never grant one.
|
||||
# Refusing here can only ever withhold an exemption, never grant one.
|
||||
REPAIR_DESC="Human verdict raced this exemption write — re-post the verdict"
|
||||
# THE SECOND SENTINEL, and the two are NOT interchangeable (ersatztv#849). `REPAIR_DESC`
|
||||
# asserts a fact — a human verdict existed and this job's write buried it. Most of the
|
||||
@@ -385,8 +385,8 @@ jobs:
|
||||
# Package Management versions live in `Directory.Packages.props`, so a `.csproj` edit
|
||||
# attributed to Renovate is anomalous by construction. Such a PR is not blocked, it simply
|
||||
# needs a real verdict, which is the correct handling for a PR carrying source changes.
|
||||
# NOTE the npm manifests are deliberately ABSENT. An earlier draft included
|
||||
# `web/package.json` / `web/package-lock.json` "so a first SPA bump cannot deadlock". That was
|
||||
# NOTE the npm manifests are deliberately ABSENT. Including
|
||||
# `web/package.json` / `web/package-lock.json` "so a first SPA bump cannot deadlock" would be
|
||||
# a self-inflicted code-execution vector for zero benefit: `renovate.json` sets
|
||||
# `enabledManagers: ["nuget", "github-actions", "dockerfile"]`, so Renovate does not manage npm
|
||||
# in this repo at all, while `package.json` carries `scripts` that CI EXECUTES (`npm ci`,
|
||||
@@ -416,7 +416,7 @@ jobs:
|
||||
# not prose about the project — they are the documents that DEFINE the completion protocol,
|
||||
# the merge-consent convention and the H10 rule itself. `.claude/` being protected while the
|
||||
# file that specifies what `.claude/` enforces was docs-only-exempt is the same
|
||||
# self-exemption the header rules out, one directory over. Found by cold review
|
||||
# self-exemption the header rules out, one directory over. Measured
|
||||
# (ersatztv#751): driving the real classify body with a lone `CLAUDE.md` change produced
|
||||
# `review-verdict/h10=success`, "Exempt: docs-only change (no code, no protected path)".
|
||||
#
|
||||
@@ -481,9 +481,9 @@ jobs:
|
||||
# every time. Measured 2026-08-29, this repo puts 8 contexts on a `main` head. That case
|
||||
# already stalled before this change — with an ABSENT required check, which reads as "not
|
||||
# reviewed yet" — so what changes is that the stall now says why.
|
||||
# THE RETURN VALUE IS THE WHOLE POINT, and the first version did not have one: the `else`
|
||||
# branch ended with a successful `echo`, so the function returned 0 after BOTH POSTs failed
|
||||
# and its fence caller took the `exit 0` beside it as though the head had been marked. An
|
||||
# THE RETURN VALUE IS THE WHOLE POINT. Without one the `else`
|
||||
# branch ends with a successful `echo`, so the function returns 0 after BOTH POSTs failed
|
||||
# and its fence caller takes the `exit 0` beside it as though the head had been marked. An
|
||||
# explicit `return` per arm, and every caller acts on it.
|
||||
replace_unknown_state() { # $1 = the ::error:: naming what could not be established
|
||||
echo "::error::$1"
|
||||
@@ -508,10 +508,10 @@ jobs:
|
||||
exit 1
|
||||
}
|
||||
|
||||
# DEFINED HERE, BEFORE ANY USE. An earlier round defined these AFTER the classification
|
||||
# chain that calls them, so `count_matching` was `command not found` on every run, the
|
||||
# PROTECTED branch silently never fired, and three "protected path" tests still passed —
|
||||
# they reached `pending` by another route, so the guard being dead was invisible.
|
||||
# DEFINED HERE, BEFORE ANY USE. Defining them AFTER the classification
|
||||
# chain that calls them makes `count_matching` `command not found` on every run, the
|
||||
# PROTECTED branch silently never fires, and three "protected path" tests still pass —
|
||||
# they reach `pending` by another route, so the guard being dead is invisible.
|
||||
#
|
||||
# HOW THE PATH PREDICATES ARE EVALUATED, and why neither obvious spelling is used.
|
||||
#
|
||||
@@ -559,7 +559,7 @@ jobs:
|
||||
|
||||
|
||||
# --- Is there already a verdict for THIS sha? ----------------------------------------
|
||||
# NOTE the heading no longer says "never overwrite". It cannot promise that: the read below
|
||||
# NOTE this heading does NOT say "never overwrite". It cannot promise that: the read below
|
||||
# and the POST at the end of this job are not atomic, so a human verdict posted in between is
|
||||
# still overwritten. The re-read immediately before the POST narrows that window; it does not
|
||||
# close it. Tracked as ersatztv#706 rather than claimed as solved.
|
||||
@@ -582,8 +582,8 @@ jobs:
|
||||
read_existing_verdict() {
|
||||
local json row rv try
|
||||
# `.statuses` IS `null`, NOT `[]`, ON A HEAD WITH NO STATUSES YET — the same nil-slice
|
||||
# serialization as the timeline terminator, found by cold review of the fix for that one
|
||||
# (ersatztv#751). Measured on this instance: PR #739's head 5fa672e2 returns
|
||||
# serialization as the timeline terminator (ersatztv#751).
|
||||
# Measured on this instance: PR #739's head 5fa672e2 returns
|
||||
# `{"state":"pending","total_count":0,"statuses":null}`. An `array`-only gate read that as
|
||||
# unreadable and took the `exit 1` below, so the job posted NOTHING — fail-closed, but the
|
||||
# user-visible outcome is exactly the one this issue is about: an exempt PR left with no
|
||||
@@ -633,7 +633,7 @@ jobs:
|
||||
st_kind=$(printf '%s' "$json" | jq -r '.statuses | type' 2>/dev/null) || st_kind=""
|
||||
# NUMBER, not `jq -r` text: `jq -r` renders the JSON number 0 and the JSON string "0"
|
||||
# identically, so a schema-corrupted `"total_count": "0"` would satisfy a string compare
|
||||
# (cold review reproduced this). Requiring the type as well pins the accept path to a real
|
||||
# (measured, not assumed). Requiring the type as well pins the accept path to a real
|
||||
# numeric zero.
|
||||
st_total=$(printf '%s' "$json" | jq -r 'if (.total_count | type) == "number" then (.total_count | tostring) else "x" end' 2>/dev/null) || st_total="x"
|
||||
st_ok=no
|
||||
@@ -671,7 +671,7 @@ jobs:
|
||||
replace_unknown_and_die "Could not read existing commit statuses for ${SHA:0:7} (.statuses was '${st_kind:-unparseable}', total_count '${st_total}') after a retry, so any ${CONTEXT} already on this head — including one posted by an account this gate does not accept verdicts from — can neither be read nor re-derived."
|
||||
fi
|
||||
# `// []` so the null case cannot hard-error here under `set -e` once it is accepted above.
|
||||
# TYPE-SAFE, AND A SCHEMA FAILURE REPLACES RATHER THAN DYING (ersatztv#849 round 3).
|
||||
# TYPE-SAFE, AND A SCHEMA FAILURE REPLACES RATHER THAN DYING (ersatztv#849).
|
||||
# `.statuses` being an array was checked; its ELEMENTS were not. A single scalar in that
|
||||
# array makes `select(.context == ...)` hard-error, jq exits 5, and under `set -e` this
|
||||
# unguarded assignment took the step down — BEFORE any of the replacement paths below,
|
||||
@@ -702,7 +702,7 @@ jobs:
|
||||
# direction for a required check, which must withhold a green rather than grant one.
|
||||
unreadable=$(printf '%s' "$json" | jq -r '[(.statuses // [])[] | select(type != "object" or ((.context | type) != "string"))] | length' 2>/dev/null) || unreadable=""
|
||||
case "$unreadable" in ''|*[!0-9]*) unreadable=1 ;; esac
|
||||
# ANY UNREADABLE ELEMENT, whether or not a target row was also found. Round 8 scoped this
|
||||
# ANY UNREADABLE ELEMENT, whether or not a target row was also found. A narrowing scoped it
|
||||
# to "no readable row was found", on the reasoning that a malformed row BESIDE one we did
|
||||
# read is noise. That reasoning does not hold: an element whose `.context` cannot be read
|
||||
# cannot be shown to be a different context, so it may be a mangled rendering of this
|
||||
@@ -758,8 +758,8 @@ jobs:
|
||||
# 736649b3 returned 8 rows keyed id/context/status/creator/description/created_at/
|
||||
# updated_at/url, ids 14..30 ascending. `// ""` so a server that ever stopped sending it
|
||||
# degrades to the pre-existing text comparison rather than to a false "changed".
|
||||
# A WRONG TYPE IS NOT AN ABSENT VALUE (ersatztv#849 round 4). Round 3 type-tested these
|
||||
# four fields and resolved a failure to `""` — which for `.creator` means "no creator",
|
||||
# A WRONG TYPE IS NOT AN ABSENT VALUE (ersatztv#849). Type-testing these
|
||||
# four fields and resolving a failure to `""` means, for `.creator`, "no creator",
|
||||
# i.e. unattributable, i.e. RE-DERIVE. That turned a corrupt row into an exemption
|
||||
# `success` where `origin/main` had died before writing anything: measured, a head
|
||||
# carrying `h10=failure` with `"creator": 7` posts `Exempt: docs-only change` here and
|
||||
@@ -784,9 +784,7 @@ jobs:
|
||||
# `.creator.login` HARD-ERRORS on any non-object creator — the same defect #763 fixed in
|
||||
# the post-write filter, still live on this read. `(.creator | type)` short-circuits it,
|
||||
# so the step is not killed; the wrong TYPE then takes the fault route above rather than
|
||||
# reading as "no creator", which is what made it re-derivable. (An earlier version of
|
||||
# this comment described that re-derivation as the intended behaviour — it was the
|
||||
# regression the block above records.)
|
||||
# reading as "no creator", which is what made it re-derivable.
|
||||
ex_creator=$(printf '%s' "$row" | jq -r --arg f "$SCHEMA_FAULT" 'if (.creator | type) == "object" then (.creator.login // "") elif (.creator | type) == "null" then "" else $f end')
|
||||
ex_desc=$(printf '%s' "$row" | jq -r --arg f "$SCHEMA_FAULT" 'if (.description | type) == "string" then .description elif (.description | type) == "null" then "" else $f end')
|
||||
for _f in "$ex_id" "$ex_state" "$ex_creator" "$ex_desc"; do
|
||||
@@ -797,8 +795,8 @@ jobs:
|
||||
# A `case` prefix test rather than grep: the description is a single short string, and this
|
||||
# removes one more pipeline from a security predicate entirely. The PATTERN is a literal, so
|
||||
# there is no glob-injection concern from $ex_desc.
|
||||
# A human verdict also has to have been formed against THIS base (ersatztv#698, found in
|
||||
# round-4 review). `post-review-verdict.sh` records the base it reviewed in the status
|
||||
# A human verdict also has to have been formed against THIS base (ersatztv#698).
|
||||
# `post-review-verdict.sh` records the base it reviewed in the status
|
||||
# description — `Review-verdict: MERGEABLE @ abc1234 (base: main)` — precisely because
|
||||
# retargeting changes the effective diff without moving the head sha (ersatztv#632).
|
||||
# Without this check the sha-binding is escapable through the HUMAN path rather than the
|
||||
@@ -1002,8 +1000,7 @@ jobs:
|
||||
# triggered (push -> `pull_request_target` run) pairs on PRs #802, #834 and #761 the
|
||||
# `pull_push` event predates its own run's `started_at` by 26-102s, itself a lower bound
|
||||
# since the job runs a checkout and several steps before counting (69s end to end on run
|
||||
# 2385). An earlier version of this comment rested the claim on that margin alone and
|
||||
# called it a deployment property; see `ci.verdict-write-retarget-fence`, which also names
|
||||
# 2385). See `ci.verdict-write-retarget-fence`, which also names
|
||||
# the one window this does NOT cover — a PR's FIRST push is recorded about a second after
|
||||
# creation and fires no `synchronize`, so an `opened` run landing inside it would abstain
|
||||
# with no successor queued.
|
||||
@@ -1135,12 +1132,11 @@ jobs:
|
||||
# instance; the worst case with the per-page retry below is 40 requests and 20
|
||||
# one-second sleeps — every page blipping once and recovering — which is 20s of
|
||||
# sleep on top. MEASURED by executing this function against scripted responses,
|
||||
# not counted by eye: the first draft of this sentence said 19, reasoning that
|
||||
# the last page could not sleep. It can.
|
||||
# not counted by eye: 19 would be the count if the last page could not sleep. It can.
|
||||
#
|
||||
# WALL-CLOCK, which is what the timeout is actually for. An earlier version of this
|
||||
# comment reasoned that only the last page can pay two timeouts, since a page whose
|
||||
# BOTH attempts fail ends the walk, and put the bound at ~335s. That bounds TIMEOUTS,
|
||||
# WALL-CLOCK, which is what the timeout is actually for. A PLAUSIBLE reading is
|
||||
# that only the last page can pay two timeouts, since a page whose BOTH attempts
|
||||
# fail ends the walk, which puts the bound at ~335s. That bounds TIMEOUTS,
|
||||
# not elapsed time: attempt 1 can burn its full 15s and attempt 2 still SUCCEED
|
||||
# slowly, so the page costs ~31s and the walk carries on. The pessimum is therefore
|
||||
# 20 x (15 + 1 + 15) = 620s per walk — ~21min on the exempt path's three walks — and
|
||||
@@ -1161,7 +1157,7 @@ jobs:
|
||||
# count with `rt_ok=yes`. Both re-reads share the blind spot, so the counts agree and
|
||||
# the exemption is granted — the SAME construction as before, at the cap instead of at
|
||||
# page 2, at roughly 10x the price. State that price ONCE and in one unit, because
|
||||
# two earlier drafts of this comment stated it two ways 45 lines apart. The FILTERED
|
||||
# two statements of it in one file drift into disagreement. The FILTERED
|
||||
# BLOCK is 50 rows in both cases — that is the part which must be inline review
|
||||
# comments. What grew is the timeline the attacker has to build around it: ~100 rows
|
||||
# before (a non-empty page 1, then a 50-row block at a 50-aligned offset) against more
|
||||
@@ -1209,8 +1205,8 @@ jobs:
|
||||
# figure that rots: a real PR's timeline always carries at least one event on page 1
|
||||
# (the PR is created by a push, which is itself an event). Spot-checked across
|
||||
# #752/#753/#749/#739/#717, all non-empty; the counts themselves are deliberately
|
||||
# not recorded here because timelines grow and an earlier version of this comment
|
||||
# cited five numbers of which three were stale within days. Trusting a zero count
|
||||
# not recorded here because timelines grow and three of five such
|
||||
# counts went stale within days. Trusting a zero count
|
||||
# from an anomalous first page would mean trusting that no retarget happened on the
|
||||
# strength of a response we cannot explain, so it falls through to `rt_ok=no`, which
|
||||
# withholds the exemption and asks for a human verdict — the safe direction.
|
||||
@@ -1238,8 +1234,8 @@ jobs:
|
||||
# truthiness of its LAST OUTPUT, which is the exit-status subtlety that already bit
|
||||
# this workflow at jq 1.6 (ersatztv#647), and the runner still ships 1.6. A count read
|
||||
# as a VALUE and then range-checked in shell cannot be read two ways, and it matches
|
||||
# every other tally here. (The first draft of this guard used `jq -e`, safely by
|
||||
# accident — the array is known non-empty by the check above, so the empty-input
|
||||
# every other tally here. (Using `jq -e` here would be safe by accident — the
|
||||
# array is known non-empty by the check above, so the empty-input
|
||||
# divergence could not fire — but "safe because of a property three lines up" is
|
||||
# exactly the reasoning the rule exists to retire.)
|
||||
bad=$(printf '%s' "$raw" | jq -r '[.[] | select(type != "object" or (.type | type) != "string")] | length' 2>/dev/null) || bad=""
|
||||
@@ -1272,8 +1268,8 @@ jobs:
|
||||
# Both callers below want the WHOLE history, and a single `?limit=100` read cannot give it:
|
||||
# `limit` clamps to the server-wide `MAX_RESPONSE_ITEMS`, measured at 50.
|
||||
#
|
||||
# BE PRECISE ABOUT WHAT THAT COST, because the issue and an earlier version of this comment
|
||||
# both overstated it. Under the server default (`created_unix DESC`) page 1 holds the NEWEST
|
||||
# BE PRECISE ABOUT WHAT THAT COST, because the issue overstates it.
|
||||
# Under the server default (`created_unix DESC`) page 1 holds the NEWEST
|
||||
# rows, and ids are monotonic with `created_at` (measured). So page 1 already carried the
|
||||
# true maximum id, AND every row newer than the mark — which is the only thing the
|
||||
# post-write check selects on. A single-page read therefore missed a raced verdict only if
|
||||
@@ -1323,7 +1319,7 @@ jobs:
|
||||
# EVERY exit publishes what WAS read, not just the complete ones. `ph_ok` alone says
|
||||
# whether the list is whole; a caller that can still use a partial list must be able to
|
||||
# reach it, and the high-water mark below is exactly such a caller — see the fail-open it
|
||||
# otherwise creates (ersatztv#763, round 2).
|
||||
# otherwise creates (ersatztv#763).
|
||||
while [ "$page" -le 20 ]; do
|
||||
raw=""
|
||||
kind=""
|
||||
@@ -1417,9 +1413,9 @@ jobs:
|
||||
# combined endpoint is latest-per-context (15 rows on each of the four) and the list
|
||||
# endpoint returns every row ever posted (43-63 across them).
|
||||
#
|
||||
# THE MECHANISM, so this is re-derivable without a live PR. An earlier draft of this comment
|
||||
# got it wrong in a way worth recording, because the wrong version is the intuitive one:
|
||||
# it said `creator: null` means `CreatorID == 0`. It does not. At v1.27.1 an Actions job
|
||||
# THE MECHANISM, so this is re-derivable without a live PR. The INTUITIVE version is worth
|
||||
# recording as WRONG, because it is the one a reader will re-derive:
|
||||
# `creator: null` does NOT mean `CreatorID == 0`. At v1.27.1 an Actions job
|
||||
# authenticates as the SYNTHETIC Actions user, `ActionsUserID = -2`
|
||||
# (`models/user/user_system.go`), which is never a row in the `user` table, and
|
||||
# `NewCommitStatus` stores `CreatorID = opts.Creator.ID` — so an Actions-posted status
|
||||
@@ -1522,7 +1518,7 @@ jobs:
|
||||
pre_state=$ex_state
|
||||
pre_creator=$ex_creator
|
||||
pre_desc=$ex_desc
|
||||
# THE ID IS PART OF THE SNAPSHOT (ersatztv#849, round 2). The triple cannot see a
|
||||
# THE ID IS PART OF THE SNAPSHOT (ersatztv#849). The triple cannot see a
|
||||
# REPLACEMENT whose text matches, and the two sentinels are byte-identical by design, so
|
||||
# "another run replaced this row while we classified" was invisible for exactly the rows
|
||||
# where it matters most. Where the server does not send an id both sides are empty and the
|
||||
@@ -1538,7 +1534,7 @@ jobs:
|
||||
# this line — creator, allow-list and description together — is what says so. A separate
|
||||
# `::warning::` annotation was tried and WITHDRAWN: it duplicated all three values from
|
||||
# this line, its only delta was a severity whose rendering could not be probed, and it
|
||||
# produced three defects across three review rounds (a message that read "from 'timothy',
|
||||
# produced three successive defects (a message that read "from 'timothy',
|
||||
# which is NOT in H10_REVIEWERS ('timothy')", an unescaped API value interpolated into a
|
||||
# workflow command, and a test that asserted its absence on a path that exits two guards
|
||||
# earlier and so could never fail). One line that is always right beats two where the
|
||||
@@ -1571,7 +1567,7 @@ jobs:
|
||||
if [ "$ex_unverified" = yes ]; then
|
||||
echo "${CONTEXT} on ${SHA:0:7} carries the unverified-write sentinel from an earlier run — reconciling it against the per-POST status history."
|
||||
page_statuses
|
||||
# THE WITNESS: THE SENTINEL'S OWN ROW MUST BE IN THE HISTORY (ersatztv#849, round 2).
|
||||
# THE WITNESS: THE SENTINEL'S OWN ROW MUST BE IN THE HISTORY (ersatztv#849).
|
||||
# `ex_unverified=yes` means the COMBINED endpoint just returned the sentinel for this
|
||||
# sha, and `/statuses/{sha}` keeps one row per POST — so a complete history that does
|
||||
# NOT contain it, empty ones included, is a response that cannot be true. The post-write
|
||||
@@ -1586,7 +1582,7 @@ jobs:
|
||||
# PERMANENT sentinel. This asks for a SPECIFIC row already known to exist, and failing it
|
||||
# carries the sentinel forward for THIS run only — the next run retries.
|
||||
# THE WITNESS IS THE CURRENT ROW'S ID, not merely a row with the right text
|
||||
# (ersatztv#849 round 3). Matching on description alone is satisfied by an OLDER
|
||||
# (ersatztv#849). Matching on description alone is satisfied by an OLDER
|
||||
# identical sentinel, which is precisely what a fixed point produces: with S1 and a
|
||||
# buried human verdict below the CURRENT S2, a read carrying only S1 satisfies the
|
||||
# witness, `buried` sees nothing, the sentinel clears, and the verdict ends up below the
|
||||
@@ -1596,7 +1592,7 @@ jobs:
|
||||
# `$ex_id` COMES FROM THE COMBINED READ, so it names the row that is current right now.
|
||||
# When the server omits it — not observed on this instance, where every row carries one —
|
||||
# there is nothing to match on and the check degrades to the description, which is the
|
||||
# pre-round-3 behaviour rather than a new hole.
|
||||
# description-only behaviour rather than a new hole.
|
||||
if [ -n "$ex_id" ]; then
|
||||
witness=$(printf '%s' "$ph_rows" | jq -r --arg c "$CONTEXT" --argjson wid "$ex_id" \
|
||||
'[.[] | select(type == "object")
|
||||
@@ -1704,10 +1700,10 @@ jobs:
|
||||
for v in "$n_protected" "$n_not_manifest" "$n_not_docs"; do
|
||||
case "$v" in
|
||||
''|*[!0-9]*)
|
||||
# REPLACES, for the reason every other refusal here does (ersatztv#849 round 3): this
|
||||
# REPLACES, for the reason every other refusal here does (ersatztv#849): this
|
||||
# run has already DECLINED to inherit whatever the head carries, so exiting without
|
||||
# writing leaves that row authoritative. The old message said no status "will be
|
||||
# written", which was true and beside the point — the question is what is standing.
|
||||
# writing leaves that row authoritative. A message saying no status "will be
|
||||
# written" is true and beside the point — the question is what is standing.
|
||||
replace_unknown_and_die "A path predicate returned '${v}' instead of a count for ${SHA:0:7}, so the classifier is not operating and this run cannot say what belongs on this head." ;;
|
||||
esac
|
||||
done
|
||||
@@ -1779,8 +1775,8 @@ jobs:
|
||||
state=success
|
||||
desc="Exempt: $reason"
|
||||
elif [ "$ex_repair" = yes ]; then
|
||||
# CARRY THE SENTINEL FORWARD. This branch exists because the first version of it did not,
|
||||
# and cold review reproduced the consequence: refusing the exemption but posting the
|
||||
# CARRY THE SENTINEL FORWARD. This branch exists because the consequence of omitting it
|
||||
# was reproduced: refusing the exemption but posting the
|
||||
# GENERIC pending description overwrote the very sentinel the refusal depends on, so the
|
||||
# next run saw an ordinary machine `pending`, re-derived it, and posted `success` — burying
|
||||
# the human rejection two events after the repair instead of one. The block has to be a
|
||||
@@ -1812,12 +1808,12 @@ jobs:
|
||||
# HIGH-WATER MARK for the post-write verification (ersatztv#706 race 2). Taken FIRST — before
|
||||
# the re-read below, before the fence, before the POST — and the ORDER IS THE POINT.
|
||||
#
|
||||
# An earlier version captured it just before the POST, "as late as possible". Cold review
|
||||
# caught that as a High: everything between the re-read and a late mark is a blind gap. A
|
||||
# Capturing it just before the POST, "as late as possible", is WRONG, and badly so:
|
||||
# everything between the re-read and a late mark is a blind gap. A
|
||||
# human verdict landing there is invisible to the re-read (which already happened) AND
|
||||
# excluded from the post-write check (its id is BELOW a mark taken afterwards), so it is
|
||||
# silently overwritten with no repair. That gap spans the entire retarget re-count — up to 20
|
||||
# timeline round-trips — so it was far wider than the one-round-trip residual being claimed.
|
||||
# timeline round-trips — far wider than a one-round-trip residual.
|
||||
#
|
||||
# Taking the mark first closes the read side completely: any row newer than the mark is caught
|
||||
# either by the re-read (abstain, post nothing) or by the post-write check (repair). There is
|
||||
@@ -1898,13 +1894,11 @@ jobs:
|
||||
max_id_before=-1
|
||||
fi
|
||||
|
||||
# MARK ANY WRITE NOTHING CAN VERIFY (ersatztv#849 route 1). The heading said "withhold the
|
||||
# EXEMPTION" while the block downgrades every re-derivable state, which is narrower than
|
||||
# what it does.
|
||||
# MARK ANY WRITE NOTHING CAN VERIFY (ersatztv#849 route 1).
|
||||
#
|
||||
# EVERY RE-DERIVABLE WRITE IS DOWNGRADED, not only `success` (corrected in round 2). An
|
||||
# earlier version restricted this to the exemption, reasoning that a sticky generic
|
||||
# `pending` "withholds nothing, since an unreviewed PR is blocked already". That analysed
|
||||
# EVERY RE-DERIVABLE WRITE IS DOWNGRADED, not only `success`. Restricting
|
||||
# this to the exemption, on the reasoning that a sticky generic
|
||||
# `pending` "withholds nothing, since an unreviewed PR is blocked already", analyses
|
||||
# the wrong PR. The damaging case is a PR that IS exemptible and only got the generic
|
||||
# `pending` from a transient enumeration failure: the generic description carries no
|
||||
# marker, the post-write check below does not run without a mark, so a human verdict
|
||||
@@ -1961,17 +1955,17 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# A SENTINEL THAT APPEARED MID-RUN (ersatztv#706, round-3 review). The re-read above recomputes
|
||||
# A SENTINEL THAT APPEARED MID-RUN (ersatztv#706). The re-read above recomputes
|
||||
# `ex_repair`, and until this guard existed nothing downstream read it: the POST writes the
|
||||
# `$state` frozen at classification time, so a STALE OVERLAPPING RUN would post its `success`
|
||||
# straight over a sentinel another run had just written — burying a human rejection, with no
|
||||
# repair (the human row is below this run's mark) and no log. That fails toward SUCCESS, so it
|
||||
# was not covered by the "repair fails toward pending" residual; it is the exact outcome this
|
||||
# whole change exists to prevent, reached through the run overlap this branch itself measured.
|
||||
# whole change exists to prevent, reached through a measured run overlap.
|
||||
#
|
||||
# THE RULE IS "NEVER REPLACE A SENTINEL WITH A NON-SENTINEL", not "never overwrite it with a
|
||||
# success". A first draft of this guard tested `state = success`, which is one branch too
|
||||
# narrow: a run can reach the POST on `state=pending` carrying the GENERIC description — most
|
||||
# success". Testing `state = success` here is one branch too narrow: a run can reach
|
||||
# the POST on `state=pending` carrying the GENERIC description — most
|
||||
# realistically after a transient enumeration failure (`complete != yes`) — and that run
|
||||
# passes a success-only guard, passes the fence, and overwrites the sentinel with ordinary
|
||||
# text. The next run then sees no sentinel, re-derives, and posts `success`: the same buried
|
||||
@@ -2036,7 +2030,7 @@ jobs:
|
||||
# instead of a re-derivable description. Letting `pending` through here is still right — the
|
||||
# alternative strands every PR whenever the timeline is unreadable — but the masking itself
|
||||
# remains a cost, so the trade is "immediate block, one repaired write", not "free".
|
||||
# ABSTAINING IS A HANDOFF ONLY WHEN THERE IS NOTHING TO HAND OFF (ersatztv#849 round 3).
|
||||
# ABSTAINING IS A HANDOFF ONLY WHEN THERE IS NOTHING TO HAND OFF (ersatztv#849).
|
||||
# The two arms below are right not to write their CLASSIFICATION — it was computed against
|
||||
# a base or a head the PR may no longer have, and the mutation that invalidated it has
|
||||
# already queued a successor. But when this run DECLINED to inherit a row the head carries,
|
||||
@@ -2106,13 +2100,12 @@ jobs:
|
||||
# in the helper that may be stronger than the reconcilable sentinel. It can only ever
|
||||
# withhold an exemption.
|
||||
mark_desc="$UNVERIFIED_DESC"
|
||||
# `$desc` ALONE, because the classification has already collected the others. Round 8
|
||||
# promoted on `$pre_desc` and `$ex_desc` as well, and both are unreachable as a sole
|
||||
# `$desc` ALONE, because the classification has already collected the others. Promoting
|
||||
# on `$pre_desc` and `$ex_desc` as well adds nothing — both are unreachable as a sole
|
||||
# cause: a repair sentinel at the FIRST read sets `ex_repair`, which forces
|
||||
# `desc="$REPAIR_DESC"`; one arriving mid-run is caught by the sentinel guard unless this
|
||||
# run is itself writing that string, which is the same condition. Keeping them meant two
|
||||
# clauses no fixture could ever distinguish — the shape this branch has spent several
|
||||
# rounds either proving or declaring, and here the honest answer is that they are
|
||||
# run is itself writing that string, which is the same condition. Keeping them means two
|
||||
# clauses no fixture could ever distinguish, and the honest answer is that they are
|
||||
# redundant rather than unprovable. `$desc` needs no state test: it is this run''s own
|
||||
# decision, and it is `pending` whenever it is that string.
|
||||
if [ "$desc" = "$REPAIR_DESC" ]; then mark_desc="$REPAIR_DESC"; fi
|
||||
@@ -2150,11 +2143,11 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
if { [ "$retargets_before_ok" != yes ] || [ "$rt_ok" != yes ]; } && [ "$state" = "success" ]; then
|
||||
# ABSTAINING HERE WAS A FAIL-OPEN WHEN THE HEAD ALREADY CARRIED A ROW (ersatztv#849,
|
||||
# round 2). This branch is reached only after the classification DECLINED to inherit
|
||||
# ABSTAINING HERE WAS A FAIL-OPEN WHEN THE HEAD ALREADY CARRIED A ROW (ersatztv#849).
|
||||
# This branch is reached only after the classification DECLINED to inherit
|
||||
# whatever `review-verdict/h10` the head carries — that is why it is re-deriving — so
|
||||
# posting nothing leaves the declined row current. The old message said the context
|
||||
# "stays absent", which is true only for a head that had none; on a head carrying a
|
||||
# posting nothing leaves the declined row current. A message saying the context
|
||||
# "stays absent" is true only for a head that had none; on a head carrying a
|
||||
# machine or off-list `success` it is the opposite of what happens, and no retarget or
|
||||
# push need have occurred, so no successor run is guaranteed either.
|
||||
#
|
||||
@@ -2312,7 +2305,7 @@ jobs:
|
||||
# over — it carries `creator: null` (an Actions-token POST, measured; see the provenance
|
||||
# note above) and its description is `Exempt: …`, not `Review-verdict:` — so the count is
|
||||
# of human verdicts that did not exist when the mark was taken.
|
||||
# TWO row shapes count as "something raced this write", not one (round-5 review).
|
||||
# TWO row shapes count as "something raced this write", not one.
|
||||
#
|
||||
# (a) a HUMAN verdict — non-null creator, `Review-verdict:` description;
|
||||
# (b) a machine SENTINEL — null creator, description exactly `$REPAIR_DESC`.
|
||||
@@ -2534,8 +2527,8 @@ jobs:
|
||||
# a machine-written `success` is re-derived, not inherited, so that successor changes the
|
||||
# answer. A retarget landing before it is caught here.
|
||||
#
|
||||
# WHAT THAT BUYS IS TRANSIENT INSTEAD OF PERMANENT, not "the green never stands" — which is
|
||||
# what this comment claimed until round-2 review. The `success` is live between its POST
|
||||
# WHAT THAT BUYS IS TRANSIENT INSTEAD OF PERMANENT, not "the green never stands".
|
||||
# The `success` is live between its POST
|
||||
# and the repair below, including the timeline round trips in between, so branch protection
|
||||
# or an already-scheduled auto-merge can observe it. THAT WINDOW GREW WITH #870 and the
|
||||
# sentence is dated accordingly: the walk in between is now 20 requests rather than ~2,
|
||||
|
||||
@@ -18,8 +18,7 @@ public class GetSearchFieldValuesHandler(IDbContextFactory<TvContext> dbContextF
|
||||
/// Rows read per round trip when walking the list-valued (JSON-array) columns on
|
||||
/// <c>SongMetadata</c>, and the ceiling on rows read per request.
|
||||
/// <para>
|
||||
/// These count ACTUAL ROWS, and arriving at that took four tries — each earlier attempt bounded a
|
||||
/// quantity that sounded like rows and was not. A fixed <c>LIMIT</c> budget bounded the RESULT, and
|
||||
/// These count ACTUAL ROWS. A fixed <c>LIMIT</c> budget bounded the RESULT, and
|
||||
/// the pre-filter (allowed to over-match) starved it with rows that could not match. Keyset paging
|
||||
/// with a <c>LIMIT</c> bounded CANDIDATES RETURNED — but a query matching nothing must evaluate
|
||||
/// every eligible row before it can return an empty page, so rows inspected stayed unbounded. A
|
||||
@@ -36,8 +35,7 @@ public class GetSearchFieldValuesHandler(IDbContextFactory<TvContext> dbContextF
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Be precise about what is bounded: LOGICAL ROWS RETURNED AND MATERIALIZED, and the number of
|
||||
/// round trips. Not physical work, and not bytes.</b> Two things break the stronger reading, and an
|
||||
/// earlier version of this comment asserted it anyway:
|
||||
/// round trips. Not physical work, and not bytes.</b> Two things break the stronger reading:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// MySQL purge lag. Deleted clustered-index records survive until purge runs, and a range
|
||||
@@ -142,7 +140,7 @@ public class GetSearchFieldValuesHandler(IDbContextFactory<TvContext> dbContextF
|
||||
// over-match, even though the column collation (utf8mb4_0900_ai_ci) is accent-insensitive: the driver
|
||||
// binds the LIKE pattern with a BINARY collation, so the comparison is accent-sensitive in practice.
|
||||
// A hand-typed probe using a LITERAL pattern DOES over-match; that is a different query from the one
|
||||
// this code runs, and mistaking the two is how an earlier revision of the decision record got it wrong.
|
||||
// this code runs, and mistaking the two gives a false read on whether this predicate over-matches.
|
||||
if (source is not null && ContainsNonAscii(query) && IsSqlite(dbContext))
|
||||
{
|
||||
values.AddRange(
|
||||
@@ -472,7 +470,7 @@ public class GetSearchFieldValuesHandler(IDbContextFactory<TvContext> dbContextF
|
||||
/// ordering key</i>, which positions the scan and never discards a row, whereas a residual
|
||||
/// predicate throws away rows the engine already produced. <c>LIMIT</c> only truncates what
|
||||
/// survives a residual predicate, so with one present it bounds the output rather than the row
|
||||
/// count — which is how every earlier revision scanned past its own bound. With none, <c>LIMIT n</c>
|
||||
/// count — a gap wide enough to scan straight past a nominal row-count bound. With none, <c>LIMIT n</c>
|
||||
/// yields <c>n</c> logical rows. Null payloads are dropped in memory by
|
||||
/// <see cref="ParseElements" />.
|
||||
/// </para>
|
||||
|
||||
@@ -359,9 +359,9 @@ public class WatermarkSelectorDecoResolutionTests
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The channel-level fallback is deliberately an INDEPENDENTLY RESOLVABLE `ChannelLogo` watermark whose
|
||||
/// cached file exists. An earlier version of this test gave the fallback the same missing custom path as
|
||||
/// the playout-item watermark, which made it unfalsifiable: a wrongly-widened guard would have fallen
|
||||
/// through to a fallback that also resolved to None, so the assertion held either way.
|
||||
/// cached file exists. Giving the fallback the same missing custom path as the playout-item watermark
|
||||
/// would make the test unfalsifiable: a wrongly-widened guard would fall through to a fallback that also
|
||||
/// resolves to None, so the assertion would hold either way.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Missing_But_Named_Custom_Playout_Item_Watermark_Should_Not_Fall_Through()
|
||||
|
||||
@@ -464,7 +464,7 @@ public class CreateChannelFromLineupHandlerTests
|
||||
[Test]
|
||||
public async Task Should_Reject_WeightedShuffle_For_A_Multi_Item_Lineup()
|
||||
{
|
||||
// regression (#70, found by adversarial review of PR #402): a 2+ entry lineup is persisted as a
|
||||
// regression (#70, PR #402): a 2+ entry lineup is persisted as a
|
||||
// Playlist, and PlaylistEnumerator has no default arm -- an order it doesn't know leaves the
|
||||
// enumerator null and the items vanish from the playlist with nothing reported. This handler is the
|
||||
// THIRD writer of PlaylistItem.PlaybackOrder and was missed when the other two were gated.
|
||||
|
||||
@@ -358,9 +358,9 @@ public class ScheduleItemResponseRoundTripTests
|
||||
// that for this fixture's payload: it forwards every Id, takes the id-based reconcile, and
|
||||
// updates rows in place. So Id compares equal, and the exemption was unnecessary.
|
||||
//
|
||||
// Two mutations of this fixture, both EXECUTED — recorded as results, with no account of why,
|
||||
// because three earlier drafts of this comment each supplied a confident mechanism for a
|
||||
// correct observation and two of them were contradicted by the code:
|
||||
// Two mutations of this fixture, both EXECUTED — recorded as results, with no account of why:
|
||||
// a confident mechanism for this observation is easy to get wrong, and two independent ones
|
||||
// were each contradicted by the code:
|
||||
//
|
||||
// ToReplaceCommand passes `null` for EVERY id -> test stays GREEN
|
||||
// ToReplaceCommand passes `null` for index 0 only -> test goes RED, "Id differs"
|
||||
|
||||
@@ -372,7 +372,7 @@ public class GetSearchFieldValuesHandlerTests
|
||||
[Test]
|
||||
public async Task List_Valued_Walk_Reads_Live_Rows_Regardless_Of_Id_Density()
|
||||
{
|
||||
// THE round-4 killer. That revision bounded the Id KEYSPACE, and keyspace is not rows: with 20,000
|
||||
// Bounding the Id KEYSPACE is the killer here: keyspace is not rows — with 20,000
|
||||
// historical rows deleted and one live song at Id 20001, the walk spent its whole allowance on empty
|
||||
// ranges and returned [] for a table containing exactly one row. Capacity degraded linearly with
|
||||
// deletion ratio, and no ratio was safe -- one placed gap hid the next match.
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace ErsatzTV.Tests.Integration;
|
||||
/// the decision record <c>media.nullable-primitive-collection-mutation</c>.
|
||||
/// Assigning one on a TRACKED entity flips it to <see cref="EntityState.Modified" />, and the next
|
||||
/// <c>SaveChanges</c> writes <c>[]</c> over what the database held as <c>NULL</c> — the exact
|
||||
/// mechanism an adversarial review demonstrated in ersatztv#691, which is why that issue's
|
||||
/// mechanism demonstrated in ersatztv#691, which is why that issue's
|
||||
/// entity-level guard was reverted in favour of guarding at the READ SITE.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
||||
@@ -176,7 +176,7 @@ Tracked under epic **#62**; sub-issues #63–#68:
|
||||
|
||||
## 7. Out of scope & next steps
|
||||
|
||||
**Out of scope now:** the full #59 implementation spec, React app architecture (routing/state/auth/build/deploy), the media-libraries/collections browser (Pass 2), browser-based channel playback (tracked separately in #60), the actual rebuild. (Channel creation was originally deferred to Pass 2 but is now specified in §6.)
|
||||
**Out of scope now:** the full #59 implementation spec, React app architecture (routing/state/auth/build/deploy), the media-libraries/collections browser (Pass 2), browser-based channel playback (tracked separately in #60), the actual rebuild. (Channel creation is specified in §6, not deferred to Pass 2.)
|
||||
|
||||
**Next steps (after a look is locked):**
|
||||
1. Design the §6 Create Channel screen in Claude Design; file the §6 backend-gap issues.
|
||||
|
||||
+4
-2
@@ -1148,8 +1148,10 @@ one case that's easy to forget and easy to detect: a PR that touches a SPA scree
|
||||
`::warning::` annotation.
|
||||
|
||||
**2. The session-narrative reminder** (ersatztv#784) runs `scripts/check-doc-narrative.py --diff`
|
||||
over the lines this PR **adds** to `docs/**/*.md` (minus `docs/decisions/**`, exempt wholesale) and
|
||||
root-level `*.md`, flagging text that narrates the document's own revision history —
|
||||
over the lines this PR **adds** to `docs/**/*.md` (minus `docs/decisions/**`, exempt wholesale),
|
||||
root-level `*.md`, and — since ersatztv#876 — every tracked file under `.claude/`, `.gitea/`, `.husky/`
|
||||
and `scripts/` regardless of extension (minus the detector and its own test, whose fixtures are the
|
||||
phrasings), flagging text that narrates the artifact's own revision history —
|
||||
`docs.no-session-narrative`. It is advisory **by design and permanently**: a narrative detector is a
|
||||
string predicate over prose, the class `docs/defect-shapes-773.md` §4 argues must never be
|
||||
load-bearing, so the script exits 0 on every path including a bad argument or an unresolvable base
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -10,8 +10,8 @@ narrative detector is a string predicate over prose, and `docs/defect-shapes-773
|
||||
withdrawn `test_review_verdict_vocabulary_parity.py` — six review rounds, then deleted — is the
|
||||
empirical case). Do not convert this into a gate; the decision record says no in as many words.
|
||||
|
||||
WHY PYTHON AND NOT SHELL. The first implementation hand-parsed `git diff -U0` output in bash by
|
||||
matching line prefixes, and cold review demonstrated four separate defects in that one parser: the
|
||||
WHY PYTHON AND NOT SHELL. Hand-parsing `git diff -U0` output in bash by matching line prefixes
|
||||
produced four separate defects in that one parser: the
|
||||
`\\ No newline at end of file` marker was counted as content, an added line whose own text began
|
||||
`++ ` was eaten by the `+++ ` header arm, `core.quotePath` hid non-ASCII paths, and `read` dropped a
|
||||
final unterminated line. Those are four instances of one mistake — deciding what a diff line IS from
|
||||
@@ -54,7 +54,25 @@ except Exception: # noqa: S110 — a stdout that cannot be reconfigured is not
|
||||
# the 35 have more than one commit, so revisions do happen — which is precisely the case where the
|
||||
# rule's remedy (move it to the commit message) is still available. The exemption costs the only
|
||||
# reach the detector has and buys only quiet in a sweep a person runs deliberately.
|
||||
EXEMPT_PREFIXES = ("docs/decisions/",)
|
||||
#
|
||||
# `scripts/tests/fixtures/` is exempt for the same reason one level down (#876): it holds COPIES of
|
||||
# decision records and other test DATA, which carry whatever phrasing the test under them needs and
|
||||
# are not an artifact anyone edits for a reader. Exempting it is also what keeps the depth
|
||||
# measurement in the record true — its record copies sit at four to six slashes.
|
||||
EXEMPT_PREFIXES = ("docs/decisions/", "scripts/tests/fixtures/")
|
||||
|
||||
# The PROCESS corpus (#876): hooks, workflows, scripts and their tests, scanned regardless of
|
||||
# extension because the artifacts there are shell, YAML, Python and jq, not Markdown. It is in the
|
||||
# population on the #812 argument run forward — `--diff` sees only ADDED lines, and this is where
|
||||
# narrative is being ADDED: 287 of the 453 sites the #876 sweep found outside the docs corpus were
|
||||
# under 30 days old (measured 2026-09-03 at `fb5592971`), against a `docs/superpowers/**` that had
|
||||
# not moved since 2026-07-23. `web/` and the C# projects are left out on the same measurement: 3 of
|
||||
# the 74 PATTERNS-matching sites lived there, across roughly 4,600 tracked files.
|
||||
PROCESS_PREFIXES = (".claude/", ".gitea/", ".husky/", "scripts/")
|
||||
|
||||
# Exempt by NAME, not by prefix: this file and its test carry the phrasings as pattern and fixture,
|
||||
# so they would be permanent hits — the false positive that makes an advisory check stop being read.
|
||||
EXEMPT_FILES = ("scripts/check-doc-narrative.py", "scripts/tests/test_check_doc_narrative.py")
|
||||
|
||||
# Session-narrative phrasings. Deliberately narrow: each is first person or names a revision of THIS
|
||||
# artifact. Broad words that also appear in legitimate dated history ("previously", "was wrong") are
|
||||
@@ -84,14 +102,19 @@ WARNING = (
|
||||
|
||||
|
||||
def is_scanned_path(path: str) -> bool:
|
||||
"""The population: `docs/**/*.md` minus `docs/decisions/**`, plus root-level `*.md`.
|
||||
"""The population: `docs/**/*.md` minus `docs/decisions/**`, plus root-level `*.md`, plus every
|
||||
tracked file under `PROCESS_PREFIXES` regardless of extension, minus `EXEMPT_FILES`.
|
||||
|
||||
Stated positively and in one place so the record's `mechanics:` can quote it exactly. Skills,
|
||||
`web/`, and other nested markdown outside `docs/` are deliberately NOT in scope.
|
||||
Stated positively and in one place so the record's `mechanics:` can quote it exactly. `web/`,
|
||||
C# source, and nested markdown outside `docs/` and the process prefixes are deliberately NOT in
|
||||
scope. This is a PATH predicate; `run_all` additionally restricts the population to tracked
|
||||
regular files, because a symlink's content is its target, not the artifact.
|
||||
"""
|
||||
if not path.endswith(".md"):
|
||||
if path in EXEMPT_FILES or any(path.startswith(p) for p in EXEMPT_PREFIXES):
|
||||
return False
|
||||
if any(path.startswith(p) for p in EXEMPT_PREFIXES):
|
||||
if any(path.startswith(p) for p in PROCESS_PREFIXES):
|
||||
return True
|
||||
if not path.endswith(".md"):
|
||||
return False
|
||||
return path.startswith("docs/") or "/" not in path
|
||||
|
||||
@@ -167,6 +190,11 @@ def added_lines(diff: str):
|
||||
# `path` should never name a file the added lines do not belong to. A
|
||||
# `--diff-filter=d` on the git call was removed rather than kept beside it: a second
|
||||
# mechanism nobody can witness failing is how a duplicate guard hides its twin.
|
||||
# Git terminates the `+++` filename with a TAB when the path contains a space, and a
|
||||
# path carrying that tab matches nothing in `is_scanned_path` — the file is dropped
|
||||
# SILENTLY, the same scanned-0 channel the config pins above close. Witnessed by a
|
||||
# positive control with a space in the name.
|
||||
p = p.split("\t", 1)[0]
|
||||
path = None if p == "/dev/null" else (p[2:] if p.startswith("b/") else p)
|
||||
elif raw.startswith("+"):
|
||||
lineno += 1
|
||||
@@ -212,14 +240,30 @@ def run_diff(base: str, out: list[str]) -> int:
|
||||
|
||||
def run_all(out: list[str]) -> int:
|
||||
# Population from `git ls-files`, never a filesystem walk — an untracked scratch file is not
|
||||
# part of the corpus (#778).
|
||||
rc, listing = git("ls-files", "-z", "--", "*.md")
|
||||
# part of the corpus (#778). The STAGE listing, because the population is tracked REGULAR files
|
||||
# (mode 100644/100755): a tracked symlink (120000) is a path the predicate admits whose worktree
|
||||
# content is its TARGET — a directory (`.claude/skills/jellyfin`, a cross-repo skill link) or a
|
||||
# file that may sit outside the population — and a gitlink (160000) is a submodule pointer with
|
||||
# no content of its own. `--diff` carries no such filter: a gitlink's diff content is a sha, and
|
||||
# a symlink's is its target PATH, scanned like any other added line — a phrasing inside a path
|
||||
# name would draw an advisory warning on the link, which is harmless. The stage listing emits
|
||||
# one row PER STAGE for an unmerged path, so modes are gathered per path first: a path is opened
|
||||
# once, and only when EVERY stage is a regular blob — in an add/add conflict between a symlink
|
||||
# and a regular file, the worktree holds one of them and a per-row test would let the regular
|
||||
# row authorise opening the other.
|
||||
rc, listing = git("ls-files", "-s", "-z")
|
||||
if rc != 0:
|
||||
print("doc-narrative: could not list tracked files — SCANNED NOTHING.")
|
||||
return -1
|
||||
modes: dict[str, set[str]] = {}
|
||||
for entry in listing.split("\0"):
|
||||
if not entry:
|
||||
continue
|
||||
meta, _, path = entry.partition("\t")
|
||||
modes.setdefault(path, set()).add(meta.split(" ", 1)[0])
|
||||
scanned = 0
|
||||
for path in listing.split("\0"):
|
||||
if not path or not is_scanned_path(path):
|
||||
for path, path_modes in modes.items():
|
||||
if not all(m.startswith("100") for m in path_modes) or not is_scanned_path(path):
|
||||
continue
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
|
||||
@@ -119,8 +119,8 @@ case $(printf '%s' "$verdict" | jq -r '.verdict // ""' 2>/dev/null || true) in
|
||||
# NOT given separate arms — because this script's contract publishes exactly five class words and
|
||||
# `unreadable` already means precisely "no comparison was made", which is true of both.
|
||||
#
|
||||
# A separate `unreadable)` arm printing the same word was written first and then deleted: cold
|
||||
# review measured it as a no-op (removing it left the suite green), and the merge hook's own
|
||||
# A separate `unreadable)` arm printing the same word is a measured no-op (removing it
|
||||
# left the suite green), and the merge hook's own
|
||||
# comment condemns exactly that — an arm no observation can distinguish is a comment with syntax.
|
||||
# The hook can afford two arms because it has two distinct REASON STRINGS to hand an operator;
|
||||
# here there is one output word, so a second arm would be decoration.
|
||||
|
||||
@@ -127,8 +127,8 @@ comments=$(cat)
|
||||
# "convention not adopted" and downgrade a hard block into an ask.
|
||||
#
|
||||
# Each body is emitted as a JSON STRING on its own line (newlines escaped by JSON), so comment
|
||||
# boundaries are carried out-of-band. An earlier version joined bodies with a literal sentinel line;
|
||||
# a comment containing that sentinel could forge a boundary, reset fence state mid-body, and expose a
|
||||
# boundaries are carried out-of-band. Joining bodies with a literal sentinel line instead would let
|
||||
# a comment containing that sentinel forge a boundary, reset fence state mid-body, and expose a
|
||||
# verdict that was still inside an unclosed fence. In-band delimiters are forgeable by whoever writes
|
||||
# the data — and here that is anyone who can comment on the PR.
|
||||
# The shape is asserted IN jq so a payload that isn't an array of comment objects with STRING bodies
|
||||
@@ -189,7 +189,7 @@ verdicts=$(printf '%s\n' "$encoded" | while IFS= read -r encoded_body; do
|
||||
# A shorter or different marker while a fence is open is content, so it neither closes nor prints.
|
||||
outside=$(printf '%s\n' "$body" | awk '
|
||||
{
|
||||
# Raw HTML blocks are the third code-block form (#629 round 5): <pre>, <code> and HTML
|
||||
# Raw HTML blocks are the third code-block form (#629): <pre>, <code> and HTML
|
||||
# comments all render their contents literally, so a verdict inside one is an example, not an
|
||||
# approval. Tracked as a simple depth/marker count rather than parsed — the direction of error
|
||||
# is to strip MORE, which can only ever withhold approval.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# Prove — at runtime, every run — that the delimiter ban actually DETECTS a delimiter (ersatztv#767).
|
||||
#
|
||||
# WHY THIS EXISTS AS A SHELL STEP RATHER THAN A TEST. The `scan` job's guarantee used to be a pytest
|
||||
# that asserted the ban command works. Review then disarmed the whole gate with ONE new file at the
|
||||
# repo root — a `pytest.ini` carrying `addopts = -k "not delimiter_banned"`, or a `conftest.py` with
|
||||
# that asserted the ban command works. DEMONSTRATED: ONE new file at the repo root disarms the whole gate — a
|
||||
# `pytest.ini` carrying `addopts = -k "not delimiter_banned"`, or a `conftest.py` with
|
||||
# `pytest_collection_modifyitems` — which deselects the ban test AND the tests guarding it, leaving
|
||||
# every job green while a delimiter sits in `build`'s `Smoke` body and the image publishes. That tier
|
||||
# sits above every `env:` a test can reconstruct, and above the tests themselves: anything living
|
||||
@@ -11,8 +11,8 @@
|
||||
#
|
||||
# IT RUNS IN THE REAL CHECKOUT, NOT A COPY, and that is the whole point. An isolated copy does not
|
||||
# inherit the repo-root `pytest.ini`/`conftest.py`/`pyproject.toml` that a disarm would live in, so a
|
||||
# copy-based proof reports the ban healthy while the job's actual invocation is deselected — measured,
|
||||
# and it is why the first version of this script was wrong. Same cwd, same rootdir, same config and
|
||||
# copy-based proof reports the ban healthy while the job's actual invocation is
|
||||
# deselected — measured. Same cwd, same rootdir, same config and
|
||||
# the same BAN INVOCATION as the run being vouched for — not the same command line, since the
|
||||
# workflow step also runs the wiring tests and this runs only the ban file. Only the workflow file
|
||||
# differs, and only for a moment.
|
||||
@@ -34,8 +34,8 @@
|
||||
# passing (the ban is not enforcing), pytest failing for a NON-test reason (collection error, nothing
|
||||
# collected — cannot prove anything), and pytest failing on some OTHER test while the ban test itself
|
||||
# was deselected. Only the ban test's `[build]` parametrisation failing counts as proof. Both weaker
|
||||
# readings were live bugs in earlier drafts of this file: "non-zero means it noticed" reported OK on a
|
||||
# collection error, and a later draft still fell through to OK when an unrelated test reddened.
|
||||
# readings have been live bugs here: "non-zero means it noticed" reported OK on a
|
||||
# collection error, and the other still fell through to OK when an unrelated test reddened.
|
||||
#
|
||||
# NOTE ON THE OPENER: this file must never contain the literal two-character expression opener, both
|
||||
# because `scan`'s own run bodies are asserted delimiter-free and because a file containing it is the
|
||||
@@ -81,8 +81,8 @@ detected=$?
|
||||
set -e
|
||||
|
||||
# THREE OUTCOMES, EXHAUSTIVELY, and only the last one is a pass. "Non-zero means it noticed" is not
|
||||
# good enough — that was this script's own first bug, and an earlier draft of this very block still
|
||||
# fell through to OK when pytest exited 1 for a DIFFERENT test's failure while the ban test was
|
||||
# good enough, and neither is "exit 1 means the ban test failed": both fall through to
|
||||
# OK when pytest exits 1 for a DIFFERENT test's failure while the ban test was
|
||||
# deselected. Requiring the exact expected failure is what makes this a proof rather than a liveness
|
||||
# check. (`[build]` because the ban is parametrised per job, and `build` is the one whose dropped
|
||||
# `Smoke` publishes an unsmoked image.)
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
# `review-verdict.yml`, which runs WITHOUT a `container:`. `test` and `migrations` run INSIDE the CI
|
||||
# toolchain image, so their `/tmp` is the job container's own and starts empty. That follows from
|
||||
# `container:`, NOT from a measurement: the build-lane probe confirmed only that `RUNNER_TEMP` is
|
||||
# `/tmp` here (the marker landed at `/tmp/etv-ci-steps-ran-test-1910-1`) — it says nothing about the
|
||||
# directory being private or empty, and an earlier draft of this comment cited it as though it did.
|
||||
# `/tmp` here (the marker landed at `/tmp/etv-ci-steps-ran-test-1910-1`) — it says nothing about
|
||||
# the directory being private or empty.
|
||||
# The fresh container is what actually rules out a stale marker here; the keying is defence in depth.
|
||||
#
|
||||
# It is kept because container-per-job is a property of how the lane is configured today, not a
|
||||
@@ -74,8 +74,8 @@ EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
# NO SILENT FALLBACK FOR THE RUN IDENTITY — found by cold review. The first version defaulted to
|
||||
# `nojob`/`norunid`/`1`, and those are REUSABLE: with `GITHUB_RUN_ID` unset, every run on the host
|
||||
# NO SILENT FALLBACK FOR THE RUN IDENTITY. Defaulting to `nojob`/`norunid`/`1` is
|
||||
# REUSABLE: with `GITHUB_RUN_ID` unset, every run on the host
|
||||
# would share ONE marker file, so a leftover from any earlier run would satisfy the guard on a run
|
||||
# whose step was dropped. A silent PASS — the exact failure the keying exists to remove, reintroduced
|
||||
# by the code meant to implement it.
|
||||
@@ -95,8 +95,8 @@ EOF
|
||||
# then simply read off run 1916: `Marker identity: job=test run=1916 attempt=1 (from the runner)`
|
||||
# and the same for `migrations`. Both required jobs, on the lane that matters.
|
||||
#
|
||||
# That measurement is what promoted it from warn-and-default to REQUIRED, which is why the residual
|
||||
# this comment used to describe — a rerun inheriting attempt 1's markers — no longer exists FOR THE
|
||||
# That measurement is what promoted it from warn-and-default to REQUIRED, which is why the rerun
|
||||
# residual — a rerun inheriting attempt 1's markers — no longer exists FOR THE
|
||||
# CONTAINER JOBS. If a future runner stops exporting any of the three, every job reddens with a
|
||||
# message naming the variable; that is loud, instantly diagnosable, and the correct direction for a
|
||||
# required check.
|
||||
@@ -117,8 +117,8 @@ marker_path() {
|
||||
if [ -n "$missing" ]; then
|
||||
# NOTHING IS PRINTED TO STDOUT HERE, and that is load-bearing rather than style: this
|
||||
# function's stdout IS its return value (it is always called inside `$( )`), so a notice
|
||||
# printed here is captured INTO the path. An earlier revision did exactly that and both
|
||||
# sub-commands then failed on a nonexistent directory. Caught by
|
||||
# printed here is captured INTO the path. Printing one makes both
|
||||
# sub-commands fail on a nonexistent directory. Pinned by
|
||||
# test_a_degraded_run_IDENTITY_*, which is why that test asserts on the exit status and on
|
||||
# the absence of any marker file rather than only on the message.
|
||||
echo "::error::ci-step-ran.sh cannot identify this run —${missing} not set. The marker path would fall back to a name other runs also use, and a stale marker would make the dropped-step guard PASS on a run whose step never executed (ersatztv#756). Refusing rather than degrading to a reusable name." >&2
|
||||
@@ -200,7 +200,7 @@ cmd_assert() {
|
||||
echo "Skip gate fired (docs_only='${ETV_DOCS_ONLY:-}', already_validated='${ETV_REVALIDATE_SKIP:-}') — the gated steps were not expected to run."
|
||||
fi
|
||||
|
||||
# RE-CHECKED AFTER GATING, not only on argv — found by cold review, which reproduced it:
|
||||
# RE-CHECKED AFTER GATING, not only on argv — reproduced:
|
||||
# `ETV_DOCS_ONLY=true … assert --always --gated foo` printed "All 0 expected step(s) executed"
|
||||
# and exited 0. The argv check above cannot see that, because the set is emptied by the gate, not
|
||||
# by the caller. Unreachable with today's argv (both jobs pass `--always detect revalidate`), but
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
# fail fast (1-2s) when the pull fails. This runs in PARALLEL: the diagnosis is present the moment
|
||||
# anyone looks, and the happy path pays nothing.
|
||||
#
|
||||
# UNKNOWN IS NOT A PASS, and this is where the first draft was wrong. It warned and exited 0 on
|
||||
# every answer that was not 200 or 404, which makes "curl is missing from this runner", "the
|
||||
# UNKNOWN IS NOT A PASS. Warning and exiting 0 on every answer that is not 200 or
|
||||
# 404 makes "curl is missing from this runner", "the
|
||||
# registry moved", and "DNS changed" all indistinguishable from a healthy pin — a job that is green
|
||||
# forever having checked nothing, in a file whose header claims the opposite. Unknown answers are
|
||||
# RETRIED (they are usually transient) and then FAIL. The message stays distinct from the deleted
|
||||
|
||||
@@ -58,7 +58,7 @@ RECORD_CEILING_DEFAULT = 60
|
||||
# it is cutting into the bulk rather than marking a tail. See `ceiling_calibration` for why the fine
|
||||
# percentile claim is reported instead of asserted.
|
||||
#
|
||||
# The floor is NOT "at least one record" — that was the first draft and it was nearly unfalsifiable:
|
||||
# The floor is NOT "at least one record" — that is nearly unfalsifiable:
|
||||
# measured on the live corpus it accepted every ceiling from 39 to 229, including the ceiling of 200
|
||||
# this module's own docstring offered as the case it catches (one 230-line record keeps the count
|
||||
# nonzero). A 2% floor rejects 200/229/230 and still leaves ~5x headroom below today's 9.8%.
|
||||
@@ -441,10 +441,9 @@ def record_wing_files(records_dir: Path | None = None, archive_dir: Path | None
|
||||
`RECORDS_DIR.rglob` — so such a file IS a corpus source while being exempt from the guard.
|
||||
|
||||
So the exemption is by exact RELATIVE PATH, never by basename. The only entry is
|
||||
`archive/README.md`, a hand-written directory README that really does exist — an earlier
|
||||
version of this function excluded any wing-root `README.md` "since no such file exists today",
|
||||
which was simply false and would additionally have exempted a future `records/README.md`, i.e.
|
||||
reintroduced the very hole one directory over.
|
||||
`archive/README.md`, a hand-written directory README that really does exist — excluding any
|
||||
wing-root `README.md` "since no such file exists today" is simply false, and would additionally
|
||||
exempt a future `records/README.md`, i.e. reintroduce the very hole one directory over.
|
||||
"""
|
||||
records_dir = dl.RECORDS_DIR if records_dir is None else records_dir
|
||||
archive_dir = dl.ARCHIVE_DIR if archive_dir is None else archive_dir
|
||||
|
||||
@@ -92,8 +92,8 @@ etv_hook_fire_scrub() {
|
||||
# A STRICTER scrub for the one value that becomes a PATH. The record scrub above deliberately keeps
|
||||
# `/` and `.` — tool names like `mcp__gitea__x` and event paths read better with them — but those
|
||||
# are exactly the two characters that turn a session id into `../../escaped`. Passing a value
|
||||
# through a scrub is not the same as passing it through the RIGHT scrub, and the first version of
|
||||
# this fix reused the record scrubber and left the traversal wide open while reading as fixed.
|
||||
# through a scrub is not the same as passing it through the RIGHT scrub: reusing the record
|
||||
# scrubber here leaves the traversal wide open while reading as fixed.
|
||||
etv_hook_fire_scrub_component() {
|
||||
printf '%s' "${1:-}" | tr -c 'A-Za-z0-9_-' '_' | cut -c1-120
|
||||
}
|
||||
@@ -168,8 +168,8 @@ etv_hook_fire_classify() {
|
||||
fi
|
||||
[ -z "$d" ] && d=$(printf '%s' "$out" | sed -n 's/.*"decision"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
|
||||
# A NON-CANONICAL VALUE IS RECORDED AS SUCH, not normalised into a valid one. An earlier version
|
||||
# lowercased whatever it found, so `"permissionDecision":"Deny"` was filed as a clean `deny` —
|
||||
# A NON-CANONICAL VALUE IS RECORDED AS SUCH, not normalised into a valid one. Lowercasing
|
||||
# whatever is found files `"permissionDecision":"Deny"` as a clean `deny` —
|
||||
# manufacturing a decision the harness may never have honoured. The documented values are
|
||||
# lowercase; anything else is a hook bug, and the log should say so rather than launder it.
|
||||
case "$d" in
|
||||
@@ -405,9 +405,8 @@ etv_hook_fire_flush() {
|
||||
# rescue path delivered 52 bytes where the hook wrote 53, and every JSON parser downstream
|
||||
# accepts the short form without complaint. Pinned by `test_stdout_is_replayed_BYTE_EXACT`.
|
||||
#
|
||||
# NO `2>/dev/null` ON THE `exec` — see the stdin comment. An earlier version of THIS line had
|
||||
# it, eight lines below the comment forbidding it, which is why the rule is now restated at
|
||||
# every `exec` rather than once.
|
||||
# NO `2>/dev/null` ON THE `exec` — see the stdin comment. The rule is restated at every `exec`
|
||||
# rather than once because it has been violated eight lines below a single statement of it.
|
||||
if [ -r "${ETV_HOOK_FIRE_STDOUT_TMP:-}" ]; then
|
||||
# Common path: stream the FILE straight through. A shell variable cannot hold a NUL byte, so
|
||||
# replaying via `$(...)` silently drops them and warns on stderr; `cat` is byte-exact for any
|
||||
|
||||
@@ -109,18 +109,18 @@ fi
|
||||
# warning line or a date prefix as the version — `2026.07.26 jq-1.6` parsed as 2026.07, which sails
|
||||
# over the floor. Anchoring keeps every legitimate form (`jq-1.6`, `jq version 1.6`, `jq-1.7.1`,
|
||||
# `jq-1.6-dirty`, `jq-1.6 (Debian 1.6-2.1)`) and rejects the rest, which then fails closed below.
|
||||
# FIRST LINE ONLY, and bounded everywhere. Both bounds are load-bearing; this is the third round on
|
||||
# this one predicate and each previous version failed for a variant of the same reason.
|
||||
# FIRST LINE ONLY, and bounded everywhere. Both bounds are load-bearing; this is the third narrowing of
|
||||
# this one predicate and each earlier narrowing failed for a variant of the same reason.
|
||||
#
|
||||
# * First line only. `[[:space:]]` matches NEWLINES, so an "anchored" pattern still scanned the
|
||||
# whole output: `jq\n2.34: cannot load` matched `jq`, crossed the newline as separator, and
|
||||
# parsed 2.34 — fail-open, the round-2 bug narrowed but not closed. `[[:blank:]]` (space/tab
|
||||
# parsed 2.34 — the fail-open narrowed but not closed. `[[:blank:]]` (space/tab
|
||||
# only) plus a first-line slice confines the match to the line that can actually carry a version.
|
||||
# * Bounded digit runs. This is the round-1 mechanism resurrected. The regex guaranteed the
|
||||
# * Bounded digit runs. This is the original fail-open mechanism resurrected. The regex guaranteed the
|
||||
# operands were digits but not that they fit in `test`'s integer range, so a 23-digit major made
|
||||
# `[ "$major" -lt "$min_major" ]` error with "integer expression expected" — and `set -e` exempts
|
||||
# a failing command in an `if` condition, so the conditional read false and THE FLOOR WAS NEVER
|
||||
# ASSERTED, exit 0. Exactly what the empty-string case did in round 1. `{1,9}` keeps every
|
||||
# ASSERTED, exit 0. Exactly what the empty-string case did before the bound. `{1,9}` keeps every
|
||||
# operand inside a 32-bit integer, so the comparison can no longer error.
|
||||
# * Bounded separator runs, so the pattern cannot be walked across arbitrary filler.
|
||||
first=${raw%%$'\n'*}
|
||||
|
||||
@@ -75,15 +75,15 @@
|
||||
# order is right — precedent for a style is not evidence the behaviour is correct, and this chain
|
||||
# was wrong about everything else on this line. If a server ever sends two different non-empty
|
||||
# names, decide then, with the payload in hand.
|
||||
# THREE outcomes per field, not two, and collapsing the last two is a regression cold review
|
||||
# caught in the first draft of this fix. A field either supplies a NAME, or SKIPs (it is simply
|
||||
# THREE outcomes per field, not two, and collapsing the last two is a REGRESSION.
|
||||
# A field either supplies a NAME, or SKIPs (it is simply
|
||||
# not carrying one: absent, null, or the empty string Gitea sends for a glob rule), or POISONs
|
||||
# (it is PRESENT holding a type a name cannot have).
|
||||
#
|
||||
# The first draft fell through to `rule_name` on anything that was not a usable string, which
|
||||
# made a malformed field indistinguishable from an unsupplied one. Measured: with `branch_name`
|
||||
# Falling through to `rule_name` on anything that was not a usable string
|
||||
# makes a malformed field indistinguishable from an unsupplied one. Measured: with `branch_name`
|
||||
# a number, boolean or array beside `rule_name:"main"`, the predecessor THREW and the gate asked,
|
||||
# while that draft answered `exact` and AUTO-GRANTED — and with `rule_name:"develop"` it answered
|
||||
# while that fallback answered `exact` and AUTO-GRANTED — and with `rule_name:"develop"` it answered
|
||||
# `none`, denying on "the full rule list was read and none matches" about a field it never read.
|
||||
# Both directions of the very defect this file is fixing, reintroduced one shape over.
|
||||
#
|
||||
@@ -94,8 +94,8 @@
|
||||
elif $v == null then {skip: true}
|
||||
else {poison: true} end;
|
||||
# POISON IS CHECKED ON BOTH FIELDS BEFORE EITHER NAME IS TAKEN, so the invariant above holds
|
||||
# field-agnostically. The first draft short-circuited on `branch_name` and only consulted
|
||||
# `rule_name` as a fallback, which made the rule order-dependent: `{"branch_name":42,
|
||||
# field-agnostically. Short-circuiting on `branch_name` and only consulting
|
||||
# `rule_name` as a fallback makes the rule order-dependent: `{"branch_name":42,
|
||||
# "rule_name":"main"}` poisoned, while `{"branch_name":"main","rule_name":42}` answered `exact`
|
||||
# off the good field and never looked at the malformed one. Gitea 1.27.1 does not send that
|
||||
# second shape — a plain rule sets both fields equal, a glob rule sets `branch_name` to `""` —
|
||||
|
||||
@@ -98,8 +98,8 @@ unset _etv_h10_root
|
||||
# Reset to 0 HERE, at load time, so that "derived successfully" is a DATA dependency the query
|
||||
# function requires rather than a control-flow step a caller is trusted to have taken.
|
||||
#
|
||||
# ATTRIBUTED CAREFULLY, because the obvious second reason is not this line's. An earlier version
|
||||
# claimed the reset defeats an inherited `ETV_H10_REVIEWERS_OK=1` from the environment, and cited a
|
||||
# ATTRIBUTED CAREFULLY, because the obvious second reason is not this line's. That reason is that
|
||||
# the reset defeats an inherited `ETV_H10_REVIEWERS_OK=1` from the environment, backed by a
|
||||
# measurement showing a hostile environment does not get a verdict past the writer. The measurement
|
||||
# is real; the conclusion does not follow. Removing this reset produces a byte-identical refusal,
|
||||
# because `etv_h10_reviewers_load` runs on that path and overwrites both variables anyway. What the
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
# still denies — an unexplained deny on a gate whose whole job is to be explicable.
|
||||
#
|
||||
# WHY IT IS NOT A TEST. ersatztv#774 tried to hold the two copies together with a parity test that
|
||||
# extracted both vocabularies from their own shell source with regexes and compared them. Six cold
|
||||
# review rounds each found another shell construction that either escaped the extractor (`SHIP*)`, a
|
||||
# extracted both vocabularies from their own shell source with regexes and compared them. Six
|
||||
# successive fixes each met another shell construction that either escaped the extractor (`SHIP*)`, a
|
||||
# glob in the arm label; an unquoted `SHIP-IT) state=success`) or reddened it on a correct tree (a
|
||||
# `<word>) state=` inside a heredoc; a column-zero `esac` in a string truncating the scoped match).
|
||||
# Each fix was locally correct and the sequence converged on nothing, because a regex over shell
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
Written because the caller's original check only asked `[ -x command ]`, which a
|
||||
DIRECTORY satisfies (`[ -x /bin ]` is true), so it could report a pass for a server
|
||||
that cannot run. Review then found the first version of THIS file had the same
|
||||
weakness one level in: it accepted any response carrying the right id, so a server
|
||||
that was not the configured one — or that answered with a malformed body — passed.
|
||||
that cannot run. The same weakness recurs one level in: accepting any response
|
||||
carrying the right id passes a server that was not the configured one — or that
|
||||
answered with a malformed body.
|
||||
Hence the shape and identity checks below: "it answered" is not "it answered
|
||||
correctly", and a smoke test that cannot tell them apart is decoration.
|
||||
|
||||
@@ -22,13 +22,13 @@ deliberately share a stage code, so read the message, not the number.
|
||||
|
||||
ACCEPTED LIMIT: a server that writes a gigabyte with no newline can still exhaust
|
||||
memory before the timeout fires. Guarding that needs the very frame-capping reader
|
||||
whose caps were themselves a defect in an earlier round, and the input here is our OWN
|
||||
whose caps are themselves the defect described below, and the input here is our OWN
|
||||
configured server on a developer machine — not an adversary. Stated rather than
|
||||
silently unhandled.
|
||||
|
||||
Deliberately NOT bounded by message/line caps. A first attempt added them and
|
||||
they were the defect: an over-long line had its suffix re-parsed as a fresh
|
||||
message (a false green), and a cap reached before the awaited reply reported
|
||||
Deliberately NOT bounded by message/line caps — adding them IS the defect: an
|
||||
over-long line has its suffix re-parsed as a fresh message (a false green), and
|
||||
a cap reached before the awaited reply reports
|
||||
"server did not start". What IS bounded is the set of retained DECODED responses
|
||||
— only a reply to the request in flight is kept, notifications are dropped as
|
||||
they arrive — and wall-clock, via the caller's timeout. The raw line buffer is
|
||||
@@ -225,7 +225,7 @@ def main() -> int:
|
||||
# `dotnet run` execs a CHILD (csharp-lsp-mcp), so the leader exiting on
|
||||
# SIGTERM says nothing about the descendant. Always follow up with SIGKILL to
|
||||
# the saved group: a stale server surviving a probe is exactly the litter
|
||||
# this session found at start-up.
|
||||
# observed accumulating at start-up.
|
||||
if pgid is not None:
|
||||
for sig in (signal.SIGTERM, signal.SIGKILL):
|
||||
try:
|
||||
|
||||
@@ -280,8 +280,8 @@ printf 'posted status: %s = %s on %s\n' "$STATUS_CONTEXT" "$state" "$short"
|
||||
# itself on the next PR event. It is not repaired here on purpose: a second corrective write is the
|
||||
# sticky-sentinel mechanism ersatztv#849 is separately designing, and inventing a parallel one on
|
||||
# this path would be two mechanisms for one invariant.
|
||||
# WHY OUR ROW IS ALWAYS IN THIS RESPONSE — measured, and the first version of this comment had it
|
||||
# wrong in a way worth recording. This endpoint PAGES, and its `total_count` reports the PAGE rather
|
||||
# WHY OUR ROW IS ALWAYS IN THIS RESPONSE — measured, and the INTUITIVE reading is wrong in a way
|
||||
# worth recording. This endpoint PAGES, and its `total_count` reports the PAGE rather
|
||||
# than the total (`?limit=2` on a 15-context head returns 2 rows and `total_count: 2`), so a
|
||||
# truncated body is not detectable from any field in it. That looked like a risk of the verdict row
|
||||
# falling off a page. It is not: Gitea selects the MAX id per context, orders those DESCENDING, and
|
||||
@@ -311,10 +311,10 @@ status_readback=$(api_get "repos/$owner/$repo/commits/$sha/status?limit=100") \
|
||||
# the array TYPE is tested rather than assumed; a body that merely lost its array yields no entry
|
||||
# and is refused below, exactly like one that carries no verdict.
|
||||
# Three outcomes are kept apart below, each with its own message, because they call for different
|
||||
# actions and an earlier draft collapsed them into one: a body whose `.statuses` is not an array at
|
||||
# actions: a body whose `.statuses` is not an array at
|
||||
# all, a well-formed body that does not carry our context, and a body carrying MORE THAN ONE row for
|
||||
# it. Collapsing them also made the array-TYPE test unwitnessable — the absent-entry check caught
|
||||
# every case on its own, so disarming the type test left the suite green.
|
||||
# it. Collapsing them also makes the array-TYPE test unwitnessable — the absent-entry check catches
|
||||
# every case on its own, so disarming the type test leaves the suite green.
|
||||
readback_matches=$(printf '%s' "$status_readback" | jq -c --arg c "$STATUS_CONTEXT" \
|
||||
'if (.statuses | type) == "array" then (.statuses | map(select(.context == $c))) else null end') \
|
||||
|| die "the '$STATUS_CONTEXT' status was posted on $short, but the read-back body could not be parsed — no comment was written."
|
||||
@@ -340,8 +340,8 @@ esac
|
||||
if [ "$readback_count" = 0 ]; then
|
||||
# Does NOT assert a single cause. Reaching here means the row this run just POSTed is absent from
|
||||
# a response that, by the ordering measured above, should have it FIRST — so the honest statement
|
||||
# is that something removed or replaced it, not a guess at which. Paging is deliberately not named:
|
||||
# an earlier version named it, and it is not a live candidate for THIS row.
|
||||
# is that something removed or replaced it, not a guess at which. Paging is deliberately not
|
||||
# named: it is not a live candidate for THIS row.
|
||||
#
|
||||
# `.statuses` is known to be an array here (the branch above refused otherwise), so `length` cannot
|
||||
# fail and needs no fallback. The `|| readback_rows=...` guard that used to be here was dead for a
|
||||
|
||||
@@ -213,9 +213,9 @@ while [ "$page" -le "$MAX_PAGES" ]; do
|
||||
# is rejecting rows whose vocabulary we do not recognise — where a source path may be absent, or
|
||||
# carried in some other field we are not reading. Without it, `"Renamed"` with a capital R, or an
|
||||
# absent status, silently takes the `else true` branch of the clause below and skips the
|
||||
# "renamed rows MUST carry previous_filename" requirement entirely. (An earlier version of this
|
||||
# comment claimed the source path would be "dropped", which is not the mechanism; a maintainer
|
||||
# who tested that claim would find it false and might conclude the check is redundant.)
|
||||
# "renamed rows MUST carry previous_filename" requirement entirely. The source path is NOT
|
||||
# "dropped" — that is not the mechanism, and a maintainer who tested that claim would find it
|
||||
# false and might conclude the check is redundant.
|
||||
# `modified` is accepted alongside `changed` deliberately. Live Gitea emits `changed`; a re-derivation
|
||||
# on 1.27.1 (2026-08-28, ersatztv#747) over the file rows of the 200 most recently updated PRs saw
|
||||
# `changed`, `added`, `renamed` and `deleted` — all four already in the list below. `copied` and
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#
|
||||
# THE RULE THIS ENFORCES. `testing.guard-ships-with-mutation-proof` says a guard is not
|
||||
# tested because a test involving it passes; it ships with a proof it can fail. The same
|
||||
# argument applies to every bug fix, and nothing enforced it. #776 and #793 each took six
|
||||
# review rounds, and in both the recurring mechanism was one thing: a fix's test was
|
||||
# argument applies to every bug fix, and nothing enforced it. In #776 and #793 the
|
||||
# recurring mechanism was one thing: a fix's test was
|
||||
# written to confirm the fix, not to discriminate against its absence.
|
||||
#
|
||||
# HOW. Run the named tests at the commit (CONTROL — must be GREEN), then again in a
|
||||
@@ -14,8 +14,8 @@
|
||||
#
|
||||
# ONLY pytest exit 1 COUNTS AS RED, and that is the whole safety argument. "Non-zero" is
|
||||
# not "the test failed": 2 is an interrupted collection, 3 an internal error, 4 a usage
|
||||
# error, 5 nothing collected, and a killed run gives 143. Cold review measured a SIGTERM
|
||||
# being read as red in an early DRAFT — cancellation masquerading as evidence. On the first
|
||||
# error, 5 nothing collected, and a killed run gives 143. A SIGTERM read as red was
|
||||
# measured in an early DRAFT — cancellation masquerading as evidence. On the first
|
||||
# COMMITTED version the reproducible false PROVEN is the marker case (a failed `cd` giving
|
||||
# the subshell status 1), not the signal one; see the decision record.
|
||||
# `--continue-on-collection-errors` is passed so a genuine collection failure
|
||||
@@ -56,9 +56,9 @@ SELECTOR="${2:-}"
|
||||
|
||||
TMP=""
|
||||
TMP_ENUM=""
|
||||
# ONE cleanup, ONE EXIT trap. An earlier version installed a SECOND `trap ... EXIT` later in
|
||||
# the script to remove TMP_ENUM; `on_signal` then ran `trap - EXIT`, disarming it, so a
|
||||
# signalled run leaked that directory. Folding both removals in here removes the ordering
|
||||
# ONE cleanup, ONE EXIT trap. A SECOND `trap ... EXIT` installed later in
|
||||
# the script to remove TMP_ENUM does not survive: `on_signal` runs `trap - EXIT`, disarming it,
|
||||
# so a signalled run leaks that directory. Folding both removals in here removes the ordering
|
||||
# subtlety rather than adding a third trap to compensate for it.
|
||||
cleanup() {
|
||||
if [ -n "$TMP" ]; then
|
||||
@@ -199,7 +199,7 @@ mkdir -p "$TMP/tmp-control" "$TMP/tmp-reverted" || die "could not create phase t
|
||||
# PYTEST_RC IS READ FROM A MARKER, NOT FROM THE SUBSHELL. `( cd X && pytest ) ; rc=$?`
|
||||
# returns the SUBSHELL's status, and a failed `cd` or a failed redirection also yields 1 —
|
||||
# which the "only exit 1 is red" rule would then accept as a witnessed test failure with
|
||||
# pytest never having run. Cold review found exactly that. The marker file is written only
|
||||
# pytest never having run. The marker file is written only
|
||||
# after pytest RETURNS, so its absence means "pytest did not complete" and can never be
|
||||
# mistaken for a verdict.
|
||||
PYTEST_RC=""
|
||||
|
||||
@@ -88,8 +88,7 @@ def pytest_configure(config):
|
||||
the group this layer cannot help with and the reason there is a second one.
|
||||
|
||||
INSTRUMENT `Popen` ONLY. `subprocess.run`, `call` and `check_output` all reach `Popen`, so
|
||||
wrapping `run` as well counts every launch twice and doubles every figure — which is how the
|
||||
first version of this measurement was wrong.
|
||||
wrapping `run` as well counts every launch twice and doubles every figure.
|
||||
"""
|
||||
global _SESSION_LOG_DIR
|
||||
_SESSION_LOG_DIR = tempfile.mkdtemp(prefix="etv-hook-fire-session-")
|
||||
|
||||
@@ -69,19 +69,19 @@ def _anchored(path: Path, cwd) -> str:
|
||||
"""Absolute realpath, resolving a RELATIVE value against the CHILD's working directory.
|
||||
|
||||
`Popen(cwd=...)` changes the directory the child resolves a relative `ETV_HOOK_FIRE_LOG_DIR`
|
||||
against, so anchoring at the parent's cwd models the wrong process. Cross-family review found
|
||||
that as a live bypass: `ETV_HOOK_FIRE_LOG_DIR=.cache/ersatztv/hook-fire` with `cwd=$HOME` lands
|
||||
against, so anchoring at the parent's cwd models the wrong process. That is a LIVE BYPASS:
|
||||
`ETV_HOOK_FIRE_LOG_DIR=.cache/ersatztv/hook-fire` with `cwd=$HOME` lands
|
||||
exactly on the shared log while a parent-anchored check sees an unrelated path under the repo
|
||||
and clears it.
|
||||
|
||||
THREE cwd shapes, not four. `Popen` accepts str, bytes and `PathLike`; it calls `os.fsencode`,
|
||||
which rejects an int, so a file DESCRIPTOR is not a launch shape and needs no handling here —
|
||||
measured on CPython 3.9 and 3.13, and pinned by
|
||||
`test_the_guard_JUDGES_every_cwd_SHAPE_that_Popen_ACCEPTS`. A previous version of this added a
|
||||
branch for the int case; the branch was DEAD, because `os.fspath` already raises `TypeError` on
|
||||
`test_the_guard_JUDGES_every_cwd_SHAPE_that_Popen_ACCEPTS`. A branch for the int case is DEAD
|
||||
code, because `os.fspath` already raises `TypeError` on
|
||||
an int, and deleting it left that branch's own test green. `os.fsdecode(os.fspath(...))`
|
||||
normalises the three shapes that do exist, which is what the FIRST version got wrong — it called
|
||||
`os.fspath` alone, and `os.path.join` then raised on mixing str with bytes.
|
||||
normalises the three shapes that do exist; `os.fspath` alone leaves
|
||||
`os.path.join` raising on mixing str with bytes.
|
||||
|
||||
realpath, not `==`: `/tmp` is `/private/tmp` on macOS and `$HOME` may itself be a symlink.
|
||||
"""
|
||||
@@ -100,7 +100,7 @@ def _decoded(env: Mapping) -> dict[str, str]:
|
||||
launch is rejected as carrying no isolated dir — a false positive on a correctly isolated
|
||||
child — while a bytes VALUE makes `Path(...)` raise `TypeError`. Both are the "red on a
|
||||
legitimate launch" failure ersatztv#809 explicitly forbids, and they are the same shape as the
|
||||
bytes-`cwd` regression an earlier round shipped.
|
||||
bytes-`cwd` regression `_anchored` records.
|
||||
|
||||
Done here rather than at each lookup so there is one place that knows about the two encodings,
|
||||
and no later reader has to remember which call sites handle which.
|
||||
@@ -196,9 +196,9 @@ class ProductionLogGuard:
|
||||
volumes are case-insensitive, so `$HOME/.cache/ErsatzTV/hook-fire` is the same directory as
|
||||
`$HOME/.cache/ersatztv/hook-fire` and is cleared. Left as a note rather than fixed with an
|
||||
inode comparison, deliberately: nothing spells it that way, the likeliest wrong value — the
|
||||
sink's own default transliterated — matches case exactly, and this change has already had two
|
||||
consecutive review rounds whose finding was created by the previous round's fix, which is this
|
||||
corpus's stated trigger for subtracting a layer rather than adding one.
|
||||
sink's own default transliterated — matches case exactly, and two consecutive fixes here have
|
||||
each created the next finding, which is this corpus's stated trigger for subtracting a layer
|
||||
rather than adding one.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -128,8 +128,8 @@ def _clean_env(**extra: str) -> dict[str, str]:
|
||||
stay out of. A git hook exports several of them, and this suite runs from one.
|
||||
|
||||
So this is a DENY-BY-DEFAULT boundary rather than a list of the variables anyone has thought of:
|
||||
every `GIT_*` is dropped and only the identity this module sets itself is put back. Enumerating
|
||||
the dangerous ones is how the first version of this function shipped covering three of them.
|
||||
every `GIT_*` is dropped and only the identity this module sets itself is put back. An enumeration
|
||||
of the dangerous ones shipped here once, covering three of them — a list is what this replaces.
|
||||
"""
|
||||
env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
|
||||
env.update(extra)
|
||||
|
||||
@@ -135,8 +135,8 @@ MUTATIONS: tuple[Mutation, ...] = (
|
||||
"`drift` on an added context, the mutant reports `match` — a permanent no-op that would "
|
||||
"confirm the snapshot fresh forever. The proof asserts the mutant's EXACT verdict "
|
||||
"`(0, 'match')` rather than merely 'not drift', and ships a positive control for its own "
|
||||
"tmp layout: an earlier draft copied the script without the classifier it loads beside "
|
||||
"itself, so the mutant exited 2 with empty stdout and 'not drift' was satisfied by a copy "
|
||||
"tmp layout: a copy of the script without the classifier it loads beside "
|
||||
"itself exits 2 with empty stdout, and 'not drift' is then satisfied by a copy "
|
||||
"that never ran. The clause string is the one the proof test asserts on before mutating.",
|
||||
),
|
||||
Mutation(
|
||||
@@ -205,7 +205,7 @@ MUTATIONS: tuple[Mutation, ...] = (
|
||||
"The clause is the exact defect this guard was written after: `derived_guard_files` read "
|
||||
"its CALLERS from the index and then admitted the paths they name on `Path.exists()`, so a "
|
||||
"tracked workflow naming a script that exists on one machine only entered the population "
|
||||
"there, red on that checkout and green in CI (#778's third shape, found by cold review "
|
||||
"there, red on that checkout and green in CI (#778's third shape, "
|
||||
"inside #806 itself). Note what this mutation does NOT do: on a clean tree the mutated set "
|
||||
"is identical, so `test_guard_inventory.py`'s own assertions stay green — only narrowing "
|
||||
"the index, which is what the proof does, separates them. That is why the proof has to "
|
||||
@@ -382,7 +382,7 @@ MUTATIONS: tuple[Mutation, ...] = (
|
||||
"be disarmed in it directly (`testing.verification-code-needs-its-own-proof`). The clause is "
|
||||
"the unguessable request id, which AT THE `initialize` STAGE is the only thing refusing a "
|
||||
"server that answers before it is asked: the pending-registration cannot help there, because "
|
||||
"that id is already in flight when the pre-answer arrives, which is why #793 round 5 replaced "
|
||||
"that id is already in flight when the pre-answer arrives, which is why #793 replaced "
|
||||
"the lock rather than tightening it. Disarmed, the stub's pre-answer is "
|
||||
"accepted at `initialize` and the run dies one stage later at `tools/list`, so the proof "
|
||||
"asserts the STAGE (rc 9 and the initialize diagnostic) rather than mere failure: the mutant "
|
||||
@@ -406,7 +406,7 @@ MUTATIONS: tuple[Mutation, ...] = (
|
||||
expect="this file says COVERED, the SPA guard says CREATE",
|
||||
why="This guard IS a test, so disarming it makes it absent rather than red — the checker-guard "
|
||||
"exception applies and the mutation goes into the guarded ARTIFACT, the SPA guard's disposition "
|
||||
"table. The VALUE is the clause: cold review demonstrated that flipping this row from ANNOTATED "
|
||||
"table. The VALUE is the clause: flipping this row from ANNOTATED "
|
||||
"to CREATE retires the requirement that MultiCollectionItemRequest be annotated, so deleting the "
|
||||
"Complete<...> from MultiCollectionsScreen.toItemRequest then leaves every suite green with "
|
||||
"#807's silent weight reset live again. The row is named in full rather than by the bare "
|
||||
@@ -442,7 +442,7 @@ MUTATIONS: tuple[Mutation, ...] = (
|
||||
"the defect itself rather than a caricature of it — ersatztv#883 put a vitest run back into "
|
||||
"a gitless stage, and every image build failed from that commit until #887. WHAT THIS PROVES "
|
||||
"IS NARROWER THAN IT LOOKS, and saying so is the point: the guard no longer decides whether "
|
||||
"a command RUNS the suite (that predicate was wrong nine times across three review rounds), "
|
||||
"a command RUNS the suite (that predicate was wrong nine times), "
|
||||
"it compares the stage's commands against a pin. So this mutation proves the pin is "
|
||||
"compared and reported — not that any particular spelling is recognised, because none needs "
|
||||
"to be. The mutant is deliberately the UNFILTERED spelling: the filtered one is what broke, "
|
||||
@@ -472,8 +472,8 @@ MUTATIONS: tuple[Mutation, ...] = (
|
||||
# test is THE proof, which is #775's scope; this file can only verify one afterwards.
|
||||
|
||||
UNDECLARED: dict[str, str] = {
|
||||
# NO GROUPING. An earlier version sorted these into "driven through their deciding path" and
|
||||
# "not driven at all", and the sort was wrong twice in successive review rounds — in both
|
||||
# NO GROUPING. Sorting these into "driven through their deciding path" and
|
||||
# "not driven at all" was wrong twice — in both
|
||||
# directions, over entries whose own text said the opposite. A category above a list is a second
|
||||
# classification of the same facts, and it drifts the moment one entry's situation changes. Each
|
||||
# entry states its own case instead.
|
||||
|
||||
@@ -147,10 +147,10 @@ def _active_runs(workflow: Path | None = None) -> list[str]:
|
||||
Parsed with `yaml.safe_load`, and returned WHOLE — not split into lines. Both choices are
|
||||
scar tissue.
|
||||
|
||||
Text-scanning for `run:` was round one, and cold review broke it three ways: a `run: |` block
|
||||
Text-scanning for `run:` was round one, and it broke three ways: a `run: |` block
|
||||
scalar was invisible; a job or step switched off still read as wired; and `run:` inside
|
||||
block-scalar *text* was extracted and executed. Round two parsed the YAML and matched a LINE
|
||||
beginning with `PYTHONPATH=.` — and review broke that too, with a heredoc:
|
||||
beginning with `PYTHONPATH=.` — and that broke too, with a heredoc:
|
||||
|
||||
run: |
|
||||
cat <<'EOF' > /dev/null
|
||||
@@ -381,7 +381,7 @@ _STEP = (
|
||||
def test_a_guard_that_stopped_RUNNING_is_DETECTED(tmp_path, label, mutate):
|
||||
"""Eight ways the catalog guard can stop gating, each of which must be caught.
|
||||
|
||||
Commenting out is one of them and was the only one the first version detected. The last two
|
||||
Commenting out is one of them, and the only one a text scan detects. The last two
|
||||
leave the command in the file, parseable and even matchable — as `echo` argument and as heredoc
|
||||
data — which is why the proof executes the step's whole script instead of a line lifted out of it.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Proofs for `scripts/check-doc-narrative.py` (ersatztv#784).
|
||||
|
||||
Every case below is a defect a cold review DEMONSTRATED in the first, shell implementation. They are
|
||||
Every case below is a defect DEMONSTRATED in the first, shell implementation. They are
|
||||
here because the never-fails invariant and the reported line numbers are both asserted in prose in
|
||||
four places (the script header, the workflow comment, `docs/guard-inventory.md` and
|
||||
`docs/remote-state-inventory.md`), and an invariant asserted only in prose is the shape this repo
|
||||
@@ -266,6 +266,8 @@ def test_a_RENAME_does_not_re_flag_the_whole_pre_existing_file(repo: Path) -> No
|
||||
# what stops a new prefix from shipping without a case here.
|
||||
EXEMPT_SAMPLES = {
|
||||
"docs/decisions/": ("docs/decisions/records/x/y.md", "docs/decisions/archive/x/y.md"),
|
||||
# test DATA under a process prefix (#876): a decision-record copy and a shell fixture
|
||||
"scripts/tests/fixtures/": ("scripts/tests/fixtures/premigration/decisions.md", "scripts/tests/fixtures/x/y.sh"),
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +283,7 @@ def test_every_exempt_genre_is_exempt_in_BOTH_modes(repo: Path, mode: str) -> No
|
||||
Each exempt sample is also paired with a MINIMAL TWIN — the same path with the exempt directory
|
||||
renamed — which raises the cost of a structural bypass but does NOT close it. Stated as a limit
|
||||
rather than a guarantee, because three successive attempts to close it were each defeated by the
|
||||
next round: a lone depth-1 control fell to a depth-1 population rule; a depth-3 control fell to a
|
||||
next: a lone depth-1 control fell to a depth-1 population rule; a depth-3 control fell to a
|
||||
`count("/") <= 3` cap; the twins themselves fell to a directory-NAME rule keyed on the renamed
|
||||
SECOND segment (`path.count("/") > 1 and "-" not in path.split("/", 2)[1]`), which skipped both
|
||||
samples and kept both twins. It reddens here now, but only because the `docs/superpowers/`
|
||||
@@ -325,27 +327,36 @@ def test_the_population_agrees_with_an_INDEPENDENT_RESTATEMENT_over_the_REAL_cor
|
||||
"unprovable".
|
||||
|
||||
The rule as the record states it — `docs/**/*.md` minus `docs/decisions/**`, plus root-level
|
||||
`*.md` — is restated below over path SEGMENTS rather than string prefixes, so the two are not
|
||||
one expression copy-pasted, then compared against `is_scanned_path` on every tracked Markdown
|
||||
path in this repo. A finite fixture can only witness the shapes someone thought to write down;
|
||||
`*.md`, plus every file under the four process directories minus the detector and its test
|
||||
(#876) — is restated below over path SEGMENTS rather than string prefixes, so the two are not
|
||||
one expression copy-pasted, then compared against `is_scanned_path` on every tracked path in
|
||||
this repo. A finite fixture can only witness the shapes someone thought to write down;
|
||||
this witnesses every real path, so any population change that alters a verdict on one of them
|
||||
reddens — the depth-1 rule and the directory-NAME rule among them.
|
||||
|
||||
It is NOT sufficient on its own, and the arm below says why with the measurement: a
|
||||
`count("/") <= 3` cap changes no real path's verdict today, so within this test only the
|
||||
synthetic arm catches it. Do not trim that arm as belt-and-braces; it is the half that sees a
|
||||
`count("/") <= 3` cap changes no real path's verdict (re-measured 2026-09-03 after #876 grew the
|
||||
population: 179 files, none deeper than three slashes — and only because `scripts/tests/fixtures/`
|
||||
is exempt, whose record copies sit at four to six), so within this test only the synthetic arm
|
||||
catches it. Do not trim that arm as belt-and-braces; it is the half that sees a
|
||||
rule the corpus has no instance of yet. (The exemption test above happens to redden on that cap
|
||||
too — its two minimal twins sit at four slashes — but that is incidental, not the arm carrying the claim.)
|
||||
"""
|
||||
|
||||
def restated(path: str) -> bool:
|
||||
parts = path.split("/")
|
||||
if parts in (["scripts", "check-doc-narrative.py"], ["scripts", "tests", "test_check_doc_narrative.py"]):
|
||||
return False # the detector and its test: exempt by NAME, their fixtures ARE the phrasings
|
||||
if parts[:3] == ["scripts", "tests", "fixtures"] and len(parts) > 3:
|
||||
return False # test DATA, including decision-record copies (#876)
|
||||
if len(parts) > 1 and parts[0] in {".claude", ".gitea", ".husky", "scripts"}:
|
||||
return True # the process corpus, any extension (#876)
|
||||
if not parts[-1].endswith(".md"):
|
||||
return False
|
||||
if len(parts) == 1:
|
||||
return True # root-level *.md
|
||||
if parts[0] != "docs":
|
||||
return False # nested markdown outside docs/ is out of scope
|
||||
return False # nested markdown outside docs/ and the process corpus is out of scope
|
||||
return parts[1] != "decisions" # docs/** minus docs/decisions/**
|
||||
|
||||
spec = importlib.util.spec_from_file_location("_cdn_pop", SCRIPT)
|
||||
@@ -353,29 +364,64 @@ def test_the_population_agrees_with_an_INDEPENDENT_RESTATEMENT_over_the_REAL_cor
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
rc = subprocess.run(["git", "ls-files", "-z", "--", "*.md"], cwd=REPO, capture_output=True, text=True, check=True)
|
||||
paths = [p for p in rc.stdout.split("\0") if p]
|
||||
assert len(paths) > 100, f"corpus looks wrong: {len(paths)} paths"
|
||||
rc = subprocess.run(["git", "ls-files", "-s", "-z"], cwd=REPO, capture_output=True, text=True, check=True)
|
||||
entries = [e.partition("\t") for e in rc.stdout.split("\0") if e]
|
||||
paths = sorted({path for _meta, _, path in entries})
|
||||
# "regular" as `run_all` defines it: EVERY stage of the path a regular blob, not any one of them.
|
||||
stage_modes: dict[str, set[str]] = {}
|
||||
for meta, _, path in entries:
|
||||
stage_modes.setdefault(path, set()).add(meta.split(" ", 1)[0])
|
||||
regular = {path for path, ms in stage_modes.items() if all(m.startswith("100") for m in ms)}
|
||||
assert len(paths) > 1000, f"corpus looks wrong: {len(paths)} paths"
|
||||
|
||||
disagree = [p for p in paths if mod.is_scanned_path(p) != restated(p)]
|
||||
assert not disagree, f"population disagrees with the restated rule on {len(disagree)}: {disagree[:8]}"
|
||||
|
||||
# Anti-vacuity: an oracle that says False everywhere would agree with a scan-nothing mutant.
|
||||
scanned = [p for p in paths if restated(p)]
|
||||
# `scanned` is what `--all` opens: the predicate over REGULAR blobs only, because a symlink or
|
||||
# gitlink the predicate admits is excluded by mode, and a claim about the scanned corpus's shape
|
||||
# must be measured over the corpus that is scanned.
|
||||
scanned = [p for p in paths if restated(p) and p in regular]
|
||||
exempt = [p for p in paths if p.startswith("docs/decisions/")]
|
||||
process = [p for p in scanned if not p.endswith(".md")]
|
||||
assert len(scanned) > 50 and len(exempt) > 50, f"{len(scanned)} scanned / {len(exempt)} exempt"
|
||||
assert len(process) > 50, f"{len(process)} non-markdown process files — the #876 arm saw nothing"
|
||||
# The record states a MEASUREMENT about this corpus — no scanned path deeper than three slashes,
|
||||
# so a `count("/") <= 3` cap is a no-op against it — and nothing else couples the claim to the
|
||||
# corpus. A red here is the claim going stale, not a defect in the path that broke it. Re-measure,
|
||||
# then re-state every site that carries the claim: the record's `mechanics:`
|
||||
# (`docs.no-session-narrative`, `mechanics:` AND the #876 paragraph of its body), this function's
|
||||
# docstring, the comment on the cross product below, and the fixtures paragraph of the exemption
|
||||
# comment in `check-doc-narrative.py`; then move this bound.
|
||||
assert max(p.count("/") for p in scanned) <= 3, "the record's depth measurement is stale — re-state it"
|
||||
assert not any(mod.is_scanned_path(p) for p in mod.EXEMPT_FILES)
|
||||
assert all(p in paths for p in mod.EXEMPT_FILES), "an EXEMPT_FILES entry names nothing tracked"
|
||||
|
||||
# The real corpus alone cannot see a rule that differs only on a shape it does not currently
|
||||
# contain — no scanned path carries more than three slashes today, so a `count("/") <= 3` cap is
|
||||
# a no-op against it and would ship latent. So the same oracle is also compared over a cross
|
||||
# product of FOUR dimensions a population rule has been observed to key on here — depth, first
|
||||
# contain — no scanned path carries more than three slashes (re-measured 2026-09-03, #876), so a
|
||||
# `count("/") <= 3` cap is a no-op against it and would ship latent. So the same oracle is also
|
||||
# compared over a cross product of FOUR dimensions a population rule has been observed to key on here — depth, first
|
||||
# segment, second segment, extension — widened with basename, case and dotted directories after
|
||||
# a review round found a mutant keyed on each. This ENUMERATES; it is not a universal over the
|
||||
# a mutant keyed on each was found. This ENUMERATES; it is not a universal over the
|
||||
# space of rules, and a dimension on neither list has simply not been measured.
|
||||
heads = ["docs", "Docs", "web", "scripts", ".github", ""]
|
||||
seconds = ["decisions", "superpowers", "other", "decisions-not", "Decisions", ".hidden", ""]
|
||||
leaves = ["f", "index", "README"]
|
||||
exts = [".md", ".markdown", ".MD", ".txt", ""]
|
||||
heads = ["docs", "Docs", "web", "scripts", "Scripts", ".github", ".gitea", ".claude", ".husky", "scripts-not", ""]
|
||||
seconds = [
|
||||
"decisions",
|
||||
"superpowers",
|
||||
"other",
|
||||
"decisions-not",
|
||||
"Decisions",
|
||||
".hidden",
|
||||
"tests",
|
||||
"hooks",
|
||||
"tests/fixtures",
|
||||
"tests/fixtures-not",
|
||||
"",
|
||||
]
|
||||
leaves = ["f", "index", "README", "check-doc-narrative", "test_check_doc_narrative"]
|
||||
# `.py.bak` is the strict extension of an exempt NAME: it separates `path in EXEMPT_FILES` from
|
||||
# a `startswith` over the same tuple, which the real corpus cannot (nothing tracked extends it).
|
||||
exts = [".md", ".markdown", ".MD", ".txt", ".py", ".py.bak", ".sh", ".yml", ""]
|
||||
synthetic = set()
|
||||
for ext in exts:
|
||||
for leaf in leaves:
|
||||
@@ -401,14 +447,122 @@ def test_every_exempt_prefix_has_a_case_above() -> None:
|
||||
|
||||
|
||||
def test_markdown_outside_the_stated_population_is_not_scanned(repo: Path) -> None:
|
||||
"""The population is `docs/**/*.md` minus `EXEMPT_PREFIXES`, plus root-level `*.md`. A skill
|
||||
under `.claude/` is out of scope, and the record's `mechanics:` says so — out of the DETECTOR,
|
||||
still bound by the RULE."""
|
||||
(repo / ".claude" / "skills" / "s").mkdir(parents=True)
|
||||
(repo / ".claude" / "skills" / "s" / "SKILL.md").write_text(f"{NARRATIVE}\n", encoding="utf-8")
|
||||
(repo / "README.md").write_text(f"{NARRATIVE}\n", encoding="utf-8")
|
||||
"""The population is `docs/**/*.md` minus `EXEMPT_PREFIXES`, plus root-level `*.md`, plus the
|
||||
PROCESS corpus (#876) regardless of extension, minus `EXEMPT_FILES`. Nested markdown under
|
||||
`web/` is out of scope, and the record's `mechanics:` says so — out of the DETECTOR, still bound
|
||||
by the RULE. A skill under `.claude/`, a shell hook and a Python test are IN, and the detector's
|
||||
own test file — which carries these phrasings as fixtures — is out BY NAME, so its twin one
|
||||
directory over is still scanned."""
|
||||
for rel in (
|
||||
"web/docs/x.md",
|
||||
".claude/skills/s/SKILL.md",
|
||||
".claude/hooks/h.sh",
|
||||
"scripts/tests/test_x.py",
|
||||
"scripts/tests/test_check_doc_narrative.py",
|
||||
"scripts/tests/x/test_check_doc_narrative.py",
|
||||
"README.md",
|
||||
):
|
||||
(repo / rel).parent.mkdir(parents=True, exist_ok=True)
|
||||
(repo / rel).write_text(f"# {NARRATIVE}\n", encoding="utf-8")
|
||||
commit(repo)
|
||||
assert hits(run(repo, "--all").stdout) == {("README.md", 1)}
|
||||
assert hits(run(repo, "--all").stdout) == {
|
||||
("README.md", 1),
|
||||
(".claude/skills/s/SKILL.md", 1),
|
||||
(".claude/hooks/h.sh", 1),
|
||||
("scripts/tests/test_x.py", 1),
|
||||
("scripts/tests/x/test_check_doc_narrative.py", 1),
|
||||
}
|
||||
|
||||
|
||||
def test_a_tracked_SYMLINK_is_not_scanned_and_not_counted(repo: Path) -> None:
|
||||
"""The population is tracked REGULAR files. A tracked symlink under a process prefix is a path
|
||||
the predicate admits whose worktree content is its TARGET: a directory (this repo's
|
||||
`.claude/skills/jellyfin`) would be skipped with a notice, and a file outside the population
|
||||
would be scanned under the link's name — the target's narrative attributed to a path the rule
|
||||
does not reach. Both are excluded by mode, and the count says so."""
|
||||
(repo / "web").mkdir()
|
||||
(repo / "web" / "target.md").write_text(f"{NARRATIVE}\n", encoding="utf-8")
|
||||
(repo / ".claude" / "skills").mkdir(parents=True)
|
||||
(repo / ".claude" / "skills" / "linked.md").symlink_to("../../web/target.md")
|
||||
(repo / ".claude" / "skills" / "dir").symlink_to("../../web")
|
||||
(repo / ".claude" / "hooks").mkdir()
|
||||
(repo / ".claude" / "hooks" / "h.sh").write_text(f"# {NARRATIVE}\n", encoding="utf-8")
|
||||
commit(repo)
|
||||
out = run(repo, "--all").stdout
|
||||
assert hits(out) == {(".claude/hooks/h.sh", 1)}
|
||||
assert "skipped" not in out
|
||||
# the fixture's `docs/seed.md` plus the hook: neither symlink is counted, and neither is skipped
|
||||
assert "scanned 2 file(s)" in out
|
||||
|
||||
|
||||
def test_an_UNMERGED_path_is_scanned_once_and_counted_once(repo: Path) -> None:
|
||||
"""`git ls-files -s` emits one row per index STAGE, so a path in conflict appears up to three
|
||||
times. Without de-duplication `--all` opens the worktree file once per row: the same warning
|
||||
three times over, and a population count that is not a count of files."""
|
||||
git(repo, "checkout", "-qb", "side")
|
||||
(repo / "docs" / "seed.md").write_text(f"side\n{NARRATIVE}\n", encoding="utf-8")
|
||||
commit(repo)
|
||||
git(repo, "checkout", "-q", "-")
|
||||
(repo / "docs" / "seed.md").write_text(f"main\n{NARRATIVE}\n", encoding="utf-8")
|
||||
commit(repo)
|
||||
merge = subprocess.run(["git", "merge", "side"], cwd=repo, capture_output=True, text=True)
|
||||
assert merge.returncode != 0, "the fixture must be in conflict"
|
||||
stages = subprocess.run(["git", "ls-files", "-s", "--", "docs/seed.md"], cwd=repo, capture_output=True, text=True)
|
||||
assert stages.stdout.count("\n") == 3, stages.stdout
|
||||
out = run(repo, "--all").stdout
|
||||
assert out.count("::warning") == 1
|
||||
assert "scanned 1 file(s)" in out
|
||||
|
||||
|
||||
def test_a_MIXED_MODE_conflict_is_not_opened(repo: Path) -> None:
|
||||
"""Two stages of DIFFERENT mode under one path — a symlink beside a regular blob — with the
|
||||
worktree holding one of them. A per-row mode test lets the regular row authorise opening the
|
||||
path, which may be the symlink; a path is opened only when EVERY stage is a regular blob.
|
||||
|
||||
Built with `update-index --index-info` rather than a merge: git resolves a symlink/file add/add
|
||||
conflict by RENAMING one side (`h.sh~HEAD`), so a merge never yields this index shape — an
|
||||
older git, a tool, or a hand-edited index can, and `ls-files -s` reports it exactly like this."""
|
||||
(repo / ".claude" / "hooks").mkdir(parents=True)
|
||||
(repo / ".claude" / "hooks" / "h.sh").write_text(f"# {NARRATIVE}\n", encoding="utf-8")
|
||||
commit(repo)
|
||||
blob = subprocess.run(
|
||||
["git", "rev-parse", "HEAD:.claude/hooks/h.sh"], cwd=repo, capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
link = subprocess.run(
|
||||
["git", "hash-object", "-w", "--stdin"], cwd=repo, input="../../web/t.sh", capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
subprocess.run(
|
||||
["git", "update-index", "--index-info"],
|
||||
cwd=repo,
|
||||
input=(
|
||||
"0 0000000000000000000000000000000000000000\t.claude/hooks/h.sh\n"
|
||||
f"100644 {blob} 2\t.claude/hooks/h.sh\n"
|
||||
f"120000 {link} 3\t.claude/hooks/h.sh\n"
|
||||
),
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
stages = subprocess.run(
|
||||
["git", "ls-files", "-s", "--", ".claude/hooks/h.sh"], cwd=repo, capture_output=True, text=True
|
||||
)
|
||||
assert {line.split(" ", 1)[0] for line in stages.stdout.splitlines()} == {"100644", "120000"}, stages.stdout
|
||||
out = run(repo, "--all").stdout
|
||||
assert hits(out) == set()
|
||||
assert "skipped" not in out
|
||||
assert "scanned 1 file(s)" in out # the fixture's docs/seed.md only
|
||||
|
||||
|
||||
def test_a_path_with_a_SPACE_is_scanned_in_diff_mode(repo: Path) -> None:
|
||||
"""Git terminates the `+++` filename field with a TAB when the path contains a space. A parser
|
||||
that keeps the tab asks `is_scanned_path("docs/my notes.md\\t")`, which is False, and the file is
|
||||
dropped with no notice — the scanned-0 channel one level below the config pins. The positive
|
||||
control is the same content at a space-free path in the same diff."""
|
||||
(repo / "docs" / "my notes.md").write_text(f"{NARRATIVE}\n", encoding="utf-8")
|
||||
(repo / "docs" / "mynotes.md").write_text(f"{NARRATIVE}\n", encoding="utf-8")
|
||||
commit(repo)
|
||||
out = run(repo, "--diff", "HEAD~1").stdout
|
||||
assert hits(out) == {("docs/my notes.md", 1), ("docs/mynotes.md", 1)}
|
||||
assert "scanned 2 file(s)" in out
|
||||
|
||||
|
||||
# --- the detector actually detects ---------------------------------------------------------------
|
||||
|
||||
@@ -243,10 +243,10 @@ SENTINEL_CLASSIFIER = (
|
||||
def test_ONE_classifier_TWO_callers_BEHAVIOURALLY():
|
||||
"""The structural property #787 relies on, PROVEN by swapping the file — not by reading source.
|
||||
|
||||
A string-presence check was the seventh test in this change to pass for the wrong reason: cold
|
||||
review re-inlined a BYTE-IDENTICAL copy of the classifier into the merge hook, left a comment
|
||||
naming `scripts/lib/branch-rule-classifier.jq` above it, and the whole suite stayed green at
|
||||
exactly 1062 passed. A comment satisfies a substring assertion. And the byte-identical inline is
|
||||
A string-presence check passes for the wrong reason here: re-inlining a BYTE-IDENTICAL copy of
|
||||
the classifier into the merge hook, with a comment naming
|
||||
`scripts/lib/branch-rule-classifier.jq` above it, left the whole suite green at exactly 1062
|
||||
passed. A comment satisfies a substring assertion, and the byte-identical inline is
|
||||
precisely the drift-creating refactor that matters, because it AGREES on day one — a weakened
|
||||
copy would have been caught behaviourally.
|
||||
|
||||
@@ -281,10 +281,10 @@ def _mutant_tree(tmp_path, source_text):
|
||||
"""The script in a RUNNABLE layout: it resolves `lib/branch-rule-classifier.jq` relative to its
|
||||
own directory, so a bare copy exits 2 before reaching any of its logic.
|
||||
|
||||
This is not incidental plumbing. The first version of the mutation proof below copied only the
|
||||
script, so the mutant died on `classifier not readable` with EMPTY stdout — and an assertion that
|
||||
stdout merely differed from `drift` was satisfied by that empty string. The proof passed for the
|
||||
wrong reason and the `MUTATION` grade it justified was unsupported.
|
||||
This is not incidental plumbing. A mutation proof that copies only the script kills the mutant
|
||||
on `classifier not readable` with EMPTY stdout — and an assertion that stdout merely differs
|
||||
from `drift` is satisfied by that empty string, so the proof passes for the wrong reason and
|
||||
the `MUTATION` grade it justifies is unsupported.
|
||||
"""
|
||||
victim = tmp_path / "mutant.sh"
|
||||
victim.write_text(source_text)
|
||||
@@ -454,9 +454,9 @@ CLASSIFIER = REPO_ROOT / "scripts" / "lib" / "branch-rule-classifier.jq"
|
||||
def classify(payload: str, branch: str = "main") -> tuple[int, str]:
|
||||
"""The classifier ALONE, with no script wrapping it. `check-required-contexts.sh` maps both a
|
||||
declared `unreadable` verdict and a jq CRASH onto the same output word, so a test that only reads
|
||||
that word cannot tell the two apart — cold review demonstrated exactly that by mutating the
|
||||
classifier's structural guard to `if false`, making jq exit 5, and watching the wrapper still
|
||||
print `unreadable`. Asserting the raw verdict is what pins the mechanism."""
|
||||
that word cannot tell the two apart — measured: mutating the classifier's structural guard to
|
||||
`if false` makes jq exit 5 and the wrapper still prints `unreadable`. Asserting the raw verdict
|
||||
is what pins the mechanism."""
|
||||
proc = subprocess.run(
|
||||
["jq", "--arg", "b", branch, "-c", "-f", str(CLASSIFIER)],
|
||||
input=payload,
|
||||
@@ -538,11 +538,12 @@ def test_an_EMPTY_array_is_still_a_real_finding():
|
||||
|
||||
|
||||
def test_a_MALFORMED_rule_name_poisons_even_beside_a_GOOD_branch_name():
|
||||
"""Poison is field-agnostic, which the first draft of the fix got wrong in one direction.
|
||||
"""Poison is field-agnostic, and a short-circuit on `branch_name` gets that wrong in one
|
||||
direction.
|
||||
|
||||
It short-circuited on `branch_name`, so `{"branch_name":42,"rule_name":"main"}` poisoned while
|
||||
`{"branch_name":"main","rule_name":42}` answered `exact` off the good field and never looked at
|
||||
the malformed one — an invariant the comment claimed and the code did not hold. Gitea 1.27.1 does
|
||||
A short-circuiting implementation poisons on `{"branch_name":42,"rule_name":"main"}` while
|
||||
`{"branch_name":"main","rule_name":42}` answers `exact` off the good field and never looks at
|
||||
the malformed one — contradicting the field-agnostic invariant its own comment states. Gitea 1.27.1 does
|
||||
not send that second shape, so this pins a property rather than a bug: "a malformed field poisons
|
||||
the list" has to be true of BOTH fields or it is not the rule that is written down.
|
||||
"""
|
||||
|
||||
@@ -78,7 +78,7 @@ def test_fence_state_does_not_leak_between_comments():
|
||||
assert classify(["Example:\n```\nnot a verdict", verdict("MERGEABLE", HEAD)]) == ("positive", 0)
|
||||
|
||||
|
||||
# --- found by cross-family review of the first fix (all three reproduced before fixing) ----------
|
||||
# --- three more false-opens, all reproduced before fixing ----------------------------------------
|
||||
|
||||
|
||||
def test_falseopen_tilde_fence_is_also_stripped():
|
||||
@@ -106,8 +106,8 @@ def test_falseopen_sha_field_needs_a_right_boundary(suffix):
|
||||
def test_falseopen_a_body_cannot_forge_a_comment_boundary():
|
||||
"""The separator between comments must be out-of-band.
|
||||
|
||||
An earlier version joined bodies with a literal `\\x01BODY-BOUNDARY\\x01` line. A comment
|
||||
containing that line could reset fence state mid-body and expose a verdict still inside an
|
||||
Joining bodies with a literal `\\x01BODY-BOUNDARY\\x01` line is forgeable: a comment
|
||||
containing that line resets fence state mid-body and exposes a verdict still inside an
|
||||
unclosed fence — an in-band delimiter is forgeable by whoever writes the data, and here that is
|
||||
anyone who can comment on the PR.
|
||||
"""
|
||||
@@ -121,7 +121,7 @@ def test_a_valid_verdict_may_carry_trailing_prose():
|
||||
assert classify([f"{verdict('MERGEABLE', HEAD)} (all findings resolved)"]) == ("positive", 0)
|
||||
|
||||
|
||||
# --- found by a third review round: fence LENGTH, and masked reader failures ---------------------
|
||||
# --- fence LENGTH, and masked reader failures ----------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -180,7 +180,7 @@ def test_a_fenced_example_alongside_a_real_blocked_verdict_still_blocks():
|
||||
assert classify([fenced, verdict("BLOCKED", HEAD)]) == ("negative", 0)
|
||||
|
||||
|
||||
# --- found by a fifth review round: raw HTML is the third code-block form ------------------------
|
||||
# --- raw HTML is the third code-block form -------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -361,7 +361,7 @@ def test_missing_head_argument_is_an_input_error():
|
||||
assert p.returncode == 2
|
||||
|
||||
|
||||
# --- found by re-review of the fix commit: the READ path must fail closed too ------------------
|
||||
# --- the READ path must fail closed too --------------------------------------------------------
|
||||
|
||||
|
||||
def test_a_hostile_tmpdir_does_not_hide_a_verdict():
|
||||
@@ -443,29 +443,26 @@ NEGATIVE_WORDS = ["BLOCKED", "NOT-MERGEABLE"]
|
||||
def test_each_verdict_word_retains_its_established_polarity(word, expected):
|
||||
"""The five established tokens still classify the way reviewers rely on. READ side only.
|
||||
|
||||
NAMED FOR WHAT IT IS. An earlier version called itself a disjointness test and its docstring
|
||||
said "no word may be in both vocabularies". Cold review was right that this is an overclaim:
|
||||
pinning the observable classification of five hardcoded tokens cannot establish a universal
|
||||
property over every token the scripts accept, and for THAT property an omitted token is not a
|
||||
vacuous pass — it is precisely the untested member. This is a polarity regression, and the
|
||||
honest scope is the five words listed. Universal disjointness needs one shared vocabulary both
|
||||
scripts read (ersatztv#788).
|
||||
NAMED FOR WHAT IT IS. Calling this a disjointness test — "no word may be in both vocabularies"
|
||||
— is an overclaim: pinning the observable classification of five hardcoded tokens cannot
|
||||
establish a universal property over every token the scripts accept, and for THAT property an
|
||||
omitted token is not a vacuous pass — it is precisely the untested member. This is a polarity
|
||||
regression, and the honest scope is the five words listed. Universal disjointness needs one
|
||||
shared vocabulary both scripts read (ersatztv#788).
|
||||
|
||||
WHY IT SURVIVED ITS PARENT. That test asserted set equality between the write side's `case` arms
|
||||
and the read side's `POS_RE`/`NEG_RE` by parsing shell with regexes, and six review rounds each
|
||||
found another construction it mis-read; it was deleted rather than patched a seventh time. But
|
||||
and the read side's `POS_RE`/`NEG_RE` by parsing shell with regexes, and six successive fixes each
|
||||
met another construction it mis-read; it was deleted rather than patched a seventh time. But
|
||||
it carried a SECOND, separable invariant that had nothing to do with parsing, and deleting the
|
||||
file silently took that with it — caught by cold review of the deletion, which is exactly the
|
||||
"enumerate what a workaround provided before removing it" rule
|
||||
(`process.enumerate-workaround-behaviors-before-deleting`).
|
||||
file silently took that with it — exactly the "enumerate what a workaround provided before
|
||||
removing it" rule (`process.enumerate-workaround-behaviors-before-deleting`).
|
||||
|
||||
WHY THIS VERSION IS SOUND WHERE ITS PARENT WAS NOT. It EXECUTES the real classifier instead of
|
||||
reading its source, so no shell construction can fool it. `check-review-verdict.sh` sets
|
||||
`is_pos` and `is_neg` from two INDEPENDENT `grep -iqE` calls, so a word in both patterns sets
|
||||
both flags and precedence decides.
|
||||
|
||||
WHICH DIRECTION THIS CATCHES, MEASURED RATHER THAN REASONED — the first draft of this docstring
|
||||
asserted it and was checked afterwards, which is the wrong order. `check-review-verdict.sh:284`
|
||||
WHICH DIRECTION THIS CATCHES, MEASURED RATHER THAN REASONED. `check-review-verdict.sh:284`
|
||||
reads `if [ "$is_pos" = 1 ]; then head_pos=1; else head_neg=1; fi`, so `is_pos` wins per line
|
||||
and an overlapping word resolves POSITIVE. Both mutations were run against this test:
|
||||
|
||||
|
||||
@@ -84,8 +84,7 @@ def _workflow_job_contexts() -> dict[str, str]:
|
||||
job runs `test_ci_release_path_scan_job.py`, whose harness executes THIS file inside a poisoned
|
||||
COPY of the tree that is not a git repository, where `git ls-files` exits 128. (The scan job
|
||||
itself runs in a real checkout — the non-git tree is the harness's, which is why the constraint
|
||||
is real but the shorthand "the scan job runs this in a non-git copy" is imprecise.) An earlier
|
||||
draft of this change walked straight into it.
|
||||
is real but the shorthand "the scan job runs this in a non-git copy" is imprecise.)
|
||||
|
||||
SYNTHESIZED and matched by equality, never parsed. Gitea names an Actions context
|
||||
`<workflow name> / <job name> (<event>)`, and both names are already in the YAML — so building
|
||||
@@ -224,9 +223,9 @@ MARKED_JOBS = _derive_marked_jobs()
|
||||
# `build`'s "Smoke + IPTV E2E" step runs AFTER `Build and push`, so on a `v*` tag the image is
|
||||
# already in the registry as the release candidate and this step is what decides whether it was ever
|
||||
# booted. A drop there publishes an unsmoked candidate and goes green, and `DeployStack jazz-media`
|
||||
# promotes exactly that image — not a "smaller cost than a required context", which is what an
|
||||
# earlier draft of the decision record claimed. Its two payloads moved into the step's `env:`, which
|
||||
# is the free half of the escape hatch, so the ban costs nothing there.
|
||||
# promotes exactly that image — not a "smaller cost than a required context". Its two payloads
|
||||
# moved into the step's `env:`, which is the free half of the escape hatch, so the ban costs nothing
|
||||
# there.
|
||||
#
|
||||
# `functional-e2e` is deliberately NOT here even though it is delimiter-free today: it is advisory by
|
||||
# declaration (not a required check, not a `needs:` of `build`), so the rule stays "ban where a drop
|
||||
@@ -249,7 +248,7 @@ assert set(MARKED_JOBS) <= set(DELIMITER_BAN_JOBS), (
|
||||
"still carry the delimiter that makes the runner drop the step in the first place (ersatztv#756)."
|
||||
)
|
||||
|
||||
# THE RAW OPENER, not a closed `${{ … }}` pair — found by cold review. The runner's rewrite is
|
||||
# THE RAW OPENER, not a closed `${{ … }}` pair. The runner's rewrite is
|
||||
# triggered by the OPENER; a closed-pair regex therefore misses `# ${{` with no closer, which would
|
||||
# sail through an "absolute" ban and still drop the step. Nothing in these jobs may contain the
|
||||
# opener at all, so matching it directly is both simpler and strictly stronger. `_EXPR` is kept for
|
||||
@@ -334,7 +333,7 @@ def test_the_delimiter_banned_jobs_have_NO_expression_delimiter_in_any_run_body(
|
||||
|
||||
The scope is `DELIMITER_BAN_JOBS` — see the comment there for why `build` is in
|
||||
and `functional-e2e` is not. Do NOT restate this docstring as "scoped to the required pair":
|
||||
round 2 moved `build`'s two payloads into `env:` and brought it into the ban, and this docstring
|
||||
`build`'s two payloads moved into `env:`, which brought it into the ban, and this docstring
|
||||
sits directly above the decorator that parametrises over the wider set.
|
||||
|
||||
The escape hatch when a value really is needed is the step's `env:` block, which is interpolated
|
||||
@@ -444,7 +443,7 @@ def test_the_guard_expects_EXACTLY_the_set_of_marked_keys_in_the_right_bucket(jo
|
||||
f"{sorted(keys)}. Keys marked-but-unexpected are unguarded drops; keys "
|
||||
"expected-but-unmarked redden every run."
|
||||
)
|
||||
# AN UNRECOGNISED `if:` IS REJECTED, never silently bucketed — found by both reviewers. The
|
||||
# AN UNRECOGNISED `if:` IS REJECTED, never silently bucketed. The
|
||||
# protocol only knows two conditions: absent (always runs) and exactly the skip gate. A marked
|
||||
# step carrying a third condition (`if: github.event_name == 'push'`, or the `always() && <gate>`
|
||||
# spelling the peak-anon steps already use) would fall through to "always", the suite would go
|
||||
@@ -534,13 +533,13 @@ def test_the_guards_OWN_body_cannot_be_dropped_by_the_mechanism_it_guards_agains
|
||||
f"the '{job}' guard is no longer a bare invocation: {guard['run']!r}. Keeping it to one "
|
||||
"command is what makes a delimiter impossible rather than merely absent."
|
||||
)
|
||||
# THE VALUES, not just the names — found by cold review. Asserting the keys alone accepts
|
||||
# THE VALUES, not just the names. Asserting the keys alone accepts
|
||||
# `ETV_DOCS_ONLY: ${{ steps.detect.outputs.doc_only }}` (note the typo), which names a real
|
||||
# context so the repo-wide expression check passes it too. The guard would then read an EMPTY
|
||||
# value on a docs-only run, demand the gated steps that were correctly skipped, and redden a
|
||||
# REQUIRED context on every docs-only PR.
|
||||
# THE TWO MAPPINGS MUST BE PRESENT AND CORRECT — but this deliberately does NOT demand that the
|
||||
# `env:` block contain ONLY them. An earlier version compared the whole dict, which false-redded
|
||||
# `env:` block contain ONLY them. Comparing the whole dict false-reds
|
||||
# on adding an unrelated variable (an `LC_ALL`, say) and on the equally-valid `${{x}}` spacing;
|
||||
# a red here blocks every merge through the combined status, so brittleness is a real cost and
|
||||
# not a free strictness win. Whitespace inside the delimiters is normalised for the same reason.
|
||||
@@ -576,7 +575,7 @@ def _mark_line(step) -> str:
|
||||
|
||||
|
||||
# THE GATE VALUES DEFAULT TO `"false"`, WHICH IS WHAT THE RUNNER ACTUALLY SENDS — and getting this
|
||||
# wrong made the whole suite blind. Found by cold review, which demonstrated it: every behavioural
|
||||
# wrong made the whole suite blind. DEMONSTRATED: every behavioural
|
||||
# test used to leave these UNSET, so the guard was never once driven at its production values. Change
|
||||
# the gate in `ci-step-ran.sh` from `= "true"` to `-n` — a one-token regression — and all 30 tests
|
||||
# stayed GREEN while the guard, run with the real environment, reported
|
||||
@@ -596,8 +595,8 @@ def _mark_line(step) -> str:
|
||||
# CATCH, which is exactly why the gate must treat anything that is not `true` as "widen what is
|
||||
# required". `None` (unset) is the same case reached a different way.
|
||||
#
|
||||
# A test double is an assertion about what the real system sends, and the earlier version of this one
|
||||
# was wrong about the only field the guard branches on.
|
||||
# A test double is an assertion about what the real system sends, and this one has already been
|
||||
# wrong about the only field the guard branches on.
|
||||
GATE_VALUES_IN_THE_WILD = ("false", "", None)
|
||||
|
||||
|
||||
@@ -810,10 +809,10 @@ def test_a_key_is_matched_WHOLE_not_as_a_substring(tmp_path):
|
||||
def test_a_degraded_run_IDENTITY_refuses_rather_than_sharing_a_marker_path(tmp_path):
|
||||
"""`GITHUB_RUN_ID` absent must REFUSE, not fall back to a name every run shares.
|
||||
|
||||
The first version of `marker_path` defaulted to `nojob`/`norunid`/`1`. Those are reusable, so a
|
||||
A `marker_path` defaulting to `nojob`/`norunid`/`1` uses names every run shares, so a
|
||||
leftover marker from any earlier run on the host would satisfy the guard on a run whose step was
|
||||
dropped — a silent PASS, which is the precise failure the run-keying exists to remove,
|
||||
reintroduced by the code implementing it. Found by cold review.
|
||||
reintroduced by the code implementing it.
|
||||
|
||||
Asserted on BOTH sub-commands: a refusal that only `assert` honoured would let `mark` write to a
|
||||
shared path and leave the two disagreeing about where the file is.
|
||||
@@ -849,8 +848,8 @@ def test_the_marker_identity_is_REPORTED_on_stdout_every_run(tmp_path):
|
||||
and the same for `migrations`. That is what promoted the variable from warn-and-default to
|
||||
required.
|
||||
|
||||
Asserted because cold review demonstrated three mutations of this reporting — deleting the echo,
|
||||
mis-deriving the attempt, inverting the provenance — all surviving a 50-green suite. It is a
|
||||
Asserted because three mutations of this reporting — deleting the echo,
|
||||
mis-deriving the attempt, inverting the provenance — all survived a 50-green suite. It is a
|
||||
documented contract (the record's `mechanics:`), and a future reader is told to trust it.
|
||||
"""
|
||||
marks = [_mark_line(s) for s, _ in _marked("test")]
|
||||
@@ -866,8 +865,7 @@ def test_the_marker_identity_is_REPORTED_on_stdout_every_run(tmp_path):
|
||||
|
||||
|
||||
def test_a_skip_gate_that_empties_the_expected_set_REFUSES(tmp_path):
|
||||
"""The anti-vacuity check has to run AFTER the gate, not only on argv. Cold review reproduced
|
||||
this exactly:
|
||||
"""The anti-vacuity check has to run AFTER the gate, not only on argv. REPRODUCED exactly:
|
||||
|
||||
ETV_DOCS_ONLY=true … assert --always --gated foo
|
||||
-> "All 0 expected step(s) executed", exit 0
|
||||
@@ -888,14 +886,13 @@ def test_a_skip_gate_that_empties_the_expected_set_REFUSES(tmp_path):
|
||||
"docs_only", ["true", "false", "", None], ids=lambda v: f"docs-{v if v is not None else 'unset'}"
|
||||
)
|
||||
def test_the_skip_gate_over_the_WHOLE_value_matrix(docs_only, revalidate, tmp_path):
|
||||
"""Every combination of the two gate values, not just the diagonal — cold review's last finding.
|
||||
"""Every combination of the two gate values, not just the diagonal.
|
||||
|
||||
Round 3 fixed the suite's blindness to the production value `false`, but still only exercised
|
||||
matched pairs and single-`true` cases. `(true, true)` is REACHABLE — a docs-only PR merged to
|
||||
`main` whose tree was already validated sets both — and an exclusive-or regression would pass
|
||||
every other test here while demanding all the gated markers on a run that legitimately skipped
|
||||
those steps. That reddens BOTH required contexts, which is the false-red direction: it deadlocks
|
||||
every merge rather than letting one through.
|
||||
Matched pairs and single-`true` cases are not enough on their own. `(true, true)` is REACHABLE
|
||||
— a docs-only PR merged to `main` whose tree was already validated sets both — and an
|
||||
exclusive-or regression would pass every other test here while demanding all the gated markers
|
||||
on a run that legitimately skipped those steps. That reddens BOTH required contexts, which is
|
||||
the false-red direction: it deadlocks every merge rather than letting one through.
|
||||
|
||||
The property asserted is the whole contract in one line: with only the `--always` keys marked,
|
||||
the guard passes exactly when the gate says the gated steps were skipped — `true` in EITHER
|
||||
|
||||
@@ -69,8 +69,8 @@ _DOC = yaml.safe_load(WORKFLOW.read_text())
|
||||
# `BARE_RUNNER_JOBS` — two hand-written literals enumerating jobs by name.
|
||||
#
|
||||
# WHY A LITERAL WAS THERE AT ALL, because the reason is real and the replacement has to answer it.
|
||||
# The first version of this file compared `container_jobs(doc)` against `pinned_jobs(doc)` and
|
||||
# nothing else. That is blind to the mutation that matters most: delete a `container:` block and the
|
||||
# Comparing `container_jobs(doc)` against `pinned_jobs(doc)` and
|
||||
# nothing else is blind to the mutation that matters most: delete a `container:` block and the
|
||||
# job leaves BOTH sets together, so the comparison stays balanced and reports green — the very
|
||||
# fail-open this file exists to close, reproduced one level up. A guard whose expected set shrinks
|
||||
# in step with the thing it guards is not a guard. So the population needs an ANCHOR that does not
|
||||
@@ -82,7 +82,7 @@ _DOC = yaml.safe_load(WORKFLOW.read_text())
|
||||
# instead of in a Python file three directories away — so it is reachable by anyone reading that job,
|
||||
# and it moves with the job when the job is renamed or removed.
|
||||
#
|
||||
# NOT "in the same diff hunk", which an earlier version of this note claimed. MEASURED distance from
|
||||
# NOT "in the same diff hunk". MEASURED distance from
|
||||
# each `container:` block to its marker: test 6 lines, functional-e2e 6, api-docs 7, format 7 —
|
||||
# and migrations 58, because the whole `services:` block sits between them. A container-only edit to
|
||||
# `migrations` shows no marker in its hunk at all. Adjacency is a convenience that mostly holds; the
|
||||
@@ -269,8 +269,8 @@ def jobs_whose_steps_need_the_toolchain(doc) -> dict[str, set[str]]:
|
||||
|
||||
ONE-DIRECTIONAL ON PURPOSE, and this is the limit to state rather than let review find. Step
|
||||
text is a NECESSARY condition, never a sufficient one: a job whose ONLY toolchain use sat behind
|
||||
a script would be invisible here. MEASURED, because the first version of this note asserted the
|
||||
blind spot was live and it is not — all five declared toolchain jobs are detected directly,
|
||||
a script would be invisible here. MEASURED: that blind spot is NOT live —
|
||||
all five declared toolchain jobs are detected directly,
|
||||
`functional-e2e` included, which runs `dotnet restore` and `npm ci` in its own steps before
|
||||
handing off to `scripts/e2e-local.sh`. The blind-spot set is EMPTY today. The converse —
|
||||
'declared toolchain, so some step must name a tool' — is still not asserted, because a future
|
||||
@@ -499,8 +499,8 @@ def test_docker_build_is_the_ONLY_workflow_pinning_the_toolchain_image():
|
||||
guard, no single-tag check and no partition, silently, while `pin_population_faults`'s own error
|
||||
text claims "All container jobs must run the same toolchain image".
|
||||
|
||||
Found by cold review, which correctly noted this file criticises `MARKED_JOBS` for exactly this
|
||||
and then shipped the same shape without even the dated comment `MARKED_JOBS` then carried.
|
||||
This file criticised `MARKED_JOBS` for exactly this and then shipped the same shape, without
|
||||
even the dated comment `MARKED_JOBS` then carried.
|
||||
`MARKED_JOBS` has since been derived (#787), and `TOOLCHAIN_JOBS`/`BARE_RUNNER_JOBS` — the
|
||||
literals this file once carried — were replaced by the per-job `env.CI_EXECUTION_CLASS` marker
|
||||
in #789. This scope check is what still keeps `WORKFLOW` honest: the marker says which class a
|
||||
|
||||
@@ -6,8 +6,8 @@ But the ban was enforced ONLY by `test_the_delimiter_banned_jobs_have_NO_express
|
||||
in `script-tests` — `on: pull_request`, not a required context. Nothing re-checked it on a `v*` tag
|
||||
push, which is exactly when the candidate is published.
|
||||
|
||||
WHY A JOB AND NOT A STEP IN `build`, and why this file is structural. The first cut of #767 put a
|
||||
bespoke stdlib scanner in `build` itself. Two independent reviews killed it on two counts, and both
|
||||
WHY A JOB AND NOT A STEP IN `build`, and why this file is structural. A bespoke stdlib scanner in
|
||||
`build` itself is wrong on two counts, and both
|
||||
are worth keeping written down because both are easy to re-invent:
|
||||
|
||||
* A guard step inside `build` cannot protect `build`. If the runner drops it, the job carries on
|
||||
@@ -15,9 +15,9 @@ are worth keeping written down because both are easy to re-invent:
|
||||
cannot be dropped", but the only thing enforcing THAT was the same PR-only test being
|
||||
backstopped. Circular. As a `needs:` of `build`, a red here means `build` never runs at all.
|
||||
* The bespoke scanner hand-parsed YAML (to avoid provisioning PyYAML on `build`'s bare runner) and
|
||||
had ~10 false NEGATIVES within one review round — flow mappings, a quoted `"run":` key, aliases,
|
||||
had ~10 false NEGATIVES — flow mappings, a quoted `"run":` key, aliases,
|
||||
multiline quoted scalars. It was strictly WEAKER than the check it backstopped, in the only
|
||||
direction that matters. The fix was to delete it and run the real PyYAML-based test, which needs
|
||||
direction that matters. This job runs the real PyYAML-based test instead, which needs
|
||||
no second definition of "what is a `run:` body" and so has no drift surface.
|
||||
|
||||
The detection ALGORITHM is not reimplemented here — it lives in `test_ci_dropped_step_guard.py`, and
|
||||
@@ -26,9 +26,9 @@ exists, `build` depends on it, nothing can skip it, its own steps cannot be sile
|
||||
ONE end-to-end probe that executes the scan step's real command against a poisoned copy of the repo
|
||||
and requires it to fail.
|
||||
|
||||
THAT PROBE IS THE LOAD-BEARING TEST, and it exists because shape assertions lost twice. Round 2
|
||||
replaced a substring check with checks ABOUT the command — bare-token argument, no `-k`, no `||` —
|
||||
and round 3 then disarmed the gate seven more ways that all left the suite green: `echo`ing the
|
||||
THAT PROBE IS THE LOAD-BEARING TEST, and it exists because shape assertions lost twice. Checks
|
||||
ABOUT the command — bare-token argument, no `-k`, no `||` — replaced a substring check,
|
||||
and the gate was then disarmed seven more ways that all left the suite green: `echo`ing the
|
||||
command instead of running it, flags moved past a `\` continuation, a trailing `exit 0`,
|
||||
`if false; then … fi`, `set +e`, and `PYTEST_ADDOPTS` in the step's `env:`. Two further disarms lived
|
||||
in the sibling module where no shape check could ever reach — the ban test's parametrize list swapped
|
||||
@@ -36,10 +36,10 @@ to `MARKED_JOBS`, and its opener regex neutered. Running the command settles eve
|
||||
because it asks the only question that matters: with a delimiter in `build`'s `Smoke` body, does this
|
||||
command fail? The shape checks are kept as a faster, more specific signal, not as the guarantee.
|
||||
|
||||
ROUND 4 THEN FOUND THE TWIN OF THE ROUND-3 FIX, which is the reusable lesson here: fixing the STEP
|
||||
THE TWIN OF THAT FIX IS THE REUSABLE LESSON HERE: fixing the STEP
|
||||
`env:` tier did not generalise, and the same `PYTEST_ADDOPTS` disarm placed one tier up — on the JOB
|
||||
— defeated the probe, because the probe reconstructed only the step's env. It now layers all three
|
||||
tiers (workflow, job, step). The same round found that `needs:` is not by itself a gate: an
|
||||
tiers (workflow, job, step). Nor is `needs:` by itself a gate: an
|
||||
`always()` in `build`'s `if:` downgrades the edge to mere ordering, and the delimiter ban does not
|
||||
cover `if:` expressions, so nothing else would have objected.
|
||||
|
||||
@@ -51,7 +51,7 @@ poisoned tree while the probe reports healthy. That one is BANNED rather than mo
|
||||
be a second implementation of precisely the kind #767 already deleted once. The rule after any fix
|
||||
here: enumerate the tiers and the twin, and where a tier cannot be observed, forbid it.
|
||||
|
||||
(Two disarms review reported were checked and are NOT real: `--ignore=` and a `conftest.py`
|
||||
(Two proposed disarms were checked and are NOT real: `--ignore=` and a `conftest.py`
|
||||
`collect_ignore` do not suppress a file pytest was given explicitly as an argument — measured, the
|
||||
ban test still ran and still failed. Recorded so they are not re-litigated. But note what refuting
|
||||
them did NOT establish: the WORKING attacks through that same configuration channel — a repo-root
|
||||
@@ -403,14 +403,14 @@ def test_the_scan_step_REALLY_FAILS_on_a_poisoned_workflow(tmp_path):
|
||||
It executes the ban step's actual `run:` body — with its actual `env:` — against a copy of the
|
||||
repo whose `build`/`Smoke` body carries an injected delimiter, and requires a non-zero exit.
|
||||
|
||||
This exists because shape checks lost. An earlier round asserted things ABOUT the command
|
||||
(the ban test's path is a bare token, no `-k`/`--deselect`, no `||`) and review then disarmed the
|
||||
gate anyway, every time leaving the suite green: `echo`ing the command instead of running it,
|
||||
This exists because shape checks lost. With things asserted ABOUT the command
|
||||
(the ban test's path is a bare token, no `-k`/`--deselect`, no `||`), the gate was disarmed
|
||||
anyway, every time leaving the suite green: `echo`ing the command instead of running it,
|
||||
flags moved past a `\\` continuation, a trailing `exit 0`, `if false; then … fi`, `set +e`, and
|
||||
`PYTEST_ADDOPTS` in the step's `env:`. Enumerating shapes of a command cannot win; running the
|
||||
command settles them together, including the two in the sibling module that no shape check could
|
||||
reach (a parametrize list swapped to `MARKED_JOBS`, and a neutered opener regex). No count is
|
||||
given on purpose — an earlier draft's tally disagreed with its own list, and the number was never
|
||||
given on purpose — a tally here has already disagreed with its own list, and the number was never
|
||||
the point.
|
||||
|
||||
The negative control below is what stops this passing for the wrong reason. And note the limit of
|
||||
@@ -457,7 +457,7 @@ def test_the_PROOF_SCRIPT_itself_refuses_when_the_ban_is_deselected(tmp_path):
|
||||
|
||||
The disarm reproduced here is the real one: a repo-root `conftest.py` that deselects the ban test
|
||||
entirely, which makes pytest exit 5 (nothing collected) rather than fail — the exact reading that
|
||||
made an earlier draft of the script report the gate healthy while an unsmoked candidate would
|
||||
reports the gate healthy while an unsmoked candidate would
|
||||
publish.
|
||||
"""
|
||||
repo = _repo_copy(tmp_path)
|
||||
@@ -479,8 +479,8 @@ def test_the_PROOF_SCRIPT_itself_refuses_when_the_ban_is_deselected(tmp_path):
|
||||
# NON-ZERO IS NOT ENOUGH — the script could exit non-zero because its own harness broke (a moved
|
||||
# anchor, an unwritable tree, no python3), which would make this test pass while proving nothing.
|
||||
# Require one of its real verdicts. Measured, this path yields "pytest exited 5" (nothing
|
||||
# collected), which is precisely the deselection disarm and precisely the reading an earlier draft
|
||||
# mistook for success.
|
||||
# collected), which is precisely the deselection disarm and precisely the reading that looks like
|
||||
# success.
|
||||
combined = res.stdout + res.stderr
|
||||
# THE SPECIFIC branch, not a disjunction over the script's verdicts. Measured: a total deselect
|
||||
# makes pytest exit 5, so this lands on "cannot prove anything". Accepting any verdict would let
|
||||
@@ -498,8 +498,8 @@ def test_the_PROOF_SCRIPT_refuses_when_the_WRONG_test_fails(tmp_path):
|
||||
"""The third branch, which the aggregate 'deselect ⇒ non-zero' control does not reach.
|
||||
|
||||
Deselecting only the `[build]` parametrisation while some unrelated test fails gives pytest exit
|
||||
1 — a real test failure, just not the one that proves anything. An earlier draft read that as
|
||||
success. Reproduced here because that branch was added to fix a live bug and was otherwise
|
||||
1 — a real test failure, just not the one that proves anything.
|
||||
Reproduced here because that branch was added to fix a live bug and was otherwise
|
||||
exercised by nothing: making it unreachable left both guard files green.
|
||||
"""
|
||||
repo = _repo_copy(tmp_path)
|
||||
@@ -509,8 +509,7 @@ def test_the_PROOF_SCRIPT_refuses_when_the_WRONG_test_fails(tmp_path):
|
||||
)
|
||||
# The unrelated failure has to live INSIDE the ban file: the script runs that file and nothing
|
||||
# else, so a failing test in a sibling module is never collected and the run would exit 0 —
|
||||
# landing on the "not enforcing" branch instead of the one under test. (First draft of this test
|
||||
# did exactly that and was red for the wrong reason.)
|
||||
# landing on the "not enforcing" branch instead of the one under test.
|
||||
ban = repo / BAN_TEST_FILE
|
||||
ban.write_text(ban.read_text() + "\n\ndef test_an_unrelated_failure_for_this_probe():\n assert False\n")
|
||||
res = subprocess.run(
|
||||
|
||||
@@ -152,7 +152,7 @@ def test_MUTATION_a_deleted_tag_is_reported_as_a_failure(preflight):
|
||||
|
||||
@pytest.mark.parametrize("code", ["TRANSPORT", "503"])
|
||||
def test_an_unknown_answer_FAILS_and_is_not_reported_as_gone(preflight, code):
|
||||
"""The first draft warned and exited 0 here, which is how a preflight becomes a no-op.
|
||||
"""Warning and exiting 0 here is how a preflight becomes a no-op.
|
||||
|
||||
A missing `curl`, a moved registry or a DNS change all land in this arm, and each would have
|
||||
been green forever. It fails — but with its own wording, because "could not verify" and "IS
|
||||
|
||||
@@ -5,7 +5,7 @@ saying, per schema, whether the SPA must annotate it `Complete<T>`, must not, or
|
||||
its POPULATION (which schemas can drop a member) from the generated types and asserts set equality
|
||||
against those keys — so a new optional member cannot ship undispositioned.
|
||||
|
||||
It did not check the VALUE, and cold review demonstrated that the value is a kill switch: flip
|
||||
It did not check the VALUE, and the value is a kill switch, measured: flip
|
||||
`MultiCollectionItemRequest` from `ANNOTATED` to `CREATE` and delete the `Complete<…>` from
|
||||
`MultiCollectionsScreen.toItemRequest`, and the whole suite stays green while the defect #807 exists
|
||||
to prevent — every weight silently reset to 1 on save — is live again. The row's own note still said
|
||||
|
||||
@@ -915,10 +915,9 @@ def test_a_wing_root_README_is_exempt_by_PATH_not_by_basename(tmp_path):
|
||||
"""`docs/decisions/archive/README.md` really exists (a hand-written directory README), so it
|
||||
must be exempt — but by exact relative path, not by basename.
|
||||
|
||||
An earlier version excluded ANY wing-root `README.md` on the stated grounds that no such file
|
||||
existed. That was false, and it would additionally have exempted a future
|
||||
`records/README.md` — reintroducing the basename hole one directory over, in the wing that
|
||||
matters most."""
|
||||
Excluding ANY wing-root `README.md` on the grounds that no such file exists is false, and it
|
||||
would additionally exempt a future `records/README.md` — reintroducing the basename hole one
|
||||
directory over, in the wing that matters most."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "good.md").write_text(_GOOD)
|
||||
(archive / "README.md").write_text("# archive\n\nHand-written directory README.\n")
|
||||
@@ -1067,7 +1066,7 @@ def test_ceiling_calibration_detects_drift_in_BOTH_directions():
|
||||
assert not dv.ceiling_calibration(recs, 10).marks_tail, "below p90 it cuts into the bulk"
|
||||
assert not dv.ceiling_calibration(recs, 999).marks_tail, "above p95 it is parked among outliers"
|
||||
|
||||
# BOTH ends of `marks_tail` are inclusive. Review found the upper one unpinned — `ceiling <= p95`
|
||||
# BOTH ends of `marks_tail` are inclusive. The upper one was unpinned — `ceiling <= p95`
|
||||
# mutated to `<` survived the whole suite. It is notice-only rather than blocking, but an
|
||||
# unpinned boundary is how a documented claim quietly stops being true.
|
||||
assert dv.ceiling_calibration(recs, 20).marks_tail, "p90 itself must satisfy the lower bound"
|
||||
@@ -1083,10 +1082,10 @@ def test_ceiling_calibration_detects_drift_in_BOTH_directions():
|
||||
def test_the_coarse_bound_REJECTS_a_badly_placed_ceiling():
|
||||
"""The blocking property must have teeth.
|
||||
|
||||
Review's strongest finding on the first draft: a floor of `fraction_over > 0` was nearly
|
||||
unfalsifiable — measured on the live corpus it accepted every ceiling from 39 to 229, including
|
||||
the ceiling of 200 the docstring itself offered as the case it catches, because one 230-line
|
||||
record keeps the count nonzero. A FRACTION floor is what restores the teeth.
|
||||
A floor of `fraction_over > 0` is nearly unfalsifiable — measured on the live corpus it
|
||||
accepted every ceiling from 39 to 229, including the ceiling of 200 the docstring itself
|
||||
offered as the case it catches, because one 230-line record keeps the count nonzero. A
|
||||
FRACTION floor is what restores the teeth.
|
||||
|
||||
The rejections are pinned on a SYNTHETIC distribution: asserting that a specific absurd ceiling
|
||||
stays rejected by the live corpus is itself growth-coupled (three new 200+ line records flip the
|
||||
@@ -1094,9 +1093,9 @@ def test_the_coarse_bound_REJECTS_a_badly_placed_ceiling():
|
||||
"""
|
||||
# The TEETH are demonstrated on an owned distribution, for the reason in
|
||||
# `test_v4_would_have_reddened_where_v5_holds`: an assertion that a specific absurd ceiling is
|
||||
# rejected by the LIVE corpus is itself growth-coupled (review found that three new 200+ line
|
||||
# records would flip the 200 arm). 100 records of 30 lines and one of 230 — an outlier-only
|
||||
# tail, which is precisely the shape a badly-placed ceiling fails to distinguish.
|
||||
# rejected by the LIVE corpus is itself growth-coupled (three new 200+ line records would flip
|
||||
# the 200 arm). 100 records of 30 lines and one of 230 — an outlier-only tail, which is
|
||||
# precisely the shape a badly-placed ceiling fails to distinguish.
|
||||
synthetic = [_rec_body(f"a.s{i}", 30) for i in range(100)] + [_rec_body("a.outlier", 230)]
|
||||
|
||||
for bad in (200, 229, 230):
|
||||
@@ -1121,7 +1120,7 @@ def test_ceiling_calibration_is_empty_safe():
|
||||
|
||||
|
||||
def test_the_minority_band_BOUNDARIES_are_exactly_where_documented():
|
||||
"""Pins both constants AND both inclusivities, which review found entirely unmutated.
|
||||
"""Pins both constants AND both inclusivities, which were entirely unmutated.
|
||||
|
||||
Mutating `0.02 -> 0.03`, `0.25 -> 0.30`, or either `<=` to `<` passed all eight calibration
|
||||
tests. These are not free parameters — they ARE the documented CI-red thresholds, so a silent
|
||||
@@ -1164,7 +1163,7 @@ def test_the_minority_band_BOUNDARIES_are_exactly_where_documented():
|
||||
|
||||
|
||||
def test_ceiling_calibration_IGNORES_keyless_records_and_counts_the_rest():
|
||||
"""`n` and the `if r.key` filter, both of which review found unpinned.
|
||||
"""`n` and the `if r.key` filter, both of which were unpinned.
|
||||
|
||||
`main()` passes the UNFILTERED record list, so the filter is load-bearing in production while
|
||||
every live-corpus test hands this function a pre-filtered list — the oracle and production's
|
||||
@@ -1175,9 +1174,9 @@ def test_ceiling_calibration_IGNORES_keyless_records_and_counts_the_rest():
|
||||
`n` itself lost its only pin when the over-tight live test was deleted: a mutation returning
|
||||
`n=1` passed everything, which would print a wrong denominator in the drift notice.
|
||||
|
||||
The oracle is DYNAMIC and runs at two distinct cardinalities on purpose. The first attempt
|
||||
asserted `n == 10` against a ten-record fixture, and review killed it: a mutation returning a
|
||||
constant 10 for every input satisfied it while changing the live denominator from 183 to 10 —
|
||||
The oracle is DYNAMIC and runs at two distinct cardinalities on purpose. Asserting `n == 10`
|
||||
against a ten-record fixture does not hold it: a mutation returning a constant 10 for every
|
||||
input satisfies it while changing the live denominator from 183 to 10 —
|
||||
preserving the exact production defect the test claims to close. A single hardcoded count
|
||||
cannot distinguish "counts the input" from "returns this number".
|
||||
"""
|
||||
@@ -1219,7 +1218,7 @@ def test_v4_would_have_reddened_where_v5_holds():
|
||||
"""The v4-vs-v5 contrast, on a distribution the test OWNS rather than the live corpus.
|
||||
|
||||
THIRD TIME for this defect class in one change, which is why the fix is to remove the coupling
|
||||
rather than patch the instance. Round 1 of review caught it in the drift test; round 2 caught it
|
||||
rather than patch the instance. The same coupling turned up in the drift test and again
|
||||
here, in what looked like a safe `if before.marks_tail:` guard — the GUARD was conditional but
|
||||
the CONCLUSION was still an assertion about live order statistics, and appending 16 ordinary
|
||||
30-line records (nothing long, nothing unusual) makes `after.marks_tail` true again and fires it:
|
||||
@@ -1445,9 +1444,9 @@ def test_main_reports_ceiling_drift_as_a_NOTICE_and_still_exits_0(capsys):
|
||||
red — the entire point of the v5 split.
|
||||
|
||||
The ceiling is DERIVED as one line above the longest record, so it is off the tail boundary by
|
||||
definition. A hardcoded 999 looked safe and was not: review showed ten valid 1000-line records
|
||||
would put p95 at 1000, making 999 calibrated — so the notice would stop firing and this test
|
||||
would go RED, for a corpus change that is nobody's defect.
|
||||
definition. A hardcoded 999 looked safe and was not: ten valid 1000-line records would put p95
|
||||
at 1000, making 999 calibrated — so the notice would stop firing and this test would go RED,
|
||||
for a corpus change that is nobody's defect.
|
||||
"""
|
||||
longest = max(dv.record_prose_lines(r) for r in dl.all_active_records() if r.key)
|
||||
assert dv.main(["--record-ceiling", str(longest + 1)]) == 0
|
||||
@@ -1460,8 +1459,8 @@ def test_main_reports_ceiling_drift_as_a_NOTICE_and_still_exits_0(capsys):
|
||||
def test_main_reports_drift_IFF_the_ceiling_is_off_the_tail_boundary(capsys):
|
||||
"""The complement of the test above — asserting the WIRING, not the corpus's current state.
|
||||
|
||||
The obvious way to write this is `dv.main([]); assert "drifted" not in err`, and that is a trap
|
||||
review caught: `main()` emits the notice exactly when `p90 <= 60 <= p95` is false over the LIVE
|
||||
The obvious way to write this is `dv.main([]); assert "drifted" not in err`, and that is a
|
||||
trap: `main()` emits the notice exactly when `p90 <= 60 <= p95` is false over the LIVE
|
||||
corpus, so such a test fails under precisely the condition #688 exists to stop failing — it
|
||||
would move v4's assertion three functions down and leave it in the same blocking job. Today p90
|
||||
sits exactly ON the ceiling, so ONE new over-ceiling record would have reddened it.
|
||||
@@ -1475,9 +1474,9 @@ def test_main_reports_drift_IFF_the_ceiling_is_off_the_tail_boundary(capsys):
|
||||
assert recs, "corpus is empty — the derived ceilings need at least one record"
|
||||
lengths = sorted(dv.record_prose_lines(r) for r in recs)
|
||||
|
||||
# Both ceilings are DERIVED so each branch is guaranteed by construction, not by luck. Review
|
||||
# caught the earlier version relying on the live 60/999 pair: once one 61-line record lands,
|
||||
# BOTH of those drift, and an UNCONDITIONAL notice would have passed the test.
|
||||
# Both ceilings are DERIVED so each branch is guaranteed by construction, not by luck. Relying
|
||||
# on the live 60/999 pair does not: once one 61-line record lands,
|
||||
# BOTH of those drift, and an UNCONDITIONAL notice would pass the test.
|
||||
# * p90 itself is always calibrated — `p90 <= p90 <= p95` holds for any distribution.
|
||||
# * one line above the longest record is always off the tail — it exceeds p95 by definition.
|
||||
quiet_ceiling = lengths[min(int(len(lengths) * 0.90), len(lengths) - 1)]
|
||||
|
||||
@@ -56,7 +56,7 @@ def _env(**extra: str) -> dict[str, str]:
|
||||
THE MACHINE'S GIT CONFIG IS NEUTRALISED TOO, and that is not belt-and-braces — it is what keeps
|
||||
this file from passing here and failing in CI. A developer checkout has a global `user.email`;
|
||||
the CI container has none, and git refuses to commit without one ("Author identity unknown").
|
||||
The fixtures below commit, so the first version of this file was green locally and red in CI on
|
||||
The fixtures below commit, so without this the file is green locally and red in CI on
|
||||
exactly that. Pointing the global and system config at `os.devnull` makes the ambient
|
||||
configuration UNAVAILABLE rather than merely overridden, so a missing `-c` flag in `_git` fails
|
||||
everywhere instead of only where the machine is unhelpful — the local run reproduces CI by
|
||||
@@ -216,9 +216,9 @@ def test_the_fixture_can_actually_graft(request, fixture_name, depth, tmp_path):
|
||||
"""The raw `--depth=N` fetch the detector must not perform DOES graft this fixture.
|
||||
|
||||
Parametrised over BOTH fixtures and BOTH depths, because a control that covers only one of
|
||||
them leaves the other free to go vacuous unreported — which is what happened: the PR arm was
|
||||
first written against the 31-commit origin, where a depth of 200 has nothing to cut, and it
|
||||
passed against the unfixed script. One control per (fixture, depth) pair the file relies on.
|
||||
them leaves the other free to go vacuous unreported — and that is MEASURED: the PR arm against
|
||||
the 31-commit origin, where a depth of 200 has nothing to cut, passed against the unfixed
|
||||
script. One control per (fixture, depth) pair the file relies on.
|
||||
"""
|
||||
origin = request.getfixturevalue(fixture_name)
|
||||
repo = _clone(origin, tmp_path / f"control-{depth}")
|
||||
@@ -279,8 +279,8 @@ def test_the_push_arm_leaves_a_COMPLETE_clone_complete(origin_url, tmp_path):
|
||||
def test_the_pull_request_arm_leaves_a_COMPLETE_clone_complete(deep_origin_url, tmp_path):
|
||||
"""A complete checkout on the PR arm, N=200. NO PRODUCTION CONSUMER TODAY — deliberately.
|
||||
|
||||
Said plainly because the first version of this docstring got it wrong: on `pull_request` the only
|
||||
jobs running this script are the three `fetch-depth: 2` ones, so the PR arm's `--depth=200` was
|
||||
MEASURED: on `pull_request` the only
|
||||
jobs running this script are the three `fetch-depth: 2` ones, so the PR arm's `--depth=200` is
|
||||
inert rather than latently firing. What this pins is the SHARED CLAUSE — the next `fetch-depth: 0`
|
||||
consumer added to this arm must not silently inherit the graft that cost #836. It is a guard
|
||||
against a future shape, not a reproduction of a shipped one, and grading it as the latter would
|
||||
|
||||
@@ -57,18 +57,18 @@ GUARD_TESTS = ("scripts/tests", ("test_*.py",))
|
||||
KINDS = {"GUARD", "TOOLING", "PROOF"}
|
||||
PROOFS = {"MUTATION", "BEHAVIOUR-ONLY", "NONE"}
|
||||
|
||||
# `scripts/x.sh` AND `scripts/tests/x.py`. The first version omitted the `/`, and the consequence was
|
||||
# not theoretical: the three guard files this inventory shipped with were themselves outside the
|
||||
# population it checked for completeness, so they acquired no rows and the guard stayed green. Cold
|
||||
# review found it. A completeness guard blind to its own author's new guards is the defect this
|
||||
# whole change is about, so the miss is recorded here rather than quietly corrected.
|
||||
# `scripts/x.sh` AND `scripts/tests/x.py`. Omitting the `/` had a consequence that was not
|
||||
# theoretical: the three guard files this inventory shipped with were themselves outside the
|
||||
# population it checked for completeness, so they acquired no rows and the guard stayed green.
|
||||
# A completeness guard blind to its own author's new guards is the defect this whole change is
|
||||
# about, so the miss is recorded here rather than quietly corrected.
|
||||
_SCRIPT_REF = re.compile(r"scripts/(?:[a-z0-9_.-]+/)?[a-z0-9_.-]+\.(?:sh|py)")
|
||||
_ROW = re.compile(r"^\|\s*`([^`]+)`\s*\|([^|]*)\|\s*([A-Z-]+)\s*\|\s*([A-Z-]+)\s*\|([^|]*)\|\s*$", re.M)
|
||||
|
||||
# The prose summary, parsed so it cannot drift from the table it summarises. It already had:
|
||||
# shipped as "28 guards, 4 tooling … 6 … 3 … 19" against a table holding 27/5/6/3/18, because it was
|
||||
# a hand-maintained mirror with no equality check — #773's Family C inside the deliverable arguing
|
||||
# against it. Both reviewers caught it independently.
|
||||
# against it.
|
||||
_SUMMARY = re.compile(
|
||||
r"(\d+)\s+guards?,\s+(\d+)\s+tooling\s+scripts?,\s+(\d+)\s+proof\s+files?\.\s+"
|
||||
r"\*\*(\d+)\s+guards?\s+carry\s+a\s+mutation\s+proof;\s+(\d+)\s+(?:are|is)\s+behaviour-only;\s+"
|
||||
@@ -132,18 +132,17 @@ def wired_hook_files() -> set[str]:
|
||||
|
||||
Directory membership is not execution. A hook whose settings.json registration is deleted keeps
|
||||
its file, keeps its inventory row, and stops running — and the table would go on describing a
|
||||
working guard. That is #631 and #719's shape ("wired is not running") one level down, and it was
|
||||
the derivation's blind spot until cold review named it.
|
||||
working guard. That is #631 and #719's shape ("wired is not running") one level down.
|
||||
|
||||
COMMENT LINES ARE STRIPPED from the husky hooks first, and that is not a refinement — the first
|
||||
version of this function counted a mention anywhere, and `.husky/pre-commit:7` reads
|
||||
COMMENT LINES ARE STRIPPED from the husky hooks first, and that is not a refinement — counting
|
||||
a mention anywhere counts `.husky/pre-commit:7`, which reads
|
||||
|
||||
# CI where a base ref exists). Fail-open shim — see .claude/hooks/decisions-guard.sh.
|
||||
|
||||
one line above the real invocation. Delete line 8 and keep line 7 and the hook would still have
|
||||
read as wired, which is the exact substitution of mention for invocation this function exists to
|
||||
stop, reproduced inside the fix for it. `.claude/settings.json` needs no stripping: JSON has no
|
||||
comments, so every occurrence there is in a real command string.
|
||||
one line above the real invocation. Delete line 8 and keep line 7 and the hook would still read
|
||||
as wired, which is the exact substitution of mention for invocation this function exists to
|
||||
stop. `.claude/settings.json` needs no stripping: JSON has no comments, so every occurrence
|
||||
there is in a real command string.
|
||||
"""
|
||||
text = (REPO_ROOT / ".claude" / "settings.json").read_text()
|
||||
for husky in tracked_paths(*HUSKY):
|
||||
@@ -308,9 +307,8 @@ def test_every_proof_ref_points_at_a_row_marked_PROOF():
|
||||
def test_the_summary_counts_match_the_table():
|
||||
"""The prose is DERIVED-checked, not hand-maintained.
|
||||
|
||||
It shipped wrong the first time — "28 guards, 4 tooling … 19 have none" against a table holding
|
||||
27/5/…/18 — because it was a mirror with no equality check. Both cold reviewers found it
|
||||
independently, which is the clearest possible evidence that a summary nobody checks is a summary
|
||||
A mirror with no equality check shipped "28 guards, 4 tooling … 19 have none" against a table
|
||||
holding 27/5/…/18. A summary nobody checks is a summary
|
||||
nobody can trust.
|
||||
"""
|
||||
rows = inventory_rows()
|
||||
|
||||
@@ -345,7 +345,7 @@ def test_no_derivation_admits_an_untracked_file(label, derive):
|
||||
|
||||
# The directory-listing APIs a Python file population is realistically written with. NOT every way a
|
||||
# process can list a directory — `subprocess.run(["ls"])`, a module-level alias captured before the
|
||||
# patch, and any C-level call all walk straight past this, all three verified by cold review. That
|
||||
# patch, and any C-level call all walk straight past this, all three verified. That
|
||||
# bounds what the check below can claim, and the docstring says so rather than implying a sandbox.
|
||||
# Reading a file stays allowed: `derived_guard_files` must read workflow bodies.
|
||||
_ENUMERATORS = (
|
||||
@@ -394,10 +394,10 @@ def test_no_derivation_ENUMERATES_the_filesystem(label, derive):
|
||||
returned. Reading is untouched, so a derivation may still parse the workflow bodies it scrapes
|
||||
for referenced scripts.
|
||||
|
||||
WHAT IT DOES NOT COVER. The boundary is not "synchronous", which is what two earlier drafts
|
||||
said and what measurement disproved — a thread that outlives the `derive()` call but finishes
|
||||
while its result is being drained IS caught, as is a `__del__` firing during that drain. The
|
||||
boundary is mechanical rather than temporal: a call to one of the SPIES is observed, wherever and
|
||||
WHAT IT DOES NOT COVER. The boundary is not "synchronous", which measurement disproves — a
|
||||
thread that outlives the `derive()` call but finishes while its result is being drained IS
|
||||
caught, as is a `__del__` firing during that drain. The boundary is mechanical rather than
|
||||
temporal: a call to one of the SPIES is observed, wherever and
|
||||
whenever it happens in this process before the assertion below. "While the patch is active"
|
||||
under-claims it — the spy appends to a list that outlives the patch, so a reference captured
|
||||
during the window and invoked after it still records. What decides observation is whether the
|
||||
|
||||
@@ -60,9 +60,10 @@ SINK = REPO_ROOT / "scripts" / "hook-fire-log.sh"
|
||||
#
|
||||
# `{**os.environ}` at module level is the shape #785 shipped: a suite that snapshots the environment
|
||||
# once, at collection, and hands that stale mapping to every subprocess it launches. Because the
|
||||
# autouse isolation fixture runs at test SETUP, such a snapshot used to predate it and every hook
|
||||
# driven from it wrote to `$HOME/.cache/ersatztv/hook-fire/` — 58 records per run, with every
|
||||
# assertion green, since the fire-log library is fail-open by design.
|
||||
# autouse isolation fixture runs at test SETUP, such a snapshot PREDATES it. Before isolation was
|
||||
# installed at `pytest_configure` (pre-collection), every hook driven from that snapshot wrote to
|
||||
# `$HOME/.cache/ersatztv/hook-fire/` — 58 records per run, measured, with every assertion green,
|
||||
# since the fire-log library is fail-open by design.
|
||||
#
|
||||
# `test_the_suite_does_not_write_to_the_PRODUCTION_log` asserts this specimen is now SAFE, which is
|
||||
# the cross-suite claim that test has always made in its docstring and never checked in its body.
|
||||
@@ -92,7 +93,7 @@ def hook_scripts() -> list[Path]:
|
||||
"""THE POPULATION, from the GIT INDEX. Never a list, and never the filesystem (ersatztv#806).
|
||||
|
||||
The filesystem is not an authoritative source: an untracked `.sh` dropped in `.claude/hooks/` —
|
||||
a scratch copy, a half-written hook — used to enter this population and be demanded to carry
|
||||
a scratch copy, a half-written hook — would enter this population and be demanded to carry
|
||||
instrumentation, reddening the suite on that checkout while CI, which never sees the file, stayed
|
||||
green. That is #778's third shape, and a guard that fails everywhere except where it runs trains
|
||||
its readers to ignore it.
|
||||
@@ -130,14 +131,14 @@ def instrumentation_faults(text: str, name: str) -> list[str]:
|
||||
# the `[ -r ] && .` line untouched and disables all reporting silently — a mutation the first
|
||||
# version of this checker passed clean.
|
||||
#
|
||||
# EVERY assignment, and EXACTLY ONE of them. Reading only the first is a hole cold review found
|
||||
# and measured: a hook carrying the canonical self-located line and then a SECOND
|
||||
# EVERY assignment, and EXACTLY ONE of them. Reading only the first is a hole, and it is
|
||||
# MEASURED: a hook carrying the canonical self-located line and then a SECOND
|
||||
# `ETV_HOOK_FIRE_LIB=${SOME_OTHER_VAR:-…}/scripts/hook-fire-log.sh` passed this checker clean on
|
||||
# all 13 hooks, and the shell would source the LAST value. The sourcing line reads the variable's
|
||||
# final value, so the checker must judge every writer of it — checking the first is checking the
|
||||
# one the defect does not use.
|
||||
# THE AUTHORITY IS BYTE-IDENTITY, not a pattern — and that is a withdrawal, recorded rather than
|
||||
# quietly performed. Three review rounds attacked a lexical rule over this line: `${VAR:-<self>}`
|
||||
# quietly performed. Three successive lexical rules over this line each fell: `${VAR:-<self>}`
|
||||
# satisfied a containment test; then backticks and `$((…))`; then `$(printenv VAR)`, `$1`, `$?`,
|
||||
# an INDENTED or `export`ed reassignment further down, and `$'…'` quoting that makes the required
|
||||
# token literal rather than expanded. Every fix admitted the next shape, which is this repo's own
|
||||
@@ -180,7 +181,7 @@ def instrumentation_faults(text: str, name: str) -> list[str]:
|
||||
# answering). Disarm an arm here with `pass`. `if False:` is safe on an arm with no `elif` after
|
||||
# it — `mutation_manifest.py` disarms byte-identity that way every run — but on the two begin-call
|
||||
# arms below it falls through to a `.group(1)` on None and reds with an AttributeError: a red for
|
||||
# the wrong reason that reads like a proof, and cost this branch two review rounds. A
|
||||
# the wrong reason that reads like a proof. A
|
||||
# hook carrying both canonical lines plus `ETV_HOOK_FIRE_DISABLE=1` trips exactly one fault, and
|
||||
# it is not byte-identity's, so a reader who sorts those into "already covered" deletes a live
|
||||
# detector.
|
||||
@@ -221,8 +222,8 @@ def instrumentation_faults(text: str, name: str) -> list[str]:
|
||||
if "${BASH_SOURCE[0]}" not in value:
|
||||
faults.append(f"{name}: the sink path is not self-located from ${{BASH_SOURCE[0]}}: {value}")
|
||||
|
||||
# ...and MENTIONING `${BASH_SOURCE[0]}` is not the same as being DECIDED by it. Cold review
|
||||
# measured the gap: `${ETV_HOOKS_ROOT:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../..")}`
|
||||
# ...and MENTIONING `${BASH_SOURCE[0]}` is not the same as being DECIDED by it. The gap is
|
||||
# MEASURED: `${ETV_HOOKS_ROOT:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../..")}`
|
||||
# — literally "the next `${SOME_OTHER_DIR:-…}`" the comment above says it stops — passed on
|
||||
# all 13 hooks, because the self-location sitting in the `:-` FALLBACK satisfied a
|
||||
# containment test while the environment still won whenever the variable was set. A
|
||||
@@ -231,8 +232,8 @@ def instrumentation_faults(text: str, name: str) -> list[str]:
|
||||
# So the value must contain NO expansion at all beyond `${BASH_SOURCE[0]}` itself and the
|
||||
# `$(…)` command substitution the canonical line is MADE of. Four spellings can let something
|
||||
# outside this file choose the tree, and all four fail: `${…}`, a bare `$NAME`, an arithmetic
|
||||
# `$((…))`, and a BACKTICK substitution — the last two found by self-audit rather than by a
|
||||
# reviewer, and both slip past a `${…}`/`$NAME` test when `${BASH_SOURCE[0]}` also appears
|
||||
# `$((…))`, and a BACKTICK substitution — the last two
|
||||
# slip past a `${…}`/`$NAME` test when `${BASH_SOURCE[0]}` also appears
|
||||
# (`` `cat /tmp/root` `` names a tree while containing no `$` at all). Neither has any use in
|
||||
# this line, so refusing them costs nothing and "could not tell where this resolves from"
|
||||
# fails rather than passing quietly.
|
||||
@@ -322,9 +323,9 @@ def test_the_population_is_not_empty():
|
||||
def test_the_stripper_removes_EXACTLY_the_preamble_and_nothing_else():
|
||||
"""The A/B control must be the hook minus the instrumentation — no more, no less.
|
||||
|
||||
This compared `strip_instrumentation(hook)` against the hook at the merge base, which asserted
|
||||
two unrelated things at once: that the stripper is exact, AND that no hook was edited in this PR
|
||||
for any other reason. The second is not a property worth pinning — it went red the moment
|
||||
Comparing `strip_instrumentation(hook)` against the hook at the merge base asserts
|
||||
two unrelated things at once: that the stripper is exact, AND that no hook was edited on the
|
||||
branch for any other reason. The second is not a property worth pinning — it went red the moment
|
||||
`pretooluse-bom-guard.sh` had a real defect fixed (`xxd`, absent on the CI runner, made it
|
||||
fail open) — and coupling them means a legitimate change reads as a broken control.
|
||||
|
||||
@@ -619,8 +620,8 @@ def test_the_husky_launched_population_is_not_empty():
|
||||
def _pushed_tree(tmp_path: Path, hook: Path, sink_body: str) -> Path:
|
||||
"""A repo-SHAPED scratch tree holding this hook and a `scripts/hook-fire-log.sh` of `sink_body`.
|
||||
|
||||
The pair below used to make its relative launch from REPO_ROOT itself, which was wrong twice
|
||||
over and cold review measured both: `prepush-rebase-check.sh` reaches `git fetch origin main` on
|
||||
A relative launch from REPO_ROOT itself is wrong twice
|
||||
over, and both were measured: `prepush-rebase-check.sh` reaches `git fetch origin main` on
|
||||
an empty ref list, so every suite run made a NETWORK CALL and rewrote `FETCH_HEAD` in the very
|
||||
checkout under test — the one ref this repo's process treats as the baseline — and
|
||||
`decisions-guard.sh` ran the full decisions validator over the live tree. A unit test must not
|
||||
@@ -741,7 +742,7 @@ def test_a_LATER_reassignment_the_regex_cannot_see_is_DETECTED(hook):
|
||||
|
||||
The diagnostic arms below it read `^ETV_HOOK_FIRE_LIB=(.*)$`, anchored at column zero, so an
|
||||
INDENTED reassignment inside an `if`, or an `export`ed one, is invisible to them while bash
|
||||
executes it and the `. `-source line takes the LAST value. Cold review measured both passing a
|
||||
executes it and the `. `-source line takes the LAST value. Both were MEASURED passing a
|
||||
checker that had just been "fixed" to read every assignment — which is what retired the pattern
|
||||
approach in favour of pinning the two canonical lines.
|
||||
|
||||
@@ -768,7 +769,7 @@ def test_the_NEXT_env_var_to_be_invented_is_DETECTED(hook):
|
||||
"""THE MUTATION PROOF for the no-other-expansion arm, and the reason that arm exists.
|
||||
|
||||
Banning `CLAUDE_PROJECT_DIR` by name only moves the defect to the next name. This is that shape,
|
||||
and cold review measured it passing on all 13 hooks against the arm that merely required
|
||||
and it was MEASURED passing on all 13 hooks against the arm that merely required
|
||||
`${BASH_SOURCE[0]}` to APPEAR: the self-location sits in the `:-` fallback, so the containment
|
||||
test is satisfied while the environment still decides whenever `$ETV_HOOKS_ROOT` is set.
|
||||
|
||||
@@ -807,7 +808,7 @@ def test_the_NEXT_env_var_to_be_invented_is_DETECTED(hook):
|
||||
# reach with a bare payload get a constructed positive case in `positive_cases()` below, and
|
||||
# `test_the_AB_is_not_VACUOUS_for_any_hook` refuses to let any hook rely on this matrix alone.
|
||||
#
|
||||
# Stated because the first version of this file claimed the opposite in three places: this matrix
|
||||
# MEASURED, not assumed: this matrix
|
||||
# alone produced empty-vs-empty comparisons for 8 of 13 hooks and a zero exit status for all 13, so
|
||||
# deleting the entire stdout replay left the differential test green for four hooks — two of which
|
||||
# issue `deny`. An A/B over silent allows proves transparency on the one path where there is nothing
|
||||
@@ -1121,7 +1122,7 @@ def _normalise_stderr(err: bytes) -> bytes:
|
||||
"""
|
||||
# The two fields are normalised TOGETHER, as one anchored prefix, not independently. A global
|
||||
# `line \d+:` substitution also rewrites application text that happens to contain that phrase,
|
||||
# and cold review demonstrated it collapsing two genuinely different diagnostics ("highest
|
||||
# and was DEMONSTRATED collapsing two genuinely different diagnostics ("highest
|
||||
# private fd line 0" vs "line 4") into one — hiding exactly the kind of fd-state difference this
|
||||
# comparison exists to catch.
|
||||
return re.sub(rb"(?m)^[^\s:]*/[^\s:]*\.sh: line \d+:", b"<script>: line <n>:", err)
|
||||
@@ -1150,7 +1151,7 @@ def test_instrumentation_changes_NOTHING_the_harness_can_see(hook, sandbox, posi
|
||||
|
||||
@pytest.mark.parametrize("hook", hook_scripts(), ids=lambda h: h.stem)
|
||||
def test_the_AB_is_not_VACUOUS_for_any_hook(hook, sandbox, positives):
|
||||
"""ANTI-VACUITY FOR CLAIM 2 — the assertion the first version of this file was missing.
|
||||
"""ANTI-VACUITY FOR CLAIM 2.
|
||||
|
||||
`test_instrumentation_changes_NOTHING_the_harness_can_see` compares the two halves against each
|
||||
other. If a hook emits nothing and exits 0 on every case, that comparison is `("", 0) == ("", 0)`
|
||||
@@ -1448,12 +1449,12 @@ def _report_decoy_tree(tmp_path: Path) -> Path:
|
||||
def test_CLAUDE_PROJECT_DIR_cannot_choose_which_trees_hooks_the_REPORT_describes(tmp_path, sandbox):
|
||||
"""THE READ SIDE'S half of ersatztv#891, which is the change this file makes to the sink itself.
|
||||
|
||||
`etv_hook_fire_repo_root` used to prefer `$CLAUDE_PROJECT_DIR` whenever it named a tree with a
|
||||
`.claude/hooks`. That reads as harmless — a report decides nothing — but the report's two halves
|
||||
Preferring `$CLAUDE_PROJECT_DIR` whenever it names a tree with a
|
||||
`.claude/hooks` reads as harmless — a report decides nothing — but the report's two halves
|
||||
ARE the population and the log, so resolving them from two roots produces a confident table about
|
||||
another checkout: NEVER-FIRED rows for hooks that fired, and rows for hooks this tree does not
|
||||
have. `process.hook-resolves-inputs-from-repo-root` requires a behavioural test PER SITE, and
|
||||
this is that test for this site — cold review measured its absence by restoring the preference
|
||||
this is that test for this site — its absence was MEASURED by restoring the preference
|
||||
verbatim and watching the whole file stay green.
|
||||
"""
|
||||
project = _report_decoy_tree(tmp_path)
|
||||
@@ -1510,14 +1511,14 @@ def test_the_report_REFUSES_a_root_that_does_not_OWN_this_sink(tmp_path, sandbox
|
||||
Self-location answers "two directories above wherever I am", which is this repo only when this
|
||||
file sits in it. A copy or symlink whose grandparent HAPPENS to hold a `.claude/hooks` otherwise
|
||||
roots there and prints a confident table about another tree — the wrong-root case the sink's own
|
||||
comment claims is enforced rather than hoped for. It was shipped unexecuted: cold review measured
|
||||
that replacing the clause with `:` left this whole file green, because the sibling test below
|
||||
comment claims is enforced rather than hoped for. It was shipped unexecuted — MEASURED: replacing
|
||||
the clause with `:` left this whole file green, because the sibling test below
|
||||
copies the sink into `<tmp>/scripts/`, where the root DOES own it.
|
||||
|
||||
THREE arms, and the third is the one that tests OWNERSHIP rather than mere presence. The first
|
||||
two roots hold no `scripts/hook-fire-log.sh` at all, so `-ef` never evaluates against an existing
|
||||
left-hand side and a strictly weaker `[ -e … ]` would pass them both — measured by cold review,
|
||||
which left the whole file green on that mutant. The `other-sink` arm gives the fake root a
|
||||
left-hand side and a strictly weaker `[ -e … ]` would pass them both — MEASURED: that mutant
|
||||
left the whole file green. The `other-sink` arm gives the fake root a
|
||||
DIFFERENT `scripts/hook-fire-log.sh`, where presence and identity finally disagree: `-e` accepts
|
||||
and prints a confident table about the other tree, `-ef` refuses.
|
||||
|
||||
@@ -1602,8 +1603,8 @@ def test_the_report_REFUSES_an_empty_population(tmp_path, sandbox):
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
# PROOFS FOR THE ROUND-2 FIXES. Each of these went red before its fix and green after; each names
|
||||
# the failure it pins, because "this test exists for a reason" is not a reason.
|
||||
# PROOFS FOR THE DEFECTS THIS INSTRUMENTATION SHIPPED. Each went red before its fix and green
|
||||
# after; each names the failure it pins, because "this test exists for a reason" is not a reason.
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1636,7 +1637,7 @@ def test_stderr_is_NOT_silenced(sandbox):
|
||||
|
||||
|
||||
def test_output_SURVIVES_a_vanished_stdout_tempfile(sandbox):
|
||||
"""The swallowed-`deny` path, found by cold review by stubbing `mktemp`.
|
||||
"""The swallowed-`deny` path, also reachable by stubbing `mktemp`.
|
||||
|
||||
Two coupled mistakes: the fd restore lived inside the same conditional as the replay, so a
|
||||
missing file left stdout still pointing at the temp target; and the replay read the PATH, so an
|
||||
@@ -1773,8 +1774,8 @@ def test_the_suite_does_not_write_to_the_PRODUCTION_log(sandbox):
|
||||
oracle was global mutable state: an unrelated session firing a hook inside the window failed
|
||||
this test with `a test run modified the production hook-fire log` — an accusation about the
|
||||
suite when the writer was another process. Observed on three separate branches, green on an
|
||||
immediate re-run each time, and corroborated by an independent reviewer who found a file in that
|
||||
directory written 13 seconds earlier by someone else (ersatztv#822). The misattribution was the
|
||||
immediate re-run each time, and corroborated by a file in that
|
||||
directory written 13 seconds earlier by another process (ersatztv#822). The misattribution was the
|
||||
expensive part: it points the next reader at the suite, and the honest diagnosis costs a re-run
|
||||
plus a directory listing.
|
||||
|
||||
@@ -2489,8 +2490,8 @@ def test_a_launch_NOT_CARRYING_the_isolated_log_dir_FAILS_THE_LAUNCH():
|
||||
with pytest.raises(AssertionError, match="inside a shared hook-fire log"):
|
||||
launch({**scratch, ENV_VAR: str(SHARED_LOG_DIRS[0] / "sub")})
|
||||
|
||||
# (5) the variable set RELATIVE, with a `cwd` that makes it land on a shared log. Found by
|
||||
# cross-family review as a live bypass: a relative value resolves against the CHILD's directory,
|
||||
# (5) the variable set RELATIVE, with a `cwd` that makes it land on a shared log. A LIVE
|
||||
# BYPASS: a relative value resolves against the CHILD's directory,
|
||||
# so a guard anchoring it at the parent's sees an unrelated path under the repo and clears it.
|
||||
# The guard raises before `Popen` runs anything, so this launch never enters that directory.
|
||||
shared = SHARED_LOG_DIRS[0]
|
||||
@@ -2529,20 +2530,20 @@ def test_specimen_drives_a_hook():
|
||||
def test_the_guard_JUDGES_every_env_and_cwd_SHAPE_that_Popen_ACCEPTS(tmp_path):
|
||||
"""`env` and `cwd` each have several legal shapes; the guard must judge, not crash, on all.
|
||||
|
||||
`Popen` accepts `cwd` as str, bytes or `PathLike`. The first version of the relative-path
|
||||
anchoring called `os.fspath(cwd)` and then `os.path.join`, which raises `TypeError` on mixing
|
||||
`Popen` accepts `cwd` as str, bytes or `PathLike`. Relative-path
|
||||
anchoring that calls `os.fspath(cwd)` and then `os.path.join` raises `TypeError` on mixing
|
||||
str with bytes — a guard that raises `TypeError` on a legitimate launch is not a stricter guard,
|
||||
it is a broken one, and the failure names the wrong thing entirely.
|
||||
|
||||
THE PREMISE IS PINNED, not assumed. A file descriptor is NOT a fourth shape: `Popen` calls
|
||||
`os.fsencode(cwd)`, which rejects an int. A previous version of the guard added a branch for
|
||||
that case; the branch was dead — `os.fspath` already raises on an int — and deleting it left its
|
||||
`os.fsencode(cwd)`, which rejects an int. A branch for
|
||||
that case is dead code — `os.fspath` already raises on an int — and deleting it left its
|
||||
own test green, so it was removed rather than proved. The assertion below is what makes that
|
||||
removal safe: if a future CPython starts accepting a descriptor, this goes red and says so.
|
||||
"""
|
||||
# A RELATIVE value, so the anchoring is actually REACHED. An absolute one short-circuits before
|
||||
# `cwd` is consulted at all — an earlier draft of this test used one and passed identically for
|
||||
# every shape, including shapes that crashed the guard the moment anything relative arrived.
|
||||
# `cwd` is consulted at all — using one here passes identically for every shape, including
|
||||
# shapes that crash the guard the moment anything relative arrives.
|
||||
relative = {ENV_VAR: "isolated/log"}
|
||||
for cwd in (str(tmp_path), tmp_path, os.fsencode(str(tmp_path))):
|
||||
assert isolation_violation(relative, cwd) is None, (
|
||||
@@ -2555,8 +2556,7 @@ def test_the_guard_JUDGES_every_env_and_cwd_SHAPE_that_Popen_ACCEPTS(tmp_path):
|
||||
|
||||
# AND THE ANCHOR MUST BIND TO THE LAUNCH'S cwd, for every shape — not merely fail to crash.
|
||||
# The same relative value judged against two different `cwd`s must give two different verdicts;
|
||||
# otherwise `cwd` is being ignored, which is the round-3 defect, and the shape loop above would
|
||||
# pass while it was.
|
||||
# otherwise `cwd` is being ignored, and the shape loop above would pass while it was.
|
||||
shared_parent = SHARED_LOG_DIRS[0].parent.parent.parent
|
||||
for shape in (str, Path, os.fsencode):
|
||||
onto_shared = shape(str(shared_parent))
|
||||
|
||||
@@ -20,9 +20,8 @@ release cut fails at the image build and `:latest` stops being republished.
|
||||
|
||||
## WHY THIS GUARD PINS TEXT INSTEAD OF PARSING IT
|
||||
|
||||
The first three versions of this file asked "does this command RUN the suite, and can it FAIL?" of
|
||||
arbitrary shell text. That predicate was wrong nine times across three review rounds, and every one
|
||||
of the nine was the same mechanism:
|
||||
A predicate asking "does this command RUN the suite, and can it FAIL?" of arbitrary shell text was
|
||||
wrong nine times, and every one of the nine was the same mechanism:
|
||||
|
||||
* heredoc bodies — skipped as data, but BuildKit EXECUTES `RUN <<EOF`, and the opener regex also
|
||||
fired inside quotes (`echo "tags<<__EOT__"`), which blinded the scan over the last 303 lines of
|
||||
@@ -42,8 +41,8 @@ file decides what a command MEANS.** The commands that may run in the risky plac
|
||||
text; anything else — a heredoc, a wrapper, a new spelling, a rewrite — is simply not equal to the
|
||||
pin and reddens.
|
||||
|
||||
WHAT A PIN IS AND IS NOT IMMUNE TO, stated precisely because an earlier version of this paragraph
|
||||
claimed the absolute "a pin cannot have a false green" and cold review refuted it by execution. A
|
||||
WHAT A PIN IS AND IS NOT IMMUNE TO, stated precisely because the absolute "a pin cannot have a
|
||||
false green" is FALSE, refuted by execution. A
|
||||
pin is immune to the class that defeated the parser: a DIFFERENT SPELLING of the command. It is not
|
||||
immune to the same text MEANING something else. The routes found so far, and what each is worth,
|
||||
are enumerated ONCE — in `docs/guard-inventory.md`'s row for this guard, which is where the count
|
||||
@@ -117,15 +116,15 @@ WHAT IS NOT COVERED, stated rather than implied:
|
||||
|
||||
* A Dockerfile stage that does not carry the SPA source is unpinned. The boundary is what
|
||||
`copies_spa_source` recognises — a context copy of `web/` or `.`, a matching GLOB, or a stage
|
||||
copy whose source names `web` — and NOT the wider "has the suite available to run", which is
|
||||
what an earlier version of this sentence claimed. A stage copy that RENAMES the tree on the way
|
||||
copy whose source names `web` — and NOT the wider "has the suite available to run". A stage
|
||||
copy that RENAMES the tree on the way
|
||||
in (`COPY --from=web-build /source/web /elsewhere`) has no `web` segment in its source and is
|
||||
not recognised. What that boundary EXCLUDES, measured: a source that is an ANCESTOR of the
|
||||
tree (`COPY --from=web-build /source /src` brings `/source/web` along without naming it), and
|
||||
one that names it only as `/source/.`. Both are backstopped by the substring sweep — an
|
||||
ancestor-copy stage running `npm t`, `npx vitest` or a new npm script is caught. (This sentence
|
||||
was wrong three times before it was written from a measurement; a destination rename such as
|
||||
`/source/web /elsewhere` IS recognised, because only sources are read.) The
|
||||
ancestor-copy stage running `npm t`, `npx vitest` or a new npm script is caught. (MEASURED, not
|
||||
reasoned: a destination rename such as `/source/web /elsewhere` IS recognised, because only
|
||||
sources are read.) The
|
||||
false-NEGATIVE direction is what drops a stage out of the check, so the predicate is pinned in
|
||||
both directions by its own test, and the substring sweep below is the backstop for whatever it
|
||||
still misses.
|
||||
@@ -134,7 +133,7 @@ WHAT IS NOT COVERED, stated rather than implied:
|
||||
split does not name, and the one that behaves worst: a population going short is caught by an
|
||||
equality, a pin going stale goes loudly red, but a SELECTOR going short is silent. It was short
|
||||
by exactly one entry (`npm t`, npm's own alias, which this file names among the spellings that
|
||||
defeated the parser) until cold review measured a stage escaping through it. `PUBLISH_ACTION` is
|
||||
defeated the parser) until a stage was MEASURED escaping through it. `PUBLISH_ACTION` is
|
||||
the same category and is NOT covered — anti-vacuity proves the selector is non-empty, which the
|
||||
real `build` job guarantees, and cannot prove it complete: a publish through a different action
|
||||
is invisible to it. `test_no_run_BODY_builds_or_pushes_an_image` covers only the shell-body form.
|
||||
@@ -205,19 +204,19 @@ GATING_STEP_WORKDIR = "web"
|
||||
GATING_WORKFLOW_SHELL = "bash"
|
||||
|
||||
# The premise under the `docs_only` half of `GATING_STEP_IF`: the publish step is gated on it too, so
|
||||
# the arm that skips the suite ships nothing. Round-1 review found this stated as fact with nothing
|
||||
# checking it, one level out from the same defect.
|
||||
# the arm that skips the suite ships nothing. It is CHECKED rather than stated as fact — an
|
||||
# unchecked premise is one level out from the same defect.
|
||||
PUBLISH_STEP_NAME = "Build and push"
|
||||
PUBLISH_STEP_IF = "steps.detect.outputs.docs_only != 'true'"
|
||||
|
||||
# `RUN npm run build` and `RUN npm ci` execute whatever `web/package.json` says they do, and that
|
||||
# file is neither a Dockerfile nor a workflow. Cold review re-armed ersatztv#887 through it twice
|
||||
# file is neither a Dockerfile nor a workflow. ersatztv#887 was RE-ARMED through it twice
|
||||
# without touching any pinned line: `"build": "vitest run && tsc -b && vite build"` puts the suite
|
||||
# back into the gitless stage, and `"prepare"` is run by `npm ci`. So the SCRIPTS THAT MENTION VITEST
|
||||
# are pinned too — exactly one, and its body.
|
||||
PACKAGE_JSON = "web/package.json"
|
||||
# THE WHOLE MAP, not the scripts that mention vitest. Selecting on the literal `vitest` was a
|
||||
# SELECTOR where a PIN was available, and cold review re-armed ersatztv#887 through four spellings
|
||||
# SELECTOR where a PIN was available, and ersatztv#887 was RE-ARMED through four spellings
|
||||
# none of which contain it: `"build": "npm run test -- --run && …"`, `"build": "npm t -- …"`, and the
|
||||
# lifecycle hooks `prebuild` (npm runs it before `npm run build`) and `preinstall` (before `npm ci`).
|
||||
# Both pinned RUN lines in the gitless stage execute whatever this map says, so the map is pinned.
|
||||
@@ -311,7 +310,7 @@ OUTRANKING_CONFIG_PREFIXES = ("vitest.config.", "vitest.workspace.", "vitest.pro
|
||||
SUITE_MENTIONS = ("npm t", "npm run test", "yarn test", "pnpm test", "bun test", "vitest")
|
||||
# `npm t` rather than `npm test`: it is npm's own documented alias, it SUBSUMES `npm test` as a
|
||||
# substring, and this file enumerates it among the spellings that defeated the parser. Leaving it out
|
||||
# meant an unpinned stage running `npm t -- --run` passed the sweep — measured by cold review.
|
||||
# meant an unpinned stage running `npm t -- --run` passed the sweep — MEASURED.
|
||||
|
||||
|
||||
def _normalise(text: str) -> str:
|
||||
@@ -370,7 +369,7 @@ def instructions(text: str) -> list[str]:
|
||||
"""The Dockerfile instructions, comments dropped and `\\` continuations joined.
|
||||
|
||||
This is the whole of the file's syntax handling. It does not interpret what a `RUN` DOES — the
|
||||
string is compared against a pin — so the shell traps that defeated three earlier versions
|
||||
string is compared against a pin — so the shell traps that defeated the parser
|
||||
(heredocs, `#` inside a command, compound punctuation) cannot reach any decision here.
|
||||
"""
|
||||
joined: list[str] = []
|
||||
@@ -441,7 +440,7 @@ def copies_spa_source(command: str) -> bool:
|
||||
# such segment).
|
||||
return any("web" in source.strip("/").split("/") for source in remainder.split()[:-1])
|
||||
# `remainder` is already a plain space-joined string by here — the exec form was parsed above,
|
||||
# before the `--from=` branch. A second `startswith("[")` test used to sit here and was dead
|
||||
# before the `--from=` branch. A second `startswith("[")` test here is dead
|
||||
# code: measured by poisoning it with a `raise`, which left every test green.
|
||||
arguments = remainder.split()
|
||||
if len(arguments) < 2:
|
||||
@@ -453,8 +452,8 @@ def _source_reaches_web(source: str) -> bool:
|
||||
"""Could this COPY source bring `web/` in?
|
||||
|
||||
Exact (`web`, `web/…`), whole-context (`.`, `./`), and GLOB: `COPY web*/ ./` matches `web/` and
|
||||
so carries the source, while `webhooks/` does not — measured False for the glob by cold review,
|
||||
which left the receiving stage unpinned. `fnmatch` against the literal segment `web` is what
|
||||
so carries the source, while `webhooks/` does not — MEASURED False for the glob, which left
|
||||
the receiving stage unpinned. `fnmatch` against the literal segment `web` is what
|
||||
separates the two without hand-listing patterns.
|
||||
"""
|
||||
normalised = source.removeprefix("./").lstrip("/").rstrip("/")
|
||||
@@ -648,7 +647,7 @@ def test_the_GATING_STEP_is_exactly_what_was_reviewed() -> None:
|
||||
|
||||
Pinning the BODY is what makes "unfiltered" and "not suppressed" true without parsing: a
|
||||
`--exclude`, a `|| true`, a pipe, a `set +e`, a `true || npm test` that never executes, are each
|
||||
a different string from the pin. Three earlier versions tried to recognise those individually and
|
||||
a different string from the pin. Recognising those individually was tried three times and
|
||||
missed `true || npm test` and `continue-on-error: ${{ … }}` among others.
|
||||
"""
|
||||
named = [
|
||||
@@ -668,7 +667,7 @@ def test_the_GATING_STEP_is_exactly_what_was_reviewed() -> None:
|
||||
body = step.get("run")
|
||||
# PER LINE, not over the whole body: `_normalise` collapses newlines, so changing `run: |` to
|
||||
# `run: >` folds the two lines into one command whose normalised text is byte-identical to the
|
||||
# pin. Measured by cold review — the marker script then swallows the suite as its arguments.
|
||||
# pin. MEASURED — the marker script then swallows the suite as its arguments.
|
||||
if not isinstance(body, str) or _normalise_lines(body) != _normalise_lines(GATING_STEP_RUN):
|
||||
faults.append(
|
||||
f"the gating step's `run:` is not the pinned command.\n pinned: {GATING_STEP_RUN!r}\n"
|
||||
@@ -747,8 +746,8 @@ def test_the_GATING_STEP_is_exactly_what_was_reviewed() -> None:
|
||||
def test_the_npm_SCRIPT_MAP_is_exactly_what_was_reviewed() -> None:
|
||||
"""The whole map, because a pinned `RUN npm …` executes whatever it says.
|
||||
|
||||
An earlier version pinned only the scripts whose body contained `vitest`, which is a SELECTOR —
|
||||
the category this file elsewhere calls the worst-behaved, because going short is silent. It went
|
||||
Pinning only the scripts whose body contains `vitest` is a SELECTOR —
|
||||
the category this file elsewhere calls the worst-behaved, because going short is silent. It goes
|
||||
short four ways, each re-arming ersatztv#887 in the gitless stage with every other pin matching:
|
||||
`npm run test`, `npm t`, and the `prebuild`/`preinstall` LIFECYCLE HOOKS, which npm runs for
|
||||
`npm run build` and `npm ci` without anything naming them. Pinning the map removes the category:
|
||||
@@ -770,7 +769,7 @@ def test_the_VITE_CONFIG_is_exactly_what_was_reviewed() -> None:
|
||||
|
||||
`test.exclude`/`include` decide which specs the gating run collects, and `plugins:` can shell out
|
||||
to the suite from a build hook — both inside a file the Dockerfile never mentions. Pinning a
|
||||
BLOCK of it was defeated seven measured ways across three review rounds: a decoy copy above
|
||||
BLOCK of it was defeated seven measured ways: a decoy copy above
|
||||
`defineConfig` with the real member respelled `test: {`, `test : {` or `"test": {`; the same for
|
||||
`plugins:`; and two that never touched the marker — `[…].concat([evil])` and a trailing
|
||||
`...moreTest` spread, which replaces the pinned object because `defineConfig` is identity in both
|
||||
@@ -835,8 +834,8 @@ def test_the_DOCS_ONLY_arm_cannot_publish_an_image() -> None:
|
||||
"""The premise under half of `GATING_STEP_IF`, asserted rather than assumed.
|
||||
|
||||
The gating step is allowed to skip when `docs_only` is true. That is only safe because the
|
||||
publish step skips on the same condition, so the arm ships nothing. Round-1 review found this
|
||||
stated as fact in the prose with nothing checking it — the same shape as the defect one level in.
|
||||
publish step skips on the same condition, so the arm ships nothing. Stating that in prose with
|
||||
nothing checking it is the same shape as the defect one level in.
|
||||
"""
|
||||
step = find_step(GATING_WORKFLOW, "build", PUBLISH_STEP_NAME)
|
||||
assert step is not None, f"{GATING_WORKFLOW}: job `build` has no step named {PUBLISH_STEP_NAME!r}."
|
||||
@@ -852,7 +851,7 @@ def test_the_DOCS_ONLY_arm_cannot_publish_an_image() -> None:
|
||||
def test_only_the_PINNED_step_mentions_the_suite() -> None:
|
||||
"""A substring sweep, deliberately not a semantic predicate.
|
||||
|
||||
Every earlier version tried to decide whether a command RUNS the suite and was wrong nine times.
|
||||
Deciding whether a command RUNS the suite was wrong nine times.
|
||||
This asks the much weaker question — does any OTHER step's shell body mention it at all — whose
|
||||
failure mode is a false red asking a human to look, never a false green.
|
||||
"""
|
||||
@@ -909,8 +908,8 @@ def test_no_run_BODY_builds_or_pushes_an_image() -> None:
|
||||
|
||||
A job doing `docker build -f ./docker/Dockerfile … && docker push …` in a shell body is not in
|
||||
that population, so the positive invariant is silently not applied to it — and anti-vacuity does
|
||||
not notice, because the real `build` job is still there. Measured MISSED by cold review after
|
||||
this assertion was dropped in the rewrite; it is restored rather than reasoned away.
|
||||
not notice, because the real `build` job is still there. MEASURED MISSED without this
|
||||
assertion, which is why it is asserted rather than reasoned away.
|
||||
"""
|
||||
stray: list[str] = []
|
||||
for path in workflow_files():
|
||||
@@ -971,7 +970,7 @@ def test_the_STAGE_SPLIT_finds_the_real_stages() -> None:
|
||||
def test_the_SPA_CARRYING_predicate_reads_every_COPY_SPELLING() -> None:
|
||||
"""Pinned both ways: every stage answered False is SKIPPED by the negative assertion.
|
||||
|
||||
The synthetic spellings were MEASURED False by cold review before this was widened; a new stage
|
||||
The synthetic spellings were MEASURED False before the predicate was widened; a new stage
|
||||
using any of them would have gone unpinned and unchecked.
|
||||
"""
|
||||
for spelling in (
|
||||
@@ -1014,7 +1013,7 @@ def test_the_SPA_CARRYING_predicate_reads_every_COPY_SPELLING() -> None:
|
||||
|
||||
|
||||
def test_the_INSTRUCTION_JOIN_survives_what_defeated_the_PARSER() -> None:
|
||||
"""The shapes that broke three earlier versions must not break the pin comparison.
|
||||
"""The shapes that broke the parser must not break the pin comparison.
|
||||
|
||||
None of these needs interpreting — they only need to arrive at the comparison intact, so that a
|
||||
stage running them is NOT EQUAL to its pin. Each was a measured false green when this file tried
|
||||
|
||||
@@ -43,12 +43,11 @@ def preflight(tmp_path):
|
||||
def with_jq(self, version_line, stderr="", exit_code=0):
|
||||
"""Install a fake `jq` reporting `version_line` for --version.
|
||||
|
||||
`stderr` and `exit_code` exist because an earlier version of this shim ALWAYS exited 0
|
||||
and never wrote to stderr — so it structurally could not observe the worst failure this
|
||||
script has: a jq that cannot start. The preflight was folding stderr into the parse via
|
||||
`2>&1` and discarding the exit status, so a glibc-mismatch message containing `2.34`
|
||||
parsed as version 2.34 and PASSED the floor. Every case the shim could express was clean,
|
||||
so every test passed.
|
||||
`stderr` and `exit_code` exist because a shim that ALWAYS exits 0 and never writes to
|
||||
stderr structurally cannot observe the worst failure this script has: a jq that cannot
|
||||
start. The preflight was folding stderr into the parse via `2>&1` and discarding the
|
||||
exit status, so a glibc-mismatch message containing `2.34` parsed as version 2.34 and
|
||||
PASSED the floor. Every case such a shim can express is clean, so every test passes.
|
||||
"""
|
||||
shim = bindir / "jq"
|
||||
body = '#!/bin/sh\nif [ "$1" = "--version" ]; then\n'
|
||||
@@ -67,11 +66,11 @@ def preflight(tmp_path):
|
||||
|
||||
def run(self, *args):
|
||||
env = dict(os.environ)
|
||||
# PATH contains ONLY the shim dir. An earlier draft appended /usr/bin:/bin "for the
|
||||
# basics" and the missing-jq test passed vacuously against the developer machine's real
|
||||
# /usr/bin/jq — the negative case was never negative. The script needs nothing from PATH
|
||||
# but jq itself (`command -v` is a builtin, and bash is invoked by absolute path), so
|
||||
# there is nothing to keep.
|
||||
# PATH contains ONLY the shim dir. Appending /usr/bin:/bin "for the basics" makes the
|
||||
# missing-jq test pass vacuously against the developer machine's real /usr/bin/jq — the
|
||||
# negative case is never negative. The script needs nothing from PATH but jq itself
|
||||
# (`command -v` is a builtin, and bash is invoked by absolute path), so there is nothing
|
||||
# to keep.
|
||||
env["PATH"] = str(bindir)
|
||||
return subprocess.run([BASH, str(SCRIPT), *args], env=env, capture_output=True, text=True)
|
||||
|
||||
@@ -238,7 +237,7 @@ def test_a_number_that_is_not_the_VERSION_is_not_accepted_as_one(preflight, vers
|
||||
],
|
||||
)
|
||||
def test_an_OUT_OF_RANGE_digit_run_fails_closed(preflight, version_line):
|
||||
"""The round-1 fail-open mechanism, resurrected via an over-long number.
|
||||
"""The original fail-open mechanism, resurrected via an over-long number.
|
||||
|
||||
A regex that guarantees *digits* does not guarantee they fit `test`'s integer range. With a
|
||||
23-digit major, `[ "$major" -lt "$min_major" ]` errors with "integer expression expected" — and
|
||||
@@ -266,11 +265,10 @@ def test_an_OUT_OF_RANGE_digit_run_fails_closed(preflight, version_line):
|
||||
# so the separator restriction is satisfied and cannot save us — the newline must be excluded
|
||||
# from the separator class AND the parse confined to line one.
|
||||
#
|
||||
# Without these, a round-5 mutation check found that reverting BOTH of those changes together
|
||||
# Without these, mutation measured that reverting BOTH of those changes together
|
||||
# (`[[:blank:]]`→`[[:space:]]` and parsing `$raw` instead of `$first`) left the whole suite
|
||||
# GREEN: the four cases above are all killed by the separator alone, so they attributed the fix
|
||||
# to the wrong layer. A test that passes for the wrong reason is how the previous three rounds
|
||||
# each shipped a defect.
|
||||
# to the wrong layer.
|
||||
"jq\nversion\n9.9",
|
||||
"jq\nversion 9.9",
|
||||
"jq \n version \n 9.9",
|
||||
@@ -280,8 +278,7 @@ def test_a_number_AFTER_the_jq_token_is_not_reachable_across_filler(preflight, v
|
||||
"""Two independent layers keep a stray number from being read as the version, and both are
|
||||
pinned here: the separator must be one of the forms real jq emits (`jq-1.6` / `jq version 1.6`),
|
||||
AND the match is confined to the first line with `[[:blank:]]` (which, unlike `[[:space:]]`,
|
||||
does not match a newline). Round 3's 'anchor' had neither and parsed `jq\\n2.34: cannot load`
|
||||
as 2.34."""
|
||||
does not match a newline). An 'anchor' with neither parses `jq\\n2.34: cannot load` as 2.34."""
|
||||
preflight.with_jq(version_line)
|
||||
r = preflight.run()
|
||||
assert r.returncode == 1, f"{version_line!r} was accepted as a version"
|
||||
|
||||
@@ -149,7 +149,7 @@ def test_a_verdict_with_no_recorded_base_gets_no_opinion(hook, desc):
|
||||
window closes on its own: verdicts are per-head and short-lived, so every verdict posted after
|
||||
#632 carries the field.
|
||||
|
||||
Asserting on the word "base" rather than on the issue tag, per cold review: the tag-only check
|
||||
Asserting on the word "base" rather than on the issue tag: the tag-only check
|
||||
would have passed for a base-specific ask or deny whose wording happened to omit it, which is
|
||||
the failure mode most likely to appear when someone edits these messages.
|
||||
"""
|
||||
@@ -181,8 +181,8 @@ def test_a_malformed_status_MEMBER_asks_too(hook, failure):
|
||||
def test_an_UNREADABLE_status_response_asks_rather_than_skipping_the_check(hook, failure):
|
||||
""" "Could not check" is a third outcome, not a quiet synonym for "no base recorded".
|
||||
|
||||
The first draft collapsed the two: an unreadable status response produced an empty
|
||||
`recorded_base`, took the graceful-adoption path, and skipped validation in silence — after
|
||||
Collapsing the two makes an unreadable status response produce an empty
|
||||
`recorded_base`, take the graceful-adoption path, and skip validation in silence — after
|
||||
which a later successful status read could still auto-grant, emitting "merge gate: satisfied"
|
||||
for a comparison that never happened. A transient Gitea hiccup is not evidence that the base is
|
||||
unchanged.
|
||||
|
||||
@@ -252,10 +252,9 @@ def test_ordinary_row_without_previous_filename_is_still_valid(hook):
|
||||
Requiring it globally would reject every normal modified/added row and make the gate refuse
|
||||
all exemptions — which the 'withholds' tests above could not distinguish from working.
|
||||
|
||||
Every row carries a `status`: since the round-3 hardening an ABSENT status fails closed (it
|
||||
Every row carries a `status`: an ABSENT status fails closed (it
|
||||
would otherwise dodge the `renamed => previous_filename REQUIRED` clause), which is asserted by
|
||||
`test_a_rename_disguised_by_an_unknown_status_is_rejected[None]`. The statusless row this test
|
||||
used to carry was incidental to what it is actually pinning.
|
||||
`test_a_rename_disguised_by_an_unknown_status_is_rejected[None]`.
|
||||
"""
|
||||
hook.set_pages(
|
||||
[
|
||||
@@ -399,9 +398,9 @@ def test_newline_in_previous_filename_is_also_rejected(hook):
|
||||
"""Same hole via the rename side — `previous_filename` is flattened identically.
|
||||
|
||||
NOTE the payload's second segment must itself be allow-list-PASSING (`docs/Program.cs`, not
|
||||
`ErsatzTV/Program.cs`). The first version of this test used the latter, which the allow-list
|
||||
rejects on its own merits, so the test passed with the newline guard entirely removed — it
|
||||
asserted the outcome without ever exercising the mechanism. That is the same
|
||||
`ErsatzTV/Program.cs`). The latter is rejected by the allow-list on its own merits, so a test
|
||||
using it passes with the newline guard entirely removed — it
|
||||
asserts the outcome without ever exercising the mechanism. That is the same
|
||||
filter-hides-the-defect trap the guard itself is about."""
|
||||
hook.set_pages(
|
||||
[{"filename": "docs/ok.md", "previous_filename": "safe.md\ndocs/Program.cs", "status": "renamed"}], []
|
||||
@@ -492,8 +491,8 @@ def test_gitea_real_status_values_are_accepted(hook):
|
||||
assert hook.exempted() is True
|
||||
|
||||
|
||||
# --- allow-list ANCHOR pins (round-3 review: three surviving mutants) --------------------------
|
||||
# The round-3 `..` finding was an anchor subversion, and mutating the anchors showed no test
|
||||
# --- allow-list ANCHOR pins (three surviving mutants) ------------------------------------------
|
||||
# The `..` finding was an anchor subversion, and mutating the anchors showed no test
|
||||
# covered them: dropping `^` from the docs/ alternative, or `$` from `.md`, both survived.
|
||||
|
||||
|
||||
@@ -533,7 +532,7 @@ def test_object_valued_status_is_also_rejected(hook):
|
||||
|
||||
# --- The `grep -q` / pipefail inversion, on the ADVISORY side (ersatztv#698) --------------------
|
||||
#
|
||||
# Round-2 cross-family review noted the enforced gate gained large-input regression tests while the
|
||||
# The enforced gate gained large-input regression tests while the
|
||||
# hook — which carries the SAME predicate — did not. The hook's blast radius is smaller (a missing
|
||||
# prompt, not a green required check), but `ci.shared-pr-file-enumeration` exists precisely because
|
||||
# the copy with LESS authority is the one that quietly keeps a bug. So test both.
|
||||
|
||||
@@ -180,8 +180,8 @@ def test_an_UNREADABLE_head_on_re_read_ASKS_rather_than_denying(hook):
|
||||
def test_the_head_deny_covers_BOTH_merge_paths(hook, mwcs):
|
||||
"""The twin-miss shape, tested because this exact hook has already been bitten by it.
|
||||
|
||||
The base re-read first landed INSIDE the scheduled-auto-merge branch only, and cold review
|
||||
found the consequence with this repo's own fixture: scheduled+retarget denied while
|
||||
The base re-read first landed INSIDE the scheduled-auto-merge branch only, and the
|
||||
consequence showed up on this repo's own fixture: scheduled+retarget denied while
|
||||
immediate+retarget auto-GRANTED. The head check is placed at the same hoist precisely so it
|
||||
sits ABOVE the point where the two paths diverge — but "it is above the split" is a claim about
|
||||
the source, and the thing worth pinning is the OUTCOME on both paths.
|
||||
|
||||
@@ -75,10 +75,10 @@ def respond(body, code="200"):
|
||||
sys.stdout.write(code)
|
||||
sys.exit(0)
|
||||
|
||||
# RECORD BEFORE FILTERING. This recorder used to live inside the `endswith` branch below, which
|
||||
# made the "no ref reaches the URL" assertion unfalsifiable: the only URLs it could record were ones
|
||||
# that already satisfied it, so a by-name request was invisible to the very test written to forbid
|
||||
# it. Cold review reintroduced a by-name lookup in the hook and the suite stayed 33/33 green. That is
|
||||
# RECORD BEFORE FILTERING. A recorder inside the `endswith` branch below would
|
||||
# make the "no ref reaches the URL" assertion unfalsifiable: the only URLs it could record are ones
|
||||
# that already satisfy it, so a by-name request is invisible to the very test written to forbid
|
||||
# it. MEASURED: reintroducing a by-name lookup in the hook left the suite 33/33 green. That is
|
||||
# the filter-on-the-asserted-property defect this PR's sibling record is about, committed inside the
|
||||
# guard against it — so the recorder now sees EVERY branch-protection URL, whatever its shape.
|
||||
if "/branch_protections" in url:
|
||||
@@ -114,8 +114,8 @@ if url.rstrip("/").endswith("/branch_protections"):
|
||||
"status_check_contexts": ["review-verdict/h10"]}]))
|
||||
if mode == "MALFORMED-NAME-BESIDE-GOOD-ONE":
|
||||
# `branch_name` PRESENT holding a number, beside a `rule_name` that names the base exactly
|
||||
# and requires every context. The first draft of #859's fix fell through to `rule_name` on
|
||||
# any non-usable `branch_name`, so this answered `exact` and AUTO-GRANTED — where the
|
||||
# and requires every context. A fix that falls through to `rule_name` on
|
||||
# any non-usable `branch_name` answers `exact` here and AUTO-GRANTS — where the
|
||||
# predecessor threw and asked. A field that is present and malformed is not a field that is
|
||||
# absent; it is evidence the payload is not what the program thinks it is.
|
||||
respond(json.dumps([{"branch_name": 42, "rule_name": "main", "enable_status_check": True,
|
||||
@@ -398,7 +398,7 @@ def test_positive_control_a_guarded_base_REACHES_the_check_and_still_auto_grants
|
||||
"""Without this, every test above passes if the hook denies on all paths — which it very nearly
|
||||
does, since this PR is non-docs and several later conditions are stubbed only loosely.
|
||||
|
||||
Asserting the absence of one phrase was not enough (cold review): an unrelated early `ask`, or a
|
||||
Asserting the absence of one phrase was not enough: an unrelated early `ask`, or a
|
||||
differently-worded deny, would satisfy it while proving nothing. So this pins all three of the
|
||||
things that must be true — the branch-protection endpoint was actually CALLED, the decision is
|
||||
`allow`, and the reason is the satisfied-gate message rather than any refusal.
|
||||
@@ -524,7 +524,7 @@ def test_an_UNREADABLE_branch_protection_asks_rather_than_denying_or_passing(hoo
|
||||
|
||||
|
||||
def test_an_IMMEDIATE_merge_is_not_subjected_to_this_check(hook):
|
||||
"""Scope, deliberately narrow — and stated without the overclaim cold review removed.
|
||||
"""Scope, deliberately narrow.
|
||||
|
||||
An immediate merge is not window-FREE: the hook returns `allow` and a separate call performs the
|
||||
merge, so a push can still land in between. What it lacks is a SCHEDULER — nothing waits on
|
||||
@@ -566,7 +566,7 @@ def test_a_SCALAR_status_row_asks_instead_of_killing_the_hook(hook):
|
||||
and `.description` but not `.status`, so an object row with a numeric status passes it and does
|
||||
reach the new validator — `test_a_NON_STRING_status_reaches_the_scheduled_validator` covers that
|
||||
and goes red when the clause is removed. So the clause is masked for scalar rows and load-bearing
|
||||
for that one. An earlier draft called the whole clause defence-in-depth, understating it in the
|
||||
for that one. Calling the whole clause defence-in-depth understates it, in the
|
||||
opposite direction from this repo's usual error.
|
||||
|
||||
So this asserts the OBSERVABLE contract — a decision is always emitted for this payload — which
|
||||
@@ -585,7 +585,7 @@ def test_a_PERSISTENT_retarget_denies_on_the_IMMEDIATE_path_too(hook):
|
||||
"""The twin. The re-read first landed inside the scheduled branch only, so this exact case —
|
||||
same fixture, `merge_when_checks_succeed` absent — AUTO-GRANTED while its sibling denied.
|
||||
|
||||
Cold review demonstrated it side by side, and it is the shape this repo has on record as
|
||||
Demonstrated side by side, it is the shape this repo has on record as
|
||||
"fix one path, then check its TWIN": the fix was applied where the defect was noticed, and the
|
||||
other consumer of the same stale value kept it. The re-read is now hoisted above every
|
||||
base-dependent decision rather than duplicated into the branch that happened to be under review.
|
||||
@@ -598,7 +598,7 @@ def test_a_PERSISTENT_retarget_denies_on_the_IMMEDIATE_path_too(hook):
|
||||
|
||||
|
||||
def test_a_PERSISTENT_retarget_after_the_first_read_denies(hook):
|
||||
"""The defect this guard had itself, found in the fifth cold-review round.
|
||||
"""The defect this guard had itself.
|
||||
|
||||
`$base_ref` is captured from the PR snapshot at the top of the hook, and everything between
|
||||
then and the branch-protection lookup is round trips — the file enumeration alone can be forty
|
||||
@@ -664,8 +664,8 @@ def test_an_unreadable_rule_LIST_asks_rather_than_denying(hook):
|
||||
def test_the_protection_lookup_puts_NO_ref_in_the_url(hook):
|
||||
"""The successor to a URL-encoding test, and the reason it could be retired.
|
||||
|
||||
The ref used to be interpolated into `branch_protections/{name}`, where a base like
|
||||
`release/26.4` injected a path separator and 404'd — read as "unprotected". That endpoint is
|
||||
Interpolating the ref into `branch_protections/{name}` lets a base like
|
||||
`release/26.4` inject a path separator and 404 — read as "unprotected". That endpoint is
|
||||
gone: it performed no matching and knew nothing about rule precedence, so a 200 from it proved
|
||||
less than it looked. Only the LIST endpoint is read now, which takes no ref at all, so the whole
|
||||
encoding hazard is removed by construction rather than escaped.
|
||||
@@ -701,11 +701,11 @@ def test_a_rule_name_with_REGEX_METACHARACTERS_does_not_match_a_different_base(h
|
||||
|
||||
|
||||
def test_a_GLOB_rule_whose_literal_part_has_a_metacharacter_still_MATCHES(hook):
|
||||
"""The positive control the first escaping attempt lacked, and the reason it looked green.
|
||||
"""The positive control escaping alone lacks, and the reason it can look green.
|
||||
|
||||
Escaping is only half the property: `*` must still span. The first version emitted TWO
|
||||
backslashes (`\\.` = "a literal backslash, then any character"), which made every rule
|
||||
containing a metacharacter UNMATCHABLE — so the fallback found nothing and hard-denied with the
|
||||
Escaping is only half the property: `*` must still span. Emitting TWO
|
||||
backslashes (`\\.` = "a literal backslash, then any character") makes every rule
|
||||
containing a metacharacter UNMATCHABLE — so the fallback finds nothing and hard-denies with the
|
||||
stated cause that no rule can govern the base — a false-open converted into a false DENY.
|
||||
|
||||
A negative-only assertion cannot see that: a rule matched literally and a rule made unmatchable
|
||||
@@ -793,7 +793,7 @@ def test_a_BACKSLASH_ESCAPED_metacharacter_in_a_rule_name_is_undecidable_not_abs
|
||||
def test_the_precedence_check_runs_even_when_an_exactly_named_rule_EXISTS(hook):
|
||||
"""The twin the restructure deletes, pinned so it cannot come back.
|
||||
|
||||
The hook used to look the rule up by NAME first and only enumerate the list on a 404. That
|
||||
Looking the rule up by NAME first and enumerating the list only on a 404 is that twin. That
|
||||
by-name endpoint is an exact DB lookup that performs no matching and knows nothing about
|
||||
precedence, so on a 200 — the path this repo actually takes, since its rule IS named `main` —
|
||||
the gate granted having consulted one rule and never asked which rule Gitea would apply. The
|
||||
@@ -925,7 +925,7 @@ def test_a_DRIFTED_contexts_snapshot_ASKS_on_BOTH_merge_paths(hook, scheduled):
|
||||
branch and never runs on an immediate merge — the common case. Hanging the freshness check off
|
||||
it would have fired it only when an auto-merge was armed. This file already records that exact
|
||||
shape once: the base re-read "first landed inside the scheduled-auto-merge branch only", and
|
||||
cold review found scheduled+retarget denied while immediate+retarget auto-GRANTED.
|
||||
scheduled+retarget denied while immediate+retarget auto-GRANTED.
|
||||
"""
|
||||
hook.set_branch_protection("DRIFTED-CONTEXTS")
|
||||
decision = hook.decision(scheduled=scheduled)
|
||||
@@ -1023,8 +1023,8 @@ def test_an_UNREADABLE_class_gets_its_OWN_arm_not_the_catch_all(hook):
|
||||
what actually happened is that branch protection or the snapshot came back malformed."""
|
||||
hook.set_branch_protection("NON-STRING-MEMBER")
|
||||
# THE IMMEDIATE PATH, deliberately. On the scheduled path the h10 required-check arm sees the
|
||||
# same malformed payload and asks first, so asserting there proves nothing about this arm — the
|
||||
# first draft of this test did exactly that and passed without ever reaching the code it names.
|
||||
# same malformed payload and asks first, so asserting there proves nothing about this arm: it
|
||||
# passes without ever reaching the code it names.
|
||||
reason = hook.decision(scheduled=False)["hookSpecificOutput"]["permissionDecisionReason"]
|
||||
assert "could not consume" in reason, f"the unreadable arm did not fire; some other arm answered: {reason[:200]}"
|
||||
assert "not a class this hook understands" not in reason, (
|
||||
@@ -1053,8 +1053,8 @@ def test_the_freshness_arm_does_NOT_fire_for_a_DIFFERENT_repo(hook):
|
||||
`.gitea/required-status-contexts.json` — a confident statement about a repo it does not describe.
|
||||
"""
|
||||
# The foreign repo's protection must actually DIFFER from this repo's snapshot, or the arm would
|
||||
# report `match` and stay silent whether or not it ran — which is how the first draft of this
|
||||
# test passed with the repo gate disabled. Asserting the absence of a string only means something
|
||||
# report `match` and stay silent whether or not it ran — a test arranged that way passes with
|
||||
# the repo gate disabled. Asserting the absence of a string only means something
|
||||
# when the string WOULD be there without the guard.
|
||||
hook.set_branch_protection("DRIFTED-CONTEXTS")
|
||||
out = hook.decision(repo="server-management")["hookSpecificOutput"]
|
||||
@@ -1072,8 +1072,8 @@ def test_the_freshness_arm_does_NOT_fire_for_a_DIFFERENT_repo(hook):
|
||||
def test_the_HOOK_loads_the_SHARED_classifier_behaviourally(hook, tmp_path):
|
||||
"""The other half of the one-copy property, proven by swapping the file rather than reading source.
|
||||
|
||||
Cold review re-inlined a BYTE-IDENTICAL copy of the classifier into this hook, left a comment
|
||||
naming the shared path above it, and the whole suite stayed green — a substring assertion cannot
|
||||
Re-inlining a BYTE-IDENTICAL copy of the classifier into this hook, under a comment
|
||||
naming the shared path, left the whole suite green — a substring assertion cannot
|
||||
tell a loaded file from a mentioned one, and the byte-identical inline is exactly the refactor
|
||||
that drifts later because it agrees today. Replacing the shared program with a sentinel that can
|
||||
only answer `undecidable` must therefore change what the hook DECIDES.
|
||||
@@ -1181,7 +1181,7 @@ def test_a_rule_with_an_UNUSABLE_NAME_asks_and_names_that_cause(hook, shape):
|
||||
turns the same `none` into `nomatch` -> ask, wrong but safe, while HERE it is a refusal stating a
|
||||
finding nobody established.
|
||||
|
||||
Both shapes are pinned because they used to fail DIFFERENTLY and converge only after the fix.
|
||||
Both shapes are pinned because they FAIL DIFFERENTLY without the fix and converge only with it.
|
||||
`BOTH-FIELDS-ABSENT` produced the false `none` -> deny. `NUMERIC-NAME` made jq throw, killing the
|
||||
program, and reached a safe ask only because the caller's catch-all absorbed the silence — a
|
||||
correct outcome resting on a crash. A number is not a usable name either, so both are now
|
||||
@@ -1351,8 +1351,7 @@ GRANTING_LOGGER = (
|
||||
|
||||
|
||||
def test_the_SOURCED_fire_log_library_cannot_be_chosen_by_CLAUDE_PROJECT_DIR(hook, tmp_path):
|
||||
"""The find cold review turned up while checking #858, and the one that makes the rest of this
|
||||
PR worth anything.
|
||||
"""The bypass that makes every other arm in this hook decorative (#858).
|
||||
|
||||
`ETV_HOOK_FIRE_LIB` was `${CLAUDE_PROJECT_DIR:-<self>}/scripts/hook-fire-log.sh` and is
|
||||
`. `-SOURCED at the top of the hook — so it is not a log DESTINATION, it is CODE, executed before
|
||||
@@ -1435,14 +1434,14 @@ def test_a_real_gitea_GLOB_rule_alone_is_not_reported_as_no_rule_at_all(hook):
|
||||
|
||||
|
||||
def test_a_MALFORMED_name_field_does_not_defer_to_its_sibling(hook):
|
||||
"""A regression the first draft of #859's fix introduced, found by cold review and measured.
|
||||
"""The regression a collapsed fall-through introduces in #859's fix, measured both ways.
|
||||
|
||||
`rulename` must distinguish a field that SUPPLIES NO NAME (absent, null, or the empty string
|
||||
Gitea sends for a glob rule — fall through to the sibling) from one that is PRESENT AND
|
||||
MALFORMED (poison the list). The first draft collapsed them, so `{"branch_name":42,
|
||||
"rule_name":"main"}` resolved to `main`, answered `exact`, and AUTO-GRANTED a scheduled merge —
|
||||
where the predecessor threw and asked. With `rule_name:"develop"` the same draft answered
|
||||
`none` and DENIED on "the full rule list was read and none matches", about a field never read.
|
||||
MALFORMED (poison the list). Collapsing them makes `{"branch_name":42,
|
||||
"rule_name":"main"}` resolve to `main`, answer `exact`, and AUTO-GRANT a scheduled merge —
|
||||
where the predecessor threw and asked. With `rule_name:"develop"` the same collapse answers
|
||||
`none` and DENIES on "the full rule list was read and none matches", about a field never read.
|
||||
|
||||
Both directions of the defect this whole change is fixing, reintroduced one shape over. That is
|
||||
why the fall-through is narrow and typed rather than "anything that is not a usable string".
|
||||
@@ -1463,7 +1462,7 @@ def test_the_classifier_CATCH_ALL_arm_still_has_a_guard(hook):
|
||||
|
||||
It lost its only test when `UNPARSEABLE-RULES` moved to the `unnamed-rule` arm: the remaining
|
||||
parametrised shapes (`GARBAGE`, `EMPTY`) are diverted one branch EARLIER by the array gate and
|
||||
never reach the `case` at all. Cold review measured the consequence — changing `*)` to set
|
||||
never reach the `case` at all. The consequence is MEASURED — changing `*)` to set
|
||||
`bp_code=200` left the whole suite green while `["main"]` regressed to "HTTP '200' — Gitea
|
||||
unreachable" about a read that plainly succeeded, which is the exact defect the sentinel exists
|
||||
to prevent.
|
||||
|
||||
@@ -4,9 +4,9 @@ The migration is a MOVE: content relocates, nothing is rewritten. That makes cor
|
||||
*provable* rather than reviewable — parse the corpus before, run the migration, parse it after,
|
||||
and assert the two Record sets are identical field-for-field with byte-identical rationale.
|
||||
|
||||
**Runs against a checked-in FIXTURE, not the live corpus.** An earlier version migrated the real
|
||||
`docs/` tree, which made it a one-shot: the moment the real migration landed, the tree was already
|
||||
split, the harness had nothing to move, and the tests failed for a reason that had nothing to do
|
||||
**Runs against a checked-in FIXTURE, not the live corpus.** Migrating the real
|
||||
`docs/` tree would make this a one-shot: once the real migration landed, the tree is already
|
||||
split, the harness has nothing to move, and the tests fail for a reason that has nothing to do
|
||||
with the code under test. A fixture keeps these tests exercising the *script* rather than the
|
||||
repo's current state, so they stay meaningful and green after the migration ships.
|
||||
|
||||
@@ -30,7 +30,7 @@ def _independent_strip(rec: dl.Record) -> str:
|
||||
|
||||
Deliberately a second implementation. Computing the "before" baseline with
|
||||
`mig._rationale_body` made the byte-identity proof circular: a bug in that one function
|
||||
corrupts both sides equally and the assertion still passes. Review proved it — collapsing
|
||||
corrupts both sides equally and the assertion still passes. PROVED: collapsing
|
||||
interior blank lines there left all harness tests green.
|
||||
"""
|
||||
lines = rec.body.split("\n")
|
||||
@@ -186,9 +186,9 @@ def test_same_file_anchor_bullets_are_dropped(migrated):
|
||||
def test_every_generated_link_resolves_from_its_own_file(migrated):
|
||||
"""Resolve each link relative to the file it LIVES IN — the base the reader's browser uses.
|
||||
|
||||
The earlier version of this test asserted existence from `docs/decisions/` regardless of where
|
||||
the link sat, which is the wrong base for `docs/decisions.md` (parent `docs/`) and for the
|
||||
archive files (parent `docs/decisions/archive/`). It therefore encoded the very bug it was
|
||||
Asserting existence from `docs/decisions/` regardless of where
|
||||
the link sits is the wrong base for `docs/decisions.md` (parent `docs/`) and for the
|
||||
archive files (parent `docs/decisions/archive/`). It encodes the very bug this is
|
||||
meant to catch: 105 links dangled while it stayed green.
|
||||
"""
|
||||
_, _, tmp = migrated
|
||||
|
||||
@@ -302,8 +302,8 @@ def test_the_DETECTOR_survivor_set_is_empty_ON_PURPOSE():
|
||||
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, though an earlier revision put it there: it is not
|
||||
DETECTOR-gated and fires for any declared mutation that fails to redden, and
|
||||
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.
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ WHY IT IS A DERIVED GUARD AND NOT A TABLE IN A DOC. #807 shipped the disposition
|
||||
TWICE and got it wrong BOTH times, each time by sorting a schema on its NAME rather than on what its
|
||||
endpoint does:
|
||||
|
||||
round 1 a prose sentence exempted "create/update" — `updateMultiCollection` and
|
||||
`updateFFmpegProfile` are full replaces, and both were live silent drops.
|
||||
round 2 a hand-written table replaced that sentence and omitted `ArtworkContentTypeModel`,
|
||||
because `…Model` reads as a response model. It is reachable from `PUT /channels/{id}`.
|
||||
attempt 1 a prose sentence exempted "create/update" — `updateMultiCollection` and
|
||||
`updateFFmpegProfile` are full replaces, and both were live silent drops.
|
||||
attempt 2 a hand-written table replaced that sentence and omitted `ArtworkContentTypeModel`,
|
||||
because `…Model` reads as a response model. It is reachable from `PUT /channels/{id}`.
|
||||
|
||||
Two misses from one mechanism, so the mechanism goes rather than the list getting a third patch.
|
||||
`testing.guard-derives-population-from-source` is explicit that a hand-written list is "a filter
|
||||
@@ -37,8 +37,8 @@ resolves. That resolver walks `allOf`, `oneOf`, `anyOf`, `if`/`then`/`else`, `de
|
||||
`items`/`prefixItems` and inline objects under `properties`, and deliberately contributes nothing
|
||||
for `additionalProperties`/`patternProperties` (which name no fixed members) — each pinned by a
|
||||
case in `test_composition_is_resolved_the_way_JSON_Schema_means_it`. It does NOT follow `$ref`;
|
||||
that is the component walk's job. An earlier draft said "anywhere in the request graph"; inline bodies were invisible at
|
||||
the time, so the universal was false the day it was written.
|
||||
that is the component walk's job — so the reach is what `_resolve` resolves and NOT the universal
|
||||
"anywhere in the request graph".
|
||||
|
||||
Set equality is asserted in BOTH directions and reported separately, because they are opposite
|
||||
defects: `missing` is a schema the API can drop and nobody has ruled on (the #807 defect), `phantom`
|
||||
@@ -172,8 +172,8 @@ def _request_reachable(doc: dict) -> set[str]:
|
||||
Two deliberate non-restrictions, both because this population has now been drawn by hand wrongly
|
||||
twice and every hand-drawn edge is a place to be wrong again:
|
||||
|
||||
NO VERB ALLOW-LIST. An earlier draft scanned POST/PUT/PATCH, which reads as obviously right and
|
||||
already had an exception: `DELETE /api/v1/media-items` carries a request body. Rather than argue
|
||||
NO VERB ALLOW-LIST. Scanning POST/PUT/PATCH reads as obviously right and
|
||||
already has an exception: `DELETE /api/v1/media-items` carries a request body. Rather than argue
|
||||
that a DELETE body cannot cause a full-replace drop — probably true, and exactly the kind of
|
||||
"probably" that produced this record's two live misses — every operation carrying a request body
|
||||
seeds the walk, and anything it surfaces must acquire a stated disposition.
|
||||
@@ -217,8 +217,8 @@ def _optional_of(schema: object) -> set[str]:
|
||||
required, so a droppable member vanishes from the population with nothing failing (measured
|
||||
2026-08-23). Subtracting inside each namespace first and qualifying only the survivors means
|
||||
the two sets are never mixed, so the collision cannot arise and there is no separator to
|
||||
defend. An earlier version instead ASSERTED that no property name contains a dot, which is a
|
||||
guard where a restructure was available.
|
||||
defend. ASSERTING instead that no property name contains a dot is a
|
||||
guard where a restructure is available.
|
||||
|
||||
Residual, stated because it is real: if a literal `a.b` and a nested `a` -> `b` are BOTH
|
||||
optional they conflate into one reported string. That is a diagnostic ambiguity, not a miss —
|
||||
@@ -248,7 +248,7 @@ def _resolve(schema: object) -> tuple[set[str], set[str], set[str]]:
|
||||
|
||||
CONJUNCTION vs DISJUNCTION. `allOf` arms ALL apply, so their `required` sets UNION. `oneOf` and
|
||||
`anyOf` arms are ALTERNATIVES, so a member is only genuinely required when EVERY alternative
|
||||
requires it — the `required` sets INTERSECT. Unioning them instead (the first version) marks a
|
||||
requires it — the `required` sets INTERSECT. Unioning them instead marks a
|
||||
member required because one arm requires it, hiding the arm that lets a client omit it. That is
|
||||
the drop this whole guard exists to catch, so getting it backwards is not a detail.
|
||||
|
||||
@@ -285,10 +285,10 @@ def _resolve(schema: object) -> tuple[set[str], set[str], set[str]]:
|
||||
# way: collect their properties (a client may send them) and DISCARD their `required` (it binds
|
||||
# only on a branch that may not be taken, so the member is omittable).
|
||||
#
|
||||
# An earlier version put `then`/`else` in the conjunctive list above, unioning their `required`.
|
||||
# That is the polarity error this function warns about above, committed in the same block:
|
||||
# Putting `then`/`else` in the conjunctive list above, unioning their `required`,
|
||||
# is the polarity error this function warns about above:
|
||||
# `then` and `else` are MUTUALLY EXCLUSIVE, so a member required only under `then` is omittable
|
||||
# whenever `if` does not match, and the guard reported it as required — the silent-miss
|
||||
# whenever `if` does not match, and the guard would report it as required — the silent-miss
|
||||
# direction, which is the one this whole file exists to catch. `if`'s `required` is discarded
|
||||
# for a different reason (it selects a branch rather than obliging anyone), and
|
||||
# `dependentSchemas` for a third (it binds only when its trigger key is present), but the
|
||||
@@ -436,7 +436,7 @@ def test_the_walks_ASSUMPTIONS_about_the_document_still_hold() -> None:
|
||||
Both are cheap to assert and neither is asserted anywhere else, so a change in the emitter
|
||||
would otherwise shrink this guard's population without failing anything.
|
||||
|
||||
A THIRD assumption used to sit here unstated and was already violated: that every request body
|
||||
A THIRD assumption sat here unstated and was already violated: that every request body
|
||||
`$ref`s a named component schema. `POST /api/v1/artwork/uploads` declares its body inline, so
|
||||
both `_optional_members` (which iterates `components.schemas`) and `_request_reachable` (which
|
||||
seeds from `$ref`s) were blind to it. That one is not an assumption any more —
|
||||
|
||||
@@ -127,9 +127,9 @@ if "/commits/" in url and url.split("?")[0].endswith("/status"):
|
||||
# sorts FIRST and is on page 1 whatever the page size. Measured against the live instance: a head
|
||||
# with ids [17,19,...,41,43] returns 41 and 43 at `?limit=2`, the two highest.
|
||||
#
|
||||
# An earlier version of this shim appended the new row LAST and sliced from the front, which made
|
||||
# it fall off the page and manufactured a truncation the server cannot produce. Two tests were
|
||||
# written against that fiction. Getting the ORDER right is what makes the paging model faithful;
|
||||
# Appending the new row LAST and slicing from the front makes it fall off the page and
|
||||
# manufactures a truncation the server cannot produce, and tests then get written against that
|
||||
# fiction. Getting the ORDER right is what makes the paging model faithful;
|
||||
# `DEFAULT_PAGING_NUM` (30) and the `MAX_RESPONSE_ITEMS` (50) clamp are modelled too, and
|
||||
# `total_count` reports the PAGE rather than the total, as measured.
|
||||
filler = int((state / "status_filler").read_text()) if (state / "status_filler").exists() else 0
|
||||
@@ -260,8 +260,8 @@ def test_refuses_when_head_moves_mid_flight(gitea):
|
||||
assert result.returncode != 0
|
||||
assert "UNREVIEWED" in result.stderr
|
||||
assert gitea.statuses() == [], "no status may be written once the reviewed head is stale"
|
||||
# And no comment either, since ersatztv#792. This assertion used to say the opposite — the
|
||||
# comment went first, so a refusal left `Review-verdict: MERGEABLE @ <sha>` on the PR with no
|
||||
# And no comment either, since ersatztv#792: with the comment written FIRST, a refusal left
|
||||
# `Review-verdict: MERGEABLE @ <sha>` on the PR with no
|
||||
# status behind it, which reads to an operator as consent that was never granted.
|
||||
assert gitea.comments() == [], "a refusal must leave no verdict comment standing in for a status"
|
||||
|
||||
@@ -410,11 +410,11 @@ def test_refuses_when_the_pr_has_no_resolvable_base(gitea):
|
||||
def test_a_failed_HEAD_RECHECK_writes_no_status(gitea):
|
||||
"""Fail-closed on the re-read itself, not just on a moved head.
|
||||
|
||||
This guard was previously implicit: `sha_now=$(api_get ... | jq ...)` aborted under `set -e` +
|
||||
`pipefail` when the GET failed. Nothing asserted it, so folding the head and base re-reads into
|
||||
one `$(... || true)` variable silently converted it to fail-OPEN — both guards see an empty
|
||||
string, both no-op, and the status is written having confirmed nothing. Asserted now so the
|
||||
behaviour is a contract rather than a side effect of a shell option.
|
||||
Left IMPLICIT, this guard is only a side effect: `sha_now=$(api_get ... | jq ...)` aborts under
|
||||
`set -e` + `pipefail` when the GET fails. Asserted by nothing, folding the head and base
|
||||
re-reads into one `$(... || true)` variable silently converts it to fail-OPEN — both guards see
|
||||
an empty string, both no-op, and the status is written having confirmed nothing. Asserted here
|
||||
so the behaviour is a contract rather than a side effect of a shell option.
|
||||
"""
|
||||
gitea.set_head_sequence(SHA_A, "GONE")
|
||||
result = gitea.run("42", "MERGEABLE")
|
||||
@@ -440,10 +440,10 @@ WRITE_NEGATIVE = ["BLOCKED", "NOT-MERGEABLE"]
|
||||
[(w, "success") for w in WRITE_POSITIVE] + [(w, "failure") for w in WRITE_NEGATIVE],
|
||||
)
|
||||
def test_each_verdict_word_posts_its_established_polarity(word, expected, gitea):
|
||||
"""The WRITE side of the polarity pair, added after cold review found only the read side.
|
||||
"""The WRITE side of the polarity pair.
|
||||
|
||||
ersatztv#774 withdrew a test that compared the two verdict vocabularies by parsing shell, and
|
||||
rescued its disjointness half into test_check_review_verdict.py. Review of that rescue found it
|
||||
rescued its disjointness half into test_check_review_verdict.py. That rescue
|
||||
covered only `check-review-verdict.sh`: an in-memory mutation adding `BLOCKED` to the SUCCESS
|
||||
arm here produced a write-side overlap the deleted test caught and the rescue did not, because
|
||||
the rescue never executes this script. That was a real, undisclosed loss and this closes it.
|
||||
@@ -476,7 +476,7 @@ def test_each_verdict_word_posts_its_established_polarity(word, expected, gitea)
|
||||
],
|
||||
)
|
||||
def test_a_reread_that_LOSES_a_field_refuses_instead_of_posting(head_seq, base_seq, field, gitea):
|
||||
"""The fail-OPEN one level below the TOCTOU guard, found by cold review (ersatztv#778).
|
||||
"""The fail-OPEN one level below the TOCTOU guard (ersatztv#778).
|
||||
|
||||
Both re-read checks were written as `[ -n "$x" ] && [ "$x" != "$want" ]`. That conjunct makes an
|
||||
EMPTY value a no-op: a well-formed 2xx response that merely omits `.head.sha` or `.base.ref`
|
||||
@@ -890,10 +890,10 @@ def test_a_status_row_carrying_NO_state_field_says_so_instead_of_blaming_a_race(
|
||||
def test_a_readback_MISSING_our_context_reports_the_row_count_and_names_no_cause(gitea):
|
||||
"""The row this run wrote is the newest on the head and sorts first, so its absence means
|
||||
something removed or replaced it — the diagnostic reports how many rows it read and does not
|
||||
guess at which. Paging is deliberately NOT offered as a cause: an earlier version named it, on
|
||||
the strength of `total_count` reporting the page rather than the total, before the selection
|
||||
ORDER was measured (max id per context, descending) and the truncation shown to be unproducible
|
||||
for this row. This also witnesses the row count, which the empty-body case cannot reach."""
|
||||
guess at which. Paging is deliberately NOT offered as a cause: `total_count` reports the page
|
||||
rather than the total, which suggests it, but the selection ORDER — max id per context,
|
||||
descending — makes that truncation unproducible for this row.
|
||||
This also witnesses the row count, which the empty-body case cannot reach."""
|
||||
gitea.set_status_body(
|
||||
json.dumps(
|
||||
{
|
||||
@@ -936,7 +936,7 @@ def _lib(snippet, cwd, workflow_body=None):
|
||||
|
||||
|
||||
def test_a_GLOB_entry_is_rejected_rather_than_expanded_against_the_working_directory(tmp_path):
|
||||
"""The documented fail-open, reached one file over from where it was first found.
|
||||
"""The documented fail-open, one file over from where it is recorded.
|
||||
|
||||
`for x in $list` and `$(printf %s "$list")` both perform PATHNAME EXPANSION, so a `*` entry
|
||||
expands to the FILENAMES around it — and any of those matching the plain-login class would
|
||||
@@ -1080,10 +1080,10 @@ def test_TWO_rows_for_our_context_are_ambiguous_rather_than_resolved_by_taking_t
|
||||
def test_an_EMPTY_response_body_is_refused_as_a_SHAPE_problem(gitea):
|
||||
"""An empty body yields no `.statuses` array, so it must refuse on the shape branch.
|
||||
|
||||
This test previously claimed to witness the row-count fallback and did not: an empty body exits
|
||||
This does NOT witness the row-count fallback: an empty body exits
|
||||
at the array-TYPE branch and never reaches the count. The count IS witnessed, by
|
||||
`test_a_readback_MISSING_our_context_...`, which supplies a real array with no matching row. The
|
||||
fallback the old test was written for turned out to be dead code and is gone — `.statuses` is
|
||||
`test_a_readback_MISSING_our_context_...`, which supplies a real array with no matching row. That
|
||||
fallback was dead code and is gone — `.statuses` is
|
||||
known to be an array by the time the count runs.
|
||||
"""
|
||||
gitea.set_status_body("")
|
||||
|
||||
@@ -7,10 +7,10 @@ inline in `.gitea/workflows/review-verdict.yml` (ENFORCED — it writes the bran
|
||||
ersatztv#643 hardening landed on the advisory copy and never reached the enforced one, so the copy
|
||||
with real authority ended up strictly weaker than the copy without.
|
||||
|
||||
The specific thing this suite pins is the point of ersatztv#649's second Done-when box. A round-4
|
||||
review traced that the enforced copy's fail-closed behaviour on a garbage response was INCIDENTAL,
|
||||
not designed: `n` came back empty, `[ "$n" -lt 50 ]` errored to false, the loop ran to MAX_PAGES and
|
||||
left complete=no. The right answer, reached through a bash arithmetic error that any refactor of the
|
||||
The specific thing this suite pins is the point of ersatztv#649's second Done-when box. The
|
||||
enforced copy's fail-closed behaviour on a garbage response was INCIDENTAL, not designed: `n` came
|
||||
back empty, `[ "$n" -lt 50 ]` errored to false, the loop ran to MAX_PAGES and left complete=no. The
|
||||
right answer, reached through a bash arithmetic error that any refactor of the
|
||||
loop could have silently flipped. Every failure-path test below therefore asserts a NON-ZERO exit
|
||||
explicitly, so the behaviour is a contract rather than a coincidence.
|
||||
|
||||
@@ -225,9 +225,8 @@ def test_non_array_body_fails_closed(enumerate_files):
|
||||
|
||||
|
||||
def test_an_OBJECT_OF_VALID_ROWS_isolates_the_top_level_array_check(enumerate_files):
|
||||
"""Cold review found `test_non_array_body_fails_closed` passing for the wrong reason, and the
|
||||
first attempt to fix it failed for a THIRD reason — worth recording, because both near-misses
|
||||
look like coverage.
|
||||
"""`test_non_array_body_fails_closed` passes for the wrong reason, and the nearest fixes for
|
||||
it fail for a THIRD reason — worth recording, because both near-misses look like coverage.
|
||||
|
||||
`jq`'s `all(.[]; …)` iterates an object's VALUES, so the top-level `type == "array"` check is
|
||||
only load-bearing when those values would themselves validate:
|
||||
@@ -301,7 +300,7 @@ def test_renamed_row_without_previous_filename_fails_closed(enumerate_files):
|
||||
|
||||
|
||||
def test_head_differing_from_the_expected_sha_fails_closed(enumerate_files):
|
||||
"""Narrowed to what this actually proves, per cold review.
|
||||
"""Scoped to what this actually proves.
|
||||
|
||||
The stub serves the alternate sha from the BINDING read (the one after paging; since
|
||||
ersatztv#698 the script also reads the PR object BEFORE paging, to bind the base). So this
|
||||
@@ -469,11 +468,11 @@ def test_the_base_comparator_is_the_BRANCH_NAME_never_the_TIP_SHA():
|
||||
Comparing tips would fail every enumeration on every unrelated merge to `main`: a self-inflicted
|
||||
deadlock dressed as a security control.
|
||||
|
||||
This is a STRUCTURAL assertion on purpose, and the previous version of this test is why. It was
|
||||
written behaviourally as `head_moves_to(SHA)` — the sha that was ALREADY current — so it modelled
|
||||
no movement at all and was simply a duplicate positive control. It would have passed just as
|
||||
happily against a script comparing tip shas. The stub serves only branch names, so no behavioural
|
||||
test in this harness can distinguish the two comparators; say so and assert the source instead.
|
||||
This is a STRUCTURAL assertion on purpose. A behavioural version — `head_moves_to(SHA)`, the
|
||||
sha that is ALREADY current — models no movement at all and is a duplicate positive control that
|
||||
would pass just as happily against a script comparing tip shas. The stub serves only branch
|
||||
names, so no behavioural test in this harness can distinguish the two comparators; say so and
|
||||
assert the source instead.
|
||||
"""
|
||||
src = SCRIPT.read_text()
|
||||
assert ".base.ref" in src, "the enumeration no longer reads .base.ref"
|
||||
@@ -497,8 +496,8 @@ def test_a_SHORT_page_does_not_end_the_enumeration(enumerate_files):
|
||||
# --- The same guard, under the runner's jq 1.6 ------------------------------------------------
|
||||
#
|
||||
# `test_transport_failure_mid_pagination_fails_closed` above does NOT isolate the explicit
|
||||
# `if [ -z "${raw//[[:space:]]/}" ]` clause — cold review claimed this and mutation confirmed it:
|
||||
# deleting that clause leaves the whole suite green on a developer Mac, because jq 1.8 rejects empty
|
||||
# `if [ -z "${raw//[[:space:]]/}" ]` clause — mutation confirms it: deleting that clause leaves
|
||||
# the whole suite green on a developer Mac, because jq 1.8 rejects empty
|
||||
# input on its own. jq 1.6 does not, and the runner ships 1.6 — so the one environment where the
|
||||
# clause is load-bearing was the one environment with no coverage. That is the #643/#647 failure
|
||||
# class exactly, reproduced in the test suite meant to prevent it.
|
||||
@@ -704,8 +703,8 @@ def test_the_caller_passes_the_BASE_REF_argument(caller):
|
||||
workflow would die on `set -u` (fail-closed, but it takes every PR with it), while the hook would
|
||||
pass an empty base and lose every docs-only exemption. Pin the call site itself.
|
||||
|
||||
Matching is deliberately narrow. A first draft keyed on "any line mentioning the script name" and
|
||||
matched the enum_error MESSAGE string, failing for a reason that had nothing to do with the call.
|
||||
Matching is deliberately narrow. Keying on "any line mentioning the script name" matches the
|
||||
enum_error MESSAGE string, failing for a reason that has nothing to do with the call.
|
||||
The workflow also invokes through `"$ENUM"` rather than the literal path, so the alias is resolved
|
||||
here and asserted to point at the shared script — otherwise this test could be satisfied while
|
||||
`ENUM` pointed somewhere else entirely.
|
||||
@@ -724,8 +723,8 @@ def test_the_caller_passes_the_BASE_REF_argument(caller):
|
||||
f"{caller.relative_to(REPO_ROOT)} calls the enumeration with {len(args)} quoted "
|
||||
f"arguments, expected 5 including the expected base ref: {ln.strip()!r}"
|
||||
)
|
||||
# Counting five arguments is not enough — cold review caught that passing `"$SHA"` twice
|
||||
# satisfied the count while stalling every real exemption. Name the fifth.
|
||||
# Counting five arguments is not enough — passing `"$SHA"` twice satisfies the count
|
||||
# while stalling every real exemption. Name the fifth.
|
||||
assert re.search(r"(?i)base", args[4]), (
|
||||
f"{caller.relative_to(REPO_ROOT)} passes {args[4]} as the 5th argument; it must be the "
|
||||
f"expected BASE ref: {ln.strip()!r}"
|
||||
@@ -739,8 +738,8 @@ def test_the_caller_does_not_reimplement_the_enumeration(caller):
|
||||
drift happened the first time. What must be prevented is a SECOND implementation existing.
|
||||
"""
|
||||
# An inline `pulls/<n>/files?` fetch is the signature of a re-inlined copy. Match on the endpoint
|
||||
# alone, NOT on `?limit=` — an earlier version anchored the query string, so a copy written as
|
||||
# `files?page=1&limit=50` would have walked straight past a guard that exists to stop exactly
|
||||
# alone, NOT on `?limit=` — anchoring the query string lets a copy written as
|
||||
# `files?page=1&limit=50` walk straight past a guard that exists to stop exactly
|
||||
# that. Still evadable by a copy that builds the URL without a literal `?`, so this narrows the
|
||||
# gap rather than closing it.
|
||||
assert not re.search(r"pulls/\$?\{?\w+\}?/files\?", _code_lines(caller)), (
|
||||
@@ -772,7 +771,7 @@ def _sentinel(name: str) -> str:
|
||||
Both sentinels are FIXED POINTS: the classification recognises its own previous output and
|
||||
refuses to grant an exemption over it. A test carrying its own copy of the literal would keep
|
||||
passing after the workflow's copy was reworded, while the real chain silently broke — the exact
|
||||
shape of the round-3 defect the fixed-point test exists to catch. Reading it from the body binds
|
||||
shape of the defect the fixed-point test exists to catch. Reading it from the body binds
|
||||
the two, and the count assertion means a renamed or duplicated assignment is a loud failure
|
||||
rather than a wrong string.
|
||||
"""
|
||||
@@ -826,8 +825,8 @@ def test_the_workflow_trigger_is_pull_request_TARGET_scoped_to_main():
|
||||
trigger is ADDED back alongside it.
|
||||
"""
|
||||
on = _workflow_triggers()
|
||||
# EXACT SET, not "target present and plain absent". Cold review found the weaker pair of
|
||||
# assertions green after ADDING `workflow_dispatch:` or `push:` alongside the safe trigger —
|
||||
# EXACT SET, not "target present and plain absent". The weaker pair of assertions stays green
|
||||
# after ADDING `workflow_dispatch:` or `push:` alongside the safe trigger —
|
||||
# both are ref-resolved and both get secrets, so either one restores an equivalent
|
||||
# self-supplied-definition path while the test reports clean. Enumerating the two known-bad
|
||||
# extra triggers would have the same hole one trigger later; pinning the whole set does not.
|
||||
@@ -913,11 +912,11 @@ def test_the_workflow_runs_the_jq_preflight_in_FLOOR_mode_only():
|
||||
check on `main`, so a pin would turn any jq upgrade on the runner into a repo-wide merge
|
||||
deadlock — a required gate failing because an upstream package manager did its job.
|
||||
"""
|
||||
# CODE only, for the reason `_code_lines` documents: the first draft of this assertion read the
|
||||
# raw text and went red on the workflow's own comment explaining why `--expect` is banned here.
|
||||
# CODE only, for the reason `_code_lines` documents: reading the raw text goes red on the
|
||||
# workflow's own comment explaining why `--expect` is banned here.
|
||||
code = _code_lines(WORKFLOW)
|
||||
# A bare `"jq-preflight.sh" in code` is NOT enough, and cold review was right to say so: the
|
||||
# path also appears in the `if [ -x ./scripts/jq-preflight.sh ]` presence guard, so deleting the
|
||||
# A bare `"jq-preflight.sh" in code` is NOT enough: the path also appears in the
|
||||
# `if [ -x ./scripts/jq-preflight.sh ]` presence guard, so deleting the
|
||||
# actual invocation would leave that substring behind and the assertion green. Require a line
|
||||
# that INVOKES it.
|
||||
steps = [s for s in _workflow_steps() if "jq-preflight.sh" in (s.get("run") or "")]
|
||||
@@ -988,8 +987,8 @@ if "-X" in args and args[args.index("-X") + 1] == "POST":
|
||||
# pending" from "posted pending once".
|
||||
#
|
||||
# Re-serialized COMPACT rather than appended verbatim: `jq -n` pretty-prints by default, so the
|
||||
# payloads arrive spanning several lines and a raw append would produce a file that is not JSONL
|
||||
# at all. (Found the honest way — the first draft of this recorder broke both race-2 tests.)
|
||||
# payloads arrive spanning several lines and a raw append would produce a file that is not
|
||||
# JSONL at all.
|
||||
with (out / "posted_all.jsonl").open("a") as fh:
|
||||
fh.write(json.dumps(json.loads(payload)) + "\n")
|
||||
print("{}")
|
||||
@@ -1003,9 +1002,9 @@ if "-X" in args and args[args.index("-X") + 1] == "POST":
|
||||
# type-sensitive: a NON-EMPTY page is a bare array of events, each with a `type`, and a retarget is
|
||||
# `change_target_branch` (confirmed on PR #703, the route-1 reproduction, which carries exactly two;
|
||||
# and on PR #717, never retargeted, which carries none). A page PAST THE END is the JSON value `null`
|
||||
# — NOT an empty array (measured again at 1.27.1 on PR #752, ersatztv#751). The earlier version of
|
||||
# this comment claimed measured fidelity while the terminator below printed `[]`; that discrepancy is
|
||||
# why the fence's type gate was never exercised and shipped rejecting every real timeline.
|
||||
# — NOT an empty array (measured again at 1.27.1 on PR #752, ersatztv#751). A terminator printing
|
||||
# `[]` instead leaves the fence's type gate unexercised, which is how it shipped rejecting every
|
||||
# real timeline.
|
||||
if "/timeline" in url:
|
||||
mode = os.environ.get("STUB_TIMELINE_MODE", "none")
|
||||
page = 1
|
||||
@@ -1031,8 +1030,7 @@ if "/timeline" in url:
|
||||
# as `transport`. Only a parseable non-array body distinguishes "break out of the retry on
|
||||
# anything readable" from "break only on a shape the `case` below accepts", and that
|
||||
# narrowing is a real fail-open — an error object would be read as the walk's terminator
|
||||
# verdict instead of being retried. The first draft of this fixture used the 502 page and
|
||||
# the mutant passed it.
|
||||
# verdict instead of being retried. With the 502 page as the fixture the mutant passes.
|
||||
rctr = out / "timeline_requests.txt"
|
||||
rseen = int(rctr.read_text()) if rctr.exists() else 0
|
||||
rctr.write_text(str(rseen + 1))
|
||||
@@ -1070,9 +1068,6 @@ if "/timeline" in url:
|
||||
# incremented only on the LAST-REAL-PAGE branch below, after the `page > pages` and
|
||||
# `page < pages` early exits — so it counts REAL PAGES SERVED, not requests: it holds 1
|
||||
# after the before-count, and `>= 2` first becomes true partway through the after-count.
|
||||
# (An earlier version of this comment said "requests 0-1 are the before-count", which is a
|
||||
# mechanism nobody measured; the arrangement is produced either way, but the description
|
||||
# was of a different program.)
|
||||
#
|
||||
# This is the only arrangement that separates the head arm from its trust guard: with both
|
||||
# counts trusted the guard is a no-op, and with both untrusted `pushes_before` is 0 too, so
|
||||
@@ -1335,9 +1330,9 @@ if "/statuses/" in url:
|
||||
# That is impossible — the endpoint returns one row per POST — but it is well-formed, so
|
||||
# nothing retries it and the walk reports SUCCESS over it.
|
||||
#
|
||||
# NO RACED VERDICT IS PLANTED, deliberately. An earlier shape appended one to dramatise
|
||||
# the stakes, and it was inert twice over: unconditionally it also joined the PRE-write
|
||||
# read, which set the mark to its OWN id — the strict `> $since` then excluded it — and
|
||||
# NO RACED VERDICT IS PLANTED, deliberately. Appending one to dramatise the stakes is
|
||||
# inert twice over: unconditionally it also joins the PRE-write
|
||||
# read, which sets the mark to its OWN id — the strict `> $since` then excludes it — and
|
||||
# gated to the post-write read it is never served at all, because page 1 answers `null`
|
||||
# before any row reaches the wire. A row the test
|
||||
# cannot observe is decoration that reads as coverage. What is actually under test is
|
||||
@@ -1427,7 +1422,7 @@ if "/statuses/" in url:
|
||||
seen = int(ctr.read_text()) if ctr.exists() else 0
|
||||
ctr.write_text(str(seen + 1))
|
||||
pages = [ordinary[:50], ordinary[50:], [dict(raced_row)] if seen > 0 else []]
|
||||
# SEEDED ROWS (ersatztv#849 round 2). The reconciliation may only clear the sentinel over a
|
||||
# SEEDED ROWS (ersatztv#849). The reconciliation may only clear the sentinel over a
|
||||
# history that CONTAINS it, which is the one shape the mode-driven fixtures above cannot
|
||||
# express: they describe a head as it was BEFORE this job ever wrote to it, and the sentinel
|
||||
# is by definition a row an EARLIER run already POSTed. Seeding is how a fixture says "this
|
||||
@@ -1486,7 +1481,7 @@ if "/statuses/" in url:
|
||||
# fixture where BOTH are false, `if false` disarms two guards at once and isolates neither.
|
||||
sys.exit(22)
|
||||
if hist_page == 1 and mode == "postwrite-page1-error":
|
||||
# FAILS THE POST-WRITE WALK ONLY (ersatztv#849 round 2). `premark-page1-error` fails the
|
||||
# FAILS THE POST-WRITE WALK ONLY (ersatztv#849). `premark-page1-error` fails the
|
||||
# FIRST logical read, which is the mark; the repair floor needs a run that got its mark, made
|
||||
# its write, and THEN could not read the history back. Both attempts of the second logical
|
||||
# read fail, so the retry cannot rescue it.
|
||||
@@ -1718,7 +1713,7 @@ if "/status" in url:
|
||||
print(json.dumps({"state": "pending", "total_count": len(rows), "statuses": rows}))
|
||||
sys.exit(0)
|
||||
if mode == "sentinel-replaced-mid-run":
|
||||
# THE SAME SENTINEL TEXT AT TWO DIFFERENT IDS (ersatztv#849 round 2). Both reads return an
|
||||
# THE SAME SENTINEL TEXT AT TWO DIFFERENT IDS (ersatztv#849). Both reads return an
|
||||
# unverified sentinel whose description is byte-identical — which is what a fixed point IS —
|
||||
# so only the row id distinguishes "the row I snapshotted" from "a row another run wrote
|
||||
# while I classified". Ids are carried here and nowhere else in this stub because this is the
|
||||
@@ -1738,7 +1733,7 @@ if "/status" in url:
|
||||
sys.exit(0)
|
||||
if mode.startswith("sentinel-appears-on-read:"):
|
||||
# A repair sentinel written by ANOTHER, overlapping run between this job's first read and its
|
||||
# last-moment re-read (ersatztv#706 round 3). Creator is null: the sentinel is machine-written.
|
||||
# last-moment re-read (ersatztv#706). Creator is null: the sentinel is machine-written.
|
||||
nth = int(mode.split(":", 1)[1])
|
||||
ctr = out / "status_reads.txt"
|
||||
n = int(ctr.read_text()) if ctr.exists() else 0
|
||||
@@ -1934,11 +1929,11 @@ def _run_classify(
|
||||
# at the wrong endpoint, sha, host or repo would otherwise leave these tests green while the real
|
||||
# required check was never written.
|
||||
#
|
||||
# Two ways this check could disable itself, both found by cold review of an earlier draft:
|
||||
# * it was guarded by `if url_file.exists()`, so deleting the recorder in the stub turned it
|
||||
# into a no-op and every test stayed green — a verifier that silently opts out;
|
||||
# * it compared only the URL SUFFIX, so a POST to the right path on the WRONG HOST OR REPO
|
||||
# passed. Compare the whole URL against the env this job was given.
|
||||
# Two ways this check could disable itself:
|
||||
# * guarding it with `if url_file.exists()` — deleting the recorder in the stub then turns it
|
||||
# into a no-op and every test stays green, a verifier that silently opts out;
|
||||
# * comparing only the URL SUFFIX — a POST to the right path on the WRONG HOST OR REPO then
|
||||
# passes. Compare the whole URL against the env this job was given.
|
||||
posted = tmp_path / "posted.json"
|
||||
url_file = tmp_path / "posted_url.txt"
|
||||
if posted.exists():
|
||||
@@ -1987,7 +1982,7 @@ def _assert_withheld(tmp_path, r, why, expect_rc):
|
||||
|
||||
|
||||
def test_a_FAILING_enumeration_withholds_the_exemption_even_when_stdout_looks_docs_only(tmp_path):
|
||||
"""The mutation that previously survived: ignore the exit status, trust stdout."""
|
||||
"""The mutation this closes: ignore the exit status, trust stdout."""
|
||||
posted, r = _run_classify(tmp_path, "#!/usr/bin/env bash\n" + DOCS_ONLY + "exit 1\n")
|
||||
assert posted is not None, f"the job posted no status at all: {r.stderr}"
|
||||
assert posted["state"] == "pending", (
|
||||
@@ -2039,11 +2034,11 @@ def test_a_code_file_defeats_the_docs_only_exemption(tmp_path):
|
||||
|
||||
def test_a_BOT_pr_touching_a_protected_path_is_NOT_exempt(tmp_path):
|
||||
"""`PROTECTED` guards the BOT exemption specifically, and mutation testing is how that got
|
||||
stated correctly. The first version of this test used a docs-only+protected file list and
|
||||
passed even with the `PROTECTED` clause deleted — `PROTECTED` (`.claude/ .gitea/ .husky/
|
||||
scripts/ docker/ci/`) and `DOCS_ONLY` (`docs/`, root `*.md`) are DISJOINT, so on the docs-only
|
||||
path that clause can never fire and the `DOCS_ONLY` check was doing all the work. The test
|
||||
looked like it covered the self-exemption hole and covered nothing.
|
||||
stated correctly. A docs-only+protected file list passes even with the `PROTECTED` clause
|
||||
deleted — `PROTECTED` (`.claude/ .gitea/ .husky/ scripts/ docker/ci/`) and `DOCS_ONLY`
|
||||
(`docs/`, root `*.md`) are DISJOINT, so on the docs-only path that clause can never fire and
|
||||
the `DOCS_ONLY` check does all the work. Such a test looks like it covers the self-exemption
|
||||
hole and covers nothing.
|
||||
|
||||
Renovate lands patch bumps unattended via Gitea's own auto-merge, so a bot PR that edits the
|
||||
gate, CI, the hooks, or the scripts they call is the one path where an unreviewed change to the
|
||||
@@ -2079,8 +2074,8 @@ def test_a_transport_failure_on_the_STATUS_READ_REPLACES_the_unknown_state(tmp_p
|
||||
verdict exists" would let the job post over a real human verdict, so the exemption is still
|
||||
withheld — but "withheld" now means REPLACED, not merely not-written (ersatztv#849 route 5).
|
||||
|
||||
This test used to assert `posted is None`, on the reasoning that declining to write protects a
|
||||
verdict this job cannot see. True when the head carries a REAL verdict; exactly wrong when it
|
||||
Asserting `posted is None` here — on the reasoning that declining to write protects a verdict
|
||||
this job cannot see — is true when the head carries a REAL verdict and exactly wrong when it
|
||||
carries a FORGED one. An off-list credential's `review-verdict/h10=success` is the status #742
|
||||
exists to revoke, revocation happens by re-deriving the row, and an unreadable read is the one
|
||||
thing that stops it. The job went red — and its own job status is not a required check, so
|
||||
@@ -2190,11 +2185,11 @@ def test_a_transport_failure_under_jq_1_6_STILL_refuses_to_classify(tmp_path):
|
||||
jq 1.6, which is what the runner ships**. Remove that check and, on a dev Mac's 1.8, the guard
|
||||
still fires and every test stays green — the bug is invisible locally.
|
||||
|
||||
An earlier version of this pinned the construct STRUCTURALLY instead, on the stated grounds that
|
||||
"no behavioural test can catch this on a dev machine". That was wrong: this file already imports
|
||||
`_JQ16_SHIM` for `pr-changed-files.sh`, so the runner's quirk is reproducible here. The structural
|
||||
version was also weaker than it looked — it stripped only FULL-LINE comments, so leaving the
|
||||
literal as a trailing comment on the surviving `if` satisfied it while the real guard was gone.
|
||||
Pinning the construct STRUCTURALLY instead, on the grounds that "no behavioural test can catch
|
||||
this on a dev machine", is wrong: this file already imports `_JQ16_SHIM` for
|
||||
`pr-changed-files.sh`, so the runner's quirk is reproducible here. A structural version is also
|
||||
weaker than it looks — it strips only FULL-LINE comments, so leaving the literal as a trailing
|
||||
comment on the surviving `if` satisfies it while the real guard is gone.
|
||||
|
||||
This test is strictly stronger: it catches that mutant, needs no comment-stripping, and fails for
|
||||
the right reason. Verified by mutation under both jq versions.
|
||||
@@ -2413,13 +2408,13 @@ def test_a_verdict_shaped_SUCCESS_from_a_NON_allowlisted_account_is_RE_DERIVED(t
|
||||
# a reviewer has been added to the repo and forgotten from `H10_REVIEWERS`, and a tidy-up that
|
||||
# drops any of its three values must redden something. Asserted on the LOG LINE the job always
|
||||
# reaches, not on a separate annotation: the annotation that used to carry this was withdrawn
|
||||
# after cold review measured that deleting it left the whole suite green.
|
||||
# because deleting it left the whole suite green.
|
||||
assert creator in r.stdout, (
|
||||
f"the diagnostic does not name the account whose verdict was discarded. Log:\n{r.stdout[-1200:]}"
|
||||
)
|
||||
# The VALUE, not the word. An earlier draft asserted `"H10_REVIEWERS" in stdout`, which the
|
||||
# remediation sentence satisfies on its own — so deleting the allow-list from the diagnostic left
|
||||
# it green. Derived from the shipped literal rather than restated here.
|
||||
# The VALUE, not the word. Asserting `"H10_REVIEWERS" in stdout` is satisfied by the
|
||||
# remediation sentence on its own — so deleting the allow-list from the diagnostic leaves it
|
||||
# green. Derived from the shipped literal rather than restated here.
|
||||
assert f"allow-list='{_h10_reviewers_literal()}'" in r.stdout, (
|
||||
"the diagnostic does not print the accepted set, so a reviewer left off it cannot tell why "
|
||||
f"their verdict was re-derived. Log:\n{r.stdout[-1200:]}"
|
||||
@@ -2485,7 +2480,7 @@ def test_an_account_that_merely_RESEMBLES_an_allowlisted_login_is_RE_DERIVED(tmp
|
||||
def test_the_RACED_verdict_check_still_counts_a_NON_allowlisted_creator(tmp_path):
|
||||
"""THE POLARITY TEST, and the reason the allow-list was not applied everywhere `creator` is read.
|
||||
|
||||
NOT for the obvious reason, which cold review showed to be unchecked: a genuine reviewer is on
|
||||
NOT for the obvious reason, which does not hold: a genuine reviewer is on
|
||||
`$H10_REVIEWERS` by construction, so membership here would still count every real verdict, and an
|
||||
off-list row is one the job deliberately overwrote a few lines earlier.
|
||||
|
||||
@@ -2514,7 +2509,7 @@ def test_the_RACED_verdict_check_still_counts_a_NON_allowlisted_creator(tmp_path
|
||||
|
||||
|
||||
def test_the_MID_RUN_abstain_still_fires_for_a_NON_allowlisted_creator(tmp_path):
|
||||
"""The SECOND mirror-image site, and the one the first draft of #742 got wrong.
|
||||
"""The SECOND mirror-image site, and the one a #742-style narrowing gets wrong.
|
||||
|
||||
`read_existing_verdict` is called twice. The first call decides "may this be INHERITED" and is
|
||||
correctly allow-list-gated. The second — the last-moment re-read immediately before the POST —
|
||||
@@ -2576,9 +2571,9 @@ def test_every_test_name_cited_by_the_exemption_provenance_record_EXISTS():
|
||||
"""A record that names a test as its mutation proof must name one that can be run.
|
||||
|
||||
This is not hypothetical: renaming a test while splitting the `success`/`failure` cases left
|
||||
`ci.exemption-provenance` citing `..._status_from_...` when the tree had `..._SUCCESS_from_...`,
|
||||
found by cold review. A citation that cannot be resolved is worse than none — it reads as a
|
||||
checked proof and sends the next reader looking for a test that does not exist.
|
||||
`ci.exemption-provenance` citing `..._status_from_...` when the tree had `..._SUCCESS_from_...`.
|
||||
A citation that cannot be resolved is worse than none — it reads as a checked proof and sends
|
||||
the next reader looking for a test that does not exist.
|
||||
|
||||
Deliberately scoped to the ONE record that carries this change's proofs, and derived from the
|
||||
record's own text rather than from a list restated here, so a newly cited name is covered the
|
||||
@@ -2704,9 +2699,9 @@ def test_an_UNATTRIBUTABLE_failure_is_still_RE_DERIVED(tmp_path, shape, creator,
|
||||
The clause is `[ "$ex_attributable" = yes ] && [ "$ex_state" = "failure" ]`, and the comment at
|
||||
the site says plainly that widening it to a bare `[ "$ex_state" = failure ]` is deliberately NOT
|
||||
done: that would quietly opt `failure` out of the base binding, which exists because a verdict
|
||||
earned against another base is not a verdict for this diff. Cold cross-family review pointed out
|
||||
that every existing `failure` case supplied a well-formed, base-matching, non-null-creator row,
|
||||
so the bare-widening mutation passed the whole suite.
|
||||
earned against another base is not a verdict for this diff. Every existing `failure` case
|
||||
supplied a well-formed, base-matching, non-null-creator row, so the bare-widening mutation
|
||||
passed the whole suite.
|
||||
|
||||
Docs-only on purpose: re-deriving is then visible as an exemption `success` being posted, where
|
||||
short-circuiting would post nothing at all.
|
||||
@@ -2838,11 +2833,10 @@ def test_a_REAL_human_verdict_is_still_NEVER_overwritten(tmp_path, existing):
|
||||
f"overwrote a human '{existing}' verdict written by timothy — the never-overwrite property "
|
||||
"has been lost while fixing route 3"
|
||||
)
|
||||
# NO "the diagnostic did not fire" ASSERTION HERE. An earlier version of this test carried one,
|
||||
# captioned as pinning a guard on the annotation — but both parametrizations exit at a
|
||||
# short-circuit two guards earlier and never reach the diagnostic at all, so the assertion was
|
||||
# true by construction and could not fail. The annotation it guarded has since been withdrawn;
|
||||
# the lesson is kept because a caption claiming a proof is worse than no assertion.
|
||||
# NO "the diagnostic did not fire" ASSERTION HERE. Both parametrizations exit at a
|
||||
# short-circuit two guards earlier and never reach the diagnostic at all, so such an assertion
|
||||
# is true by construction and cannot fail. The annotation it would guard has since been
|
||||
# withdrawn; the note is kept because a caption claiming a proof is worse than no assertion.
|
||||
|
||||
|
||||
def test_a_PENDING_status_from_a_previous_run_is_replaced_normally(tmp_path):
|
||||
@@ -2861,7 +2855,7 @@ def test_a_PENDING_status_from_a_previous_run_is_replaced_normally(tmp_path):
|
||||
|
||||
@pytest.mark.parametrize("path", ["web/package.json", "web/package-lock.json"])
|
||||
def test_the_npm_manifests_are_NOT_exempt(tmp_path, path):
|
||||
"""Cross-family review called an earlier draft's inclusion of these a Blocker, correctly.
|
||||
"""Including these in the exemption is a Blocker.
|
||||
|
||||
`renovate.json` sets `enabledManagers: ["nuget", "github-actions", "dockerfile"]`, so Renovate does
|
||||
not manage npm here at all — the entry bought nothing. Meanwhile `package.json` carries `scripts`
|
||||
@@ -2876,16 +2870,16 @@ def test_the_npm_manifests_are_NOT_exempt(tmp_path, path):
|
||||
)
|
||||
|
||||
|
||||
# --- The `grep -q` + `pipefail` inversion (found by cross-family review of this PR) ---------------
|
||||
# --- The `grep -q` + `pipefail` inversion ---------------------------------------------------------
|
||||
#
|
||||
# `grep -q` exits at its FIRST match, so the upstream writer takes SIGPIPE (141) once the path list
|
||||
# exceeds the pipe buffer. Under `set -o pipefail` the pipeline is then a FAILURE even though grep
|
||||
# MATCHED, inverting every guard built on `printf … | grep -q`. Reproduced at 171KB / 1901 paths,
|
||||
# comfortably inside the enumerator's 2000-file cap.
|
||||
#
|
||||
# These are the regression guards, and they are the tests the original round did not have: every
|
||||
# earlier case used a handful of short paths, far below the buffer, so the whole class was invisible.
|
||||
# The construct PREDATES #698, so `main` carried this hole with no retarget or bot account required.
|
||||
# These are the regression guards. Every earlier case used a handful of short paths, far below the
|
||||
# buffer, so the whole class was invisible. The construct PREDATES #698, so `main` carried this hole
|
||||
# with no retarget or bot account required.
|
||||
|
||||
|
||||
def _many_docs(n=1900):
|
||||
@@ -2959,8 +2953,8 @@ def test_positive_control_no_late_verdict_still_posts_normally(tmp_path):
|
||||
|
||||
# --- The PROTECTED branch must actually EXECUTE, not merely coincide with the right answer --------
|
||||
#
|
||||
# Round-3 review found `count_matching` being called before its definition, so it was
|
||||
# `command not found` on every run and the PROTECTED branch never fired. Three "protected path" tests
|
||||
# `count_matching` was called before its definition, so it was `command not found` on every run and
|
||||
# the PROTECTED branch never fired. Three "protected path" tests
|
||||
# passed anyway, because a protected path is also not a manifest and not docs-only, so the job reached
|
||||
# `pending` down a different route. Asserting the STATE could not see it; the guard was dead and the
|
||||
# suite was green.
|
||||
@@ -2975,8 +2969,8 @@ def test_a_protected_path_is_rejected_BY_THE_PROTECTED_BRANCH(tmp_path):
|
||||
assert posted["state"] == "pending"
|
||||
# The DISCRIMINATOR is the job's `Decision:` line, not the status description: for `pending` the
|
||||
# description is always "Awaiting review verdict for <sha>", identical no matter which branch
|
||||
# produced it. A first draft of this test asserted on the description and failed against a WORKING
|
||||
# guard — the assertion has to be aimed at something that actually differs per branch.
|
||||
# produced it. Asserting on the description fails against a WORKING guard — the assertion has
|
||||
# to be aimed at something that actually differs per branch.
|
||||
assert "protected" in r.stdout.lower(), (
|
||||
"the PR was not exempted, but NOT via the protected-path branch — it reached the same verdict "
|
||||
f"by another route, so that guard may be dead. Decision log:\n{r.stdout[-800:]}"
|
||||
@@ -3031,7 +3025,7 @@ def test_the_classify_step_runs_without_SHELL_ERRORS(tmp_path, paths, author):
|
||||
|
||||
|
||||
def test_a_human_verdict_formed_against_ANOTHER_BASE_is_not_inherited(tmp_path):
|
||||
"""Round-4 review: the sha-binding is escapable through the HUMAN verdict path.
|
||||
"""The sha-binding is escapable through the HUMAN verdict path.
|
||||
|
||||
Get a genuine `success` on head H while it targets scratch base S (benign diff there), then
|
||||
retarget H onto `main`, where its diff contains unreviewed code. Creator is real, prefix is real,
|
||||
@@ -3121,7 +3115,7 @@ def test_an_EMPTY_recorded_base_is_treated_as_a_mismatch(tmp_path):
|
||||
|
||||
|
||||
def test_a_branch_name_containing_a_PAREN_cannot_truncate_into_the_current_base(tmp_path):
|
||||
"""Round-5 review, and the sharpest finding of the five: it needs no forgery and no #697.
|
||||
"""The sharpest of these escapes: it needs no forgery and no #697.
|
||||
|
||||
`main)evil` is a VALID git branch name (`git check-ref-format --branch 'main)evil'` succeeds). A
|
||||
genuine verdict earned while head H targeted it is written `(base: main)evil)`. Any implementation
|
||||
@@ -3149,10 +3143,10 @@ def test_a_branch_name_containing_a_PAREN_cannot_truncate_into_the_current_base(
|
||||
|
||||
# --- ersatztv#706: the retarget fence (race 1) and the post-write repair (race 2) -----------------
|
||||
#
|
||||
# These three stay 1.25.4-dated on purpose, but NOT all for the same reason — an earlier version of
|
||||
# this note gave one blanket reason and cold review showed it fits only the third. The first two were
|
||||
# measured on probe PR #722, and a scratch PR is still raisable; they were simply not re-run
|
||||
# (ersatztv#747 did not re-measure the `pull_request_target` half either). Only the third — Gitea
|
||||
# These three stay 1.25.4-dated on purpose, but NOT all for the same reason — a blanket reason fits
|
||||
# only the third. The first two were measured on probe PR #722, and a scratch PR is still raisable;
|
||||
# they were simply not re-run (ersatztv#747 did not re-measure the `pull_request_target` half
|
||||
# either). Only the third — Gitea
|
||||
# auto-cancelling superseded `push` runs — used the disposable scratch-BRANCH push route, and that
|
||||
# route is genuinely gone. See `ci.verdict-write-retarget-fence`, re-checked 2026-09-02
|
||||
# (ersatztv#869).
|
||||
@@ -3209,7 +3203,7 @@ def _fold_continuations(lines):
|
||||
|
||||
The exit-status detector is per-line, so a `jq \` whose flag sits on the next line would slip
|
||||
past it. Folding first closes that — but only if the fold matches the shell, and the separator is
|
||||
where it goes wrong. An earlier version joined with a SPACE, which is not cosmetic: it turns
|
||||
where it goes wrong. Joining with a SPACE is not cosmetic: it turns
|
||||
`jq -\` + `e '.'` into `jq - e '.'` (two tokens, no flag) where bash produces `jq -e '.'`.
|
||||
|
||||
WHICH SPLITS ARE ACTUALLY REACHABLE, measured rather than assumed, because the obvious
|
||||
@@ -3319,7 +3313,7 @@ def test_the_continuation_fold_matches_the_SHELL(raw, folded):
|
||||
(""" x=$(jq -re '.a')""", True),
|
||||
(""" x=$(jq -er '.a')""", True),
|
||||
(""" x=$(jq -e '.a')""", True),
|
||||
# The two the leading-token parser missed, named by cold review of round 4.
|
||||
# The two the leading-token parser missed.
|
||||
(""" x=$(jq --argjson e 1 -e '.')""", True),
|
||||
(""" x=$(jq '.a' -e)""", True),
|
||||
(""" n=$(printf '%s' "$raw" | jq -r 'length')""", False),
|
||||
@@ -3335,7 +3329,7 @@ def test_the_continuation_fold_matches_the_SHELL(raw, folded):
|
||||
def test_the_jq_exit_status_detector_reads_FLAGS_not_a_substring(line, expected):
|
||||
"""The detector is itself a string-matching predicate, so it gets its own cases.
|
||||
|
||||
`"jq -e" in line` was the first version. It misses `jq -re`, `jq -er`, `jq -e` and
|
||||
A bare `"jq -e" in line` misses `jq -re`, `jq -er`, `jq -e` and
|
||||
`jq --exit-status` — three spellings of the same request (it catches `jq -er`, since `jq -e`
|
||||
is a substring of it) — and a guard a rename walks straight
|
||||
through is not a guard. The false-positive rows matter as much: an `e` inside the jq PROGRAM is
|
||||
@@ -3352,11 +3346,11 @@ def test_count_pr_mutations_uses_NO_jq_e(tmp_path):
|
||||
already took this workflow's enforced gate down once (ersatztv#647), on a runner that still ships
|
||||
1.6. Every tally here is therefore read with `jq -r` and range-checked in shell.
|
||||
|
||||
This exists because the rule was BROKEN one commit after it was quoted (ersatztv#803 round 2 added
|
||||
a row-validation guard using `jq -e`, three lines below the comment forbidding it) and nothing
|
||||
caught it — it was found by hand while re-reading the diff. A convention stated only in prose is
|
||||
one refactor from being false, and this one is function-scoped: `jq -e` is legitimate elsewhere in
|
||||
this same workflow and in the hook, so a file-wide grep would be wrong.
|
||||
This exists because the rule was BROKEN one commit after it was quoted (ersatztv#803 added a
|
||||
row-validation guard using `jq -e`, three lines below the comment forbidding it) and nothing
|
||||
caught it. A convention stated only in prose is one refactor from being false, and this one is
|
||||
function-scoped: `jq -e` is legitimate elsewhere in this same workflow and in the hook, so a
|
||||
file-wide grep would be wrong.
|
||||
|
||||
Comments are stripped, so the paragraphs explaining the rule do not redden it.
|
||||
"""
|
||||
@@ -3380,9 +3374,9 @@ def test_count_pr_mutations_uses_NO_jq_e(tmp_path):
|
||||
# assertion above cannot tell the two apart — so a `jq -e` past that point would be invisible
|
||||
# while the guard reported success.
|
||||
#
|
||||
# The check is STRUCTURAL, not a pinned statement. An earlier version asserted that the body
|
||||
# contained `page=$(( page + 1 ))`, which is a real line near the end and would have worked —
|
||||
# and would also have reddened on any refactor of the increment, for no reason connected to
|
||||
# The check is STRUCTURAL, not a pinned statement. Asserting that the body contains
|
||||
# `page=$(( page + 1 ))` would work — it is a real line near the end — and would also redden
|
||||
# on any refactor of the increment, for no reason connected to
|
||||
# this guard. A guard that cries wolf gets waived, which is how the repo already has to
|
||||
# document one CI job as "do nothing". Requiring the capture to END on the closing brace says
|
||||
# the same thing about extent while surviving every edit to the body.
|
||||
@@ -3524,9 +3518,8 @@ def test_a_PR_PUSHED_BEFORE_the_run_but_QUIET_during_it_is_STILL_exempt(tmp_path
|
||||
# reachable at all. The arms are evaluated base-first, so a tally that folds PUSHES into the retarget
|
||||
# total is caught only by the push-moves row (the base arm fires and pre-empts the head message);
|
||||
# folding retargets into the push total is NOT observable from here for the same ordering reason,
|
||||
# and is deliberately not claimed to be. Stating that is the point — an earlier version of this test
|
||||
# asserted the base-moves row alone while its docstring claimed it caught the folding mutation, and
|
||||
# it stayed GREEN under exactly that mutation.
|
||||
# and is deliberately not claimed to be. Stating that is the point — asserting the base-moves row
|
||||
# alone while claiming to catch the folding mutation stays GREEN under exactly that mutation.
|
||||
_AXIS_ROWS = [
|
||||
(
|
||||
"moves:0,1",
|
||||
@@ -3724,8 +3717,8 @@ def test_a_FILTERED_page_before_a_FULL_cap_page_is_still_untrusted(tmp_path):
|
||||
|
||||
(The reset is ALSO covered from the other side: deleting it makes the three filtered-page tests
|
||||
above go red, because a stale `yes` suppresses the tally on every later non-empty page and the
|
||||
hidden events stop being counted. Measured, not assumed — an early review round predicted those
|
||||
three would stay green.)
|
||||
hidden events stop being counted. Measured, not assumed — the intuition that those three would
|
||||
stay green is wrong.)
|
||||
"""
|
||||
posted, r = _run_classify(
|
||||
tmp_path,
|
||||
@@ -3763,9 +3756,8 @@ def test_a_SINGLE_transient_blip_does_not_cost_the_exemption(tmp_path, blip):
|
||||
|
||||
That is what kills the narrowing `if [ -n "$kind" ]; then break; fi`, under which an error object
|
||||
ends the retry and is then refused by the `case`, costing the exemption. An UNREADABLE body does
|
||||
not kill it: jq fails, `kind=""`, and the walk takes `transport`'s path. The first version of
|
||||
this fixture used a 502 HTML page for exactly that reason and the mutant passed it — measured,
|
||||
not assumed.
|
||||
not kill it: jq fails, `kind=""`, and the walk takes `transport`'s path. With a 502 HTML page
|
||||
as the fixture, for exactly that reason, the mutant passes — measured, not assumed.
|
||||
"""
|
||||
posted, r = _run_classify(
|
||||
tmp_path,
|
||||
@@ -3990,17 +3982,17 @@ def test_BOTH_hook_directories_are_in_the_PROTECTED_pattern():
|
||||
)
|
||||
|
||||
|
||||
# --- ersatztv#706, round 2: findings from the cold adversarial review ----------------------------
|
||||
# --- ersatztv#706: further findings on the fence and the repair ----------------------------------
|
||||
|
||||
|
||||
def test_the_high_water_MARK_is_captured_BEFORE_the_last_moment_re_read():
|
||||
"""The High finding of round 2, pinned as the ORDERING property it actually is.
|
||||
"""The ORDERING property the high-water mark rests on, pinned as an order rather than an output.
|
||||
|
||||
The mark was originally taken "as late as possible", just before the POST. That leaves everything
|
||||
between the last-moment re-read and the mark as a blind gap: a human verdict landing there is
|
||||
invisible to the re-read (already done) and excluded from the post-write check (its id is BELOW a
|
||||
mark taken afterwards), so it is overwritten with no repair. The gap spanned the whole retarget
|
||||
re-count — up to 20 timeline round-trips — not the single round-trip that was being claimed.
|
||||
Taking the mark "as late as possible", just before the POST, leaves everything between the
|
||||
last-moment re-read and the mark as a blind gap: a human verdict landing there is invisible to
|
||||
the re-read (already done) and excluded from the post-write check (its id is BELOW a mark taken
|
||||
afterwards), so it is overwritten with no repair. The gap spans the whole retarget re-count — up
|
||||
to 20 timeline round-trips, not the single round-trip it looks like.
|
||||
|
||||
Taking the mark FIRST closes the read side: any row newer than the mark is caught either by the
|
||||
re-read (abstain) or by the post-write check (repair). This is asserted structurally because the
|
||||
@@ -4010,8 +4002,8 @@ def test_the_high_water_MARK_is_captured_BEFORE_the_last_moment_re_read():
|
||||
src = _classify_step()["run"]
|
||||
# Assert on BOTH the init and the FETCH: keying only on the init line would let a refactor that
|
||||
# splits initialisation from the read slide the actual round-trip back past the re-read while this
|
||||
# test stayed green (round-3 review nit).
|
||||
# The FETCH is now the first bare `page_statuses` CALL (ersatztv#763 replaced the single
|
||||
# test stayed green. The FETCH is now the first bare `page_statuses` CALL (ersatztv#763 replaced
|
||||
# the single
|
||||
# `hist_before=$(gh …)` read with the paged walk). Anchoring on the call and not on the function
|
||||
# definition matters: the definition sits with the other helpers near the top of the step, so
|
||||
# keying on it would place the "fetch" far earlier than the round-trip actually happens and this
|
||||
@@ -4031,7 +4023,7 @@ def test_the_high_water_MARK_is_captured_BEFORE_the_last_moment_re_read():
|
||||
|
||||
|
||||
def test_a_previously_REPAIRED_head_is_never_re_exempted(tmp_path):
|
||||
"""The Medium finding of round 2: the repair used to last exactly one event.
|
||||
"""Without a sentinel, the repair lasts exactly one event.
|
||||
|
||||
After a repair, the status is a machine-written `pending` — indistinguishable, to the next run,
|
||||
from an ordinary one. That run re-derived it, posted `success`, and took a fresh high-water mark
|
||||
@@ -4090,8 +4082,8 @@ def test_the_fence_gates_PENDING_TOO_not_only_the_exemption(tmp_path):
|
||||
|
||||
|
||||
def test_an_UNTRUSTED_count_withholds_the_exemption_BY_THAT_BRANCH(tmp_path):
|
||||
"""Round 2 test-gap: the existing untrusted-count test asserted only "posted nothing", which a
|
||||
crash also produces. Assert the discriminator and a clean exit."""
|
||||
"""Asserting only "posted nothing" is satisfied by a crash too. Assert the discriminator and a
|
||||
clean exit."""
|
||||
posted, r = _run_classify(tmp_path, _emitting("docs/a.md"), timeline_mode="unreadable")
|
||||
_assert_withheld(tmp_path, r, "test_an_UNTRUSTED_count_withholds_the_exemption_BY_THAT_BRANCH", expect_rc=0)
|
||||
assert r.returncode == 0, f"the job died rather than declining cleanly: {r.stderr[-800:]}"
|
||||
@@ -4103,13 +4095,13 @@ def test_an_UNTRUSTED_count_withholds_the_exemption_BY_THAT_BRANCH(tmp_path):
|
||||
|
||||
|
||||
def test_the_repair_sentinel_is_a_FIXED_POINT_across_consecutive_runs(tmp_path):
|
||||
"""Round-3 finding: durability is a fixed point, and only a CHAIN can assert a fixed point.
|
||||
"""Durability is a fixed point, and only a CHAIN can assert a fixed point.
|
||||
|
||||
The first version of the sentinel refused the exemption but posted the GENERIC pending
|
||||
description, erasing the marker it depends on. The next run then saw an ordinary machine
|
||||
`pending`, re-derived it, and posted `success` — burying the human rejection two events after the
|
||||
repair instead of one. The single-hop test passed throughout, and the positive control asserting
|
||||
that an ordinary machine `pending` DOES re-derive was itself the proof of the second hop.
|
||||
A sentinel that refuses the exemption but posts the GENERIC pending description erases the
|
||||
marker it depends on. The next run then sees an ordinary machine `pending`, re-derives it, and
|
||||
posts `success` — burying the human rejection two events after the repair instead of one. A
|
||||
single-hop test passes throughout, and the positive control asserting that an ordinary machine
|
||||
`pending` DOES re-derive is itself the proof of the second hop.
|
||||
|
||||
So chain two runs: feed run N's posted description in as run N+1's existing status. The property
|
||||
is that the sentinel branch's own output re-triggers the sentinel branch, forever.
|
||||
@@ -4143,7 +4135,7 @@ def test_the_repair_sentinel_is_a_FIXED_POINT_across_consecutive_runs(tmp_path):
|
||||
|
||||
|
||||
def test_a_sentinel_APPEARING_MID_RUN_stops_a_stale_run_overwriting_it(tmp_path):
|
||||
"""Round-3 finding, and the only one in this series that failed toward SUCCESS.
|
||||
"""The only case in this series that failed toward SUCCESS.
|
||||
|
||||
Two runs overlap for the same sha — the regime this branch measured live (probe PR #722: the older
|
||||
run finished 20s after the newer one started). Run B catches a raced human BLOCKED and repairs to
|
||||
@@ -4185,7 +4177,7 @@ def test_positive_control_a_sentinel_present_from_the_START_still_posts_pending(
|
||||
|
||||
|
||||
def test_a_PENDING_path_run_also_refuses_to_clobber_a_mid_run_sentinel(tmp_path):
|
||||
"""Round-4 finding: the guard's first form tested `state = success`, one branch too narrow.
|
||||
"""A guard testing `state = success` is one branch too narrow.
|
||||
|
||||
A run can reach the POST on `pending` carrying the GENERIC description — most realistically after a
|
||||
transient enumeration failure — and a success-only guard waves it through. It then overwrites the
|
||||
@@ -4212,7 +4204,7 @@ def test_a_PENDING_path_run_also_refuses_to_clobber_a_mid_run_sentinel(tmp_path)
|
||||
|
||||
|
||||
def test_a_SENTINEL_landing_above_the_mark_also_triggers_the_repair(tmp_path):
|
||||
"""Round-5 finding: the post-write filter counted only HUMAN rows, and that is not sufficient.
|
||||
"""Counting only HUMAN rows in the post-write filter is not sufficient.
|
||||
|
||||
With two overlapping runs A and B, the human BLOCKED can land BELOW A's high-water mark — so A
|
||||
cannot see it — while B masks it with an exemption `success` and only afterwards writes the
|
||||
@@ -4306,7 +4298,7 @@ _EXPR_LITERALS = frozenset({"true", "false", "null"})
|
||||
def _yaml_string_scalars(path: Path):
|
||||
"""Every string scalar in the parsed document — keys and values, recursively.
|
||||
|
||||
Deliberately PARSED rather than raw (ersatztv#751, cold re-review). A `${{ … }}` in an ordinary
|
||||
Deliberately PARSED rather than raw (ersatztv#751). A `${{ … }}` in an ordinary
|
||||
top-level YAML comment is inert: the runner never evaluates it, so redding on it would be a false
|
||||
positive of exactly the kind this file has now produced twice. PyYAML drops those comments, which
|
||||
is the behaviour wanted here.
|
||||
@@ -4378,20 +4370,19 @@ def test_the_verdict_workflow_has_NO_expression_delimiter_in_any_run_body():
|
||||
"value, so a bad payload there cannot take the body with it."
|
||||
)
|
||||
# ANTI-VACUITY, and it has to be the right property. A `run:` body the YAML walk never reached
|
||||
# would make the assertion above vacuously green — the failure mode to guard against. An earlier
|
||||
# version of this guard compared the file's TOTAL delimiter count against the count inside
|
||||
# `with:`/`env:` values, which is a different and wrong claim: it banned expressions everywhere
|
||||
# else in the file too. Both reviewers reproduced the false red — writing `if: ${{ always() }}`,
|
||||
# the standard and equivalent spelling of the `if:` two steps below, turned this test red, as did
|
||||
# a delimiter in an inert top-level YAML comment. Neither is unsafe, and a red here blocks every
|
||||
# merge through the combined status, so the guard was strictly more dangerous than the thing it
|
||||
# was protecting against.
|
||||
# would make the assertion above vacuously green — the failure mode to guard against. Comparing
|
||||
# the file's TOTAL delimiter count against the count inside `with:`/`env:` values is a different
|
||||
# and wrong claim: it bans expressions everywhere else in the file too. The false red is
|
||||
# reproducible — writing `if: ${{ always() }}`, the standard and equivalent spelling of the
|
||||
# `if:` two steps below, turns this test red, as does a delimiter in an inert top-level YAML
|
||||
# comment. Neither is unsafe, and a red here blocks every merge through the combined status, so
|
||||
# such a guard is strictly more dangerous than the thing it protects against.
|
||||
#
|
||||
# ANTI-VACUITY WITHOUT A SECOND PARSER. A first attempt counted `run:` keys in the raw text and
|
||||
# compared that to the walk. Cold re-review showed the regex only recognised an indented `run:`
|
||||
# whose value starts with `|` or `>`, so legal spellings (`- run: |`, a single-line
|
||||
# `run: echo ok`) counted as zero declarations and false-redded the file, while a `run: |` line
|
||||
# sitting INSIDE a shell heredoc counted as a declaration. Hand-parsing YAML to check a YAML parse
|
||||
# ANTI-VACUITY WITHOUT A SECOND PARSER. Counting `run:` keys in the raw text and comparing that
|
||||
# to the walk needs a regex over YAML: one recognising only an indented `run:` whose value
|
||||
# starts with `|` or `>` counts legal spellings (`- run: |`, a single-line `run: echo ok`) as
|
||||
# zero declarations and false-reds the file, while a `run: |` line sitting INSIDE a shell
|
||||
# heredoc counts as a declaration. Hand-parsing YAML to check a YAML parse
|
||||
# is the wrong shape: it adds a second, worse parser whose disagreements are all false alarms, and
|
||||
# a red here blocks every merge through the combined status.
|
||||
#
|
||||
@@ -4399,9 +4390,9 @@ def test_the_verdict_workflow_has_NO_expression_delimiter_in_any_run_body():
|
||||
# classifier is ~700 lines; a walk that returned nothing, or only the short steps, is the failure
|
||||
# to catch. Both are asserted on content, which no spelling change can spoof.
|
||||
bodies = [s["run"] for _, s in _iter_workflow_steps(WORKFLOW) if s.get("run")]
|
||||
# NOT `>= 3`, and not merely non-empty either. `>= 3` had zero slack — cold review deleted the
|
||||
# optional jq-preflight step, a legitimate simplification, and this redded claiming the classifier
|
||||
# had not been examined, which was untrue. But relaxing it to `assert bodies` threw away the only
|
||||
# NOT `>= 3`, and not merely non-empty either. `>= 3` had zero slack — deleting the optional
|
||||
# jq-preflight step, a legitimate simplification, redded this claiming the classifier had not
|
||||
# been examined, which was untrue. But relaxing it to `assert bodies` threw away the only
|
||||
# check that the walk reached ALL run-bearing steps: `max(len) > 5000` proves it reached the
|
||||
# classifier and nothing about the short ones, so a helper that silently stopped yielding them
|
||||
# would let an unscanned delimiter through.
|
||||
@@ -4440,10 +4431,9 @@ def test_every_workflow_expression_names_a_REAL_context_or_function():
|
||||
context that does not exist. All of them fail the same silent way, and in an `if:` the
|
||||
consequence is the same shape as #751: the step does not run and nothing is red.
|
||||
|
||||
BE PRECISE ABOUT WHAT THIS ENFORCES, because the first version of this docstring was not and
|
||||
both reviewers caught it: it checks that THE HEAD TOKEN of each dotted path is a known context or
|
||||
function. Nothing more. That catches the historical defect — `pr number` fails on `pr` — and a
|
||||
payload naming a context that does not exist. It does NOT catch:
|
||||
BE PRECISE ABOUT WHAT THIS ENFORCES: it checks that THE HEAD TOKEN of each dotted path is a
|
||||
known context or function. Nothing more. That catches the historical defect — `pr number` fails
|
||||
on `pr` — and a payload naming a context that does not exist. It does NOT catch:
|
||||
|
||||
* syntactically invalid expressions whose tokens are all known: `${{ github.ref == }}` and
|
||||
`${{ github.event.pull_request.head.sha + }}` both pass, verified;
|
||||
@@ -4547,9 +4537,9 @@ def test_a_dropped_classify_step_FAILS_the_job_instead_of_going_green():
|
||||
f"the dropped-step guard's `if:` is {guard.get('if')!r}; it must be `always()` (bare or "
|
||||
"wrapped), or it will be skipped on exactly the runs where the classifier failed"
|
||||
)
|
||||
# INSIDE the missing-marker branch, not merely somewhere in the body. Cold review pointed out
|
||||
# that a bare `exit 1` substring is satisfied by an unreachable `if false; then exit 1; fi` while
|
||||
# the real branch says `exit 0` — the test passes and a dropped classifier goes green again. The
|
||||
# INSIDE the missing-marker branch, not merely somewhere in the body. A bare `exit 1` substring
|
||||
# is satisfied by an unreachable `if false; then exit 1; fi` while the real branch says
|
||||
# `exit 0` — the test passes and a dropped classifier goes green again. The
|
||||
# behavioural test below is the real proof; this keeps the structural one from being satisfiable
|
||||
# by dead code.
|
||||
missing_branch = re.search(r'if \[ ! -f "\$marker" \]; then(.*?)\bfi\b', guard["run"], re.S)
|
||||
@@ -4610,8 +4600,8 @@ def test_the_fence_TRUSTS_the_count_and_POSTS_when_the_timeline_terminates(tmp_p
|
||||
def _marker_steps():
|
||||
"""(classify body PREFIX through the marker write, guard body that CHECKS it) — from one parse.
|
||||
|
||||
THE PREFIX, NOT THE MATCHING LINES. An earlier version picked out the lines matching `RAN_MARKER=`
|
||||
and `: > "$RAN_MARKER"` and ran those two alone. Cold re-review showed that passes even if the
|
||||
THE PREFIX, NOT THE MATCHING LINES. Picking out the lines matching `RAN_MARKER=`
|
||||
and `: > "$RAN_MARKER"` and running those two alone passes even if the
|
||||
write is moved into a function nobody calls, or under `if false`: the extractor finds the text,
|
||||
runs it at top level, the marker appears, and the test reports the guard proven while production
|
||||
writes no marker at all. Executing the real prefix — everything from the top of the body down to
|
||||
@@ -4635,7 +4625,7 @@ def _marker_steps():
|
||||
|
||||
def test_the_dropped_step_guard_BEHAVIOURALLY_fails_without_the_marker_and_passes_with_it(tmp_path):
|
||||
"""Executes the guard, instead of reading it — the structural test above cannot prove the exit
|
||||
code, and cold review was right that a bare `exit 1` substring is satisfiable by dead code.
|
||||
code, and a bare `exit 1` substring is satisfiable by dead code.
|
||||
|
||||
The marker is created by running THE CLASSIFY STEP'S OWN two prologue lines under the same
|
||||
environment, never by rebuilding the path in Python. That is the point: it proves the two steps
|
||||
@@ -4693,7 +4683,7 @@ def test_the_dropped_step_guard_BEHAVIOURALLY_fails_without_the_marker_and_passe
|
||||
# assertion makes the intent explicit and fails loudly rather than subtly.
|
||||
body_before = "\n".join(ln for ln in prologue.splitlines() if not ln.lstrip().startswith("#"))
|
||||
# Covers all four bash spellings: `mk() {`, `mk(){`, `function mk {`, `function mk() {`. The third
|
||||
# was added when review found the first regex missed it, and the FOURTH still slipped that fix —
|
||||
# was missed by a regex over the first two spellings, and the FOURTH still slipped the regex extended to it —
|
||||
# the union form is the natural next spelling once `function mk {` is caught. Budget three rounds
|
||||
# for any string-matching predicate.
|
||||
assert not re.search(r"^\s*(function\s+)?\w+\s*(\(\s*\))?\s*\{", body_before, re.M), (
|
||||
@@ -4710,7 +4700,7 @@ def test_the_dropped_step_guard_BEHAVIOURALLY_fails_without_the_marker_and_passe
|
||||
|
||||
@pytest.mark.parametrize("shape", ["null", "array"], ids=["statuses-null", "statuses-empty-array"])
|
||||
def test_a_head_with_NO_statuses_YET_is_readable_and_still_gets_its_exemption(tmp_path, shape):
|
||||
"""The twin of the timeline terminator, found by cold review of the fix for that one (#751).
|
||||
"""The twin of the timeline terminator (#751).
|
||||
|
||||
`GET /commits/{sha}/status` returns `{"state":"pending","total_count":0,"statuses":null}` for a
|
||||
head that has no statuses yet — measured on PR #739's head 5fa672e2. `read_existing_verdict`
|
||||
@@ -4739,7 +4729,7 @@ def test_the_GOVERNANCE_docs_are_protected_and_get_no_docs_only_exemption(tmp_pa
|
||||
the merge-consent convention and the H10 rule itself. `DOCS_ONLY` (`^(docs/|[^/]*\\.md$)`) matched
|
||||
them, so a PR editing the document that specifies what `.claude/` enforces was auto-exemptible
|
||||
while `.claude/` itself was protected: the same self-exemption the workflow header rules out, one
|
||||
directory over. Found by cold review of #751 by driving this exact case through the real body.
|
||||
directory over.
|
||||
|
||||
Latent until #751, because no exemption `success` was writable at all while the classify step was
|
||||
dropped — restoring the exemptions is what makes it reachable, which is why it is fixed there.
|
||||
@@ -4765,8 +4755,8 @@ def test_a_terminator_on_PAGE_ONE_does_not_certify_a_zero_retarget_count(tmp_pat
|
||||
|
||||
THE INVARIANT, not a figure that rots: a real PR's timeline always carries at least one event on
|
||||
page 1, because the PR is created by a push and that is itself an event. Spot-checked non-empty
|
||||
across #752/#753/#749/#739/#717; the counts are not recorded, because an earlier version of this
|
||||
docstring cited five and three were stale within days. So a terminator on page 1 is anomalous, not
|
||||
across #752/#753/#749/#739/#717; the counts are not recorded, because they rot — of five once
|
||||
cited here, three were stale within days. So a terminator on page 1 is anomalous, not
|
||||
empty. Trusting a zero count from it would mean
|
||||
certifying that no retarget happened on the strength of a response we cannot explain, which is the
|
||||
one thing the fence exists to refuse. Withholding the exemption is the safe direction: the PR asks
|
||||
@@ -4795,7 +4785,7 @@ def test_a_SECOND_PAGE_of_statuses_refuses_to_conclude_that_no_verdict_exists(tm
|
||||
* "refuse when the page came back full at the requested limit of 100" — this instance caps `limit`
|
||||
at `MAX_RESPONSE_ITEMS`, measured at 50 (`/issues?limit=100` returns 50), so a response can never
|
||||
hold 100 rows and the comparison was DEAD CODE. The repo already documented that cap in three
|
||||
places; the guard was written against 100 anyway and a cold review caught it.
|
||||
places; the guard was written against 100 anyway.
|
||||
|
||||
So the job asks the server, and only when the row is absent from page 1. Any rows on page 2 mean
|
||||
the list is longer than one page and the verdict may be beyond it.
|
||||
@@ -4823,7 +4813,7 @@ def test_a_SINGLE_page_of_statuses_reads_normally(tmp_path):
|
||||
|
||||
def test_a_STRING_total_count_is_not_accepted_as_numeric_zero(tmp_path):
|
||||
"""`jq -r` renders the JSON number 0 and the JSON string "0" identically, so a text compare
|
||||
accepts a schema-corrupted `"total_count": "0"` as "no statuses" (cold re-review reproduced it).
|
||||
accepts a schema-corrupted `"total_count": "0"` as "no statuses" (reproduced, not assumed).
|
||||
|
||||
The live schema uses an integer, so this is not a live failure — it is the difference between a
|
||||
guard that holds because the input happens to be well-formed and one that holds because it checks.
|
||||
@@ -4845,7 +4835,7 @@ def test_a_STRING_total_count_is_not_accepted_as_numeric_zero(tmp_path):
|
||||
ids=["garbage", "transport-error"],
|
||||
)
|
||||
def test_an_UNREADABLE_page_2_refuses_to_conclude_that_no_verdict_exists(tmp_path, mode, why):
|
||||
"""The two refuse branches of the completeness probe, which cold review found untested.
|
||||
"""The two refuse branches of the completeness probe.
|
||||
|
||||
Worth a test rather than trusting the shape: this file's history is two consecutive guards that
|
||||
were UNREACHABLE — the timeline walk's type gate that never saw a real terminator, and a
|
||||
@@ -4943,7 +4933,7 @@ def test_a_partial_mark_is_SAFE_because_the_newest_rows_are_on_page_1(tmp_path):
|
||||
|
||||
The history holds a PRE-EXISTING base-mismatched verdict at id 7055 — older than the newest row,
|
||||
so nothing it does raced this write. Under DESC the salvaged mark is 7059 and that row is below
|
||||
it: the exemption correctly STANDS. Under ASC (`sort=highestindex`, withdrawn in round 4) page 1
|
||||
it: the exemption correctly STANDS. Under ASC (`sort=highestindex`, withdrawn) page 1
|
||||
would be 7000..7049, the mark 7049, and that same untouched row would test as NEWER than the mark
|
||||
— a sticky repair on a head nothing raced, which is exactly #761.
|
||||
|
||||
@@ -5380,11 +5370,11 @@ def test_an_UNREADABLE_history_page_2_also_repairs_rather_than_leaving_green(tmp
|
||||
# `test_MUTATION_restoring_the_default_operator_on_the_description_...` DO restore `origin/main`
|
||||
# text verbatim; `test_MUTATION_restoring_the_SUCCESS_only_no_mark_downgrade_...` and
|
||||
# `test_MUTATION_reading_a_malformed_FIELD_as_absent_...` restore text an EARLIER COMMIT ON THIS
|
||||
# BRANCH shipped, which is where two cold reviews found survivors.
|
||||
# BRANCH shipped, which is where survivors were found.
|
||||
# * `test_MUTATION_a_GENERIC_pending_...` restores the shape #742 attempted and WITHDREW, not
|
||||
# `main` — which had no downgrade at all.
|
||||
# * `test_MUTATION_restoring_the_SUCCESS_only_no_mark_downgrade_...` restores the predicate this
|
||||
# branch itself shipped one commit earlier, which is where a cold review found it surviving the
|
||||
# branch itself shipped one commit earlier, which is where it survived the
|
||||
# whole suite: the nearest existing proof mutated the DESCRIPTION the downgrade writes, not its
|
||||
# SCOPE, and its fixture ran a succeeding enumeration, so `state=success` there and the
|
||||
# `success`-only predecessor fired identically.
|
||||
@@ -5598,8 +5588,8 @@ def _sentinel_row(row_id=4000):
|
||||
A fixture that puts the sentinel on the COMBINED endpoint without putting it in the per-POST
|
||||
history describes a head that cannot exist: `/statuses/{sha}` returns one row per POST, so the
|
||||
sentinel the combined endpoint is showing must be in there. Seeding it is not decoration — the
|
||||
reconciliation now requires exactly this row as its witness, and the first version of these tests
|
||||
passed against the impossible shape.
|
||||
reconciliation now requires exactly this row as its witness, and without it these tests pass
|
||||
against the impossible shape.
|
||||
"""
|
||||
return {
|
||||
"id": row_id,
|
||||
@@ -5685,9 +5675,8 @@ def test_MUTATION_reconciling_on_an_UNREADABLE_history_re_exempts_a_head(tmp_pat
|
||||
is satisfied. `reconcile-page2-error` serves page 1 — carrying the seeded sentinel, so the
|
||||
witness is 1 — and fails page 2 of the reconciliation walk, so `ph_ok` is `no`. Dropping the
|
||||
completeness operand therefore clears the sentinel over a list the job knows it did not finish
|
||||
reading, which is where a buried verdict would be. An earlier version used a fixture with BOTH
|
||||
operands false and mutated the whole condition to `if false`, which disarms two guards at once
|
||||
and isolates neither.
|
||||
reading, which is where a buried verdict would be. A fixture with BOTH operands false, mutating
|
||||
the whole condition to `if false`, disarms two guards at once and isolates neither.
|
||||
|
||||
That the cleared sentinel can be sitting on a real verdict is shown by
|
||||
`test_the_reconciliation_UPGRADES_to_the_repair_sentinel_when_a_verdict_is_BURIED`, which is the
|
||||
@@ -5739,8 +5728,8 @@ def test_an_IMPOSSIBLE_EMPTY_history_does_NOT_reconcile_the_sentinel_away(tmp_pa
|
||||
needs that answer. Clearing on it let the run classify normally and exempt a head whose sentinel
|
||||
may have been sitting on a rejection.
|
||||
|
||||
The first version of the RECONCILED_AWAY test above used exactly this fixture, so the clear path
|
||||
was only ever asserted against the impossible shape.
|
||||
The RECONCILED_AWAY test above must not use this fixture: the clear path would then be asserted
|
||||
only against the impossible shape.
|
||||
"""
|
||||
posted, r = _run_classify(
|
||||
tmp_path,
|
||||
@@ -5948,7 +5937,7 @@ def test_MUTATION_declining_to_replace_an_unreadable_combined_read_leaves_the_fo
|
||||
|
||||
|
||||
def test_MUTATION_a_page_2_refusal_that_only_EXITS_leaves_the_head_unmarked(tmp_path):
|
||||
"""The page-2 completeness probe refuses AND replaces (ersatztv#849 round 2).
|
||||
"""The page-2 completeness probe refuses AND replaces (ersatztv#849).
|
||||
|
||||
It was excluded from the replacement on the reasoning that the probe fires when NO row for this
|
||||
context was on page 1, so there is no green of any provenance to leave standing. That is
|
||||
@@ -5981,7 +5970,7 @@ def test_MUTATION_a_page_2_refusal_that_only_EXITS_leaves_the_head_unmarked(tmp_
|
||||
|
||||
|
||||
def test_MUTATION_an_untrusted_fence_that_only_ABSTAINS_leaves_the_declined_row_current(tmp_path):
|
||||
"""The untrusted-fence branch writes rather than abstains (ersatztv#849 round 2).
|
||||
"""The untrusted-fence branch writes rather than abstains (ersatztv#849).
|
||||
|
||||
It is reached only AFTER the classification declined to inherit whatever `h10` the head carries —
|
||||
that is why it is re-deriving — so posting nothing leaves the declined row current, and no
|
||||
@@ -6098,7 +6087,7 @@ def test_MUTATION_removing_the_repair_FLOOR_downgrades_the_repair_sentinel(tmp_p
|
||||
|
||||
|
||||
def test_an_OBSERVED_retarget_MARKS_a_row_this_run_declined(tmp_path):
|
||||
"""Abstaining is a handoff only when there is nothing to hand off (ersatztv#849 round 3).
|
||||
"""Abstaining is a handoff only when there is nothing to hand off (ersatztv#849).
|
||||
|
||||
The arm is right not to post its CLASSIFICATION — computed against a base the PR may no longer
|
||||
target — but when the head already carries a row this run DECLINED to inherit, posting nothing
|
||||
@@ -6155,13 +6144,13 @@ def test_MUTATION_not_marking_the_declined_row_leaves_it_authoritative(tmp_path)
|
||||
def test_a_MALFORMED_element_BESIDE_a_readable_row_still_refuses(tmp_path):
|
||||
"""An element whose `.context` cannot be read cannot be shown to be a DIFFERENT context.
|
||||
|
||||
Round 8 dropped malformed neighbours once a target row was found, reasoning that a bad row beside
|
||||
a good one is noise. It is not, and the rule is now unconditional: the unreadable element may be a
|
||||
Dropping malformed neighbours once a target row was found reasons that a bad row beside a good
|
||||
one is noise. It is not, and the rule is unconditional: the unreadable element may be a
|
||||
mangled rendering of this head's own rejection, and the one-row-per-context invariant that would
|
||||
rule that out is exactly what a schema-corrupt response has already broken.
|
||||
|
||||
This fixture is the one round 8 shipped as a POSITIVE control — a scalar beside an off-list
|
||||
`success` — and it was the failing case: the branch re-derived the off-list row and POSTed an
|
||||
This fixture reads as a POSITIVE control — a scalar beside an off-list `success` — and is the
|
||||
failing case: the branch re-derived the off-list row and POSTed an
|
||||
exemption where `origin/main` errored on the scalar and posted nothing. The assertion is
|
||||
therefore inverted, not adjusted.
|
||||
|
||||
@@ -6176,7 +6165,7 @@ def test_a_MALFORMED_element_BESIDE_a_readable_row_still_refuses(tmp_path):
|
||||
|
||||
|
||||
def test_MUTATION_ignoring_a_malformed_element_when_a_row_was_found_greens_the_head(tmp_path):
|
||||
"""Round 8's own scoping, restored: refuse only when NO readable row was found."""
|
||||
"""The narrower scoping, restored as a mutant: refuse only when NO readable row was found."""
|
||||
posted, rm = _run_classify(
|
||||
tmp_path / "mutant",
|
||||
_emitting("docs/a.md"),
|
||||
@@ -6443,7 +6432,7 @@ def test_a_STRING_id_on_the_current_row_takes_the_SCHEMA_FAULT_route(tmp_path):
|
||||
def test_EVERY_consumed_field_of_the_existing_row_takes_the_SCHEMA_FAULT_route(tmp_path, field):
|
||||
"""Swept by FIELD, not written once for the field that produced it.
|
||||
|
||||
The `.creator` case is the one a review measured, and a route written for it alone leaves the
|
||||
The `.creator` case is the one that was measured, and a route written for it alone leaves the
|
||||
other three to the reader's assumption — which is the per-field gap this repo keeps re-learning.
|
||||
Each of the four is read by a decision: `.creator` and `.description` by the provenance test,
|
||||
`.status` by both short-circuits, `.id` by the reconciliation witness.
|
||||
@@ -6784,7 +6773,7 @@ def test_a_reviewers_verdict_ARRIVING_MID_RUN_is_not_buried_by_an_abstaining_arm
|
||||
|
||||
|
||||
def test_MUTATION_deciding_the_arms_refusals_on_the_FIRST_read_buries_it(tmp_path):
|
||||
"""Restores the round-4 form: the allow-list veto reading only the opening snapshot."""
|
||||
"""Restores the narrower form: the allow-list veto reading only the opening snapshot."""
|
||||
posted, rm = _run_classify(
|
||||
tmp_path / "mutant",
|
||||
_emitting("docs/a.md"),
|
||||
@@ -6899,9 +6888,9 @@ def test_a_generic_PENDING_with_no_mark_also_becomes_the_sentinel(tmp_path):
|
||||
def test_a_MALFORMED_creator_FIELD_on_the_existing_row_is_unknown_state_not_an_absent_one(tmp_path):
|
||||
"""A wrong TYPE is not an absent value, and reading it as one is a licence to re-derive.
|
||||
|
||||
Round 3 type-tested the four consumed fields and resolved a failure to `""`. For `.creator` that
|
||||
means "no creator", i.e. unattributable, i.e. re-derive — so a head carrying a human `failure`
|
||||
with a corrupt creator was greened. `origin/main` died on `.creator.login` BEFORE writing
|
||||
Type-testing the four consumed fields and resolving a failure to `""` means, for `.creator`,
|
||||
"no creator", i.e. unattributable, i.e. re-derive — so a head carrying a human `failure`
|
||||
with a corrupt creator is greened. `origin/main` died on `.creator.login` BEFORE writing
|
||||
anything, which is fail-closed, so this was a direction regression rather than a residual.
|
||||
|
||||
The rationale that produced it came from #763, whose site is the POST-WRITE filter: there, dying
|
||||
@@ -7126,7 +7115,7 @@ def test_MUTATION_ignoring_the_write_result_reports_a_clean_abstention(tmp_path)
|
||||
|
||||
|
||||
def test_MUTATION_reading_a_malformed_FIELD_as_absent_greens_a_rejection(tmp_path):
|
||||
"""The round-3 form restored: type-test, then fall back to the empty string."""
|
||||
"""The weaker form restored: type-test, then fall back to the empty string."""
|
||||
posted, rm = _run_classify(
|
||||
tmp_path / "mutant",
|
||||
_emitting("docs/a.md"),
|
||||
@@ -7145,7 +7134,7 @@ def test_MUTATION_reading_a_malformed_FIELD_as_absent_greens_a_rejection(tmp_pat
|
||||
def test_MUTATION_restoring_the_SUCCESS_only_no_mark_downgrade_leaves_a_re_derivable_pending(tmp_path):
|
||||
"""The exact predecessor from this branch's own previous commit, restored.
|
||||
|
||||
This is the mutation the round-2 suite did not have: the nearest proof mutated the DESCRIPTION
|
||||
This is the mutation the suite was missing: the nearest proof mutated the DESCRIPTION
|
||||
the downgrade writes, not its SCOPE, and its fixture ran a succeeding enumeration — so
|
||||
`state=success` there and the `success`-only predicate fired identically. Nothing reached the
|
||||
downgrade with `state=pending`, and the predecessor survived the whole suite.
|
||||
|
||||
@@ -288,7 +288,7 @@ def test_MUTATION_REORDERING_the_unset_after_the_nested_git_call_lets_drift_thro
|
||||
This asserts on the NESTED DIFF's verdict, not on pre-push's exit code. Relocating the `unset`
|
||||
to the end of the file also makes it the script's last statement, and a bare `unset` succeeds —
|
||||
so `returncode == 0` would hold here even if the nested git call had worked perfectly. That is a
|
||||
test passing for the wrong reason, and it was written that way in this file's first draft.
|
||||
test passing for the wrong reason.
|
||||
"""
|
||||
_positive_control(tmp_path, "reorder")
|
||||
|
||||
|
||||
@@ -177,9 +177,8 @@ def test_root_commit_REFUSES(tmp_path: Path) -> None:
|
||||
def test_runs_under_the_system_bash(fixrepo: Path) -> None:
|
||||
"""macOS ships /bin/bash 3.2, where `mapfile` is absent AND yields an empty array
|
||||
instead of erroring. Assert by EXECUTING under that bash, not by grepping the source
|
||||
for "mapfile" — the first version of this test did the latter and matched the comment
|
||||
explaining why mapfile is avoided, which is a string predicate failing exactly as
|
||||
docs/defect-shapes-773.md §3.7 says they do."""
|
||||
for "mapfile" — a grep matches the comment explaining why mapfile is avoided, which is
|
||||
a string predicate failing exactly as docs/defect-shapes-773.md §3.7 says they do."""
|
||||
system_bash = Path("/bin/bash")
|
||||
if not system_bash.exists():
|
||||
pytest.skip("/bin/bash not present")
|
||||
@@ -230,10 +229,9 @@ def test_MUTATION_disarming_the_UNPROVEN_clause_reddens_the_refusal_test(
|
||||
|
||||
`docs/guard-inventory.md` is explicit that MUTATION means a mutation was executed and
|
||||
the named test was *witnessed red* — feeding the real script a rejecting input is
|
||||
BEHAVIOUR-ONLY, and the column "is not a grading curve". An earlier version of this
|
||||
test deleted the clause and then asserted the MUTANT misbehaved, which left this test
|
||||
green and proved nothing about whether the clause is load-bearing. Cold review caught
|
||||
that, and it was right.
|
||||
BEHAVIOUR-ONLY, and the column "is not a grading curve". Deleting the clause and then
|
||||
asserting the MUTANT misbehaved leaves this test green and proves nothing about
|
||||
whether the clause is load-bearing.
|
||||
|
||||
So: delete the `RC -eq 0 -> UNPROVEN` clause (the CLAUSE, not the file — #510), then
|
||||
re-run the UNCHANGED `test_unrelated_test_is_UNPROVEN` against the mutant in a nested
|
||||
@@ -318,8 +316,8 @@ def test_SIGTERM_mid_run_never_reports_PROVEN(tmp_path: Path) -> None:
|
||||
# Assert the observable THIS fix introduced, not merely "non-zero and no PROVEN":
|
||||
# the pre-fix script also satisfied those two, by accident — its `trap cleanup EXIT INT
|
||||
# TERM` fired, deleted $TMP, execution continued, and a later step died 5. Two different
|
||||
# bugs landing on the same observable is not a witnessed fix. Cold review measured that
|
||||
# pair failing to separate old from new; rc==5 AND the handler's own message do separate
|
||||
# bugs landing on the same observable is not a witnessed fix. That pair was measured
|
||||
# failing to separate old from new; rc==5 AND the handler's own message do separate
|
||||
# them.
|
||||
assert proc.returncode == 5, f"a signalled run must exit 5 from on_signal, got {proc.returncode}\n{out}\n{err}"
|
||||
assert "interrupted by signal" in err, (
|
||||
@@ -332,10 +330,9 @@ def test_SIGTERM_mid_run_never_reports_PROVEN(tmp_path: Path) -> None:
|
||||
def test_a_harness_failure_is_NOT_reported_as_PROVEN(tmp_path: Path) -> None:
|
||||
"""The marker-absence branch, reached the way the old false green was reached.
|
||||
|
||||
An earlier version stubbed `python3` to exit 127, which does NOT reach the marker logic:
|
||||
the marker IS written (127) and the script exits via the control-failure branch. Cold
|
||||
review measured that, and it is why the round-2 commit came back UNPROVEN from this
|
||||
tool's own gate — the fix was executed by no test.
|
||||
Stubbing `python3` to exit 127 does NOT reach the marker logic: the marker IS written
|
||||
(127) and the script exits via the control-failure branch. Measured: a commit taking
|
||||
that route came back UNPROVEN from this tool's own gate, its fix executed by no test.
|
||||
|
||||
The real shape is `( cd X && pytest ); rc=$?` returning 1 because `cd` FAILED and pytest
|
||||
never ran; pre-fix that was accepted as red and produced PROVEN. Reproduced by shimming
|
||||
|
||||
@@ -46,13 +46,12 @@ CLASSES = {"PINNED", "CAS", "UNSAFE-KNOWN", "N/A"}
|
||||
# NO CONTENT FILTER. The population is every file in the scoped directories, and a file that reads
|
||||
# no remote state earns an `N/A` row rather than silently staying out.
|
||||
#
|
||||
# The first version filtered on a token list (`curl`, `wget`, `urllib`, ...) and called that a
|
||||
# SCOPE choice rather than a population filter. Cold review rejected the distinction and was right:
|
||||
# the list omitted `git fetch`, which is this repo's most common remote read, so
|
||||
# `.claude/hooks/prepush-rebase-check.sh` — which fetches `origin/main` and derives a PUSH DECISION
|
||||
# from it — was structurally invisible to a guard whose stated claim is "every executable that
|
||||
# reads live remote state". Three more (`prepush-clean-worktree-check.sh`, `ci-detect-docs-only.sh`,
|
||||
# `refresh-shared-checkout.sh`) were missing for the same reason.
|
||||
# A token-list filter (`curl`, `wget`, `urllib`, ...) reads as a SCOPE choice and is a population
|
||||
# filter; the distinction does not hold. The list omitted `git fetch`, which is this repo's most
|
||||
# common remote read, so `.claude/hooks/prepush-rebase-check.sh` — which fetches `origin/main` and
|
||||
# derives a PUSH DECISION from it — was structurally invisible to a guard whose stated claim is
|
||||
# "every executable that reads live remote state". Three more (`prepush-clean-worktree-check.sh`,
|
||||
# `ci-detect-docs-only.sh`, `refresh-shared-checkout.sh`) were missing for the same reason.
|
||||
#
|
||||
# That is precisely the defect `testing.guard-derives-population-from-source` describes: a filter
|
||||
# cannot see the member that is missing, because the absent member is not a row the predicate
|
||||
@@ -172,9 +171,9 @@ def _inventory_section(text: str) -> str:
|
||||
end = text.index("## Limits", start)
|
||||
except ValueError:
|
||||
# Reachable when '## Limits' exists but PRECEDES '## The inventory' — the presence check
|
||||
# above passes and the bounded search does not. An earlier version put an `end <= start`
|
||||
# guard here instead, which `str.index(…, start)` makes unreachable by construction: it
|
||||
# either returns an index >= start or raises. A guard that cannot execute proves nothing.
|
||||
# above passes and the bounded search does not. An `end <= start` guard here instead is
|
||||
# unreachable by construction: `str.index(…, start)` either returns an index >= start or
|
||||
# raises, and a guard that cannot execute proves nothing.
|
||||
raise AssertionError(
|
||||
f"{INVENTORY.name}: '## Limits' precedes '## The inventory', so the parsed window "
|
||||
"would be empty and every completeness assertion would pass vacuously."
|
||||
@@ -239,8 +238,8 @@ def test_MUTATION_PROOF_a_dropped_row_and_a_phantom_row_are_both_detected():
|
||||
|
||||
Both directions are mutated, because they are different defects: a dropped row is an
|
||||
unclassified script, a phantom row is a table claiming coverage it has lost. This ran for real
|
||||
on the day it was written — `dependency-scan.yml` was genuinely absent from the first draft of
|
||||
the inventory and this comparison is what found it.
|
||||
on the day it was written — `dependency-scan.yml` was genuinely absent from the inventory and
|
||||
this comparison is what found it.
|
||||
"""
|
||||
text = INVENTORY.read_text(encoding="utf-8")
|
||||
population = derived_population()
|
||||
@@ -276,8 +275,8 @@ def test_every_unsafe_row_states_why_the_residual_is_accepted():
|
||||
text = _inventory_section(INVENTORY.read_text(encoding="utf-8"))
|
||||
thin = []
|
||||
for line in text.splitlines():
|
||||
# TABLE ROWS ONLY. The first version matched any line containing the token, so the prose in
|
||||
# "Columns" that DEFINES `UNSAFE-KNOWN` was parsed as a row and the split blew up. A guard
|
||||
# TABLE ROWS ONLY. Matching any line containing the token parses the prose in
|
||||
# "Columns" that DEFINES `UNSAFE-KNOWN` as a row, and the split blows up. A guard
|
||||
# that reads its own documentation as data is the failure this whole change is about.
|
||||
if not line.startswith("|") or "`UNSAFE-KNOWN`" not in line:
|
||||
continue
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""The H10 verdict vocabulary is declared ONCE and both sides derive from it (ersatztv#788).
|
||||
|
||||
`scripts/post-review-verdict.sh` (WRITE: word -> commit-status state) and
|
||||
`scripts/check-review-verdict.sh` (READ: comment -> classification for the merge-consent hook) used
|
||||
to carry two hand-written copies of the word list. A word the write side treats as positive but the
|
||||
`scripts/check-review-verdict.sh` (READ: comment -> classification for the merge-consent hook) must
|
||||
not carry two hand-written copies of the word list. A word the write side treats as positive but the
|
||||
read side does not sends `review-verdict/h10` green while the hook still denies.
|
||||
|
||||
WHAT THIS FILE DELIBERATELY DOES NOT DO. It does not compare the two scripts' sources, and nothing
|
||||
here scrapes a vocabulary out of shell text. ersatztv#774 shipped exactly that and withdrew it after
|
||||
six cold-review rounds, each finding another shell construction that escaped the extractor or
|
||||
six rounds, each finding another shell construction that escaped the extractor or
|
||||
reddened it on a correct tree; a regex over shell source is not a shell parser and the sequence
|
||||
converged on nothing. See `docs/decisions/records/testing/guard-derives-population-from-source.md`.
|
||||
|
||||
@@ -412,7 +412,7 @@ def test_a_TRUNCATED_library_is_refused_rather_than_half_used(sandbox):
|
||||
#
|
||||
# `etv_verdict__is_array` removes the `set -u` abort, so with it in place validation returns 1
|
||||
# cleanly and the ordinary `if ! validate; then exit 2; fi` fires — which means every other refusal
|
||||
# test in this file stays green even with the sentinel deleted. Cold review measured exactly that:
|
||||
# test in this file stays green even with the sentinel deleted. MEASURED:
|
||||
# all three sentinel guards removed, 150 tests still passing.
|
||||
#
|
||||
# So the sentinel is proved by DISARMING THE OTHER GUARD FIRST. With the array assertion gone the
|
||||
@@ -426,7 +426,7 @@ def test_a_TRUNCATED_library_is_refused_rather_than_half_used(sandbox):
|
||||
# side's two are defence-in-depth: `post-review-verdict.sh` runs under `set -e`, so the abort that
|
||||
# defeats the read side kills the writer outright (measured: rc=1, nothing posted). They are kept
|
||||
# because the writer should not depend on `set -e` staying, but their current proof is a COUNT, not
|
||||
# a behaviour, and saying otherwise would be the round-2 defect one level down.
|
||||
# a behaviour, and saying otherwise would be that same overclaim one level down.
|
||||
ARRAY_ASSERTION = (
|
||||
" for name in ETV_VERDICT_POSITIVE_WORDS ETV_VERDICT_NEGATIVE_WORDS; do\n"
|
||||
' if ! etv_verdict__is_array "$name"; then\n'
|
||||
|
||||
@@ -81,7 +81,7 @@ ROLE_KIND = {GUARD: "GUARD", REPORT_ONLY: "REPORT-ONLY"}
|
||||
# equality reports a guard job with no entry — measured: injecting a pipe into one Blocks cell
|
||||
# reddens `test_the_inventory_covers_exactly_the_guard_JOBS_that_exist` naming that job.
|
||||
#
|
||||
# THE CASE IT DOES NOT COVER, because an earlier version of this note claimed "every malformed row":
|
||||
# THE CASE IT DOES NOT COVER, because the claim above is NOT "every malformed row":
|
||||
# a malformed DUPLICATE is invisible. Its job is already satisfied by the well-formed row, so set
|
||||
# equality holds, `duplicate_row_faults` never sees the second occurrence, and the Kind check has
|
||||
# nothing to compare. A contradictory row that also happens to be malformed therefore passes. No
|
||||
@@ -184,9 +184,9 @@ def inventory_row_list() -> list[tuple[str, str]]:
|
||||
def duplicate_row_faults(rows: list[tuple[str, str]]) -> list[str]:
|
||||
"""THE DETECTOR, named so its live test and its mutation proof share one implementation.
|
||||
|
||||
An earlier draft inlined this loop in the live test and had the proof rebuild the comparison
|
||||
with its own `len(set(...))`. That proof stayed green with the live check disarmed, because it
|
||||
was exercising arithmetic it had written itself rather than the code that ships — the shape
|
||||
Inlining this loop in the live test and having the proof rebuild the comparison with its own
|
||||
`len(set(...))` leaves the proof green with the live check disarmed, because it exercises
|
||||
arithmetic it wrote itself rather than the code that ships — the shape
|
||||
`testing.guard-ships-with-mutation-proof` calls a behavioural test dressed as a proof.
|
||||
"""
|
||||
seen: dict[str, str] = {}
|
||||
@@ -217,8 +217,8 @@ def coverage_faults(declared: set[str], listed: set[str]) -> tuple[set[str], set
|
||||
|
||||
Named for the same reason `duplicate_row_faults` is: three `test_MUTATION_*` below used to
|
||||
rebuild `declared - listed` inline, and a proof that reimplements the comparison stays green
|
||||
when the shipped one is disarmed — a behavioural test wearing a MUTATION name. Cold review
|
||||
demonstrated exactly that by flattening the live assertion and watching the file stay green.
|
||||
when the shipped one is disarmed — a behavioural test wearing a MUTATION name. Measured:
|
||||
flattening the live assertion left the file green.
|
||||
"""
|
||||
return declared - listed, listed - declared
|
||||
|
||||
@@ -532,10 +532,11 @@ def test_every_tracked_job_APPEARS_in_the_parse(tmp_path):
|
||||
def test_a_commented_out_marker_does_NOT_count(tmp_path):
|
||||
"""A commented-out marker must read as ABSENT — asserted through `declared_roles` ITSELF.
|
||||
|
||||
An earlier draft called `yaml.safe_load` on a literal and asserted comments are not values. That
|
||||
is a fact about PyYAML, which this suite does not own and no change here could break: replacing
|
||||
`declared_roles` with a grep-based reader left that version GREEN. It now drives the real reader
|
||||
over a real file through the `paths` seam, so a reader that ever became a text scan fails here.
|
||||
Calling `yaml.safe_load` on a literal and asserting comments are not values tests a fact about
|
||||
PyYAML, which this suite does not own and no change here could break: replacing
|
||||
`declared_roles` with a grep-based reader leaves that shape GREEN. So this drives the real
|
||||
reader over a real file through the `paths` seam, and a reader that ever became a text scan
|
||||
fails here.
|
||||
"""
|
||||
wf = tmp_path / "commented.yml"
|
||||
wf.write_text("jobs:\n x:\n env:\n # CI_JOB_ROLE: guard\n OTHER: 1\n steps: []\n")
|
||||
|
||||
@@ -61,7 +61,7 @@ def _canonical_action(uses: str) -> str:
|
||||
It over-matches in one direction on purpose: a genuinely different action whose path happens to
|
||||
END in `actions/checkout` would be treated as a checkout and required to carry the flag. That
|
||||
costs a spurious requirement on an action nobody has; the opposite error costs a live credential.
|
||||
Spellings normalised here, each of which escaped an earlier version of this function: an absolute
|
||||
Spellings normalised here, each of which escaped a simpler normalisation: an absolute
|
||||
URL with or without a scheme, a host with no dot, a doubled slash, `@`-userinfo before the host,
|
||||
a `.git` suffix, and any letter case.
|
||||
"""
|
||||
|
||||
@@ -54,10 +54,10 @@ def _env() -> dict:
|
||||
|
||||
Two things it must get right.
|
||||
|
||||
`CLAUDE_PROJECT_DIR` is NOT pinned, and that is deliberate. It used to be, because the hooks
|
||||
`CLAUDE_PROJECT_DIR` is NOT pinned, and that is deliberate. A pin made sense while the hooks
|
||||
resolved `scripts/hook-fire-log.sh` from it and a mutated copy in `tmp_path` would otherwise find
|
||||
no sink and lose its instrumentation — a second difference from its subject. Since ersatztv#891
|
||||
the hooks self-locate THAT path, so the pin steered nothing while still reading as a control. It
|
||||
the hooks self-locate THAT path, so a pin steers nothing while still reading as a control. It
|
||||
is scoped to the sink deliberately: `pretooluse-bom-guard.sh` still reads `$CLAUDE_PROJECT_DIR`
|
||||
to find the tree a commit acts on, which the rule preserves on purpose — neither hook this file
|
||||
drives is that one. `_mutate` now gives each mutant a repo-shaped root instead,
|
||||
@@ -206,8 +206,8 @@ def test_driving_a_hook_LANDS_its_records_in_the_ISOLATED_dir(tmp_path):
|
||||
"""The invariant, asserted at the EFFECT rather than at the helper that is supposed to produce it.
|
||||
|
||||
`test_the_subprocess_env_CARRIES_...` above checks `_env()`'s return value, and that is not the
|
||||
same claim: `_env()` can be perfectly correct while a call site passes something else. Cold
|
||||
review demonstrated exactly that — restore the module-level snapshot and change one `env=_env()`
|
||||
same claim: `_env()` can be perfectly correct while a call site passes something else. That is
|
||||
DEMONSTRATED — restore the module-level snapshot and change one `env=_env()`
|
||||
back to `env=_ENV`, and every test in this file passes while 54 records go somewhere it did not
|
||||
choose. Before ersatztv#809's pre-collection isolation that somewhere was the REAL log; it is now
|
||||
the session directory, so the guard still catches the mistake but the consequence is contained.
|
||||
@@ -218,8 +218,8 @@ def test_driving_a_hook_LANDS_its_records_in_the_ISOLATED_dir(tmp_path):
|
||||
So this drives a real hook through the real `_run()` and asserts the records landed where the
|
||||
fixture put them.
|
||||
|
||||
ITS SCOPE, stated because the first version of this docstring claimed more than it delivers: it
|
||||
guards THE LAUNCH PATH IT DRIVES, not the file. Cold review demonstrated the gap — add a second
|
||||
ITS SCOPE, stated precisely because it is narrower than it looks: it
|
||||
guards THE LAUNCH PATH IT DRIVES, not the file. The gap is DEMONSTRATED — add a second
|
||||
launcher alongside `_run()` that passes a stale snapshot and point the mutation tests at it, and
|
||||
this test stays green while 18 records go to the session dir rather than the fixture's, because
|
||||
the hooks IT drives still log correctly. (Before ersatztv#809 that somewhere was the REAL log.)
|
||||
@@ -387,7 +387,7 @@ def _mutate(src: Path, tmp_path: Path, old: str, new: str, why: str) -> Path:
|
||||
`<root>/.claude/hooks/<name>.sh` beside a copy of the real sink at `<root>/scripts/` makes the
|
||||
self-location land, so the only difference is the clause.
|
||||
|
||||
This used to be arranged by pinning `$CLAUDE_PROJECT_DIR` at the real repository, which worked
|
||||
Arranging this by pinning `$CLAUDE_PROJECT_DIR` at the real repository worked
|
||||
only while the hooks read that variable. They no longer do, and an env var that no longer steers
|
||||
anything is worse than no arrangement at all: it reads as a control that is really a no-op.
|
||||
"""
|
||||
|
||||
@@ -143,10 +143,10 @@ describe('scanOptionalSchemaMembers — unreadable shapes FAIL rather than silen
|
||||
expect(() => scanOptionalSchemaMembers(wrap(' "A": { ["computed"]?: string };'))).toThrow(/computed key/);
|
||||
});
|
||||
|
||||
// Every shape below was MEASURED hiding members by cold review, against the real scanner, after an
|
||||
// earlier fix threw on intersections alone and claimed in a comment that `allOf` was "the one
|
||||
// non-object shape that can hide members". `generateTypes` emits a union for `oneOf`/`anyOf` and
|
||||
// for a plain nullable object (`type: ["object","null"]`), so none of these is hypothetical.
|
||||
// Every shape below was MEASURED hiding members against the real scanner, so a guard that throws
|
||||
// on intersections alone is not enough — `allOf` is not "the one non-object shape that can hide
|
||||
// members". `generateTypes` emits a union for `oneOf`/`anyOf` and for a plain nullable object
|
||||
// (`type: ["object","null"]`), so none of these is hypothetical.
|
||||
it.each([
|
||||
['intersection (allOf)', ' "A": ComponentA & ComponentB;'],
|
||||
['union of refs (oneOf)', ' "A": ComponentA | ComponentB;'],
|
||||
|
||||
@@ -27,7 +27,7 @@ import * as ts from 'typescript';
|
||||
* block says `Complete<T>` a dozen times), in string literals, and in `import type { Complete }`.
|
||||
* A `TypeReferenceNode` is a structurally different AST node from all three, so they are excluded
|
||||
* by NODE KIND rather than by a preceding-character heuristic — the same argument
|
||||
* `pageSizeScan.ts` makes, and this repo has a withdrawn guard (six review rounds, then deleted)
|
||||
* `pageSizeScan.ts` makes, and this repo has a withdrawn guard (six successive fixes, then deleted)
|
||||
* recording what happens when a source-shaped predicate is built out of string matching instead.
|
||||
*
|
||||
* ## The alias indirection, and why it is resolved rather than assumed
|
||||
@@ -83,7 +83,7 @@ export interface SchemaAlias {
|
||||
function scriptKindFor(fileName: string): ts.ScriptKind {
|
||||
// `.mts`/`.cts` parse as plain TS; only `.tsx` needs the JSX grammar. Same rule as
|
||||
// `pageSizeScan.ts` — stated here rather than shared, because a helper on both sides of a
|
||||
// comparison cancels out of it (the #819 round-5 finding recorded in the pageSize guard).
|
||||
// comparison cancels out of it (the #819 finding recorded in the pageSize guard).
|
||||
return fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ export function scanOptionalSchemaMembers(dtsText: string): Map<string, string[]
|
||||
|
||||
function visit(node: ts.Node): void {
|
||||
// Constrained to `interface components { schemas: … }` at the TOP LEVEL of the file, not "any
|
||||
// property named `schemas` anywhere". Cold review demonstrated the unconstrained form: a
|
||||
// property named `schemas` anywhere". A measured failure of the unconstrained form: a
|
||||
// `declare module` block elsewhere carrying its own `schemas` member silently OVERWROTE a real
|
||||
// schema's optional list with an empty one, which removes a droppable schema from the
|
||||
// population without reddening anything.
|
||||
@@ -244,13 +244,13 @@ export function scanOptionalSchemaMembers(dtsText: string): Map<string, string[]
|
||||
if (schema.type && !ts.isTypeLiteralNode(schema.type) && !isEnumShaped(schema.type)) {
|
||||
// Anything that is neither an object literal nor enum-shaped may CARRY members this
|
||||
// scanner cannot enumerate, and skipping it drops them from the droppable population with
|
||||
// nothing reddening. An earlier version threw on intersections only, on the stated ground
|
||||
// that `allOf` was "the one non-object shape that can hide members". Review measured that
|
||||
// false by execution: a union of refs, a union of object literals, a `$ref` alias and an
|
||||
// array each hid members too, and `generateTypes` emits a union for `oneOf`/`anyOf` AND for
|
||||
// a plain nullable object (`type: ["object","null"]`). So the test is now positive — is it
|
||||
// a shape we can enumerate, or one we have shown carries nothing — rather than a list of
|
||||
// shapes to reject, which is the enumeration that was already wrong once.
|
||||
// nothing reddening. Throwing on intersections alone is not enough: `allOf` is not "the one
|
||||
// non-object shape that can hide members". Measured by execution, a union of refs, a union
|
||||
// of object literals, a `$ref` alias and an array each hid members too, and `generateTypes`
|
||||
// emits a union for `oneOf`/`anyOf` AND for a plain nullable object
|
||||
// (`type: ["object","null"]`). So the test is POSITIVE — is it a shape we can enumerate, or
|
||||
// one we have shown carries nothing — rather than a list of shapes to reject, which is the
|
||||
// enumeration that measurement defeated.
|
||||
throw new Error(
|
||||
`completeAnnotationScan: schema \`${name}\` has a type this scanner cannot enumerate ` +
|
||||
`(kind ${ts.SyntaxKind[schema.type.kind]}). Teach it the shape rather than letting the ` +
|
||||
|
||||
@@ -106,8 +106,8 @@ const DISPOSITIONS: Record<string, { disposition: Disposition; note: string }> =
|
||||
CreateChannelFromLineupAdvancedOptionsRequest: {
|
||||
// MUST-NOT-ANNOTATE rather than CREATE, so the prohibition is asserted rather than merely
|
||||
// described: annotating this one is a BUG for the same reason as the server-computed case, just
|
||||
// arrived at differently. Cold review pointed out that CREATE was permissive here while the note
|
||||
// said annotating would break the semantics.
|
||||
// arrived at differently. CREATE would be permissive here while the note says annotating would
|
||||
// break the semantics.
|
||||
disposition: 'MUST-NOT-ANNOTATE',
|
||||
note: 'POST /channels/from-lineup. Omission is LOAD-BEARING: an omitted override means INHERIT the template value, with a separate `clear` list to force NONE. `Complete<T>` would collapse that third state into explicit-null.'
|
||||
},
|
||||
@@ -236,19 +236,19 @@ function discoverAnnotations(): DiscoveredAnnotation[] {
|
||||
*
|
||||
* Test files are scanned (a `Complete<…>` there is still an application, and #807's own regression
|
||||
* fixture lives in one) but they cannot satisfy the requirement that a schema be annotated in
|
||||
* production. Cold review demonstrated why with a working bypass: strip `Complete<>` off the
|
||||
* `updateFFmpegProfile` WRAPPER and retype `ffmpegProfiles.test.ts`'s fixture to name
|
||||
* `UpdateFFmpegProfileRequest`, and a per-schema "annotated somewhere" rule stays green while the
|
||||
* wrapper stops protecting every future caller.
|
||||
* production. A working bypass shows why: strip `Complete<>` off the `updateFFmpegProfile`
|
||||
* WRAPPER and retype `ffmpegProfiles.test.ts`'s fixture to name `UpdateFFmpegProfileRequest`, and
|
||||
* a per-schema "annotated somewhere" rule stays green while the wrapper stops protecting every
|
||||
* future caller.
|
||||
*/
|
||||
function isProductionSource(rootRelativePath: string): boolean {
|
||||
// `.spec.` and `setupTests.ts` are here because cold review measured the `.test.`-only form false:
|
||||
// `.spec.` and `setupTests.ts` are here because the `.test.`-only form is false:
|
||||
// vitest's own default include is `**/*.{test,spec}.?(c|m)[jt]s?(x)` (measured on 4.1.9), so a `*.spec.ts` fixture was
|
||||
// a TEST to the runner and PRODUCTION to this predicate, and could discharge the obligation while
|
||||
// the real builder lost its annotation. Same for the setup file, which vitest loads for every run.
|
||||
//
|
||||
// WHAT THIS COVERS, bounded rather than universal — an earlier version of this comment said it had
|
||||
// to be true "in all of the spellings vitest accepts", and review measured THAT false in turn:
|
||||
// WHAT THIS COVERS, bounded rather than universal — the claim is NOT true "in all of the
|
||||
// spellings vitest accepts", as the uncovered residual below shows:
|
||||
// `*.test.*`, `*.spec.*`, and the ONE setup file named in `vite.config.ts`, whose path is pinned by
|
||||
// the scope-pin test so a rename reddens. NOT covered: a SECOND `setupFiles` entry. vitest would
|
||||
// load it every run, so an annotation there discharges the obligation — measured, with the real
|
||||
@@ -378,8 +378,9 @@ describe('Complete<T> application guard (#820)', () => {
|
||||
}, 20000);
|
||||
|
||||
it('scans nearly ALL tracked TypeScript, so a scope edit cannot quietly drop a quarter of it', () => {
|
||||
// Named pins catch a narrowing that happens to hit one of five files; cold review dropped 65 of
|
||||
// 257 files with every pin and floor still green. This bounds the whole population instead.
|
||||
// Named pins catch a narrowing that happens to hit one of five files; a measured narrowing
|
||||
// dropped 65 of 257 files with every pin and floor still green. This bounds the whole
|
||||
// population instead.
|
||||
//
|
||||
// The denominator is deliberately NOT computed through `isInScopeForAnnotations`: a helper on
|
||||
// both sides of a comparison cancels out of it, which is the #819 finding recorded at length in
|
||||
@@ -398,12 +399,12 @@ describe('Complete<T> application guard (#820)', () => {
|
||||
}, 20000);
|
||||
|
||||
it('keeps the modules carrying the motivating defects inside the scanned population (scope pin)', () => {
|
||||
// A floor cannot see SELECTIVE narrowing: cold review showed that excluding one path from
|
||||
// `isInScopeForAnnotations` stays comfortably above every count while erasing this guard's reach
|
||||
// over exactly the file whose defect motivated it. These pin the paths rather than the count.
|
||||
// A floor cannot see SELECTIVE narrowing: excluding one path from `isInScopeForAnnotations`
|
||||
// stays comfortably above every count while erasing this guard's reach over exactly the file
|
||||
// whose defect motivated it. These pin the paths rather than the count.
|
||||
// `isProductionSource` hardcodes 'src/setupTests.ts', mirroring `vite.config.ts`'s `setupFiles`
|
||||
// with nothing coupling them. Renaming the setup file would leave the exclusion stale, vitest
|
||||
// would still load the new one for every run, and the round-5 `.spec.ts` bypass would reopen
|
||||
// would still load the new one for every run, and the `.spec.ts` bypass would reopen
|
||||
// green. Pinning it turns that silent staleness into a red — the same remedy as the paths below.
|
||||
expect(
|
||||
trackedSources.tracked,
|
||||
@@ -434,8 +435,8 @@ describe('Complete<T> application guard (#820)', () => {
|
||||
|
||||
it('no file declares its OWN Complete, which would make an annotation an identity type', () => {
|
||||
// `Complete<X>` is matched by NAME. A local `type Complete<T> = T` would satisfy every
|
||||
// annotation assertion here while providing none of the checking — a false green cold review
|
||||
// demonstrated. Nothing declares one today; this keeps it that way.
|
||||
// annotation assertion here while providing none of the checking — a demonstrated false green.
|
||||
// Nothing declares one today; this keeps it that way.
|
||||
const shadows = trackedInScopeSources()
|
||||
.filter(({ file }) => file !== 'src/api/completeRequest.ts')
|
||||
.filter(({ file, text }) => declaresLocalComplete(text, file))
|
||||
|
||||
@@ -98,11 +98,11 @@ const case3: SyntheticRequest = {
|
||||
// was live before #807 — dropping it from `MultiCollectionsScreen.toItemRequest` typechecked clean
|
||||
// and would have reset every weight to 1 on the next full-replace save.
|
||||
//
|
||||
// An earlier version of this case pinned `ReplaceDecoTemplateRequest.name`, a REQUIRED member, and
|
||||
// then reasoned in a comment that no real request type could discriminate "because none has an
|
||||
// optional member". That was false when written — `scripts/tests/test_optional_request_members.py`
|
||||
// derives the schemas that do, and there are several — and it cost the proof its most valuable
|
||||
// case. No count is given here on purpose: that population is derived, and every hand-written
|
||||
// Pinning this case to a REQUIRED member instead — `ReplaceDecoTemplateRequest.name`, say — costs
|
||||
// the proof its most valuable case. The reasoning that invites it, that no real request type can
|
||||
// discriminate "because none has an optional member", is false:
|
||||
// `scripts/tests/test_optional_request_members.py` derives the schemas that do, and there are
|
||||
// several. No count is given here on purpose: that population is derived, and every hand-written
|
||||
// version of it on this issue has been wrong. Do not re-pin this to a required member: check
|
||||
// the schema's `required` array
|
||||
// first, and prefer a member that a builder could actually drop.
|
||||
|
||||
@@ -49,8 +49,7 @@
|
||||
* literal, and the check does not fire — with or without a spread in it. Measured on the
|
||||
* pre-#807 tree: several construction sites accepted a phantom field, three of them with no
|
||||
* spread and no inferred local. Annotating each site's return type is what restores this
|
||||
* direction. (No tally: "construction site" is not a derived population, and the count in an
|
||||
* earlier draft was wrong.)
|
||||
* direction. (No tally: "construction site" is not a derived population.)
|
||||
*
|
||||
* ## Explicit `undefined` is still allowed, deliberately
|
||||
*
|
||||
|
||||
@@ -69,11 +69,10 @@ describe('titleContainsQuery (#651 — compile typed text, never forward raw Luc
|
||||
expect(titleContainsQuery('Show Alpha')).toBe('title:*Show\\ Alpha*');
|
||||
});
|
||||
|
||||
// The previous version of this test hand-copied a sample string and claimed to cover "every
|
||||
// Lucene special" — it silently omitted `&` and `|`, and a completeness test that carries its own
|
||||
// list of what to check cannot see what is missing from that list (#651 F2). Drive the assertion
|
||||
// from the exported character set instead, one character at a time, so adding a character to the
|
||||
// set without escaping it fails here.
|
||||
// Hand-copying a sample string and claiming to cover "every Lucene special" silently omitted `&`
|
||||
// and `|`: a completeness test that carries its own list of what to check cannot see what is
|
||||
// missing from that list (#651 F2). Drive the assertion from the exported character set instead,
|
||||
// one character at a time, so adding a character to the set without escaping it fails here.
|
||||
it.each(LIBRARY_PICKER_LUCENE_SPECIALS.split(''))('escapes the Lucene special %j', (char) => {
|
||||
expect(titleContainsQuery(`a${char}b`)).toBe(`title:*a\\${char}b*`);
|
||||
});
|
||||
@@ -157,8 +156,8 @@ describe('searchLibraryBrowseItems (#685 — AddItemsDialog sibling of searchLib
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// The reviewer proved this helper was dead code to the suite: deleting its clamp, or deleting
|
||||
// its gate, both left the whole suite green. These three tests mirror the ones above for
|
||||
// This helper was dead code to the suite, measured: deleting its clamp, or deleting its gate,
|
||||
// both left the whole suite green. These three tests mirror the ones above for
|
||||
// searchLibraryPickerOptions so the same bound is pinned for the sibling helper.
|
||||
|
||||
it('#651 F4: CLAMPS an oversized pageSize rather than forwarding it', async () => {
|
||||
|
||||
@@ -65,7 +65,7 @@ import { scanPageSizeSites } from './pageSizeScan';
|
||||
* is that test's actual subject, and it runs against fixed inline fixtures, so it has no churn.
|
||||
*
|
||||
* `scanPageSizeSites` itself is verified against inline fixture source strings covering every
|
||||
* input class a text-level scanner previously got wrong (comment-in-string, template
|
||||
* input class a text-level scanner gets wrong (comment-in-string, template
|
||||
* interpolation, ternary, `??`, JSX container, same-line duplicates, parameter/nested
|
||||
* destructuring, a type literal, a string containing the text `pageSize: 100`) in
|
||||
* `pageSizeScan.test.ts` — that test does not depend on the real repo, so it protects the SCANNER
|
||||
@@ -106,10 +106,9 @@ import { scanPageSizeSites } from './pageSizeScan';
|
||||
* user-authored query that surfaces its match count
|
||||
* (`SmartCollectionDialog`). That list is illustrative and NOT
|
||||
* exhaustive: a site qualifies by rendering a real `totalCount`-backed
|
||||
* hint, not by resembling one of these four. (#684 review: an earlier
|
||||
* revision of this comment called it "the whole list" while the registry
|
||||
* below already held a fourth — the same false-exhaustiveness defect this
|
||||
* PR exists to remove.)
|
||||
* hint, not by resembling one of these four. (Calling it "the whole list"
|
||||
* would be the same false-exhaustiveness defect #684 exists to remove —
|
||||
* the registry below already holds a fourth.)
|
||||
* - 'paged-ui' — real paging UI (a page/"load more" control, or a user-adjustable
|
||||
* page-size selector, keyed to a genuine `totalCount`), so a `pageSize`
|
||||
* at or below the cap is correct as-is.
|
||||
@@ -181,7 +180,7 @@ const REGISTRY: RegistryEntry[] = [
|
||||
'requires it be discovered twice. No request is issued below LIBRARY_PICKER_MIN_QUERY (a ' +
|
||||
'blank form submit and a kind-chip click below the gate both resolve every kind to ' +
|
||||
'{items: [], totalCount: 0} via the HELPER\'s own gate — CollectionsScreen no longer keeps a ' +
|
||||
'second copy of this check; the #685 second review proved the two masked each other), the ' +
|
||||
'second copy of this check; the two masked each other), the ' +
|
||||
'typed text is compiled via titleContainsQuery rather than forwarded raw, and each kind is ' +
|
||||
'bounded to one request per settled query at LIBRARY_PICKER_RESULTS rows. Unlike the #685 ' +
|
||||
'first fix, this is NOT "nothing left to hint at": the per-kind cap can still truncate the ' +
|
||||
@@ -192,7 +191,7 @@ const REGISTRY: RegistryEntry[] = [
|
||||
'BOTH ingress paths into the searched kinds — MediaKindFilter (the explicit-chip path) and ' +
|
||||
'DEFAULT_SEARCH_KINDS (the `all` fan-out) are each derived from ADDABLE_TYPE_LIST via ' +
|
||||
'`(typeof ADDABLE_TYPE_LIST)[number]`, so adding a non-addable kind to either is a compile ' +
|
||||
'error. Enforcing only the first was the #685 round-3 review finding: the hint sums ' +
|
||||
'error. Enforcing only the first was the #685 finding: the hint sums ' +
|
||||
'PRE-filter totalCounts against POST-filter rows, so one unenforced ingress is enough to ' +
|
||||
'overstate it with every row of that kind dropped.'
|
||||
},
|
||||
@@ -458,7 +457,7 @@ export function populationIsDegenerate(presentCount: number, inScopeCount: numbe
|
||||
*/
|
||||
export function isInScopeSourcePath(rootRelativePath: string): boolean {
|
||||
// The basename is computed INLINE rather than through a shared `basename()` helper, and that is
|
||||
// the #819 round-5 fix rather than a style choice: a helper here is a helper the restatement in
|
||||
// the #819 fix rather than a style choice: a helper here is a helper the restatement in
|
||||
// `the shared scope predicate is EXACTLY its two documented components` would also call, which
|
||||
// puts it on both sides of that comparison where it cancels. It was shared, and teaching it to
|
||||
// return '' for deep paths removed 15 real files and blinded the guard to a planted call site
|
||||
@@ -468,7 +467,7 @@ export function isInScopeSourcePath(rootRelativePath: string): boolean {
|
||||
}
|
||||
|
||||
// Extracted from `listSourceFiles`'s inline condition so it's independently testable (#650
|
||||
// follow-up round 4): a plant that adds a real `.mts` FILE and observes the guard notice it
|
||||
// follow-up): a plant that adds a real `.mts` FILE and observes the guard notice it
|
||||
// proves the behavior exists today, but pins nothing — revert the glob back to `.ts`/`.tsx` and
|
||||
// both the real-source guard AND `pageSizeScan.test.ts`'s `.mts`/`.cts` PARSING tests stay green,
|
||||
// because this repo has no committed `.mts`/`.cts` source and `scanPageSizeSites` parses any
|
||||
@@ -481,7 +480,7 @@ export function isInScopeSourcePath(rootRelativePath: string): boolean {
|
||||
export function isScannableSourceFileName(name: string): boolean {
|
||||
// `.mts`/`.cts` are legal TS extensions `tsconfig.app.json`'s `include` covers alongside
|
||||
// `.ts`/`.tsx` — none exist in this repo today, but the glob must not silently skip one if it
|
||||
// ever does (#650 follow-up round 3 MEDIUM finding).
|
||||
// ever does (#650 follow-up).
|
||||
return (
|
||||
/\.(ts|tsx|mts|cts)$/.test(name) && !/\.test\.(tsx?|mts|cts)$/.test(name) && !name.endsWith('.guard.test.ts')
|
||||
);
|
||||
@@ -674,11 +673,12 @@ function multisetExcess(left: Map<string, number>, right: Map<string, number>):
|
||||
}
|
||||
|
||||
// These 4 tests are BASELINE assertions about the guard's steady-state behavior against the
|
||||
// current repo snapshot — they all pass equally on the clean `b90f8a3b` commit (before this
|
||||
// round's scanner rewrite), so none of them individually PROVE this round's fixes. What actually
|
||||
// current repo snapshot — they all pass equally on the clean `b90f8a3b` commit (before the
|
||||
// scanner rewrite), so none of them individually PROVE that rewrite's fixes. What actually
|
||||
// regression-pins the scanner's fixes is `pageSizeScan.test.ts` (synthetic fixtures per input
|
||||
// class, verified against the prior scanner where the review asked for it) — these 4 just confirm
|
||||
// the guard, wired to whichever scanner it currently uses, still holds over real source.
|
||||
// class, verified against the prior scanner for the fixtures that could fail it, enumerated in
|
||||
// that file's header) — these 4 just confirm the guard, wired to
|
||||
// whichever scanner it currently uses, still holds over real source.
|
||||
describe('pageSize call-site guard (#650)', () => {
|
||||
it.each([
|
||||
['screens/TraktListsScreen.ts', true],
|
||||
@@ -697,14 +697,14 @@ describe('pageSize call-site guard (#650)', () => {
|
||||
['noextension', false]
|
||||
])(
|
||||
'isScannableSourceFileName(%s) === %s — the file-discovery predicate itself, independent of ' +
|
||||
'whether the repo happens to contain a matching file (#650 follow-up round 4)',
|
||||
'whether the repo happens to contain a matching file (#650 follow-up)',
|
||||
(name, expected) => {
|
||||
// A prior verification planted a REAL .mts file and observed the guard notice it — that
|
||||
// proved the .mts/.cts fix works today, but pinned nothing: reverting the glob back to
|
||||
// `.ts`/`.tsx` leaves both the real-source guard AND pageSizeScan.test.ts's .mts/.cts
|
||||
// PARSING tests green, since this repo has no committed .mts/.cts source and the scanner
|
||||
// parses any non-.tsx filename as plain TS regardless of extension. Asserting on the
|
||||
// predicate BY FILENAME, with no filesystem involved, is what actually regression-pins it.
|
||||
// Planting a REAL .mts file and observing the guard notice it proves the .mts/.cts fix works
|
||||
// today, but pins nothing: reverting the glob back to `.ts`/`.tsx` leaves both the
|
||||
// real-source guard AND pageSizeScan.test.ts's .mts/.cts PARSING tests green, since this
|
||||
// repo has no committed .mts/.cts source and the scanner parses any non-.tsx filename as
|
||||
// plain TS regardless of extension. Asserting on the predicate BY FILENAME, with no
|
||||
// filesystem involved, is what actually regression-pins it.
|
||||
expect(isScannableSourceFileName(name)).toBe(expected);
|
||||
}
|
||||
);
|
||||
@@ -789,9 +789,9 @@ describe('pageSize call-site guard (#650)', () => {
|
||||
// So spell the intended scope out once, deliberately, and require the predicate to equal it
|
||||
// over the REAL tracked population.
|
||||
//
|
||||
// BOTH halves are restated here, and the second one is the whole point. An earlier version
|
||||
// called `isScannableSourceFileName` on this side too, delegating the filename half to the same
|
||||
// function `isInScopeSourcePath` calls — which put it on both sides of the comparison, where it
|
||||
// BOTH halves are restated here, and the second one is the whole point. Calling
|
||||
// `isScannableSourceFileName` on this side too, delegating the filename half to the same
|
||||
// function `isInScopeSourcePath` calls, puts it on both sides of the comparison, where it
|
||||
// cancels algebraically. MEASURED: adding `&& !name.endsWith('.d.ts')` to that predicate
|
||||
// dropped a real tracked file and adding a term that skips lowercase-initial `.tsx` dropped
|
||||
// eleven, both with the whole suite green. Delegating half a decomposition proves
|
||||
@@ -945,8 +945,8 @@ describe('pageSize call-site guard (#650)', () => {
|
||||
])(
|
||||
'populationIsDegenerate(%i, %i) === %s — the floor, pinned rather than asserted inline',
|
||||
(present, inScope, expected) => {
|
||||
// Two identical floor assertions used to sit side by side here, so deleting either left the
|
||||
// suite green — `duplicate guards mask each other`. One assertion now, over a named function
|
||||
// Two identical floor assertions side by side would let deleting either leave the suite
|
||||
// green — `duplicate guards mask each other`. One assertion here, over a named function
|
||||
// with its own fixtures, so disarming it reddens by name.
|
||||
expect(populationIsDegenerate(present, inScope)).toBe(expected);
|
||||
}
|
||||
@@ -954,7 +954,7 @@ describe('pageSize call-site guard (#650)', () => {
|
||||
|
||||
it('every in-scope path the WALK sees is accounted for by git — tracked, or reported untracked', () => {
|
||||
// The population SOURCE's own clause proof, and the one comparison here that is not derived
|
||||
// from `trackedSources.tracked` on both sides (#819 round 6).
|
||||
// from `trackedSources.tracked` on both sides (#819).
|
||||
//
|
||||
// Everything else in this file compares two things computed from that one array: the scope
|
||||
// restatement, the shortfall direction, the floor's denominator. So a filter applied inside the
|
||||
@@ -1127,9 +1127,8 @@ describe('pageSize call-site guard (#650)', () => {
|
||||
|
||||
// No live 'deviation' entries as of #685 (the last one — CollectionsScreen.tsx's raw-50 window
|
||||
// — was fixed and reclassified 'search-bounded' above). The anti-vacuity
|
||||
// `expect(deviations.length).toBeGreaterThan(0)` this comment used to enforce is deleted
|
||||
// DELIBERATELY here, per its own instruction, rather than left to silently pass over an empty
|
||||
// list — re-add it the day a new 'deviation' entry is registered.
|
||||
// `expect(deviations.length).toBeGreaterThan(0)` is omitted DELIBERATELY here, rather than left
|
||||
// to silently pass over an empty list — re-add it the day a new 'deviation' entry is registered.
|
||||
|
||||
for (const entry of deviations) {
|
||||
expect(entry.issue, `${entry.file}:${entry.value} is a deviation but names no tracking issue`).toEqual(
|
||||
|
||||
@@ -10,16 +10,16 @@ import { pageSizeSiteId, scanPageSizeSites, type PageSizeSite } from './pageSize
|
||||
* the INPUT CLASSES that expose a text-level scanner's blind spots. Pinning the exact discovered
|
||||
* set against synthetic source strings closes that gap.
|
||||
*
|
||||
* Not every fixture here is a REGRESSION pin against the prior (round-1, `b90f8a3b`) bracket-
|
||||
* tracking scanner — a round-3 review found that round 1's simple `pageSize:\s*value` regex
|
||||
* already handled a bare URL-string or a bare `??` context correctly on its own (a `//` inside a
|
||||
* string, or the token immediately before `{`, only mattered to round 1's OWN heuristics, not to
|
||||
* a plain regex match). Those two are labelled CONTRACT fixtures below — they pin the documented
|
||||
* behavior going forward, not a fix. The fixtures that genuinely fail against round 1 (verified)
|
||||
* are: the string CONTAINING the literal text `pageSize: 100`, the template-literal
|
||||
* interpolation, the same-line ternary identity/multiplicity, the JSX shorthand container,
|
||||
* parameter destructuring, nested destructuring, and the type-literal declaration — plus the
|
||||
* combined multi-case fixture, which fails round 1 for several of those reasons at once.
|
||||
* Not every fixture here is a REGRESSION pin against the prior (`b90f8a3b`) bracket-tracking
|
||||
* scanner — that scanner's simple `pageSize:\s*value` regex already handled a bare URL-string or a
|
||||
* bare `??` context correctly on its own (a `//` inside a string, or the token immediately before
|
||||
* `{`, only mattered to its OWN heuristics, not to a plain regex match). Those two are labelled
|
||||
* CONTRACT fixtures below — they pin the documented behavior going forward, not a fix. The
|
||||
* fixtures that genuinely fail against `b90f8a3b` (verified) are: the string CONTAINING the
|
||||
* literal text `pageSize: 100`, the template-literal interpolation, the same-line ternary
|
||||
* identity/multiplicity, the JSX shorthand container, parameter destructuring, nested
|
||||
* destructuring, and the type-literal declaration — plus the combined multi-case fixture, which
|
||||
* fails it for several of those reasons at once.
|
||||
*/
|
||||
|
||||
function ids(sites: PageSizeSite[]): string[] {
|
||||
@@ -39,13 +39,13 @@ describe('scanPageSizeSites', () => {
|
||||
expect(ids(sites)).toEqual(['1:10:shorthand:pageSize']);
|
||||
});
|
||||
|
||||
it('is NOT fooled by a "//" inside a string literal — CONTRACT fixture, not a round-1 regression pin (M-3)', () => {
|
||||
// NOTE: round 1's unconditional literal regex (`pageSize:\s*(\d+|identifier)`) already
|
||||
it('is NOT fooled by a "//" inside a string literal — CONTRACT fixture, not a regression pin (M-3)', () => {
|
||||
// NOTE: the prior scanner's unconditional literal regex (`pageSize:\s*(\d+|identifier)`) already
|
||||
// matched this exact input correctly on its own — a `//` inside a string never confused THAT
|
||||
// narrower pattern. This pins the AST scanner's documented contract going forward; it is the
|
||||
// COMBINED multi-case fixture below (and the M-3-shaped case buried inside it — a literal
|
||||
// `//` immediately preceding a real call site on the SAME conceptual scan) that actually
|
||||
// fails against round 1's comment-stripping step, not this input in isolation.
|
||||
// fails against the prior scanner's comment-stripping step, not this input in isolation.
|
||||
const source = [`const endpoint = 'https://example.test';`, `getFoo({ pageSize: 100 });`, ''].join('\n');
|
||||
const sites = scanPageSizeSites(source, 'fixture.ts');
|
||||
expect(ids(sites)).toEqual(['2:10:literal:100']);
|
||||
@@ -71,10 +71,10 @@ describe('scanPageSizeSites', () => {
|
||||
expect(sites[0].column).not.toBe(sites[1].column);
|
||||
});
|
||||
|
||||
it('finds an object literal on the right-hand side of ?? — CONTRACT fixture, not a round-1 regression pin (M-4)', () => {
|
||||
// NOTE: like the URL fixture above, round 1's literal-form regex already matched this exact
|
||||
// `pageSize: 50` text correctly on its own — `??` doesn't change what characters precede the
|
||||
// match on the line. This pins the documented contract, not a round-1 regression.
|
||||
it('finds an object literal on the right-hand side of ?? — CONTRACT fixture, not a regression pin (M-4)', () => {
|
||||
// NOTE: like the URL fixture above, the prior scanner's literal-form regex already matched this
|
||||
// exact `pageSize: 50` text correctly on its own — `??` doesn't change what characters precede
|
||||
// the match on the line. This pins the documented contract, not a regression against it.
|
||||
const source = `getFoo(options ?? { pageSize: 50 });\n`;
|
||||
const sites = scanPageSizeSites(source, 'fixture.ts');
|
||||
expect(ids(sites)).toEqual(['1:21:literal:50']);
|
||||
@@ -176,7 +176,7 @@ describe('scanPageSizeSites', () => {
|
||||
expect(ids(sites)).toEqual(['2:23:literal:10']);
|
||||
});
|
||||
|
||||
// ---- round-3 MEDIUM finding: transparent TS wrappers around the initializer -----------------
|
||||
// ---- transparent TS wrappers around the initializer -----------------------------------------
|
||||
|
||||
it('finds a literal wrapped in "as const" (transparent to the runtime value)', () => {
|
||||
const source = `getFoo({ pageSize: 100 as const });\n`;
|
||||
@@ -207,7 +207,7 @@ describe('scanPageSizeSites', () => {
|
||||
expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]);
|
||||
});
|
||||
|
||||
// ---- round-3 MEDIUM finding: non-Identifier property names -----------------------------------
|
||||
// ---- non-Identifier property names -----------------------------------------------------------
|
||||
|
||||
it('finds a quoted string property key ("pageSize": 100)', () => {
|
||||
const source = `getFoo({ 'pageSize': 100 });\n`;
|
||||
@@ -231,7 +231,7 @@ describe('scanPageSizeSites', () => {
|
||||
expect(scanPageSizeSites(source, 'fixture.ts')).toEqual([]);
|
||||
});
|
||||
|
||||
// ---- round-3 MEDIUM finding: .mts/.cts are never silently skipped -----------------------------
|
||||
// ---- .mts/.cts are never silently skipped -----------------------------------------------------
|
||||
|
||||
it('scans .mts source (parses as plain TS, no JSX grammar)', () => {
|
||||
const source = `export function loadPage() {\n return getFoo({ pageSize: 100 });\n}\n`;
|
||||
|
||||
@@ -7,8 +7,8 @@ import * as ts from 'typescript';
|
||||
* (`pageSizeScan.test.ts`, which scans synthetic source strings and does NOT touch the repo) can
|
||||
* exercise the exact same scanning logic.
|
||||
*
|
||||
* A prior hand-rolled regex/bracket-tracking version of this scan was replaced after a review
|
||||
* found it defeated by comments-in-strings, template-literal interpolations, ternary/`??`
|
||||
* A prior hand-rolled regex/bracket-tracking version of this scan was replaced because it was
|
||||
* defeated by comments-in-strings, template-literal interpolations, ternary/`??`
|
||||
* contexts, JSX containers, and same-line duplicates — each a DIFFERENT input class a text-level
|
||||
* lexer has to special-case one at a time. The TypeScript compiler API sidesteps the whole
|
||||
* category: comments and string/template CONTENTS are trivia/literal text the parser never
|
||||
@@ -19,7 +19,7 @@ import * as ts from 'typescript';
|
||||
* `const { pageSize } = x`) — so those are excluded by NODE KIND, not by a preceding-character
|
||||
* heuristic that can be fooled by an unrelated `{`/`(`/`,`.
|
||||
*
|
||||
* A round-3 review found the AST version still had its own — smaller, but real — false
|
||||
* The AST version still had its own — smaller, but real — false
|
||||
* negatives: an initializer wrapped in a transparent TS construct (`pageSize: 100 as const`,
|
||||
* `pageSize: 100 satisfies number`, `pageSize: (100)`) was rejected outright because only a bare
|
||||
* `NumericLiteral`/`Identifier` was checked; a property written as a quoted string key
|
||||
@@ -50,14 +50,14 @@ function scriptKindFor(fileName: string): ts.ScriptKind {
|
||||
// `.mts`/`.cts` parse as plain TS (no JSX support), same as `.ts` — only `.tsx` needs the JSX
|
||||
// grammar. `tsconfig.app.json`'s `include` covers all of `src`, and `.mts`/`.cts` are legal
|
||||
// TS extensions the guard's file-discovery glob must not silently skip even though none exist
|
||||
// in this repo today (#650 follow-up round 3 MEDIUM finding).
|
||||
// in this repo today (#650 follow-up).
|
||||
return fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
||||
}
|
||||
|
||||
// Unwraps TS constructs that are transparent to the runtime VALUE but would otherwise hide a
|
||||
// numeric literal / identifier from a naive node-kind check: `expr as T`, `expr satisfies T`,
|
||||
// and `(expr)`. `pageSize: 100 as const` and `pageSize: 100 satisfies number` are both real
|
||||
// fixed-100 call sites; only the TS type-checking wrapper differs (#650 follow-up round 3 MEDIUM).
|
||||
// fixed-100 call sites; only the TS type-checking wrapper differs (#650 follow-up).
|
||||
function unwrapTransparentExpression(node: ts.Expression): ts.Expression {
|
||||
let current = node;
|
||||
for (;;) {
|
||||
@@ -76,7 +76,7 @@ function unwrapTransparentExpression(node: ts.Expression): ts.Expression {
|
||||
// A property name is `pageSize` whether written as a plain identifier (`pageSize: 100`), a
|
||||
// quoted string key (`'pageSize': 100`), or a computed key that's STATICALLY a `'pageSize'`
|
||||
// string literal (`['pageSize']: 100`) — all three compile to the identical property, so all
|
||||
// three are real call sites (#650 follow-up round 3 MEDIUM). A computed key that ISN'T a literal
|
||||
// three are real call sites (#650 follow-up). A computed key that ISN'T a literal
|
||||
// (e.g. `[dynamicKeyVar]: 100`) can't be resolved statically and is correctly left unmatched.
|
||||
function isPageSizePropertyName(name: ts.PropertyName): boolean {
|
||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
|
||||
|
||||
@@ -2,9 +2,9 @@ import { describe, expect, it } from 'vitest';
|
||||
import { INT32_MAX, INT32_MIN, isSelectionId, selectionIdOrNull } from './selectionId';
|
||||
|
||||
// This predicate is the single point of failure for every id that reaches editor state across three
|
||||
// screens (#651 round 8), and until now it was only exercised indirectly through screen tests. The
|
||||
// endpoints matter most: swapping either `>=`/`<=` for a strict comparison is the classic mutation
|
||||
// on exactly this code, and nothing else in the suite would notice.
|
||||
// screens (#651), and screen tests exercise it only indirectly. The endpoints matter most: swapping
|
||||
// either `>=`/`<=` for a strict comparison is the classic mutation on exactly this code, and
|
||||
// nothing else in the suite would notice.
|
||||
describe('isSelectionId', () => {
|
||||
it('accepts the INCLUSIVE int32 endpoints', () => {
|
||||
expect(INT32_MAX).toBe(2_147_483_647);
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
// preset. The API binds every one of them as a 32-bit integer, so a value outside that domain is
|
||||
// not merely odd: it renders and commits happily and then fails on write.
|
||||
//
|
||||
// This lives in ONE place on purpose. #651 round 7 added the check inside the search picker's
|
||||
// option validator — the site where the defect was found — leaving list-backed options and the
|
||||
// selection restored from a detail read unguarded, so the identical malformed value entered editor
|
||||
// state through a different door (round 8). The predicate belongs at the BOUNDARY the class
|
||||
// crosses: every path by which an id from the wire becomes editor state.
|
||||
// This lives in ONE place on purpose. Putting the check inside the search picker's option
|
||||
// validator — the site where the defect was found — leaves list-backed options and the selection
|
||||
// restored from a detail read unguarded, so the identical malformed value enters editor state
|
||||
// through a different door (#651). The predicate belongs at the BOUNDARY the class crosses: every
|
||||
// path by which an id from the wire becomes editor state.
|
||||
// DELIBERATELY not `id > 0`. Most id checks in this codebase (`routing.ts` and eight screens) use
|
||||
// `Number.isInteger(id) && id > 0` because they are asking "could this id EXIST?". This predicate
|
||||
// asks a different question — "can the API BIND this value as its `int` parameter?" — so `0` and
|
||||
|
||||
@@ -842,16 +842,16 @@ describe('Channel Builder (#89)', () => {
|
||||
'single-flight: a "Load more" click while a page-0 refresh is in flight is IGNORED (not ' +
|
||||
'queued), so a naive inverse settlement order (page 1 resolving before page 0) is no ' +
|
||||
'longer reachable — page 1 is only ever requested AFTER page 0 settles, and both end up ' +
|
||||
'present with the cursor correctly at page 2 for the next click (#650 follow-up round 3)',
|
||||
'present with the cursor correctly at page 2 for the next click (#650 follow-up)',
|
||||
async () => {
|
||||
// Round 1 fixed "page 0 settles first, wrongly clears the append spinner". Round 3's
|
||||
// review found the MIRROR image reachable through the exact same click sequence: if the
|
||||
// click were still allowed to start B/page-1 while B/page-0 was in flight, page 1 could
|
||||
// resolve FIRST, append onto stale query-A rows, and then page 0's later resolution would
|
||||
// silently replace/erase it without resetting the cursor — permanently losing page 1. Round
|
||||
// 3's fix removes the click's ability to start a second fetch AT ALL while any fetch (page-0
|
||||
// refresh or append) is outstanding, so this ordering can't arise irrespective of which
|
||||
// network response happens to land first.
|
||||
// A narrower fix for "page 0 settles first, wrongly clears the append spinner" leaves the
|
||||
// MIRROR image reachable through the exact same click sequence: if the click were still
|
||||
// allowed to start B/page-1 while B/page-0 was in flight, page 1 could resolve FIRST, append
|
||||
// onto stale query-A rows, and then page 0's later resolution would silently replace/erase
|
||||
// it without resetting the cursor — permanently losing page 1. The fix removes the click's
|
||||
// ability to start a second fetch AT ALL while any fetch (page-0 refresh or append) is
|
||||
// outstanding, so this ordering can't arise irrespective of which network response happens
|
||||
// to land first.
|
||||
const pageA0 = [browseItem({ id: 1, mediaItemId: 1, title: 'Item A1' })];
|
||||
const pageB0 = [browseItem({ id: 2, mediaItemId: 2, title: 'Item B1' })];
|
||||
const pageB1 = [browseItem({ id: 3, mediaItemId: 3, title: 'Item B2' })];
|
||||
@@ -1217,7 +1217,7 @@ describe('Channel Builder (#89)', () => {
|
||||
|
||||
it(
|
||||
'single-flight: calling loadMore() twice back-to-back issues only ONE fetch — the second ' +
|
||||
'call is ignored, not queued as a second overlapping request (#650 follow-up round 3)',
|
||||
'call is ignored, not queued as a second overlapping request (#650 follow-up)',
|
||||
async () => {
|
||||
// Drives the hook directly via `renderHook` (bypassing the DOM/disabled-button layer
|
||||
// entirely) to prove the guard lives in the HOOK, not just the presentation: even a
|
||||
@@ -1260,7 +1260,7 @@ describe('Channel Builder (#89)', () => {
|
||||
|
||||
it(
|
||||
'single-flight: calling loadMore() directly while a page-0 refresh is still in flight is ' +
|
||||
'IGNORED — no page-1 request is ever issued (#650 follow-up round 4)',
|
||||
'IGNORED — no page-1 request is ever issued (#650 follow-up)',
|
||||
async () => {
|
||||
// ChannelBuilder.test.tsx's component-level single-flight test asserts on the DISABLED
|
||||
// BUTTON — during a page-0 refresh, `loadingMore` disables the native button, so
|
||||
@@ -1339,7 +1339,7 @@ describe('Channel Builder (#89)', () => {
|
||||
|
||||
it(
|
||||
'a failed "Load more" page is retried as the SAME page number, not skipped or duplicated ' +
|
||||
'(#650 follow-up round 3)',
|
||||
'(#650 follow-up)',
|
||||
async () => {
|
||||
const page0 = [browseItem({ id: 1, mediaItemId: 1, mediaType: 'TelevisionShow', title: 'Item A' })];
|
||||
const page1Items = [browseItem({ id: 2, mediaItemId: 2, mediaType: 'TelevisionShow', title: 'Item B' })];
|
||||
@@ -1379,10 +1379,10 @@ describe('Channel Builder (#89)', () => {
|
||||
await waitFor(() => expect(result.current.loadMoreError).not.toBeNull());
|
||||
// The failed page must not be silently skipped: items stay at just page 0's row, and the
|
||||
// NEXT "Load more" call must re-request page 1 — never page 2 — or that row is permanently
|
||||
// lost. (An earlier compare-and-set rollback attempt made this assertion fail: it left the
|
||||
// cursor at page 1 only when nothing else had raced ahead, but a genuinely single-flight
|
||||
// hook has no "else" case left to distinguish — the unconditional rollback below is what
|
||||
// that guarantee simplifies down to.)
|
||||
// lost. (A compare-and-set rollback fails this assertion: it leaves the cursor at page 1 only
|
||||
// when nothing else has raced ahead, but a genuinely single-flight hook has no "else" case
|
||||
// left to distinguish — the unconditional rollback below is what that guarantee simplifies
|
||||
// down to.)
|
||||
expect(result.current.state.items.map((item) => item.title)).toEqual(['Item A']);
|
||||
expect(televisionShowPageNumsRequested).toEqual([0, 1]);
|
||||
|
||||
|
||||
@@ -82,11 +82,11 @@ async function loadLibraryItems(
|
||||
}
|
||||
|
||||
/**
|
||||
* #650 follow-up (review round 3): rounds 1-2 kept fixing one overlapping page-0-refresh/append
|
||||
* interleaving only to have the reviewer find its mirror image (page-0-settles-first, then
|
||||
* page-1-settles-first; a compare-and-set rollback that traded duplication for a permanently
|
||||
* skipped page). The hook still PERMITTED a page-0 refresh and an append to be in flight
|
||||
* together, so every ordering of "which one wins" was a distinct bug to enumerate.
|
||||
* #650 follow-up: fixing one overlapping page-0-refresh/append interleaving at a time only
|
||||
* surfaces its mirror image (page-0-settles-first, then page-1-settles-first; a compare-and-set
|
||||
* rollback that traded duplication for a permanently skipped page). The hook still PERMITTED a
|
||||
* page-0 refresh and an append to be in flight together, so every ordering of "which one wins"
|
||||
* was a distinct bug to enumerate.
|
||||
*
|
||||
* This version enforces SINGLE-FLIGHT instead: at most one fetch (a page-0 refresh OR an append)
|
||||
* is ever outstanding for the current query generation. `busyRef` is the guard — `loadMore` reads
|
||||
@@ -173,7 +173,7 @@ export function useLibraryBrowse(source: 'library' | 'collections', query: strin
|
||||
reqRef.current = reqId;
|
||||
pageRef.current = 0;
|
||||
// Claims the single-flight slot for this generation's own page-0 fetch — `loadMore` is a
|
||||
// no-op until it settles, closing the exact window earlier rounds kept losing: a "Load more"
|
||||
// no-op until it settles, closing the exact window a per-interleaving fix keeps missing: a "Load more"
|
||||
// click that lands while a fresh query's page-0 refresh is still in flight.
|
||||
busyRef.current = true;
|
||||
queueMicrotask(() => setLoadingMore(true));
|
||||
@@ -184,7 +184,7 @@ export function useLibraryBrowse(source: 'library' | 'collections', query: strin
|
||||
if (busyRef.current) {
|
||||
// Single-flight: ignored outright rather than queued. Queuing (or racing) a second fetch
|
||||
// against whichever one is already in flight is exactly what produced every
|
||||
// page-ordering defect earlier rounds kept chasing one interleaving at a time.
|
||||
// page-ordering defect this hook had, one interleaving at a time.
|
||||
return;
|
||||
}
|
||||
busyRef.current = true;
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('round-trip: parse(compile(tree)) === tree', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// #578 / #651 review round 2: drive the REAL `lcg` into its maximum state instead of recomputing
|
||||
// #578 / #651: drive the REAL `lcg` into its maximum state instead of recomputing
|
||||
// the divisor in the test. The generator is `s -> (1664525*s + 1013904223) mod 2^32`, which is a
|
||||
// bijection (the multiplier is odd), so the seed whose FIRST step lands on 0xffffffff can be solved
|
||||
// exactly: seed = (0xffffffff - 1013904223) * 1664525^-1 mod 2^32 = 653637408. On the old
|
||||
|
||||
@@ -368,7 +368,7 @@ function SourcePicker({
|
||||
}
|
||||
|
||||
if (type === 'RerunFirstRun' || type === 'RerunRerun') {
|
||||
// Out-of-list injection (round-3 review F2): the picker source is a Class A `loadAllPages`
|
||||
// Out-of-list injection (#644 follow-up F2): the picker source is a Class A `loadAllPages`
|
||||
// loop (SchedulesScreen.loadAllRerunCollections) that can legitimately return an incomplete
|
||||
// page set, so the item's stored rerunCollectionId may not be among `pickers.rerunCollections`
|
||||
// — mirrors RerunCollectionsScreen's and PlaylistsScreen's `selectedInList` prepend.
|
||||
@@ -437,8 +437,8 @@ function SourcePicker({
|
||||
search={search}
|
||||
// The picker is NOT remounted when the item's collection type changes, so results must be
|
||||
// bound to the type that produced them — otherwise a Collection hit stays clickable under a
|
||||
// SmartCollection label and stores a Collection id in the SmartCollection field (#651 round 4
|
||||
// HIGH-1). `type` names both the search function and the id namespace, so it IS the source.
|
||||
// SmartCollection label and stores a Collection id in the SmartCollection field (#651).
|
||||
// `type` names both the search function and the id namespace, so it IS the source.
|
||||
source={type}
|
||||
onSelect={(id, name) => setSelection(id, name)}
|
||||
onClear={() => setSelection(null, null)}
|
||||
|
||||
@@ -254,7 +254,7 @@ describe('SearchPicker', () => {
|
||||
expect(input).not.toHaveAttribute('aria-activedescendant');
|
||||
});
|
||||
|
||||
it('BLOCKER 2: Enter cannot commit a result from the PREVIOUS query', async () => {
|
||||
it('Enter cannot commit a result from the PREVIOUS query', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const release: { resolve: ((items: SearchPickerOption[]) => void) | null } = { resolve: null };
|
||||
const search = vi
|
||||
@@ -290,7 +290,7 @@ describe('SearchPicker', () => {
|
||||
expect(onSelect).toHaveBeenCalledWith(7, 'Beta Two');
|
||||
});
|
||||
|
||||
it('BLOCKER 3: the picker recovers from Escape by typing, without a blur', async () => {
|
||||
it('the picker recovers from Escape by typing, without a blur (#651)', async () => {
|
||||
const search = vi.fn<(query: string) => Promise<SearchPickerOption[]>>().mockResolvedValue(OPTIONS);
|
||||
renderPicker({ search });
|
||||
|
||||
@@ -307,7 +307,7 @@ describe('SearchPicker', () => {
|
||||
expect(await screen.findByRole('listbox', { name: 'Movie results' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('BLOCKER 3 / round 3: reopening onto CURRENT results re-queries nothing and stays navigable', async () => {
|
||||
it('reopening onto CURRENT results re-queries nothing and stays navigable', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const search = vi.fn<(query: string) => Promise<SearchPickerOption[]>>().mockResolvedValue(OPTIONS);
|
||||
renderPicker({ onSelect, search });
|
||||
@@ -338,7 +338,7 @@ describe('SearchPicker', () => {
|
||||
expect(onSelect).toHaveBeenCalledWith(2, 'Beta');
|
||||
});
|
||||
|
||||
it('round 3 HIGH: a stale result cannot be committed by POINTER either', async () => {
|
||||
it('a stale result cannot be committed by POINTER either', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const release: { resolve: ((items: SearchPickerOption[]) => void) | null } = { resolve: null };
|
||||
const search = vi
|
||||
@@ -373,7 +373,7 @@ describe('SearchPicker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---- #651 review round 4: results carry their SOURCE, not just their query text ----
|
||||
// ---- #651: results carry their SOURCE, not just their query text ----
|
||||
describe('result provenance', () => {
|
||||
const COLLECTION_HITS: SearchPickerOption[] = [{ id: 5, name: 'News Collection' }];
|
||||
const SMART_HITS: SearchPickerOption[] = [{ id: 8, name: 'News Smart' }];
|
||||
@@ -382,7 +382,7 @@ describe('SearchPicker', () => {
|
||||
{ id: 2, name: 'Beta' }
|
||||
];
|
||||
|
||||
it('HIGH-1: changing the SOURCE re-queries and retires the old namespace\'s results', async () => {
|
||||
it('changing the SOURCE re-queries and retires the old namespace\'s results', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const search = vi
|
||||
.fn<(query: string) => Promise<SearchPickerOption[]>>()
|
||||
@@ -397,9 +397,9 @@ describe('SearchPicker', () => {
|
||||
expect(await screen.findByRole('option', { name: 'News Collection' })).toBeInTheDocument();
|
||||
expect(search).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Same component instance (no `key`), same query text — only the id namespace changed. The
|
||||
// round-3 re-query guard keyed on query alone, which SUPPRESSED this request and left the
|
||||
// Collection hit clickable under the new source.
|
||||
// Same component instance (no `key`), same query text — only the id namespace changed. A
|
||||
// re-query guard keyed on query alone SUPPRESSES this request and leaves the Collection hit
|
||||
// clickable under the new source.
|
||||
rerender(
|
||||
<SearchPicker
|
||||
label="Movie"
|
||||
@@ -423,7 +423,7 @@ describe('SearchPicker', () => {
|
||||
expect(onSelect).not.toHaveBeenCalledWith(5, 'News Collection');
|
||||
});
|
||||
|
||||
it('MEDIUM-5: a FAILED search is retried on reopen, not cached as "no matches" forever', async () => {
|
||||
it('a FAILED search is retried on reopen, not cached as "no matches" forever', async () => {
|
||||
const search = vi
|
||||
.fn<(query: string) => Promise<SearchPickerOption[]>>()
|
||||
.mockRejectedValueOnce(new Error('boom'))
|
||||
@@ -438,10 +438,9 @@ describe('SearchPicker', () => {
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(/Search failed/i);
|
||||
expect(search).toHaveBeenCalledTimes(1);
|
||||
|
||||
// ...and must NOT retry on its own initiative. The original version of this test pressed
|
||||
// Escape immediately, cancelling the very timer that would have exposed the storm (#651 round
|
||||
// 5): a fresh `{ok:false}` object re-ran the effect, the success guard declined it, and
|
||||
// another request was scheduled 250ms later, forever.
|
||||
// ...and must NOT retry on its own initiative. Pressing Escape here would cancel the very
|
||||
// timer that exposes the storm (#651): a fresh `{ok:false}` object re-ran the effect, the
|
||||
// success guard declined it, and another request was scheduled 250ms later, forever.
|
||||
await new Promise((resolve) => setTimeout(resolve, 900));
|
||||
expect(search).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -467,11 +466,11 @@ describe('SearchPicker', () => {
|
||||
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
|
||||
// "There genuinely are no matches" is an answer; re-asking would be the round-3 defect.
|
||||
// "There genuinely are no matches" is an answer; re-asking would be the duplicate-request defect.
|
||||
expect(search).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('MEDIUM-3: a malformed 2xx body does not crash the picker, and is retryable', async () => {
|
||||
it('a malformed 2xx body does not crash the picker, and is retryable', async () => {
|
||||
// `client.ts` turns malformed JSON into `undefined` rather than rejecting, so a naive
|
||||
// `setResults(undefined)` throws on the next render reading `results.length`.
|
||||
const search = vi
|
||||
@@ -504,12 +503,12 @@ describe('SearchPicker', () => {
|
||||
{ label: 'a NaN id', payload: [{ id: Number.NaN, name: 'Not a number' }] },
|
||||
{ label: 'one bad element among good ones', payload: [{ id: 1, name: 'Alpha' }, null] },
|
||||
// The API binds `selectedId` as a 32-bit integer, so these render and commit happily and
|
||||
// then fail server-side — another adjacent shape (#651 round 7).
|
||||
// then fail server-side — another adjacent shape (#651).
|
||||
{ label: 'a fractional id', payload: [{ id: 1.5, name: 'Fractional' }] },
|
||||
{ label: 'an id above int32', payload: [{ id: 2_147_483_648, name: 'Too big' }] },
|
||||
{ label: 'an id below int32', payload: [{ id: -2_147_483_649, name: 'Too small' }] },
|
||||
{ label: 'an Infinity id', payload: [{ id: Number.POSITIVE_INFINITY, name: 'Infinite' }] }
|
||||
])('MEDIUM-3 (round 6): rejects $label rather than rendering or committing it', async ({ payload }) => {
|
||||
])('rejects $label rather than rendering or committing it', async ({ payload }) => {
|
||||
// `Array.isArray` checks the CONTAINER, not the CONTENTS: `[null]` passes it, reaches
|
||||
// `setResults`, and throws on `option.id` during render. A wrong-typed id would commit an
|
||||
// invalid value through `onSelect`.
|
||||
@@ -526,7 +525,7 @@ describe('SearchPicker', () => {
|
||||
|
||||
// Renders (does not throw) and offers nothing — a malformed list is not a partial answer.
|
||||
// And it must READ as a failure: "No matches" would tell the user the library genuinely has
|
||||
// nothing, with no hint that anything went wrong or that reopening retries (#651 round 7).
|
||||
// nothing, with no hint that anything went wrong or that reopening retries (#651).
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(/Search failed/i);
|
||||
expect(screen.queryByText(/No matches/)).not.toBeInTheDocument();
|
||||
expect(screen.queryAllByRole('option')).toHaveLength(0);
|
||||
@@ -539,7 +538,7 @@ describe('SearchPicker', () => {
|
||||
expect(await screen.findByRole('option', { name: 'Alpha' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('MEDIUM-3: a never-settling search stops loading instead of spinning forever', async () => {
|
||||
it('a never-settling search stops loading instead of spinning forever', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const search = vi.fn<(query: string) => Promise<SearchPickerOption[]>>(() => new Promise(() => {}));
|
||||
|
||||
@@ -43,10 +43,10 @@ const SEARCH_TIMEOUT_MS = 10_000;
|
||||
|
||||
// `Array.isArray` checks the CONTAINER, not the CONTENTS: `[null]` passes it, reaches `setResults`,
|
||||
// and throws on `option.id` during render; an element with a missing or wrong-typed `id`/`name`
|
||||
// yields a broken option or commits an invalid value (#651 round 6). Nothing between the network
|
||||
// yields a broken option or commits an invalid value (#651). Nothing between the network
|
||||
// and `onSelect` re-checks these, so validate every element before storing it.
|
||||
// The id must be what the API can actually BIND — see `isSelectionId`. This is one of several
|
||||
// ingresses for the same class; the predicate is shared so they cannot drift apart (#651 round 8).
|
||||
// ingresses for the same class; the predicate is shared so they cannot drift apart (#651).
|
||||
function isSearchPickerOption(value: unknown): value is SearchPickerOption {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return false;
|
||||
@@ -75,12 +75,12 @@ export function SearchPicker({
|
||||
const [results, setResults] = useState<SearchPickerOption[]>([]);
|
||||
// The PROVENANCE of `results`: which source and query produced them, and whether that attempt
|
||||
// succeeded. All three matter.
|
||||
// - source: results from another namespace are not stale, they are wrong (#651 round 4 HIGH-1);
|
||||
// - source: results from another namespace are not stale, they are wrong (#651);
|
||||
// - query: distinguishes an empty result set from "hasn't searched yet", so the no-matches
|
||||
// copy doesn't flash between a keystroke and its debounce;
|
||||
// - ok: a FAILED search must not be cached as a successful empty result, or the re-query
|
||||
// guard below turns a transient 500 into a permanent "No matches" that reopening,
|
||||
// blurring and refocusing can never retry (#651 round 4 MEDIUM-5).
|
||||
// blurring and refocusing can never retry (#651).
|
||||
const [resultsFor, setResultsFor] = useState<null | { ok: boolean; query: string; source: string }>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// -1 = no option highlighted (the ARIA combobox "virtual cursor" is parked).
|
||||
@@ -90,7 +90,7 @@ export function SearchPicker({
|
||||
// The last (source, query) an actual REQUEST was issued for — success or failure. A failed
|
||||
// attempt writes a fresh `resultsFor` object, which re-runs the effect; without this the success
|
||||
// guard would decline the cached failure and schedule another request 250ms later, forever
|
||||
// (#651 round 5 MEDIUM-2: a persistent outage became a request storm alternating "No matches"
|
||||
// (#651: a persistent outage became a request storm alternating "No matches"
|
||||
// and a spinner). Reopening or editing the query is what re-arms a retry — both explicit user
|
||||
// actions — so this is cleared there rather than on a timer.
|
||||
const attemptRef = useRef<null | string>(null);
|
||||
@@ -112,13 +112,13 @@ export function SearchPicker({
|
||||
|
||||
// Already holding SUCCESSFUL results for exactly this source and query — e.g. reopening after
|
||||
// Escape. Re-running the search would issue a duplicate request whose response resets the
|
||||
// highlight the user has since moved, leaving Enter doing nothing (#651 round 3, MEDIUM).
|
||||
// highlight the user has since moved, leaving Enter doing nothing (#651).
|
||||
if (resultsFor !== null && resultsFor.ok && resultsFor.source === source && resultsFor.query === trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already TRIED this exact (source, query) and it failed. Do not retry on our own initiative —
|
||||
// `rearm()` on reopen/edit is what allows another attempt (#651 round 5 MEDIUM-2).
|
||||
// `rearm()` on reopen/edit is what allows another attempt (#651).
|
||||
const attempt = `${source}\u0000${trimmed}`;
|
||||
if (attemptRef.current === attempt) {
|
||||
return;
|
||||
@@ -140,7 +140,7 @@ export function SearchPicker({
|
||||
attemptRef.current = attempt;
|
||||
// Bound the wait. `search` is a caller-supplied promise with no abort signal, so a request
|
||||
// that never settles would otherwise leave the picker spinning forever with no way back
|
||||
// (#651 round 5 MEDIUM-3). The underlying request is not cancelled — the seq guard already
|
||||
// (#651). The underlying request is not cancelled — the seq guard already
|
||||
// discards a late resolution — but the UI stops waiting on it.
|
||||
Promise.race([
|
||||
search(trimmed),
|
||||
@@ -154,7 +154,7 @@ export function SearchPicker({
|
||||
// `readJsonResponse` swallows a SyntaxError), and `setResults(undefined)` then throws
|
||||
// on the next render reading `results.length`. Treat anything that is not a well-formed
|
||||
// list of options as a FAILED attempt — not an authoritative empty answer — so it is
|
||||
// retryable rather than cached (#651 round 5 MEDIUM-3, round 6).
|
||||
// retryable rather than cached (#651).
|
||||
if (!Array.isArray(items) || !items.every(isSearchPickerOption)) {
|
||||
setResults([]);
|
||||
setResultsFor({ ok: false, query: trimmed, source });
|
||||
@@ -191,12 +191,12 @@ export function SearchPicker({
|
||||
// SmartCollection label — so they are not shown at all. Results from the same source but an older
|
||||
// query stay VISIBLE while the next response loads (hiding them flickers on every keystroke) but
|
||||
// become inert: typing "Al", highlighting Alpha, retyping "Be" and pressing Enter must not select
|
||||
// Alpha (#651 round 2 BLOCKER 2, round 4 HIGH-1).
|
||||
// Alpha (#651).
|
||||
const sameSource = resultsFor !== null && resultsFor.source === source;
|
||||
const resultsCurrent = sameSource && resultsFor.query === trimmedQuery;
|
||||
// A failed attempt (network error, timeout, or a malformed payload rejected wholesale) must NOT
|
||||
// read as an authoritative "no matches": the user would have no signal that anything went wrong,
|
||||
// nor that reopening retries (#651 round 7).
|
||||
// nor that reopening retries (#651).
|
||||
const searchFailed = open && !loading && resultsCurrent && !resultsFor.ok;
|
||||
const noMatches = open && !loading && results.length === 0 && resultsCurrent && resultsFor.ok;
|
||||
const listboxOpen = open && sameSource && results.length > 0;
|
||||
@@ -209,7 +209,7 @@ export function SearchPicker({
|
||||
const choose = (option: SearchPickerOption) => {
|
||||
// ONE gate for every commit path — keyboard, pointer, and anything added later. Gating the two
|
||||
// call sites individually is what let a stale option stay clickable after Enter was fixed
|
||||
// (#651 review round 3, HIGH): the class is "committing a result from a superseded query", not
|
||||
// (#651): the class is "committing a result from a superseded query", not
|
||||
// "pressing Enter".
|
||||
if (!resultsCurrent) {
|
||||
return;
|
||||
@@ -233,7 +233,7 @@ export function SearchPicker({
|
||||
|
||||
// Escape (or a selection) closes the popup while focus stays in the input, so ArrowDown has to
|
||||
// be able to REOPEN it. Without this the picker is stuck until the user blurs and refocuses,
|
||||
// because `onFocus` never fires again (#651 review round 2, BLOCKER 3).
|
||||
// because `onFocus` never fires again (#651).
|
||||
if (!open && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
||||
event.preventDefault();
|
||||
setOpen(true);
|
||||
@@ -317,8 +317,8 @@ export function SearchPicker({
|
||||
onChange={(event) => {
|
||||
setQuery(event.target.value);
|
||||
// The highlight described the OLD result set; drop it now rather than when the next
|
||||
// response happens to arrive (BLOCKER 2). Typing also reopens a popup Escape closed,
|
||||
// so the picker recovers without a blur (BLOCKER 3).
|
||||
// response happens to arrive (#651). Typing also reopens a popup Escape closed,
|
||||
// so the picker recovers without a blur (#651).
|
||||
setActiveIndex(-1);
|
||||
setOpen(true);
|
||||
rearm();
|
||||
|
||||
@@ -644,15 +644,15 @@ describe('CollectionsScreen', () => {
|
||||
|
||||
// A failed search must NOT fall into the min-query guidance branch: the user typed a valid
|
||||
// 2-character query and got a 500, so telling them to "type at least 2 characters" states
|
||||
// something false about what they just did. The error banner is the whole message (#685
|
||||
// review round 3 — that branch conflated "nothing searched yet" with "the last search failed").
|
||||
// something false about what they just did. The error banner is the whole message (#685 — that
|
||||
// branch conflated "nothing searched yet" with "the last search failed").
|
||||
expect(
|
||||
within(dialog).queryByText(`Type at least ${LIBRARY_PICKER_MIN_QUERY} characters to search.`)
|
||||
).not.toBeInTheDocument();
|
||||
// Nor "No results", which asserts a search that COMPLETED and found nothing. Both empty-state
|
||||
// messages are suppressed on error, leaving the alert banner as the whole message. Pinned
|
||||
// positively as well as negatively so a refactor cannot satisfy this by rendering nothing at
|
||||
// all (#685 review round 4).
|
||||
// all (#685).
|
||||
expect(within(dialog).queryByText('No results — try a search above.')).not.toBeInTheDocument();
|
||||
expect(within(dialog).getByRole('alert')).toHaveTextContent(/500/);
|
||||
});
|
||||
|
||||
@@ -81,13 +81,12 @@ const ADDABLE_TYPES = new Set<LibraryBrowseItem['mediaType']>(ADDABLE_TYPE_LIST)
|
||||
// ingress into `kinds` (the `all` branch of runSearch), and the truncation hint sums pre-filter
|
||||
// totalCounts against post-ADDABLE_TYPES rows. A non-addable kind here would overstate the hint
|
||||
// with every one of its rows dropped, and typing it as the whole mediaType union let that through
|
||||
// (#685 review round 3 — one of two ingress paths was enforced, which is not an invariant).
|
||||
// (#685 — one of two ingress paths was enforced, which is not an invariant).
|
||||
const DEFAULT_SEARCH_KINDS: AddableKind[] = ['Movie', 'TelevisionShow', 'Artist'];
|
||||
|
||||
// The single derivation every ingress into the searched kinds goes through. Named rather than
|
||||
// spelled inline at each use so the "one list, all ingresses" property is visible at a glance —
|
||||
// a fourth ingress is most likely to be written by copying one of the existing three (#685
|
||||
// review round 4).
|
||||
// a fourth ingress is most likely to be written by copying one of the existing three (#685).
|
||||
type AddableKind = (typeof ADDABLE_TYPE_LIST)[number];
|
||||
|
||||
type MediaKindFilter = 'all' | AddableKind;
|
||||
@@ -251,10 +250,10 @@ function AddItemsDialog({
|
||||
|
||||
// `searchLibraryBrowseItems` owns the min-query gate, the pageSize clamp, and the
|
||||
// raw-Lucene-vs-compiled-query choice (§3b: "the bound belongs to the helper, not the caller").
|
||||
// This screen used to keep a SECOND copy of the min-query check here — the #685 review proved
|
||||
// the two masked each other (deleting either one left the whole suite green, so the boundary
|
||||
// test pinned nothing) — so there is now exactly one gate, in the helper, and every caller
|
||||
// (blank form submit, kind-chip click) routes through it unconditionally.
|
||||
// A SECOND copy of the min-query check on this screen would mask the helper's: with both,
|
||||
// deleting either one leaves the whole suite green and the boundary test pins nothing (#685).
|
||||
// So there is exactly one gate, in the helper, and every caller (blank form submit, kind-chip
|
||||
// click) routes through it unconditionally.
|
||||
//
|
||||
// #740: `runSearch` is reachable twice concurrently — a form submit fans out over
|
||||
// DEFAULT_SEARCH_KINDS while a kind-chip click issues a single-kind search — so the two guards
|
||||
@@ -428,8 +427,7 @@ function AddItemsDialog({
|
||||
</p>
|
||||
{/* Conditionally mounted, unlike the aria-live hint below — deliberately, not by oversight:
|
||||
`role="alert"` is the one live-region role screen readers reliably announce on INSERTION,
|
||||
so mounting it together with its content is right here and would be wrong there (#685
|
||||
review round 4 flagged the divergence as unexplained). */}
|
||||
so mounting it together with its content is right here and would be wrong there (#685). */}
|
||||
{error && (
|
||||
<span className="ctv-field-error" role="alert">
|
||||
{error}
|
||||
@@ -438,14 +436,14 @@ function AddItemsDialog({
|
||||
{submittedQuery !== '' && (
|
||||
// Names the query the CURRENT results (or the empty-results/"no matches" state) belong
|
||||
// to — never the live input, so a submitted-then-cleared query, or a selection made under
|
||||
// an earlier query, is never shown without saying which search produced it (#685 review).
|
||||
// an earlier query, is never shown without saying which search produced it (#685).
|
||||
<p className="ctv-collections-picker-note">
|
||||
Results for “{submittedQuery}”
|
||||
</p>
|
||||
)}
|
||||
{/* The live region is mounted UNCONDITIONALLY with the condition inside it: most screen
|
||||
readers only announce mutations to a region that already existed, so creating the region
|
||||
and its text in the same commit announces nothing (#685 review round 3). */}
|
||||
and its text in the same commit announces nothing (#685). */}
|
||||
<p aria-live="polite" className="ctv-collections-picker-note">
|
||||
{totalMatches > results.length ? `Showing ${results.length} of ${totalMatches} matches — narrow your search.` : ''}
|
||||
</p>
|
||||
@@ -453,17 +451,16 @@ function AddItemsDialog({
|
||||
{results.length === 0 && submittedQuery === '' && !error ? (
|
||||
// Keyed to submittedQuery, not the live input: without it, backspacing the query back
|
||||
// below the min-query length after a successful search wiped the rendered rows AND their
|
||||
// checkmarks while `selected` (and the Add button's count) still held them (#685 review
|
||||
// finding 1) — and, separately, submitting a blank/cleared query after a real selection
|
||||
// must still say the selection persists rather than rendering as if nothing was ever
|
||||
// searched (#685 second review).
|
||||
// checkmarks while `selected` (and the Add button's count) still held them (#685) — and,
|
||||
// separately, submitting a blank/cleared query after a real selection must still say the
|
||||
// selection persists rather than rendering as if nothing was ever searched (#685).
|
||||
<div className="ctv-collections-picker-empty">
|
||||
Type at least {LIBRARY_PICKER_MIN_QUERY} characters to search.
|
||||
</div>
|
||||
) : results.length === 0 && !searching && !error ? (
|
||||
// Also suppressed on `error`: "No results" asserts a search that COMPLETED and found
|
||||
// nothing, which is false when the request failed. The role="alert" banner above is the
|
||||
// whole message in that state (#685 review round 4).
|
||||
// whole message in that state (#685).
|
||||
<div className="ctv-collections-picker-empty">No results — try a search above.</div>
|
||||
) : (
|
||||
results.map((item) => {
|
||||
|
||||
@@ -219,7 +219,7 @@ function draftFromPreset(preset: FillerPreset & Draft): Draft {
|
||||
allowWatermarks: preset.allowWatermarks,
|
||||
// Every entity-reference id is normalized on the way in: a value the API cannot bind is treated
|
||||
// as ABSENT so it surfaces as "no selection" (Save disabled) rather than as an apparently-valid
|
||||
// selection that fails on submit (#651 round 8).
|
||||
// selection that fails on submit (#651).
|
||||
collectionId: selectionIdOrNull(preset.collectionId),
|
||||
collectionType: preset.collectionType,
|
||||
count: preset.count,
|
||||
@@ -665,7 +665,7 @@ function FillerEditor({ mode }: { mode: { id: number; kind: 'edit' } | { kind: '
|
||||
const pickerSelectedInList =
|
||||
pickerSelectedId != null &&
|
||||
pickerItems.some((item) => activeConfig?.itemId(item) === pickerSelectedId);
|
||||
// Out-of-list injection (round-3 review F2): the non-searchable picker loads only ONE bounded
|
||||
// Out-of-list injection (#644 follow-up F2): the non-searchable picker loads only ONE bounded
|
||||
// page (LIBRARY_BROWSE_PAGE_CAP), so a preset whose stored id sits outside that page would
|
||||
// otherwise render as "(none)" while the draft still holds the id.
|
||||
const pickerOptions = [
|
||||
@@ -854,7 +854,7 @@ function FillerEditor({ mode }: { mode: { id: number; kind: 'edit' } | { kind: '
|
||||
// draft id is null, so the picker renders its search input and the label is unused.)
|
||||
// Adding a render-time check back would be defensive, not a fix, and an unreachable
|
||||
// guard is an untested one — so if a path ever appears, add the guard WITH the test
|
||||
// that reaches it. #651 review round 3.
|
||||
// that reaches it. #651.
|
||||
selectedName={selectedLabel?.name ?? null}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -358,7 +358,7 @@ describe('PlaybackTroubleshootingScreen', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('promotes to running on the start POST 200 and settles a fast-completing clip (Codex race fix)', async () => {
|
||||
it('promotes to running on the start POST 200 and settles a fast-completing clip', async () => {
|
||||
setLocation('/app/troubleshooting/playback?mediaItem=5');
|
||||
// PrepareTroubleshootingPlayback calls statusStore.Reset() server-side and the start POST returns
|
||||
// 200 only after segments exist, so any settled status seen afterward is THIS session's. A short
|
||||
|
||||
@@ -473,7 +473,7 @@ export function PlaybackTroubleshootingScreen() {
|
||||
}
|
||||
setPlayerSrc(src);
|
||||
setPlayToken((current) => current + 1);
|
||||
// Fast-completion race (Codex MAJOR): the POST returns 200 only after segments exist, so a short
|
||||
// Fast-completion race: the POST returns 200 only after segments exist, so a short
|
||||
// clip can settle between polls; applyStatus discards settled states while 'starting', turning a
|
||||
// genuine completion into a false 30s startup-failure. PrepareTroubleshootingPlayback calls
|
||||
// statusStore.Reset() server-side, so ANY status observed after a 200 belongs to THIS session —
|
||||
|
||||
@@ -321,10 +321,10 @@ describe('PlaylistsScreen', () => {
|
||||
expect(browseCalls()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('HIGH-2: options from the PREVIOUS item type are not selectable while the new type loads', async () => {
|
||||
it('options from the PREVIOUS item type are not selectable while the new type loads', async () => {
|
||||
// Same defect as RerunCollectionsScreen, second screen: switching a playlist item's type on a
|
||||
// slow connection must not leave the old namespace's options on offer under the new label
|
||||
// (#651 round 4 HIGH-2).
|
||||
// (#651).
|
||||
const smartRelease: { resolve: (() => void) | null } = { resolve: null };
|
||||
|
||||
const fetchMock = mockApi();
|
||||
@@ -358,12 +358,12 @@ describe('PlaylistsScreen', () => {
|
||||
expect(within(picker()).queryByRole('option', { name: 'Favorites' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ---- #651 round 9: making the stated invariant actually true on this screen ----
|
||||
// ---- #651: making the stated invariant actually true on this screen ----
|
||||
|
||||
it.each([
|
||||
{ id: 1.5, label: 'a fractional mediaItemId' },
|
||||
{ id: 2_147_483_648, label: 'a mediaItemId above int32' }
|
||||
])('round 9: $label drops its NAME too, blocks Save, and cannot reach a PUT', async ({ id }) => {
|
||||
])('$label drops its NAME too, blocks Save, and cannot reach a PUT', async ({ id }) => {
|
||||
// The invariant claimed in spa-conventions — "surfaces as no selection, Save disabled, zero
|
||||
// writes reachable" — was false here in all three respects: the name survived the dropped id
|
||||
// (so the row read "Cool Movie" while the draft held null), Save had no selection check, and
|
||||
@@ -407,9 +407,9 @@ describe('PlaylistsScreen', () => {
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('round 9: an unbindable playlist GROUP id is not offered and cannot be posted', async () => {
|
||||
it('an unbindable playlist GROUP id is not offered and cannot be posted', async () => {
|
||||
// `playlistGroupId` is seeded from the wire and submitted as an entity reference — the same
|
||||
// class, on the same screen, which the round-8 sweep missed.
|
||||
// class, on the same screen.
|
||||
const fetchMock = mockApi({
|
||||
onRequest: (url, method) =>
|
||||
url === '/api/v1/playlists/groups' && method === 'GET'
|
||||
|
||||
@@ -238,7 +238,7 @@ function draftFromItem(item: PlaylistItem): DraftItem {
|
||||
// NOTE the name is cleared with the id below whenever `selectionIdOrNull` rejects one: keeping a
|
||||
// label for an id we refused to store makes the row claim "Blade Runner" while the draft holds
|
||||
// null — two contradictory statements about the same item, and no "no selection" signal
|
||||
// (#651 round 9).
|
||||
// (#651).
|
||||
|
||||
switch (source) {
|
||||
case 'collection':
|
||||
@@ -365,7 +365,7 @@ function AddPlaylistDialog({
|
||||
}) {
|
||||
// The group id is seeded from the wire and submitted as an entity reference, so it is a selection
|
||||
// id by `selectionId.ts`'s own definition and gets the same boundary treatment — otherwise "every
|
||||
// path by which an id from the wire becomes editor state" is not literally true (#651 round 9).
|
||||
// path by which an id from the wire becomes editor state" is not literally true (#651).
|
||||
const bindableGroups = groups.filter((group) => isSelectionId(group.id));
|
||||
const [groupId, setGroupId] = useState(() => (bindableGroups[0] ? String(bindableGroups[0].id) : ''));
|
||||
const [name, setName] = useState('');
|
||||
@@ -425,7 +425,7 @@ function PlaylistEditor({ playlistId, onBack, onSaved }: { playlistId: number; o
|
||||
const [items, setItems] = useState<DraftItem[]>([]);
|
||||
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
||||
// Options carry the TYPE that produced them — the previous type's options must not stay
|
||||
// selectable under the new type's label while its load is in flight (#651 round 4 HIGH-2).
|
||||
// selectable under the new type's label while its load is in flight (#651).
|
||||
const [pickerFor, setPickerFor] = useState<{ items: PickerOption[]; type: CollectionType } | null>(null);
|
||||
const [pickerError, setPickerError] = useState<string | null>(null);
|
||||
const [pickerHint, setPickerHint] = useState<PickerLoadResult['hint']>('none');
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { RerunCollectionsScreen } from './RerunCollectionsScreen';
|
||||
|
||||
// A single-record GET response. It ALWAYS carries an ETag, because a real server does and because
|
||||
// the editor now refuses to open without one (#651 round 6): mocks that omitted the header were
|
||||
// the editor refuses to open without one (#651): mocks that omitted the header were
|
||||
// silently exercising a force-write path that must not exist. Use this for every detail GET.
|
||||
function detailResponse(body: unknown, etag = '"v1"'): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
@@ -361,7 +361,7 @@ describe('RerunCollectionsScreen', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the "List may be incomplete" badge when the rerun-collections load does not complete (round-3 review)', async () => {
|
||||
it('renders the "List may be incomplete" badge when the rerun-collections load does not complete', async () => {
|
||||
let callCount = 0;
|
||||
|
||||
vi.spyOn(window, 'fetch').mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
@@ -387,7 +387,7 @@ describe('RerunCollectionsScreen', () => {
|
||||
expect(callCount).toBe(2);
|
||||
});
|
||||
|
||||
it('screen-level: an older overlapping refresh must NOT overwrite a newer one (seqRef guard, round-3 review)', async () => {
|
||||
it('screen-level: an older overlapping refresh must NOT overwrite a newer one (seqRef guard)', async () => {
|
||||
const itemA = { ...rerunCollections[0], id: 101, name: 'Item A' };
|
||||
const itemB = { ...rerunCollections[0], id: 102, name: 'Item B' };
|
||||
const staleAfterA = { ...rerunCollections[0], id: 201, name: 'Stale After A' };
|
||||
@@ -479,8 +479,7 @@ describe('RerunCollectionsScreen', () => {
|
||||
// The list opened a Collection (id 5, "Favorites"); by the time the detail GET lands the record
|
||||
// has been changed to RemoteStream, whose response carries the #671 null selection. The draft
|
||||
// comes wholly from that response, so the Collection id cannot leak into the RemoteStream
|
||||
// namespace — the defect this originally caught, now prevented structurally rather than by a
|
||||
// merge rule (#651 round 3 HIGH, round 5 redesign).
|
||||
// namespace — the defect this catches, prevented structurally rather than by a merge rule (#651).
|
||||
let detailCalls = 0;
|
||||
mockApi({
|
||||
list: [{ ...storedSelection, collectionType: 'Collection', selectedId: 5, selectedName: 'Favorites' }],
|
||||
@@ -506,11 +505,11 @@ describe('RerunCollectionsScreen', () => {
|
||||
expect(screen.getByLabelText('Remote Stream search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('HIGH-2: options from the PREVIOUS type are not selectable while the new type loads', async () => {
|
||||
it('options from the PREVIOUS type are not selectable while the new type loads', async () => {
|
||||
// Slow connection: switching Collection -> SmartCollection clears the selection immediately but
|
||||
// the smart-collections request is still in flight. The old Collection options must not remain
|
||||
// on offer under the SmartCollection label — picking one would write a Collection id into the
|
||||
// SmartCollection namespace (#651 round 4 HIGH-2).
|
||||
// SmartCollection namespace (#651).
|
||||
const smartRelease: { resolve: (() => void) | null } = { resolve: null };
|
||||
|
||||
const fetchMock = mockApi({ list: [] });
|
||||
@@ -551,13 +550,12 @@ describe('RerunCollectionsScreen', () => {
|
||||
});
|
||||
|
||||
|
||||
// ---- #651 review round 5: the editor initializes ONCE, from the detail GET ----
|
||||
// ---- #651: the editor initializes ONCE, from the detail GET ----
|
||||
//
|
||||
// Every reconciliation test above was deleted with the machinery it exercised. Rounds 2-4 each
|
||||
// produced a HIGH finding in the layer that merged a late detail response into a draft the user
|
||||
// was already editing; round 5 removed the race instead of refereeing it. What is asserted now is
|
||||
// the ABSENCE of that surface: there is no editable draft until the record lands, so there is
|
||||
// nothing to reconcile, nothing to pin, and no way to pair one record's id with another's ETag.
|
||||
// Every version of the layer that merged a late detail response into a draft the user was already
|
||||
// editing produced a HIGH finding, so the race is removed rather than refereed. What is asserted
|
||||
// now is the ABSENCE of that surface: there is no editable draft until the record lands, so there
|
||||
// is nothing to reconcile, nothing to pin, and no way to pair one record's id with another's ETag.
|
||||
|
||||
it('renders no editable form until the detail GET lands — there is no draft to reconcile', async () => {
|
||||
const detailRelease: { resolve: (() => void) | null } = { resolve: null };
|
||||
@@ -621,7 +619,7 @@ describe('RerunCollectionsScreen', () => {
|
||||
});
|
||||
|
||||
it('Save always carries If-Match — a draft cannot exist without its ETag', async () => {
|
||||
// The round-4 false-conflict hole: a path that produced a draft but left `etagRef` null made the
|
||||
// The false-conflict hole: a path that produces a draft but leaves `etagRef` null makes the
|
||||
// next PUT a FORCE-WRITE. Writing the ETag in the same callback that sets the draft makes that
|
||||
// unreachable by construction; this pins it.
|
||||
const fetchMock = mockApi({
|
||||
@@ -653,11 +651,10 @@ describe('RerunCollectionsScreen', () => {
|
||||
});
|
||||
|
||||
it('412 -> Reload: the form is ABSENT while pending, and a dirty selection is discarded even when the server sends none', async () => {
|
||||
// Replaces the deleted "conflict Reload with a null server selection" regression. Both halves
|
||||
// matter and the previous version had neither: its second GET resolved immediately (so it never
|
||||
// observed a pending Reload) and returned a non-null selection (so removing `setDraft(null)`
|
||||
// could leave it green). Here the reload is HELD OPEN and returns `selectedId: null` — the #671
|
||||
// shape that round 3 showed could resurrect the user's dirty id over a collaborator's change.
|
||||
// Both halves matter: a second GET that resolves immediately never observes a pending Reload,
|
||||
// and one returning a non-null selection leaves removing `setDraft(null)` green. Here the
|
||||
// reload is HELD OPEN and returns `selectedId: null` — the #671 shape that can resurrect the
|
||||
// user's dirty id over a collaborator's change.
|
||||
const reloadRelease: { resolve: (() => void) | null } = { resolve: null };
|
||||
let detailCalls = 0;
|
||||
|
||||
@@ -726,9 +723,9 @@ describe('RerunCollectionsScreen', () => {
|
||||
});
|
||||
|
||||
it('a #671 unnamed selection disables Save with a visible reason — it never silently saves a guess', async () => {
|
||||
// Replaces the F1 family. Under initialize-once there is no list-supplied id to preserve (there
|
||||
// never was one in production), so the honest outcome is: no selection, Save disabled, and the
|
||||
// badge says why. The defect to avoid is a SILENT one — saving something the server didn't say.
|
||||
// Under initialize-once there is no list-supplied id to preserve (there never was one in
|
||||
// production), so the honest outcome is: no selection, Save disabled, and the badge says why.
|
||||
// The defect to avoid is a SILENT one — saving something the server didn't say.
|
||||
mockApi({
|
||||
list: [{ ...storedSelection, collectionType: 'RemoteStream' }],
|
||||
onRequest: (url, method) =>
|
||||
@@ -748,13 +745,13 @@ describe('RerunCollectionsScreen', () => {
|
||||
// Absent, empty and whitespace-only are not three cases — they are one: "no usable concurrency
|
||||
// token". Validating only `null` let an empty ETag through the gate into an editable draft, where
|
||||
// `updateRerunCollection`'s `ifMatch ? … : undefined` dropped it as falsy and force-wrote over a
|
||||
// collaborator (#651 round 7 HIGH). Each shape must be unable to produce a draft at all.
|
||||
// collaborator (#651). Each shape must be unable to produce a draft at all.
|
||||
it.each([
|
||||
{ etag: undefined, label: 'no ETag header at all' },
|
||||
{ etag: '', label: 'an empty ETag' },
|
||||
{ etag: ' ', label: 'a whitespace-only ETag' },
|
||||
{ etag: '\t\n', label: 'a tab/newline ETag' }
|
||||
])('HIGH: $label yields an error, never an editable draft and never a PUT', async ({ etag }) => {
|
||||
])('$label yields an error, never an editable draft and never a PUT', async ({ etag }) => {
|
||||
const fetchMock = mockApi({
|
||||
list: [storedSelection],
|
||||
onRequest: (url, method) =>
|
||||
@@ -786,7 +783,8 @@ describe('RerunCollectionsScreen', () => {
|
||||
it('a padded ETag is TRIMMED and sent as If-Match, not dropped', async () => {
|
||||
// The other half of "one class": a usable token surrounded by whitespace must still be USED.
|
||||
// NOTE this is a regression guard, not a demonstration of a defect — `Headers` strips outer
|
||||
// HTTP whitespace before the app sees it, so this passes pre-#651-round-7 too. It exists so a
|
||||
// HTTP whitespace before the app sees it, so this passes with or without `usableEtag`'s trim.
|
||||
// It exists so a
|
||||
// future `usableEtag` that rejects (rather than trims) padding cannot land silently.
|
||||
const fetchMock = mockApi({
|
||||
list: [storedSelection],
|
||||
@@ -863,7 +861,7 @@ describe('RerunCollectionsScreen', () => {
|
||||
});
|
||||
|
||||
// The deadline CANCELS the work rather than merely abandoning the wait: otherwise each Retry
|
||||
// would stack another live connection (#651 round 7).
|
||||
// would stack another live connection (#651).
|
||||
expect(state.aborts).toBe(1);
|
||||
expect(state.inFlight).toBe(0);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
@@ -911,8 +909,8 @@ describe('RerunCollectionsScreen', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Guards the ABORT/RACE COMPOSITION, not a round-6 defect: round 6's race guard already covered
|
||||
// the ordinary case. What this fails is an abort-ONLY implementation whose fetch ignores its
|
||||
// Guards the ABORT/RACE COMPOSITION, not the race alone: the race guard already covers the
|
||||
// ordinary case. What this fails is an abort-ONLY implementation whose fetch ignores its
|
||||
// signal — which is exactly the mutation it was written for, and why the stub below deliberately
|
||||
// ignores `init.signal`.
|
||||
it('a detail read that settles LATE, after the deadline, cannot revive the editor', async () => {
|
||||
@@ -962,16 +960,16 @@ describe('RerunCollectionsScreen', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- #651 round 8: the int32 boundary, on EVERY ingress ----
|
||||
// ---- #651: the int32 boundary, on EVERY ingress ----
|
||||
//
|
||||
// Round 7 put the predicate inside the search picker's option validator — the site the defect was
|
||||
// found at — leaving the two other doors into `draft.selectedId` open. These pin them.
|
||||
// Putting the predicate inside the search picker's option validator — the site the defect was
|
||||
// found at — leaves the two other doors into `draft.selectedId` open. These pin them.
|
||||
|
||||
it.each([
|
||||
{ id: 1.5, label: 'a fractional selectedId' },
|
||||
{ id: 2_147_483_648, label: 'a selectedId above int32' },
|
||||
{ id: -2_147_483_649, label: 'a selectedId below int32' }
|
||||
])('round 8: $label in the DETAIL response cannot enable Save or reach a PUT', async ({ id }) => {
|
||||
])('$label in the DETAIL response cannot enable Save or reach a PUT', async ({ id }) => {
|
||||
mockApi({
|
||||
list: [storedSelection],
|
||||
onRequest: (url, method) =>
|
||||
@@ -997,7 +995,7 @@ describe('RerunCollectionsScreen', () => {
|
||||
it.each([
|
||||
{ id: 1.5, label: 'a fractional id' },
|
||||
{ id: 2_147_483_648, label: 'an id above int32' }
|
||||
])('round 8: a LIST-BACKED option with $label is not offered, so it cannot be selected or saved', async ({ id }) => {
|
||||
])('a LIST-BACKED option with $label is not offered, so it cannot be selected or saved', async ({ id }) => {
|
||||
const fetchMock = mockApi({
|
||||
list: [],
|
||||
onRequest: (url, method) =>
|
||||
|
||||
@@ -109,8 +109,7 @@ function orderOptionsWithCurrent(type: RerunCollectionType, current: PlaybackOrd
|
||||
}
|
||||
|
||||
// One ingress for list-backed options. An id the API cannot bind is DROPPED rather than offered:
|
||||
// an unselectable option is better than one that fails on save (#651 round 8 — the boundary, not
|
||||
// the site).
|
||||
// an unselectable option is better than one that fails on save (#651 — the boundary, not the site).
|
||||
function toPickerOptions(list: Array<{ id: unknown; name?: null | string }>): PickerOption[] {
|
||||
return list.flatMap((entry) =>
|
||||
isSelectionId(entry.id) ? [{ id: entry.id, name: entry.name ?? `#${entry.id}` }] : []
|
||||
@@ -236,7 +235,7 @@ const LOAD_TIMEOUT_MS = 15_000;
|
||||
// whitespace-only header are not three cases — they are one: "no usable token". Treating only
|
||||
// `null` as absent let an empty ETag through the fail-closed gate and into an editable draft, where
|
||||
// `updateRerunCollection`'s `ifMatch ? … : undefined` then dropped it as falsy and force-wrote over
|
||||
// a collaborator (#651 round 7 HIGH). Returning the TRIMMED token means `etagRef` can only ever
|
||||
// a collaborator (#651). Returning the TRIMMED token means `etagRef` can only ever
|
||||
// hold something that will actually be sent.
|
||||
function usableEtag(etag: string | null | undefined): string | null {
|
||||
const trimmed = etag?.trim() ?? '';
|
||||
@@ -256,11 +255,11 @@ function draftFromRerun(rerun: RerunCollection): Draft {
|
||||
};
|
||||
}
|
||||
|
||||
// NOTE (#651 review round 5): there is deliberately NO draft-reconciliation machinery here — no
|
||||
// NOTE (#651): there is deliberately NO draft-reconciliation machinery here — no
|
||||
// touched-field tracking, no field-wise hydrate, no conflict predicate. Every one of those existed
|
||||
// to merge a late detail response into a draft the user was already editing, and that reconciliation
|
||||
// surface produced a HIGH finding in three consecutive review rounds, including three cross-user
|
||||
// lost updates. The race is removed rather than refereed: the draft is initialized EXACTLY ONCE,
|
||||
// surface produced a HIGH finding in every attempted form, including three cross-user lost
|
||||
// updates. The race is removed rather than refereed: the draft is initialized EXACTLY ONCE,
|
||||
// from the detail GET, and the form does not exist until it lands.
|
||||
//
|
||||
// Seeding from the list row (which is what created the race) could never have helped anyway:
|
||||
@@ -299,9 +298,9 @@ function RerunCollectionEditor({
|
||||
);
|
||||
// Options carry the TYPE that produced them. Keeping a bare array meant the previous type's
|
||||
// options stayed on screen under the new type's label while its load was in flight, so a slow
|
||||
// connection let the user store e.g. a Collection id in the SmartCollection namespace (#651
|
||||
// round 4 HIGH-2). Same rule as the search picker's `source`: an id is only meaningful inside
|
||||
// the namespace it came from.
|
||||
// connection let the user store e.g. a Collection id in the SmartCollection namespace (#651).
|
||||
// Same rule as the search picker's `source`: an id is only meaningful inside the namespace it
|
||||
// came from.
|
||||
const [pickerFor, setPickerFor] = useState<{ items: PickerOption[]; type: RerunCollectionType } | null>(null);
|
||||
const [pickerError, setPickerError] = useState<string | null>(null);
|
||||
const [pickerHint, setPickerHint] = useState<PickerLoadResult['hint']>('none');
|
||||
@@ -333,7 +332,7 @@ function RerunCollectionEditor({
|
||||
let timeoutId = 0;
|
||||
// The deadline ABORTS the request rather than just abandoning the wait: an abandoned GET stays
|
||||
// in flight, so each Retry would stack another outstanding connection and eventually delay the
|
||||
// very retry meant to recover (#651 round 7). Cleared on settlement and on unmount.
|
||||
// very retry meant to recover (#651). Cleared on settlement and on unmount.
|
||||
const controller = new AbortController();
|
||||
let timedOutFlag = false;
|
||||
// Two jobs, deliberately BOTH: `abort` cancels the work so retries cannot stack connections,
|
||||
@@ -359,7 +358,7 @@ function RerunCollectionEditor({
|
||||
// here, alongside the ETag" is not the same as "a draft implies a token": the header can be
|
||||
// absent, empty, or whitespace, and each of those ends with `updateRerunCollection` sending
|
||||
// no `If-Match` at all — a silent force-write. An editor that cannot save safely must not
|
||||
// exist (#651 rounds 6 and 7).
|
||||
// exist (#651).
|
||||
const token = usableEtag(meta.etag);
|
||||
if (token === null) {
|
||||
setLoadError(
|
||||
@@ -466,7 +465,7 @@ function RerunCollectionEditor({
|
||||
return (
|
||||
<div className="ctv-collections">
|
||||
<div className="ctv-channels-actionbar">
|
||||
{/* A way out while loading: a hung request must never be a dead end (#651 round 6). */}
|
||||
{/* A way out while loading: a hung request must never be a dead end (#651). */}
|
||||
<Button onClick={onBack} size="sm" startIcon={<ArrowLeft aria-hidden="true" size={14} />} variant="ghost">
|
||||
All rerun collections
|
||||
</Button>
|
||||
@@ -548,8 +547,8 @@ function RerunCollectionEditor({
|
||||
setConflictOpen(false);
|
||||
setSaveError(null);
|
||||
// Dropping the draft IS the replace: the form unmounts, so there is no window in which the user
|
||||
// can edit something the incoming record is about to overwrite (#651 round 4 MEDIUM-4, now
|
||||
// structural rather than guarded).
|
||||
// can edit something the incoming record is about to overwrite (#651 — structural rather than
|
||||
// guarded).
|
||||
setDraft(null);
|
||||
setLoadKey((key) => key + 1);
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ function loadAllRerunCollections(signal?: AbortSignal): Promise<RerunCollection[
|
||||
// log it so a stalled/incomplete rerun-collections load doesn't silently render as whole.
|
||||
// A superseded/aborted load (retry, or a newer boot supersedes this one — see the abort
|
||||
// below) also returns `complete: false`; that's expected, not a defect, so don't warn on it
|
||||
// (round-3 review F3).
|
||||
// (#644 follow-up F3).
|
||||
console.warn('SchedulesScreen: rerun-collections picker load did not complete; some items may be missing');
|
||||
}
|
||||
return items;
|
||||
@@ -427,10 +427,10 @@ export function SchedulesScreen() {
|
||||
// the draft is still dirty, so without the gate a user could discard-to-open the editor, flip
|
||||
// shuffle, and have the in-flight items PUT resolve AFTER the shuffle reload — clobbering the
|
||||
// freshly normalized draft with the pre-shuffle response. Blocking Edit while saving closes that
|
||||
// race (Codex review, PR #242). Delete is gated on `saving` for the same class of race: it is the
|
||||
// race (PR #242). Delete is gated on `saving` for the same class of race: it is the
|
||||
// only schedule-switch path (via applySwitch) that guardedSwitch's `saving` guard didn't already
|
||||
// cover, so deleting mid-save could switch schedules and let the stale items PUT clobber the next
|
||||
// schedule's draft (Codex re-review, PR #242).
|
||||
// schedule's draft (PR #242).
|
||||
const openEditForm = () => {
|
||||
if (dirtyRef.current && !window.confirm(DIRTY_PROMPT)) {
|
||||
return;
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('resolveTrackedSourceFiles (#819)', () => {
|
||||
});
|
||||
|
||||
it('reports UNTRACKED paths from a SEPARATE query, so a narrowed index cannot hide behind them', () => {
|
||||
// #819 round 6: every other comparison in the guard is between two things derived from
|
||||
// #819: every other comparison in the guard is between two things derived from
|
||||
// `tracked`, so a filter applied to `tracked` cancels out of all of them. `others` comes from a
|
||||
// different git query and is what makes that filter visible — narrowing `tracked` adds nothing
|
||||
// here, so the walk finds a file in neither list.
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface TrackedSources {
|
||||
* Paths under `src/` that git reports as UNTRACKED (ignored ones included), from a SEPARATE
|
||||
* `ls-files --others` query.
|
||||
*
|
||||
* This exists so the consumer can catch a narrowing of `tracked` itself (#819 round 6). Every
|
||||
* This exists so the consumer can catch a narrowing of `tracked` itself (#819). Every
|
||||
* comparison the guard makes is between two things derived from `tracked`, so a filter applied
|
||||
* HERE shrinks both sides and cancels — real files leave the population with the whole suite
|
||||
* green, and the guard goes blind rather than merely quiet. `tracked ∪ others` is what is on
|
||||
|
||||
Reference in New Issue
Block a user