Skip to content

test(runtime): pin the reply-staging ordering in handleEvent (#1366) - #1370

Merged
lilyshen0722 merged 1 commit into
mainfrom
test/handleevent-staging-ordering
Sep 1, 2026
Merged

lilyshen0722 merged 1 commit into
mainfrom
test/handleevent-staging-ordering

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Closes #1366.

@sprint-review proved the gap rather than asserting it: moving commitStagedReply above the post in handleEvent re-introduces #1344 with all 30 tests green. staging.test.ts's six cases drive staging.ts through an injected Map and pin the primitives well — stage, resolve, commit, prune, shape-guard. None pins the call site, and the call site is where the ordering lives.

This adds the one assertion neither existing suite makes, through the mock harness agent-do.delivery-nonce.test.ts already established (mock ../src/cap, construct the DO over a fake DurableObjectState, drive it with alarm()).

Three cases:

  • a failed post leaves the stage intactstaged:event-1 still holds the reply, processedEventIds is unset, ackEvent was never called;
  • the redelivery replays it without re-running the model — post throws on the first alarm(), succeeds on the second, and runTurn is called once across both. That is hosted runtime: postMessage failure after a successful turn re-runs the turn #1344's contract stated directly: one paid turn across two deliveries;
  • a NO_REPLY turn posts nothing and still clears its stage — the branch that skips the post must not leak an orphan.

Mutation control

node 22, clean worktree at 374f4c7c, vitest run.

tree result
main 30 passed (30)
main + this file 33 passed (33)
mutated (commit moved above the post), without this file 30 passed (30) — the exclusion arm
mutated, with this file 2 failedexpected "spy" to be called 1 times, but got 2 times

The discriminating assertion is the runTurn call count, which is the paid resource #1344 is about. Source restored afterwards (git diff empty on src/), tsc --noEmit exits 0.

Not addressed here, deliberately

#1366's two smaller notes are untouched: the STAGE_TTL_MS justification is a claim about kernel redelivery cadence asserted in a worker comment with no reader, and pruneStaged does a full prefix list on every staged turn. Both are about staging.ts, not the call site, and neither is what the issue's title names.

NOT VERIFIED: same caveat as the issue — this runs under vitest with an injected storage object, not under workerd/wrangler, so real Durable Object get/put/delete/list semantics remain unexercised. The harness gains a list stub because the existing one predates pruneStaged.

🤖 Generated with Claude Code

sprint-review proved on #1366 that moving `commitStagedReply` above the
post re-introduces #1344 with the whole suite green: staging.ts's six
cases drive the primitives through an injected Map and pin them well,
but nothing exercised the CALL SITE, which is where the order lives.

Three cases through `alarm()`, using the mock harness the delivery-nonce
suite already established:

- a failed post leaves `staged:<id>` intact, the event unprocessed and
  unacked;
- the redelivery replays that stage and `runTurn` is called ONCE across
  both deliveries — #1344's whole contract, one paid turn;
- a NO_REPLY turn posts nothing and still clears its stage.

Mutation control, node 22, at 374f4c7: baseline 30/30 -> 33/33 with
this file. With the commit moved above the post, 2 of the 3 go red
("expected spy to be called 1 times, but got 2") and all 30 pre-existing
tests stay green — the exclusion arm #1366 measured. Source restored,
`tsc --noEmit` clean.

The storage harness gains a `list` stub; the existing one predates
`pruneStaged`, which lists on every stage.

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

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gate on #1370 at 6182763a (base=main, 1 file, +101/-0, mergeable_state: clean). PASS — this closes the gap in #1366, and I verified that by mutation rather than by reading it. This PR fixes a finding I filed, so the only thing worth reporting is whether the new tests actually discriminate. They do.

Baseline at this head: 33 passed (8 files).

The decisive check. The mutation that motivated #1366 — moving commitStagedReply above postMessage, which passed 30/30 green on main — now fails. Anchor asserted to match exactly once before applying, tree restored after:

M1  commit above post          Tests  2 failed | 31 passed
    × a failed post leaves the stage intact and the event unprocessed
    × the redelivery replays the staged reply without re-running the model

That is the gap closed, measured the same way it was opened.

The three cases are discriminating, not overlapping. The third case survives M1, so I gave it its own mutations rather than accepting it as decorative:

mutation tests red
M1 — commitStagedReply above the post 1 & 2
M2 — drop the NO_REPLY guard (if (true)) 3 only
M3 — never commit (void commitStagedReply) 2 & 3

Every case reds on at least one mutation, and the three mutations red different subsets — so the cases pin distinct behaviour rather than restating each other. git status --porcelain empty afterwards.

The design is the right one. Driving the DO through alarm() twice — post throws, then succeeds — and asserting runTurn was called once across both deliveries tests the actual #1344 invariant (one paid model turn per event) rather than a proxy for it. Asserting processedEventIds is unset and ackEvent was not called on the failed pass pins the redelivery precondition too, which is what makes the second alarm() meaningful instead of incidental. Reusing the agent-do.delivery-nonce.test.ts harness keeps this consistent with the file that already mocks ../src/cap.

The list addition to the fake storage is necessary, not incidental — pruneStaged lists on every stage, and the pre-existing harness predates it. The inline comment says exactly that, which is the right amount of explanation.

Nothing blocking. Nothing non-blocking either — I looked for over-specification (assertions that would red on a harmless refactor) and did not find any: every assertion is on observable behaviour, none on call counts of internal helpers or on message shape beyond the one toHaveBeenLastCalledWith that pins the replayed text.

NOT VERIFIED: still no workerd/wrangler run — the DO storage contract is exercised through a fake Map-backed object, so real get/put/delete/list semantics remain untested here, same caveat I carried on #1346 and #1366. I did not run tsc --noEmit. The two smaller notes from #1366 are untouched by this PR and stay open: the STAGE_TTL_MS justification asserts a kernel redelivery bound ("retires after 3 redeliveries") that nothing in this repo checks, and pruneStaged does a full prefix list on every staged turn. Neither blocks this; flagging so they aren't assumed closed along with the ordering.

@lilyshen0722
lilyshen0722 merged commit f729619 into main Sep 1, 2026
11 checks passed
@lilyshen0722
lilyshen0722 deleted the test/handleevent-staging-ordering branch September 1, 2026 09:54
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.

Reply-staging ordering in handleEvent is untested: moving commitStagedReply above the post restores #1344 with 30/30 green

1 participant