import { expect, test } from "@playwright/test"; import { startReviewServer } from "../server.js"; import type { Feedback, ForgeReview, Review, ReviewServer } from "../types.js"; const mergeBaseSha = "0123456789abcdef0123456789abcdef01234567"; const baseTipSha = "1111111111111111111111111111111111111111"; const headSha = "2222222222222222222222222222222222222222"; const ancestorSha = "4444444444444444444444444444444444444444"; function makeForge(): ForgeReview { return { provider: "github", providerLabel: "GitHub", repository: "acme/strata", number: 42, url: "https://github.com/acme/strata/pull/42", title: "Reject anonymous access at the request boundary", body: "Authentication now fails closed before a request reaches the account service.\n\n", author: "octo-reviewer", baseRef: "main", baseSha: baseTipSha, headRef: "auth-boundary", headSha, checkoutBranch: "strata/pr-42-auth-boundary", comments: [ { id: "discussion-hostile", url: "javascript:alert('discussion')", author: "maintainer", body: "General discussion: ", kind: "discussion", outdated: false, }, { id: "review-summary", url: "http://github.example/review/7", author: "security-reviewer", body: "Review summary: keep the rejection before account lookup.", kind: "review", outdated: false, }, { id: "inline-current-new", url: "https://github.com/acme/strata/pull/42#discussion_r100", author: "security-reviewer", body: "This exception keeps missing credentials out of the service layer.", kind: "inline", path: "src/authenticate.ts", side: "new", line: 14, commit: headSha, outdated: false, }, { id: "inline-current-old", url: "https://github.com/acme/strata/pull/42#discussion_r101", author: "maintainer", body: "The guest fallback was the original privilege leak.", kind: "inline", path: "src/authenticate.ts", side: "old", line: 14, commit: headSha, outdated: false, replyTo: "discussion_r88", }, { id: "inline-outdated-missing", url: "https://github.com/acme/strata/pull/42#discussion_r102", author: "former-reviewer", body: "This referred to a line removed by the latest push.", kind: "inline", path: "src/removed.ts", side: "new", line: 900, commit: "3333333333333333333333333333333333333333", outdated: true, }, { id: "inline-ancestor-current", url: "https://github.com/acme/strata/pull/42#discussion_r103", author: "former-reviewer", body: "This was authored earlier and remains mapped to the current line.", kind: "inline", path: "src/authenticate.ts", side: "new", line: 14, commit: ancestorSha, outdated: false, }, { id: "inline-outdated-current-line", url: "https://github.com/acme/strata/pull/42#discussion_r104", author: "former-reviewer", body: "The adapter marks this otherwise valid coordinate as outdated.", kind: "inline", path: "src/authenticate.ts", side: "new", line: 14, commit: ancestorSha, outdated: true, }, { id: "inline-missing-line", url: "file:///tmp/not-a-source-link", author: "maintainer", body: "The head is current, but this anchor is absent from the captured diff.", kind: "inline", path: "src/authenticate.ts", side: "new", line: 999, commit: headSha, outdated: false, }, ], }; } function makeReview(forge: ForgeReview | null = makeForge()): Review { return { snapshot: { id: "same-code-snapshot", repoRoot: "/safe/forge-fixture", source: { kind: "base", ref: baseTipSha }, base: mergeBaseSha, head: headSha, hunks: [ { id: "auth-boundary", path: "src/authenticate.ts", header: "@@ -11,8 +11,9 @@ export async function authenticate", lines: [ { kind: "context", text: "export async function authenticate(request: Request) {", oldLine: 11, newLine: 11, }, { kind: "context", text: ' const token = request.headers.get("authorization");', oldLine: 12, newLine: 12, }, { kind: "context", text: "", oldLine: 13, newLine: 13 }, { kind: "delete", text: " if (!token) return { role: 'guest' };", oldLine: 14, }, { kind: "add", text: " if (!token) throw new AuthenticationError();", newLine: 14, }, { kind: "add", text: "", newLine: 15, }, { kind: "context", text: " const account = await accounts.findByToken(token);", oldLine: 15, newLine: 16, }, { kind: "context", text: " return { role: account.role, accountId: account.id };", oldLine: 16, newLine: 17, }, { kind: "context", text: "}", oldLine: 17, newLine: 18 }, ], }, { id: "auth-test", path: "test/authenticate.test.ts", header: "@@ -22,2 +22,7 @@ test missing credentials", lines: [ { kind: "context", text: 'test("rejects requests without credentials", async () => {', oldLine: 22, newLine: 22, }, { kind: "add", text: " await expect(authenticate(requestWithoutToken))", newLine: 23, }, { kind: "add", text: " .rejects.toBeInstanceOf(AuthenticationError);", newLine: 24, }, { kind: "add", text: " expect(accounts.findByToken).not.toHaveBeenCalled();", newLine: 25, }, { kind: "context", text: "});", oldLine: 23, newLine: 26 }, ], }, ], skipped: [], }, plan: { summary: "Reject missing credentials before account lookup and prove the boundary.", cohorts: [ { title: "Authentication", layers: [ { id: "request-boundary", title: "Request boundary", summary: "Review the rejection path and its regression test together.", hunks: [ { id: "auth-boundary", summary: "Missing credentials now fail closed.", }, { id: "auth-test", summary: "The service lookup remains untouched after rejection.", }, ], }, ], }, ], }, draft: { reviewed: [], findings: [], notes: "" }, ...(forge ? { forge } : {}), }; } let server: ReviewServer | undefined; test.afterEach(() => { server?.close(); server = undefined; }); test("renders current forge anchors beside code and keeps external discussion separate", async ({ page, }, testInfo) => { const submitted: Feedback[] = []; const externalRequests: string[] = []; page.on("request", (request) => { if (!request.url().startsWith("http://127.0.0.1:")) { externalRequests.push(request.url()); } }); server = await startReviewServer({ review: makeReview(), isCurrent: async () => true, refresh: async () => makeReview(), ask: async () => "answer", save: () => {}, submit: async (feedback) => { submitted.push(structuredClone(feedback)); }, }); await page.setViewportSize({ width: 1440, height: 900 }); await page.emulateMedia({ colorScheme: "light" }); await page.goto(server.url); await expect(page.locator("#forge-region")).toBeVisible(); await expect(page.locator("#forge-source")).toHaveText("GitHub"); await expect(page.locator("#forge-source svg")).toHaveCount(1); await expect(page.locator("#forge-heading")).toHaveText( "Reject anonymous access at the request boundary", ); await expect(page.locator("#forge-refs")).toContainText("Base tip"); await expect(page.locator("#forge-refs")).toContainText(baseTipSha); await expect(page.locator("#forge-refs")).toContainText("Merge base"); await expect(page.locator("#forge-refs")).toContainText(mergeBaseSha); await expect(page.locator("#forge-refs")).toContainText(headSha); await expect(page.locator("#forge-checkout")).toContainText( "Snapshot is bound to the exact PR base tip and head commit", ); await expect(page.locator("#forge-link")).toHaveAttribute( "href", "https://github.com/acme/strata/pull/42", ); await expect( page.locator( '.imported-comments [data-forge-comment-id="inline-current-new"]', ), ).toContainText("src/authenticate.ts · new:14"); await expect( page.locator( '.imported-comments [data-forge-comment-id="inline-current-old"]', ), ).toBeVisible(); await expect( page.locator( '.imported-comments [data-forge-comment-id="inline-ancestor-current"]', ), ).toContainText("authored earlier"); await expect( page.locator( '.imported-comments [data-forge-comment-id="inline-outdated-current-line"]', ), ).toHaveCount(0); await expect( page.locator( '.imported-comments [data-forge-comment-id="inline-outdated-missing"]', ), ).toHaveCount(0); await page.keyboard.press("g"); await page.keyboard.press("d"); await expect(page.getByRole("tab", { name: "Details" })).toBeFocused(); await page.keyboard.press("Tab"); await expect(page.locator("#forge-link")).toBeFocused(); await page.locator("#forge-discussions").evaluate((element) => { (element as HTMLDetailsElement).open = true; }); await expect( page.locator( '[data-forge-comment-id="inline-outdated-missing"] .forge-comment-state', ), ).toHaveText("Outdated · Unmapped"); await expect( page.locator( '#forge-comments [data-forge-comment-id="inline-ancestor-current"] .forge-comment-state', ), ).toHaveText("Current anchor"); await expect( page.locator( '[data-forge-comment-id="inline-outdated-current-line"] .forge-comment-state', ), ).toHaveText("Outdated"); await expect( page.locator( '[data-forge-comment-id="inline-missing-line"] .forge-comment-state', ), ).toHaveText("Unmapped"); await expect(page.locator("#forge-comments")).toContainText( "", ); await expect(page.locator("img")).toHaveCount(0); await expect( page.locator('[data-forge-comment-id="discussion-hostile"] a'), ).toHaveCount(0); await expect( page.locator('[data-forge-comment-id="review-summary"] a'), ).toHaveCount(0); await expect( page.locator('[data-forge-comment-id="inline-missing-line"] a'), ).toHaveCount(0); for (const href of await page .locator("a[href]") .evaluateAll((links) => links.map((link) => (link as HTMLAnchorElement).href), )) { expect(href.startsWith("https://")).toBe(true); } await page.locator("#forge-discussions").evaluate((element) => { (element as HTMLDetailsElement).open = false; }); await page.screenshot({ path: testInfo.outputPath("forge-review-desktop-light.png"), }); await page.emulateMedia({ colorScheme: "dark" }); await page.screenshot({ path: testInfo.outputPath("forge-review-desktop-dark.png"), }); await page.getByRole("tab", { name: "Feedback" }).click(); await expect(page.locator("#findings")).toHaveText("No line comments."); await page .getByRole("button", { name: "Comment on new line 14 in src/authenticate.ts", }) .click(); await page.locator("#comment-text").fill("Keep this rejection covered."); await page.getByRole("button", { name: "Add comment" }).click(); await expect(page.locator(".finding")).toHaveCount(1); await expect(page.locator(".finding")).toContainText( "Keep this rejection covered.", ); page.once("dialog", (dialog) => dialog.accept()); await page.getByRole("button", { name: "Send feedback to Pi" }).click(); await expect(page.locator("#status")).toHaveText("Feedback sent to Pi"); expect(submitted).toHaveLength(1); expect(submitted[0].findings).toHaveLength(1); expect(submitted[0].findings[0].text).toBe("Keep this rejection covered."); expect(JSON.stringify(submitted[0])).not.toContain("privilege leak"); expect(externalRequests).toEqual([]); }); for (const mismatch of [ { name: "non-base snapshot source", apply(review: Review) { review.snapshot.source = { kind: "working" }; }, }, { name: "wrong base tip ref", apply(review: Review) { review.snapshot.source.ref = "5555555555555555555555555555555555555555"; }, }, { name: "wrong head commit", apply(review: Review) { review.snapshot.head = "6666666666666666666666666666666666666666"; }, }, ]) { test(`does not map forge anchors for ${mismatch.name}`, async ({ page }) => { const review = makeReview(); mismatch.apply(review); server = await startReviewServer({ review, isCurrent: async () => true, refresh: async () => review, ask: async () => "answer", save: () => {}, submit: async () => {}, }); await page.goto(server.url); await expect(page.locator("#forge-checkout")).toContainText( "Snapshot is not bound to the exact PR base tip and head commit", ); await expect( page.locator( '.imported-comments [data-forge-comment-id="inline-current-new"]', ), ).toHaveCount(0); await expect( page.locator( '.imported-comments [data-forge-comment-id="inline-ancestor-current"]', ), ).toHaveCount(0); await expect( page.locator( '[data-forge-comment-id="inline-current-new"] .forge-comment-state', ), ).toHaveText("Outdated"); }); } test("clears forge source and commit details when refresh removes forge context", async ({ page, }) => { server = await startReviewServer({ review: makeReview(), isCurrent: async () => true, refresh: async () => makeReview(null), ask: async () => "answer", save: () => {}, submit: async () => {}, }); await page.goto(server.url); await expect(page.locator("#forge-refs .forge-ref")).toHaveCount(3); page.once("dialog", (dialog) => dialog.accept()); await page.getByRole("button", { name: "Refresh" }).click(); await expect(page.locator("#status")).toHaveText("Review refreshed"); await expect(page.locator("#forge-region")).toBeHidden(); await expect(page.locator("#forge-source")).toBeEmpty(); await expect(page.locator("#forge-link")).toBeEmpty(); await expect(page.locator("#forge-link")).not.toHaveAttribute("href", /./); await expect(page.locator("#forge-refs")).toBeEmpty(); await expect(page.locator("#forge-checkout")).toBeEmpty(); await expect(page.locator("#forge-comments")).toBeEmpty(); }); test("keeps PR details usable on mobile and falls back for another provider", async ({ page, }, testInfo) => { const forge = makeForge(); forge.provider = "smallforge"; forge.providerLabel = "Small Forge"; forge.url = "ftp://forge.example/acme/strata/reviews/42"; server = await startReviewServer({ review: makeReview(forge), isCurrent: async () => true, refresh: async () => makeReview(forge), ask: async () => "answer", save: () => {}, submit: async () => {}, }); await page.setViewportSize({ width: 390, height: 780 }); await page.emulateMedia({ colorScheme: "dark" }); await page.goto(server.url); await expect(page.locator("#detail-region")).toBeHidden(); await page.getByRole("button", { name: "Show review drawer" }).click(); await expect(page.locator("#forge-source")).toHaveText("Small Forge"); await expect(page.locator("#forge-source svg")).toHaveCount(0); await expect(page.locator("#forge-link")).toBeHidden(); await expect(page.locator("#forge-region")).toBeInViewport(); expect( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, ), ).toBe(true); await page.screenshot({ path: testInfo.outputPath("forge-review-mobile-dark.png"), }); }); test("preserves the ordinary browser review when forge metadata is absent", async ({ page, }) => { server = await startReviewServer({ review: makeReview(null), isCurrent: async () => true, refresh: async () => makeReview(null), ask: async () => "answer", save: () => {}, submit: async () => {}, }); await page.goto(server.url); await expect(page.locator("#forge-region")).toBeHidden(); await expect(page.locator(".imported-comments-row")).toHaveCount(0); await expect(page.locator("#diff-heading")).toHaveText("All changes"); await expect(page.locator(".hunk")).toHaveCount(2); await expect(page.locator("#status")).toHaveText("Review ready"); });