test(auth): update auth profile coverage

This commit is contained in:
Peter Steinberger
2026-01-26 19:04:42 +00:00
parent 526303d9a2
commit aa2a1a17e3
14 changed files with 121 additions and 849 deletions

View File

@@ -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 });
}
});
});
});

View File

@@ -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 });
}
});
});

View File

@@ -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 });
}
});
});

View File

@@ -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 });
}
});
});

View File

@@ -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 });
}
});
});

View File

@@ -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 });
}
});
});

View File

@@ -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 });
}
});
});

View File

@@ -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({

View File

@@ -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) {

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { type AuthProfileStore, CLAUDE_CLI_PROFILE_ID } from "../agents/auth-profiles.js";
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
describe("buildAuthChoiceOptions", () => {
@@ -9,60 +9,18 @@ describe("buildAuthChoiceOptions", () => {
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: false,
platform: "linux",
});
expect(options.find((opt) => opt.value === "github-copilot")).toBeDefined();
});
it("includes Claude Code CLI option on macOS even when missing", () => {
it("includes setup-token option for Anthropic", () => {
const store: AuthProfileStore = { version: 1, profiles: {} };
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
const claudeCli = options.find((opt) => opt.value === "claude-cli");
expect(claudeCli).toBeDefined();
expect(claudeCli?.hint).toBe("reuses existing Claude Code auth · requires Keychain access");
});
it("skips missing Claude Code CLI option off macOS", () => {
const store: AuthProfileStore = { version: 1, profiles: {} };
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "linux",
});
expect(options.find((opt) => opt.value === "claude-cli")).toBeUndefined();
});
it("uses token hint when Claude Code CLI credentials exist", () => {
const store: AuthProfileStore = {
version: 1,
profiles: {
[CLAUDE_CLI_PROFILE_ID]: {
type: "token",
provider: "anthropic",
token: "token",
expires: Date.now() + 60 * 60 * 1000,
},
},
};
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
const claudeCli = options.find((opt) => opt.value === "claude-cli");
expect(claudeCli?.hint).toContain("token ok");
expect(options.some((opt) => opt.value === "token")).toBe(true);
});
it("includes Z.AI (GLM) auth choice", () => {
@@ -70,8 +28,6 @@ describe("buildAuthChoiceOptions", () => {
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
expect(options.some((opt) => opt.value === "zai-api-key")).toBe(true);
@@ -82,8 +38,6 @@ describe("buildAuthChoiceOptions", () => {
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
expect(options.some((opt) => opt.value === "minimax-api")).toBe(true);
@@ -95,8 +49,6 @@ describe("buildAuthChoiceOptions", () => {
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
expect(options.some((opt) => opt.value === "moonshot-api-key")).toBe(true);
@@ -108,8 +60,6 @@ describe("buildAuthChoiceOptions", () => {
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
expect(options.some((opt) => opt.value === "ai-gateway-api-key")).toBe(true);
@@ -120,8 +70,6 @@ describe("buildAuthChoiceOptions", () => {
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
expect(options.some((opt) => opt.value === "synthetic-api-key")).toBe(true);
@@ -132,8 +80,6 @@ describe("buildAuthChoiceOptions", () => {
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
expect(options.some((opt) => opt.value === "chutes")).toBe(true);
@@ -144,8 +90,6 @@ describe("buildAuthChoiceOptions", () => {
const options = buildAuthChoiceOptions({
store,
includeSkip: false,
includeClaudeCliIfMissing: true,
platform: "darwin",
});
expect(options.some((opt) => opt.value === "qwen-portal")).toBe(true);

View File

@@ -244,7 +244,7 @@ describe("channels command", () => {
authMocks.loadAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
"anthropic:claude-cli": {
"anthropic:default": {
type: "oauth",
provider: "anthropic",
access: "token",
@@ -252,7 +252,7 @@ describe("channels command", () => {
expires: 0,
created: 0,
},
"openai-codex:codex-cli": {
"openai-codex:default": {
type: "oauth",
provider: "openai",
access: "token",
@@ -268,8 +268,8 @@ describe("channels command", () => {
auth?: Array<{ id: string }>;
};
const ids = payload.auth?.map((entry) => entry.id) ?? [];
expect(ids).toContain("anthropic:claude-cli");
expect(ids).toContain("openai-codex:codex-cli");
expect(ids).toContain("anthropic:default");
expect(ids).toContain("openai-codex:default");
});
it("stores default account names in accounts when multiple accounts exist", async () => {

View File

@@ -0,0 +1,109 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { maybeRemoveDeprecatedCliAuthProfiles } from "./doctor-auth.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
let originalAgentDir: string | undefined;
let originalPiAgentDir: string | undefined;
let tempAgentDir: string | undefined;
function makePrompter(confirmValue: boolean): DoctorPrompter {
return {
confirm: vi.fn().mockResolvedValue(confirmValue),
confirmRepair: vi.fn().mockResolvedValue(confirmValue),
confirmAggressive: vi.fn().mockResolvedValue(confirmValue),
confirmSkipInNonInteractive: vi.fn().mockResolvedValue(confirmValue),
select: vi.fn().mockResolvedValue(""),
shouldRepair: confirmValue,
shouldForce: false,
};
}
beforeEach(() => {
originalAgentDir = process.env.CLAWDBOT_AGENT_DIR;
originalPiAgentDir = process.env.PI_CODING_AGENT_DIR;
tempAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-auth-"));
process.env.CLAWDBOT_AGENT_DIR = tempAgentDir;
process.env.PI_CODING_AGENT_DIR = tempAgentDir;
});
afterEach(() => {
if (originalAgentDir === undefined) {
delete process.env.CLAWDBOT_AGENT_DIR;
} else {
process.env.CLAWDBOT_AGENT_DIR = originalAgentDir;
}
if (originalPiAgentDir === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = originalPiAgentDir;
}
if (tempAgentDir) {
fs.rmSync(tempAgentDir, { recursive: true, force: true });
tempAgentDir = undefined;
}
});
describe("maybeRemoveDeprecatedCliAuthProfiles", () => {
it("removes deprecated CLI auth profiles from store + config", async () => {
if (!tempAgentDir) throw new Error("Missing temp agent dir");
const authPath = path.join(tempAgentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
`${JSON.stringify(
{
version: 1,
profiles: {
"anthropic:claude-cli": {
type: "oauth",
provider: "anthropic",
access: "token-a",
refresh: "token-r",
expires: Date.now() + 60_000,
},
"openai-codex:codex-cli": {
type: "oauth",
provider: "openai-codex",
access: "token-b",
refresh: "token-r2",
expires: Date.now() + 60_000,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const cfg = {
auth: {
profiles: {
"anthropic:claude-cli": { provider: "anthropic", mode: "oauth" },
"openai-codex:codex-cli": { provider: "openai-codex", mode: "oauth" },
},
order: {
anthropic: ["anthropic:claude-cli"],
"openai-codex": ["openai-codex:codex-cli"],
},
},
} as const;
const next = await maybeRemoveDeprecatedCliAuthProfiles(cfg, makePrompter(true));
const raw = JSON.parse(fs.readFileSync(authPath, "utf8")) as {
profiles?: Record<string, unknown>;
};
expect(raw.profiles?.["anthropic:claude-cli"]).toBeUndefined();
expect(raw.profiles?.["openai-codex:codex-cli"]).toBeUndefined();
expect(next.auth?.profiles?.["anthropic:claude-cli"]).toBeUndefined();
expect(next.auth?.profiles?.["openai-codex:codex-cli"]).toBeUndefined();
expect(next.auth?.order?.anthropic).toBeUndefined();
expect(next.auth?.order?.["openai-codex"]).toBeUndefined();
});
});

View File

@@ -154,13 +154,13 @@ describe("applyAuthProfileConfig", () => {
},
},
{
profileId: "anthropic:claude-cli",
profileId: "anthropic:work",
provider: "anthropic",
mode: "oauth",
},
);
expect(next.auth?.order?.anthropic).toEqual(["anthropic:claude-cli", "anthropic:default"]);
expect(next.auth?.order?.anthropic).toEqual(["anthropic:work", "anthropic:default"]);
});
});

View File

@@ -335,81 +335,6 @@ describe("provider usage loading", () => {
);
});
it("prefers claude-cli token for Anthropic usage snapshots", async () => {
await withTempHome(
async () => {
const stateDir = process.env.CLAWDBOT_STATE_DIR;
if (!stateDir) throw new Error("Missing CLAWDBOT_STATE_DIR");
const agentDir = path.join(stateDir, "agents", "main", "agent");
fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
fs.writeFileSync(
path.join(agentDir, "auth-profiles.json"),
`${JSON.stringify(
{
version: 1,
profiles: {
"anthropic:default": {
type: "token",
provider: "anthropic",
token: "token-default",
expires: Date.UTC(2100, 0, 1, 0, 0, 0),
},
"anthropic:claude-cli": {
type: "token",
provider: "anthropic",
token: "token-cli",
expires: Date.UTC(2100, 0, 1, 0, 0, 0),
},
},
},
null,
2,
)}\n`,
"utf8",
);
const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body);
const headers =
typeof body === "string" ? undefined : { "Content-Type": "application/json" };
return new Response(payload, { status, headers });
};
const mockFetch = vi.fn<Parameters<typeof fetch>, ReturnType<typeof fetch>>(
async (input, init) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
if (url.includes("api.anthropic.com/api/oauth/usage")) {
const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers.Authorization).toBe("Bearer token-cli");
return makeResponse(200, {
five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" },
});
}
return makeResponse(404, "not found");
},
);
const summary = await loadProviderUsageSummary({
now: Date.UTC(2026, 0, 7, 0, 0, 0),
providers: ["anthropic"],
agentDir,
fetch: mockFetch,
});
expect(summary.providers).toHaveLength(1);
expect(summary.providers[0]?.provider).toBe("anthropic");
expect(summary.providers[0]?.windows[0]?.label).toBe("5h");
expect(mockFetch).toHaveBeenCalled();
},
{ prefix: "clawdbot-provider-usage-" },
);
});
it("falls back to claude.ai web usage when OAuth scope is missing", async () => {
const cookieSnapshot = process.env.CLAUDE_AI_SESSION_KEY;
process.env.CLAUDE_AI_SESSION_KEY = "sk-ant-web-1";