Files
bot-agents/src/post_test.ts
T
Danny Kim 3565988249
Agents / agent (pull_request) Has been cancelled
Check / deno (pull_request) Failing after 35s
Recognize sign-in failures for every agent and fall through
Codex re-runs device login on revoked, expired, or reused sessions and
token_revoked/401 errors, not only invalid_refresh_token. Claude and
OpenCode credential rejections are classified too. Sign-in failures fall
through to the next profile, and the failure comment names each tried
profile with a fixed reason label.
2026-10-12 11:38:21 +09:00

297 lines
8.7 KiB
TypeScript

import { assert, assertEquals, assertRejects } from "jsr:@std/assert@1";
import { type PostContext, postRuntime } from "./post.ts";
import type { RuntimeManifest, RuntimeOutcome } from "./runtime.ts";
const context: PostContext = {
apiUrl: "https://gitea.example/api/v1",
repository: "owner/repo",
issueIndex: "7",
eventName: "issue_comment",
};
async function fixture(
source: Extract<RuntimeOutcome, { ok: false }> | { ok: true; body: string },
review?: string,
anchors?: unknown,
): Promise<{
manifestPath: string;
runtimeDir: string;
reviewerFile: string;
reviewPath: string;
outcomeFile: string;
}> {
const runtimeDir = await Deno.makeTempDir();
const reviewerFile = `${runtimeDir}/reviewer-token`;
const outcomeFile = `${runtimeDir}/outcome.json`;
const profileHome = `${runtimeDir}/profile-0`;
await Deno.mkdir(profileHome);
await Deno.writeTextFile(reviewerFile, "reviewer-secret");
const outcome: RuntimeOutcome = source.ok
? {
...source,
profile: { type: "codex", model: "gpt-test" },
reviewPath: `${profileHome}/review.md`,
anchorsPath: `${profileHome}/anchors.json`,
}
: source;
await Deno.writeTextFile(outcomeFile, JSON.stringify(outcome));
if (outcome.ok) {
if (review !== undefined) {
await Deno.writeTextFile(outcome.reviewPath, review, { mode: 0o600 });
await Deno.chmod(outcome.reviewPath, 0o600);
}
if (anchors !== undefined) {
await Deno.writeTextFile(outcome.anchorsPath, JSON.stringify(anchors), {
mode: 0o600,
});
await Deno.chmod(outcome.anchorsPath, 0o600);
}
}
const manifestPath = `${runtimeDir}/manifest.json`;
const manifest: RuntimeManifest = {
runtimeDir,
manifestPath,
reviewerFile,
outcomeFile,
profiles: [{
index: 0,
type: "claude",
model: "test",
uid: Deno.uid()!,
gid: Deno.gid()!,
home: profileHome,
tokenFile: null,
}],
};
await Deno.writeTextFile(manifestPath, JSON.stringify(manifest));
return {
manifestPath,
runtimeDir,
reviewerFile,
reviewPath: `${profileHome}/review.md`,
outcomeFile,
};
}
const missing = async (path: string): Promise<boolean> => {
try {
await Deno.stat(path);
return false;
} catch (error) {
if (error instanceof Deno.errors.NotFound) return true;
throw error;
}
};
Deno.test("posts a successful issue result and removes runtime credentials", async () => {
const result = await fixture({ ok: true, body: "done" });
const requests: Array<{ url: string; init?: RequestInit }> = [];
await postRuntime(result.manifestPath, context, (url, init) => {
requests.push({ url: String(url), init });
return Promise.resolve(new Response(null, { status: 201 }));
});
assertEquals(requests.length, 1);
assertEquals(
requests[0].url,
`${context.apiUrl}/repos/owner/repo/issues/7/comments`,
);
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",
});
assert(await missing(result.runtimeDir));
});
Deno.test("retries a rejected inline review without anchors", async () => {
const result = await fixture(
{ ok: true, body: "ignored final response" },
"Approved\nReady",
[{ path: "run.ts", line: 1, side: "new", body: "Finding" }],
);
const bodies: unknown[] = [];
await postRuntime(result.manifestPath, {
...context,
eventName: "pull_request",
}, (_url, init) => {
bodies.push(JSON.parse(String(init?.body)));
return Promise.resolve(
new Response(null, { status: bodies.length === 1 ? 422 : 200 }),
);
});
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));
});
Deno.test("reports each tried profile and keeps the action failed", async () => {
const result = await fixture({
ok: false,
failures: [
{ type: "codex", model: "gpt-test", reason: "sign-in failed" },
{ type: "claude", model: "claude-test", reason: "usage exhausted" },
{ type: "opencode", model: "openrouter/test", reason: "failed" },
],
});
const bodies: unknown[] = [];
await assertRejects(
() =>
postRuntime(result.manifestPath, context, (_url, init) => {
bodies.push(JSON.parse(String(init?.body)));
return Promise.resolve(new Response(null, { status: 201 }));
}),
Error,
"agent phase failed",
);
assertEquals(bodies, [{
body: "AI bot failed.\n\n> Tried: `codex` · `gpt-test` (sign-in failed), " +
"`claude` · `claude-test` (usage exhausted), " +
"`opencode` · `openrouter/test` (failed)",
}]);
assert(!JSON.stringify(bodies).includes("reviewer-secret"));
assert(await missing(result.runtimeDir));
});
Deno.test("drops failure entries that are not known labels", async () => {
const result = await fixture({ ok: false, failures: [] });
await Deno.writeTextFile(
result.outcomeFile,
JSON.stringify({
ok: false,
failures: [
{ type: "codex", model: "gpt-test", reason: "token sk-secret leaked" },
{ type: "shell", model: "x", reason: "failed" },
{ type: "claude", model: "claude-test", reason: "sign-in failed" },
],
}),
);
const bodies: unknown[] = [];
await assertRejects(
() =>
postRuntime(result.manifestPath, context, (_url, init) => {
bodies.push(JSON.parse(String(init?.body)));
return Promise.resolve(new Response(null, { status: 201 }));
}),
Error,
"agent phase failed",
);
assertEquals(bodies, [{
body:
"AI bot failed.\n\n> Tried: `claude` · `claude-test` (sign-in failed)",
}]);
});
Deno.test("reports a generic failure when the runner wrote no outcome", async () => {
const result = await fixture({ ok: false, failures: [] });
await Deno.remove(result.outcomeFile);
const bodies: unknown[] = [];
await assertRejects(
() =>
postRuntime(result.manifestPath, context, (_url, init) => {
bodies.push(JSON.parse(String(init?.body)));
return Promise.resolve(new Response(null, { status: 201 }));
}),
Error,
"agent phase failed",
);
assertEquals(bodies, [{ body: "AI bot failed.\n\n> Profile: none" }]);
assert(await missing(result.runtimeDir));
});
Deno.test("removes runtime credentials when posting fails", async () => {
const result = await fixture({ ok: true, body: "done" });
await assertRejects(
() =>
postRuntime(
result.manifestPath,
context,
() => Promise.resolve(new Response("failed", { status: 500 })),
),
Error,
"500",
);
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" });
await Deno.symlink(result.reviewerFile, result.reviewPath);
let requests = 0;
await assertRejects(
() =>
postRuntime(result.manifestPath, context, () => {
requests++;
return Promise.resolve(new Response(null, { status: 200 }));
}),
Error,
"regular private file",
);
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> Tried: `codex` · `gpt-test` (failed)",
}]);
assert(await missing(result.runtimeDir));
});