Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/strata/__tests__/server.test.ts

Raw
import { request as httpRequest } from "node:http";
import { afterEach, describe, expect, it, vi } from "vitest";
import { startReviewServer } from "../server.js";
import type {
	AskRequest,
	Draft,
	Review,
	ReviewServer,
	ServerOptions,
} from "../types.js";

function makeReview(id = "snapshot-1"): Review {
	return {
		snapshot: {
			id,
			repoRoot: "/safe/project",
			source: { kind: "working" },
			base: "base-id",
			head: "head-id",
			hunks: [
				{
					id: "hunk-a",
					path: "src/a.ts",
					header: "@@ -1,2 +1,2 @@",
					lines: [
						{ kind: "delete", text: "old", oldLine: 1 },
						{ kind: "add", text: "new", newLine: 1 },
						{ kind: "context", text: "same", oldLine: 2, newLine: 2 },
					],
				},
			],
			skipped: [{ path: "media.bin", reason: "binary" }],
		},
		plan: {
			summary: "One focused change",
			cohorts: [
				{
					title: "Core",
					layers: [
						{
							id: "layer-a",
							title: "Behavior",
							summary: "Changes behavior",
							hunks: [{ id: "hunk-a", summary: "Replace old with new" }],
							flow: ["Input", "Output"],
						},
					],
				},
			],
		},
		draft: { reviewed: [], findings: [], notes: "" },
	};
}

function options(overrides: Partial<ServerOptions> = {}): ServerOptions {
	return {
		review: makeReview(),
		isCurrent: vi.fn(async () => true),
		refresh: vi.fn(async () => makeReview("snapshot-2")),
		ask: vi.fn(async (request) => `Answer: ${request.question}`),
		save: vi.fn(),
		submit: vi.fn(async () => {}),
		...overrides,
	};
}

type Session = {
	server: ReviewServer;
	origin: string;
	cookie: string;
	revision: number;
	snapshotId: string;
};

const servers = new Set<ReviewServer>();

afterEach(async () => {
	await Promise.all([...servers].map((server) => server.close()));
	servers.clear();
});

async function openSession(serverOptions: ServerOptions): Promise<Session> {
	const server = await startReviewServer(serverOptions);
	servers.add(server);
	return openExisting(server);
}

async function api(
	session: Session,
	path: string,
	body?: unknown,
	extraHeaders: Record<string, string> = {},
): Promise<Response> {
	const response = await fetch(`${session.origin}${path}`, {
		method: body === undefined ? "GET" : "POST",
		headers: {
			Cookie: session.cookie,
			...(body === undefined
				? {}
				: {
						Origin: session.origin,
						"Content-Type": "application/json",
						...(path === "/api/close"
							? {}
							: {
									"If-Match": String(session.revision),
									"X-Strata-Snapshot": session.snapshotId,
								}),
					}),
			...extraHeaders,
		},
		body: body === undefined ? undefined : JSON.stringify(body),
	});
	const payload = await response
		.clone()
		.json()
		.catch(() => undefined);
	if (payload && Number.isSafeInteger(payload.revision)) {
		session.revision = payload.revision;
		session.snapshotId = payload.snapshotId;
	}
	return response;
}

describe("review server HTTP boundary", () => {
	it("exchanges its one-time URL token and authenticates pages and assets", async () => {
		const server = await startReviewServer(options());
		servers.add(server);
		const origin = new URL(server.url).origin;
		const unauthenticated = await fetch(origin);
		expect(unauthenticated.status).toBe(401);
		expect(unauthenticated.headers.get("cache-control")).toBe("no-store");

		const session = await openExisting(server);
		const page = await api(session, "/");
		expect(page.status).toBe(200);
		expect(page.headers.get("content-security-policy")).toContain(
			"script-src 'self'",
		);
		expect(await page.text()).toContain("STRATA");
		const script = await api(session, "/assets/app.js");
		expect(script.status).toBe(200);
		expect(script.headers.get("content-type")).toContain("text/javascript");
		const unauthenticatedAsset = await fetch(`${origin}/assets/style.css`);
		expect(unauthenticatedAsset.status).toBe(401);
	});

	it("rejects forged hosts, origins, and oversized requests", async () => {
		const session = await openSession(options());
		const forgedHost = await requestStatus(session, "/api/state", {
			Host: "localhost.invalid",
			Cookie: session.cookie,
		});
		expect(forgedHost).toBe(400);
		const forgedOrigin = await api(
			session,
			"/api/draft",
			{ reviewed: [], findings: [], notes: "" },
			{ Origin: "http://localhost.invalid" },
		);
		expect(forgedOrigin.status).toBe(403);
		const oversized = await fetch(`${session.origin}/api/draft`, {
			method: "POST",
			headers: {
				Cookie: session.cookie,
				Origin: session.origin,
				"Content-Type": "application/json",
			},
			body: JSON.stringify({ notes: "x".repeat(70_000) }),
		});
		expect(oversized.status).toBe(413);
	});

	it("serves state and saves only valid complete drafts", async () => {
		const save = vi.fn();
		const session = await openSession(options({ save }));
		const state = await api(session, "/api/state");
		expect(await state.json()).toMatchObject({
			revision: 0,
			snapshotId: "snapshot-1",
			review: { snapshot: { id: "snapshot-1" } },
			threads: [],
		});

		const draft: Draft = {
			reviewed: ["hunk-a"],
			findings: [
				{
					id: "finding-a",
					hunkId: "hunk-a",
					side: "new",
					line: 1,
					severity: "major",
					text: "Please verify this.",
				},
			],
			notes: "Review note",
		};
		const saved = await api(session, "/api/draft", draft);
		expect(saved.status).toBe(200);
		expect(await saved.json()).toMatchObject({
			revision: 1,
			snapshotId: "snapshot-1",
			draft,
		});
		expect(save).toHaveBeenCalledWith(draft);

		const invalid = await api(session, "/api/draft", {
			...draft,
			findings: [{ ...draft.findings[0], line: 99 }],
		});
		expect(invalid.status).toBe(400);
		expect(save).toHaveBeenCalledTimes(1);
		const extra = await api(session, "/api/draft", { ...draft, extra: true });
		expect(extra.status).toBe(400);
	});

	it("rejects missing or stale optimistic bindings without overwriting another tab", async () => {
		const save = vi.fn();
		const session = await openSession(options({ save }));
		const secondTab = await openExisting(session.server);
		const missing = await fetch(`${session.origin}/api/draft`, {
			method: "POST",
			headers: {
				Cookie: session.cookie,
				Origin: session.origin,
				"Content-Type": "application/json",
			},
			body: JSON.stringify({ reviewed: [], findings: [], notes: "missing" }),
		});
		expect(missing.status).toBe(409);
		expect((await missing.json()).code).toBe("stale_client");

		const first = await api(session, "/api/draft", {
			reviewed: [],
			findings: [],
			notes: "first tab",
		});
		expect(first.status).toBe(200);
		expect(await first.json()).toMatchObject({
			revision: 1,
			snapshotId: "snapshot-1",
		});
		const stale = await api(secondTab, "/api/draft", {
			reviewed: [],
			findings: [],
			notes: "second tab",
		});
		expect(stale.status).toBe(409);
		expect((await stale.json()).code).toBe("stale_client");
		expect(save).toHaveBeenCalledTimes(1);
	});

	it("asks only known layers and returns plain bounded answers", async () => {
		const ask = vi.fn(
			async (request: AskRequest) => `<b>${request.question}</b>`,
		);
		const session = await openSession(options({ ask }));
		const response = await api(session, "/api/ask", {
			layerId: "layer-a",
			hunkId: "hunk-a",
			question: "Why?",
		});
		expect(response.status).toBe(200);
		expect(await response.json()).toMatchObject({
			answer: "<b>Why?</b>",
			revision: 1,
			snapshotId: "snapshot-1",
			threads: [
				{
					layerId: "layer-a",
					messages: [
						{
							question: "Why?",
							answer: "<b>Why?</b>",
							hunkId: "hunk-a",
						},
					],
				},
			],
		});
		expect(ask).toHaveBeenCalledWith(
			{
				layerId: "layer-a",
				hunkId: "hunk-a",
				question: "Why?",
				history: [],
			},
			expect.any(AbortSignal),
		);
		const unknown = await api(session, "/api/ask", {
			layerId: "missing",
			hunkId: null,
			question: "Why?",
		});
		expect(unknown.status).toBe(400);
		const browserHistory = await api(session, "/api/ask", {
			layerId: "layer-a",
			hunkId: null,
			question: "Again?",
			history: [],
		});
		expect(browserHistory.status).toBe(400);
		const whitespace = await api(session, "/api/ask", {
			layerId: null,
			hunkId: null,
			question: " \n\t ",
		});
		expect(whitespace.status).toBe(400);
		const oversizedUtf8 = await api(session, "/api/ask", {
			layerId: null,
			hunkId: null,
			question: "é".repeat(3_000),
		});
		expect(oversizedUtf8.status).toBe(400);
	});

	it("owns separate layer and All changes histories and rejects invalid focus", async () => {
		const review = makeReview();
		review.snapshot.hunks.push({
			id: "hunk-b",
			path: "src/b.ts",
			header: "@@ -1 +1 @@",
			lines: [{ kind: "add", text: "b", newLine: 1 }],
		});
		review.plan.cohorts[0]?.layers.push({
			id: "layer-b",
			title: "Other",
			summary: "Other behavior",
			hunks: [{ id: "hunk-b", summary: "Adds b" }],
		});
		const requests: AskRequest[] = [];
		const session = await openSession(
			options({
				review,
				ask: async (request) => {
					requests.push(request);
					return `Answer ${requests.length}`;
				},
			}),
		);
		expect(
			(
				await api(session, "/api/ask", {
					layerId: "layer-a",
					hunkId: "hunk-b",
					question: "Wrong focus",
				})
			).status,
		).toBe(400);
		await api(session, "/api/ask", {
			layerId: null,
			hunkId: "hunk-b",
			question: "Whole review",
		});
		await api(session, "/api/ask", {
			layerId: "layer-a",
			hunkId: "hunk-a",
			question: "Layer question",
		});
		await api(session, "/api/ask", {
			layerId: "layer-a",
			hunkId: null,
			question: "Follow-up",
		});
		expect(requests[0]).toMatchObject({ layerId: null, history: [] });
		expect(requests[1]).toMatchObject({ layerId: "layer-a", history: [] });
		expect(requests[2]?.history).toEqual([
			{
				question: "Layer question",
				answer: "Answer 2",
				hunkId: "hunk-a",
			},
		]);
		const state = await (await api(session, "/api/state")).json();
		expect(state.threads).toHaveLength(2);
		expect(state.threads[0].layerId).toBeNull();
	});

	it("checks freshness before Ask and again before storing the answer", async () => {
		const ask = vi.fn(async () => "Answer");
		const staleBefore = await openSession(
			options({ isCurrent: vi.fn(async () => false), ask }),
		);
		const refused = await api(staleBefore, "/api/ask", {
			layerId: null,
			hunkId: null,
			question: "Question",
		});
		expect(refused.status).toBe(409);
		expect(ask).not.toHaveBeenCalled();

		const freshness = vi
			.fn<() => Promise<boolean>>()
			.mockResolvedValueOnce(true)
			.mockResolvedValueOnce(false);
		const staleAfter = await openSession(
			options({ isCurrent: freshness, ask }),
		);
		const discarded = await api(staleAfter, "/api/ask", {
			layerId: null,
			hunkId: null,
			question: "Question",
		});
		expect(discarded.status).toBe(409);
		expect(ask).toHaveBeenCalledOnce();
		const state = await (await api(staleAfter, "/api/state")).json();
		expect(state).toMatchObject({ revision: 0, threads: [] });
	});

	it("rejects Ask history growth at 128 completed exchanges without truncation", async () => {
		const session = await openSession(options({ ask: async () => "Answer" }));
		for (let index = 0; index < 128; index++) {
			const response = await api(session, "/api/ask", {
				layerId: null,
				hunkId: null,
				question: `Question ${index}`,
			});
			expect(response.status).toBe(200);
		}
		const refused = await api(session, "/api/ask", {
			layerId: "layer-a",
			hunkId: null,
			question: "One too many",
		});
		expect(refused.status).toBe(409);
		expect((await refused.json()).code).toBe("history_limit");
		const state = await (await api(session, "/api/state")).json();
		expect(state.revision).toBe(128);
		expect(state.threads[0].messages).toHaveLength(128);
	});

	it("busy-rejects overlapping callbacks and aborts outstanding work on close", async () => {
		let started!: () => void;
		const didStart = new Promise<void>((resolve) => {
			started = resolve;
		});
		let aborted = false;
		const ask = vi.fn(
			async (_request: AskRequest, signal: AbortSignal) =>
				new Promise<string>((_resolve, reject) => {
					started();
					signal.addEventListener("abort", () => {
						aborted = true;
						reject(new Error("aborted"));
					});
				}),
		);
		const session = await openSession(options({ ask }));
		const asking = api(session, "/api/ask", {
			layerId: "layer-a",
			hunkId: null,
			question: "Wait",
		}).catch(() => undefined);
		await didStart;
		const busy = await api(session, "/api/draft", {
			reviewed: [],
			findings: [],
			notes: "queued",
		});
		expect(busy.status).toBe(409);
		await session.server.close();
		await asking;
		expect(aborted).toBe(true);
	});

	it("awaits request cleanup before close settles", async () => {
		const started = Promise.withResolvers<void>();
		const release = Promise.withResolvers<string>();
		let signal: AbortSignal | undefined;
		let cleaned = false;
		const session = await openSession(
			options({
				ask: async (_request, requestSignal) => {
					signal = requestSignal;
					started.resolve();
					try {
						return await release.promise;
					} finally {
						cleaned = true;
					}
				},
			}),
		);
		const asking = api(session, "/api/ask", {
			layerId: "layer-a",
			hunkId: null,
			question: "Wait",
		}).catch(() => undefined);
		try {
			await started.promise;
			let settled = false;
			const closing = session.server.close();
			void closing.then(() => {
				settled = true;
			});
			expect(session.server.close()).toBe(closing);
			expect(signal?.aborted).toBe(true);
			await Promise.resolve();
			expect(settled).toBe(false);
			expect(cleaned).toBe(false);
			release.resolve("late answer");
			await closing;
			expect(cleaned).toBe(true);
		} finally {
			release.resolve("cleanup");
			await session.server.close();
			await asking;
		}
	});

	it("refuses stale submission, preserves its draft, then clears after success", async () => {
		const save = vi.fn();
		const submit = vi.fn(async () => {});
		const isCurrent = vi
			.fn<() => Promise<boolean>>()
			.mockResolvedValueOnce(false)
			.mockResolvedValueOnce(true);
		const session = await openSession(options({ isCurrent, save, submit }));
		const draft: Draft = {
			reviewed: ["hunk-a"],
			findings: [],
			notes: "Keep me",
		};
		const stale = await api(session, "/api/submit", { draft });
		expect(stale.status).toBe(409);
		expect((await stale.json()).code).toBe("stale");
		expect(submit).not.toHaveBeenCalled();
		const state = await api(session, "/api/state");
		expect((await state.json()).review.draft).toEqual(draft);

		const sent = await api(session, "/api/submit", { draft });
		expect(sent.status).toBe(200);
		expect(submit).toHaveBeenCalledWith({ ...draft, snapshotId: "snapshot-1" });
		expect(save).toHaveBeenLastCalledWith({
			reviewed: [],
			findings: [],
			notes: "",
		});
	});

	it("requires draft confirmation, checks freshness, and safely replaces review", async () => {
		const isCurrent = vi.fn(async () => false);
		const refresh = vi.fn(async () => makeReview("snapshot-2"));
		const session = await openSession(
			options({
				review: {
					...makeReview(),
					draft: { reviewed: [], findings: [], notes: "draft" },
				},
				isCurrent,
				refresh,
			}),
		);
		const refused = await api(session, "/api/refresh", { confirm: false });
		expect(refused.status).toBe(409);
		expect((await refused.json()).code).toBe("confirm_required");
		expect(isCurrent).toHaveBeenCalledTimes(1);
		expect(refresh).not.toHaveBeenCalled();

		const accepted = await api(session, "/api/refresh", { confirm: true });
		expect(accepted.status).toBe(200);
		const payload = await accepted.json();
		expect(payload.wasCurrent).toBe(false);
		expect(payload.review.snapshot.id).toBe("snapshot-2");
		expect(refresh).toHaveBeenCalledWith(expect.any(AbortSignal));
	});

	it("clears server-owned threads on refresh", async () => {
		const session = await openSession(options());
		await api(session, "/api/ask", {
			layerId: null,
			hunkId: "hunk-a",
			question: "Before refresh",
		});
		const refreshed = await api(session, "/api/refresh", { confirm: true });
		expect(refreshed.status).toBe(200);
		expect((await refreshed.json()).threads).toEqual([]);
		expect((await (await api(session, "/api/state")).json()).threads).toEqual(
			[],
		);
	});

	it("closes explicitly through the authenticated endpoint", async () => {
		const session = await openSession(options());
		const response = await api(session, "/api/close", {});
		expect(response.status).toBe(200);
		expect(await response.json()).toMatchObject({
			closed: true,
			revision: 0,
			snapshotId: "snapshot-1",
		});
	});
});

function requestStatus(
	session: Session,
	path: string,
	headers: Record<string, string>,
): Promise<number | undefined> {
	return new Promise((resolve, reject) => {
		const request = httpRequest(
			`${session.origin}${path}`,
			{ headers },
			(response) => {
				response.resume();
				response.on("end", () => resolve(response.statusCode));
			},
		);
		request.on("error", reject);
		request.end();
	});
}

async function openExisting(server: ReviewServer): Promise<Session> {
	const exchange = await fetch(server.url, { redirect: "manual" });
	const cookie = exchange.headers.get("set-cookie")?.split(";", 1)[0];
	if (!cookie) throw new Error("server did not set an authentication cookie");
	const origin = new URL(server.url).origin;
	const state = await fetch(`${origin}/api/state`, {
		headers: { Cookie: cookie },
	}).then((response) => response.json());
	return {
		server,
		origin,
		cookie,
		revision: state.revision,
		snapshotId: state.snapshotId,
	};
}