diff --git a/src/commands/onboard-auth.test.ts b/src/commands/onboard-auth.test.ts index a8577376992..d33504b1124 100644 --- a/src/commands/onboard-auth.test.ts +++ b/src/commands/onboard-auth.test.ts @@ -14,6 +14,8 @@ import { setupAuthTestEnv, } from "./test-wizard-helpers.js"; +const legacyAuthProfilePathFor = (dir: string) => path.join(dir, "auth-profiles.json"); + const providerEnvVarsById = vi.hoisted( (): Record => ({ "cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"], @@ -30,43 +32,6 @@ vi.mock("../config/paths.js", () => ({ resolveStateDir: () => process.env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw-state", })); -vi.mock("../agents/auth-profiles/profiles.js", async () => { - const fs = await import("node:fs"); - const path = await import("node:path"); - return { - upsertAuthProfile: (params: { profileId: string; credential: unknown; agentDir?: string }) => { - const stateDir = process.env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw-state"; - const agentDir = params.agentDir ?? path.join(stateDir, "agents", "main", "agent"); - const file = path.join(agentDir, "auth-profiles.json"); - fs.mkdirSync(agentDir, { recursive: true }); - const existing = (() => { - try { - return JSON.parse(fs.readFileSync(file, "utf8")) as { - version?: number; - profiles?: Record; - }; - } catch { - return { version: 1, profiles: {} }; - } - })(); - fs.writeFileSync( - file, - `${JSON.stringify( - { - version: existing.version ?? 1, - profiles: { - ...existing.profiles, - [params.profileId]: params.credential, - }, - }, - null, - 2, - )}\n`, - ); - }, - }; -}); - vi.mock("../agents/provider-auth-aliases.js", () => ({ resolveProviderIdForAuth: (provider: string) => { const normalized = provider.trim().toLowerCase(); @@ -90,13 +55,12 @@ describe("writeOAuthCredentials", () => { ]); let tempStateDir: string; - const authProfilePathFor = (dir: string) => path.join(dir, "auth-profiles.json"); afterEach(async () => { await lifecycle.cleanup(); }); - it("writes auth-profiles.json under the default agent dir", async () => { + it("writes OAuth credentials under the default agent dir SQLite store", async () => { const env = await setupAuthTestEnv("openclaw-oauth-"); lifecycle.setStateDir(env.stateDir); const defaultAgentDir = path.join(env.stateDir, "agents", "main", "agent"); @@ -118,8 +82,11 @@ describe("writeOAuthCredentials", () => { type: "oauth", }); + await expect(fs.readFile(legacyAuthProfilePathFor(env.agentDir), "utf8")).rejects.toMatchObject( + { code: "ENOENT" }, + ); await expect( - fs.readFile(path.join(env.agentDir, "auth-profiles.json"), "utf8"), + fs.readFile(legacyAuthProfilePathFor(defaultAgentDir), "utf8"), ).rejects.toMatchObject({ code: "ENOENT" }); }); @@ -148,15 +115,17 @@ describe("writeOAuthCredentials", () => { }); for (const dir of [mainAgentDir, kidAgentDir, workerAgentDir]) { - const raw = await fs.readFile(authProfilePathFor(dir), "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = await readAuthProfilesForAgent<{ profiles?: Record; - }; + }>(dir); expect(parsed.profiles?.["openai-codex:default"]).toMatchObject({ refresh: "refresh-sync", access: "access-sync", type: "oauth", }); + await expect(fs.readFile(legacyAuthProfilePathFor(dir), "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); } }); @@ -180,18 +149,25 @@ describe("writeOAuthCredentials", () => { await writeOAuthCredentials("openai-codex", creds, kidAgentDir); - const kidRaw = await fs.readFile(authProfilePathFor(kidAgentDir), "utf8"); - const kidParsed = JSON.parse(kidRaw) as { + const kidParsed = await readAuthProfilesForAgent<{ profiles?: Record; - }; + }>(kidAgentDir); expect(kidParsed.profiles?.["openai-codex:default"]).toMatchObject({ access: "access-kid", type: "oauth", }); - await expect(fs.readFile(authProfilePathFor(mainAgentDir), "utf8")).rejects.toMatchObject({ + await expect(readAuthProfilesForAgent(mainAgentDir)).rejects.toThrow( + "Expected SQLite auth profile store", + ); + await expect(fs.readFile(legacyAuthProfilePathFor(kidAgentDir), "utf8")).rejects.toMatchObject({ code: "ENOENT", }); + await expect(fs.readFile(legacyAuthProfilePathFor(mainAgentDir), "utf8")).rejects.toMatchObject( + { + code: "ENOENT", + }, + ); }); it("syncs siblings from explicit agentDir outside OPENCLAW_STATE_DIR", async () => { @@ -219,20 +195,25 @@ describe("writeOAuthCredentials", () => { // All siblings under the external root should have credentials for (const dir of [extMain, extKid, extWorker]) { - const raw = await fs.readFile(authProfilePathFor(dir), "utf8"); - const parsed = JSON.parse(raw) as { + const parsed = await readAuthProfilesForAgent<{ profiles?: Record; - }; + }>(dir); expect(parsed.profiles?.["openai-codex:default"]).toMatchObject({ refresh: "refresh-ext", access: "access-ext", type: "oauth", }); + await expect(fs.readFile(legacyAuthProfilePathFor(dir), "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); } // Global state dir should NOT have credentials written const globalMain = path.join(tempStateDir, "agents", "main", "agent"); - await expect(fs.readFile(authProfilePathFor(globalMain), "utf8")).rejects.toMatchObject({ + await expect(readAuthProfilesForAgent(globalMain)).rejects.toThrow( + "Expected SQLite auth profile store", + ); + await expect(fs.readFile(legacyAuthProfilePathFor(globalMain), "utf8")).rejects.toMatchObject({ code: "ENOENT", }); }); @@ -411,8 +392,11 @@ describe("upsertApiKeyProfile", () => { key: "sk-minimax-test", }); + await expect(fs.readFile(legacyAuthProfilePathFor(env.agentDir), "utf8")).rejects.toMatchObject( + { code: "ENOENT" }, + ); await expect( - fs.readFile(path.join(env.agentDir, "auth-profiles.json"), "utf8"), + fs.readFile(legacyAuthProfilePathFor(defaultAgentDir), "utf8"), ).rejects.toMatchObject({ code: "ENOENT" }); }); }); diff --git a/src/plugins/provider-auth-helpers.ts b/src/plugins/provider-auth-helpers.ts index 0d749d7ef9a..4085124e3ef 100644 --- a/src/plugins/provider-auth-helpers.ts +++ b/src/plugins/provider-auth-helpers.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { resolveDefaultAgentDir } from "../agents/agent-scope-config.js"; import { buildAuthProfileId } from "../agents/auth-profiles/identity.js"; -import { upsertAuthProfile } from "../agents/auth-profiles/profiles.js"; +import { upsertAuthProfile, upsertAuthProfileWithLock } from "../agents/auth-profiles/profiles.js"; import type { OAuthCredentials } from "../agents/pi-ai-contract.js"; import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js"; import { resolveStateDir } from "../config/paths.js"; @@ -261,7 +261,7 @@ function resolveSiblingAgentDirs(primaryAgentDir: string): string[] { const real = safeRealpathSync(dir); if (real && !seen.has(real)) { seen.add(real); - result.push(real); + result.push(dir); } } return result; @@ -291,7 +291,7 @@ export async function writeOAuthCredentials( ...(options?.displayName ? { displayName: options.displayName } : {}), }; - upsertAuthProfile({ + await upsertAuthProfileWithLock({ profileId, credential, agentDir: resolvedAgentDir, @@ -305,7 +305,7 @@ export async function writeOAuthCredentials( continue; } try { - upsertAuthProfile({ + await upsertAuthProfileWithLock({ profileId, credential, agentDir: targetAgentDir, diff --git a/test/helpers/auth-wizard.ts b/test/helpers/auth-wizard.ts index 9b58b4f6d3a..2ca181338e7 100644 --- a/test/helpers/auth-wizard.ts +++ b/test/helpers/auth-wizard.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { vi } from "vitest"; +import { loadPersistedAuthProfileStore } from "../../src/agents/auth-profiles/persisted.js"; import type { RuntimeEnv } from "../../src/runtime.js"; import { makeTempWorkspace } from "../../src/test-helpers/workspace.js"; import { captureEnv } from "../../src/test-utils/env.js"; @@ -82,11 +83,10 @@ export function requireOpenClawAgentDir(): string { return agentDir; } -function authProfilePathForAgent(agentDir: string): string { - return path.join(agentDir, "auth-profiles.json"); -} - export async function readAuthProfilesForAgent(agentDir: string): Promise { - const raw = await fs.readFile(authProfilePathForAgent(agentDir), "utf8"); - return JSON.parse(raw) as T; + const store = loadPersistedAuthProfileStore(agentDir); + if (!store) { + throw new Error(`Expected SQLite auth profile store for ${agentDir}`); + } + return store as T; }