import { spawn } from "node:child_process"; import { existsSync, watch } from "node:fs"; import { appendFile, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { aroundEach, describe, expect, it, vi } from "vitest"; import { cleanGitEnvironment } from "../git.js"; import { isGitHubReviewCurrent as isCurrent, openGitHubReview as openReview, refreshGitHubReview as refreshReview, } from "../github.js"; import { cleanupNative, git, nativeOperation, nativeTest, testSignal, withFakeExecutable, withFakeGh, } from "./fixture.js"; // Always exercise the native adapter, with cancellation bounded by the test. const openGitHubReview = ( ...[cwd, id, confirm, signal]: Parameters ) => nativeOperation(() => openReview(cwd, id, confirm, testSignal(signal))); const isGitHubReviewCurrent = ( ...[cwd, review, signal]: Parameters ) => nativeOperation(() => isCurrent(cwd, review, testSignal(signal))); const refreshGitHubReview = ( ...[cwd, review, signal]: Parameters ) => nativeOperation(() => refreshReview(cwd, review, testSignal(signal))); aroundEach(async (run, context) => { try { await nativeTest(run, context); } finally { vi.restoreAllMocks(); } }); async function cancelAfterMarker( marker: string, message: string, start: (signal: AbortSignal) => Promise, ): Promise { const controller = new AbortController(); const watcher = watch(path.dirname(marker)); const started = new Promise((resolve, reject) => { watcher.on("change", () => { if (existsSync(marker)) resolve(); }); watcher.on("error", reject); }); const opening = start(controller.signal); try { await Promise.race([ started, opening.then(() => { throw new Error("PR opening completed before the cancellation signal"); }), ]); expect(existsSync(marker)).toBe(true); const pid = Number(await readFile(marker, "utf8")); expect(Number.isInteger(pid) && pid > 0).toBe(true); controller.abort(new Error(message)); await expect(opening).rejects.toThrow(message); expect(() => process.kill(pid, 0)).toThrow(/ESRCH/); } finally { controller.abort(); watcher.close(); await opening.catch(() => {}); } } function gitInput( cwd: string, input: string, ...args: string[] ): Promise { return nativeOperation( () => new Promise((resolve, reject) => { const signal = testSignal(); signal.throwIfAborted(); const child = spawn("git", args, { cwd, env: cleanGitEnvironment(), stdio: ["pipe", "pipe", "pipe"], }); const abort = () => { child.kill(); }; signal.addEventListener("abort", abort, { once: true }); const stdout: Buffer[] = []; const stderr: Buffer[] = []; child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk)); child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk)); let failure: Error | undefined; child.once("error", (error) => { failure = error; }); child.stdin.once("error", (error) => { failure = error; }); child.once("close", (code) => { signal.removeEventListener("abort", abort); if (signal.aborted) reject(signal.reason); else if (failure) reject(failure); else if (code === 0) resolve(Buffer.concat(stdout).toString("utf8").trim()); else reject(new Error(Buffer.concat(stderr).toString("utf8").trim())); }); child.stdin.end(input); }), ); } async function repository() { return nativeOperation(async () => { testSignal().throwIfAborted(); const root = await mkdtemp(path.join(tmpdir(), "strata-github-")); cleanupNative(() => rm(root, { recursive: true, force: true })); await git(root, "init", "--quiet", "--initial-branch=main"); await appendFile( path.join(root, ".git", "config"), "[user]\n\tname = Strata Test\n\temail = strata@example.invalid\n", ); await writeFile(path.join(root, "change.ts"), "export const value = 1;\n"); await git(root, "add", "change.ts"); await git(root, "commit", "--quiet", "-m", "base"); const base = await git(root, "rev-parse", "HEAD"); await git(root, "checkout", "--quiet", "-b", "feature"); await writeFile(path.join(root, "change.ts"), "export const value = 2;\n"); await git(root, "commit", "--quiet", "-am", "feature"); const head = await git(root, "rev-parse", "HEAD"); await git(root, "checkout", "--quiet", "main"); await git( root, "remote", "add", "origin", "https://github.com/acme/project.git", ); return { root, base, head }; }); } function fakeGh( metadata: Record, pages: Record = {}, ) { return ` const metadata = ${JSON.stringify(metadata)}; const pages = ${JSON.stringify(pages)}; if (args[0] === "pr" && args[1] === "view") process.stdout.write(JSON.stringify(metadata)); else if (args[0] === "api") { if (!args.includes("GET") || args.some((arg) => ["POST", "PATCH", "PUT", "DELETE"].includes(arg))) process.exitCode = 91; else { const endpoint = args.at(-1); const key = Object.keys(pages).find((candidate) => endpoint.startsWith(candidate)); process.stdout.write(JSON.stringify(key ? pages[key] : [])); } } else process.exitCode = 92; `; } function metadata( base: string, head: string, overrides: Record = {}, ) { return { number: 7, url: "https://github.com/acme/project/pull/7", title: "Bound retries", body: "Keep failures visible.", author: { login: "review-author" }, baseRefName: "main", baseRefOid: base, headRefName: "feature", headRefOid: head, headRepository: { name: "project", nameWithOwner: "acme/project" }, headRepositoryOwner: { login: "acme" }, ...overrides, }; } describe.sequential("GitHub forge adapter", () => { it("loads all review comment kinds through read-only API calls and checks out the pinned existing branch", async () => { const { root, base, head } = await repository(); const pages = { "repos/acme/project/issues/7/comments": [ { id: 11, html_url: "https://github.com/acme/project/pull/7#issuecomment-11", user: { login: "discussant" }, body: "Discussion", }, ], "repos/acme/project/pulls/7/reviews": [ { id: 12, html_url: "https://github.com/acme/project/pull/7#pullrequestreview-12", user: { login: "reviewer" }, body: "Review summary", }, ], "repos/acme/project/pulls/7/comments": [ { id: 13, html_url: "https://github.com/acme/project/pull/7#discussion_r13", user: { login: "inline-reviewer" }, body: "Current line", path: "change.ts", side: "RIGHT", line: 1, commit_id: head, position: 1, }, { id: 14, html_url: "https://github.com/acme/project/pull/7#discussion_r14", user: { login: "inline-reviewer" }, body: "Old line", path: "change.ts", original_side: "LEFT", original_line: 1, original_commit_id: base, position: null, in_reply_to_id: 13, }, ], }; await withFakeGh(fakeGh(metadata(base, head), pages), async () => { const confirm = vi.fn(async () => true); const review = await openGitHubReview( root, "7", confirm, new AbortController().signal, ); expect(review).toMatchObject({ provider: "github", providerLabel: "GitHub", repository: "acme/project", number: 7, baseSha: base, headSha: head, checkoutBranch: "feature", }); expect(review.comments).toEqual([ expect.objectContaining({ id: "discussion:11", kind: "discussion", outdated: false, }), expect.objectContaining({ id: "review:12", kind: "review", outdated: false, }), expect.objectContaining({ id: "inline:13", kind: "inline", path: "change.ts", side: "new", line: 1, commit: head, outdated: false, }), expect.objectContaining({ id: "inline:14", replyTo: "inline:13", outdated: true, }), ]); expect(confirm).toHaveBeenCalledWith( "Switch to GitHub PR checkout?", expect.stringContaining( "Repository: acme/project\nPR: #7\nCurrent branch: main\nTarget branch: feature", ), expect.any(AbortSignal), ); expect(await git(root, "branch", "--show-current")).toBe("feature"); expect(await git(root, "rev-parse", "HEAD")).toBe(head); }); }); it("creates a non-colliding PR branch for a fork without overwriting existing branches", async () => { const { root, base, head } = await repository(); await git(root, "branch", "-D", "feature"); await git(root, "branch", "strata/pr-8-fork-work", "main"); const fork = metadata(base, head, { number: 8, url: "https://github.com/acme/project/pull/8", headRefName: "fork/work", headRepository: { name: "project", nameWithOwner: "contributor/project" }, headRepositoryOwner: { login: "contributor" }, }); await withFakeGh(fakeGh(fork), async () => { const review = await openGitHubReview( root, "https://github.com/acme/project/pull/8", async () => true, new AbortController().signal, ); expect(review.checkoutBranch).toBe("strata/pr-8-fork-work-2"); expect(await git(root, "rev-parse", review.checkoutBranch)).toBe(head); expect(await git(root, "rev-parse", "strata/pr-8-fork-work")).toBe(base); }); }); it("refuses a dirty checkout before mutation", async () => { const first = await repository(); await writeFile(path.join(first.root, "untracked.txt"), "dirty\n"); await withFakeGh(fakeGh(metadata(first.base, first.head)), async () => { await expect( openGitHubReview( first.root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/clean worktree/); expect(await git(first.root, "branch", "--show-current")).toBe("main"); }); }); it("refuses a denied checkout before mutation", async () => { const denied = await repository(); await withFakeGh(fakeGh(metadata(denied.base, denied.head)), async () => { await expect( openGitHubReview( denied.root, "7", async () => false, new AbortController().signal, ), ).rejects.toThrow(/not confirmed/); expect(await git(denied.root, "branch", "--show-current")).toBe("main"); }); }); it("refuses a checkout without UI before mutation", async () => { const denied = await repository(); await withFakeGh(fakeGh(metadata(denied.base, denied.head)), async () => { await expect( openGitHubReview( denied.root, "7", undefined, new AbortController().signal, ), ).rejects.toThrow(/no interactive confirmation/); expect(await git(denied.root, "branch", "--show-current")).toBe("main"); }); }); it("refuses a divergent checkout before mutation", async () => { const divergent = await repository(); await git(divergent.root, "checkout", "--quiet", "feature"); await writeFile(path.join(divergent.root, "other.ts"), "export {};\n"); await git(divergent.root, "add", "other.ts"); await git(divergent.root, "commit", "--quiet", "-m", "local commit"); await git(divergent.root, "checkout", "--quiet", "main"); await withFakeGh( fakeGh(metadata(divergent.base, divergent.head)), async () => { await expect( openGitHubReview( divergent.root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/diverges/); }, ); }); it("refuses protected checkout paths before mutation", async () => { const protectedChange = await repository(); const blob = await git( protectedChange.root, "rev-parse", `${protectedChange.head}:change.ts`, ); const tree = await gitInput( protectedChange.root, `100644 blob ${blob}\tchange.ts\n100644 blob ${blob}\t.env\n`, "mktree", ); const protectedHead = await git( protectedChange.root, "commit-tree", tree, "-p", protectedChange.base, "-m", "protected metadata fixture", ); const protectedMetadata = metadata(protectedChange.base, protectedHead, { headRefName: "protected-fixture", headRepository: { name: "project", nameWithOwner: "contributor/project", }, headRepositoryOwner: { login: "contributor" }, }); await withFakeGh(fakeGh(protectedMetadata), async () => { const confirm = vi.fn(async () => true); await expect( openGitHubReview( protectedChange.root, "7", confirm, new AbortController().signal, ), ).rejects.toThrow(/protected path \.env/); expect(confirm).toHaveBeenCalledOnce(); expect(await git(protectedChange.root, "branch", "--show-current")).toBe( "main", ); }); }); it("disables configured fsmonitor hooks on the real status route", async () => { const { root, base, head } = await repository(); const marker = path.join(root, "fsmonitor-ran"); await git(root, "config", "core.fsmonitor", "fake-fsmonitor"); await withFakeExecutable( "fake-fsmonitor", `require("node:fs").writeFileSync(${JSON.stringify(marker)}, "ran"); process.stdout.write("2\\n");`, async () => { await withFakeGh(fakeGh(metadata(base, head)), async () => { const review = await openGitHubReview( root, "7", async () => true, new AbortController().signal, ); expect(review.checkoutBranch).toBe("feature"); expect(existsSync(marker)).toBe(false); }); }, ); }); it("rejects external checkout filters configured during confirmation without executing them", async () => { const fixture = await repository(); await git(fixture.root, "checkout", "--quiet", "feature"); await writeFile( path.join(fixture.root, ".gitattributes"), "change.ts filter=guard\n", ); await git(fixture.root, "add", ".gitattributes"); await git(fixture.root, "commit", "--quiet", "--amend", "--no-edit"); fixture.head = await git(fixture.root, "rev-parse", "HEAD"); await git(fixture.root, "checkout", "--quiet", "main"); const marker = path.join(fixture.root, "filter-ran"); await withFakeExecutable( "fake-filter", `require("node:fs").writeFileSync(${JSON.stringify(marker)}, "ran");`, async () => { await withFakeGh( fakeGh(metadata(fixture.base, fixture.head)), async () => { await expect( openGitHubReview( fixture.root, "7", async () => { await git( fixture.root, "config", "filter.guard.clean", "fake-filter", ); await git( fixture.root, "config", "filter.guard.smudge", "fake-filter", ); await git( fixture.root, "config", "filter.guard.process", "fake-filter", ); return true; }, new AbortController().signal, ), ).rejects.toThrow(/external clean, smudge, or process filters/); expect(existsSync(marker)).toBe(false); expect(await git(fixture.root, "branch", "--show-current")).toBe( "main", ); }, ); }, ); }); it("rejects repository credential helpers before fetch", async () => { const credential = await repository(); const missing = "1".repeat(40); const credentialMarker = path.join(credential.root, "credential-ran"); await git( credential.root, "config", "credential.helper", "!fake-credential", ); await withFakeExecutable( "fake-credential", `require("node:fs").writeFileSync(${JSON.stringify(credentialMarker)}, "ran");`, async () => { await withFakeGh( fakeGh(metadata(missing, credential.head)), async () => { await expect( openGitHubReview( credential.root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/repository credential helpers/); expect(existsSync(credentialMarker)).toBe(false); }, ); }, ); }); it("rejects repository URL rewrites before fetch", async () => { const missing = "1".repeat(40); const rewrite = await repository(); const rewriteMarker = path.join(rewrite.root, "rewrite-ran"); await git( rewrite.root, "config", "url.unsafe://redirect/.insteadOf", "https://github.com/", ); await withFakeExecutable( "git-remote-unsafe", `require("node:fs").writeFileSync(${JSON.stringify(rewriteMarker)}, "ran");`, async () => { await withFakeGh(fakeGh(metadata(missing, rewrite.head)), async () => { await expect( openGitHubReview( rewrite.root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/repository URL rewrites/); expect(existsSync(rewriteMarker)).toBe(false); }); }, ); }); it("rejects repository include config without following it", async () => { const fixture = await repository(); const included = path.join(fixture.root, "included.config"); await writeFile(included, '[filter "included"]\n\tclean = fake-filter\n'); await git(fixture.root, "config", "include.path", included); await withFakeGh(fakeGh(metadata(fixture.base, fixture.head)), async () => { await expect( openGitHubReview( fixture.root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/repository include config/); }); }); it("uses gh as Git's fixed credential helper for a private HTTPS fetch", async () => { const fixture = await repository(); const missing = "2".repeat(40); const marker = path.join(fixture.root, "gh-credential-ran"); const program = ` const metadata = ${JSON.stringify(metadata(missing, fixture.head))}; if (args[0] === "auth" && args[1] === "git-credential") { require("node:fs").writeFileSync(${JSON.stringify(marker)}, "ran"); process.stdout.write("username=strata-test\\npassword=controlled-test-value\\n"); } else if (args[0] === "pr" && args[1] === "view") process.stdout.write(JSON.stringify(metadata)); else if (args[0] === "api") process.stdout.write("[]"); else process.exitCode = 92; `; await withFakeExecutable( "git-remote-https", `const result = require("node:child_process").spawnSync("git", ["credential", "fill"], { input: "protocol=https\\nhost=github.com\\n\\n", encoding: "utf8" }); if (result.status !== 0) process.exitCode = 90; else process.exitCode = 1;`, async () => { await withFakeGh(program, async () => { await expect( openGitHubReview( fixture.root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/could not fetch pinned commit/); expect(existsSync(marker)).toBe(true); }); }, ); }); it("paginates discussion through read-only API calls", async () => { const paginated = await repository(); const discussions = Array.from({ length: 101 }, (_, index) => ({ id: index + 1, html_url: `https://github.com/acme/project/pull/7#issuecomment-${index + 1}`, user: { login: `author-${index + 1}` }, body: `Discussion ${index + 1}`, })); const paginatedGh = ` const metadata = ${JSON.stringify(metadata(paginated.base, paginated.head))}; const discussions = ${JSON.stringify(discussions)}; if (args[0] === "pr" && args[1] === "view") process.stdout.write(JSON.stringify(metadata)); else if (args[0] === "api") { if (!args.includes("GET") || args.some((arg) => ["POST", "PATCH", "PUT", "DELETE"].includes(arg))) process.exitCode = 91; else { const endpoint = args.at(-1); const page = Number(/[?&]page=(\\d+)/.exec(endpoint)?.[1] ?? "1"); const values = endpoint.startsWith("repos/acme/project/issues/7/comments") ? discussions.slice((page - 1) * 100, page * 100) : []; process.stdout.write(JSON.stringify(values)); } } else process.exitCode = 92; `; await withFakeGh(paginatedGh, async () => { const review = await openGitHubReview( paginated.root, "7", async () => true, new AbortController().signal, ); expect(review.comments).toHaveLength(101); expect(review.comments.at(-1)?.id).toBe("discussion:101"); }); }); it("rejects malformed forge metadata", async () => { const malformed = await repository(); await withFakeGh( fakeGh( metadata(malformed.base, malformed.head, { baseRefOid: "not-an-oid" }), ), async () => { await expect( openGitHubReview( malformed.root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/invalid PR base commit/); }, ); }); it("rejects foreign PR URLs", async () => { const { root, base, head } = await repository(); await withFakeGh(fakeGh(metadata(base, head)), async () => { await expect( openGitHubReview( root, "https://github.com/other/project/pull/7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/does not belong/); }); }); it("rejects unsafe imported links", async () => { const { root, base, head } = await repository(); const pages = { "repos/acme/project/issues/7/comments": [ { id: 1, html_url: "javascript:alert(1)", user: { login: "attacker" }, body: "", }, ], }; await withFakeGh(fakeGh(metadata(base, head), pages), async () => { await expect( openGitHubReview( root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/unsafe discussion comment url/); }); }); it.each(["same-commit branch switch", "dirty worktree"])( "rejects freshness after a %s", async (change) => { const { root, base, head } = await repository(); await git(root, "checkout", "--quiet", "feature"); await withFakeGh(fakeGh(metadata(base, head)), async () => { const review = await openGitHubReview( root, undefined, async () => true, new AbortController().signal, ); expect( await isGitHubReviewCurrent( root, review, new AbortController().signal, ), ).toBe(true); if (change === "same-commit branch switch") { await git(root, "branch", "same-sha", head); await git(root, "checkout", "--quiet", "same-sha"); } else { await writeFile(path.join(root, "local.txt"), "dirty\n"); } expect( await isGitHubReviewCurrent( root, review, new AbortController().signal, ), ).toBe(false); }); }, ); it("requires reopen for a new PR head", async () => { const changed = await repository(); await withFakeGh(fakeGh(metadata(changed.base, changed.head)), async () => { const review = await openGitHubReview( changed.root, "7", async () => true, new AbortController().signal, ); await git( changed.root, "checkout", "--quiet", "-b", "remote-new", changed.head, ); await writeFile( path.join(changed.root, "next.ts"), "export const next = true;\n", ); await git(changed.root, "add", "next.ts"); await git( changed.root, "commit", "--quiet", "-m", "new remote head fixture", ); const nextHead = await git(changed.root, "rev-parse", "HEAD"); await git(changed.root, "checkout", "--quiet", "feature"); await withFakeGh(fakeGh(metadata(changed.base, nextHead)), async () => { await expect( refreshGitHubReview( changed.root, review, new AbortController().signal, ), ).rejects.toThrow(/new head commit.*reopen/s); }); }); }); it("validates discussion before confirmation and leaves the original branch on failure", async () => { const { root, base, head } = await repository(); const program = ` const metadata = ${JSON.stringify(metadata(base, head))}; if (args[0] === "pr") process.stdout.write(JSON.stringify(metadata)); else if (args.at(-1).startsWith("repos/acme/project/issues/7/comments")) process.stdout.write("{}"); else process.stdout.write("[]"); `; await withFakeGh(program, async () => { const confirm = vi.fn(async () => true); await expect( openGitHubReview(root, "7", confirm, new AbortController().signal), ).rejects.toThrow(/discussion page must be an array/); expect(confirm).not.toHaveBeenCalled(); expect(await git(root, "branch", "--show-current")).toBe("main"); }); }); it("leaves the original branch when comment loading is cancelled", async () => { const { root, base, head } = await repository(); const marker = path.join(root, "comment-started"); const program = ` if (args[0] === "pr") process.stdout.write(${JSON.stringify(JSON.stringify(metadata(base, head)))}); else if (args.at(-1).startsWith("repos/acme/project/issues/7/comments")) { require("node:fs").writeFileSync(${JSON.stringify(`${marker}.pending`)}, String(process.pid)); require("node:fs").renameSync(${JSON.stringify(`${marker}.pending`)}, ${JSON.stringify(marker)}); Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10000); } else process.stdout.write("[]"); `; await withFakeGh(program, async () => { await cancelAfterMarker( marker, "cancelled while loading comments", (signal) => openGitHubReview(root, "7", async () => true, signal), ); expect(await git(root, "branch", "--show-current")).toBe("main"); }); }); it("rejects metadata changed after confirmation without switching", async () => { const { root, base, head } = await repository(); const marker = path.join(root, ".git", "metadata-loaded"); const first = metadata(base, head); const changed = metadata(base, head, { title: "Changed during confirmation", }); const program = ` if (args[0] === "pr") { const fs = require("node:fs"); const value = fs.existsSync(${JSON.stringify(marker)}) ? ${JSON.stringify(changed)} : ${JSON.stringify(first)}; fs.writeFileSync(${JSON.stringify(marker)}, "loaded"); process.stdout.write(JSON.stringify(value)); } else process.stdout.write("[]"); `; await withFakeGh(program, async () => { await expect( openGitHubReview( root, "7", async () => true, new AbortController().signal, ), ).rejects.toThrow(/metadata changed after confirmation/); expect(await git(root, "branch", "--show-current")).toBe("main"); }); }); it("rejects an existing target branch changed during confirmation", async () => { const { root, base, head } = await repository(); await withFakeGh(fakeGh(metadata(base, head)), async () => { await expect( openGitHubReview( root, "7", async () => { await git(root, "branch", "-f", "feature", base); return true; }, new AbortController().signal, ), ).rejects.toThrow(/target branch feature changed/); expect(await git(root, "branch", "--show-current")).toBe("main"); }); }); it("rejects a target branch created during confirmation", async () => { const { root, base, head } = await repository(); await git(root, "branch", "-D", "feature"); const fork = metadata(base, head, { number: 8, url: "https://github.com/acme/project/pull/8", headRefName: "fork/work", headRepository: { name: "project", nameWithOwner: "contributor/project", }, headRepositoryOwner: { login: "contributor" }, }); await withFakeGh(fakeGh(fork), async () => { await expect( openGitHubReview( root, "8", async () => { await git(root, "branch", "strata/pr-8-fork-work", base); return true; }, new AbortController().signal, ), ).rejects.toThrow(/target branch strata\/pr-8-fork-work changed/); expect(await git(root, "branch", "--show-current")).toBe("main"); }); }); it("reuses the current branch for bare fork PR resolution without confirmation", async () => { const { root, base, head } = await repository(); await git(root, "checkout", "--quiet", "feature"); const fork = metadata(base, head, { headRepository: { name: "project", nameWithOwner: "contributor/project", }, headRepositoryOwner: { login: "contributor" }, }); await withFakeGh(fakeGh(fork), async () => { const confirm = vi.fn(async () => true); const review = await openGitHubReview( root, undefined, confirm, new AbortController().signal, ); expect(review.checkoutBranch).toBe("feature"); expect(confirm).not.toHaveBeenCalled(); expect(await git(root, "branch", "--show-current")).toBe("feature"); }); }); it("rejects an unpushed current branch for bare PR resolution", async () => { const { root, base, head } = await repository(); await git(root, "checkout", "--quiet", "feature"); await writeFile(path.join(root, "local.ts"), "export {};\n"); await git(root, "add", "local.ts"); await git(root, "commit", "--quiet", "-m", "unpushed"); await withFakeGh(fakeGh(metadata(base, head)), async () => { await expect( openGitHubReview( root, undefined, undefined, new AbortController().signal, ), ).rejects.toThrow(/unpushed commits/); expect(await git(root, "branch", "--show-current")).toBe("feature"); }); }); it("validates an explicit requested PR against the returned identity", async () => { const { root, base, head } = await repository(); await withFakeGh(fakeGh(metadata(base, head)), async () => { await expect( openGitHubReview( root, "8", async () => true, new AbortController().signal, ), ).rejects.toThrow(/different PR than the one requested/); expect(await git(root, "branch", "--show-current")).toBe("main"); }); }); it("validates an explicit requested PR URL against the returned identity", async () => { const { root, base, head } = await repository(); await withFakeGh(fakeGh(metadata(base, head)), async () => { await expect( openGitHubReview( root, "https://github.com/acme/project/pull/8", async () => true, new AbortController().signal, ), ).rejects.toThrow(/different PR than the one requested/); expect(await git(root, "branch", "--show-current")).toBe("main"); }); }); it("keeps a currently mapped inline comment current when authored on an older commit", async () => { const { root, base, head } = await repository(); const pages = { "repos/acme/project/pulls/7/comments": [ { id: 21, html_url: "https://github.com/acme/project/pull/7#discussion_r21", user: { login: "reviewer" }, body: "Still mapped", path: "change.ts", side: "RIGHT", line: 1, original_commit_id: base, position: 1, }, ], }; await withFakeGh(fakeGh(metadata(base, head), pages), async () => { const review = await openGitHubReview( root, "7", async () => true, new AbortController().signal, ); expect(review.comments[0]).toMatchObject({ commit: base, line: 1, side: "new", outdated: false, }); }); }); it("rechecks the local branch after the is-current network call", async () => { const { root, base, head } = await repository(); let review!: Awaited>; await withFakeGh(fakeGh(metadata(base, head)), async () => { review = await openGitHubReview( root, "7", async () => true, new AbortController().signal, ); }); await git(root, "branch", "same-sha", head); const program = ` if (args[0] === "pr") { require("node:child_process").execFileSync("git", ["checkout", "--quiet", "same-sha"]); process.stdout.write(${JSON.stringify(JSON.stringify(metadata(base, head)))}); } else process.stdout.write("[]"); `; const sentinel = await repository(); try { vi.stubEnv("GIT_DIR", path.join(sentinel.root, ".git")); vi.stubEnv("GIT_WORK_TREE", sentinel.root); vi.stubEnv("GIT_INDEX_FILE", path.join(sentinel.root, ".git", "index")); await withFakeGh(program, async () => { expect( await isGitHubReviewCurrent( root, review, new AbortController().signal, ), ).toBe(false); }); } finally { vi.unstubAllEnvs(); } expect(await git(root, "branch", "--show-current")).toBe("same-sha"); expect(await git(sentinel.root, "branch", "--show-current")).toBe("main"); expect(await git(sentinel.root, "rev-parse", "HEAD")).toBe(sentinel.base); }); it("rechecks a clean checkout after all refresh network calls", async () => { const { root, base, head } = await repository(); let review!: Awaited>; await withFakeGh(fakeGh(metadata(base, head)), async () => { review = await openGitHubReview( root, "7", async () => true, new AbortController().signal, ); }); const dirtyPath = path.join(root, "network-race.txt"); const program = ` if (args[0] === "pr") process.stdout.write(${JSON.stringify(JSON.stringify(metadata(base, head)))}); else { if (args.at(-1).startsWith("repos/acme/project/pulls/7/comments")) require("node:fs").writeFileSync(${JSON.stringify(dirtyPath)}, "changed"); process.stdout.write("[]"); } `; await withFakeGh(program, async () => { await expect( refreshGitHubReview(root, review, new AbortController().signal), ).rejects.toThrow(/checkout branch, commit, or working tree changed/); }); }); it("honors cancellation before gh acquisition", async () => { const controller = new AbortController(); controller.abort(new Error("cancelled by test")); await expect( openGitHubReview("unused", "7", undefined, controller.signal), ).rejects.toThrow("cancelled by test"); }); it("honors cancellation during gh acquisition", async () => { const active = await repository(); const marker = path.join(active.root, ".gh-started"); const blockingGh = ` if (args[0] === "pr" && args[1] === "view") { require("node:fs").writeFileSync(${JSON.stringify(`${marker}.pending`)}, String(process.pid)); require("node:fs").renameSync(${JSON.stringify(`${marker}.pending`)}, ${JSON.stringify(marker)}); Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10000); } else process.exitCode = 92; `; await withFakeGh(blockingGh, async () => { await cancelAfterMarker(marker, "cancelled in flight", (signal) => openGitHubReview(active.root, "7", async () => true, signal), ); expect(await git(active.root, "branch", "--show-current")).toBe("main"); }); }); });