import { describe, it, expect, beforeAll, afterAll } from "vitest"; import PocketBase from "pocketbase"; import { relinkProject, linkCommitManually } from "@/lib/linking/engine"; import { DEMO, adminPb, requireServices } from "./helpers"; let admin: PocketBase; let projectId: string; let columnId: string; const created: Array<{ collection: string; id: string }> = []; async function makeCard(fields: Record) { const numbered = await admin .collection("cards") .getFullList({ filter: `project = "${projectId}"`, fields: "number" }); const next = Math.max(0, ...numbered.map((c) => Number(c.number) || 0)) + 1; const card = await admin.collection("cards").create({ project: projectId, column: columnId, title: "Linking probe", position: 1_000_000 + next, priority: "medium", created_by_role: "owner", accepted: true, archived: false, number: next, ...fields, }); created.push({ collection: "cards", id: card.id }); return card; } async function makeCommit(fields: Record) { // Use the card's created timestamp as the commit date by default — the test // creates both, and the linking engine refuses to attach a commit whose // authored_at predates the card's created. The helper still accepts a // caller-supplied override for the date-bounding tests. const cardCreated = new Date().toISOString(); const commit = await admin.collection("commits").create({ project: projectId, sha: `probe${Date.now()}${Math.random().toString(36).slice(2, 8)}`, short_sha: "probe00", message: "feat(probe): a thing", subject: "a thing", type: "feat", scope: "probe", author_name: "Dave", author_email: "dave@example.com", authored_at: cardCreated, is_merge: false, link_source: "none", ...fields, }); created.push({ collection: "commits", id: commit.id }); return commit; } beforeAll(async () => { await requireServices(); admin = await adminPb(); const project = await admin .collection("projects") .getFirstListItem(`slug = "${DEMO.projectSlug}"`); projectId = project.id; columnId = ( await admin .collection("columns") .getFirstListItem(`project = "${projectId}" && is_inbox = true`) ).id; }); afterAll(async () => { for (const item of created.reverse()) { await admin.collection(item.collection).delete(item.id).catch(() => {}); } }); describe("relinking is idempotent and preserves manual links", () => { it("links by scope, and running twice changes nothing", async () => { const card = await makeCard({ scope_rule: "probe" }); const commit = await makeCommit({}); await relinkProject(projectId); const first = await admin.collection("commits").getOne(commit.id); expect(first.card).toBe(card.id); expect(first.link_source).toBe("scope"); await relinkProject(projectId); const second = await admin.collection("commits").getOne(commit.id); expect(second.card).toBe(card.id); // `updated` must not move: re-linking writes only where the answer changed. expect(second.updated).toBe(first.updated); }); it("never overwrites a manual link", async () => { const scoped = await makeCard({ scope_rule: "probe" }); const other = await makeCard({ scope_rule: "" }); const commit = await makeCommit({}); await linkCommitManually(commit.id, other.id); await relinkProject(projectId); const after = await admin.collection("commits").getOne(commit.id); expect(after.card).toBe(other.id); expect(after.card).not.toBe(scoped.id); expect(after.link_source).toBe("manual"); }); it("does not let a card claim commits from before it existed", async () => { const old = await makeCommit({ scope: "backdated", authored_at: "2020-01-01T00:00:00.000Z", }); await makeCard({ scope_rule: "backdated" }); await relinkProject(projectId); const after = await admin.collection("commits").getOne(old.id); expect(after.card).toBe(""); expect(after.link_source).toBe("none"); }); it("never links a merge commit", async () => { await makeCard({ scope_rule: "mergeprobe" }); const merge = await makeCommit({ scope: "mergeprobe", is_merge: true }); await relinkProject(projectId); const after = await admin.collection("commits").getOne(merge.id); expect(after.card).toBe(""); }); });