fix(ui): fence QR polling during cancellation

This commit is contained in:
jesse-merhi
2026-08-10 22:14:08 +10:00
parent 52271a9696
commit 402be28476
2 changed files with 60 additions and 2 deletions
@@ -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<HTMLButtonElement>(".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<ReturnType<typeof mountPage>>["page"]) => page.store.exitSetup()],
[
"open model setup",
(page: Awaited<ReturnType<typeof mountPage>>["page"]) => page.store.openModelSetup(),
],
[
"open channels",
(page: Awaited<ReturnType<typeof mountPage>>["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
@@ -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;