Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/strata/__e2e__/forge.spec.ts

Raw
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<img src=x onerror=alert('description')>",
		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: <img src=x onerror=alert('comment')>",
				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(
		"<img src=x onerror=alert('comment')>",
	);
	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");
});