import { appendFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { aroundEach, describe, expect, it, vi } from "vitest"; import { assertGitHubCheckout } from "../github.js"; import { collectGitHubSnapshot, formatFeedback } from "../implementation.js"; import type { Feedback, ForgeReview, Snapshot } from "../types.js"; import { git, nativeOperation, nativeTest, openFixture, testSignal, withFakeGh, } from "./fixture.js"; let fixture: Awaited> | undefined; aroundEach(async (run, context) => { try { await nativeTest(run, context); } finally { fixture = undefined; vi.restoreAllMocks(); } }); async function connect(url: string) { const response = await fetch(url, { redirect: "manual", signal: testSignal(), }); const cookie = response.headers.get("set-cookie")?.split(";")[0] ?? ""; const origin = new URL(url).origin; let version = await ( await fetch(`${origin}/api/state`, { headers: { Cookie: cookie }, signal: testSignal(), }) ).json(); return (route: string, body?: unknown) => nativeOperation(async () => { const response = await fetch(`${origin}/api/${route}`, { signal: testSignal(), method: body === undefined ? "GET" : "POST", headers: { Cookie: cookie, Origin: origin, "Content-Type": "application/json", "If-Match": String(version.revision), "X-Strata-Snapshot": version.snapshotId, }, body: body === undefined ? undefined : JSON.stringify(body), }); const payload = await response.clone().json(); if (payload.revision !== undefined) version = payload; return response; }); } function ghProgram(metadata: Record): string { return ` if (args[0] === "pr") process.stdout.write(${JSON.stringify(JSON.stringify(metadata))}); else process.stdout.write("[]"); `; } async function excludeTestHome(root: string): Promise { await appendFile(path.join(root, ".git", "info", "exclude"), ".test-home/\n"); } describe("forge feedback handoff", () => { it("carries exact snapshot, checkout, PR, anchor, and source discussion identities without treating imports as findings", () => { const snapshot: Snapshot = { id: "snapshot-1", repoRoot: "/work/project", source: { kind: "base", ref: "b".repeat(40) }, base: "a".repeat(40), head: "c".repeat(40), hunks: [ { id: "h-one", path: "src/change.ts", header: "@@ -1 +1 @@", lines: [{ kind: "add", text: "next", newLine: 1 }], }, ], skipped: [], }; const forge: ForgeReview = { provider: "github", providerLabel: "GitHub", repository: "acme/project", number: 7, url: "https://github.com/acme/project/pull/7", title: "Change", body: "Description", author: "author", baseRef: "main", baseSha: "b".repeat(40), headRef: "feature", headSha: "c".repeat(40), checkoutBranch: "strata/pr-7-feature", comments: [ { id: "inline:91", url: "https://github.com/acme/project/pull/7#discussion_r91", author: "reviewer", body: "Imported prose must not become a finding", kind: "inline", path: "src/change.ts", side: "new", line: 1, commit: "c".repeat(40), outdated: false, }, ], }; const feedback: Feedback = { snapshotId: snapshot.id, reviewed: ["h-one"], findings: [ { id: "finding-1", hunkId: "h-one", side: "new", line: 1, severity: "major", text: "Human finding", }, ], notes: "Human note", }; const result = formatFeedback(snapshot, feedback, forge); expect(result).toContain("GitHub acme/project PR #7"); expect(result).toContain("Checkout branch: strata/pr-7-feature"); expect(result).toContain(`PR base: main ${"b".repeat(40)}`); expect(result).toContain(`PR head: feature ${"c".repeat(40)}`); expect(result).toContain("inline:91 [inline] src/change.ts:1 (new)"); expect(result).not.toContain("Imported prose must not become a finding"); expect(result).toContain( "[major] src/change.ts:1 (new, h-one)\nHuman finding", ); expect(result).toContain("Ordinary local inspection and edits may proceed"); expect(result).toContain("requires separate user approval"); expect(result).toContain("is not approval to publish it"); }); }); describe.sequential("GitHub snapshot ownership", () => { it.each([ { scope: "PR number", requests: [ { host: "github.com", number: 7 }, { host: "github.com", number: 8 }, ], }, { scope: "forge host", requests: [ { host: "github.com", number: 8 }, { host: "github.example", number: 8 }, ], }, ])("isolates saved plans and drafts by $scope", async ({ requests }) => { fixture = await openFixture(); await excludeTestHome(fixture.root); await git(fixture.root, "add", "contract.ts", "consumer.ts"); await git(fixture.root, "commit", "--quiet", "-m", "feature"); const branch = await git(fixture.root, "branch", "--show-current"); const head = await git(fixture.root, "rev-parse", "HEAD"); const base = await git(fixture.root, "rev-parse", "HEAD^"); await git( fixture.root, "remote", "add", "origin", "https://github.com/acme/project.git", ); await git( fixture.root, "remote", "add", "enterprise", "https://github.example/acme/project.git", ); const current = fixture; for (const [index, request] of requests.entries()) { const url = `https://${request.host}/acme/project/pull/${request.number}`; await withFakeGh( ghProgram({ number: request.number, url, title: "Scoped review", body: "", author: { login: "author" }, baseRefName: "main", baseRefOid: base, headRefName: branch, headRefOid: head, headRepository: { name: "project", nameWithOwner: "acme/project" }, headRepositoryOwner: { login: "acme" }, }), async () => { await current.run(`--pr ${url}`); const api = await connect(current.url); const state = await (await api("state")).json(); expect(state.review.forge.url).toBe(url); expect(state.review.draft).toEqual({ reviewed: [], findings: [], notes: "", }); expect(current.calls).toBe(index + 1); expect( ( await api("draft", { reviewed: [state.review.snapshot.hunks[0].id], findings: [], notes: `Notes only for ${url}`, }) ).status, ).toBe(200); await current.run("cancel"); }, ); } }); it("detects a same-commit branch switch across snapshot collection", async () => { fixture = await openFixture(); await excludeTestHome(fixture.root); await git(fixture.root, "add", "contract.ts", "consumer.ts"); await git(fixture.root, "commit", "--quiet", "-m", "feature"); expect(await git(fixture.root, "status", "--porcelain")).toBe(""); const branch = await git(fixture.root, "branch", "--show-current"); const head = await git(fixture.root, "rev-parse", "HEAD"); const forge: ForgeReview = { provider: "github", providerLabel: "GitHub", repository: "acme/project", number: 7, url: "https://github.com/acme/project/pull/7", title: "Pinned capture", body: "", author: "author", baseRef: branch, baseSha: head, headRef: branch, headSha: head, checkoutBranch: branch, comments: [], }; let assertions = 0; const github = { assertGitHubCheckout: async ( cwd: string, review: ForgeReview, signal: AbortSignal, ) => { assertions++; await assertGitHubCheckout(cwd, review, signal); if (assertions === 1) { await git(cwd, "branch", "same-sha", head); await git(cwd, "checkout", "--quiet", "same-sha"); } }, }; await expect( collectGitHubSnapshot( fixture.root, { kind: "base", ref: head }, forge, github, testSignal(), ), ).rejects.toThrow(/checkout branch, commit, or working tree changed/); expect(assertions).toBe(2); }); it("rejects a captured HEAD that differs from the pinned PR head", async () => { fixture = await openFixture(); await excludeTestHome(fixture.root); await git(fixture.root, "add", "contract.ts", "consumer.ts"); await git(fixture.root, "commit", "--quiet", "-m", "feature"); const branch = await git(fixture.root, "branch", "--show-current"); const head = await git(fixture.root, "rev-parse", "HEAD"); const forge: ForgeReview = { provider: "github", providerLabel: "GitHub", repository: "acme/project", number: 7, url: "https://github.com/acme/project/pull/7", title: "Pinned capture", body: "", author: "author", baseRef: branch, baseSha: head, headRef: branch, headSha: "f".repeat(40), checkoutBranch: branch, comments: [], }; const assertCheckout = vi.fn(async () => {}); await expect( collectGitHubSnapshot( fixture.root, { kind: "base", ref: head }, forge, { assertGitHubCheckout: assertCheckout }, testSignal(), ), ).rejects.toThrow( /captured Git HEAD does not match the pinned GitHub PR head/, ); expect(assertCheckout).toHaveBeenCalledOnce(); }); it("keeps the active forge snapshot after a failed staged refresh", async () => { fixture = await openFixture(); await excludeTestHome(fixture.root); const baseBranch = await git(fixture.root, "branch", "--show-current"); const base = await git(fixture.root, "rev-parse", "HEAD"); await git(fixture.root, "checkout", "--quiet", "-b", "feature"); await git(fixture.root, "add", "contract.ts", "consumer.ts"); await git(fixture.root, "commit", "--quiet", "-m", "feature"); expect(await git(fixture.root, "status", "--porcelain")).toBe(""); const head = await git(fixture.root, "rev-parse", "HEAD"); await git( fixture.root, "checkout", "--quiet", "-b", "alternate-base", base, ); await writeFile(path.join(fixture.root, "alternate.ts"), "export {};\n"); await git(fixture.root, "add", "alternate.ts"); await git(fixture.root, "commit", "--quiet", "-m", "alternate base"); const alternateBase = await git(fixture.root, "rev-parse", "HEAD"); await git(fixture.root, "checkout", "--quiet", "feature"); await git( fixture.root, "remote", "add", "origin", "https://github.com/acme/project.git", ); const metadata = { number: 7, url: "https://github.com/acme/project/pull/7", title: "Stable review", body: "", author: { login: "author" }, baseRefName: baseBranch, baseRefOid: base, headRefName: "feature", headRefOid: head, headRepository: { name: "project", nameWithOwner: "acme/project" }, headRepositoryOwner: { login: "acme" }, }; await withFakeGh(ghProgram(metadata), async () => { await fixture?.run("--pr"); }); const api = await connect(fixture.url); const initial = await (await api("state")).json(); const changedMetadata = { ...metadata, baseRefName: "alternate-base", baseRefOid: alternateBase, }; vi.spyOn(fixture.ctx.modelRegistry, "complete").mockRejectedValueOnce( new Error("refresh planning failed"), ); await withFakeGh(ghProgram(changedMetadata), async () => { expect((await api("refresh", { confirm: true })).status).toBe(500); }); const afterFailure = await (await api("state")).json(); expect(afterFailure.review).toEqual(initial.review); const send = vi .spyOn(fixture.t.session, "prompt") .mockResolvedValue(undefined); await withFakeGh(ghProgram(metadata), async () => { expect( ( await api("submit", { draft: { reviewed: [], findings: [], notes: "Keep the original refresh state.", }, }) ).status, ).toBe(200); }); await vi.waitFor(() => expect(send).toHaveBeenCalled()); expect(send.mock.calls[0]?.[0]).toContain(`PR base: ${baseBranch} ${base}`); expect(send.mock.calls[0]?.[0]).not.toContain(alternateBase); }); });