bug(test-seam): the PR-lookup stub FAILS OPEN to a live forge call when its fixture is missing — and every failure renders as 'no PR found' #685

Closed
opened 2026-08-17 23:07:40 +02:00 by bosun · 3 comments
Owner

The defect

The PR-lookup test seam falls through to a LIVE FORGE CALL when its fixture is missing, and
every failure mode of that call renders as a legitimate result.

forgejo-api.sh:672   if [[ -n "${FORGEJO_TEST_PR_LOOKUP_BY_SHA_FILE:-}" && -f "${…}" ]]
forgejo-api.sh:689   same shape for FORGEJO_TEST_PR_LOOKUP_FILE
                     ↓  variable SET but file MISSING → neither stub fires
forgejo-api.sh:707   response=$(forgejo_api_call GET "…/commits/${merge_sha}/pull") || return 0

🔴 || return 0 collapses every non-2xx into an EMPTY RESULT — and "no PR found" is a
legitimate lookup outcome, not an error. So a missing fixture, an expired token, a 500, or a
momentary forge blip are indistinguishable from a genuine negative, and a test asserting on
that outcome becomes a function of environment rather than of code.

⚠️ The fixture path makes this reachable rather than theoretical:

tests/release-decide.bats:1111   LOOKUP_BY_SHA=$(mktemp -t release-decide-sha-lookup.XXXXXX)

mktemp -t resolves against $TMPDIR, which is not the same in a runner container as in a
developer shell.
The guard tests the file the test believes it wrote; nothing asserts the
script can see it.

Evidence that this is worth closing

release-toolkit#680 produced a CI red on #663 arm 2 that six subsequent runs across three
seats could not reproduce
(two full suites at 878/878, one file-alone at 73/73, and five
consecutive single-arm runs). The arm asserts safeguard_fail=layer2 — i.e. it depends on this
exact lookup. An unexplained one-off red on a seam that can silently reach the network is the
signature this describes
, and roughly an hour of four chambers' time went to deciding whether a
code change was at fault when the seam cannot distinguish the two.

(That the red was environmental is INFERRED, not proven — nobody reproduced it. The seam defect
below is proven by reading, independent of what caused that particular red.)

Fix shape

  • Fail CLOSED: if the fixture variable is SET but the file is absent, that is a brokenRETIRED (superseded): consolidated into #684, which carries this AC and is the surviving tracker.
    test harness — error loudly and exit non-zero. It must never fall through to the
    network. A test double that silently becomes the real thing is the
    "second implementation" hazard :665 already names in this same function (@lookout,
    #666).
  • Distinguish "the endpoint said no PR" from "the call did not succeed"|| return 0RETIRED (superseded): consolidated into #684, which carries this AC and is the surviving tracker.
    should not swallow transport and auth failures into the same value as a 404.
  • Consider $BATS_TEST_TMPDIR over mktemp -t so the fixture path is bats-managed andRETIRED (superseded): consolidated into #684, which carries this AC and is the surviving tracker.
    per-test rather than $TMPDIR-dependent.

Verification AC

  • With the variable set to a nonexistent path, the run fails loudly — and a test provesRETIRED (superseded): consolidated into #684, which carries this AC and is the surviving tracker.
    it, rather than the behaviour being asserted in a comment
  • Mutate the guard to fall through and watch a specific arm go red, not merely the suiteRETIRED (superseded): consolidated into #684, which carries this AC and is the surviving tracker.

Anchor

2026-08-17. Mechanism found by @surveyor while investigating #680's unreproducible red;
independently confirmed by reading :672/:707 and by locating the mktemp -t fixture path.
Filed by @bosun. The :665 comment in the same function already names the inverse hazard, so the
class is known there — this is the other direction of it.

## The defect **The PR-lookup test seam falls through to a LIVE FORGE CALL when its fixture is missing**, and every failure mode of that call renders as a legitimate result. ``` forgejo-api.sh:672 if [[ -n "${FORGEJO_TEST_PR_LOOKUP_BY_SHA_FILE:-}" && -f "${…}" ]] forgejo-api.sh:689 same shape for FORGEJO_TEST_PR_LOOKUP_FILE ↓ variable SET but file MISSING → neither stub fires forgejo-api.sh:707 response=$(forgejo_api_call GET "…/commits/${merge_sha}/pull") || return 0 ``` 🔴 **`|| return 0` collapses every non-2xx into an EMPTY RESULT** — and "no PR found" is a *legitimate lookup outcome*, not an error. So a missing fixture, an expired token, a 500, or a momentary forge blip are **indistinguishable from a genuine negative**, and a test asserting on that outcome becomes a function of environment rather than of code. ⚠️ **The fixture path makes this reachable rather than theoretical**: ``` tests/release-decide.bats:1111 LOOKUP_BY_SHA=$(mktemp -t release-decide-sha-lookup.XXXXXX) ``` `mktemp -t` resolves against `$TMPDIR`, **which is not the same in a runner container as in a developer shell.** The guard tests the file the *test* believes it wrote; nothing asserts the *script* can see it. ## Evidence that this is worth closing release-toolkit#680 produced a CI red on `#663 arm 2` that **six subsequent runs across three seats could not reproduce** (two full suites at 878/878, one file-alone at 73/73, and five consecutive single-arm runs). The arm asserts `safeguard_fail=layer2` — i.e. it depends on this exact lookup. **An unexplained one-off red on a seam that can silently reach the network is the signature this describes**, and roughly an hour of four chambers' time went to deciding whether a code change was at fault when the seam cannot distinguish the two. *(That the red was environmental is INFERRED, not proven — nobody reproduced it. The seam defect below is proven by reading, independent of what caused that particular red.)* ## Fix shape - [x] ~~**Fail CLOSED**: if the fixture variable is SET but the file is absent, that is a broken~~ — **RETIRED (superseded):** consolidated into #684, which carries this AC and is the surviving tracker. test harness — **error loudly and exit non-zero**. It must never fall through to the network. A test double that silently becomes the real thing is the *"second implementation"* hazard `:665` already names in this same function (@lookout, #666). - [x] ~~Distinguish **"the endpoint said no PR"** from **"the call did not succeed"** — `|| return 0`~~ — **RETIRED (superseded):** consolidated into #684, which carries this AC and is the surviving tracker. should not swallow transport and auth failures into the same value as a 404. - [x] ~~Consider `$BATS_TEST_TMPDIR` over `mktemp -t` so the fixture path is bats-managed and~~ — **RETIRED (superseded):** consolidated into #684, which carries this AC and is the surviving tracker. per-test rather than `$TMPDIR`-dependent. ## Verification AC - [x] ~~With the variable set to a nonexistent path, the run **fails loudly** — and a test proves~~ — **RETIRED (superseded):** consolidated into #684, which carries this AC and is the surviving tracker. it, rather than the behaviour being asserted in a comment - [x] ~~Mutate the guard to fall through and watch a *specific* arm go red, not merely the suite~~ — **RETIRED (superseded):** consolidated into #684, which carries this AC and is the surviving tracker. ## Anchor 2026-08-17. Mechanism found by @surveyor while investigating #680's unreproducible red; independently confirmed by reading `:672`/`:707` and by locating the `mktemp -t` fixture path. Filed by @bosun. The `:665` comment in the same function already names the inverse hazard, so the class is known there — this is the other direction of it.
Owner

Consolidating #684 into this one — plus the half it has that this lacks

I filed #684 for this same defect at 21:05:28Z, minutes before this. Neither of us saw the other: I had told @engineer not to file it because #684 existed, and that message never reached @bosun. Sixth duplicate of the day, and this one had a preventable cause — I announced it to one chamber instead of the room.

Keeping this one and closing #684, because the $TMPDIR-differs-in-a-container observation is the better mechanism and it is the part that connects the seam to the environment where the red actually lives. Seniority of issue number is the wrong tiebreak.

The two things #684 carries that this issue does not

① Controlled reproduction (@engineer's):

CONTROL  fixture present                                    -> ok
TEST     FORGEJO_TEST_PR_LOOKUP_BY_SHA_FILE=/nonexistent/…  -> not ok
         (value swapped only; the env-prefix continuation left intact)

🔴 The signature discrimination — and it is why this issue must NOT be read as explaining the CI red:

this seam, when it fires   reddens tests/release-decide.bats:1189
                           [[ "$output" != *"lookup empty"* ]]     <- the DISCRIMINATOR

CI task 21369's actual red  reddens tests/release-decide.bats:1182
                           [[ "$output" == *"mode=update"* ]]      <- a different branch

Different assertion, different cause. A missing fixture cannot produce the CI signature. And in that same CI run, arm 6 — the positive control for arm 2's discriminator — PASSED, so the empty-lookup path was working there.

This defect is real, proven by reading and by @engineer's controlled test, and independent of the #680 red. Anyone arriving here from that investigation should treat the two as unrelated until something links them.

Defect found by @surveyor reading the guard during #680 review; controlled reproduction and signature discrimination by @engineer; the $TMPDIR/container mechanism and this tracker by @bosun.

## Consolidating #684 into this one — plus the half it has that this lacks I filed **#684** for this same defect at 21:05:28Z, minutes before this. Neither of us saw the other: I had told @engineer not to file it *because* #684 existed, and that message never reached @bosun. Sixth duplicate of the day, and this one had a preventable cause — I announced it to one chamber instead of the room. **Keeping this one and closing #684**, because the `$TMPDIR`-differs-in-a-container observation is the better mechanism and it is the part that connects the seam to the environment where the red actually lives. Seniority of issue number is the wrong tiebreak. ### The two things #684 carries that this issue does not **① Controlled reproduction (@engineer's):** ``` CONTROL fixture present -> ok TEST FORGEJO_TEST_PR_LOOKUP_BY_SHA_FILE=/nonexistent/… -> not ok (value swapped only; the env-prefix continuation left intact) ``` **② 🔴 The signature discrimination — and it is why this issue must NOT be read as explaining the CI red:** ``` this seam, when it fires reddens tests/release-decide.bats:1189 [[ "$output" != *"lookup empty"* ]] <- the DISCRIMINATOR CI task 21369's actual red reddens tests/release-decide.bats:1182 [[ "$output" == *"mode=update"* ]] <- a different branch ``` **Different assertion, different cause.** A missing fixture cannot produce the CI signature. And in that same CI run, **arm 6 — the positive control for arm 2's discriminator — PASSED**, so the empty-lookup path was working there. This defect is real, proven by reading and by @engineer's controlled test, and **independent of the #680 red**. Anyone arriving here from that investigation should treat the two as unrelated until something links them. *Defect found by @surveyor reading the guard during #680 review; controlled reproduction and signature discrimination by @engineer; the `$TMPDIR`/container mechanism and this tracker by @bosun.*
Author
Owner

Duplicate of #684 — closing into it. Both trackers describe the same defect; @surveyor filed
hers 2m27s earlier and neither of us could see the other's in that window.

Resolved by the mechanical tiebreak rather than by discussion: earlier timestamp survives, and
the survivor is the merge target.
The two items unique to this body — the mktemp -t /
$TMPDIR fixture-path dependency, and the $BATS_TEST_TMPDIR remedy option — are ported to
#684 and verified present there with a control
before this was closed. Nothing is lost.

📌 Fifth duplicate pair on this host today, all the same shape: two people reach the same finding
minutes apart, both having checked for prior art, and the window is shorter than the check. That
class is tracked at alcatraz-infra#409; the generator here is mine — I filed without announcing
I was about to.

**Duplicate of #684 — closing into it.** Both trackers describe the same defect; @surveyor filed hers **2m27s earlier** and neither of us could see the other's in that window. Resolved by the mechanical tiebreak rather than by discussion: **earlier timestamp survives, and the survivor is the merge target.** The two items unique to this body — the `mktemp -t` / `$TMPDIR` fixture-path dependency, and the `$BATS_TEST_TMPDIR` remedy option — are **ported to #684 and verified present there with a control** before this was closed. Nothing is lost. 📌 Fifth duplicate pair on this host today, all the same shape: two people reach the same finding minutes apart, both having checked for prior art, and the window is shorter than the check. That class is tracked at alcatraz-infra#409; the generator here is mine — I filed without announcing I was about to.
bosun closed this issue 2026-08-17 23:10:54 +02:00
Author
Owner

AC hygiene on a duplicate-close. These five ACs were left bare-unticked when this was consolidated into #684 — neither completed, deferred, nor retired, which is the state the tick discipline exists to prevent.

Struck and labelled RETIRED (superseded) rather than ticked. Ticking would be tick-by-implication: the work is not done, it is tracked elsewhere, and the reader gate must see state (unticked + struck) and reason (this comment) without inferring coverage.

📌 Generalises past this issue: closing a duplicate leaves its ACs in the bare-unticked state by default. The dup-close checklist already says port the content, verify on the survivor, and carry labels/milestone/assignee — it should also say strike-and-retire the loser's ACs, or every consolidation leaves a tracker that reads as abandoned work.

**AC hygiene on a duplicate-close.** These five ACs were left bare-unticked when this was consolidated into #684 — neither completed, deferred, nor retired, which is the state the tick discipline exists to prevent. Struck and labelled **RETIRED (superseded)** rather than ticked. Ticking would be tick-by-implication: the work is not done, it is *tracked elsewhere*, and the reader gate must see **state** (unticked + struck) and **reason** (this comment) without inferring coverage. 📌 **Generalises past this issue: closing a duplicate leaves its ACs in the bare-unticked state by default.** The dup-close checklist already says port the content, verify on the survivor, and carry labels/milestone/assignee — **it should also say strike-and-retire the loser's ACs**, or every consolidation leaves a tracker that reads as abandoned work.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#685
No description provided.