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 { 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(); afterEach(async () => { await Promise.all([...servers].map((server) => server.close())); servers.clear(); }); async function openSession(serverOptions: ServerOptions): Promise { const server = await startReviewServer(serverOptions); servers.add(server); return openExisting(server); } async function api( session: Session, path: string, body?: unknown, extraHeaders: Record = {}, ): Promise { 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 === undefined ? {} : { body: 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("serves authenticated opaque diagram renderer with isolated CSP", async () => { const session = await openSession(options()); const diagram = await api(session, "/diagram"); expect(diagram.status).toBe(200); expect(diagram.headers.get("x-frame-options")).toBe("SAMEORIGIN"); expect(diagram.headers.get("content-security-policy")).toContain( "sandbox allow-scripts", ); expect(diagram.headers.get("content-security-policy")).toContain( "connect-src 'none'", ); const body = await diagram.text(); expect(body).toContain("strata.diagram.ready"); expect(body).toContain("data-parent-origin"); expect(body).toContain("const validateDiagram ="); expect(body.match(/<\/script>/giu)).toHaveLength(2); const nonce = /script-src 'nonce-([^']+)'/u.exec( diagram.headers.get("content-security-policy") ?? "", )?.[1]; expect(nonce).toBeTruthy(); expect(body.split(`nonce="${nonce}"`)).toHaveLength(3); for (const directive of [ "img-src 'none'", "frame-src 'none'", "object-src 'none'", "form-action 'none'", "base-uri 'none'", "frame-ancestors 'self'", ]) { expect(diagram.headers.get("content-security-policy")).toContain( directive, ); } const main = await api(session, "/"); expect(main.headers.get("content-security-policy")).not.toContain( "unsafe-inline", ); expect(main.headers.get("x-frame-options")).toBe("DENY"); expect((await fetch(`${session.origin}/diagram`)).status).toBe(401); expect((await api(session, "/assets/mermaid.min.js")).status).toBe(404); }); 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 ranged = await api(session, "/api/draft", { ...draft, findings: [{ ...draft.findings[0], endLine: 2 }], }); expect(ranged.status).toBe(200); const missingRangeLine = await api(session, "/api/draft", { ...draft, findings: [{ ...draft.findings[0], endLine: 99 }], }); expect(missingRangeLine.status).toBe(400); 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) => `${request.question}`, ); 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: "Why?", revision: 1, snapshotId: "snapshot-1", threads: [ { layerId: "layer-a", messages: [ { question: "Why?", answer: "Why?", 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>() .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((resolve) => { started = resolve; }); let aborted = false; const ask = vi.fn( async (_request: AskRequest, signal: AbortSignal) => new Promise((_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(); const release = Promise.withResolvers(); 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>() .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: ["hunk-a"], findings: [], notes: "", }); }); it("preserves matching progress and notes across refresh without confirmation", async () => { const isCurrent = vi.fn(async () => false); const refresh = vi.fn(async () => makeReview("snapshot-2")); const draft: Draft = { reviewed: ["hunk-a"], findings: [], notes: "draft", }; const session = await openSession( options({ review: { ...makeReview(), draft }, isCurrent, refresh }), ); const refreshed = await api(session, "/api/refresh", { confirm: false }); expect(refreshed.status).toBe(200); const payload = await refreshed.json(); expect(payload.wasCurrent).toBe(false); expect(payload.review.snapshot.id).toBe("snapshot-2"); expect(payload.review.draft).toEqual(draft); expect(isCurrent).toHaveBeenCalledTimes(1); expect(refresh).toHaveBeenCalledWith(expect.any(AbortSignal)); }); it("carries reviewed IDs and ranges across a uniquely shifted matching hunk", async () => { const shifted = makeReview("snapshot-2"); shifted.snapshot.hunks[0] = { ...shifted.snapshot.hunks[0]!, id: "hunk-shifted", header: "@@ -11,2 +11,2 @@", lines: [ { kind: "delete", text: "old", oldLine: 11 }, { kind: "add", text: "new", newLine: 11 }, { kind: "context", text: "same", oldLine: 12, newLine: 12 }, ], }; const draft: Draft = { reviewed: ["hunk-a"], findings: [ { id: "finding-a", hunkId: "hunk-a", side: "new", line: 1, endLine: 2, severity: "major", text: "Range", }, ], notes: "Keep notes", }; const session = await openSession( options({ review: { ...makeReview(), draft }, refresh: async () => shifted, }), ); const refreshed = await api(session, "/api/refresh", { confirm: false }); expect(refreshed.status).toBe(200); expect((await refreshed.json()).review.draft).toEqual({ reviewed: ["hunk-shifted"], findings: [ { ...draft.findings[0], hunkId: "hunk-shifted", line: 11, endLine: 12 }, ], notes: "Keep notes", }); }); it("requires confirmation only for orphaned findings and retains notes", async () => { const changed = makeReview("snapshot-2"); changed.snapshot.hunks[0] = { ...changed.snapshot.hunks[0]!, lines: [{ kind: "add", text: "changed", newLine: 1 }], }; const draft: Draft = { reviewed: ["hunk-a"], findings: [ { id: "finding-a", hunkId: "hunk-a", side: "new", line: 1, severity: "major", text: "Keep this until confirmed", }, ], notes: "Notes survive", }; const session = await openSession( options({ review: { ...makeReview(), draft }, refresh: async () => changed, }), ); const refused = await api(session, "/api/refresh", { confirm: false }); expect(refused.status).toBe(409); expect((await refused.json()).code).toBe("confirm_required"); expect( (await (await api(session, "/api/state")).json()).review.draft, ).toEqual(draft); const accepted = await api(session, "/api/refresh", { confirm: true }); expect(accepted.status).toBe(200); expect((await accepted.json()).review.draft).toEqual({ reviewed: [], findings: [], notes: "Notes survive", }); }); 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("persists opaque recoveries without touching drafts and requires current state to remove", async () => { const saveRecoveries = vi.fn(); const save = vi.fn(); const session = await openSession(options({ save, saveRecoveries })); const stale = await openExisting(session.server); const appended = await api(session, "/api/recovery", { recovery: { snapshotId: "old-snapshot", text: "{not validated draft}" }, }); expect(appended.status).toBe(200); const recovery = (await appended.json()).recoveries[0]; expect(recovery).toMatchObject({ snapshotId: "old-snapshot", text: "{not validated draft}", }); expect(save).not.toHaveBeenCalled(); expect(saveRecoveries).toHaveBeenLastCalledWith([recovery]); expect( (await (await api(session, "/api/state")).json()).review.draft, ).toEqual(makeReview().draft); const duplicate = await api(session, "/api/recovery", { recovery: { snapshotId: "old-snapshot", text: "{not validated draft}" }, }); expect(duplicate.status).toBe(200); expect(saveRecoveries).toHaveBeenCalledTimes(1); expect( ( await api(session, "/api/draft", { ...makeReview().draft, notes: "Newer acknowledged draft", }) ).status, ).toBe(200); const refused = await api(stale, "/api/recovery", { remove: recovery.id }); expect(refused.status).toBe(409); expect((await refused.json()).code).toBe("stale_client"); const removed = await api(session, "/api/recovery", { remove: recovery.id, }); expect(removed.status).toBe(200); expect(saveRecoveries).toHaveBeenLastCalledWith([]); }); it("does not close when durable recovery persistence fails", async () => { const session = await openSession( options({ saveRecoveries: () => { throw new Error("disk unavailable"); }, }), ); const response = await api(session, "/api/close", { recovery: { snapshotId: "snapshot-1", text: "local work" }, }); expect(response.status).toBe(500); expect((await api(session, "/api/state")).status).toBe(200); }); it("stores close recovery while Ask is busy", async () => { const started = Promise.withResolvers(); const saveRecoveries = vi.fn(); const session = await openSession( options({ saveRecoveries, ask: async (_request, signal) => new Promise((_resolve, reject) => { started.resolve(); signal.addEventListener("abort", () => reject(new Error("aborted")), ); }), }), ); const asking = api(session, "/api/ask", { layerId: null, hunkId: null, question: "Wait", }).catch(() => undefined); await started.promise; const closed = await api(session, "/api/close", { recovery: { snapshotId: "snapshot-1", text: "pending work" }, }); expect(closed.status).toBe(200); expect(saveRecoveries).toHaveBeenCalledWith([ expect.objectContaining({ snapshotId: "snapshot-1", text: "pending work", }), ]); await asking; }); it("advances revision before failed feedback submission", async () => { const submit = vi.fn(async () => { throw new Error("feedback failed"); }); const session = await openSession(options({ submit })); const stale = await openExisting(session.server); const draft = { reviewed: [], findings: [], notes: "keep me" }; expect((await api(session, "/api/submit", { draft })).status).toBe(500); expect((await api(stale, "/api/draft", draft)).status).toBe(409); expect( (await (await api(session, "/api/state")).json()).review.draft, ).toEqual(draft); }); it("enforces recovery bounds without evicting existing work", async () => { const session = await openSession(options()); for (let index = 0; index < 32; index++) { expect( ( await api(session, "/api/recovery", { recovery: { snapshotId: `snapshot-${index}`, text: String(index) }, }) ).status, ).toBe(200); } const overflow = await api(session, "/api/recovery", { recovery: { snapshotId: "overflow", text: "overflow" }, }); expect(overflow.status).toBe(409); expect( (await (await api(session, "/api/state")).json()).recoveries, ).toHaveLength(32); }); it("preserves Ask threads when refresh context is unchanged", async () => { const session = await openSession( options({ refresh: async () => makeReview() }), ); await api(session, "/api/ask", { layerId: null, hunkId: null, question: "Before refresh", }); const refreshed = await api(session, "/api/refresh", { confirm: true }); expect((await refreshed.json()).threads).toHaveLength(1); }); 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, ): Promise { 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 { 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, }; }