Files
STTracker/packages/core/test/history.test.ts
T
Lucas WintherandClaude Opus 5.5 13b81b1cd5 feat(core): entries, user history and match suggestion accept/reject
Entries carry per-field field_sources, write user_history diffs, resolve
aliased/merged works, and support quick add, per-user moves (with alias and
draft tombstone) and keyset-paginated library listing with child rollups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:34:47 +01:00

98 lines
4.3 KiB
TypeScript

import { expect, test } from "bun:test";
import { forEachDialect } from "@sttracker/db/testing";
import { NotFoundError } from "../src/errors.ts";
import { getEntry, moveEntryToWork, quickAdd, softDeleteEntry, upsertEntry } from "../src/entries.ts";
import { getHistory, recordHistory, restoreVersion } from "../src/history.ts";
import { makeUser, makeWork } from "./user-layer-fixtures.ts";
forEachDialect("history", (getDb) => {
test("recordHistory stores only the changed keys", async () => {
const db = getDb();
const userId = await makeUser(db);
const id = await recordHistory(db.k, {
userId,
entity: "entry",
entityId: "e1",
before: { status: "planned", rating: 50, tags: ["a"] },
after: { status: "completed", rating: 50, tags: ["a"] },
actor: "user",
});
expect(id).not.toBeNull();
const none = await recordHistory(db.k, {
userId,
entity: "entry",
entityId: "e1",
before: { rating: 1 },
after: { rating: 1 },
actor: "user",
});
expect(none).toBeNull();
const rows = await getHistory(db, userId, "entry", "e1");
expect(rows).toHaveLength(1);
expect(rows[0]!.before).toEqual({ status: "planned" });
expect(rows[0]!.after).toEqual({ status: "completed" });
expect(rows[0]!.actor).toBe("user");
});
test("restoreVersion brings back previous review", async () => {
const db = getDb();
const userId = await makeUser(db);
const workId = await makeWork(db);
const e = await upsertEntry(db, userId, workId, { review: "first take" }, "user");
await upsertEntry(db, userId, workId, { review: "second take", rating: 60 }, "plugin:trakt:c1");
const history = await getHistory(db, userId, "entry", e.id);
expect(history).toHaveLength(2);
const latest = history[0]!; // newest first
expect(latest.before).toEqual({ review: "first take", rating: null });
expect(latest.actor).toBe("plugin:trakt:c1");
await restoreVersion(db, userId, latest.id);
const view = await getEntry(db, userId, e.id);
expect(view.review).toBe("first take");
expect(view.rating).toBeNull();
expect(view.fieldSources.review).toBe("user");
expect(view.fieldSources.rating).toBe("user");
expect(await getHistory(db, userId, "entry", e.id)).toHaveLength(3);
});
test("restoreVersion undoes a soft delete", async () => {
const db = getDb();
const userId = await makeUser(db);
const e = await upsertEntry(db, userId, await makeWork(db), { status: "planned" }, "user");
await softDeleteEntry(db, userId, e.id, "user");
const [del] = await getHistory(db, userId, "entry", e.id);
expect(del!.after.deletedAt).toBeString();
await restoreVersion(db, userId, del!.id);
expect((await getEntry(db, userId, e.id)).status).toBe("planned");
});
test("restoreVersion undoes a move back to the draft work", async () => {
const db = getDb();
const userId = await makeUser(db);
const draft = await quickAdd(db, userId, { title: "Dune??" });
const shared = await makeWork(db, { title: "Dune", externalIds: { isbn13: "1" } });
await moveEntryToWork(db, userId, draft.id, shared, "user");
const [move] = await getHistory(db, userId, "entry", draft.id);
await restoreVersion(db, userId, move!.id);
const back = await getEntry(db, userId, draft.id);
expect(back.workId).toBe(draft.workId);
expect(back.draft).toBe(true);
const work = await db.k.selectFrom("works").selectAll().where("id", "=", draft.workId).executeTakeFirstOrThrow();
expect(work.merged_into_id).toBeNull();
// the shared work now aliases back to the draft for this user; no alias cycle remains
const aliases = await db.k.selectFrom("work_aliases").selectAll().where("user_id", "=", userId).execute();
expect(aliases.map((a) => [a.from_work_id, a.to_work_id])).toEqual([[shared, draft.workId]]);
});
test("another user's history is invisible", async () => {
const db = getDb();
const ada = await makeUser(db);
const bob = await makeUser(db, "bob");
const e = await upsertEntry(db, ada, await makeWork(db), { review: "x" }, "user");
expect(await getHistory(db, bob, "entry", e.id)).toEqual([]);
const [h] = await getHistory(db, ada, "entry", e.id);
await expect(restoreVersion(db, bob, h!.id)).rejects.toThrow(NotFoundError);
});
});