Files
STTracker/packages/core/test/sync/change-titles.test.ts
T
Lucas WintherandClaude Opus 5.5 c2e8c83f86 feat(core,server): log feed, titles on changes, work titles on copies
GET log (engagements and listens, keyset over at/kind/id), listChanges
title and intoTitle, listCopies workTitle, all via batched lookups.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 22:11:53 +01:00

96 lines
4.0 KiB
TypeScript

import { expect, test } from "bun:test";
import type { Db } from "@sttracker/db/db";
import { newId, nowIso } from "@sttracker/db/ids";
import { toJson } from "@sttracker/db/json";
import { forEachDialect } from "@sttracker/db/testing";
import { addEngagement } from "../../src/engagements.ts";
import { upsertEntry } from "../../src/entries.ts";
import { createList } from "../../src/lists.ts";
import { listChanges } from "../../src/sync/changes.ts";
import { makeConnection, makeUser, makeWork } from "./helpers.ts";
async function event(
db: Db,
userId: string,
connectionId: string,
e: { type: "changed" | "removed" | "merged"; targetEntity: string; targetId: string; payload?: Record<string, unknown> },
): Promise<string> {
const id = newId();
await db.k
.insertInto("change_events")
.values({
id,
user_id: userId,
connection_id: connectionId,
run_id: null,
record_id: null,
type: e.type,
target_entity: e.targetEntity,
target_id: e.targetId,
payload: toJson(e.payload ?? {}),
status: "pending",
created_at: nowIso(),
})
.execute();
return id;
}
async function record(db: Db, connectionId: string, key: string, targetEntity: string, targetId: string): Promise<void> {
const now = nowIso();
await db.k
.insertInto("source_records")
.values({
id: newId(),
connection_id: connectionId,
key,
kind: targetEntity,
current_hash: "h",
first_seen: now,
last_seen: now,
deleted_at: null,
merged_into_key: null,
target_entity: targetEntity,
target_id: targetId,
})
.execute();
}
forEachDialect("change titles", (getDb) => {
test("events carry their target's title, and merged events the merge target's", async () => {
const db = getDb();
const userId = await makeUser(db);
const conn = await makeConnection(db, userId, "hardcover");
const dune = await upsertEntry(db, userId, await makeWork(db, { title: "Dune" }), { overrides: { title: "Dune (1965)" } }, "user");
const piranesi = await upsertEntry(db, userId, await makeWork(db, { title: "Piranesi" }), {}, "user");
const deluxe = await upsertEntry(db, userId, await makeWork(db, { title: "Piranesi (Deluxe)" }), {}, "user");
const read = await addEngagement(db, userId, piranesi.id, {}, "user");
const list = await createList(db, userId, { name: "Want to read" });
await record(db, conn, "b:deluxe", "entry", deluxe.id);
const onEntry = await event(db, userId, conn, { type: "changed", targetEntity: "entry", targetId: dune.id, payload: { field: "rating", from: 80, to: 60 } });
const onRead = await event(db, userId, conn, { type: "removed", targetEntity: "engagement", targetId: read.id });
const onList = await event(db, userId, conn, { type: "removed", targetEntity: "list", targetId: list.id });
const merged = await event(db, userId, conn, { type: "merged", targetEntity: "entry", targetId: piranesi.id, payload: { key: "b:p", intoKey: "b:deluxe" } });
const unknownMerge = await event(db, userId, conn, { type: "merged", targetEntity: "entry", targetId: piranesi.id, payload: { key: "b:p", intoKey: "b:nope" } });
const byId = new Map((await listChanges(db, userId)).items.map((c) => [c.id, c]));
expect(byId.get(onEntry)!.title).toBe("Dune (1965)");
expect(byId.get(onRead)!.title).toBe("Piranesi");
expect(byId.get(onList)!.title).toBe("Want to read");
expect(byId.get(merged)!.title).toBe("Piranesi");
expect(byId.get(merged)!.intoTitle).toBe("Piranesi (Deluxe)");
expect(byId.get(unknownMerge)!.intoTitle).toBeNull();
expect(byId.get(onEntry)!.intoTitle).toBeUndefined();
});
test("a missing target gives a null title", async () => {
const db = getDb();
const userId = await makeUser(db);
const conn = await makeConnection(db, userId, "hardcover");
const id = await event(db, userId, conn, { type: "removed", targetEntity: "entry", targetId: newId() });
const [c] = (await listChanges(db, userId)).items;
expect(c!.id).toBe(id);
expect(c!.title).toBeNull();
});
});