kanban-dev-webapp/tests/integration/linking.test.ts

136 lines
4.3 KiB
TypeScript
Raw Normal View History

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<string, unknown>) {
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<string, unknown>) {
// 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("");
});
});