mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(ui): poll settling QR setup
This commit is contained in:
@@ -110,6 +110,37 @@ describe("custodian QR wizard step", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps polling when acknowledgement enters non-input settlement", async () => {
|
||||
vi.useFakeTimers();
|
||||
const settlingResult = {
|
||||
sessionId: SESSION_ID,
|
||||
reply: "Setup is still finishing the QR attempt.",
|
||||
action: "none" as const,
|
||||
wizardSettling: true,
|
||||
};
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(qrResult())
|
||||
.mockResolvedValueOnce(settlingResult)
|
||||
.mockResolvedValueOnce(terminalResult("Device linked."));
|
||||
const { page } = await mountPage(createContext(request).context);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
page.querySelector<HTMLButtonElement>(".custodian__wizard-step .btn.primary")?.click();
|
||||
await waitForFast(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
expect(page.store.wizardInputPending).toBe(false);
|
||||
expect(page.store.wizardSettling).toBe(true);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await waitForFast(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
expect(request.mock.calls[2]?.[1]).toEqual({
|
||||
sessionId: SESSION_ID,
|
||||
pollStepId: "qr-step",
|
||||
});
|
||||
expect(page.store.wizardSettling).toBe(false);
|
||||
expect(page.textContent).toContain("Device linked.");
|
||||
});
|
||||
|
||||
it("keeps the QR and resumes polling when Continue was definitely unsent", async () => {
|
||||
vi.useFakeTimers();
|
||||
const request = vi
|
||||
@@ -599,7 +630,7 @@ describe("custodian QR wizard step", () => {
|
||||
sessionId: SESSION_ID,
|
||||
reply: "Setup is still finishing the QR attempt.",
|
||||
action: "none" as const,
|
||||
wizardInputPending: true,
|
||||
wizardSettling: true,
|
||||
};
|
||||
const request = vi
|
||||
.fn()
|
||||
@@ -614,6 +645,8 @@ describe("custodian QR wizard step", () => {
|
||||
await page.updateComplete;
|
||||
expect(page.querySelector(".wizard-step__qr")).toBeNull();
|
||||
expect(page.textContent).toContain("This QR code expired.");
|
||||
expect(page.store.wizardInputPending).toBe(false);
|
||||
expect(page.store.wizardSettling).toBe(true);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(request.mock.calls.filter((call) => call[1]?.pollStepId === "qr-step")).toHaveLength(2);
|
||||
|
||||
@@ -58,6 +58,7 @@ export class CustodianSessionStore {
|
||||
sending = false;
|
||||
sensitive = false;
|
||||
wizardInputPending = false;
|
||||
wizardSettling = false;
|
||||
wizardValue: unknown;
|
||||
wizardSecretVisible = false;
|
||||
questionReplyUncertain = false;
|
||||
@@ -331,22 +332,26 @@ export class CustodianSessionStore {
|
||||
}
|
||||
|
||||
answerWizardStep(message: CustodianMessage, value: unknown): void {
|
||||
if (!message.step || !this.wizardInputPending) {
|
||||
const step = message.step;
|
||||
if (
|
||||
!step ||
|
||||
(!this.wizardInputPending && !(this.wizardSettling && step.type === "qr" && value === false))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const submission = custodianWizardSubmission(message.step, value);
|
||||
const submission = custodianWizardSubmission(step, value);
|
||||
const client = this.activeClient;
|
||||
if (!submission || !client || !this.chatAvailable || this.sending || this.setupRequired) {
|
||||
this.emit();
|
||||
return;
|
||||
}
|
||||
const displayText = message.step.sensitive ? t("custodian.sensitiveReply") : submission.display;
|
||||
const displayText = step.sensitive ? t("custodian.sensitiveReply") : submission.display;
|
||||
const sessionContinuity = this.sessionContinuity;
|
||||
const settleQrAcknowledgement =
|
||||
message.step.type === "qr"
|
||||
step.type === "qr"
|
||||
? this.qrScheduler.beginAcknowledgement(
|
||||
client,
|
||||
message.step.id,
|
||||
step.id,
|
||||
() => this.sessionContinuity === sessionContinuity && this.activeClient === client,
|
||||
)
|
||||
: undefined;
|
||||
@@ -450,7 +455,10 @@ export class CustodianSessionStore {
|
||||
this.input = "";
|
||||
this.wizardValue = undefined;
|
||||
this.wizardSecretVisible = false;
|
||||
this.sensitive = this.wizardInputPending = this.questionReplyUncertain = false;
|
||||
this.sensitive = false;
|
||||
this.wizardInputPending = false;
|
||||
this.wizardSettling = false;
|
||||
this.questionReplyUncertain = false;
|
||||
this.error = null;
|
||||
this.setupIssue = null;
|
||||
this.earlierBoundaryAfterId = this.messages.at(-1)?.id ?? null;
|
||||
@@ -478,9 +486,10 @@ export class CustodianSessionStore {
|
||||
client !== this.sessionClient;
|
||||
const ownershipChanged =
|
||||
this.sessionContinuity !== null && continuity.key !== this.sessionContinuity.key;
|
||||
const pendingQrStepId = this.wizardInputPending
|
||||
? this.messages.findLast((message) => message.step?.type === "qr")?.step?.id
|
||||
: undefined;
|
||||
const pendingQrStepId =
|
||||
this.wizardInputPending || this.wizardSettling
|
||||
? this.messages.findLast((message) => message.step?.type === "qr")?.step?.id
|
||||
: undefined;
|
||||
if (
|
||||
client === this.activeClient &&
|
||||
!variantChanged &&
|
||||
@@ -554,9 +563,10 @@ export class CustodianSessionStore {
|
||||
if (!this.retryParams) {
|
||||
this.error = requestWasPending ? this.error : null;
|
||||
}
|
||||
const pendingStep = this.wizardInputPending
|
||||
? this.messages.findLast((message) => message.step !== null)?.step
|
||||
: null;
|
||||
const pendingStep =
|
||||
this.wizardInputPending || this.wizardSettling
|
||||
? this.messages.findLast((message) => message.step !== null)?.step
|
||||
: null;
|
||||
if (pendingStep?.type === "qr") {
|
||||
// A reconnect invalidates the old timer, but the Gateway still owns the QR session.
|
||||
this.qrScheduler.schedulePoll(client, pendingStep.id);
|
||||
@@ -619,7 +629,10 @@ export class CustodianSessionStore {
|
||||
this.input = "";
|
||||
this.wizardValue = undefined;
|
||||
this.wizardSecretVisible = false;
|
||||
this.sensitive = this.wizardInputPending = this.questionReplyUncertain = false;
|
||||
this.sensitive = false;
|
||||
this.wizardInputPending = false;
|
||||
this.wizardSettling = false;
|
||||
this.questionReplyUncertain = false;
|
||||
this.earlierBoundaryAfterId = null;
|
||||
}
|
||||
|
||||
@@ -647,6 +660,7 @@ export class CustodianSessionStore {
|
||||
const requestAbort = new AbortController();
|
||||
this.requestAbort = requestAbort;
|
||||
const pollStepId = options?.pollStepId;
|
||||
const settlingStepId = pollStepId ?? params.wizardAnswer?.stepId;
|
||||
const epoch = ++this.requestEpoch;
|
||||
let delivery: eventNudgeState.CustodianSendDelivery = "unsent";
|
||||
if (!pollStepId) {
|
||||
@@ -687,16 +701,18 @@ export class CustodianSessionStore {
|
||||
}
|
||||
this.messages = replaceCustodianQrStep(this.messages, result.step);
|
||||
this.wizardInputPending = result.wizardInputPending === true;
|
||||
this.wizardSettling = false;
|
||||
this.qrScheduler.scheduleStep(client, result.step);
|
||||
return "sent";
|
||||
}
|
||||
if (pollStepId && result.wizardInputPending === true && result.step === undefined) {
|
||||
if (settlingStepId && result.wizardSettling === true && result.step === undefined) {
|
||||
// An externally owned QR can outlive its presentation. Keep observations short and
|
||||
// poll again so Cancel remains responsive while the owner settles in the background.
|
||||
this.messages = scrubCustodianQrSteps(this.messages, pollStepId);
|
||||
this.messages = scrubCustodianQrSteps(this.messages, settlingStepId);
|
||||
this.questionReplyUncertain = false;
|
||||
this.wizardInputPending = true;
|
||||
this.qrScheduler.schedulePoll(client, pollStepId);
|
||||
this.wizardInputPending = false;
|
||||
this.wizardSettling = true;
|
||||
this.qrScheduler.schedulePoll(client, settlingStepId);
|
||||
return "sent";
|
||||
}
|
||||
this.qrScheduler.clear();
|
||||
@@ -706,6 +722,7 @@ export class CustodianSessionStore {
|
||||
}
|
||||
this.sensitive = result.sensitive === true;
|
||||
this.wizardInputPending = result.wizardInputPending === true;
|
||||
this.wizardSettling = result.wizardSettling === true;
|
||||
[this.retryParams, this.setupIssue] = [null, null];
|
||||
const step = result.step ?? null;
|
||||
const question = step ? null : parseCustodianQuestion(result.question);
|
||||
|
||||
@@ -133,9 +133,10 @@ class CustodianSurface extends OpenClawLightDomElement {
|
||||
`;
|
||||
}
|
||||
const emptyError = store.messages.length === 0 && store.error !== null && !store.sending;
|
||||
const activeWizardMessage = store.wizardInputPending
|
||||
? store.messages.findLast((message) => message.step !== null)
|
||||
: undefined;
|
||||
const activeWizardMessage =
|
||||
store.wizardInputPending || store.wizardSettling
|
||||
? store.messages.findLast((message) => message.step !== null)
|
||||
: undefined;
|
||||
return html`
|
||||
<section
|
||||
class="custodian-surface ${this.compact ? "custodian-surface--panel" : ""} ${emptyError
|
||||
|
||||
@@ -66,7 +66,9 @@ export class CustodianQrScheduler {
|
||||
}
|
||||
if (outcome !== "rejected" && !delivered) {
|
||||
delivered = true;
|
||||
if (this.stepId === stepId) {
|
||||
// A received acknowledgement may already have scheduled observation of this step.
|
||||
// Clear only pre-response state so the replacement settlement poll survives.
|
||||
if (this.stepId === stepId && this.pollTimer === null) {
|
||||
this.clear();
|
||||
}
|
||||
this.callbacks.onExpire(stepId, true);
|
||||
|
||||
Reference in New Issue
Block a user