Files
bot-agents/src/post/post.test.ts
T
Danny Kim 063eb09750 Replace NodeJS dict aliases with maps and named field reads
Co-authored-by: Danny Kim <temeddix@gmail.com>
Co-committed-by: Danny Kim <temeddix@gmail.com>
2026-10-20 05:01:49 +00:00

334 lines
9.3 KiB
TypeScript

import { chmod, mkdir, rm, stat, symlink, writeFile } from "node:fs/promises";
import { assert, expect, test } from "vite-plus/test";
import { isNotFound } from "#common";
import type { RuntimeFailure, RuntimeManifest, RuntimeOutcome } from "#runtime";
import { currentGid, currentUid, IS_WINDOWS, makeTempDir } from "#testing";
import { type Fetcher, type PostContext, postRuntime } from "./post.ts";
const context: PostContext = {
apiUrl: "https://gitea.example/api/v1",
repository: "owner/repo",
issueIndex: "7",
eventName: "issue_comment",
};
interface Fixture {
manifestPath: string;
runtimeDir: string;
reviewerFile: string;
reviewPath: string;
outcomeFile: string;
}
interface FixtureSuccess {
ok: true;
body: string;
}
type FixtureSource = RuntimeFailure | FixtureSuccess;
// One request the fake Gitea received.
interface RecordedRequest {
url: string;
init: RequestInit;
}
async function writePrivate(path: string, text: string): Promise<void> {
await writeFile(path, text, { mode: 0o600 });
await chmod(path, 0o600);
}
async function writeArtifacts(
outcome: RuntimeOutcome,
review?: string,
anchors?: unknown,
): Promise<void> {
if (!outcome.ok) {
return;
}
if (review !== undefined) {
await writePrivate(outcome.reviewPath, review);
}
if (anchors !== undefined) {
await writePrivate(outcome.anchorsPath, JSON.stringify(anchors));
}
}
function manifestFor(runtimeDir: string, profileHome: string): RuntimeManifest {
return {
runtimeDir,
manifestPath: `${runtimeDir}/manifest.json`,
reviewerFile: `${runtimeDir}/reviewer-token`,
outcomeFile: `${runtimeDir}/outcome.json`,
profiles: [
{
index: 0,
type: "CLAUDE",
model: "test",
uid: currentUid(),
gid: currentGid(),
home: profileHome,
tokenFile: null,
},
],
};
}
async function fixture(
source: FixtureSource,
review?: string,
anchors?: unknown,
): Promise<Fixture> {
const runtimeDir = await makeTempDir();
const profileHome = `${runtimeDir}/profile-0`;
const manifest = manifestFor(runtimeDir, profileHome);
await mkdir(profileHome);
await writeFile(manifest.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 writeFile(manifest.outcomeFile, JSON.stringify(outcome));
await writeArtifacts(outcome, review, anchors);
await writeFile(manifest.manifestPath, JSON.stringify(manifest));
const { manifestPath, reviewerFile, outcomeFile } = manifest;
return {
manifestPath,
runtimeDir,
reviewerFile,
reviewPath: `${profileHome}/review.md`,
outcomeFile,
};
}
const missing = async (path: string): Promise<boolean> => {
try {
await stat(path);
return false;
} catch (error) {
if (isNotFound(error)) {
return true;
}
throw error;
}
};
// The JSON body of one request the fake Gitea received.
function requestBody(init?: RequestInit): unknown {
return typeof init?.body === "string" ? JSON.parse(init.body) : undefined;
}
// A fake Gitea that records each request body and answers with `status`.
function recordingFetch(bodies: unknown[], status = 201): Fetcher {
return async (_url, init) => {
bodies.push(requestBody(init));
const resolved = await Promise.resolve(new Response(null, { status }));
return resolved;
};
}
test("posts a successful issue result and removes runtime credentials", async () => {
const result = await fixture({ ok: true, body: "done" });
const requests: RecordedRequest[] = [];
await postRuntime(result.manifestPath, context, async (url, init) => {
requests.push({ url, init });
const resolved = await Promise.resolve(new Response(null, { status: 201 }));
return resolved;
});
expect(requests.length).toBe(1);
const [request] = requests;
expect(request?.url).toStrictEqual(
`${context.apiUrl}/repos/owner/repo/issues/7/comments`,
);
expect(requestBody(request?.init)).toStrictEqual({
body: "done\n\n> Profile: `codex` · `gpt-test`",
});
expect(request?.init?.headers).toStrictEqual({
Authorization: "token reviewer-secret",
"Content-Type": "application/json",
});
assert(await missing(result.runtimeDir));
});
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",
},
async (_url, init) => {
bodies.push(requestBody(init));
const resolved = await Promise.resolve(
new Response(null, { status: bodies.length === 1 ? 422 : 200 }),
);
return resolved;
},
);
expect(bodies[0]).toMatchObject({
comments: [{ body: "Finding\n\n> Profile: `codex` · `gpt-test`" }],
});
expect(bodies[1]).toMatchObject({
comments: [],
body: "✅ Approved\nReady\n\n> Profile: `codex` · `gpt-test`",
});
assert(await missing(result.runtimeDir));
});
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 expect(
postRuntime(result.manifestPath, context, recordingFetch(bodies)),
).rejects.toThrow("agent phase failed");
expect(bodies).toStrictEqual([
{
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));
});
test("drops failure entries that are not known labels", async () => {
const result = await fixture({ ok: false, failures: [] });
await writeFile(
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 expect(
postRuntime(result.manifestPath, context, recordingFetch(bodies)),
).rejects.toThrow("agent phase failed");
expect(bodies).toStrictEqual([
{
body: "AI bot failed.\n\n> Tried: `claude` · `claude-test` (sign-in failed)",
},
]);
});
test("reports a generic failure when the runner wrote no outcome", async () => {
const result = await fixture({ ok: false, failures: [] });
await rm(result.outcomeFile);
const bodies: unknown[] = [];
await expect(
postRuntime(result.manifestPath, context, recordingFetch(bodies)),
).rejects.toThrow("agent phase failed");
expect(bodies).toStrictEqual([{ body: "AI bot failed.\n\n> Profile: none" }]);
assert(await missing(result.runtimeDir));
});
test("removes runtime credentials when posting fails", async () => {
const result = await fixture({ ok: true, body: "done" });
await expect(
postRuntime(result.manifestPath, context, async () => {
const resolved = await Promise.resolve(
new Response("failed", { status: 500 }),
);
return resolved;
}),
).rejects.toThrow("500");
assert(await missing(result.runtimeDir));
});
test("preserves a Gitea transport failure without retrying a comment", async () => {
const result = await fixture({ ok: true, body: "done" });
let requests = 0;
await expect(
postRuntime(result.manifestPath, context, async () => {
requests += 1;
await Promise.resolve();
throw new Error(`network failure ${requests}`);
}),
).rejects.toThrow("network failure 1");
expect(requests).toBe(1);
assert(await missing(result.runtimeDir));
});
test.skipIf(IS_WINDOWS)(
"refuses to follow an agent artifact symlink",
async () => {
const result = await fixture({ ok: true, body: "done" });
await symlink(result.reviewerFile, result.reviewPath);
let requests = 0;
await expect(
postRuntime(result.manifestPath, context, async () => {
requests += 1;
const resolved = await Promise.resolve(
new Response(null, { status: 200 }),
);
return resolved;
}),
).rejects.toThrow("regular private file");
expect(requests).toBe(1);
assert(await missing(result.runtimeDir));
},
);
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 expect(
postRuntime(
result.manifestPath,
{
...context,
eventName: "pull_request",
},
recordingFetch(bodies),
),
).rejects.toThrow("did not write");
expect(bodies).toStrictEqual([
{
body: "AI bot failed.\n\n> Tried: `codex` · `gpt-test` (failed)",
},
]);
assert(await missing(result.runtimeDir));
});