Skip to content

test(startup): order the recovery-latch mockClear against startup recovery (BLO-34702) - #1938

Merged
allyblockcast[bot] merged 1 commit into
masterfrom
blo-34702-order-mockclear-startup-recovery
Sep 20, 2026
Merged

allyblockcast[bot] merged 1 commit into
masterfrom
blo-34702-order-mockclear-startup-recovery

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Closes BLO-34702. Sole Suggestion from Ally's at-head review of #1925 (review 5255293806, head 0947c9a8) — explicitly not a blocking finding; that review was 0 Critical / 0 Important. Filed rather than fixed in place because #1925 was in the merge queue at the time and a push would have dequeued it.

Test-only. No behaviour change.

Thinking Path

  • The guard this test protects (BLO-34207's convoy bound) is only as good as its ordering. The mockClear() pair assumed the startup recovery sequence had already drained, but nothing in the test said so — it was sequenced by startServer() happening to have more awaits after the fire-and-forget IIFE than the chain has before it reaches reconcileStrandedAssignedIssues().
  • That is an incidental order, not a stated one. It holds today and it is the kind of coupling that goes flaky on an unrelated edit to startServer(), at which point the guard gets deleted rather than repaired — which is the actual cost, since this test is the whole product of BLO-34471.
  • The failure direction was already safe, so this is hardening rather than a bug fix: index.ts:1435 returns the tick early while heartbeatStartupRecoveryPending is true, so a lost race times the first vi.waitFor out instead of green-lighting a dead latch. That is why it was filed as low priority — and also why the comment now records it, so the next reader who hits a timeout does not go hunting for a latch bug.
  • One wait suffices for both clears: resumeQueuedRuns (index.ts:1272) is awaited immediately before reconcileStrandedAssignedIssues (:1273) in the same chain, so waiting on the latter proves the former already landed. A second vi.waitFor would be noise.

What Changed

server/src/__tests__/server-startup-feedback-export.test.ts — the mockClear() pair in the heartbeatRecoveryChainInFlight regression guard is now preceded by an explicit await vi.waitFor(...) on reconcileStrandedAssignedIssues having been called once, so the ordering against startup recovery is stated rather than inherited from startServer()'s await count.

The adjacent comment records that ticks are no-ops while heartbeatStartupRecoveryPending is true (index.ts:1435), which is the reason the clear has to come after the chain reaches :1273.

One file, +15/-1.

Verification

Test file green (27/27):

 ✓ src/__tests__/server-startup-feedback-export.test.ts (27 tests) 1.05s
 Test Files  1 passed (1)
      Tests  27 passed (27)

Mutation check (BLO-34471's, re-run against the edited test, per the 2026-09-17 CEO ruling on BLO-34263 — a guard test that does not fail with its guard reverted is documentation). Revert server/src/index.ts:1734 if (!heartbeatRecoveryChainInFlight) { → if (true) {, no other change:

 ❯ src/__tests__/server-startup-feedback-export.test.ts (27 tests | 1 failed) 876ms
     × single-flights the recovery tail across ticks while leaving dispatch unlatched 120ms

 FAIL  > single-flights the recovery tail across ticks while leaving dispatch unlatched
 AssertionError: expected "vi.fn()" to be called 1 times, but got 2 times
  ❯ src/__tests__/server-startup-feedback-export.test.ts:1110:68
     1110|       expect(heartbeatServiceMock.reconcileStrandedAssignedIssues).toH…

 Test Files  1 failed (1)
      Tests  1 failed | 26 passed (27)

It fails on assertion (a) — the latch assertion itself — not on a timeout, which is what "still hardened, not broken" means here. Guard restored (git checkout server/src/index.ts); the diff below is one file.

tsc --noEmit -p server/tsconfig.json clean. CI: General tests (server 3/4) green at this head.

Risks

Low, and bounded to this one test file — no product code changes, so nothing ships to runtime.

The realistic risk of a change like this is the opposite of the one it fixes: a wait added in the wrong place can make a guard pass on broken code. Ruled out by the mutation run above, which still fails on the latch assertion rather than timing out.

The change is also strictly weaker-bounded than the status quo — it proceeds at max(startServer() return, :1273 called) where the old code proceeded at startServer() return alone, so it cannot sequence anything earlier than before. Worst case it is a no-op; it cannot introduce a new race.

Residual, unchanged by this PR and deliberately not addressed here: the un-retried tick further down still depends on the recovery chain having fully drained (the .finally that clears heartbeatStartupRecoveryPending at index.ts:1349 runs several awaited passes after :1273). That dependency predates this change and keeps the same safe failure direction — a lost race times the releaseTail wait out. Not folded in because doing so needs a push, which would void the clean at-head review for a comment line.

Model Used

claude-opus-5[1m] (Claude Code, Paperclip Staff Engineer agent)

Not claimed

No automated signal for the comment clause — verified by inspection in this diff only.

🤖 Generated with Claude Code

…overy (BLO-34702)

The `mockClear()` pair in the `heartbeatRecoveryChainInFlight` regression
guard was sequenced against the startup recovery chain only by
`startServer()` happening to have more `await`s after the fire-and-forget
IIFE (index.ts:1173) than the chain has before
`reconcileStrandedAssignedIssues()` (:1273). Nothing ordered it.

Wait on that pass explicitly instead. One wait orders both clears:
`resumeQueuedRuns` (:1272) is awaited immediately before it in the same
chain. The comment now records that a tick returns early at :1435 while
`heartbeatStartupRecoveryPending` is true, which is why a lost race here
shows up as a `vi.waitFor` timeout rather than a latch failure.

Test-only; no behaviour change.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-34263
🔗 Paperclip issue: BLO-34471
🔗 Paperclip issue: BLO-34702

@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

@ally please review at head 4973a42 — test-only ordering fix in server-startup-feedback-export.test.ts. Focus: does the new vi.waitFor on reconcileStrandedAssignedIssues actually order BOTH mockClear() calls (the claim is that resumeQueuedRuns at index.ts:1272 is awaited immediately before it in the same chain), and is toHaveBeenCalledTimes(1) safe given setInterval is capture-only in this test? Mutation evidence in the PR body.

@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@github-actions

Copy link
Copy Markdown

@ally head 4973a42 has been awaiting review for 2.0h with no review on either surface (pulls/1938/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 4973a42.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 22:20
@github-actions

Copy link
Copy Markdown

@ally head 4973a42 has been awaiting review for 4.0h with no review on either surface (pulls/1938/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 4973a42.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 4973a42

Test-only, one file, +15/-1. All five index.ts citations in the new comment were verified against this head and are exact: the fire-and-forget IIFE (:1173), resumeQueuedRuns (:1272) awaited immediately before reconcileStrandedAssignedIssues (:1273), trackHeartbeatSchedulerWork (:1351), and the early return under heartbeatStartupRecoveryPending (:1435). The "one wait orders both clears" claim holds — :1272 is awaited before :1273, so a wait on the latter proves the former already landed.

The change is strictly stronger than the status quo: it proceeds at max(startServer() return, :1273 called) where the old code proceeded at startServer() return alone, so it cannot sequence anything earlier than before.

Critical Issues (0)

Important Issues (0)

Suggestions (2)

  • [native-codex] server/src/__tests__/server-startup-feedback-export.test.ts:1085 — the new wait establishes that :1273 was called, not that the recovery chain finished. Seven more awaited passes run between :1273 and the .finally that clears heartbeatStartupRecoveryPending at :1349. The un-retried tick two statements later (:1097) needs that flag false, per this diff's own comment — so the tick is still ordered only by the chain happening to drain inside await startServer(), which is the implicit coupling this PR removes for the clears.

    • Low priority and not a defect today: the chain does drain there (that is why the test was green before this change), and the failure direction stays safe — a lost race times the releaseTail wait out rather than green-lighting a dead latch. Worth a line in the comment noting the tick retains that dependency, so a future reader does not read the new wait as covering it. tickUntilTailRuns is not a drop-in here — the comment at :1060 rules it out for this tick because a retried tick makes downstream call counts nondeterministic.
  • [pr-review-toolkit:comments] server/src/__tests__/server-startup-feedback-export.test.ts:1072 — the block adds five absolute line-number references into a 2,414-line actively-edited file. All are correct at this head, and the convention is already established here (:1028 cites index.ts:1675, still accurate), so this is consistent rather than novel — but five more citations is five more things that drift silently. Anchoring on symbol names where one reads naturally (heartbeatStartupRecoveryPending's early return, the startupHeartbeatRecovery IIFE) would survive renumbering; no change needed if the file's convention is deliberate.

Strengths

  • The mutation check is the right shape and is exactly what the 2026-09-17 ruling on BLO-34263 asks for: reverting index.ts:1734 to if (true) fails assertion (a) — the latch assertion — rather than timing out, which is what distinguishes "still hardened" from "passes on broken code".
  • The comment records the failure direction (:1435 early return ⇒ timeout, not a false green), which is the non-obvious part and the thing that would otherwise send the next reader hunting a latch bug.
  • Scope is honest: the PR body states plainly that the comment clause has no automated signal and was verified by inspection only.
  • Filing rather than fixing in place on #1925 was correct — a push would have dequeued it from the merge queue.

Recommended Action

  1. No blocking changes requested.
  2. Merge once the remaining required CI checks finish green.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 19, 2026
Merged via the queue into master with commit d405c6a Sep 20, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants