Show the selected bot profile and add OpenRouter fallback (#18)

Uses Codex first, then Claude, then OpenCode through OpenRouter when quota is exhausted.

Every generated comment and review now identifies the selected profile and model in a final quote. The dependent action interface remains one master key.

Verified formatting, lint, type checks, encrypted profile decryption, current OpenCode CLI flags, and 45 tests.

Reviewed-on: #18
Reviewed-by: bot <noreply@capsulizers.com>
Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #18.
This commit is contained in:
Danny Kim
2026-09-24 01:39:49 +00:00
committed by temeddix
parent 4ecdf5adc0
commit ea20f6d629
13 changed files with 429 additions and 78 deletions
+60 -4
View File
@@ -29,6 +29,7 @@ async function fixture(
const outcome: RuntimeOutcome = source.ok
? {
...source,
profile: { type: "codex", model: "gpt-test" },
reviewPath: `${profileHome}/review.md`,
anchorsPath: `${profileHome}/anchors.json`,
}
@@ -96,7 +97,9 @@ Deno.test("posts a successful issue result and removes runtime credentials", asy
requests[0].url,
`${context.apiUrl}/repos/owner/repo/issues/7/comments`,
);
assertEquals(JSON.parse(String(requests[0].init?.body)), { body: "done" });
assertEquals(JSON.parse(String(requests[0].init?.body)), {
body: "done\n\n> Profile: `codex` · `gpt-test`",
});
assertEquals(requests[0].init?.headers, {
Authorization: "token reviewer-secret",
"Content-Type": "application/json",
@@ -123,7 +126,15 @@ Deno.test("retries a rejected inline review without anchors", async () => {
});
assertEquals((bodies[0] as { comments: unknown[] }).comments.length, 1);
assertEquals(
(bodies[0] as { comments: Array<{ body: string }> }).comments[0].body,
"Finding\n\n> Profile: `codex` · `gpt-test`",
);
assertEquals((bodies[1] as { comments: unknown[] }).comments, []);
assertEquals(
(bodies[1] as { body: string }).body,
"✅ Approved\nReady\n\n> Profile: `codex` · `gpt-test`",
);
assert(await missing(result.runtimeDir));
});
@@ -141,7 +152,9 @@ Deno.test("reports a generic agent failure and keeps the action failed", async (
"agent phase failed",
);
assertEquals(bodies, [{ body: "AI bot failed." }]);
assertEquals(bodies, [{
body: "AI bot failed.\n\n> Profile: none · all profiles failed",
}]);
assert(!JSON.stringify(bodies).includes("reviewer-secret"));
assert(await missing(result.runtimeDir));
});
@@ -161,7 +174,9 @@ Deno.test("reports a generic failure when the runner wrote no outcome", async ()
"agent phase failed",
);
assertEquals(bodies, [{ body: "AI bot failed." }]);
assertEquals(bodies, [{
body: "AI bot failed.\n\n> Profile: none · all profiles failed",
}]);
assert(await missing(result.runtimeDir));
});
@@ -181,6 +196,24 @@ Deno.test("removes runtime credentials when posting fails", async () => {
assert(await missing(result.runtimeDir));
});
Deno.test("preserves a Gitea transport failure without retrying a comment", async () => {
const result = await fixture({ ok: true, body: "done" });
let requests = 0;
await assertRejects(
() =>
postRuntime(result.manifestPath, context, () => {
requests++;
return Promise.reject(new Error(`network failure ${requests}`));
}),
Error,
"network failure 1",
);
assertEquals(requests, 1);
assert(await missing(result.runtimeDir));
});
Deno.test("refuses to follow an agent artifact symlink", async () => {
if (Deno.build.os === "windows") return;
const result = await fixture({ ok: true, body: "done" });
@@ -197,6 +230,29 @@ Deno.test("refuses to follow an agent artifact symlink", async () => {
"regular private file",
);
assertEquals(requests, 0);
assertEquals(requests, 1);
assert(await missing(result.runtimeDir));
});
Deno.test("reports an attributed failure when a pull request review is missing", async () => {
const result = await fixture({ ok: true, body: "done" });
const bodies: unknown[] = [];
await assertRejects(
() =>
postRuntime(result.manifestPath, {
...context,
eventName: "pull_request",
}, (_url, init) => {
bodies.push(JSON.parse(String(init?.body)));
return Promise.resolve(new Response(null, { status: 201 }));
}),
Error,
"did not write",
);
assertEquals(bodies, [{
body: "AI bot failed.\n\n> Profile: none · all profiles failed",
}]);
assert(await missing(result.runtimeDir));
});