import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; const mocks = vi.hoisted(() => ({ clackCancel: vi.fn(), clackConfirm: vi.fn(), clackIsCancel: vi.fn((value: unknown) => value === Symbol.for("clack:cancel")), clackSelect: vi.fn(), clackText: vi.fn(), resolveDefaultAgentId: vi.fn(), resolveAgentDir: vi.fn(), resolveAgentWorkspaceDir: vi.fn(), resolveDefaultAgentWorkspaceDir: vi.fn(), upsertAuthProfile: vi.fn(), resolvePluginProviders: vi.fn(), createClackPrompter: vi.fn(), loginOpenAICodexOAuth: vi.fn(), writeOAuthCredentials: vi.fn(), loadValidConfigOrThrow: vi.fn(), updateConfig: vi.fn(), logConfigUpdated: vi.fn(), openUrl: vi.fn(), })); vi.mock("@clack/prompts", () => ({ cancel: mocks.clackCancel, confirm: mocks.clackConfirm, isCancel: mocks.clackIsCancel, select: mocks.clackSelect, text: mocks.clackText, })); vi.mock("../../agents/agent-scope.js", () => ({ resolveDefaultAgentId: mocks.resolveDefaultAgentId, resolveAgentDir: mocks.resolveAgentDir, resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir, })); vi.mock("../../agents/workspace.js", () => ({ resolveDefaultAgentWorkspaceDir: mocks.resolveDefaultAgentWorkspaceDir, })); vi.mock("../../agents/auth-profiles.js", () => ({ upsertAuthProfile: mocks.upsertAuthProfile, })); vi.mock("../../plugins/providers.js", () => ({ resolvePluginProviders: mocks.resolvePluginProviders, })); vi.mock("../../wizard/clack-prompter.js", () => ({ createClackPrompter: mocks.createClackPrompter, })); vi.mock("../openai-codex-oauth.js", () => ({ loginOpenAICodexOAuth: mocks.loginOpenAICodexOAuth, })); vi.mock("../onboard-auth.js", async (importActual) => { const actual = await importActual(); return { ...actual, writeOAuthCredentials: mocks.writeOAuthCredentials, }; }); vi.mock("./shared.js", async (importActual) => { const actual = await importActual(); return { ...actual, loadValidConfigOrThrow: mocks.loadValidConfigOrThrow, updateConfig: mocks.updateConfig, }; }); vi.mock("../../config/logging.js", () => ({ logConfigUpdated: mocks.logConfigUpdated, })); vi.mock("../onboard-helpers.js", () => ({ openUrl: mocks.openUrl, })); const { modelsAuthLoginCommand, modelsAuthPasteTokenCommand } = await import("./auth.js"); function createRuntime(): RuntimeEnv { return { log: vi.fn(), error: vi.fn(), exit: vi.fn(), }; } function withInteractiveStdin() { const stdin = process.stdin as NodeJS.ReadStream & { isTTY?: boolean }; const hadOwnIsTTY = Object.prototype.hasOwnProperty.call(stdin, "isTTY"); const previousIsTTYDescriptor = Object.getOwnPropertyDescriptor(stdin, "isTTY"); Object.defineProperty(stdin, "isTTY", { configurable: true, enumerable: true, get: () => true, }); return () => { if (previousIsTTYDescriptor) { Object.defineProperty(stdin, "isTTY", previousIsTTYDescriptor); } else if (!hadOwnIsTTY) { delete (stdin as { isTTY?: boolean }).isTTY; } }; } describe("modelsAuthLoginCommand", () => { let restoreStdin: (() => void) | null = null; let currentConfig: OpenClawConfig; let lastUpdatedConfig: OpenClawConfig | null; beforeEach(() => { vi.clearAllMocks(); restoreStdin = withInteractiveStdin(); currentConfig = {}; lastUpdatedConfig = null; mocks.clackCancel.mockReset(); mocks.clackConfirm.mockReset(); mocks.clackIsCancel.mockImplementation( (value: unknown) => value === Symbol.for("clack:cancel"), ); mocks.clackSelect.mockReset(); mocks.clackText.mockReset(); mocks.upsertAuthProfile.mockReset(); mocks.resolveDefaultAgentId.mockReturnValue("main"); mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw/agents/main"); mocks.resolveAgentWorkspaceDir.mockReturnValue("/tmp/openclaw/workspace"); mocks.resolveDefaultAgentWorkspaceDir.mockReturnValue("/tmp/openclaw/workspace"); mocks.loadValidConfigOrThrow.mockImplementation(async () => currentConfig); mocks.updateConfig.mockImplementation( async (mutator: (cfg: OpenClawConfig) => OpenClawConfig) => { lastUpdatedConfig = mutator(currentConfig); currentConfig = lastUpdatedConfig; return lastUpdatedConfig; }, ); mocks.createClackPrompter.mockReturnValue({ note: vi.fn(async () => {}), select: vi.fn(), }); mocks.loginOpenAICodexOAuth.mockResolvedValue({ type: "oauth", provider: "openai-codex", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, email: "user@example.com", }); mocks.writeOAuthCredentials.mockResolvedValue("openai-codex:user@example.com"); mocks.resolvePluginProviders.mockReturnValue([]); }); afterEach(() => { restoreStdin?.(); restoreStdin = null; }); it("supports built-in openai-codex login without provider plugins", async () => { const runtime = createRuntime(); await modelsAuthLoginCommand({ provider: "openai-codex" }, runtime); expect(mocks.loginOpenAICodexOAuth).toHaveBeenCalledOnce(); expect(mocks.writeOAuthCredentials).toHaveBeenCalledWith( "openai-codex", expect.any(Object), "/tmp/openclaw/agents/main", { syncSiblingAgents: true }, ); expect(mocks.resolvePluginProviders).not.toHaveBeenCalled(); expect(lastUpdatedConfig?.auth?.profiles?.["openai-codex:user@example.com"]).toMatchObject({ provider: "openai-codex", mode: "oauth", }); expect(runtime.log).toHaveBeenCalledWith( "Auth profile: openai-codex:user@example.com (openai-codex/oauth)", ); expect(runtime.log).toHaveBeenCalledWith( "Default model available: openai-codex/gpt-5.4 (use --set-default to apply)", ); }); it("applies openai-codex default model when --set-default is used", async () => { const runtime = createRuntime(); await modelsAuthLoginCommand({ provider: "openai-codex", setDefault: true }, runtime); expect(lastUpdatedConfig?.agents?.defaults?.model).toEqual({ primary: "openai-codex/gpt-5.4", }); expect(runtime.log).toHaveBeenCalledWith("Default model set to openai-codex/gpt-5.4"); }); it("keeps existing plugin error behavior for non built-in providers", async () => { const runtime = createRuntime(); await expect(modelsAuthLoginCommand({ provider: "anthropic" }, runtime)).rejects.toThrow( "No provider plugins found.", ); }); it("does not persist a cancelled manual token entry", async () => { const runtime = createRuntime(); const exitSpy = vi.spyOn(process, "exit").mockImplementation((( code?: string | number | null, ) => { throw new Error(`exit:${String(code ?? "")}`); }) as typeof process.exit); try { const cancelSymbol = Symbol.for("clack:cancel"); mocks.clackText.mockResolvedValue(cancelSymbol); mocks.clackIsCancel.mockImplementation((value: unknown) => value === cancelSymbol); await expect(modelsAuthPasteTokenCommand({ provider: "openai" }, runtime)).rejects.toThrow( "exit:0", ); expect(mocks.upsertAuthProfile).not.toHaveBeenCalled(); expect(mocks.updateConfig).not.toHaveBeenCalled(); expect(mocks.logConfigUpdated).not.toHaveBeenCalled(); } finally { exitSpy.mockRestore(); } }); });