Spaces:
Running
Running
| 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 { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js"; | |
| vi.mock("../providers/github-copilot-auth.js", () => ({ | |
| githubCopilotLoginCommand: vi.fn(async () => {}), | |
| })); | |
| const resolvePluginProviders = vi.hoisted(() => vi.fn(() => [])); | |
| 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; | |
| if (!agentDir) { | |
| throw new Error("OPENCLAW_AGENT_DIR not set"); | |
| } | |
| return agentDir; | |
| }; | |
| describe("applyAuthChoice", () => { | |
| const previousStateDir = process.env.OPENCLAW_STATE_DIR; | |
| const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; | |
| const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; | |
| const previousOpenrouterKey = process.env.OPENROUTER_API_KEY; | |
| const previousAiGatewayKey = process.env.AI_GATEWAY_API_KEY; | |
| const previousSshTty = process.env.SSH_TTY; | |
| const previousChutesClientId = process.env.CHUTES_CLIENT_ID; | |
| let tempStateDir: string | null = null; | |
| afterEach(async () => { | |
| vi.unstubAllGlobals(); | |
| resolvePluginProviders.mockReset(); | |
| if (tempStateDir) { | |
| await fs.rm(tempStateDir, { recursive: true, force: true }); | |
| tempStateDir = null; | |
| } | |
| if (previousStateDir === undefined) { | |
| delete process.env.OPENCLAW_STATE_DIR; | |
| } else { | |
| process.env.OPENCLAW_STATE_DIR = previousStateDir; | |
| } | |
| if (previousAgentDir === undefined) { | |
| delete process.env.OPENCLAW_AGENT_DIR; | |
| } else { | |
| process.env.OPENCLAW_AGENT_DIR = previousAgentDir; | |
| } | |
| if (previousPiAgentDir === undefined) { | |
| delete process.env.PI_CODING_AGENT_DIR; | |
| } else { | |
| process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; | |
| } | |
| if (previousOpenrouterKey === undefined) { | |
| delete process.env.OPENROUTER_API_KEY; | |
| } else { | |
| process.env.OPENROUTER_API_KEY = previousOpenrouterKey; | |
| } | |
| if (previousAiGatewayKey === undefined) { | |
| delete process.env.AI_GATEWAY_API_KEY; | |
| } else { | |
| process.env.AI_GATEWAY_API_KEY = previousAiGatewayKey; | |
| } | |
| if (previousSshTty === undefined) { | |
| delete process.env.SSH_TTY; | |
| } else { | |
| process.env.SSH_TTY = previousSshTty; | |
| } | |
| if (previousChutesClientId === undefined) { | |
| delete process.env.CHUTES_CLIENT_ID; | |
| } else { | |
| process.env.CHUTES_CLIENT_ID = previousChutesClientId; | |
| } | |
| }); | |
| 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; | |
| 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 result = await applyAuthChoice({ | |
| authChoice: "minimax-api", | |
| config: {}, | |
| prompter, | |
| runtime, | |
| setDefaultModel: true, | |
| }); | |
| expect(text).toHaveBeenCalledWith( | |
| expect.objectContaining({ message: "Enter MiniMax API key" }), | |
| ); | |
| expect(result.config.auth?.profiles?.["minimax:default"]).toMatchObject({ | |
| provider: "minimax", | |
| mode: "api_key", | |
| }); | |
| const authProfilePath = authProfilePathFor(requireAgentDir()); | |
| const raw = await fs.readFile(authProfilePath, "utf8"); | |
| const parsed = JSON.parse(raw) as { | |
| profiles?: Record<string, { key?: string }>; | |
| }; | |
| expect(parsed.profiles?.["minimax: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; | |
| 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 result = await applyAuthChoice({ | |
| authChoice: "synthetic-api-key", | |
| config: {}, | |
| prompter, | |
| runtime, | |
| setDefaultModel: true, | |
| }); | |
| expect(text).toHaveBeenCalledWith( | |
| expect.objectContaining({ message: "Enter Synthetic API key" }), | |
| ); | |
| expect(result.config.auth?.profiles?.["synthetic:default"]).toMatchObject({ | |
| provider: "synthetic", | |
| mode: "api_key", | |
| }); | |
| const authProfilePath = authProfilePathFor(requireAgentDir()); | |
| const raw = await fs.readFile(authProfilePath, "utf8"); | |
| const parsed = JSON.parse(raw) as { | |
| profiles?: Record<string, { key?: string }>; | |
| }; | |
| expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-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; | |
| 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 previousTty = process.stdin.isTTY; | |
| const stdin = process.stdin as unknown as { isTTY?: boolean }; | |
| stdin.isTTY = true; | |
| try { | |
| const result = await applyAuthChoice({ | |
| authChoice: "github-copilot", | |
| config: {}, | |
| prompter, | |
| runtime, | |
| setDefaultModel: true, | |
| }); | |
| expect(result.config.agents?.defaults?.model?.primary).toBe("github-copilot/gpt-4o"); | |
| } finally { | |
| stdin.isTTY = previousTty; | |
| } | |
| }); | |
| 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; | |
| 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 result = await applyAuthChoice({ | |
| authChoice: "opencode-zen", | |
| config: { | |
| agents: { | |
| defaults: { | |
| model: { primary: "anthropic/claude-opus-4-5" }, | |
| }, | |
| }, | |
| }, | |
| prompter, | |
| runtime, | |
| setDefaultModel: false, | |
| }); | |
| expect(text).toHaveBeenCalledWith( | |
| expect.objectContaining({ message: "Enter OpenCode Zen API key" }), | |
| ); | |
| expect(result.config.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5"); | |
| expect(result.config.models?.providers?.["opencode-zen"]).toBeUndefined(); | |
| expect(result.agentModelOverride).toBe("opencode/claude-opus-4-5"); | |
| }); | |
| 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; | |
| process.env.OPENROUTER_API_KEY = "sk-openrouter-test"; | |
| const text = vi.fn(); | |
| const select: WizardPrompter["select"] = vi.fn( | |
| async (params) => params.options[0]?.value as never, | |
| ); | |
| 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 result = await applyAuthChoice({ | |
| authChoice: "openrouter-api-key", | |
| config: {}, | |
| prompter, | |
| runtime, | |
| setDefaultModel: true, | |
| }); | |
| expect(confirm).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| message: expect.stringContaining("OPENROUTER_API_KEY"), | |
| }), | |
| ); | |
| expect(text).not.toHaveBeenCalled(); | |
| expect(result.config.auth?.profiles?.["openrouter:default"]).toMatchObject({ | |
| provider: "openrouter", | |
| mode: "api_key", | |
| }); | |
| 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 { | |
| profiles?: Record<string, { key?: string }>; | |
| }; | |
| expect(parsed.profiles?.["openrouter:default"]?.key).toBe("sk-openrouter-test"); | |
| delete process.env.OPENROUTER_API_KEY; | |
| }); | |
| 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; | |
| process.env.AI_GATEWAY_API_KEY = "gateway-test-key"; | |
| const text = vi.fn(); | |
| const select: WizardPrompter["select"] = vi.fn( | |
| async (params) => params.options[0]?.value as never, | |
| ); | |
| 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 result = await applyAuthChoice({ | |
| authChoice: "ai-gateway-api-key", | |
| config: {}, | |
| prompter, | |
| runtime, | |
| setDefaultModel: true, | |
| }); | |
| expect(confirm).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| message: expect.stringContaining("AI_GATEWAY_API_KEY"), | |
| }), | |
| ); | |
| expect(text).not.toHaveBeenCalled(); | |
| expect(result.config.auth?.profiles?.["vercel-ai-gateway:default"]).toMatchObject({ | |
| provider: "vercel-ai-gateway", | |
| mode: "api_key", | |
| }); | |
| expect(result.config.agents?.defaults?.model?.primary).toBe( | |
| "vercel-ai-gateway/anthropic/claude-opus-4.5", | |
| ); | |
| const authProfilePath = authProfilePathFor(requireAgentDir()); | |
| const raw = await fs.readFile(authProfilePath, "utf8"); | |
| const parsed = JSON.parse(raw) as { | |
| profiles?: Record<string, { key?: string }>; | |
| }; | |
| expect(parsed.profiles?.["vercel-ai-gateway:default"]?.key).toBe("gateway-test-key"); | |
| delete process.env.AI_GATEWAY_API_KEY; | |
| }); | |
| 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; | |
| process.env.SSH_TTY = "1"; | |
| process.env.CHUTES_CLIENT_ID = "cid_test"; | |
| const fetchSpy = vi.fn(async (input: string | URL) => { | |
| const url = typeof input === "string" ? input : input.toString(); | |
| if (url === "https://api.chutes.ai/idp/token") { | |
| return new Response( | |
| JSON.stringify({ | |
| access_token: "at_test", | |
| refresh_token: "rt_test", | |
| expires_in: 3600, | |
| }), | |
| { status: 200, headers: { "Content-Type": "application/json" } }, | |
| ); | |
| } | |
| if (url === "https://api.chutes.ai/idp/userinfo") { | |
| return new Response(JSON.stringify({ username: "remote-user" }), { | |
| status: 200, | |
| headers: { "Content-Type": "application/json" }, | |
| }); | |
| } | |
| return new Response("not found", { status: 404 }); | |
| }); | |
| vi.stubGlobal("fetch", fetchSpy); | |
| const text = vi.fn().mockResolvedValue("code_manual"); | |
| 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 result = await applyAuthChoice({ | |
| authChoice: "chutes", | |
| config: {}, | |
| prompter, | |
| runtime, | |
| setDefaultModel: false, | |
| }); | |
| expect(text).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| message: "Paste the redirect URL (or authorization code)", | |
| }), | |
| ); | |
| expect(result.config.auth?.profiles?.["chutes:remote-user"]).toMatchObject({ | |
| provider: "chutes", | |
| mode: "oauth", | |
| }); | |
| const authProfilePath = authProfilePathFor(requireAgentDir()); | |
| const raw = await fs.readFile(authProfilePath, "utf8"); | |
| const parsed = JSON.parse(raw) as { | |
| profiles?: Record< | |
| string, | |
| { provider?: string; access?: string; refresh?: string; email?: string } | |
| >; | |
| }; | |
| expect(parsed.profiles?.["chutes:remote-user"]).toMatchObject({ | |
| provider: "chutes", | |
| access: "at_test", | |
| refresh: "rt_test", | |
| email: "remote-user", | |
| }); | |
| }); | |
| 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; | |
| resolvePluginProviders.mockReturnValue([ | |
| { | |
| id: "qwen-portal", | |
| label: "Qwen", | |
| auth: [ | |
| { | |
| id: "device", | |
| label: "Qwen OAuth", | |
| kind: "device_code", | |
| run: vi.fn(async () => ({ | |
| profiles: [ | |
| { | |
| profileId: "qwen-portal:default", | |
| credential: { | |
| type: "oauth", | |
| provider: "qwen-portal", | |
| access: "access", | |
| refresh: "refresh", | |
| expires: Date.now() + 60 * 60 * 1000, | |
| }, | |
| }, | |
| ], | |
| configPatch: { | |
| models: { | |
| providers: { | |
| "qwen-portal": { | |
| baseUrl: "https://portal.qwen.ai/v1", | |
| apiKey: "qwen-oauth", | |
| api: "openai-completions", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| }, | |
| defaultModel: "qwen-portal/coder-model", | |
| })), | |
| }, | |
| ], | |
| }, | |
| ]); | |
| 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 result = await applyAuthChoice({ | |
| authChoice: "qwen-portal", | |
| config: {}, | |
| prompter, | |
| runtime, | |
| setDefaultModel: true, | |
| }); | |
| expect(result.config.auth?.profiles?.["qwen-portal:default"]).toMatchObject({ | |
| provider: "qwen-portal", | |
| mode: "oauth", | |
| }); | |
| expect(result.config.agents?.defaults?.model?.primary).toBe("qwen-portal/coder-model"); | |
| expect(result.config.models?.providers?.["qwen-portal"]).toMatchObject({ | |
| baseUrl: "https://portal.qwen.ai/v1", | |
| apiKey: "qwen-oauth", | |
| }); | |
| const authProfilePath = authProfilePathFor(requireAgentDir()); | |
| const raw = await fs.readFile(authProfilePath, "utf8"); | |
| const parsed = JSON.parse(raw) as { | |
| profiles?: Record<string, { access?: string; refresh?: string; provider?: string }>; | |
| }; | |
| expect(parsed.profiles?.["qwen-portal:default"]).toMatchObject({ | |
| provider: "qwen-portal", | |
| access: "access", | |
| refresh: "refresh", | |
| }); | |
| }); | |
| 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; | |
| resolvePluginProviders.mockReturnValue([ | |
| { | |
| id: "minimax-portal", | |
| label: "MiniMax", | |
| auth: [ | |
| { | |
| id: "oauth", | |
| label: "MiniMax OAuth (Global)", | |
| kind: "device_code", | |
| run: vi.fn(async () => ({ | |
| profiles: [ | |
| { | |
| profileId: "minimax-portal:default", | |
| credential: { | |
| type: "oauth", | |
| provider: "minimax-portal", | |
| access: "access", | |
| refresh: "refresh", | |
| expires: Date.now() + 60 * 60 * 1000, | |
| }, | |
| }, | |
| ], | |
| configPatch: { | |
| models: { | |
| providers: { | |
| "minimax-portal": { | |
| baseUrl: "https://api.minimax.io/anthropic", | |
| apiKey: "minimax-oauth", | |
| api: "anthropic-messages", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| }, | |
| defaultModel: "minimax-portal/MiniMax-M2.1", | |
| })), | |
| }, | |
| ], | |
| }, | |
| ]); | |
| 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 result = await applyAuthChoice({ | |
| authChoice: "minimax-portal", | |
| config: {}, | |
| prompter, | |
| runtime, | |
| setDefaultModel: true, | |
| }); | |
| expect(result.config.auth?.profiles?.["minimax-portal:default"]).toMatchObject({ | |
| provider: "minimax-portal", | |
| mode: "oauth", | |
| }); | |
| expect(result.config.agents?.defaults?.model?.primary).toBe("minimax-portal/MiniMax-M2.1"); | |
| expect(result.config.models?.providers?.["minimax-portal"]).toMatchObject({ | |
| baseUrl: "https://api.minimax.io/anthropic", | |
| apiKey: "minimax-oauth", | |
| }); | |
| const authProfilePath = authProfilePathFor(requireAgentDir()); | |
| const raw = await fs.readFile(authProfilePath, "utf8"); | |
| const parsed = JSON.parse(raw) as { | |
| profiles?: Record<string, { access?: string; refresh?: string; provider?: string }>; | |
| }; | |
| expect(parsed.profiles?.["minimax-portal:default"]).toMatchObject({ | |
| provider: "minimax-portal", | |
| access: "access", | |
| refresh: "refresh", | |
| }); | |
| }); | |
| }); | |
| describe("resolvePreferredProviderForAuthChoice", () => { | |
| it("maps github-copilot to the provider", () => { | |
| expect(resolvePreferredProviderForAuthChoice("github-copilot")).toBe("github-copilot"); | |
| }); | |
| it("maps qwen-portal to the provider", () => { | |
| expect(resolvePreferredProviderForAuthChoice("qwen-portal")).toBe("qwen-portal"); | |
| }); | |
| it("returns undefined for unknown choices", () => { | |
| expect(resolvePreferredProviderForAuthChoice("unknown" as AuthChoice)).toBeUndefined(); | |
| }); | |
| }); | |