diff --git a/ui/src/pages/custodian/custodian-page.qr.test.ts b/ui/src/pages/custodian/custodian-page.qr.test.ts index 78c901354d1d..2ee07a1bf1a3 100644 --- a/ui/src/pages/custodian/custodian-page.qr.test.ts +++ b/ui/src/pages/custodian/custodian-page.qr.test.ts @@ -88,6 +88,52 @@ describe("custodian QR wizard step", () => { await waitForFast(() => expect(page.textContent).toContain("Signal setup cancelled.")); }); + it("pauses QR polling while direct wizard cancellation settles", async () => { + vi.useFakeTimers(); + const request = vi + .fn() + .mockResolvedValueOnce(qrResult()) + .mockResolvedValueOnce(terminalResult("Signal setup cancelled.")); + const { page } = await mountPage(createContext(request).context); + await vi.advanceTimersByTimeAsync(0); + + page.querySelector(".custodian__wizard-cancel")?.click(); + await vi.advanceTimersByTimeAsync(2_000); + await page.updateComplete; + + expect(request).toHaveBeenCalledTimes(2); + expect(request.mock.calls[1]?.[1]).toEqual({ + sessionId: SESSION_ID, + wizardCancel: { stepId: "qr-step" }, + }); + expect(page.textContent).toContain("Signal setup cancelled."); + }); + + it.each([ + ["exit setup", (page: Awaited>["page"]) => page.store.exitSetup()], + [ + "open model setup", + (page: Awaited>["page"]) => page.store.openModelSetup(), + ], + [ + "open channels", + (page: Awaited>["page"]) => + page.store.openChannelsFromOnboarding(), + ], + ])("stops QR polling and scrubs image bytes when users %s", async (_name, navigate) => { + vi.useFakeTimers(); + const request = vi.fn().mockResolvedValueOnce(qrResult()); + const { page } = await mountPage(createContext(request).context); + await vi.advanceTimersByTimeAsync(0); + + navigate(page); + await vi.advanceTimersByTimeAsync(2_000); + await page.updateComplete; + + expect(request).toHaveBeenCalledOnce(); + expect(page.store.messages.some((message) => message.step?.qrDataUrl)).toBe(false); + }); + it("keeps polling when acknowledgement advances directly to another QR", async () => { vi.useFakeTimers(); const request = vi diff --git a/ui/src/pages/custodian/custodian-session-store.ts b/ui/src/pages/custodian/custodian-session-store.ts index 761947a17069..299b3953d913 100644 --- a/ui/src/pages/custodian/custodian-session-store.ts +++ b/ui/src/pages/custodian/custodian-session-store.ts @@ -375,7 +375,7 @@ export class CustodianSessionStore { const client = this.activeClient; if ( !step || - !this.wizardInputPending || + (!this.wizardInputPending && !(this.wizardSettling && step.type === "qr")) || !client || !this.chatAvailable || !this.wizardCancelAvailable || @@ -385,12 +385,22 @@ export class CustodianSessionStore { this.emit(); return; } + const sessionContinuity = this.sessionContinuity; + const settleQrCancellation = + step.type === "qr" + ? this.qrScheduler.beginAcknowledgement( + client, + step.id, + () => this.sessionContinuity === sessionContinuity && this.activeClient === client, + ) + : undefined; void this.sendUserTurn( client, { sessionId: this.sessionId, wizardCancel: { stepId: step.id } }, t("custodian.cancel"), true, - ); + () => settleQrCancellation?.("sent"), + ).then(settleQrCancellation); } exitSetup(): void { @@ -401,6 +411,8 @@ export class CustodianSessionStore { } private revokeNavigationAuthority(): void { + this.qrScheduler.clear(); + this.messages = scrubCustodianQrSteps(this.messages); this.requestAbort?.abort(); this.requestAbort = null; this.requestEpoch += 1;