Files
STTracker/packages/core/test/matching.test.ts
T
2026-10-30 15:56:05 +01:00

103 lines
4.7 KiB
TypeScript

import { expect, test } from "bun:test";
import type { Db } from "@sttracker/db/db";
import { newId, nowIso } from "@sttracker/db/ids";
import { forEachDialect } from "@sttracker/db/testing";
import { fuzzyScore, matchWork } from "../src/matching.ts";
import { createWork, getWork } from "../src/works.ts";
async function addUser(db: Db): Promise<string> {
const id = newId();
const now = nowIso();
await db.k
.insertInto("users")
.values({ id, username: `u${id}`, display_name: "U", password_hash: "x", role: "member", preferences: "{}", created_at: now, updated_at: now })
.execute();
return id;
}
test("fuzzyScore", () => {
expect(fuzzyScore({ title: "The Expanse: Leviathan Wakes" }, { title: "Leviathan Wakes" })).toBeGreaterThan(0.6);
expect(fuzzyScore({ title: "Dune" }, { title: "Dune Messiah" })).toBeLessThan(0.6);
expect(fuzzyScore({ title: "Pokémon" }, { title: "Pokemon" })).toBe(1);
expect(
fuzzyScore({ title: "Pokémon", year: 1996, creators: [{ name: "Satoshi" }] }, { title: "pokemon!", year: 1996, creators: [{ name: "satoshi" }] }),
).toBe(1);
// year off by one counts half, creator mismatch counts zero
expect(fuzzyScore({ title: "A B", year: 2000, creators: [{ name: "x" }] }, { title: "A B", year: 2001, creators: [{ name: "y" }] })).toBeCloseTo(0.8, 5);
});
forEachDialect("matching", (getDb) => {
test("match by isbn13 links existing", async () => {
const db = getDb();
const u = await addUser(db);
const id = await createWork(db, { hint: { title: "Dune", mediaType: "book", externalIds: { isbn13: "9780441013593" } } }, "t");
const r = await matchWork(db, u, { title: "Dune (Ace)", mediaType: "book", year: 1990, externalIds: { isbn13: "9780441013593", asin: "B1" } }, "plug");
expect(r).toEqual({ workId: id, created: false, draft: false });
expect((await getWork(db, id))?.year).toBe(1990);
});
test("unseen external id creates shared work", async () => {
const db = getDb();
const u = await addUser(db);
const r = await matchWork(db, u, { title: "Heat", mediaType: "movie", externalIds: { tmdb_movie: "949" } }, "p");
expect(r.created).toBe(true);
expect(r.draft).toBe(false);
expect((await getWork(db, r.workId))?.owner_user_id).toBeNull();
});
const chain = () => ({
title: "Pilot",
mediaType: "episode" as const,
externalIds: {},
position: { episode: 1, season: 1 },
parent: {
title: "Season 1",
mediaType: "season" as const,
externalIds: {},
position: { season: 1 },
parent: { title: "Lost", mediaType: "show" as const, externalIds: { tmdb_show: "4607" } },
},
});
test("episode hint creates show→season→episode chain once", async () => {
const db = getDb();
const u = await addUser(db);
const r1 = await matchWork(db, u, chain(), "p");
const r2 = await matchWork(db, u, chain(), "p");
expect(r1.created).toBe(true);
expect(r2).toEqual({ workId: r1.workId, created: false, draft: false });
expect(await db.k.selectFrom("works").select("id").execute()).toHaveLength(3);
const ep = await getWork(db, r1.workId);
expect(ep?.position).toEqual({ season: 1, episode: 1 });
expect(ep?.parent_id).not.toBeNull();
const season = await getWork(db, ep!.parent_id!);
expect(season?.media_type).toBe("season");
expect((await getWork(db, season!.parent_id!))?.media_type).toBe("show");
});
test("position key order does not matter", async () => {
const db = getDb();
const u = await addUser(db);
const a = await matchWork(db, u, chain(), "p");
const h = chain();
h.position = { season: 1, episode: 1 };
expect((await matchWork(db, u, h, "p")).workId).toBe(a.workId);
});
test("hint without ids creates private draft with suggestions", async () => {
const db = getDb();
const u = await addUser(db);
const shared = await createWork(db, { hint: { title: "Leviathan Wakes", mediaType: "book", externalIds: { isbn13: "1" } } }, "t");
await createWork(db, { hint: { title: "Leviathan Wakes", mediaType: "game", externalIds: { igdb: "1" } } }, "t");
await createWork(db, { hint: { title: "Unrelated", mediaType: "book", externalIds: { isbn13: "2" } } }, "t");
const r = await matchWork(db, u, { title: "The Expanse: Leviathan Wakes", mediaType: "book", externalIds: {} }, "p");
expect(r.created).toBe(true);
expect(r.draft).toBe(true);
expect((await getWork(db, r.workId))?.owner_user_id).toBe(u);
const sug = await db.k.selectFrom("match_suggestions").selectAll().execute();
expect(sug).toHaveLength(1);
expect(sug[0]).toMatchObject({ user_id: u, work_id: r.workId, candidate_work_id: shared, status: "pending" });
expect(sug[0]!.score).toBeGreaterThan(0.6);
});
});