import { describe, expect, test } from "bun:test"; import type { ExportedGame, LibraryExport } from "@playnite/contract"; import { resolveConfig } from "@playnite/contract"; import { computeEntries, isGuid, launchCommand, } from "../src/domain/reconcile.js"; const G1 = "11111111-1111-1111-1111-111111111111"; const G2 = "22222222-2222-2222-2222-222222222222"; const game = (over: Partial): ExportedGame => ({ id: G1, name: "Game", installed: true, hidden: false, source: "Steam", platforms: ["PC (Windows)"], cover: null, background: null, icon: null, ...over, }); const exp = (games: ExportedGame[]): LibraryExport => ({ schema: 1, generatedAt: "2026-01-01T00:00:00Z", playnite: { version: "10", mode: "Desktop" }, games, }); const noArt = () => undefined; describe("isGuid", () => { test("accepts a real Playnite guid", () => { expect(isGuid(G1)).toBe(true); }); test("rejects junk", () => { expect(isGuid("not-a-guid")).toBe(false); expect(isGuid('570" & calc')).toBe(false); }); }); describe("launchCommand", () => { test("Windows uses start + playnite:// uri", () => { expect(launchCommand(G1, "win32")).toEqual({ kind: "command", value: `start "" "playnite://playnite/start/${G1}"`, }); }); test("non-Windows falls back to xdg-open", () => { expect(launchCommand(G1, "linux")?.value).toContain("xdg-open"); }); }); describe("computeEntries filtering", () => { test("installedOnly drops uninstalled games", () => { const { entries, report } = computeEntries({ exp: exp([game({ id: G1 }), game({ id: G2, installed: false })]), config: resolveConfig({}), platform: "win32", artFor: noArt, }); expect(entries.map((e) => e.external_id)).toEqual([G1]); expect(report.skipped[0]?.reason).toBe("not installed"); }); test("includeHidden lets hidden games through", () => { const hidden = exp([game({ id: G1, hidden: true })]); expect( computeEntries({ exp: hidden, config: resolveConfig({}), platform: "win32", artFor: noArt, }).entries.length, ).toBe(0); expect( computeEntries({ exp: hidden, config: resolveConfig({ filter: { includeHidden: true } }), platform: "win32", artFor: noArt, }).entries.length, ).toBe(1); }); test("source include/exclude is case-insensitive", () => { const games = exp([ game({ id: G1, source: "Steam" }), game({ id: G2, source: "GOG" }), ]); const only = computeEntries({ exp: games, config: resolveConfig({ filter: { sources: ["steam"] } }), platform: "win32", artFor: noArt, }); expect(only.entries.map((e) => e.external_id)).toEqual([G1]); const without = computeEntries({ exp: games, config: resolveConfig({ filter: { excludeSources: ["GOG"] } }), platform: "win32", artFor: noArt, }); expect(without.entries.map((e) => e.external_id)).toEqual([G1]); }); test("invalid ids and empty titles are skipped", () => { const { entries, report } = computeEntries({ exp: exp([game({ id: "bogus" }), game({ id: G2, name: " " })]), config: resolveConfig({}), platform: "win32", artFor: noArt, }); expect(entries.length).toBe(0); expect(report.skipped.map((s) => s.reason).sort()).toEqual([ "invalid game id", "no title", ]); }); test("entries are title-sorted and per-source counted", () => { const { entries, report } = computeEntries({ exp: exp([ game({ id: G1, name: "Zelda", source: "Nintendo" }), game({ id: G2, name: "Antichamber", source: "Steam" }), ]), config: resolveConfig({}), platform: "win32", artFor: noArt, }); expect(entries.map((e) => e.title)).toEqual(["Antichamber", "Zelda"]); expect(report.perSource).toEqual({ Nintendo: 1, Steam: 1 }); }); test("artFor result is attached", () => { const { entries } = computeEntries({ exp: exp([game({ id: G1 })]), config: resolveConfig({}), platform: "win32", artFor: () => ({ portrait: "data:image/png;base64,AAAA" }), }); expect(entries[0]?.art?.portrait).toBe("data:image/png;base64,AAAA"); }); test("the report carries the export's stamp", () => { const { report } = computeEntries({ exp: exp([game({ id: G1 })]), config: resolveConfig({}), platform: "win32", artFor: noArt, }); expect(report.generatedAt).toBe("2026-01-01T00:00:00Z"); expect(report.schemaVersion).toBe(1); expect(report.considered).toBe(1); }); }); describe("per-game overrides", () => { test("an excluded game lands in `excluded`, not `skipped`", () => { const { entries, report } = computeEntries({ exp: exp([ game({ id: G1, name: "Kept" }), game({ id: G2, name: "Dropped" }), ]), config: resolveConfig({ gameOverrides: { [G2]: { exclude: true } } }), platform: "win32", artFor: noArt, }); expect(entries.map((e) => e.title)).toEqual(["Kept"]); expect(report.excluded).toEqual([{ id: G2, title: "Dropped" }]); expect(report.skipped).toEqual([]); }); test("`exclude: false` is a no-op (the UI omits the key instead)", () => { const { entries } = computeEntries({ exp: exp([game({ id: G1 })]), config: resolveConfig({ gameOverrides: { [G1]: { exclude: false } } }), platform: "win32", artFor: noArt, }); expect(entries.length).toBe(1); }); test("a game already filtered out is not double-reported as excluded", () => { const { report } = computeEntries({ exp: exp([game({ id: G1, installed: false })]), config: resolveConfig({ gameOverrides: { [G1]: { exclude: true } } }), platform: "win32", artFor: noArt, }); expect(report.excluded).toEqual([]); expect(report.skipped[0]?.reason).toBe("not installed"); }); }); describe("dataurl guard rail", () => { test("warns once the inlined-cover payload would be too large", () => { const many = Array.from({ length: 3001 }, (_, i) => game({ id: `${i.toString(16).padStart(8, "0")}-1111-1111-1111-111111111111`, name: `Game ${i}`, }), ); const { report } = computeEntries({ exp: exp(many), config: resolveConfig({ art: { mode: "dataurl" } }), platform: "win32", artFor: noArt, }); expect(report.warnings[0]).toContain('art.mode "dataurl"'); }); test("host mode never warns", () => { const { report } = computeEntries({ exp: exp([game({ id: G1 })]), config: resolveConfig({ art: { mode: "host" } }), platform: "win32", artFor: noArt, }); expect(report.warnings).toEqual([]); }); });