From e308f399926e6ded61ff6fc94bd73e418b02903d Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Thu, 17 Sep 2026 22:22:37 -0700 Subject: [PATCH] test(web): heldMs bounds in server-boundary-records are timer-deterministic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'a grouped boundary that errors still records' failed CI on #3526 with heldMs 7.28 against a >= 10 floor: a 5 ms timer (the error) and a 20 ms timer (the sibling) raced on a loaded runner, and the hold is the gap between them. B now settles a fixed 30 ms AFTER A has failed (a deferred A resolves before it throws), so the hold's lower bound is a single timer's — a timer never fires early — and the floor is 25. Same shape for 'order=together': B is released 40 ms after A has settled, not 40 ms after the stream started; floors 35 (held) and 40 (B's duration); A's duration is asserted shorter than its own hold rather than under a wall-clock constant. Co-authored-by: Claude via Cursor --- .../server/server-boundary-records.spec.tsx | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/web/test/server/server-boundary-records.spec.tsx b/packages/web/test/server/server-boundary-records.spec.tsx index 393ffcb88..f3e922992 100644 --- a/packages/web/test/server/server-boundary-records.spec.tsx +++ b/packages/web/test/server/server-boundary-records.spec.tsx @@ -310,7 +310,16 @@ describe("the other outcomes", () => { describe(" groups: heldMs", () => { test("order=together: the early boundary's record waits for the reveal and measures the hold", async () => { const seen = records(); + const aDone = deferred(); const b = deferred(); + function SlotA() { + const data = createMemo(async () => { + await delay(5); + aDone.resolve(); + return "A"; + }); + return
{data()}
; + } function SlotB() { const data = createMemo(async () => b.promise); return
{data()}
; @@ -319,7 +328,7 @@ describe(" groups: heldMs", () => { return ( a}> - + b}> @@ -329,6 +338,10 @@ describe(" groups: heldMs", () => { } const done = stream(() => ); // A has settled; the group holds its swap for B — and holds its record. + // The hold is measured from A's settle, so B is released a fixed 40 ms + // AFTER A settled (not after the stream started): the lower bound below + // is the timer's, not a race between two timers on a loaded runner. + await aDone.promise; await delay(40); expect(seen).toHaveLength(0); b.resolve("B"); @@ -344,11 +357,13 @@ describe(" groups: heldMs", () => { expect(a.event.revealGroup).toBeDefined(); expect(bRec.event.revealGroup).toBe(a.event.revealGroup); // A finished early and sat behind B: the hold is the gap, not zero; its - // own duration is still discover → settle, unchanged by the hold. - expect(a.event.durationMs).toBeLessThan(35); - expect(a.event.heldMs).toBeGreaterThanOrEqual(30); + // own duration is still discover → settle, unchanged by the hold. A + // timer never fires early, so the ≥ bounds are deterministic; the only + // wall-clock upper bound left is B's un-held reveal. + expect(a.event.durationMs).toBeLessThan(a.event.heldMs); + expect(a.event.heldMs).toBeGreaterThanOrEqual(35); // B was the one everyone waited for: it revealed as it settled. - expect(bRec.event.durationMs).toBeGreaterThanOrEqual(35); + expect(bRec.event.durationMs).toBeGreaterThanOrEqual(40); expect(bRec.event.heldMs).toBeLessThan(10); expect(a.event.outcome).toBe("settled"); expect(bRec.event.outcome).toBe("settled"); @@ -380,13 +395,27 @@ describe(" groups: heldMs", () => { const seen = records(); const error = vi.spyOn(console, "error").mockImplementation(() => {}); try { + const aFailed = deferred(); function Bad() { const data = createMemo(async () => { await delay(5); + aFailed.resolve(); throw new Error("A failed"); }); return
{data()}
; } + // B settles a fixed 30 ms after A has failed, so A's hold behind the + // group is bounded below by that timer — not by two independent timers + // racing (a 5 ms and a 20 ms timer on a loaded CI runner measured a + // 7 ms hold against a 10 ms floor). + function SlotB() { + const data = createMemo(async () => { + await aFailed.promise; + await delay(30); + return "B"; + }); + return
{data()}
; + } function App() { return ( @@ -394,7 +423,7 @@ describe(" groups: heldMs", () => {
b}> - +
); @@ -403,7 +432,7 @@ describe(" groups: heldMs", () => { expect(seen).toHaveLength(2); const failed = seen.find(r => r.event.outcome === "error")!; expect((failed.live.error as Error).message).toBe("A failed"); - expect(failed.event.heldMs).toBeGreaterThanOrEqual(10); + expect(failed.event.heldMs).toBeGreaterThanOrEqual(25); expect(seen.find(r => r.event.outcome === "settled")).toBeDefined(); } finally { error.mockRestore();