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 => { 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)); });