import { describe, expect, test } from "bun:test"; import { resolveConfig } from "../src/config.js"; import type { DetectedEmulator } from "../src/emulators.js"; import { computeDesired } from "../src/engine/reconcile.js"; import type { RomCandidate } from "../src/engine/scanner.js"; import type { ArtVerdict } from "../src/state.js"; const RA: DetectedEmulator = { id: "retroarch", name: "RetroArch", template: "{exe} -f -L {core} {rom}", supportsArchives: true, exeToken: "'/usr/bin/retroarch'", via: "path", coresDir: "/cores", cores: ["snes9x"], }; const cand = (rel: string, over: Partial = {}): RomCandidate => ({ platform: "snes", rootDir: "/roms/snes", absPath: `/roms/snes/${rel}`, relPath: rel, ext: ".sfc", stem: rel.replace(/\.[^.]+$/, ""), isArchive: false, ...over, }); const base = { roots: [{ dir: "/roms/snes", platform: "snes" }] }; const run = ( raw: Parameters[0], scan: RomCandidate[], art: Record = {}, detected = [RA], ) => computeDesired({ config: resolveConfig({ ...base, ...raw }), scan, detected, art, os: "linux", }); describe("computeDesired", () => { test("produces a quoted launch command with the resolved core path", () => { const { entries } = run({}, [cand("Chrono Trigger (USA).sfc")]); expect(entries).toHaveLength(1); expect(entries[0]).toMatchObject({ external_id: "snes/Chrono Trigger (USA).sfc", title: "Chrono Trigger", launch: { kind: "command", value: "'/usr/bin/retroarch' -f -L '/cores/snes9x_libretro.so' '/roms/snes/Chrono Trigger (USA).sfc'", }, }); }); test("per-game exclude drops the entry", () => { const { entries } = run( { gameOverrides: { "snes/A.sfc": { exclude: true } } }, [cand("A.sfc"), cand("B.sfc")], ); expect(entries.map((e) => e.external_id)).toEqual(["snes/B.sfc"]); }); test("an undetected emulator is skipped with a reason", () => { const { entries, report } = run({}, [cand("A.sfc")], {}, []); expect(entries).toHaveLength(0); expect(report.skipped[0]?.reason).toContain("not detected"); }); test("region dedupe keeps the preferred release", () => { const { entries, report } = run({ sync: { dedupeRegions: ["snes"] } }, [ cand("Sonic (USA).sfc"), cand("Sonic (Europe).sfc"), ]); expect(entries.map((e) => e.external_id)).toEqual(["snes/Sonic (USA).sfc"]); expect(report.skipped.some((s) => s.reason.includes("deduped"))).toBe(true); }); test("attaches cached artwork, keyed by external_id", () => { const art = { "snes/A.sfc": { art: { portrait: "http://x/p.png", hero: "http://x/h.png" }, provider: "steamgriddb:ab", matcher: 1, }, }; const { entries } = run({}, [cand("A.sfc")], art); expect(entries[0]?.art).toEqual({ portrait: "http://x/p.png", hero: "http://x/h.png", }); }); test("per-game art override wins on portrait but keeps other cached art", () => { const art = { "snes/A.sfc": { art: { portrait: "http://x/p.png", hero: "http://x/h.png" }, provider: "p", matcher: 1, }, }; const { entries } = run( { gameOverrides: { "snes/A.sfc": { art: "http://o/p.png" } } }, [cand("A.sfc")], art, ); expect(entries[0]?.art).toEqual({ portrait: "http://o/p.png", hero: "http://x/h.png", }); }); test("archive gating skips a zip when the emulator can't read archives", () => { const noArc = { ...RA, supportsArchives: false }; const { entries, report } = run( {}, [cand("A.zip", { ext: ".zip", isArchive: true })], {}, [noArc], ); expect(entries).toHaveLength(0); expect(report.skipped[0]?.reason).toContain("archive"); }); test("scale guard truncates deterministically and reports it", () => { const { entries, report } = run({ sync: { maxEntries: 1 } }, [ cand("B.sfc"), cand("A.sfc"), ]); expect(entries.map((e) => e.external_id)).toEqual(["snes/A.sfc"]); // sorted, first kept expect(report.truncated).toBe(1); }); });