evOo-local/.agents/skills/pr-watch/pr-digest.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

195 lines
7.3 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const script = fileURLToPath(new URL("./pr-digest", import.meta.url));
const mockGh = String.raw`#!/usr/bin/env node
const { readFileSync, appendFileSync } = require("node:fs");
const args = process.argv.slice(2);
const fixture = JSON.parse(readFileSync(process.env.FIXTURE, "utf8"));
const print = (value) => console.log(typeof value === "string" ? value : JSON.stringify(value));
if (args[0] === "repo" && args[1] === "view") print("example/project");
else if (args[0] === "pr" && args[1] === "view") {
if (args.includes("--jq")) print("abc");
else print({ headRefOid: "abc", state: "OPEN", mergeable: "MERGEABLE", headRefName: "topic", reviewDecision: "" });
} else if (args[0] === "api") {
const endpoint = args.find((arg) => arg === "graphql" || arg.startsWith("repos/"));
if (endpoint === "graphql") print("viewer");
else if (endpoint.endsWith("/replies")) {
appendFileSync(process.env.CALLS, "POST\n");
process.exit(1);
} else if (endpoint.includes("/pulls/comments/")) print("1");
else if (endpoint.includes("/check-runs?")) print(fixture.check ?? { name: "tests", status: "completed", conclusion: "success", html_url: "" });
else if (endpoint.includes("/actions/jobs/")) {
const result = require("node:child_process").spawnSync("jq", ["-r", args[args.indexOf("--jq") + 1]], {
input: JSON.stringify({ steps: fixture.steps ?? [] }), encoding: "utf8"
});
process.stdout.write(result.stdout);
process.stderr.write(result.stderr);
process.exit(result.status);
}
else if (endpoint.includes("/status?")) {}
else if (endpoint.includes("/issues/")) print(fixture.conversation ?? []);
else if (endpoint.includes("/reviews?")) print(fixture.reviews ?? []);
else if (endpoint.includes("/comments?")) print(fixture.inline ?? []);
else process.exit(2);
} else process.exit(2);
`;
function comment({
id = 12,
body = "Please fix this",
user = "reviewer",
kind = "User",
updated = "2026-01-01T00:00:00Z",
} = {}) {
return {
id,
body,
user: { login: user, type: kind },
created_at: updated,
updated_at: updated,
html_url: `https://github.com/example/project/pull/1#issuecomment-${id}`,
};
}
function runDigest(
fixture: {
conversation?: object[];
inline?: object[];
reviews?: object[];
check?: object;
steps?: object[];
},
...args: string[]
) {
const root = mkdtempSync(join(tmpdir(), "pr-digest-test-"));
try {
writeFileSync(join(root, "gh"), mockGh, { mode: 0o755 });
writeFileSync(join(root, "fixture"), JSON.stringify(fixture));
const callsPath = join(root, "calls");
const result = spawnSync("bash", [script, ...(args.length ? args : ["1"])], {
cwd: root,
env: {
...process.env,
PATH: `${root}${delimiter}${process.env.PATH}`,
FIXTURE: join(root, "fixture"),
CALLS: callsPath,
PR_DIGEST_SEEN_DIR: join(root, "seen"),
},
encoding: "utf8",
timeout: 20_000,
});
expect(result.error).toBeUndefined();
expect(result.stderr).not.toContain("jq: error");
return { ...result, calls: existsSync(callsPath) ? readFileSync(callsPath, "utf8") : "" };
} finally {
rmSync(root, { recursive: true, force: true });
}
}
const original = comment();
const reply = (body: string) =>
comment({ id: 13, body, user: "viewer", updated: "2026-01-02T00:00:00Z" });
describe("pr-digest", () => {
it.each(["Run pnpm test:integration", "Install dependencies"])(
"does not infer whether tests ran from skipped steps after %s",
(name) => {
const result = runDigest({
check: {
name: "Postgres journeys",
status: "completed",
conclusion: "failure",
html_url: "https://github.com/example/project/actions/runs/1/job/2",
},
steps: [
{ name, number: 1, conclusion: "failure" },
{ name: "Upload report", number: 2, conclusion: "skipped" },
],
});
expect(result.status).toBe(10);
expect(result.stdout).toContain(`step "${name}" failure (later steps skipped)`);
expect(result.stdout).not.toContain("tests did not run");
},
);
it.each([
[original.html_url, 0],
[`[fixed](${original.html_url})`, 0],
[`${original.html_url}.`, 0],
[`${original.html_url}3.`, 11],
[`${original.html_url}3`, 11],
["#issuecomment-12", 11],
])("matches complete permalinks: %s", (body, expected) => {
expect(runDigest({ conversation: [original, reply(body)] }).status).toBe(expected);
});
it("reopens edited conversation comments", () => {
const edited = comment({ updated: "2026-01-03T00:00:00Z" });
expect(runDigest({ conversation: [edited, reply(edited.html_url)] }).status).toBe(11);
});
it.each([
["reviewer", "User", "[vc]: Please fix this", 11],
["unknown[bot]", "Bot", "[vc]: Please fix this", 11],
["vercel[bot]", "Bot", "[vc]: generated deployment", 0],
[
"github-actions[bot]",
"Bot",
"<!-- rakazo-playwright-screenshots -->\n### Playwright screenshots\n",
0,
],
["reviewer", "User", "<h3>Greptile Summary</h3>", 11],
])("requires matching automation for noise: %s %s %s", (user, kind, body, expected) => {
expect(runDigest({ conversation: [comment({ body, user, kind })] }).status).toBe(expected);
});
it.each([
{ states: ["APPROVED"], expected: 0 },
{ states: ["COMMENTED"], expected: 0 },
{ states: ["CHANGES_REQUESTED"], expected: 11 },
{ states: ["CHANGES_REQUESTED", "APPROVED"], expected: 0 },
{ states: ["CHANGES_REQUESTED", "COMMENTED"], expected: 11 },
{ states: ["DISMISSED"], expected: 0 },
])("only blocks effective change requests: $states", ({ states, expected }) => {
const reviews = states.map((state, id) => ({
id,
state,
body: "Review summary",
submitted_at: `2026-01-0${id + 1}T00:00:00Z`,
user: { login: "reviewer", type: "User" },
}));
expect(runDigest({ reviews }).status).toBe(expected);
});
it("does not let an inline reply close a conversation", () => {
const inline = { ...reply("Already fixed"), in_reply_to_id: 19 };
expect(runDigest({ conversation: [original], inline: [inline] }).status).toBe(11);
});
it.each([
["2026-01-01T00:00:00Z", 0],
["2026-01-03T00:00:00Z", 11],
])("requires a new reply after inline edits: %s", (updated, expected) => {
const root = { ...comment({ updated }), path: "example.sh", line: 1, in_reply_to_id: null };
const response = { ...reply("Fixed"), in_reply_to_id: root.id };
expect(runDigest({ inline: [root, response] }).status).toBe(expected);
});
it("removes terminal controls", () => {
const result = runDigest({ conversation: [comment({ body: "Fix \x1b]0;title\x07 this" })] });
expect(result.status).toBe(11);
expect(result.stdout).not.toContain("\x1b");
expect(result.stdout).not.toContain("\x07");
});
it("does not retry failed replies", () => {
const result = runDigest({}, "--reply", "12", "Fixed");
expect(result.status).toBe(1);
expect(result.calls).toBe("POST\n");
});
});