Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/strata/__e2e__/continuity.spec.ts

Raw
import { expect, test } from "@playwright/test";
import type { Draft, DraftRecovery, Review, ReviewServer } from "../types.js";

const { startReviewServer }: typeof import("../server.ts") = await import(
	new URL("../../../dist/strata/server.ts", import.meta.url).href
);

function review(id = "continuity"): Review {
	return {
		snapshot: {
			id,
			repoRoot: "/safe/continuity",
			source: { kind: "working" },
			base: "base",
			head: "head",
			hunks: [
				{
					id: "change",
					path: "src/change.ts",
					header: "@@ -1,2 +1,2 @@ change",
					lines: [
						{ kind: "add", text: "new", newLine: 1 },
						{ kind: "context", text: "same", oldLine: 2, newLine: 2 },
					],
				},
			],
			skipped: [],
		},
		plan: {
			summary: "Continuity",
			cohorts: [
				{
					title: "Change",
					layers: [
						{
							id: "layer",
							title: "Change",
							summary: "Explain the change",
							hunks: [{ id: "change", summary: "Two lines" }],
						},
					],
				},
			],
		},
		draft: { reviewed: [], findings: [], notes: "" },
	};
}

let server: ReviewServer | undefined;
test.afterEach(async () => {
	await server?.close();
	server = undefined;
});

async function state(page: import("@playwright/test").Page) {
	return page.evaluate(async () =>
		(await fetch("/api/state")).json(),
	) as Promise<{ review: Review }>;
}

const options = {
	isCurrent: async () => true,
	ask: async () => "The changed line introduces the new behavior.",
	submit: async () => {},
};

test("reload never overwrites another tab with an acknowledged but older cache", async ({
	page,
}) => {
	server = await startReviewServer({
		...options,
		review: review(),
		refresh: async () => review(),
		save: () => {},
	});
	await page.goto(server.url);
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#notes").fill("Older tab note");
	await expect(page.locator("#status")).toHaveText("Draft saved");
	const other = await page.context().newPage();
	await other.goto(server.url);
	await other.getByRole("tab", { name: "Feedback", exact: true }).click();
	await other.locator("#notes").fill("Authoritative newer tab note");
	await expect(other.locator("#status")).toHaveText("Draft saved");
	let writes = 0;
	page.on("request", (request) => {
		if (request.url().endsWith("/api/draft")) writes++;
	});
	await page.reload();
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#local-recovery summary").click();
	await expect(page.locator("#recovery-content")).toContainText(
		"Older tab note",
	);
	await expect(page.locator("#notes")).toHaveValue(
		"Authoritative newer tab note",
	);
	await page.waitForTimeout(650);
	expect(writes).toBe(0);
	expect((await state(page)).review.draft.notes).toBe(
		"Authoritative newer tab note",
	);
	await other.close();
});

test("older save acknowledgement cannot erase still-debounced notes from recovery", async ({
	page,
}) => {
	server = await startReviewServer({
		...options,
		review: review(),
		refresh: async () => review(),
		save: () => {},
	});
	let releaseResponse!: () => void;
	const responseHeld = new Promise<void>((resolve) => {
		releaseResponse = resolve;
	});
	let responseReady = false;
	await page.route("**/api/draft", async (route) => {
		const response = await route.fetch();
		responseReady = true;
		await responseHeld;
		await route.fulfill({ response });
	});
	await page.goto(server.url);
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#notes").fill("Acknowledged older note");
	await expect.poll(() => responseReady).toBe(true);
	await page.clock.install({ time: 0 });
	await page.clock.pauseAt(1000);
	await page.locator("#notes").fill("Still-debounced newer note");
	releaseResponse();
	await expect(page.locator("#status")).toHaveText("Draft saved");
	const cached = await page.evaluate(() => {
		const key = Object.keys(sessionStorage).find((key) =>
			key.includes("draft"),
		);
		return JSON.parse(sessionStorage.getItem(key!)!) as Draft;
	});
	expect(cached.notes).toBe("Still-debounced newer note");
	await page.reload();
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#local-recovery summary").click();
	await expect(page.locator("#recovery-content")).toContainText(
		"Still-debounced newer note",
	);
	expect((await state(page)).review.draft.notes).toBe(
		"Acknowledged older note",
	);
});

test("failed comment save keeps the range visible and Close preserves it", async ({
	page,
}) => {
	let recoveries: DraftRecovery[] = [];
	server = await startReviewServer({
		...options,
		review: review(),
		refresh: async () => review(),
		save: () => {},
		saveRecoveries: (saved) => {
			recoveries = structuredClone(saved);
		},
	});
	await page.route("**/api/draft", (route) =>
		route.fulfill({
			status: 500,
			contentType: "application/json",
			body: JSON.stringify({ error: "Comment save unavailable" }),
		}),
	);
	await page.goto(server.url);
	for (const line of [1, 2]) {
		await page
			.getByRole("button", {
				name: `Comment on new line ${line} in src/change.ts`,
				exact: true,
			})
			.click({ modifiers: ["Shift"] });
	}
	await page.locator(".keyboard-current").focus();
	await page.keyboard.press("Space");
	await page.keyboard.press("c");
	await expect(page.locator("#comment-anchor")).toHaveText(
		"src/change.ts · new:1–2",
	);
	await page.locator("#comment-text").fill("Keep the failed range comment");
	await page.getByRole("button", { name: "Add comment", exact: true }).click();
	await expect(page.locator("#status")).toContainText(
		"Comment save unavailable",
	);
	await expect(page.locator("#comment-dialog")).not.toBeVisible();
	await expect(page.locator("#findings .finding")).toHaveCount(1);
	await expect(page.locator("#findings")).toContainText(
		"Keep the failed range comment",
	);
	await page.locator("#notes").fill("Keep the pending note too");
	page.once("dialog", (dialog) => dialog.accept());
	await page.getByRole("button", { name: "Close", exact: true }).click();
	await expect(page.locator("#status")).toContainText("Review closed");
	expect(recoveries).toHaveLength(1);
	expect(JSON.parse(recoveries[0]!.text)).toMatchObject({
		notes: "Keep the pending note too",
		findings: [
			{
				hunkId: "change",
				side: "new",
				line: 1,
				endLine: 2,
				text: "Keep the failed range comment",
			},
		],
	});
});

test("failed autosave blocks destructive Refresh and Close persists recovery for another port", async ({
	page,
}) => {
	let refreshes = 0;
	let savedRecoveries: DraftRecovery[] = [];
	server = await startReviewServer({
		...options,
		review: review(),
		refresh: async () => {
			refreshes++;
			return review("refreshed");
		},
		save: () => {},
		saveRecoveries: (recoveries) => {
			savedRecoveries = structuredClone(recoveries);
		},
	});
	await page.route("**/api/draft", (route) =>
		route.fulfill({
			status: 500,
			contentType: "application/json",
			body: JSON.stringify({ error: "Autosave unavailable" }),
		}),
	);
	await page.goto(server.url);
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#notes").fill("Keep the failed-save note");
	await expect(page.locator("#status")).toContainText("Autosave unavailable");
	page.once("dialog", (dialog) => dialog.accept());
	await page.getByRole("button", { name: "Refresh", exact: true }).click();
	await expect(page.locator("#status")).toContainText("Autosave unavailable");
	await expect(page.locator("#notes")).toHaveValue("Keep the failed-save note");
	expect(refreshes).toBe(0);
	page.once("dialog", (dialog) => dialog.accept());
	await page.getByRole("button", { name: "Close", exact: true }).click();
	await expect(page.locator("#status")).toContainText("Review closed");
	expect(savedRecoveries).toHaveLength(1);
	expect(JSON.parse(savedRecoveries[0]!.text).notes).toBe(
		"Keep the failed-save note",
	);
	await server.close();
	await page.unroute("**/api/draft");
	server = await startReviewServer({
		...options,
		review: { ...review(), recoveries: savedRecoveries },
		refresh: async () => review(),
		save: () => {},
		saveRecoveries: () => {},
	});
	await page.goto(server.url);
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#local-recovery summary").click();
	await expect(page.locator("#recovery-content")).toContainText(
		"Keep the failed-save note",
	);
	page.once("dialog", (dialog) => dialog.accept());
	await page
		.getByRole("button", { name: "Merge local draft", exact: true })
		.click();
	await expect(page.locator("#status")).toHaveText("Local draft merged");
	expect((await state(page)).review.draft.notes).toBe(
		"Keep the failed-save note",
	);
});

test("Close bypasses a stuck draft request and ignores its late rejection", async ({
	page,
}) => {
	let held: import("@playwright/test").Route | undefined;
	let recoveries: DraftRecovery[] = [];
	server = await startReviewServer({
		...options,
		review: review(),
		refresh: async () => review(),
		save: () => {},
		saveRecoveries: (value) => {
			recoveries = value;
		},
	});
	await page.route("**/api/draft", (route) => {
		held = route;
	});
	await page.goto(server.url);
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#notes").fill("Pending forever without this backup");
	await expect.poll(() => Boolean(held)).toBe(true);
	page.once("dialog", (dialog) => dialog.accept());
	await page.getByRole("button", { name: "Close", exact: true }).click();
	await expect(page.locator("#status")).toContainText("Review closed");
	expect(JSON.parse(recoveries[0]!.text).notes).toBe(
		"Pending forever without this backup",
	);
	await held!.abort();
	await page.waitForTimeout(400);
	await expect(page.locator("#status")).toHaveText(
		"Review closed. You may close this tab.",
	);
	await expect(
		page.getByRole("button", { name: "Close", exact: true }),
	).toBeDisabled();
});

test("recovery merge cancels notes debounce rather than overwriting recovered notes", async ({
	page,
}) => {
	const recovered = { reviewed: [], findings: [], notes: "Recovered note" };
	const saved: Draft[] = [];
	server = await startReviewServer({
		...options,
		review: {
			...review(),
			recoveries: [
				{
					id: "recovery",
					snapshotId: "continuity",
					text: JSON.stringify(recovered),
				},
			],
		},
		refresh: async () => review(),
		save: (draft) => {
			saved.push(structuredClone(draft));
		},
		saveRecoveries: () => {},
	});
	await page.route("**/api/draft", async (route) => {
		await new Promise((resolve) => setTimeout(resolve, 600));
		await route.continue();
	});
	await page.goto(server.url);
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#local-recovery summary").click();
	await page.locator("#notes").fill("Current local note");
	page.once("dialog", (dialog) => dialog.accept());
	await page
		.getByRole("button", { name: "Merge local draft", exact: true })
		.click();
	await expect(page.locator("#status")).toHaveText("Local draft merged");
	await page.waitForTimeout(650);
	const draft = (await state(page)).review.draft;
	expect(draft.notes).toContain("Current local note");
	expect(draft.notes).toContain("Recovered note");
	expect(saved).toHaveLength(1);
});

test("a changed snapshot never restores a comment through an accidentally reused hunk ID", async ({
	page,
}) => {
	const previous = review();
	previous.draft.findings = [
		{
			id: "finding",
			hunkId: "change",
			side: "new",
			line: 1,
			endLine: 2,
			severity: "major",
			text: "Comment on the original block",
		},
	];
	previous.draft.notes = "Keep the note separately";
	const changed = review("changed");
	changed.snapshot.hunks[0]!.lines[0]!.text =
		"Different block at the old coordinates";
	server = await startReviewServer({
		...options,
		review: previous,
		refresh: async () => changed,
		save: () => {},
		saveRecoveries: () => {},
	});
	await page.goto(server.url);
	page.once("dialog", (dialog) => dialog.accept());
	await page.getByRole("button", { name: "Refresh", exact: true }).click();
	await expect(page.locator("#source-label")).toContainText("changed");
	await page.getByRole("tab", { name: "Feedback", exact: true }).click();
	await page.locator("#local-recovery summary").click();
	await expect(page.locator("#recovery-content")).toContainText(
		"Comment on the original block",
	);
	page.once("dialog", (dialog) => dialog.accept());
	await page
		.getByRole("button", { name: "Merge local draft", exact: true })
		.click();
	await expect(page.locator("#status")).toContainText("remain in recovery");
	const latest = await state(page);
	expect(latest.review.draft.findings).toEqual([]);
	expect(latest.review.draft.notes).toBe("Keep the note separately");
	expect(latest.review.recoveries).toHaveLength(1);
});

test("unchanged Refresh retains Ask history", async ({ page }) => {
	server = await startReviewServer({
		...options,
		review: review(),
		refresh: async () => review(),
		save: () => {},
	});
	await page.goto(server.url);
	await page.getByRole("tab", { name: "Ask", exact: true }).click();
	await page.locator("#question").fill("Why this change?");
	await page.getByRole("button", { name: "Send", exact: true }).click();
	await expect(page.locator("#status")).toHaveText("Answer received");
	page.once("dialog", (dialog) => dialog.accept());
	await page.getByRole("button", { name: "Refresh", exact: true }).click();
	await expect(page.locator("#status")).toHaveText("Review refreshed");
	await page.getByRole("tab", { name: "Ask", exact: true }).click();
	await expect(page.locator("#ask-panel")).toContainText(
		"The changed line introduces the new behavior.",
	);
});