feat: Add test case for OAuth fallback failure when both secondary and main agent credentials are expired and migrate fs operations to promises API.
This commit is contained in:
@@ -1,30 +1,44 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { resolveApiKeyForProfile } from "./oauth.js";
|
import { resolveApiKeyForProfile } from "./oauth.js";
|
||||||
|
import { ensureAuthProfileStore } from "./store.js";
|
||||||
import type { AuthProfileStore } from "./types.js";
|
import type { AuthProfileStore } from "./types.js";
|
||||||
|
|
||||||
describe("resolveApiKeyForProfile", () => {
|
describe("resolveApiKeyForProfile fallback to main agent", () => {
|
||||||
|
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||||
|
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
|
||||||
|
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
let mainAgentDir: string;
|
let mainAgentDir: string;
|
||||||
let secondaryAgentDir: string;
|
let secondaryAgentDir: string;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "oauth-test-"));
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-fallback-test-"));
|
||||||
mainAgentDir = path.join(tmpDir, "agents", "main", "agent");
|
mainAgentDir = path.join(tmpDir, "agents", "main", "agent");
|
||||||
secondaryAgentDir = path.join(tmpDir, "agents", "kids", "agent");
|
secondaryAgentDir = path.join(tmpDir, "agents", "kids", "agent");
|
||||||
await fs.promises.mkdir(mainAgentDir, { recursive: true });
|
await fs.mkdir(mainAgentDir, { recursive: true });
|
||||||
await fs.promises.mkdir(secondaryAgentDir, { recursive: true });
|
await fs.mkdir(secondaryAgentDir, { recursive: true });
|
||||||
|
|
||||||
// Set env to use our temp dir
|
// Set environment variables so resolveClawdbotAgentDir() returns mainAgentDir
|
||||||
process.env.CLAWDBOT_STATE_DIR = tmpDir;
|
process.env.CLAWDBOT_STATE_DIR = tmpDir;
|
||||||
|
process.env.CLAWDBOT_AGENT_DIR = mainAgentDir;
|
||||||
|
process.env.PI_CODING_AGENT_DIR = mainAgentDir;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
delete process.env.CLAWDBOT_STATE_DIR;
|
vi.unstubAllGlobals();
|
||||||
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
||||||
vi.restoreAllMocks();
|
// Restore original environment
|
||||||
|
if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR;
|
||||||
|
else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
|
||||||
|
if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR;
|
||||||
|
else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
|
||||||
|
if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||||
|
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||||
|
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to main agent credentials when secondary agent token is expired and refresh fails", async () => {
|
it("falls back to main agent credentials when secondary agent token is expired and refresh fails", async () => {
|
||||||
@@ -46,7 +60,7 @@ describe("resolveApiKeyForProfile", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await fs.promises.writeFile(
|
await fs.writeFile(
|
||||||
path.join(secondaryAgentDir, "auth-profiles.json"),
|
path.join(secondaryAgentDir, "auth-profiles.json"),
|
||||||
JSON.stringify(secondaryStore),
|
JSON.stringify(secondaryStore),
|
||||||
);
|
);
|
||||||
@@ -64,15 +78,27 @@ describe("resolveApiKeyForProfile", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await fs.promises.writeFile(
|
await fs.writeFile(path.join(mainAgentDir, "auth-profiles.json"), JSON.stringify(mainStore));
|
||||||
path.join(mainAgentDir, "auth-profiles.json"),
|
|
||||||
JSON.stringify(mainStore),
|
|
||||||
);
|
|
||||||
|
|
||||||
// The secondary agent should fall back to main agent's credentials
|
// Mock fetch to simulate OAuth refresh failure
|
||||||
// when its own token refresh fails
|
const fetchSpy = vi.fn(async () => {
|
||||||
|
return new Response(JSON.stringify({ error: "invalid_grant" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchSpy);
|
||||||
|
|
||||||
|
// Load the secondary agent's store (will merge with main agent's store)
|
||||||
|
const loadedSecondaryStore = ensureAuthProfileStore(secondaryAgentDir);
|
||||||
|
|
||||||
|
// Call resolveApiKeyForProfile with the secondary agent's expired credentials
|
||||||
|
// This should:
|
||||||
|
// 1. Try to refresh the expired token (fails due to mocked fetch)
|
||||||
|
// 2. Fall back to main agent's fresh credentials
|
||||||
|
// 3. Copy those credentials to the secondary agent
|
||||||
const result = await resolveApiKeyForProfile({
|
const result = await resolveApiKeyForProfile({
|
||||||
store: secondaryStore,
|
store: loadedSecondaryStore,
|
||||||
profileId,
|
profileId,
|
||||||
agentDir: secondaryAgentDir,
|
agentDir: secondaryAgentDir,
|
||||||
});
|
});
|
||||||
@@ -83,11 +109,56 @@ describe("resolveApiKeyForProfile", () => {
|
|||||||
|
|
||||||
// Verify the credentials were copied to the secondary agent
|
// Verify the credentials were copied to the secondary agent
|
||||||
const updatedSecondaryStore = JSON.parse(
|
const updatedSecondaryStore = JSON.parse(
|
||||||
await fs.promises.readFile(path.join(secondaryAgentDir, "auth-profiles.json"), "utf8"),
|
await fs.readFile(path.join(secondaryAgentDir, "auth-profiles.json"), "utf8"),
|
||||||
) as AuthProfileStore;
|
) as AuthProfileStore;
|
||||||
expect(updatedSecondaryStore.profiles[profileId]).toMatchObject({
|
expect(updatedSecondaryStore.profiles[profileId]).toMatchObject({
|
||||||
access: "fresh-access-token",
|
access: "fresh-access-token",
|
||||||
expires: freshTime,
|
expires: freshTime,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("throws error when both secondary and main agent credentials are expired", async () => {
|
||||||
|
const profileId = "anthropic:claude-cli";
|
||||||
|
const now = Date.now();
|
||||||
|
const expiredTime = now - 60 * 60 * 1000; // 1 hour ago
|
||||||
|
|
||||||
|
// Write expired credentials for both agents
|
||||||
|
const expiredStore: AuthProfileStore = {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
[profileId]: {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "anthropic",
|
||||||
|
access: "expired-access-token",
|
||||||
|
refresh: "expired-refresh-token",
|
||||||
|
expires: expiredTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(secondaryAgentDir, "auth-profiles.json"),
|
||||||
|
JSON.stringify(expiredStore),
|
||||||
|
);
|
||||||
|
await fs.writeFile(path.join(mainAgentDir, "auth-profiles.json"), JSON.stringify(expiredStore));
|
||||||
|
|
||||||
|
// Mock fetch to simulate OAuth refresh failure
|
||||||
|
const fetchSpy = vi.fn(async () => {
|
||||||
|
return new Response(JSON.stringify({ error: "invalid_grant" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchSpy);
|
||||||
|
|
||||||
|
const loadedSecondaryStore = ensureAuthProfileStore(secondaryAgentDir);
|
||||||
|
|
||||||
|
// Should throw because both agents have expired credentials
|
||||||
|
await expect(
|
||||||
|
resolveApiKeyForProfile({
|
||||||
|
store: loadedSecondaryStore,
|
||||||
|
profileId,
|
||||||
|
agentDir: secondaryAgentDir,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/OAuth token refresh failed/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user