Files
bot-agents/profiles_test.ts
T
Danny Kim 449f00c736
Check / deno (pull_request) Successful in 55s
Add ordered bot profiles
2026-09-23 20:59:38 +09:00

237 lines
6.1 KiB
TypeScript

import { assertEquals, assertRejects, assertThrows } from "jsr:@std/assert@1";
import {
agentEnvironment,
parseProfiles,
readProfilesFile,
runProfiles,
sanitizedEnvironment,
} from "./profiles.ts";
Deno.test("parses profiles in fallback order with model defaults", () => {
assertEquals(
parseProfiles(
`
- type: claude
token-env: BOT_PROFILE_CLAUDE_PRIMARY
- type: codex
model: gpt-custom
`,
{ BOT_PROFILE_CLAUDE_PRIMARY: "claude-token" },
),
[
{
type: "claude",
model: "claude-sonnet-5",
token: "claude-token",
tokenEnv: "BOT_PROFILE_CLAUDE_PRIMARY",
},
{ type: "codex", model: "gpt-custom" },
],
);
});
Deno.test("rejects an empty profile list", () => {
assertThrows(() => parseProfiles("[]", {}), Error, "at least one profile");
});
Deno.test("rejects a Claude profile without its token environment variable", () => {
assertThrows(
() => parseProfiles("- type: claude", {}),
Error,
"profiles[0].token-env",
);
assertThrows(
() =>
parseProfiles(
"- type: claude\n token-env: BOT_PROFILE_CLAUDE_PRIMARY",
{},
),
Error,
"BOT_PROFILE_CLAUDE_PRIMARY is not set",
);
});
Deno.test("rejects non-profile credentials and unknown fields", () => {
assertThrows(
() =>
parseProfiles(
"- type: claude\n token-env: REVIEWER_TOKEN",
{ REVIEWER_TOKEN: "secret" },
),
Error,
"must start with BOT_PROFILE_",
);
assertThrows(
() =>
parseProfiles(
"- type: codex\n token: committed-secret",
{},
),
Error,
"profiles[0].token: unknown field",
);
});
Deno.test("reads only a profile file contained by the workspace", async () => {
const fileSystem = {
realPath(path: string) {
return Promise.resolve(path === "/repo/escaped.yml" ? "/outside" : path);
},
stat(_path: string) {
return Promise.resolve({ isFile: true });
},
readTextFile(_path: string) {
return Promise.resolve("- type: codex\n");
},
};
assertEquals(
await readProfilesFile("profiles.yml", "/repo", fileSystem),
"- type: codex\n",
);
await assertRejects(
() => readProfilesFile("/outside", "/repo", fileSystem),
Error,
"repository-relative",
);
await assertRejects(
() => readProfilesFile("escaped.yml", "/repo", fileSystem),
Error,
"must stay inside",
);
});
Deno.test("falls through only after usage exhaustion", async () => {
const visited: string[] = [];
const result = await runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
visited.push(profile.type);
if (profile.type === "claude") {
throw new Error("You've hit your usage limit");
}
return Promise.resolve("done");
},
);
assertEquals(result, "done");
assertEquals(visited, ["claude", "codex"]);
});
Deno.test("recognizes Claude's weekly limit message", async () => {
const result = await runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
if (profile.type === "claude") {
throw new Error("You've hit your limit · resets Monday");
}
return Promise.resolve("fallback");
},
);
assertEquals(result, "fallback");
});
Deno.test("recognizes provider API exhaustion status", async () => {
const results = await Promise.all(
["API Error: 402 Insufficient funds", "HTTP error: 429"]
.map((message) =>
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
if (profile.type === "claude") throw new Error(message);
return Promise.resolve("fallback");
},
)
),
);
assertEquals(results, ["fallback", "fallback"]);
});
Deno.test("preserves an ordinary agent failure", async () => {
const visited: string[] = [];
await assertRejects(
() =>
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
visited.push(profile.type);
throw new Error("tests failed");
},
),
Error,
"tests failed",
);
assertEquals(visited, ["claude"]);
});
Deno.test("does not mistake incidental quota text for exhaustion", async () => {
const visited: string[] = [];
await assertRejects(
() =>
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
visited.push(profile.type);
throw new Error("quota parser test failed at line 429");
},
),
Error,
"quota parser test failed",
);
assertEquals(visited, ["claude"]);
});
Deno.test("exposes only the selected profile credential to the agent", () => {
const base = {
HOME: "/home/bot",
PROFILES: "contains every secret",
REVIEWER_TOKEN: "reviewer",
BOT_TOKEN: "legacy",
CLAUDE_CODE_OAUTH_TOKEN: "old-claude",
ANTHROPIC_API_KEY: "old-anthropic-key",
ANTHROPIC_AUTH_TOKEN: "old-anthropic-token",
OPENAI_API_KEY: "old-openai",
CODEX_API_KEY: "old-codex-key",
CODEX_ACCESS_TOKEN: "old-codex-token",
BOT_PROFILE_CLAUDE_PRIMARY: "selected",
BOT_PROFILE_CLAUDE_FALLBACK: "fallback",
BOT_PROFILE_UNUSED: "unused",
};
const profiles = [
{
type: "claude" as const,
model: "claude-sonnet-5",
token: "selected",
tokenEnv: "BOT_PROFILE_CLAUDE_PRIMARY",
},
{
type: "claude" as const,
model: "claude-sonnet-5",
token: "fallback",
tokenEnv: "BOT_PROFILE_CLAUDE_FALLBACK",
},
];
assertEquals(
agentEnvironment(base, profiles[0], profiles),
{ HOME: "/home/bot", CLAUDE_CODE_OAUTH_TOKEN: "selected" },
);
assertEquals(sanitizedEnvironment(base, profiles), { HOME: "/home/bot" });
});