diff --git a/src/agents/auth-profiles/session-override.test.ts b/src/agents/auth-profiles/session-override.test.ts index 6de4f361506..35f97f259a1 100644 --- a/src/agents/auth-profiles/session-override.test.ts +++ b/src/agents/auth-profiles/session-override.test.ts @@ -1,18 +1,77 @@ import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../../config/config.js"; -import type { SessionEntry } from "../../config/sessions.js"; -import { withStateDirEnv } from "../../test-helpers/state-dir-env.js"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { SessionEntry } from "../../config/sessions/types.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { resolveSessionAuthProfileOverride } from "./session-override.js"; +import type { AuthProfileStore } from "./types.js"; -vi.mock("../../plugins/provider-runtime.js", () => ({ - resolveExternalAuthProfilesWithPlugins: () => [], +const authStoreMocks = vi.hoisted(() => { + const normalizeProvider = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, ""); + const state: { hasSource: boolean; store: AuthProfileStore } = { + hasSource: false, + store: { version: 1, profiles: {} }, + }; + return { + state, + ensureAuthProfileStore: vi.fn(() => state.store), + hasAnyAuthProfileStoreSource: vi.fn(() => state.hasSource), + isProfileInCooldown: vi.fn(() => false), + reset() { + state.hasSource = false; + state.store = { version: 1, profiles: {} }; + }, + resolveAuthProfileOrder: vi.fn( + ({ store, provider }: { store: AuthProfileStore; provider: string }) => { + const providerKey = normalizeProvider(provider); + const ordered = Object.entries(store.order ?? {}).find( + ([key]) => normalizeProvider(key) === providerKey, + )?.[1]; + if (ordered) { + return ordered; + } + return Object.entries(store.profiles) + .filter(([, profile]) => normalizeProvider(profile.provider) === providerKey) + .map(([profileId]) => profileId); + }, + ), + }; +}); + +vi.mock("./store.js", () => ({ + ensureAuthProfileStore: authStoreMocks.ensureAuthProfileStore, + hasAnyAuthProfileStoreSource: authStoreMocks.hasAnyAuthProfileStoreSource, })); -async function writeAuthStore(agentDir: string) { - const authPath = path.join(agentDir, "auth-profiles.json"); - const payload = { +vi.mock("./order.js", () => ({ + resolveAuthProfileOrder: authStoreMocks.resolveAuthProfileOrder, +})); + +vi.mock("./usage.js", () => ({ + isProfileInCooldown: authStoreMocks.isProfileInCooldown, +})); + +async function withAuthStateDir(run: (params: { stateDir: string }) => Promise): Promise { + const previousStateDir = process.env.OPENCLAW_STATE_DIR; + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); + const stateDir = path.join(tempRoot, "state"); + process.env.OPENCLAW_STATE_DIR = stateDir; + try { + await fs.mkdir(stateDir, { recursive: true }); + return await run({ stateDir }); + } finally { + if (previousStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previousStateDir; + } + await fs.rm(tempRoot, { recursive: true, force: true }); + } +} + +function createAuthStore(): AuthProfileStore { + return { version: 1, profiles: { "zai:work": { type: "api_key", provider: "zai", key: "sk-test" }, @@ -21,38 +80,30 @@ async function writeAuthStore(agentDir: string) { zai: ["zai:work"], }, }; - await fs.writeFile(authPath, JSON.stringify(payload), "utf-8"); } -async function writeAuthStoreWithProfiles( - agentDir: string, - params: { - profiles: Record; - order?: Record; - }, -) { - const authPath = path.join(agentDir, "auth-profiles.json"); - await fs.writeFile( - authPath, - JSON.stringify( - { - version: 1, - profiles: params.profiles, - ...(params.order ? { order: params.order } : {}), - }, - null, - 2, - ), - "utf-8", - ); +function createAuthStoreWithProfiles(params: { + profiles: Record; + order?: Record; +}): AuthProfileStore { + return { + version: 1, + profiles: params.profiles, + ...(params.order ? { order: params.order } : {}), + }; } const TEST_PRIMARY_PROFILE_ID = "openai-codex:primary@example.test"; const TEST_SECONDARY_PROFILE_ID = "openai-codex:secondary@example.test"; describe("resolveSessionAuthProfileOverride", () => { + afterEach(() => { + authStoreMocks.reset(); + vi.clearAllMocks(); + }); + it("returns early when no auth sources exist", async () => { - await withStateDirEnv("openclaw-auth-", async ({ stateDir }) => { + await withAuthStateDir(async ({ stateDir }) => { const agentDir = path.join(stateDir, "agent"); await fs.mkdir(agentDir, { recursive: true }); @@ -74,6 +125,7 @@ describe("resolveSessionAuthProfileOverride", () => { }); expect(resolved).toBeUndefined(); + expect(authStoreMocks.ensureAuthProfileStore).not.toHaveBeenCalled(); await expect(fs.access(path.join(agentDir, "auth-profiles.json"))).rejects.toMatchObject({ code: "ENOENT", }); @@ -81,10 +133,11 @@ describe("resolveSessionAuthProfileOverride", () => { }); it("keeps user override when provider alias differs", async () => { - await withStateDirEnv("openclaw-auth-", async ({ stateDir }) => { + await withAuthStateDir(async ({ stateDir }) => { const agentDir = path.join(stateDir, "agent"); await fs.mkdir(agentDir, { recursive: true }); - await writeAuthStore(agentDir); + authStoreMocks.state.hasSource = true; + authStoreMocks.state.store = createAuthStore(); const sessionEntry: SessionEntry = { sessionId: "s1", @@ -111,10 +164,11 @@ describe("resolveSessionAuthProfileOverride", () => { }); it("keeps explicit user override when stored order prefers another profile", async () => { - await withStateDirEnv("openclaw-auth-", async ({ stateDir }) => { + await withAuthStateDir(async ({ stateDir }) => { const agentDir = path.join(stateDir, "agent"); await fs.mkdir(agentDir, { recursive: true }); - await writeAuthStoreWithProfiles(agentDir, { + authStoreMocks.state.hasSource = true; + authStoreMocks.state.store = createAuthStoreWithProfiles({ profiles: { [TEST_PRIMARY_PROFILE_ID]: { type: "api_key",