This commit is contained in:
+202
@@ -0,0 +1,202 @@
|
||||
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));
|
||||
});
|
||||
Reference in New Issue
Block a user