import { describe, it, expect, beforeAll, afterAll } from "vitest"; import PocketBase from "pocketbase"; import { randomBytes } from "node:crypto"; import { APP_URL, DEMO, adminPb, requireServices } from "./helpers"; let admin: PocketBase; let projectId: string; let secondProjectId: string; let clientId: string; const createdLinkIds: string[] = []; const createdCardIds: string[] = []; const createdSubmissionIds: string[] = []; function makeToken(): string { return randomBytes(24).toString("base64url"); } async function newPhase9Link() { const token = makeToken(); const link = await admin.collection("intake_links").create({ project: "", client: clientId, label: "phase9 cap test", token, active: true, expires_at: "", max_submissions_per_day: 2, submission_count: 0, submission_count_today: 0, submission_count_today_date: "", last_submission_at: "", notes: "", }); createdLinkIds.push(link.id); return link; } beforeAll(async () => { await requireServices(); admin = await adminPb(); const project = await admin .collection("projects") .getFirstListItem(`slug = "${DEMO.projectSlug}"`); projectId = project.id; clientId = project.client; const existing = await admin .collection("projects") .getFullList({ filter: `client = "${clientId}" && name = "Phase9 cap test second"`, }); if (existing.length > 0) { secondProjectId = existing[0].id; } else { const second = await admin.collection("projects").create({ client: clientId, name: "Phase9 cap test second", slug: `phase9-cap-test-second-${Date.now()}`, status: "active", }); secondProjectId = second.id; } }); afterAll(async () => { for (const id of createdCardIds) { await admin.collection("cards").delete(id).catch(() => {}); } for (const id of createdLinkIds) { await admin.collection("intake_links").delete(id).catch(() => {}); } for (const id of createdSubmissionIds) { await admin .collection("intake_link_submissions") .delete(id) .catch(() => {}); } await admin .collection("projects") .update(projectId, { show_in_intake: false }) .catch(() => {}); await admin .collection("projects") .update(secondProjectId, { show_in_intake: false }) .catch(() => {}); }); describe("intake_link_submissions schema lands and tracks per-(link, project)", () => { it("creates a row, reads it back with the expected fields", async () => { const link = await newPhase9Link(); const today = new Date().toISOString().slice(0, 10); // Direct PocketBase write — exercises the schema only. const row = await admin.collection("intake_link_submissions").create({ link: link.id, project: projectId, date: today, count: 1, }); createdSubmissionIds.push(row.id); expect(row.link).toBe(link.id); expect(row.project).toBe(projectId); expect(row.date).toContain(today); expect(row.count).toBe(1); const readBack = await admin .collection("intake_link_submissions") .getOne(row.id); expect(readBack.count).toBe(1); // A second row for the same link, different project, same date — must // succeed because the unique index is on the COMPOSITE (link, project, // date), not on date alone. This is the property Phase 9 needs. const row2 = await admin.collection("intake_link_submissions").create({ link: link.id, project: secondProjectId, date: today, count: 1, }); createdSubmissionIds.push(row2.id); expect(row2.id).not.toBe(row.id); }); it("the unique index blocks a duplicate (link, project, date)", async () => { const link = await newPhase9Link(); const today = new Date().toISOString().slice(0, 10); await admin.collection("intake_link_submissions").create({ link: link.id, project: projectId, date: today, count: 1, }).then((r) => createdSubmissionIds.push(r.id)); // Same (link, project, date) again — should fail. let rejected = false; try { await admin.collection("intake_link_submissions").create({ link: link.id, project: projectId, date: today, count: 2, }); } catch (e) { rejected = true; // Cleanup the row the rejected create might have left. try { const todayDate = new Date().toISOString().slice(0, 10); const all = await admin.collection("intake_link_submissions").getFullList({ filter: `link = "${link.id}" && project = "${projectId}" && date = "${todayDate}"`, }); for (const r of all) createdSubmissionIds.push(r.id); } catch { // ignore } } expect(rejected).toBe(true); }); }); describe("the public route exists for Phase 9", () => { it("returns a sensible status, never 500, on a Phase 9 link", async () => { const link = await newPhase9Link(); const response = await fetch(`${APP_URL}/api/public/intake`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token: link.token, brief: { title: "x", summary: "y", subtasks: [], area: "", degraded: false }, transcript: [], }), }); expect([400, 404, 409]).toContain(response.status); }); });