Merge branch 'main' into feat/tools-alsoAllow
This commit is contained in:
@@ -2,12 +2,10 @@ import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
type AuthProfileCredential,
|
||||
type AuthProfileStore,
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
resolveAuthProfileDisplayLabel,
|
||||
} from "./auth-profiles.js";
|
||||
|
||||
export type AuthProfileSource = "claude-cli" | "codex-cli" | "store";
|
||||
export type AuthProfileSource = "store";
|
||||
|
||||
export type AuthProfileHealthStatus = "ok" | "expiring" | "expired" | "missing" | "static";
|
||||
|
||||
@@ -41,9 +39,7 @@ export type AuthHealthSummary = {
|
||||
|
||||
export const DEFAULT_OAUTH_WARN_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export function resolveAuthProfileSource(profileId: string): AuthProfileSource {
|
||||
if (profileId === CLAUDE_CLI_PROFILE_ID) return "claude-cli";
|
||||
if (profileId === CODEX_CLI_PROFILE_ID) return "codex-cli";
|
||||
export function resolveAuthProfileSource(_profileId: string): AuthProfileSource {
|
||||
return "store";
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
import { AUTH_STORE_VERSION, CODEX_CLI_PROFILE_ID } from "./auth-profiles/constants.js";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js";
|
||||
|
||||
describe("ensureAuthProfileStore", () => {
|
||||
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
||||
@@ -123,80 +122,4 @@ describe("ensureAuthProfileStore", () => {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("drops codex-cli from merged store when a custom openai-codex profile matches", async () => {
|
||||
await withTempHome(async (tempHome) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-auth-dedup-merge-"));
|
||||
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
try {
|
||||
const mainDir = path.join(root, "main-agent");
|
||||
const agentDir = path.join(root, "agent-x");
|
||||
fs.mkdirSync(mainDir, { recursive: true });
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
process.env.CLAWDBOT_AGENT_DIR = mainDir;
|
||||
process.env.PI_CODING_AGENT_DIR = mainDir;
|
||||
process.env.HOME = tempHome;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(mainDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {
|
||||
[CODEX_CLI_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "shared-access-token",
|
||||
refresh: "shared-refresh-token",
|
||||
expires: Date.now() + 3600000,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {
|
||||
"openai-codex:my-custom-profile": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "shared-access-token",
|
||||
refresh: "shared-refresh-token",
|
||||
expires: Date.now() + 3600000,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined();
|
||||
expect(store.profiles["openai-codex:my-custom-profile"]).toBeDefined();
|
||||
} finally {
|
||||
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;
|
||||
}
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
|
||||
describe("external CLI credential sync", () => {
|
||||
it("does not overwrite API keys when syncing external CLI creds", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-no-overwrite-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
// Create Claude Code CLI credentials
|
||||
const claudeDir = path.join(tempHome, ".claude");
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
const claudeCreds = {
|
||||
claudeAiOauth: {
|
||||
accessToken: "cli-access",
|
||||
refreshToken: "cli-refresh",
|
||||
expiresAt: Date.now() + 30 * 60 * 1000,
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds));
|
||||
|
||||
// Create auth-profiles.json with an API key
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-store",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
// Should keep the store's API key and still add the CLI profile.
|
||||
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe("sk-store");
|
||||
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("prefers oauth over token even if token has later expiry (oauth enables auto-refresh)", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-oauth-preferred-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
const claudeDir = path.join(tempHome, ".claude");
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
// CLI has OAuth credentials (with refresh token) expiring in 30 min
|
||||
fs.writeFileSync(
|
||||
path.join(claudeDir, ".credentials.json"),
|
||||
JSON.stringify({
|
||||
claudeAiOauth: {
|
||||
accessToken: "cli-oauth-access",
|
||||
refreshToken: "cli-refresh",
|
||||
expiresAt: Date.now() + 30 * 60 * 1000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
// Store has token credentials expiring in 60 min (later than CLI)
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
[CLAUDE_CLI_PROFILE_ID]: {
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
token: "store-token-access",
|
||||
expires: Date.now() + 60 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
// OAuth should be preferred over token because it can auto-refresh
|
||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
expect(cliProfile.type).toBe("oauth");
|
||||
expect((cliProfile as { access: string }).access).toBe("cli-oauth-access");
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
|
||||
describe("external CLI credential sync", () => {
|
||||
it("does not overwrite fresher store oauth with older CLI oauth", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-oauth-no-downgrade-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
const claudeDir = path.join(tempHome, ".claude");
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
// CLI has OAuth credentials expiring in 30 min
|
||||
fs.writeFileSync(
|
||||
path.join(claudeDir, ".credentials.json"),
|
||||
JSON.stringify({
|
||||
claudeAiOauth: {
|
||||
accessToken: "cli-oauth-access",
|
||||
refreshToken: "cli-refresh",
|
||||
expiresAt: Date.now() + 30 * 60 * 1000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
// Store has OAuth credentials expiring in 60 min (later than CLI)
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
[CLAUDE_CLI_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "store-oauth-access",
|
||||
refresh: "store-refresh",
|
||||
expires: Date.now() + 60 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
// Fresher store oauth should be kept
|
||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
expect(cliProfile.type).toBe("oauth");
|
||||
expect((cliProfile as { access: string }).access).toBe("store-oauth-access");
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("does not downgrade store oauth to token when CLI lacks refresh token", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-oauth-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
const claudeDir = path.join(tempHome, ".claude");
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
// CLI has token-only credentials (no refresh token)
|
||||
fs.writeFileSync(
|
||||
path.join(claudeDir, ".credentials.json"),
|
||||
JSON.stringify({
|
||||
claudeAiOauth: {
|
||||
accessToken: "cli-token-access",
|
||||
expiresAt: Date.now() + 30 * 60 * 1000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
// Store already has OAuth credentials with refresh token
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
[CLAUDE_CLI_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "store-oauth-access",
|
||||
refresh: "store-refresh",
|
||||
expires: Date.now() + 60 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
// Keep oauth to preserve auto-refresh capability
|
||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
expect(cliProfile.type).toBe("oauth");
|
||||
expect((cliProfile as { access: string }).access).toBe("store-oauth-access");
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
|
||||
describe("external CLI credential sync", () => {
|
||||
it("skips codex-cli sync when credentials already exist in another openai-codex profile", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-dedup-skip-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
const codexDir = path.join(tempHome, ".codex");
|
||||
fs.mkdirSync(codexDir, { recursive: true });
|
||||
const codexAuthPath = path.join(codexDir, "auth.json");
|
||||
fs.writeFileSync(
|
||||
codexAuthPath,
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: "shared-access-token",
|
||||
refresh_token: "shared-refresh-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
fs.utimesSync(codexAuthPath, new Date(), new Date());
|
||||
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:my-custom-profile": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "shared-access-token",
|
||||
refresh: "shared-refresh-token",
|
||||
expires: Date.now() + 3600000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined();
|
||||
expect(store.profiles["openai-codex:my-custom-profile"]).toBeDefined();
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("creates codex-cli profile when credentials differ from existing openai-codex profiles", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-dedup-create-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
const codexDir = path.join(tempHome, ".codex");
|
||||
fs.mkdirSync(codexDir, { recursive: true });
|
||||
const codexAuthPath = path.join(codexDir, "auth.json");
|
||||
fs.writeFileSync(
|
||||
codexAuthPath,
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: "unique-access-token",
|
||||
refresh_token: "unique-refresh-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
fs.utimesSync(codexAuthPath, new Date(), new Date());
|
||||
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:my-custom-profile": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "different-access-token",
|
||||
refresh: "different-refresh-token",
|
||||
expires: Date.now() + 3600000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined();
|
||||
expect((store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access).toBe(
|
||||
"unique-access-token",
|
||||
);
|
||||
expect(store.profiles["openai-codex:my-custom-profile"]).toBeDefined();
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("removes codex-cli profile when it duplicates another openai-codex profile", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-dedup-remove-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
const codexDir = path.join(tempHome, ".codex");
|
||||
fs.mkdirSync(codexDir, { recursive: true });
|
||||
const codexAuthPath = path.join(codexDir, "auth.json");
|
||||
fs.writeFileSync(
|
||||
codexAuthPath,
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: "shared-access-token",
|
||||
refresh_token: "shared-refresh-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
fs.utimesSync(codexAuthPath, new Date(), new Date());
|
||||
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
[CODEX_CLI_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "shared-access-token",
|
||||
refresh: "shared-refresh-token",
|
||||
expires: Date.now() + 3600000,
|
||||
},
|
||||
"openai-codex:my-custom-profile": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "shared-access-token",
|
||||
refresh: "shared-refresh-token",
|
||||
expires: Date.now() + 3600000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined();
|
||||
const saved = JSON.parse(fs.readFileSync(authPath, "utf8")) as {
|
||||
profiles?: Record<string, unknown>;
|
||||
};
|
||||
expect(saved.profiles?.[CODEX_CLI_PROFILE_ID]).toBeUndefined();
|
||||
expect(saved.profiles?.["openai-codex:my-custom-profile"]).toBeDefined();
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
|
||||
describe("external CLI credential sync", () => {
|
||||
it("syncs Claude Code CLI OAuth credentials into anthropic:claude-cli", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-sync-"));
|
||||
try {
|
||||
// Create a temp home with Claude Code CLI credentials
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
// Create Claude Code CLI credentials with refreshToken (OAuth)
|
||||
const claudeDir = path.join(tempHome, ".claude");
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
const claudeCreds = {
|
||||
claudeAiOauth: {
|
||||
accessToken: "fresh-access-token",
|
||||
refreshToken: "fresh-refresh-token",
|
||||
expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds));
|
||||
|
||||
// Create empty auth-profiles.json
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-default",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Load the store - should sync from CLI as OAuth credential
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(store.profiles["anthropic:default"]).toBeDefined();
|
||||
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe("sk-default");
|
||||
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
||||
// Should be stored as OAuth credential (type: "oauth") for auto-refresh
|
||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
expect(cliProfile.type).toBe("oauth");
|
||||
expect((cliProfile as { access: string }).access).toBe("fresh-access-token");
|
||||
expect((cliProfile as { refresh: string }).refresh).toBe("fresh-refresh-token");
|
||||
expect((cliProfile as { expires: number }).expires).toBeGreaterThan(Date.now());
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("syncs Claude Code CLI credentials without refreshToken as token type", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-token-sync-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
// Create Claude Code CLI credentials WITHOUT refreshToken (fallback to token type)
|
||||
const claudeDir = path.join(tempHome, ".claude");
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
const claudeCreds = {
|
||||
claudeAiOauth: {
|
||||
accessToken: "access-only-token",
|
||||
// No refreshToken - backward compatibility scenario
|
||||
expiresAt: Date.now() + 60 * 60 * 1000,
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds));
|
||||
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(authPath, JSON.stringify({ version: 1, profiles: {} }));
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
||||
// Should be stored as token type (no refresh capability)
|
||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
expect(cliProfile.type).toBe("token");
|
||||
expect((cliProfile as { token: string }).token).toBe("access-only-token");
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
|
||||
describe("external CLI credential sync", () => {
|
||||
it("updates codex-cli profile when Codex CLI refresh token changes", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
const codexDir = path.join(tempHome, ".codex");
|
||||
fs.mkdirSync(codexDir, { recursive: true });
|
||||
const codexAuthPath = path.join(codexDir, "auth.json");
|
||||
fs.writeFileSync(
|
||||
codexAuthPath,
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: "same-access",
|
||||
refresh_token: "new-refresh",
|
||||
},
|
||||
}),
|
||||
);
|
||||
fs.utimesSync(codexAuthPath, new Date(), new Date());
|
||||
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
[CODEX_CLI_PROFILE_ID]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "same-access",
|
||||
refresh: "old-refresh",
|
||||
expires: Date.now() - 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
expect((store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }).refresh).toBe(
|
||||
"new-refresh",
|
||||
);
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
ensureAuthProfileStore,
|
||||
} from "./auth-profiles.js";
|
||||
|
||||
describe("external CLI credential sync", () => {
|
||||
it("upgrades token to oauth when Claude Code CLI gets refreshToken", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-upgrade-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
// Create Claude Code CLI credentials with refreshToken
|
||||
const claudeDir = path.join(tempHome, ".claude");
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(claudeDir, ".credentials.json"),
|
||||
JSON.stringify({
|
||||
claudeAiOauth: {
|
||||
accessToken: "new-oauth-access",
|
||||
refreshToken: "new-refresh-token",
|
||||
expiresAt: Date.now() + 60 * 60 * 1000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Create auth-profiles.json with existing token type credential
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {
|
||||
[CLAUDE_CLI_PROFILE_ID]: {
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
token: "old-token",
|
||||
expires: Date.now() + 30 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
// Should upgrade from token to oauth
|
||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
expect(cliProfile.type).toBe("oauth");
|
||||
expect((cliProfile as { access: string }).access).toBe("new-oauth-access");
|
||||
expect((cliProfile as { refresh: string }).refresh).toBe("new-refresh-token");
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("syncs Codex CLI credentials into openai-codex:codex-cli", async () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-sync-"));
|
||||
try {
|
||||
await withTempHome(
|
||||
async (tempHome) => {
|
||||
// Create Codex CLI credentials
|
||||
const codexDir = path.join(tempHome, ".codex");
|
||||
fs.mkdirSync(codexDir, { recursive: true });
|
||||
const codexCreds = {
|
||||
tokens: {
|
||||
access_token: "codex-access-token",
|
||||
refresh_token: "codex-refresh-token",
|
||||
},
|
||||
};
|
||||
const codexAuthPath = path.join(codexDir, "auth.json");
|
||||
fs.writeFileSync(codexAuthPath, JSON.stringify(codexCreds));
|
||||
|
||||
// Create empty auth-profiles.json
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
profiles: {},
|
||||
}),
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined();
|
||||
expect((store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access).toBe(
|
||||
"codex-access-token",
|
||||
);
|
||||
},
|
||||
{ prefix: "clawdbot-home-" },
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,22 +1,11 @@
|
||||
import { readQwenCliCredentialsCached } from "../cli-credentials.js";
|
||||
import {
|
||||
readClaudeCliCredentialsCached,
|
||||
readCodexCliCredentialsCached,
|
||||
readQwenCliCredentialsCached,
|
||||
} from "../cli-credentials.js";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
EXTERNAL_CLI_NEAR_EXPIRY_MS,
|
||||
EXTERNAL_CLI_SYNC_TTL_MS,
|
||||
QWEN_CLI_PROFILE_ID,
|
||||
log,
|
||||
} from "./constants.js";
|
||||
import type {
|
||||
AuthProfileCredential,
|
||||
AuthProfileStore,
|
||||
OAuthCredential,
|
||||
TokenCredential,
|
||||
} from "./types.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean {
|
||||
if (!a) return false;
|
||||
@@ -33,25 +22,10 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr
|
||||
);
|
||||
}
|
||||
|
||||
function shallowEqualTokenCredentials(a: TokenCredential | undefined, b: TokenCredential): boolean {
|
||||
if (!a) return false;
|
||||
if (a.type !== "token") return false;
|
||||
return (
|
||||
a.provider === b.provider &&
|
||||
a.token === b.token &&
|
||||
a.expires === b.expires &&
|
||||
a.email === b.email
|
||||
);
|
||||
}
|
||||
|
||||
function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean {
|
||||
if (!cred) return false;
|
||||
if (cred.type !== "oauth" && cred.type !== "token") return false;
|
||||
if (
|
||||
cred.provider !== "anthropic" &&
|
||||
cred.provider !== "openai-codex" &&
|
||||
cred.provider !== "qwen-portal"
|
||||
) {
|
||||
if (cred.provider !== "qwen-portal") {
|
||||
return false;
|
||||
}
|
||||
if (typeof cred.expires !== "number") return true;
|
||||
@@ -59,163 +33,14 @@ function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: nu
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any existing openai-codex profile (other than codex-cli) that has the same
|
||||
* access and refresh tokens. This prevents creating a duplicate codex-cli profile
|
||||
* when the user has already set up a custom profile with the same credentials.
|
||||
*/
|
||||
export function findDuplicateCodexProfile(
|
||||
store: AuthProfileStore,
|
||||
creds: OAuthCredential,
|
||||
): string | undefined {
|
||||
for (const [profileId, profile] of Object.entries(store.profiles)) {
|
||||
if (profileId === CODEX_CLI_PROFILE_ID) continue;
|
||||
if (profile.type !== "oauth") continue;
|
||||
if (profile.provider !== "openai-codex") continue;
|
||||
if (profile.access === creds.access && profile.refresh === creds.refresh) {
|
||||
return profileId;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync OAuth credentials from external CLI tools (Claude Code CLI, Codex CLI) into the store.
|
||||
* This allows clawdbot to use the same credentials as these tools without requiring
|
||||
* separate authentication, and keeps credentials in sync when CLI tools refresh tokens.
|
||||
* Sync OAuth credentials from external CLI tools (Qwen Code CLI) into the store.
|
||||
*
|
||||
* Returns true if any credentials were updated.
|
||||
*/
|
||||
export function syncExternalCliCredentials(
|
||||
store: AuthProfileStore,
|
||||
options?: { allowKeychainPrompt?: boolean },
|
||||
): boolean {
|
||||
export function syncExternalCliCredentials(store: AuthProfileStore): boolean {
|
||||
let mutated = false;
|
||||
const now = Date.now();
|
||||
|
||||
// Sync from Claude Code CLI (supports both OAuth and Token credentials)
|
||||
const existingClaude = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
const shouldSyncClaude =
|
||||
!existingClaude ||
|
||||
existingClaude.provider !== "anthropic" ||
|
||||
existingClaude.type === "token" ||
|
||||
!isExternalProfileFresh(existingClaude, now);
|
||||
const claudeCreds = shouldSyncClaude
|
||||
? readClaudeCliCredentialsCached({
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
ttlMs: EXTERNAL_CLI_SYNC_TTL_MS,
|
||||
})
|
||||
: null;
|
||||
if (claudeCreds) {
|
||||
const existing = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
const claudeCredsExpires = claudeCreds.expires ?? 0;
|
||||
|
||||
// Determine if we should update based on credential comparison
|
||||
let shouldUpdate = false;
|
||||
let isEqual = false;
|
||||
|
||||
if (claudeCreds.type === "oauth") {
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
isEqual = shallowEqualOAuthCredentials(existingOAuth, claudeCreds);
|
||||
// Update if: no existing profile, type changed to oauth, expired, or CLI has newer token
|
||||
shouldUpdate =
|
||||
!existingOAuth ||
|
||||
existingOAuth.provider !== "anthropic" ||
|
||||
existingOAuth.expires <= now ||
|
||||
(claudeCredsExpires > now && claudeCredsExpires > existingOAuth.expires);
|
||||
} else {
|
||||
const existingToken = existing?.type === "token" ? existing : undefined;
|
||||
isEqual = shallowEqualTokenCredentials(existingToken, claudeCreds);
|
||||
// Update if: no existing profile, expired, or CLI has newer token
|
||||
shouldUpdate =
|
||||
!existingToken ||
|
||||
existingToken.provider !== "anthropic" ||
|
||||
(existingToken.expires ?? 0) <= now ||
|
||||
(claudeCredsExpires > now && claudeCredsExpires > (existingToken.expires ?? 0));
|
||||
}
|
||||
|
||||
// Also update if credential type changed (token -> oauth upgrade)
|
||||
if (existing && existing.type !== claudeCreds.type) {
|
||||
// Prefer oauth over token (enables auto-refresh)
|
||||
if (claudeCreds.type === "oauth") {
|
||||
shouldUpdate = true;
|
||||
isEqual = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid downgrading from oauth to token-only credentials.
|
||||
if (existing?.type === "oauth" && claudeCreds.type === "token") {
|
||||
shouldUpdate = false;
|
||||
}
|
||||
|
||||
if (shouldUpdate && !isEqual) {
|
||||
store.profiles[CLAUDE_CLI_PROFILE_ID] = claudeCreds;
|
||||
mutated = true;
|
||||
log.info("synced anthropic credentials from claude cli", {
|
||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||
type: claudeCreds.type,
|
||||
expires:
|
||||
typeof claudeCreds.expires === "number"
|
||||
? new Date(claudeCreds.expires).toISOString()
|
||||
: "unknown",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sync from Codex CLI
|
||||
const existingCodex = store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
const existingCodexOAuth = existingCodex?.type === "oauth" ? existingCodex : undefined;
|
||||
const duplicateExistingId = existingCodexOAuth
|
||||
? findDuplicateCodexProfile(store, existingCodexOAuth)
|
||||
: undefined;
|
||||
if (duplicateExistingId) {
|
||||
delete store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
mutated = true;
|
||||
log.info("removed codex-cli profile: credentials already exist in another profile", {
|
||||
existingProfileId: duplicateExistingId,
|
||||
removedProfileId: CODEX_CLI_PROFILE_ID,
|
||||
});
|
||||
}
|
||||
const shouldSyncCodex =
|
||||
!existingCodex ||
|
||||
existingCodex.provider !== "openai-codex" ||
|
||||
!isExternalProfileFresh(existingCodex, now);
|
||||
const codexCreds =
|
||||
shouldSyncCodex || duplicateExistingId
|
||||
? readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS })
|
||||
: null;
|
||||
if (codexCreds) {
|
||||
const duplicateProfileId = findDuplicateCodexProfile(store, codexCreds);
|
||||
if (duplicateProfileId) {
|
||||
if (store.profiles[CODEX_CLI_PROFILE_ID]) {
|
||||
delete store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
mutated = true;
|
||||
log.info("removed codex-cli profile: credentials already exist in another profile", {
|
||||
existingProfileId: duplicateProfileId,
|
||||
removedProfileId: CODEX_CLI_PROFILE_ID,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const existing = store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
|
||||
// Codex creds don't carry expiry; use file mtime heuristic for freshness.
|
||||
const shouldUpdate =
|
||||
!existingOAuth ||
|
||||
existingOAuth.provider !== "openai-codex" ||
|
||||
existingOAuth.expires <= now ||
|
||||
codexCreds.expires > existingOAuth.expires;
|
||||
|
||||
if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, codexCreds)) {
|
||||
store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds;
|
||||
mutated = true;
|
||||
log.info("synced openai-codex credentials from codex cli", {
|
||||
profileId: CODEX_CLI_PROFILE_ID,
|
||||
expires: new Date(codexCreds.expires).toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync from Qwen Code CLI
|
||||
const existingQwen = store.profiles[QWEN_CLI_PROFILE_ID];
|
||||
const shouldSyncQwen =
|
||||
|
||||
@@ -4,8 +4,7 @@ import lockfile from "proper-lockfile";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { refreshChutesTokens } from "../chutes-oauth.js";
|
||||
import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js";
|
||||
import { writeClaudeCliCredentials } from "../cli-credentials.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, CLAUDE_CLI_PROFILE_ID } from "./constants.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS } from "./constants.js";
|
||||
import { formatAuthDoctorHint } from "./doctor.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
|
||||
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
||||
@@ -72,12 +71,6 @@ async function refreshOAuthTokenWithLock(params: {
|
||||
};
|
||||
saveAuthProfileStore(store, params.agentDir);
|
||||
|
||||
// Sync refreshed credentials back to Claude Code CLI if this is the claude-cli profile
|
||||
// This ensures Claude Code continues to work after ClawdBot refreshes the token
|
||||
if (params.profileId === CLAUDE_CLI_PROFILE_ID && cred.provider === "anthropic") {
|
||||
writeClaudeCliCredentials(result.newCredentials);
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
if (release) {
|
||||
|
||||
@@ -3,13 +3,8 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||
import lockfile from "proper-lockfile";
|
||||
import { resolveOAuthPath } from "../../config/paths.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
||||
import {
|
||||
AUTH_STORE_LOCK_OPTIONS,
|
||||
AUTH_STORE_VERSION,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
log,
|
||||
} from "./constants.js";
|
||||
import { findDuplicateCodexProfile, syncExternalCliCredentials } from "./external-cli-sync.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js";
|
||||
import { syncExternalCliCredentials } from "./external-cli-sync.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js";
|
||||
|
||||
@@ -229,14 +224,14 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
||||
|
||||
function loadAuthProfileStoreForAgent(
|
||||
agentDir?: string,
|
||||
options?: { allowKeychainPrompt?: boolean },
|
||||
_options?: { allowKeychainPrompt?: boolean },
|
||||
): AuthProfileStore {
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const raw = loadJsonFile(authPath);
|
||||
const asStore = coerceAuthStore(raw);
|
||||
if (asStore) {
|
||||
// Sync from external CLI tools on every load
|
||||
const synced = syncExternalCliCredentials(asStore, options);
|
||||
const synced = syncExternalCliCredentials(asStore);
|
||||
if (synced) {
|
||||
saveJsonFile(authPath, asStore);
|
||||
}
|
||||
@@ -297,7 +292,7 @@ function loadAuthProfileStoreForAgent(
|
||||
}
|
||||
|
||||
const mergedOAuth = mergeOAuthFileIntoStore(store);
|
||||
const syncedCli = syncExternalCliCredentials(store, options);
|
||||
const syncedCli = syncExternalCliCredentials(store);
|
||||
const shouldWrite = legacy !== null || mergedOAuth || syncedCli;
|
||||
if (shouldWrite) {
|
||||
saveJsonFile(authPath, store);
|
||||
@@ -337,15 +332,6 @@ export function ensureAuthProfileStore(
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||
const merged = mergeAuthProfileStores(mainStore, store);
|
||||
|
||||
// Keep per-agent view clean even if the main store has codex-cli.
|
||||
const codexProfile = merged.profiles[CODEX_CLI_PROFILE_ID];
|
||||
if (codexProfile?.type === "oauth") {
|
||||
const duplicateId = findDuplicateCodexProfile(merged, codexProfile);
|
||||
if (duplicateId) {
|
||||
delete merged.profiles[CODEX_CLI_PROFILE_ID];
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ describe("runWithModelFallback", () => {
|
||||
const cfg = makeCfg();
|
||||
const run = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('No credentials found for profile "anthropic:claude-cli".'))
|
||||
.mockRejectedValueOnce(new Error('No credentials found for profile "anthropic:default".'))
|
||||
.mockResolvedValueOnce("ok");
|
||||
|
||||
const result = await runWithModelFallback({
|
||||
|
||||
@@ -12,7 +12,7 @@ const _makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstr
|
||||
describe("isAuthErrorMessage", () => {
|
||||
it("matches credential validation errors", () => {
|
||||
const samples = [
|
||||
'No credentials found for profile "anthropic:claude-cli".',
|
||||
'No credentials found for profile "anthropic:default".',
|
||||
"No API key found for profile openai.",
|
||||
];
|
||||
for (const sample of samples) {
|
||||
|
||||
Reference in New Issue
Block a user