fix(876): sweep session narrative out of hooks, workflows, scripts, tests and code comments; grow the detector to the process corpus (#906)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 20s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 22s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m10s
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 20s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 22s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m10s
This commit was merged in pull request #906.
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