fix: keep oauth sibling sync sqlite-local

This commit is contained in:
Peter Steinberger
2026-05-10 00:16:12 +01:00
parent 81a459c024
commit 5dce34d2e2
3 changed files with 46 additions and 62 deletions

View File

@@ -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<string, readonly string[]> => ({
"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<string, unknown>;
};
} 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<string, OAuthCredentials & { type?: string }>;
};
}>(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<string, OAuthCredentials & { type?: string }>;
};
}>(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<string, OAuthCredentials & { type?: string }>;
};
}>(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" });
});
});

View File

@@ -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,

View File

@@ -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<T>(agentDir: string): Promise<T> {
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;
}