diff --git a/src/gateway/gateway.test.ts b/src/gateway/gateway.test.ts index 17b057332f4..2cbf8d1a5a0 100644 --- a/src/gateway/gateway.test.ts +++ b/src/gateway/gateway.test.ts @@ -103,6 +103,7 @@ describe("gateway e2e", () => { "OPENCLAW_SKIP_CRON", "OPENCLAW_SKIP_CANVAS_HOST", "OPENCLAW_SKIP_BROWSER_CONTROL_SERVER", + "OPENCLAW_TEST_MINIMAL_GATEWAY", ]); const { baseUrl: openaiBaseUrl, restore } = installOpenAiResponsesMock(); @@ -116,6 +117,7 @@ describe("gateway e2e", () => { process.env.OPENCLAW_SKIP_CRON = "1"; process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1"; + process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "1"; const token = nextGatewayId("test-token"); process.env.OPENCLAW_GATEWAY_TOKEN = token; @@ -309,6 +311,7 @@ module.exports = { "OPENCLAW_SKIP_CRON", "OPENCLAW_SKIP_CANVAS_HOST", "OPENCLAW_SKIP_BROWSER_CONTROL_SERVER", + "OPENCLAW_TEST_MINIMAL_GATEWAY", ]); process.env.OPENCLAW_SKIP_CHANNELS = "1"; @@ -316,6 +319,7 @@ module.exports = { process.env.OPENCLAW_SKIP_CRON = "1"; process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1"; + process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "1"; delete process.env.OPENCLAW_GATEWAY_TOKEN; const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wizard-home-")); @@ -362,22 +366,30 @@ module.exports = { let next = start; let didSendToken = false; + const seenSteps: string[] = []; while (!next.done) { const step = next.step; if (!step) { throw new Error("wizard missing step"); } + seenSteps.push(`${step.type}:${step.id}`); const value = step.type === "text" ? wizardToken : null; if (step.type === "text") { didSendToken = true; } - next = await client.request("wizard.next", { - sessionId, - answer: { stepId: step.id, value }, - }); + next = await client.request( + "wizard.next", + { + sessionId, + answer: { stepId: step.id, value }, + }, + { timeoutMs: 60_000 }, + ); } - expect(didSendToken).toBe(true); + expect(didSendToken, `seenSteps=${seenSteps.join(",")} final=${JSON.stringify(next)}`).toBe( + true, + ); expect(next.status).toBe("done"); const parsed = JSON.parse(await fs.readFile(resolveConfigPath(), "utf8")); diff --git a/src/wizard/session.test.ts b/src/wizard/session.test.ts index bc187f1bfd7..add272e2f80 100644 --- a/src/wizard/session.test.ts +++ b/src/wizard/session.test.ts @@ -71,4 +71,23 @@ describe("WizardSession", () => { expect(done.done).toBe(true); expect(done.status).toBe("cancelled"); }); + + test("does not lose terminal completion when the last answer finishes the runner immediately", async () => { + const session = new WizardSession(async (prompter) => { + await prompter.text({ message: "Token" }); + }); + + const first = await session.next(); + expect(first.step?.type).toBe("text"); + if (!first.step) { + throw new Error("expected first step"); + } + + await session.answer(first.step.id, "ok"); + await Promise.resolve(); + + const done = await session.next(); + expect(done.done).toBe(true); + expect(done.status).toBe("done"); + }); }); diff --git a/src/wizard/session.ts b/src/wizard/session.ts index 5c4c760414f..7d6ff2a1293 100644 --- a/src/wizard/session.ts +++ b/src/wizard/session.ts @@ -163,6 +163,7 @@ class WizardSessionPrompter implements WizardPrompter { export class WizardSession { private currentStep: WizardStep | null = null; private stepDeferred: Deferred | null = null; + private pendingTerminalResolution = false; private answerDeferred = new Map>(); private status: WizardSessionStatus = "running"; private error: string | undefined; @@ -176,6 +177,10 @@ export class WizardSession { if (this.currentStep) { return { done: false, step: this.currentStep, status: this.status }; } + if (this.pendingTerminalResolution) { + this.pendingTerminalResolution = false; + return { done: true, status: this.status, error: this.error }; + } if (this.status !== "running") { return { done: true, status: this.status, error: this.error }; } @@ -247,6 +252,9 @@ export class WizardSession { private resolveStep(step: WizardStep | null) { if (!this.stepDeferred) { + if (step === null) { + this.pendingTerminalResolution = true; + } return; } const deferred = this.stepDeferred;