diff --git a/doc/plans/2026-10-25-voice-drain-repair.md b/doc/plans/2026-10-25-voice-drain-repair.md index e7f761ae35b..dbe0754408e 100644 --- a/doc/plans/2026-10-25-voice-drain-repair.md +++ b/doc/plans/2026-10-25-voice-drain-repair.md @@ -50,3 +50,23 @@ then verifies the saved reply sends on resume without another producer. All later shared-worker consumers inherit this repair through the stack. Their combined-tip checks and PR checks still need fresh verification after propagation. + +## Retention resume assertion + +The combined-tip consumer run exposed a retention test that called flush after +resume and assumed a zero activity counter meant the retained retry had run. Its +helper checks active work only; zero is now also the correct paused state. The +owner test now advances the actual retained nextWakeAt, waits for row removal, +and checks disarm. Before resume it also proves a stable idle snapshot through a +minute of quiet. All 11 retention cases pass in /tmp/sleep24-drain-tests.log. +No production scheduling deadline or dispatch behavior changed for this repair. + +## Report resume assertion + +The report test had the same active-counter-only flush assumption. It now checks +idle snapshot stability while paused, advances the retained nextWakeAt on resume, +and verifies one fetch and the available durable report before testing rollback. +All 14 report cases pass in /tmp/sleep25-drain-tests.log. An intermediate new +assertion incorrectly expected state=completed; the schema uses status=available. +That test-authoring failure is retained in /tmp/sleep25-drain-invalid-assertion.log. +The scheduler repair and business deadlines were not changed to satisfy tests. diff --git a/server/src/__tests__/voice-call-reports.test.ts b/server/src/__tests__/voice-call-reports.test.ts index 1094e0fa815..ffbf960fdc0 100644 --- a/server/src/__tests__/voice-call-reports.test.ts +++ b/server/src/__tests__/voice-call-reports.test.ts @@ -141,8 +141,13 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu }); it("retains committed work during drain and ignores rolled-back nested writes", async () => { await start(); await flush(); enabled = false; - const call = await session(); await quiet(60_000); expect(await report(call.id)).toBeUndefined(); - enabled = true; await flush(); expect(fetchReport).toHaveBeenCalledOnce(); + const call = await session(); await flush(); + const drained = idleWorkSnapshot(); + await quiet(60_000); expect(idleWorkSnapshot()).toEqual(drained); expect(await report(call.id)).toBeUndefined(); + const resumeAt = coordinator!.nextWakeAt(); expect(resumeAt).not.toBeNull(); + enabled = true; await vi.advanceTimersByTimeAsync(Math.max(0, resumeAt! - Date.now())); + await vi.waitFor(async () => { await flush(); expect(fetchReport).toHaveBeenCalledOnce(); }); + expect(await report(call.id)).toMatchObject({ status: "available" }); await expect(db.transaction(async tx => { await session({}, tx as unknown as typeof db); throw new Error("rollback"); })).rejects.toThrow("rollback"); await flush(); expect(fetchReport).toHaveBeenCalledOnce(); await quiet(); }); diff --git a/server/src/__tests__/voice-inbound-retention.test.ts b/server/src/__tests__/voice-inbound-retention.test.ts index ebafa59093c..7b07b3caac6 100644 --- a/server/src/__tests__/voice-inbound-retention.test.ts +++ b/server/src/__tests__/voice-inbound-retention.test.ts @@ -151,8 +151,13 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu }); it("keeps the first write during drain and runs it on resume", async () => { await start(); await flush(); enabled = false; - const call = await admission(); await quiet(60_000); expect(await row(call.id)).toBeDefined(); - enabled = true; await flush(); expect(await row(call.id)).toBeUndefined(); await quiet(); + const call = await admission(); await flush(); + const drained = idleWorkSnapshot(); + await quiet(60_000); expect(idleWorkSnapshot()).toEqual(drained); expect(await row(call.id)).toBeDefined(); + const resumeAt = coordinator!.nextWakeAt(); expect(resumeAt).not.toBeNull(); + enabled = true; await vi.advanceTimersByTimeAsync(Math.max(0, resumeAt! - Date.now())); + await vi.waitFor(async () => { await flush(); expect(await row(call.id)).toBeUndefined(); }); + expect(coordinator!.nextWakeAt()).toBeNull(); await quiet(); }); it("pages through more than one batch of history across companies", async () => { for (let i = 0; i < 104; i++) await admission();