203 lines
5.9 KiB
TypeScript
203 lines
5.9 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: { 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,
|
|
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" });
|
|
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[1] as { comments: unknown[] }).comments, []);
|
|
assert(await missing(result.runtimeDir));
|
|
});
|
|
|
|
Deno.test("reports a generic agent failure and keeps the action failed", async () => {
|
|
const result = await fixture({ ok: false });
|
|
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." }]);
|
|
assert(!JSON.stringify(bodies).includes("reviewer-secret"));
|
|
assert(await missing(result.runtimeDir));
|
|
});
|
|
|
|
Deno.test("reports a generic failure when the runner wrote no outcome", async () => {
|
|
const result = await fixture({ ok: false });
|
|
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." }]);
|
|
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("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, 0);
|
|
assert(await missing(result.runtimeDir));
|
|
});
|