diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts index 2408a28a9bd..0449ea40325 100644 --- a/src/infra/provider-usage.auth.normalizes-keys.test.ts +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -1,8 +1,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { clearRuntimeAuthProfileStoreSnapshots } from "../agents/auth-profiles.js"; import { NON_ENV_SECRETREF_MARKER } from "../agents/model-auth-markers.js"; +import { clearConfigCache, type OpenClawConfig } from "../config/config.js"; const resolveProviderUsageAuthWithPluginMock = vi.fn(async (..._args: unknown[]) => null); @@ -10,6 +12,12 @@ vi.mock("../plugins/provider-runtime.js", () => ({ resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock, })); +vi.mock("../agents/cli-credentials.js", () => ({ + readCodexCliCredentialsCached: () => null, + readMiniMaxCliCredentialsCached: () => null, + readQwenCliCredentialsCached: () => null, +})); + let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths; type ProviderAuth = import("./provider-usage.auth.js").ProviderAuth; @@ -36,65 +44,59 @@ describe("resolveProviderAuths key normalization", () => { }); beforeEach(() => { + clearConfigCache(); + clearRuntimeAuthProfileStoreSnapshots(); resolveProviderUsageAuthWithPluginMock.mockReset(); resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null); }); - async function withSuiteHome( - fn: (home: string) => Promise, - env: Record, - ): Promise { + afterEach(() => { + clearConfigCache(); + clearRuntimeAuthProfileStoreSnapshots(); + }); + + async function withSuiteHome(fn: (home: string) => Promise): Promise { const base = path.join(suiteRoot, `case-${++suiteCase}`); await fs.mkdir(base, { recursive: true }); await fs.mkdir(path.join(base, ".openclaw", "agents", "main", "sessions"), { recursive: true }); + return await fn(base); + } - const keysToRestore = new Set([ - "HOME", - "USERPROFILE", - "HOMEDRIVE", - "HOMEPATH", - "OPENCLAW_HOME", - "OPENCLAW_STATE_DIR", - ...Object.keys(env), - ]); - const snapshot: Record = {}; - for (const key of keysToRestore) { - snapshot[key] = process.env[key]; - } + function agentDirForHome(home: string): string { + return path.join(home, ".openclaw", "agents", "main", "agent"); + } - process.env.HOME = base; - process.env.USERPROFILE = base; - if (process.platform === "win32") { - const match = base.match(/^([A-Za-z]:)(.*)$/); - if (match) { - process.env.HOMEDRIVE = match[1]; - process.env.HOMEPATH = match[2] || "\\"; - } - } - delete process.env.OPENCLAW_HOME; - process.env.OPENCLAW_STATE_DIR = path.join(base, ".openclaw"); - for (const [key, value] of Object.entries(env)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } + function buildSuiteEnv( + home: string, + env: Record = {}, + ): NodeJS.ProcessEnv { + const suiteEnv: NodeJS.ProcessEnv = { + ...EMPTY_PROVIDER_ENV, + HOME: home, + USERPROFILE: home, + OPENCLAW_STATE_DIR: path.join(home, ".openclaw"), + ...env, + }; + const match = home.match(/^([A-Za-z]:)(.*)$/); + if (match) { + suiteEnv.HOMEDRIVE = match[1]; + suiteEnv.HOMEPATH = match[2] || "\\"; } + return suiteEnv; + } + + async function readConfigForHome(home: string): Promise> { try { - return await fn(base); - } finally { - for (const [key, value] of Object.entries(snapshot)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } + return JSON.parse( + await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"), + ) as Record; + } catch { + return {}; } } async function writeAuthProfiles(home: string, profiles: Record) { - const agentDir = path.join(home, ".openclaw", "agents", "main", "agent"); + const agentDir = agentDirForHome(home); await fs.mkdir(agentDir, { recursive: true }); await fs.writeFile( path.join(agentDir, "auth-profiles.json"), @@ -114,7 +116,7 @@ describe("resolveProviderAuths key normalization", () => { } async function writeProfileOrder(home: string, provider: string, profileIds: string[]) { - const agentDir = path.join(home, ".openclaw", "agents", "main", "agent"); + const agentDir = agentDirForHome(home); const parsed = JSON.parse( await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf8"), ) as Record; @@ -149,28 +151,26 @@ describe("resolveProviderAuths key normalization", () => { } async function resolveMinimaxAuthFromConfiguredKey(apiKey: string) { - return await withSuiteHome( - async (home) => { - await writeConfig(home, { - models: { - providers: { - minimax: { - baseUrl: "https://api.minimaxi.com", - models: [createTestModelDefinition()], - apiKey, - }, + return await withSuiteHome(async (home) => { + await writeConfig(home, { + models: { + providers: { + minimax: { + baseUrl: "https://api.minimaxi.com", + models: [createTestModelDefinition()], + apiKey, }, }, - }); + }, + }); - return await resolveProviderAuths({ - providers: ["minimax"], - }); - }, - { - ...EMPTY_PROVIDER_ENV, - }, - ); + return await resolveProviderAuths({ + providers: ["minimax"], + agentDir: agentDirForHome(home), + config: (await readConfigForHome(home)) as OpenClawConfig, + env: buildSuiteEnv(home), + }); + }); } async function expectResolvedAuthsFromSuiteHome(params: { @@ -179,19 +179,16 @@ describe("resolveProviderAuths key normalization", () => { env?: Record; setup?: (home: string) => Promise; }) { - await withSuiteHome( - async (home) => { - await params.setup?.(home); - const auths = await resolveProviderAuths({ - providers: params.providers, - }); - expect(auths).toEqual(params.expected); - }, - { - ...EMPTY_PROVIDER_ENV, - ...params.env, - }, - ); + await withSuiteHome(async (home) => { + await params.setup?.(home); + const auths = await resolveProviderAuths({ + providers: params.providers, + agentDir: agentDirForHome(home), + config: (await readConfigForHome(home)) as OpenClawConfig, + env: buildSuiteEnv(home, params.env), + }); + expect(auths).toEqual(params.expected); + }); } it.each([ @@ -401,18 +398,24 @@ describe("resolveProviderAuths key normalization", () => { const auths = await resolveProviderAuths({ providers: ["anthropic"], + agentDir: agentDirForHome(home), + config: (await readConfigForHome(home)) as OpenClawConfig, + env: buildSuiteEnv(home), }); expect(auths).toEqual([]); - }, {}); + }); }); it("skips providers without oauth-compatible profiles", async () => { - await withSuiteHome(async () => { + await withSuiteHome(async (home) => { const auths = await resolveProviderAuths({ providers: ["anthropic"], + agentDir: agentDirForHome(home), + config: (await readConfigForHome(home)) as OpenClawConfig, + env: buildSuiteEnv(home), }); expect(auths).toEqual([]); - }, {}); + }); }); it("skips oauth profiles that resolve without an api key and uses later profiles", async () => { @@ -430,9 +433,12 @@ describe("resolveProviderAuths key normalization", () => { const auths = await resolveProviderAuths({ providers: ["anthropic"], + agentDir: agentDirForHome(home), + config: (await readConfigForHome(home)) as OpenClawConfig, + env: buildSuiteEnv(home), }); expect(auths).toEqual([{ provider: "anthropic", token: "anthropic-token" }]); - }, {}); + }); }); it("skips api_key entries in oauth token resolution order", async () => { @@ -445,9 +451,12 @@ describe("resolveProviderAuths key normalization", () => { const auths = await resolveProviderAuths({ providers: ["anthropic"], + agentDir: agentDirForHome(home), + config: (await readConfigForHome(home)) as OpenClawConfig, + env: buildSuiteEnv(home), }); expect(auths).toEqual([{ provider: "anthropic", token: "token-1" }]); - }, {}); + }); }); it("ignores marker-backed config keys for provider usage auth resolution", async () => { diff --git a/src/memory/manager.async-search.test.ts b/src/memory/manager.async-search.test.ts index 7b4855a3d6a..1cbec8f1155 100644 --- a/src/memory/manager.async-search.test.ts +++ b/src/memory/manager.async-search.test.ts @@ -66,8 +66,16 @@ describe("memory search async sync", () => { const cfg = buildConfig(); manager = await createMemoryManagerOrThrow(cfg); - const pending = new Promise(() => {}); - const syncMock = vi.fn(async () => pending); + let releaseSync = () => {}; + const pending = new Promise((resolve) => { + releaseSync = () => resolve(); + }).finally(() => { + (manager as unknown as { syncing: Promise | null }).syncing = null; + }); + const syncMock = vi.fn(async () => { + (manager as unknown as { syncing: Promise | null }).syncing = pending; + return pending; + }); (manager as unknown as { sync: () => Promise }).sync = syncMock; const activeManager = manager; @@ -76,6 +84,10 @@ describe("memory search async sync", () => { } await activeManager.search("hello"); expect(syncMock).toHaveBeenCalledTimes(1); + releaseSync(); + await vi.waitFor(() => { + expect((manager as unknown as { syncing: Promise | null }).syncing).toBeNull(); + }); }); it("waits for in-flight search sync during close", async () => {