import { describe, expect, test } from "bun:test"; import type { ChangeEvent } from "../src/api.ts"; import { changeValue, describeChange, groupByRun, logMonth, scheduleLabel, SCHEDULES } from "../src/labels.ts"; const base: ChangeEvent = { id: "ev1", type: "changed", status: "pending", connectionId: "c1", runId: "r1", targetEntity: "entry", targetId: "e1", payload: {}, createdAt: "2026-10-30T10:00:00.000Z", title: "Piranesi", }; const sources = [ { id: "c1", pluginId: "hardcover", name: "Hardcover" }, { id: "c2", pluginId: "trakt", name: "Trakt" }, ]; describe("describeChange", () => { test("removed", () => { const d = describeChange({ ...base, type: "removed", payload: { key: "b:1", kind: "entry" } }, sources); expect(d.message).toBe("Hardcover no longer has this. Your copy here is safe."); expect(d.actions).toEqual([ { action: "accept", label: "Delete here too" }, { action: "ignore", label: "Keep it" }, ]); }); test("merged, with and without the target title", () => { const ev = { ...base, type: "merged", payload: { key: "b:1", intoKey: "b:2" }, intoTitle: "Piranesi (Deluxe)" }; const d = describeChange(ev, sources); expect(d.message).toBe("Hardcover merged this into Piranesi (Deluxe)."); expect(d.actions).toEqual([ { action: "accept", label: "Follow the merge" }, { action: "ignore", label: "Ignore" }, ]); expect(describeChange({ ...ev, intoTitle: null }, sources).message).toBe("Hardcover merged this into another entry."); }); test("conflict shows both sources' values", () => { const d = describeChange( { ...base, type: "conflict", connectionId: "c2", payload: { field: "rating", current: 80, currentSource: "plugin:hardcover:c1", incoming: 60, incomingSource: "plugin:trakt:c2" }, }, sources, ); expect(d.message).toBe("Trakt and Hardcover disagree about the rating."); expect(d.field).toBe("rating"); expect(d.before).toEqual({ label: "Hardcover", value: 80 }); expect(d.after).toEqual({ label: "Trakt", value: 60 }); expect(d.actions).toEqual([ { action: "accept", label: "Use the new value" }, { action: "keep_mine", label: "Keep mine" }, { action: "ignore", label: "Ignore" }, ]); }); test("changed shows yours against the source's", () => { const d = describeChange({ ...base, payload: { field: "status", from: "completed", to: "in_progress" } }, sources); expect(d.message).toBe("Hardcover changed the status."); expect(d.before).toEqual({ label: "Yours", value: "completed" }); expect(d.after).toEqual({ label: "Hardcover", value: "in_progress" }); expect(d.actions.map((a) => a.label)).toEqual(["Use the new value", "Keep mine", "Ignore"]); }); test("an unknown source still reads well", () => { const d = describeChange({ ...base, type: "removed", connectionId: null, payload: {} }, []); expect(d.message).toBe("A source no longer has this. Your copy here is safe."); }); }); describe("scheduleLabel", () => { test("the four choices", () => { expect(scheduleLabel(null)).toBe("Off"); expect(scheduleLabel(60)).toBe("Every hour"); expect(scheduleLabel(360)).toBe("Every 6 hours"); expect(scheduleLabel(1440)).toBe("Daily"); expect(SCHEDULES).toEqual([null, 60, 360, 1440]); }); test("other intervals", () => { expect(scheduleLabel(30)).toBe("Every 30 minutes"); expect(scheduleLabel(120)).toBe("Every 2 hours"); expect(scheduleLabel(2880)).toBe("Every 2 days"); }); }); describe("groupByRun", () => { test("keeps newest-first order and groups events of one run", () => { const g = groupByRun([ { ...base, id: "3", runId: "rB" }, { ...base, id: "2", runId: "rA" }, { ...base, id: "1", runId: "rB" }, ]); expect(g.map((x) => [x.runId, x.events.map((e) => e.id)])).toEqual([ ["rB", ["3", "1"]], ["rA", ["2"]], ]); }); }); describe("changeValue", () => { test("values read in the user's terms", () => { expect(changeValue("rating", 80, "5half")).toBe("★★★★☆"); expect(changeValue("rating", 85, "10")).toBe("8.5/10"); expect(changeValue("status", "in_progress", "5half", "book")).toBe("Reading"); expect(changeValue("finishedAt", { value: "2025-03", precision: "month" }, "5half")).toBe("Mar 2025"); expect(changeValue("tags", ["sci-fi", "re-read"], "5half")).toBe("sci-fi, re-read"); expect(changeValue("favorite", true, "5half")).toBe("Yes"); expect(changeValue("rating", null, "5half")).toBe("Nothing"); expect(changeValue("tags", [], "5half")).toBe("Nothing"); expect(changeValue("review", "x".repeat(300), "5half")).toHaveLength(160); }); }); describe("logMonth", () => { test("month headings from partial dates and timestamps", () => { expect(logMonth("2026-10-01")).toBe("October 2026"); expect(logMonth("2025-03")).toBe("March 2025"); expect(logMonth("2019")).toBe("2019"); expect(logMonth("2026-01-31T23:59:00.000Z")).toBe("January 2026"); }); });