Turn on upper-snake-string-unions and convert profile types, failure reasons and anchor sides at their boundaries
This commit is contained in:
+3
-3
@@ -2,16 +2,16 @@
|
|||||||
"reviewer-token": "v1:XFUvCI9PnXVaW-ig:_3S9BvXywUp29cVYGA_EzFScKwC5oHkV9x2Ld8k-V_uiG7iA8n3fmjXCArzh2Lz-JDgy3IMTHoY",
|
"reviewer-token": "v1:XFUvCI9PnXVaW-ig:_3S9BvXywUp29cVYGA_EzFScKwC5oHkV9x2Ld8k-V_uiG7iA8n3fmjXCArzh2Lz-JDgy3IMTHoY",
|
||||||
"profiles": [
|
"profiles": [
|
||||||
{
|
{
|
||||||
"type": "codex",
|
"type": "CODEX",
|
||||||
"model": "gpt-5.6-terra"
|
"model": "gpt-5.6-terra"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "claude",
|
"type": "CLAUDE",
|
||||||
"model": "claude-sonnet-5",
|
"model": "claude-sonnet-5",
|
||||||
"token": "v1:-dyfF6ZicEhjPk4C:WCuMaGd09tYjtzUCl7abloRzPITvP5C35WpR372w3O_tcaD-ls-yV5sJBdU8iIVApZVCC98sY2gOyM3o9RMa8ciJq8Fuys7KxlYOp8QiluL9AYMSQi6rEsSH71EPWnxSACgo3uDQY8bWQpUlEZvXH9O-ZN9A58dh6b3vRg"
|
"token": "v1:-dyfF6ZicEhjPk4C:WCuMaGd09tYjtzUCl7abloRzPITvP5C35WpR372w3O_tcaD-ls-yV5sJBdU8iIVApZVCC98sY2gOyM3o9RMa8ciJq8Fuys7KxlYOp8QiluL9AYMSQi6rEsSH71EPWnxSACgo3uDQY8bWQpUlEZvXH9O-ZN9A58dh6b3vRg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "opencode",
|
"type": "OPENCODE",
|
||||||
"model": "openrouter/qwen/qwen3.8-27b",
|
"model": "openrouter/qwen/qwen3.8-27b",
|
||||||
"token": "v1:4qdfJCdUx9jfiabC:8X0ic_J82lPlq_R3uTLaEbQy7UmEh7BgtG0bDKAcc8FeJ0xvEwkGCKaDoLmT5nta03hgjGdcWKjDdOSMi1kSDfp8J7tRTLtMRzDT8GX8OU_PVDPVahQIAU0"
|
"token": "v1:4qdfJCdUx9jfiabC:8X0ic_J82lPlq_R3uTLaEbQy7UmEh7BgtG0bDKAcc8FeJ0xvEwkGCKaDoLmT5nta03hgjGdcWKjDdOSMi1kSDfp8J7tRTLtMRzDT8GX8OU_PVDPVahQIAU0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ achieved.
|
|||||||
|
|
||||||
Every finding that belongs to one line of the diff goes on that line instead of
|
Every finding that belongs to one line of the diff goes on that line instead of
|
||||||
into the body. Write those to `${ANCHORS_PATH}` as a JSON array, each entry
|
into the body. Write those to `${ANCHORS_PATH}` as a JSON array, each entry
|
||||||
`{"path": "<path from the repository root>", "line": <number>, "side": "new" |
|
`{"path": "<path from the repository root>", "line": <number>, "side": "NEW" |
|
||||||
"old", "body": "<the finding>"}`.
|
"OLD", "body": "<the finding>"}`.
|
||||||
`side` is `new` for a line in the head file and `old` for one only in the base
|
`side` is `NEW` for a line in the head file and `OLD` for one only in the base
|
||||||
file; `line` is that file's own line number, and it must be a line the diff
|
file; `line` is that file's own line number, and it must be a line the diff
|
||||||
touches, or Gitea refuses the anchor. Write the file only when there is
|
touches, or Gitea refuses the anchor. Write the file only when there is
|
||||||
something to anchor, and keep each body to the what, the why, and the how, with
|
something to anchor, and keep each body to the what, the why, and the how, with
|
||||||
|
|||||||
@@ -257,9 +257,8 @@ const rules: DummyRuleMap = {
|
|||||||
"consistent-return": "off",
|
"consistent-return": "off",
|
||||||
// TypeScript's type checking covers exhaustive switches.
|
// TypeScript's type checking covers exhaustive switches.
|
||||||
"typescript/switch-exhaustiveness-check": "off",
|
"typescript/switch-exhaustiveness-check": "off",
|
||||||
// PENDING USER: union members are values in profiles.json, the Gitea API,
|
// A `default` on a switch over a union would hide a new member from TS.
|
||||||
// and posted comments; renaming them changes behavior.
|
"default-case": "off",
|
||||||
"explicit-types/upper-snake-string-unions": "off",
|
|
||||||
// Off, too much churn:
|
// Off, too much churn:
|
||||||
// Literal numbers read better than a constant per use.
|
// Literal numbers read better than a constant per use.
|
||||||
"no-magic-numbers": "off",
|
"no-magic-numbers": "off",
|
||||||
|
|||||||
+17
-17
@@ -65,7 +65,7 @@ function manifestFor(runtimeDir: string, profileHome: string): RuntimeManifest {
|
|||||||
profiles: [
|
profiles: [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
type: "claude",
|
type: "CLAUDE",
|
||||||
model: "test",
|
model: "test",
|
||||||
uid: currentUid(),
|
uid: currentUid(),
|
||||||
gid: currentGid(),
|
gid: currentGid(),
|
||||||
@@ -89,7 +89,7 @@ async function fixture(
|
|||||||
const outcome: RuntimeOutcome = source.ok
|
const outcome: RuntimeOutcome = source.ok
|
||||||
? {
|
? {
|
||||||
...source,
|
...source,
|
||||||
profile: { type: "codex", model: "gpt-test" },
|
profile: { type: "CODEX", model: "gpt-test" },
|
||||||
reviewPath: `${profileHome}/review.md`,
|
reviewPath: `${profileHome}/review.md`,
|
||||||
anchorsPath: `${profileHome}/anchors.json`,
|
anchorsPath: `${profileHome}/anchors.json`,
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ test("posts a successful issue result and removes runtime credentials", async ()
|
|||||||
const [request] = requests;
|
const [request] = requests;
|
||||||
expect(request?.url).toStrictEqual(`${context.apiUrl}/repos/owner/repo/issues/7/comments`);
|
expect(request?.url).toStrictEqual(`${context.apiUrl}/repos/owner/repo/issues/7/comments`);
|
||||||
expect(requestBody(request?.init)).toStrictEqual({
|
expect(requestBody(request?.init)).toStrictEqual({
|
||||||
body: "done\n\n> Profile: `codex` · `gpt-test`",
|
body: "done\n\n> Profile: `CODEX` · `gpt-test`",
|
||||||
});
|
});
|
||||||
expect(request?.init?.headers).toStrictEqual({
|
expect(request?.init?.headers).toStrictEqual({
|
||||||
Authorization: "token reviewer-secret",
|
Authorization: "token reviewer-secret",
|
||||||
@@ -158,7 +158,7 @@ test("posts a successful issue result and removes runtime credentials", async ()
|
|||||||
|
|
||||||
test("retries a rejected inline review without anchors", async () => {
|
test("retries a rejected inline review without anchors", async () => {
|
||||||
const result = await fixture({ ok: true, body: "ignored final response" }, "Approved\nReady", [
|
const result = await fixture({ ok: true, body: "ignored final response" }, "Approved\nReady", [
|
||||||
{ path: "run.ts", line: 1, side: "new", body: "Finding" },
|
{ path: "run.ts", line: 1, side: "NEW", body: "Finding" },
|
||||||
]);
|
]);
|
||||||
const bodies: unknown[] = [];
|
const bodies: unknown[] = [];
|
||||||
|
|
||||||
@@ -178,11 +178,11 @@ test("retries a rejected inline review without anchors", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(bodies[0]).toMatchObject({
|
expect(bodies[0]).toMatchObject({
|
||||||
comments: [{ body: "Finding\n\n> Profile: `codex` · `gpt-test`" }],
|
comments: [{ body: "Finding\n\n> Profile: `CODEX` · `gpt-test`" }],
|
||||||
});
|
});
|
||||||
expect(bodies[1]).toMatchObject({
|
expect(bodies[1]).toMatchObject({
|
||||||
comments: [],
|
comments: [],
|
||||||
body: "✅ Approved\nReady\n\n> Profile: `codex` · `gpt-test`",
|
body: "✅ Approved\nReady\n\n> Profile: `CODEX` · `gpt-test`",
|
||||||
});
|
});
|
||||||
assert(await missing(result.runtimeDir));
|
assert(await missing(result.runtimeDir));
|
||||||
});
|
});
|
||||||
@@ -191,9 +191,9 @@ test("reports each tried profile and keeps the action failed", async () => {
|
|||||||
const result = await fixture({
|
const result = await fixture({
|
||||||
ok: false,
|
ok: false,
|
||||||
failures: [
|
failures: [
|
||||||
{ type: "codex", model: "gpt-test", reason: "sign-in failed" },
|
{ type: "CODEX", model: "gpt-test", reason: "SIGN_IN_FAILED" },
|
||||||
{ type: "claude", model: "claude-test", reason: "usage exhausted" },
|
{ type: "CLAUDE", model: "claude-test", reason: "USAGE_EXHAUSTED" },
|
||||||
{ type: "opencode", model: "openrouter/test", reason: "failed" },
|
{ type: "OPENCODE", model: "openrouter/test", reason: "FAILED" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const bodies: unknown[] = [];
|
const bodies: unknown[] = [];
|
||||||
@@ -205,9 +205,9 @@ test("reports each tried profile and keeps the action failed", async () => {
|
|||||||
expect(bodies).toStrictEqual([
|
expect(bodies).toStrictEqual([
|
||||||
{
|
{
|
||||||
body:
|
body:
|
||||||
"AI bot failed.\n\n> Tried: `codex` · `gpt-test` (sign-in failed), " +
|
"AI bot failed.\n\n> Tried: `CODEX` · `gpt-test` (sign-in failed), " +
|
||||||
"`claude` · `claude-test` (usage exhausted), " +
|
"`CLAUDE` · `claude-test` (usage exhausted), " +
|
||||||
"`opencode` · `openrouter/test` (failed)",
|
"`OPENCODE` · `openrouter/test` (failed)",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert(!JSON.stringify(bodies).includes("reviewer-secret"));
|
assert(!JSON.stringify(bodies).includes("reviewer-secret"));
|
||||||
@@ -221,9 +221,9 @@ test("drops failure entries that are not known labels", async () => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
ok: false,
|
ok: false,
|
||||||
failures: [
|
failures: [
|
||||||
{ type: "codex", model: "gpt-test", reason: "token sk-secret leaked" },
|
{ type: "CODEX", model: "gpt-test", reason: "token sk-secret leaked" },
|
||||||
{ type: "shell", model: "x", reason: "failed" },
|
{ type: "shell", model: "x", reason: "FAILED" },
|
||||||
{ type: "claude", model: "claude-test", reason: "sign-in failed" },
|
{ type: "CLAUDE", model: "claude-test", reason: "SIGN_IN_FAILED" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -235,7 +235,7 @@ test("drops failure entries that are not known labels", async () => {
|
|||||||
|
|
||||||
expect(bodies).toStrictEqual([
|
expect(bodies).toStrictEqual([
|
||||||
{
|
{
|
||||||
body: "AI bot failed.\n\n> Tried: `claude` · `claude-test` (sign-in failed)",
|
body: "AI bot failed.\n\n> Tried: `CLAUDE` · `claude-test` (sign-in failed)",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -315,7 +315,7 @@ test("reports an attributed failure when a pull request review is missing", asyn
|
|||||||
|
|
||||||
expect(bodies).toStrictEqual([
|
expect(bodies).toStrictEqual([
|
||||||
{
|
{
|
||||||
body: "AI bot failed.\n\n> Tried: `codex` · `gpt-test` (failed)",
|
body: "AI bot failed.\n\n> Tried: `CODEX` · `gpt-test` (failed)",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert(await missing(result.runtimeDir));
|
assert(await missing(result.runtimeDir));
|
||||||
|
|||||||
+9
-9
@@ -16,7 +16,7 @@ export interface PostContext {
|
|||||||
interface Anchor {
|
interface Anchor {
|
||||||
path: string;
|
path: string;
|
||||||
line: number;
|
line: number;
|
||||||
side: "new" | "old";
|
side: "NEW" | "OLD";
|
||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ const VERDICTS = new Map<string, [event: string, mark: string]>([
|
|||||||
["Approved", ["APPROVED", "✅"]],
|
["Approved", ["APPROVED", "✅"]],
|
||||||
["Changes requested", ["REQUEST_CHANGES", "🛑"]],
|
["Changes requested", ["REQUEST_CHANGES", "🛑"]],
|
||||||
]);
|
]);
|
||||||
const PROFILE_TYPES = new Set<unknown>(["claude", "codex", "opencode"]);
|
const PROFILE_TYPES = new Set<unknown>(["CLAUDE", "CODEX", "OPENCODE"]);
|
||||||
const FAILURE_REASONS = new Set<unknown>(["sign-in failed", "usage exhausted", "failed"]);
|
const FAILURE_REASONS = new Set<unknown>(["SIGN_IN_FAILED", "USAGE_EXHAUSTED", "FAILED"]);
|
||||||
const isProfileType = (value: unknown): value is ProfileFailure["type"] => PROFILE_TYPES.has(value);
|
const isProfileType = (value: unknown): value is ProfileFailure["type"] => PROFILE_TYPES.has(value);
|
||||||
const isFailureReason = (value: unknown): value is FailureReason => FAILURE_REASONS.has(value);
|
const isFailureReason = (value: unknown): value is FailureReason => FAILURE_REASONS.has(value);
|
||||||
const failureComment = (failures: ProfileFailure[]): string =>
|
const failureComment = (failures: ProfileFailure[]): string =>
|
||||||
@@ -56,7 +56,7 @@ function parseAnchors(text: string, sourcePath: string): Anchor[] {
|
|||||||
const path = field(entry, "path");
|
const path = field(entry, "path");
|
||||||
const line = field(entry, "line");
|
const line = field(entry, "line");
|
||||||
const givenSide = field(entry, "side");
|
const givenSide = field(entry, "side");
|
||||||
const side = givenSide === undefined ? "new" : givenSide;
|
const side = givenSide === undefined ? "NEW" : givenSide;
|
||||||
const body = field(entry, "body");
|
const body = field(entry, "body");
|
||||||
if (typeof path !== "string" || path === "") {
|
if (typeof path !== "string" || path === "") {
|
||||||
throw new TypeError(`${at}.path: expected a repository path`);
|
throw new TypeError(`${at}.path: expected a repository path`);
|
||||||
@@ -64,8 +64,8 @@ function parseAnchors(text: string, sourcePath: string): Anchor[] {
|
|||||||
if (typeof line !== "number" || !Number.isInteger(line) || line < 1) {
|
if (typeof line !== "number" || !Number.isInteger(line) || line < 1) {
|
||||||
throw new TypeError(`${at}.line: expected a line number`);
|
throw new TypeError(`${at}.line: expected a line number`);
|
||||||
}
|
}
|
||||||
if (side !== "new" && side !== "old") {
|
if (side !== "NEW" && side !== "OLD") {
|
||||||
throw new Error(`${at}.side: expected "new" or "old"`);
|
throw new Error(`${at}.side: expected "NEW" or "OLD"`);
|
||||||
}
|
}
|
||||||
if (typeof body !== "string" || body.trim() === "") {
|
if (typeof body !== "string" || body.trim() === "") {
|
||||||
throw new TypeError(`${at}.body: expected the finding`);
|
throw new TypeError(`${at}.body: expected the finding`);
|
||||||
@@ -216,8 +216,8 @@ export async function postRuntime(
|
|||||||
comments: entries.map(({ path, line, side, body }) => ({
|
comments: entries.map(({ path, line, side, body }) => ({
|
||||||
path,
|
path,
|
||||||
body: stripAnsi(withProfile(body, outcome.profile)),
|
body: stripAnsi(withProfile(body, outcome.profile)),
|
||||||
new_position: side === "new" ? line : 0,
|
new_position: side === "NEW" ? line : 0,
|
||||||
old_position: side === "old" ? line : 0,
|
old_position: side === "OLD" ? line : 0,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -234,7 +234,7 @@ export async function postRuntime(
|
|||||||
if (error instanceof GiteaRequestError) {
|
if (error instanceof GiteaRequestError) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
await postComment(failureComment([{ ...outcome.profile, reason: "failed" }]));
|
await postComment(failureComment([{ ...outcome.profile, reason: "FAILED" }]));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { isRunnableProfileProcess, killProfileProcesses, readProfileFile } from
|
|||||||
|
|
||||||
const profile = (uid: number, home: string): RuntimeProfile => ({
|
const profile = (uid: number, home: string): RuntimeProfile => ({
|
||||||
index: uid - 20_000,
|
index: uid - 20_000,
|
||||||
type: "claude",
|
type: "CLAUDE",
|
||||||
model: "test",
|
model: "test",
|
||||||
uid,
|
uid,
|
||||||
gid: uid,
|
gid: uid,
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
} from "./profiles.ts";
|
} from "./profiles.ts";
|
||||||
|
|
||||||
test("formats profile attribution as a Markdown quote", () => {
|
test("formats profile attribution as a Markdown quote", () => {
|
||||||
expect(profileFooter({ type: "opencode", model: "openrouter/qwen/qwen3.8-27b" })).toBe(
|
expect(profileFooter({ type: "OPENCODE", model: "openrouter/qwen/qwen3.8-27b" })).toBe(
|
||||||
"> Profile: `opencode` · `openrouter/qwen/qwen3.8-27b`",
|
"> Profile: `OPENCODE` · `openrouter/qwen/qwen3.8-27b`",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,12 +20,12 @@ test("falls through only after usage exhaustion", async () => {
|
|||||||
const visited: string[] = [];
|
const visited: string[] = [];
|
||||||
const result = await runProfiles(
|
const result = await runProfiles(
|
||||||
[
|
[
|
||||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||||
{ type: "codex", model: "gpt-5.6-terra" },
|
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||||
],
|
],
|
||||||
async (profile) => {
|
async (profile) => {
|
||||||
visited.push(profile.type);
|
visited.push(profile.type);
|
||||||
if (profile.type === "claude") {
|
if (profile.type === "CLAUDE") {
|
||||||
throw new Error("You've hit your usage limit");
|
throw new Error("You've hit your usage limit");
|
||||||
}
|
}
|
||||||
const resolved = await Promise.resolve("done");
|
const resolved = await Promise.resolve("done");
|
||||||
@@ -34,17 +34,17 @@ test("falls through only after usage exhaustion", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toBe("done");
|
expect(result).toBe("done");
|
||||||
expect(visited).toStrictEqual(["claude", "codex"]);
|
expect(visited).toStrictEqual(["CLAUDE", "CODEX"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("recognizes Claude's weekly limit message", async () => {
|
test("recognizes Claude's weekly limit message", async () => {
|
||||||
const result = await runProfiles(
|
const result = await runProfiles(
|
||||||
[
|
[
|
||||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||||
{ type: "codex", model: "gpt-5.6-terra" },
|
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||||
],
|
],
|
||||||
async (profile) => {
|
async (profile) => {
|
||||||
if (profile.type === "claude") {
|
if (profile.type === "CLAUDE") {
|
||||||
throw new Error("You've hit your weekly limit · resets 12am (UTC)");
|
throw new Error("You've hit your weekly limit · resets 12am (UTC)");
|
||||||
}
|
}
|
||||||
const resolved = await Promise.resolve("fallback");
|
const resolved = await Promise.resolve("fallback");
|
||||||
@@ -60,11 +60,11 @@ test("recognizes provider API exhaustion status", async () => {
|
|||||||
["API Error: 402 Insufficient funds", "HTTP error: 429"].map(async (message) => {
|
["API Error: 402 Insufficient funds", "HTTP error: 429"].map(async (message) => {
|
||||||
const result = await runProfiles(
|
const result = await runProfiles(
|
||||||
[
|
[
|
||||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||||
{ type: "codex", model: "gpt-5.6-terra" },
|
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||||
],
|
],
|
||||||
async (profile) => {
|
async (profile) => {
|
||||||
if (profile.type === "claude") {
|
if (profile.type === "CLAUDE") {
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
const resolved = await Promise.resolve("fallback");
|
const resolved = await Promise.resolve("fallback");
|
||||||
@@ -82,15 +82,15 @@ test("falls through after a sign-in failure and records each try", async () => {
|
|||||||
const failures: ProfileFailure[] = [];
|
const failures: ProfileFailure[] = [];
|
||||||
const result = await runProfiles(
|
const result = await runProfiles(
|
||||||
[
|
[
|
||||||
{ type: "codex", model: "gpt-5.6-terra" },
|
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||||
{ type: "opencode", model: "openrouter/qwen/qwen3.8-27b" },
|
{ type: "OPENCODE", model: "openrouter/qwen/qwen3.8-27b" },
|
||||||
],
|
],
|
||||||
async (profile) => {
|
async (profile) => {
|
||||||
if (profile.type === "codex") {
|
if (profile.type === "CODEX") {
|
||||||
throw new AgentAuthError("codex sign-in failed");
|
throw new AgentAuthError("codex sign-in failed");
|
||||||
}
|
}
|
||||||
if (profile.type === "claude") {
|
if (profile.type === "CLAUDE") {
|
||||||
throw new Error("API Error: 429");
|
throw new Error("API Error: 429");
|
||||||
}
|
}
|
||||||
const resolved = await Promise.resolve("fallback");
|
const resolved = await Promise.resolve("fallback");
|
||||||
@@ -101,12 +101,12 @@ test("falls through after a sign-in failure and records each try", async () => {
|
|||||||
|
|
||||||
expect(result).toBe("fallback");
|
expect(result).toBe("fallback");
|
||||||
expect(failures).toStrictEqual([
|
expect(failures).toStrictEqual([
|
||||||
{ type: "codex", model: "gpt-5.6-terra", reason: "sign-in failed" },
|
{ type: "CODEX", model: "gpt-5.6-terra", reason: "SIGN_IN_FAILED" },
|
||||||
{ type: "claude", model: "claude-sonnet-5", reason: "usage exhausted" },
|
{ type: "CLAUDE", model: "claude-sonnet-5", reason: "USAGE_EXHAUSTED" },
|
||||||
]);
|
]);
|
||||||
expect(failureFooter(failures)).toStrictEqual(
|
expect(failureFooter(failures)).toStrictEqual(
|
||||||
"> Tried: `codex` · `gpt-5.6-terra` (sign-in failed), " +
|
"> Tried: `CODEX` · `gpt-5.6-terra` (sign-in failed), " +
|
||||||
"`claude` · `claude-sonnet-5` (usage exhausted)",
|
"`CLAUDE` · `claude-sonnet-5` (usage exhausted)",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,8 +115,8 @@ test("throws the last sign-in failure when every profile fails", async () => {
|
|||||||
await expect(
|
await expect(
|
||||||
runProfiles(
|
runProfiles(
|
||||||
[
|
[
|
||||||
{ type: "codex", model: "gpt-5.6-terra" },
|
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||||
],
|
],
|
||||||
(profile) => {
|
(profile) => {
|
||||||
throw new AgentAuthError(`${profile.type} sign-in failed`);
|
throw new AgentAuthError(`${profile.type} sign-in failed`);
|
||||||
@@ -126,10 +126,10 @@ test("throws the last sign-in failure when every profile fails", async () => {
|
|||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
expect.toSatisfy(
|
expect.toSatisfy(
|
||||||
(error: unknown) =>
|
(error: unknown) =>
|
||||||
error instanceof AgentAuthError && error.message.includes("claude sign-in failed"),
|
error instanceof AgentAuthError && error.message.includes("CLAUDE sign-in failed"),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(failures.map(({ reason }) => reason)).toStrictEqual(["sign-in failed", "sign-in failed"]);
|
expect(failures.map(({ reason }) => reason)).toStrictEqual(["SIGN_IN_FAILED", "SIGN_IN_FAILED"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preserves an ordinary agent failure", async () => {
|
test("preserves an ordinary agent failure", async () => {
|
||||||
@@ -137,8 +137,8 @@ test("preserves an ordinary agent failure", async () => {
|
|||||||
await expect(
|
await expect(
|
||||||
runProfiles(
|
runProfiles(
|
||||||
[
|
[
|
||||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||||
{ type: "codex", model: "gpt-5.6-terra" },
|
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||||
],
|
],
|
||||||
(profile) => {
|
(profile) => {
|
||||||
visited.push(profile.type);
|
visited.push(profile.type);
|
||||||
@@ -146,7 +146,7 @@ test("preserves an ordinary agent failure", async () => {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
).rejects.toThrow("tests failed");
|
).rejects.toThrow("tests failed");
|
||||||
expect(visited).toStrictEqual(["claude"]);
|
expect(visited).toStrictEqual(["CLAUDE"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does not mistake incidental quota text for exhaustion", async () => {
|
test("does not mistake incidental quota text for exhaustion", async () => {
|
||||||
@@ -154,8 +154,8 @@ test("does not mistake incidental quota text for exhaustion", async () => {
|
|||||||
await expect(
|
await expect(
|
||||||
runProfiles(
|
runProfiles(
|
||||||
[
|
[
|
||||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||||
{ type: "codex", model: "gpt-5.6-terra" },
|
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||||
],
|
],
|
||||||
(profile) => {
|
(profile) => {
|
||||||
visited.push(profile.type);
|
visited.push(profile.type);
|
||||||
@@ -163,7 +163,7 @@ test("does not mistake incidental quota text for exhaustion", async () => {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
).rejects.toThrow("quota parser test failed");
|
).rejects.toThrow("quota parser test failed");
|
||||||
expect(visited).toStrictEqual(["claude"]);
|
expect(visited).toStrictEqual(["CLAUDE"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("exposes only the selected profile credential to the agent", () => {
|
test("exposes only the selected profile credential to the agent", () => {
|
||||||
@@ -183,7 +183,7 @@ test("exposes only the selected profile credential to the agent", () => {
|
|||||||
]);
|
]);
|
||||||
const profile = {
|
const profile = {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: "claude" as const,
|
type: "CLAUDE" as const,
|
||||||
model: "claude-sonnet-5",
|
model: "claude-sonnet-5",
|
||||||
uid: 20_000,
|
uid: 20_000,
|
||||||
gid: 20_000,
|
gid: 20_000,
|
||||||
@@ -209,7 +209,7 @@ test("exposes only the selected profile credential to the agent", () => {
|
|||||||
test("uses a private Codex home without inventing an API credential", () => {
|
test("uses a private Codex home without inventing an API credential", () => {
|
||||||
const profile = {
|
const profile = {
|
||||||
index: 1,
|
index: 1,
|
||||||
type: "codex" as const,
|
type: "CODEX" as const,
|
||||||
model: "gpt-5.6-terra",
|
model: "gpt-5.6-terra",
|
||||||
uid: 20_001,
|
uid: 20_001,
|
||||||
gid: 20_001,
|
gid: 20_001,
|
||||||
@@ -229,7 +229,7 @@ test("uses a private Codex home without inventing an API credential", () => {
|
|||||||
test("exposes an OpenRouter key only to the OpenCode profile", () => {
|
test("exposes an OpenRouter key only to the OpenCode profile", () => {
|
||||||
const profile = {
|
const profile = {
|
||||||
index: 2,
|
index: 2,
|
||||||
type: "opencode" as const,
|
type: "OPENCODE" as const,
|
||||||
model: "openrouter/qwen/qwen3.8-27b",
|
model: "openrouter/qwen/qwen3.8-27b",
|
||||||
uid: 20_002,
|
uid: 20_002,
|
||||||
gid: 20_002,
|
gid: 20_002,
|
||||||
|
|||||||
+33
-13
@@ -1,6 +1,6 @@
|
|||||||
// The agent and model a profile runs, which every report names.
|
// The agent and model a profile runs, which every report names.
|
||||||
export interface ProfileIdentity {
|
export interface ProfileIdentity {
|
||||||
type: "claude" | "codex" | "opencode";
|
type: "CLAUDE" | "CODEX" | "OPENCODE";
|
||||||
model: string;
|
model: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,19 @@ export function withProfile(body: string, profile: ProfileIdentity): string {
|
|||||||
return `${body}\n\n${profileFooter(profile)}`;
|
return `${body}\n\n${profileFooter(profile)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FailureReason = "sign-in failed" | "usage exhausted" | "failed";
|
export type FailureReason = "SIGN_IN_FAILED" | "USAGE_EXHAUSTED" | "FAILED";
|
||||||
|
|
||||||
|
// The words a failure reason reads as in logs and posted comments.
|
||||||
|
function toMessage(reason: FailureReason): string {
|
||||||
|
switch (reason) {
|
||||||
|
case "SIGN_IN_FAILED":
|
||||||
|
return "sign-in failed";
|
||||||
|
case "USAGE_EXHAUSTED":
|
||||||
|
return "usage exhausted";
|
||||||
|
case "FAILED":
|
||||||
|
return "failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProfileFailure extends ProfileIdentity {
|
export interface ProfileFailure extends ProfileIdentity {
|
||||||
reason: FailureReason;
|
reason: FailureReason;
|
||||||
@@ -44,9 +56,9 @@ function isUsageExhausted(error: unknown): boolean {
|
|||||||
|
|
||||||
function failureReason(error: unknown): FailureReason {
|
function failureReason(error: unknown): FailureReason {
|
||||||
if (error instanceof AgentAuthError) {
|
if (error instanceof AgentAuthError) {
|
||||||
return "sign-in failed";
|
return "SIGN_IN_FAILED";
|
||||||
}
|
}
|
||||||
return isUsageExhausted(error) ? "usage exhausted" : "failed";
|
return isUsageExhausted(error) ? "USAGE_EXHAUSTED" : "FAILED";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function failureFooter(failures: ProfileFailure[]): string {
|
export function failureFooter(failures: ProfileFailure[]): string {
|
||||||
@@ -54,7 +66,7 @@ export function failureFooter(failures: ProfileFailure[]): string {
|
|||||||
return "> Profile: none";
|
return "> Profile: none";
|
||||||
}
|
}
|
||||||
return `> Tried: ${failures
|
return `> Tried: ${failures
|
||||||
.map(({ type, model, reason }) => `\`${type}\` · \`${model}\` (${reason})`)
|
.map(({ type, model, reason }) => `\`${type}\` · \`${model}\` (${toMessage(reason)})`)
|
||||||
.join(", ")}`;
|
.join(", ")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +83,10 @@ export async function runProfiles<T, P extends ProfileIdentity>(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const reason = failureReason(error);
|
const reason = failureReason(error);
|
||||||
failures.push({ type: profile.type, model: profile.model, reason });
|
failures.push({ type: profile.type, model: profile.model, reason });
|
||||||
if (reason === "failed" || index === profiles.length - 1) {
|
if (reason === "FAILED" || index === profiles.length - 1) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
console.error(`${profile.type} ${reason}; trying next profile`);
|
console.error(`${profile.type} ${toMessage(reason)}; trying next profile`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("profiles must contain at least one profile");
|
throw new Error("profiles must contain at least one profile");
|
||||||
@@ -120,6 +132,18 @@ export function sanitizedEnvironment(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The environment variable each agent CLI reads its credential from.
|
||||||
|
function tokenVariable(type: ProfileIdentity["type"]): string {
|
||||||
|
switch (type) {
|
||||||
|
case "CLAUDE":
|
||||||
|
return "CLAUDE_CODE_OAUTH_TOKEN";
|
||||||
|
case "CODEX":
|
||||||
|
return "OPENAI_API_KEY";
|
||||||
|
case "OPENCODE":
|
||||||
|
return "OPENROUTER_API_KEY";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function agentEnvironment(
|
export function agentEnvironment(
|
||||||
environment: ReadonlyMap<string, string>,
|
environment: ReadonlyMap<string, string>,
|
||||||
profile: ProfileHome,
|
profile: ProfileHome,
|
||||||
@@ -127,15 +151,11 @@ export function agentEnvironment(
|
|||||||
): Map<string, string> {
|
): Map<string, string> {
|
||||||
const result = sanitizedEnvironment(environment);
|
const result = sanitizedEnvironment(environment);
|
||||||
result.set("HOME", profile.home);
|
result.set("HOME", profile.home);
|
||||||
if (profile.type === "codex") {
|
if (profile.type === "CODEX") {
|
||||||
result.set("CODEX_HOME", `${profile.home}/.codex`);
|
result.set("CODEX_HOME", `${profile.home}/.codex`);
|
||||||
}
|
}
|
||||||
if (token !== undefined && token !== "") {
|
if (token !== undefined && token !== "") {
|
||||||
const tokenName = {
|
const tokenName = tokenVariable(profile.type);
|
||||||
claude: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
||||||
codex: "OPENAI_API_KEY",
|
|
||||||
opencode: "OPENROUTER_API_KEY",
|
|
||||||
}[profile.type];
|
|
||||||
result.set(tokenName, token);
|
result.set(tokenName, token);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+16
-4
@@ -150,6 +150,18 @@ async function runAgentCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The agent name the skills installer knows each profile type by.
|
||||||
|
function skillsAgent(type: RuntimeProfile["type"]): string {
|
||||||
|
switch (type) {
|
||||||
|
case "CLAUDE":
|
||||||
|
return "claude-code";
|
||||||
|
case "CODEX":
|
||||||
|
return "codex";
|
||||||
|
case "OPENCODE":
|
||||||
|
return "opencode";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareProfile(profile: RuntimeProfile): Promise<void> {
|
async function prepareProfile(profile: RuntimeProfile): Promise<void> {
|
||||||
await runAgentCommand(profile, "git", [
|
await runAgentCommand(profile, "git", [
|
||||||
"config",
|
"config",
|
||||||
@@ -163,7 +175,7 @@ async function prepareProfile(profile: RuntimeProfile): Promise<void> {
|
|||||||
...SKILLS_INSTALL.split(" "),
|
...SKILLS_INSTALL.split(" "),
|
||||||
"--global",
|
"--global",
|
||||||
"--agent",
|
"--agent",
|
||||||
profile.type === "claude" ? "claude-code" : profile.type,
|
skillsAgent(profile.type),
|
||||||
"--yes",
|
"--yes",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -451,7 +463,7 @@ async function runOpenCode(
|
|||||||
|
|
||||||
async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
|
async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
|
||||||
const token = await readProfileToken(profile);
|
const token = await readProfileToken(profile);
|
||||||
const authentication = profile.type === "codex" ? codexAuthentication(token ?? undefined) : null;
|
const authentication = profile.type === "CODEX" ? codexAuthentication(token ?? undefined) : null;
|
||||||
let stagedCodexAuth: string | null = null;
|
let stagedCodexAuth: string | null = null;
|
||||||
let body: string | null = null;
|
let body: string | null = null;
|
||||||
let resultPath: string | null = null;
|
let resultPath: string | null = null;
|
||||||
@@ -465,9 +477,9 @@ async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
|
|||||||
const rendered = await renderPrompt(profile);
|
const rendered = await renderPrompt(profile);
|
||||||
reviewPath = rendered.reviewPath;
|
reviewPath = rendered.reviewPath;
|
||||||
anchorsPath = rendered.anchorsPath;
|
anchorsPath = rendered.anchorsPath;
|
||||||
if (profile.type === "claude") {
|
if (profile.type === "CLAUDE") {
|
||||||
body = await runClaude(profile, token ?? "", rendered.prompt);
|
body = await runClaude(profile, token ?? "", rendered.prompt);
|
||||||
} else if (profile.type === "codex") {
|
} else if (profile.type === "CODEX") {
|
||||||
resultPath = await runCodex(
|
resultPath = await runCodex(
|
||||||
profile,
|
profile,
|
||||||
rendered.prompt,
|
rendered.prompt,
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ test("decrypts the central reviewer token and ordered profiles", async () => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
"reviewer-token": ENCRYPTED,
|
"reviewer-token": ENCRYPTED,
|
||||||
profiles: [
|
profiles: [
|
||||||
{ type: "codex", model: "gpt-custom" },
|
{ type: "CODEX", model: "gpt-custom" },
|
||||||
{ type: "claude", token: ENCRYPTED },
|
{ type: "CLAUDE", token: ENCRYPTED },
|
||||||
{ type: "opencode", token: ENCRYPTED },
|
{ type: "OPENCODE", token: ENCRYPTED },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
MASTER_KEY,
|
MASTER_KEY,
|
||||||
@@ -41,14 +41,14 @@ test("decrypts the central reviewer token and ordered profiles", async () => {
|
|||||||
).toStrictEqual({
|
).toStrictEqual({
|
||||||
reviewerToken: "secret-token",
|
reviewerToken: "secret-token",
|
||||||
profiles: [
|
profiles: [
|
||||||
{ type: "codex", model: "gpt-custom" },
|
{ type: "CODEX", model: "gpt-custom" },
|
||||||
{
|
{
|
||||||
type: "claude",
|
type: "CLAUDE",
|
||||||
model: "claude-sonnet-5",
|
model: "claude-sonnet-5",
|
||||||
token: "secret-token",
|
token: "secret-token",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "opencode",
|
type: "OPENCODE",
|
||||||
model: "openrouter/qwen/qwen3.8-27b",
|
model: "openrouter/qwen/qwen3.8-27b",
|
||||||
token: "secret-token",
|
token: "secret-token",
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@ test("rejects malformed central credentials", async () => {
|
|||||||
await expect(parseCredentials('{"profiles": []}', MASTER_KEY)).rejects.toThrow("reviewer-token");
|
await expect(parseCredentials('{"profiles": []}', MASTER_KEY)).rejects.toThrow("reviewer-token");
|
||||||
await expect(
|
await expect(
|
||||||
parseCredentials(
|
parseCredentials(
|
||||||
JSON.stringify({ "reviewer-token": ENCRYPTED, profiles: [{ type: "claude" }] }),
|
JSON.stringify({ "reviewer-token": ENCRYPTED, profiles: [{ type: "CLAUDE" }] }),
|
||||||
MASTER_KEY,
|
MASTER_KEY,
|
||||||
),
|
),
|
||||||
).rejects.toThrow("profiles[0].token");
|
).rejects.toThrow("profiles[0].token");
|
||||||
|
|||||||
+14
-14
@@ -3,18 +3,18 @@ import { webcrypto } from "node:crypto";
|
|||||||
import { field, isMapping } from "#common";
|
import { field, isMapping } from "#common";
|
||||||
import type { BotProfile } from "#profiles";
|
import type { BotProfile } from "#profiles";
|
||||||
|
|
||||||
interface DefaultModels {
|
// The model a profile runs when profiles.json names none.
|
||||||
claude: string;
|
function defaultModel(type: BotProfile["type"]): string {
|
||||||
codex: string;
|
switch (type) {
|
||||||
opencode: string;
|
case "CLAUDE":
|
||||||
|
return "claude-sonnet-5";
|
||||||
|
case "CODEX":
|
||||||
|
return "gpt-5.6-terra";
|
||||||
|
case "OPENCODE":
|
||||||
|
return "openrouter/qwen/qwen3.8-27b";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MODELS: DefaultModels = {
|
|
||||||
claude: "claude-sonnet-5",
|
|
||||||
codex: "gpt-5.6-terra",
|
|
||||||
opencode: "openrouter/qwen/qwen3.8-27b",
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface BotCredentials {
|
export interface BotCredentials {
|
||||||
reviewerToken: string;
|
reviewerToken: string;
|
||||||
profiles: BotProfile[];
|
profiles: BotProfile[];
|
||||||
@@ -91,13 +91,13 @@ async function parseProfile(entry: unknown, index: number, masterKey: string): P
|
|||||||
const type = field(entry, "type");
|
const type = field(entry, "type");
|
||||||
const model = field(entry, "model");
|
const model = field(entry, "model");
|
||||||
const token = field(entry, "token");
|
const token = field(entry, "token");
|
||||||
if (type !== "claude" && type !== "codex" && type !== "opencode") {
|
if (type !== "CLAUDE" && type !== "CODEX" && type !== "OPENCODE") {
|
||||||
throw new Error(`${at}.type: expected "claude", "codex", or "opencode"`);
|
throw new Error(`${at}.type: expected "CLAUDE", "CODEX", or "OPENCODE"`);
|
||||||
}
|
}
|
||||||
if (model !== undefined && (typeof model !== "string" || model === "")) {
|
if (model !== undefined && (typeof model !== "string" || model === "")) {
|
||||||
throw new Error(`${at}.model: expected a model name`);
|
throw new Error(`${at}.model: expected a model name`);
|
||||||
}
|
}
|
||||||
if ((type === "claude" || type === "opencode") && typeof token !== "string") {
|
if ((type === "CLAUDE" || type === "OPENCODE") && typeof token !== "string") {
|
||||||
throw new Error(`${at}.token: expected an encrypted credential`);
|
throw new Error(`${at}.token: expected an encrypted credential`);
|
||||||
}
|
}
|
||||||
if (token !== undefined && typeof token !== "string") {
|
if (token !== undefined && typeof token !== "string") {
|
||||||
@@ -105,7 +105,7 @@ async function parseProfile(entry: unknown, index: number, masterKey: string): P
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
model: model ?? DEFAULT_MODELS[type],
|
model: model ?? defaultModel(type),
|
||||||
...(typeof token === "string" && {
|
...(typeof token === "string" && {
|
||||||
token: await decryptCredential(token, masterKey),
|
token: await decryptCredential(token, masterKey),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ test.skipIf(!IS_ROOT)("prepares root-only reviewer and isolated profile credenti
|
|||||||
`${action}/profiles.json`,
|
`${action}/profiles.json`,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
"reviewer-token": ENCRYPTED,
|
"reviewer-token": ENCRYPTED,
|
||||||
profiles: [{ type: "claude", token: ENCRYPTED }, { type: "codex" }],
|
profiles: [{ type: "CLAUDE", token: ENCRYPTED }, { type: "CODEX" }],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ test.skipIf(!IS_ROOT)("removes prepared credentials when output publication fail
|
|||||||
`${action}/profiles.json`,
|
`${action}/profiles.json`,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
"reviewer-token": ENCRYPTED,
|
"reviewer-token": ENCRYPTED,
|
||||||
profiles: [{ type: "claude", token: ENCRYPTED }],
|
profiles: [{ type: "CLAUDE", token: ENCRYPTED }],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const result = spawnSync(
|
const result = spawnSync(
|
||||||
|
|||||||
@@ -194,9 +194,9 @@ test("central profiles fall back from Codex to Claude to OpenCode", async () =>
|
|||||||
const credentials: unknown = JSON.parse(await readFile("profiles.json", "utf8"));
|
const credentials: unknown = JSON.parse(await readFile("profiles.json", "utf8"));
|
||||||
const profiles = sequence(mapping(credentials, "profiles.json").get("profiles"), "profiles");
|
const profiles = sequence(mapping(credentials, "profiles.json").get("profiles"), "profiles");
|
||||||
expect(profiles.map((profile) => profile.get("type"))).toStrictEqual([
|
expect(profiles.map((profile) => profile.get("type"))).toStrictEqual([
|
||||||
"codex",
|
"CODEX",
|
||||||
"claude",
|
"CLAUDE",
|
||||||
"opencode",
|
"OPENCODE",
|
||||||
]);
|
]);
|
||||||
expect(profiles[2]?.get("model")).toBe("openrouter/qwen/qwen3.8-27b");
|
expect(profiles[2]?.get("model")).toBe("openrouter/qwen/qwen3.8-27b");
|
||||||
assert(String(profiles[2]?.get("token")).startsWith("v1:"));
|
assert(String(profiles[2]?.get("token")).startsWith("v1:"));
|
||||||
|
|||||||
Reference in New Issue
Block a user