diff --git a/src/commands/auth-choice.e2e.test.ts b/src/commands/auth-choice.e2e.test.ts index 2977b750e..e684854c0 100644 --- a/src/commands/auth-choice.e2e.test.ts +++ b/src/commands/auth-choice.e2e.test.ts @@ -1,8 +1,6 @@ import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import type { AuthChoice } from "./onboard-types.js"; import { captureEnv } from "../test-utils/env.js"; @@ -12,6 +10,12 @@ import { ZAI_CODING_CN_BASE_URL, ZAI_CODING_GLOBAL_BASE_URL, } from "./onboard-auth.js"; +import { + createExitThrowingRuntime, + createWizardPrompter, + readAuthProfilesForAgent, + setupAuthTestEnv, +} from "./test-wizard-helpers.js"; vi.mock("../providers/github-copilot-auth.js", () => ({ githubCopilotLoginCommand: vi.fn(async () => {}), @@ -27,8 +31,6 @@ vi.mock("../plugins/providers.js", () => ({ resolvePluginProviders, })); -const noopAsync = async () => {}; -const noop = () => {}; const authProfilePathFor = (agentDir: string) => path.join(agentDir, "auth-profiles.json"); const requireAgentDir = () => { const agentDir = process.env.OPENCLAW_AGENT_DIR; @@ -54,6 +56,21 @@ describe("applyAuthChoice", () => { "CHUTES_CLIENT_ID", ]); let tempStateDir: string | null = null; + async function setupTempState() { + const env = await setupAuthTestEnv("openclaw-auth-"); + tempStateDir = env.stateDir; + } + function createPrompter(overrides: Partial): WizardPrompter { + return createWizardPrompter(overrides, { defaultSelect: "" }); + } + async function readAuthProfiles() { + return await readAuthProfilesForAgent<{ + profiles?: Record< + string, + { key?: string; access?: string; refresh?: string; provider?: string } + >; + }>(requireAgentDir()); + } afterEach(async () => { vi.unstubAllGlobals(); @@ -68,30 +85,12 @@ describe("applyAuthChoice", () => { }); it("does not throw when openai-codex oauth fails", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); loginOpenAICodexOAuth.mockRejectedValueOnce(new Error("oauth failed")); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select: vi.fn(async () => "" as never), - multiselect: vi.fn(async () => []), - text: vi.fn(async () => ""), - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({}); + const runtime = createExitThrowingRuntime(); await expect( applyAuthChoice({ @@ -105,33 +104,15 @@ describe("applyAuthChoice", () => { }); it("prompts and writes MiniMax API key when selecting minimax-api", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); const text = vi.fn().mockResolvedValue("sk-minimax-test"); const select: WizardPrompter["select"] = vi.fn( async (params) => params.options[0]?.value as never, ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "minimax-api", @@ -149,42 +130,22 @@ describe("applyAuthChoice", () => { mode: "api_key", }); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["minimax:default"]?.key).toBe("sk-minimax-test"); }); it("prompts and writes MiniMax API key when selecting minimax-api-key-cn", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); const text = vi.fn().mockResolvedValue("sk-minimax-test"); const select: WizardPrompter["select"] = vi.fn( async (params) => params.options[0]?.value as never, ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "minimax-api-key-cn", @@ -203,42 +164,22 @@ describe("applyAuthChoice", () => { }); expect(result.config.models?.providers?.["minimax-cn"]?.baseUrl).toBe(MINIMAX_CN_API_BASE_URL); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["minimax-cn:default"]?.key).toBe("sk-minimax-test"); }); it("prompts and writes Synthetic API key when selecting synthetic-api-key", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); const text = vi.fn().mockResolvedValue("sk-synthetic-test"); const select: WizardPrompter["select"] = vi.fn( async (params) => params.options[0]?.value as never, ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "synthetic-api-key", @@ -256,42 +197,22 @@ describe("applyAuthChoice", () => { mode: "api_key", }); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-test"); }); it("prompts and writes Hugging Face API key when selecting huggingface-api-key", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); const text = vi.fn().mockResolvedValue("hf-test-token"); const select: WizardPrompter["select"] = vi.fn( async (params) => params.options[0]?.value as never, ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "huggingface-api-key", @@ -310,19 +231,14 @@ describe("applyAuthChoice", () => { }); expect(result.config.agents?.defaults?.model?.primary).toMatch(/^huggingface\/.+/); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["huggingface:default"]?.key).toBe("hf-test-token"); }); it("prompts for Z.AI endpoint when selecting zai-api-key", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); const text = vi.fn().mockResolvedValue("zai-test-key"); const select = vi.fn(async (params: { message: string }) => { @@ -332,23 +248,12 @@ describe("applyAuthChoice", () => { return "default"; }); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), + const prompter = createPrompter({ select: select as WizardPrompter["select"], multiselect, text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "zai-api-key", @@ -364,40 +269,24 @@ describe("applyAuthChoice", () => { expect(result.config.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_CN_BASE_URL); expect(result.config.agents?.defaults?.model?.primary).toBe("zai/glm-5"); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["zai:default"]?.key).toBe("zai-test-key"); }); it("uses endpoint-specific auth choice without prompting for Z.AI endpoint", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); const text = vi.fn().mockResolvedValue("zai-test-key"); const select = vi.fn(async () => "default"); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), + const prompter = createPrompter({ select: select as WizardPrompter["select"], multiselect, text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "zai-coding-global", @@ -414,10 +303,7 @@ describe("applyAuthChoice", () => { }); it("maps apiKey + tokenProvider=huggingface to huggingface-api-key flow", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); delete process.env.HF_TOKEN; delete process.env.HUGGINGFACE_HUB_TOKEN; @@ -427,23 +313,8 @@ describe("applyAuthChoice", () => { ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); const confirm = vi.fn(async () => false); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm, - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text, confirm }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "apiKey", @@ -464,41 +335,21 @@ describe("applyAuthChoice", () => { expect(result.config.agents?.defaults?.model?.primary).toMatch(/^huggingface\/.+/); expect(text).not.toHaveBeenCalled(); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["huggingface:default"]?.key).toBe("hf-token-provider-test"); }); it("does not override the global default model when selecting xai-api-key without setDefaultModel", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); const text = vi.fn().mockResolvedValue("sk-xai-test"); const select: WizardPrompter["select"] = vi.fn( async (params) => params.options[0]?.value as never, ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "xai-api-key", @@ -517,37 +368,17 @@ describe("applyAuthChoice", () => { expect(result.config.agents?.defaults?.model?.primary).toBe("openai/gpt-4o-mini"); expect(result.agentModelOverride).toBe("xai/grok-4"); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["xai:default"]?.key).toBe("sk-xai-test"); }); it("sets default model when selecting github-copilot", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select: vi.fn(async () => "" as never), - multiselect: vi.fn(async () => []), - text: vi.fn(async () => ""), - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({}); + const runtime = createExitThrowingRuntime(); const stdin = process.stdin as NodeJS.ReadStream & { isTTY?: boolean }; const hadOwnIsTTY = Object.prototype.hasOwnProperty.call(stdin, "isTTY"); @@ -578,33 +409,15 @@ describe("applyAuthChoice", () => { }); it("does not override the default model when selecting opencode-zen without setDefaultModel", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); const text = vi.fn().mockResolvedValue("sk-opencode-zen-test"); const select: WizardPrompter["select"] = vi.fn( async (params) => params.options[0]?.value as never, ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "opencode-zen", @@ -629,30 +442,12 @@ describe("applyAuthChoice", () => { }); it("does not persist literal 'undefined' when Anthropic API key prompt returns undefined", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); delete process.env.ANTHROPIC_API_KEY; const text = vi.fn(async () => undefined as unknown as string); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select: vi.fn(async () => "" as never), - multiselect: vi.fn(async () => []), - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ text }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "apiKey", @@ -667,9 +462,7 @@ describe("applyAuthChoice", () => { mode: "api_key", }); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["anthropic:default"]?.key).toBe(""); @@ -677,30 +470,12 @@ describe("applyAuthChoice", () => { }); it("does not persist literal 'undefined' when OpenRouter API key prompt returns undefined", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); delete process.env.OPENROUTER_API_KEY; const text = vi.fn(async () => undefined as unknown as string); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select: vi.fn(async () => "" as never), - multiselect: vi.fn(async () => []), - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ text }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "openrouter-api-key", @@ -715,9 +490,7 @@ describe("applyAuthChoice", () => { mode: "api_key", }); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["openrouter:default"]?.key).toBe(""); @@ -725,10 +498,7 @@ describe("applyAuthChoice", () => { }); it("uses existing OPENROUTER_API_KEY when selecting openrouter-api-key", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); process.env.OPENROUTER_API_KEY = "sk-openrouter-test"; const text = vi.fn(); @@ -737,23 +507,8 @@ describe("applyAuthChoice", () => { ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); const confirm = vi.fn(async () => true); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm, - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text, confirm }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "openrouter-api-key", @@ -775,9 +530,7 @@ describe("applyAuthChoice", () => { }); expect(result.config.agents?.defaults?.model?.primary).toBe("openrouter/auto"); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["openrouter:default"]?.key).toBe("sk-openrouter-test"); @@ -786,10 +539,7 @@ describe("applyAuthChoice", () => { }); it("ignores legacy LiteLLM oauth profiles when selecting litellm-api-key", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); process.env.LITELLM_API_KEY = "sk-litellm-test"; const authProfilePath = authProfilePathFor(requireAgentDir()); @@ -821,23 +571,8 @@ describe("applyAuthChoice", () => { ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); const confirm = vi.fn(async () => true); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm, - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text, confirm }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "litellm-api-key", @@ -865,8 +600,7 @@ describe("applyAuthChoice", () => { mode: "api_key", }); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["litellm:default"]).toMatchObject({ @@ -876,10 +610,7 @@ describe("applyAuthChoice", () => { }); it("uses existing AI_GATEWAY_API_KEY when selecting ai-gateway-api-key", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); process.env.AI_GATEWAY_API_KEY = "gateway-test-key"; const text = vi.fn(); @@ -888,23 +619,8 @@ describe("applyAuthChoice", () => { ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); const confirm = vi.fn(async () => true); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm, - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text, confirm }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "ai-gateway-api-key", @@ -928,9 +644,7 @@ describe("applyAuthChoice", () => { "vercel-ai-gateway/anthropic/claude-opus-4.6", ); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["vercel-ai-gateway:default"]?.key).toBe("gateway-test-key"); @@ -939,10 +653,7 @@ describe("applyAuthChoice", () => { }); it("uses existing CLOUDFLARE_AI_GATEWAY_API_KEY when selecting cloudflare-ai-gateway-api-key", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); process.env.CLOUDFLARE_AI_GATEWAY_API_KEY = "cf-gateway-test-key"; const text = vi @@ -954,23 +665,8 @@ describe("applyAuthChoice", () => { ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); const confirm = vi.fn(async () => true); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm, - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ select, multiselect, text, confirm }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "cloudflare-ai-gateway-api-key", @@ -994,9 +690,7 @@ describe("applyAuthChoice", () => { "cloudflare-ai-gateway/claude-sonnet-4-5", ); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record }>; }; expect(parsed.profiles?.["cloudflare-ai-gateway:default"]?.key).toBe("cf-gateway-test-key"); @@ -1009,10 +703,7 @@ describe("applyAuthChoice", () => { }); it("writes Chutes OAuth credentials when selecting chutes (remote/manual)", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); process.env.SSH_TTY = "1"; process.env.CHUTES_CLIENT_ID = "cid_test"; @@ -1038,13 +729,7 @@ describe("applyAuthChoice", () => { }); vi.stubGlobal("fetch", fetchSpy); - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const runtime = createExitThrowingRuntime(); const text: WizardPrompter["text"] = vi.fn(async (params) => { if (params.message === "Paste the redirect URL") { const lastLog = runtime.log.mock.calls.at(-1)?.[0]; @@ -1062,16 +747,7 @@ describe("applyAuthChoice", () => { async (params) => params.options[0]?.value as never, ); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select, - multiselect, - text, - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; + const prompter = createPrompter({ select, multiselect, text }); const result = await applyAuthChoice({ authChoice: "chutes", @@ -1091,9 +767,7 @@ describe("applyAuthChoice", () => { mode: "oauth", }); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record< string, { provider?: string; access?: string; refresh?: string; email?: string } @@ -1108,10 +782,7 @@ describe("applyAuthChoice", () => { }); it("writes Qwen credentials when selecting qwen-portal", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); resolvePluginProviders.mockReturnValue([ { @@ -1154,23 +825,8 @@ describe("applyAuthChoice", () => { }, ]); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select: vi.fn(async () => "" as never), - multiselect: vi.fn(async () => []), - text: vi.fn(async () => ""), - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({}); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "qwen-portal", @@ -1190,9 +846,7 @@ describe("applyAuthChoice", () => { apiKey: "qwen-oauth", }); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["qwen-portal:default"]).toMatchObject({ @@ -1203,10 +857,7 @@ describe("applyAuthChoice", () => { }); it("writes MiniMax credentials when selecting minimax-portal", async () => { - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); - process.env.OPENCLAW_STATE_DIR = tempStateDir; - process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); - process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + await setupTempState(); resolvePluginProviders.mockReturnValue([ { @@ -1249,23 +900,10 @@ describe("applyAuthChoice", () => { }, ]); - const prompter: WizardPrompter = { - intro: vi.fn(noopAsync), - outro: vi.fn(noopAsync), - note: vi.fn(noopAsync), - select: vi.fn(async () => "oauth" as never), - multiselect: vi.fn(async () => []), - text: vi.fn(async () => ""), - confirm: vi.fn(async () => false), - progress: vi.fn(() => ({ update: noop, stop: noop })), - }; - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn((code: number) => { - throw new Error(`exit:${code}`); - }), - }; + const prompter = createPrompter({ + select: vi.fn(async () => "oauth" as never) as WizardPrompter["select"], + }); + const runtime = createExitThrowingRuntime(); const result = await applyAuthChoice({ authChoice: "minimax-portal", @@ -1285,9 +923,7 @@ describe("applyAuthChoice", () => { apiKey: "minimax-oauth", }); - const authProfilePath = authProfilePathFor(requireAgentDir()); - const raw = await fs.readFile(authProfilePath, "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = (await readAuthProfiles()) as { profiles?: Record; }; expect(parsed.profiles?.["minimax-portal:default"]).toMatchObject({