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", "\n### Playwright screenshots\n", 0, ], ["reviewer", "User", "