From 439d0a86eb75c129c30742a8cc24bf18e2a6ebf3 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 25 Oct 2026 04:02:23 -0500 Subject: [PATCH] test(server): retain voice reply recovery after failed state reads Co-Authored-By: Paperclip --- .../2026-10-24-recurring-server-sleep-stack.md | 10 ++++++++++ .../2026-10-25-voice-reply-delivery-sleep-audit.md | 9 +++++++++ .../voice-session-store.integration.test.ts | 14 ++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/doc/plans/2026-10-24-recurring-server-sleep-stack.md b/doc/plans/2026-10-24-recurring-server-sleep-stack.md index c6110eb88de..9057e13f9ae 100644 --- a/doc/plans/2026-10-24-recurring-server-sleep-stack.md +++ b/doc/plans/2026-10-24-recurring-server-sleep-stack.md @@ -1255,3 +1255,13 @@ was stopped after repeated embedded PostgreSQL setup failures, not counted as a pass. Slice 22 owns voice reply delivery and begins with the multipart/outcome policy and publication-settlement producer audit. See [reply delivery audit](2026-10-25-voice-reply-delivery-sleep-audit.md). + +Slice 22 now implements voice reply delivery from committed changes and saved +retry/dispatch deadlines. The periodic reply scan and pre-publication direct push +are removed. Real normal/operator publication tests prove the final-commit wake; +turning that notification off reproduces zero delivered replies. The branch has +786 selected passing tests across isolated voice, recovery, startup, and shared +consumers, plus workspace typecheck/build. See the reply audit for retained +fixture failures and local full-suite infrastructure limitations. Publication and +latest-head review/check acceptance remain outstanding. Inbound admission cleanup, +inbound history retention, and call reports remain separate next slices. diff --git a/doc/plans/2026-10-25-voice-reply-delivery-sleep-audit.md b/doc/plans/2026-10-25-voice-reply-delivery-sleep-audit.md index 4c169de2343..cc87e82979b 100644 --- a/doc/plans/2026-10-25-voice-reply-delivery-sleep-audit.md +++ b/doc/plans/2026-10-25-voice-reply-delivery-sleep-audit.md @@ -194,3 +194,12 @@ PR CI/reviews remain required. The local all-suite command has the previously recorded embedded-PostgreSQL initialization limitation; targeted database suites run on real isolated PostgreSQL instead. Do not count skipped or uninitialized database tests as passes. + +The parent recovery suite passed all 11 tests on its isolated database. The new +reply-state read failure case also passed: the worker keeps its retry, performs +no SQL during backoff, and sends once after recovery. Together with the full run, +this verifies 108 voice cases, 16 startup cases, 11 parent recovery cases, and 651 +shared-consumer cases (786 selected tests). Workspace typecheck and build passed. +The final added failure case changed tests only after the typecheck; the workspace +build compiled the current tree. No production changes followed the full run. +Remote CI/review remains pending until this branch is published. diff --git a/server/src/__tests__/voice-session-store.integration.test.ts b/server/src/__tests__/voice-session-store.integration.test.ts index 7368d22c735..d9f3ce1ddbe 100644 --- a/server/src/__tests__/voice-session-store.integration.test.ts +++ b/server/src/__tests__/voice-session-store.integration.test.ts @@ -865,6 +865,20 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu expect(f.transport.sendCallMessage).not.toHaveBeenCalled(); await worker.quiet(); expect(worker.onError).not.toHaveBeenCalled(); } finally { await worker.coordinator.stop(); vi.useRealTimers(); } }); + it("retains phone reply work after a failed state read and retries without intervening scans", async () => { + const f = await pushFixture(); + vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true }); + const transaction = vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("reply database unavailable")); + const worker = await replyWorker(f); + transaction.mockRestore(); + try { + expect(worker.onError).toHaveBeenCalledOnce(); expect(f.transport.sendCallMessage).not.toHaveBeenCalled(); + const retryAt = worker.coordinator.nextWakeAt()!; + expect(retryAt).toBeGreaterThan(Date.now() + 20_000); await worker.quiet(10_000); + await vi.advanceTimersByTimeAsync(retryAt - Date.now()); await worker.settled(); + expect(f.transport.sendCallMessage).toHaveBeenCalledOnce(); expect(worker.onError).toHaveBeenCalledOnce(); await worker.quiet(); + } finally { transaction.mockRestore(); await worker.coordinator.stop(); vi.useRealTimers(); } + }); it("notifies phone questions once and waits for authorized retrieval before later answers", async () => { const f = await pushFixture(), interactionId = randomUUID(); await db.insert(issueThreadInteractions).values({id: interactionId, companyId: f.companyId, issueId: f.issueId, kind: "ask_user_questions", status: "pending", addresseeUserId: f.callerId, effectiveResolverPolicy: "addressee_only", payload: {version: 1, questions: [{id: "color", prompt: "Which color?", selectionMode: "single", required: true, options: [{id: "cobalt", label: "Cobalt"}]}]}});