The integration project now aliases server-only to a tiny empty stub so tests can import modules that carry 'import "server-only"' without the throwing real implementation firing during prepare. Unit tests do NOT get the alias — a pure module with a server import is exactly the kind of leak the unit project's missing condition is meant to surface.
136 lines
No EOL
4.3 KiB
TypeScript
136 lines
No EOL
4.3 KiB
TypeScript
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("");
|
|
});
|
|
}); |