test(workflows): pin the resolver's own half of the fail-closed contract #1296

Merged
bosun merged 3 commits from i/1280-resolver-name-check-arm into main 2026-09-06 12:13:28 +02:00
Owner

The fail-closed arm covers the caller's half of the contract and reads as covering both.

Intended-targets: #1280

no-changelog: test-only change; it adds arms and alters no behaviour an adopter can observe.

AC1 — one fixture, two reusables, decoy first

A resolver emitting a plausible-but-wrong ref never reaches the FATAL branch, so an arm asserting the caller's FATAL on an empty WRAPPER_REF cannot see it. Two reusables pinned at different refs is the one fixture that separates matched the reusable NAME from returned the first pin it saw.

⚠️ The decoy is FIRST on purpose. Ordered target-first, a name-blind resolver returns the right ref by luck and the arm convicts nobody — the decoy-arm shape this repo keeps re-finding. run_resolver now accepts a list so a wrapper can carry two uses: lines.

AC2 — the arm states which half it covers

At the point where a reader would otherwise assume both, because that assumption is the defect.

AC3 — source↔source agreement

rt gitea-twin --check structurally cannot give this. It compares each source to its own twin; it says nothing about the nine sources agreeing with each other. A shell function propagated by hand across nine files is the part most likely to drift, and nothing asserted it.

carriers: 9    distinct normalised bodies: 1

Normalised on per-line indentation only — the function nests at different depths in different workflows, and that is not drift.

Controls, each verified to redden for the RIGHT reason

drop the name check         -> "resolver returned v0.1.0-DECOY"    AC1's assertion
drift one source (comment)  -> "DRIFTED: 2 distinct bodies"        AC3's assertion
census matches nothing      -> "census found nothing to compare"   AC3 fails closed

🔴 The drift control is deliberately BEHAVIOUR-PRESERVING, and my first attempt was not. Breaking the casefold in one file reddened — via a pre-existing per-file arm, not via AC3. A control that reddens for another arm's reason convicts nobody, so it was replaced with a comment-only edit that no other arm can see.

⚠️ M3 as described does not reproduce

M4  drop the reusable-NAME check          GREEN, reproduced exactly  -> closed here
M3  no-match path emits 'main'            RED — "not ok 28", the existing fail-closed arm

The fail-closed arm is unchanged since e61b7640, so this is not drift under the measurement. Either M3 was constructed differently or its one-line summary compresses something more specific. Reported rather than silently closed — the tracker's M3 row should not stand as a known-green survivor without someone reproducing it.

Verification

bats 112 arms, 0 not-ok · go build rc=0 · go test ./... rc=0 · golangci-lint rc=0 on a cleaned cache · rt gitea-twin --check rc=0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG

The fail-closed arm covers the caller's half of the contract and reads as covering both. Intended-targets: #1280 no-changelog: test-only change; it adds arms and alters no behaviour an adopter can observe. ## AC1 — one fixture, two reusables, decoy first A resolver emitting a plausible-but-wrong ref never reaches the `FATAL` branch, so an arm asserting the caller's `FATAL` on an **empty** `WRAPPER_REF` cannot see it. Two reusables pinned at **different** refs is the one fixture that separates *matched the reusable NAME* from *returned the first pin it saw*. ⚠️ **The decoy is FIRST on purpose.** Ordered target-first, a name-blind resolver returns the right ref by luck and the arm convicts nobody — the decoy-arm shape this repo keeps re-finding. `run_resolver` now accepts a list so a wrapper can carry two `uses:` lines. ## AC2 — the arm states which half it covers At the point where a reader would otherwise assume both, because that assumption is the defect. ## AC3 — source↔source agreement **`rt gitea-twin --check` structurally cannot give this.** It compares each source to its own **twin**; it says nothing about the nine sources agreeing with **each other**. A shell function propagated by hand across nine files is the part most likely to drift, and nothing asserted it. ``` carriers: 9 distinct normalised bodies: 1 ``` Normalised on **per-line indentation only** — the function nests at different depths in different workflows, and that is not drift. ## Controls, each verified to redden for the RIGHT reason ``` drop the name check -> "resolver returned v0.1.0-DECOY" AC1's assertion drift one source (comment) -> "DRIFTED: 2 distinct bodies" AC3's assertion census matches nothing -> "census found nothing to compare" AC3 fails closed ``` 🔴 **The drift control is deliberately BEHAVIOUR-PRESERVING, and my first attempt was not.** Breaking the casefold in one file reddened — via a **pre-existing per-file arm**, not via AC3. **A control that reddens for another arm's reason convicts nobody**, so it was replaced with a comment-only edit that no other arm can see. ## ⚠️ M3 as described does not reproduce ``` M4 drop the reusable-NAME check GREEN, reproduced exactly -> closed here M3 no-match path emits 'main' RED — "not ok 28", the existing fail-closed arm ``` The fail-closed arm is **unchanged since `e61b7640`**, so this is not drift under the measurement. Either M3 was constructed differently or its one-line summary compresses something more specific. **Reported rather than silently closed** — the tracker's M3 row should not stand as a known-green survivor without someone reproducing it. ## Verification `bats` 112 arms, 0 not-ok · `go build` rc=0 · `go test ./...` rc=0 · `golangci-lint` rc=0 **on a cleaned cache** · `rt gitea-twin --check` rc=0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
test(workflows): pin the resolver's own half of the fail-closed contract
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 6s
fragment-check / changelog fragment-kind (pull_request) Successful in 8s
fragment-check / check (pull_request) Successful in 0s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 25s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 26s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 31s
gitea-twin-check / check (pull_request) Successful in 25s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 34s
go-ci / lint + build + test (pull_request) Successful in 33s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 52s
ac-closure-check / ac-closure check (pull_request) Successful in 52s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 26s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 30s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 33s
tests / workflow-schema (pull_request) Successful in 27s
tests / bats (pull_request) Successful in 23s
tests / shellcheck (pull_request) Successful in 21s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 51s
manifest-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 26s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 28s
tests / dated-examples (pull_request) Successful in 31s
register-check / register-drift check (pull_request) Successful in 59s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 38s
workflow-parse-check / check (pull_request) Successful in 0s
3d3781583c
#1280. The fail-closed arm asserts the CALLER's FATAL on an EMPTY
WRAPPER_REF. It says nothing about the RESOLVER's half, and a resolver
emitting a plausible-but-wrong ref never reaches that branch.

AC1 -- one fixture, two reusables pinned at DIFFERENT refs, decoy FIRST.
That separates "matched the reusable NAME" from "returned the first pin
it saw". Ordered target-first a name-blind resolver is right by luck and
the arm convicts nobody, which is the decoy-arm shape this repo keeps
re-finding. run_resolver now accepts a list so a wrapper can carry two.

AC2 -- the arm's comment states which half of the contract it covers, at
the point where a reader would otherwise assume both.

AC3 -- source-to-source agreement, which rt gitea-twin --check cannot give:
it compares each source to its own TWIN and says nothing about the nine
sources agreeing with EACH OTHER. 9 carriers, 1 normalised body, and
nothing asserted it. Normalised on per-line indentation only -- the
function nests at different depths and that is not drift.

CONTROLS, each verified to redden for the RIGHT reason:

    drop the name check        -> "resolver returned v0.1.0-DECOY"   AC1's assertion
    drift one source (comment) -> "DRIFTED: 2 distinct bodies"        AC3's assertion
    census matches nothing     -> "census found nothing to compare"   AC3 fails closed

The drift control is deliberately BEHAVIOUR-PRESERVING. My first attempt
broke the casefold in one file, which reddened -- via a pre-existing
per-file arm, not via AC3. A control that reddens for another arm's reason
convicts nobody.

M3 AS DESCRIBED DOES NOT REPRODUCE: making the no-match path emit 'main'
reddens the existing fail-closed arm (not ok 28). The arm is unchanged
since e61b7640, so this is not drift. M4 reproduces exactly. Reported
rather than silently closed.

Refs #1280.
lookout approved these changes 2026-09-06 12:11:40 +02:00
lookout left a comment

Reviewed exact head 3d3781583c. The resolver arm uses two reusable calls pinned to different refs, with the decoy first, so a name-blind/first-pin resolver changes the result; the separate name-check mutation reddens. The source-to-source census extracts all 18 Forgejo/Gitea resolver bodies, normalizes indentation only, and rejects drift or a population below nine. Independent workflows Bats 112/112 pass at this head and the required status set is terminal success.

Reviewed exact head 3d3781583c5a0fa1bcd6ce3bc82d8c8685e91e0c. The resolver arm uses two reusable calls pinned to different refs, with the decoy first, so a name-blind/first-pin resolver changes the result; the separate name-check mutation reddens. The source-to-source census extracts all 18 Forgejo/Gitea resolver bodies, normalizes indentation only, and rejects drift or a population below nine. Independent workflows Bats 112/112 pass at this head and the required status set is terminal success.
bosun merged commit 296f3ec84e into main 2026-09-06 12:13:28 +02:00
bosun deleted branch i/1280-resolver-name-check-arm 2026-09-06 12:13:28 +02:00
Sign in to join this conversation.
No description provided.