From 5bbe1a975c626b76daa22321a76c8fcd9504ffc4 Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 15 Sep 2026 16:00:22 +0000 Subject: [PATCH] Interrupt Live Brunch turns only when entering error Co-authored-by: Kostandin Angjellari --- .../live-brunch-bridge.test.ts | 102 ++++++++++++++++++ .../app/voice-interview/live-brunch-bridge.ts | 4 +- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.test.ts b/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.test.ts index 149fb6024cf..cf92584001f 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.test.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.test.ts @@ -858,6 +858,108 @@ test("repeated stopped snapshots retain a later turn", async () => { ); }); +test("entering error interrupts pending work once and suppresses its late result", async () => { + const fixture = setup(); + let release = () => {}; + fixture.submit.mockImplementationOnce(async (input) => { + await new Promise((resolve) => { + release = resolve; + }); + input.onAdmission("root"); + return { kind: "message", messageId: "user", submissionId: "root" }; + }); + fixture.bridge.acceptDelegation("pending"); + const pending = fixture.bridge.accept({ id: "one", text: "First" }); + fixture.bridge.acceptDelegation("unclaimed"); + + fixture.update({ status: "error" }); + expect(fixture.appendInstructions.mock.calls).toEqual([ + [expect.stringContaining("Ask the person to continue"), "pending"], + [expect.stringContaining("Ask the person to continue"), "unclaimed"], + ]); + fixture.update({ status: "error" }); + expect(fixture.appendInstructions).toHaveBeenCalledTimes(2); + + release(); + await pending; + fixture.bridge.responseStarted(started); + fixture.bridge.responseCompleted({ + ...started, + position: { batch: 2, index: 0 }, + }); + fixture.update({ segments: [segment()], settlements: completed }); + expect(fixture.appendCommentary).not.toHaveBeenCalled(); + expect(fixture.appendInstructions).toHaveBeenCalledTimes(2); + expect(fixture.submit).toHaveBeenCalledOnce(); +}); + +test("repeated error snapshots retain a pending recovery turn until ready settlement", async () => { + const fixture = setup(); + fixture.bridge.acceptDelegation("original"); + await fixture.bridge.accept({ id: "one", text: "First" }); + fixture.update({ status: "error" }); + expect(fixture.appendInstructions).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining("Ask the person to continue"), + "original", + ); + fixture.appendInstructions.mockClear(); + + let release = () => {}; + fixture.submit.mockImplementationOnce(async (input) => { + await new Promise((resolve) => { + release = resolve; + }); + input.onAdmission("recovery"); + return { kind: "message", messageId: "two", submissionId: "recovery" }; + }); + fixture.bridge.acceptDelegation("recovery-delegation"); + const recovery = fixture.bridge.accept({ id: "two", text: "Try again" }); + fixture.update({ status: "error" }); + fixture.update({ status: "error" }); + expect(fixture.appendInstructions).not.toHaveBeenCalled(); + + release(); + await recovery; + const response = { + ...started, + messageId: "recovery-answer", + submissionId: "recovery", + }; + fixture.bridge.responseStarted(response); + fixture.bridge.responseCompleted({ + ...response, + position: { batch: 2, index: 0 }, + }); + const chat = { + segments: [ + { + ...segment("Recovery answer"), + messageId: "recovery-answer", + submissionIds: ["recovery"], + }, + ], + settlements: [{ submissionId: "recovery", outcome: "completed" as const }], + }; + fixture.update({ ...chat, status: "error" }); + expect(fixture.appendInstructions).not.toHaveBeenCalled(); + expect(fixture.appendCommentary).not.toHaveBeenCalled(); + fixture.update(chat); + fixture.update(chat); + expect(fixture.appendCommentary).toHaveBeenCalledExactlyOnceWith( + "Recovery answer", + "recovery-delegation", + ); + expect(fixture.submit).toHaveBeenCalledTimes(2); + + fixture.bridge.acceptDelegation("next"); + await fixture.bridge.accept({ id: "three", text: "Next question" }); + fixture.update({ status: "error" }); + expect(fixture.appendInstructions).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining("Ask the person to continue"), + "next", + ); +}); + test("a locally refused long commentary is offered intact once without truncation or replay", async () => { const fixture = setup(); fixture.appendCommentary.mockReturnValue(false); diff --git a/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.ts b/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.ts index b2ca277f086..522f354c6bf 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.ts @@ -264,8 +264,10 @@ export class LiveBrunchBridge { public update(chat: Chat): void { if (this.#abort.signal.aborted) return; const stopped = chat.stopped === true && this.#chat.stopped !== true; + const enteredError = + chat.status === "error" && this.#chat.status !== "error"; this.#chat = chat; - if (stopped || chat.status === "error") { + if (stopped || enteredError) { this.#interruptTurns(); return; }