fix(sync): merge auth providers across all candidate files

readStoredAuthProvidersFromDisk() previously returned only the first
successfully-parsed auth file, missing providers that existed only in
fallback locations (e.g. github-copilot in ~/.pi/agent/auth.json when
another provider was in ~/.fusion/agent/auth.json). Now iterates all
candidates and merges entries with first-found-wins priority.
This commit is contained in:
gsxdsm
2026-04-26 11:35:44 -07:00
parent 4a07950ba8
commit 65956ee85b
2 changed files with 149 additions and 2 deletions

View File

@@ -0,0 +1,143 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mkdir, writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
// Mock getAuthFileCandidates to control which paths are checked
const mockAuthCandidates: string[] = [];
vi.mock("../auth-paths.js", () => ({
getAuthFileCandidates: () => mockAuthCandidates,
}));
// Re-import after mock
const { readStoredAuthProvidersFromDisk } = await import(
"../routes/register-settings-sync-helpers.js"
);
describe("readStoredAuthProvidersFromDisk", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = join(tmpdir(), `fn-sync-helpers-test-${Date.now()}`);
await mkdir(tempDir, { recursive: true });
mockAuthCandidates.length = 0;
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it("returns empty object when no auth files exist", async () => {
mockAuthCandidates.push(join(tempDir, "nonexistent.json"));
const result = await readStoredAuthProvidersFromDisk();
expect(result).toEqual({});
});
it("reads providers from a single auth file", async () => {
const authPath = join(tempDir, "auth.json");
await writeFile(
authPath,
JSON.stringify({
openrouter: { type: "api_key", key: "sk-or-123" },
}),
);
mockAuthCandidates.push(authPath);
const result = await readStoredAuthProvidersFromDisk();
expect(result).toEqual({
openrouter: { type: "api_key", key: "sk-or-123" },
});
});
it("merges providers across multiple auth files", async () => {
const fusionAuth = join(tempDir, "fusion-auth.json");
const piAuth = join(tempDir, "pi-auth.json");
await writeFile(
fusionAuth,
JSON.stringify({
openrouter: { type: "api_key", key: "sk-or-fusion" },
github: { type: "oauth", access: "gh-fusion-token" },
}),
);
await writeFile(
piAuth,
JSON.stringify({
minimax: { type: "api_key", key: "mm-pi-key" },
github: { type: "oauth", access: "gh-pi-token" },
}),
);
mockAuthCandidates.push(fusionAuth, piAuth);
const result = await readStoredAuthProvidersFromDisk();
expect(result).toEqual({
openrouter: { type: "api_key", key: "sk-or-fusion" },
github: { type: "oauth", access: "gh-fusion-token" },
minimax: { type: "api_key", key: "mm-pi-key" },
});
});
it("gives priority to first-found file for duplicate providers", async () => {
const firstAuth = join(tempDir, "first.json");
const secondAuth = join(tempDir, "second.json");
await writeFile(
firstAuth,
JSON.stringify({ openrouter: { type: "api_key", key: "first-key" } }),
);
await writeFile(
secondAuth,
JSON.stringify({ openrouter: { type: "api_key", key: "second-key" } }),
);
mockAuthCandidates.push(firstAuth, secondAuth);
const result = await readStoredAuthProvidersFromDisk();
expect(result.openrouter).toEqual({ type: "api_key", key: "first-key" });
});
it("skips invalid JSON files and continues to next candidate", async () => {
const badAuth = join(tempDir, "bad.json");
const goodAuth = join(tempDir, "good.json");
await writeFile(badAuth, "not valid json{{{");
await writeFile(
goodAuth,
JSON.stringify({ openrouter: { type: "api_key", key: "sk-good" } }),
);
mockAuthCandidates.push(badAuth, goodAuth);
const result = await readStoredAuthProvidersFromDisk();
expect(result).toEqual({
openrouter: { type: "api_key", key: "sk-good" },
});
});
it("picks up github-copilot from fallback when not in primary", async () => {
const fusionAuth = join(tempDir, "fusion-auth.json");
const piAuth = join(tempDir, "pi-auth.json");
await writeFile(
fusionAuth,
JSON.stringify({
openrouter: { type: "api_key", key: "sk-or-fusion" },
}),
);
await writeFile(
piAuth,
JSON.stringify({
"github-copilot": { type: "oauth", access: "copilot-token", refresh: "ghu_refresh", expires: 9999999999999 },
}),
);
mockAuthCandidates.push(fusionAuth, piAuth);
const result = await readStoredAuthProvidersFromDisk();
expect(result).toEqual({
openrouter: { type: "api_key", key: "sk-or-fusion" },
"github-copilot": { type: "oauth", access: "copilot-token", refresh: "ghu_refresh", expires: 9999999999999 },
});
});
});

View File

@@ -4,15 +4,19 @@ import { ApiError } from "../api-error.js";
import { getAuthFileCandidates, type StoredAuthProvider } from "../auth-paths.js";
export async function readStoredAuthProvidersFromDisk(): Promise<Record<string, StoredAuthProvider>> {
const merged: Record<string, StoredAuthProvider> = {};
for (const authJsonPath of getAuthFileCandidates()) {
try {
const authContent = await fsReadFile(authJsonPath, "utf-8");
return JSON.parse(authContent) as Record<string, StoredAuthProvider>;
const parsed = JSON.parse(authContent) as Record<string, StoredAuthProvider>;
for (const [provider, credential] of Object.entries(parsed)) {
merged[provider] ??= credential;
}
} catch {
// Try next candidate.
}
}
return {};
return merged;
}
/**