// The config contract: defaults live ONLY in the schema, and the Encoded side is the // authored file shape. A regression here would let a UI save bake defaults into the // operator's config.json (the raw round-trip invariant the kit's ConfigService relies on). import { describe, expect, test } from "bun:test"; import type { RawConfig } from "@playnite/contract"; import { PlayniteConfigSchema, resolveConfig } from "@playnite/contract"; import { Schema } from "effect"; const encode = Schema.encodeUnknownSync(PlayniteConfigSchema); describe("resolveConfig", () => { test("an empty file resolves to the documented defaults", () => { expect(resolveConfig({})).toEqual({ sync: { pollMinutes: 5, watch: true, debounceMs: 1500 }, filter: { installedOnly: true, includeHidden: false, sources: [], excludeSources: [], }, art: { mode: "host", maxBytes: 1_500_000, includeBackground: false }, gameOverrides: {}, }); }); test("a partial group keeps the untouched keys at their defaults", () => { const config = resolveConfig({ art: { mode: "dataurl" } }); expect(config.art.mode).toBe("dataurl"); expect(config.art.maxBytes).toBe(1_500_000); expect(config.sync.pollMinutes).toBe(5); }); test("stale keys from an older config are tolerated and dropped", () => { // 0.1.x wrote `ui` and `devEntry`; both are gone in 0.2.0. const config = resolveConfig({ ui: { standalone: true, port: 47994 }, devEntry: true, filter: { installedOnly: false }, }); expect(config.filter.installedOnly).toBe(false); expect("ui" in config).toBe(false); expect("devEntry" in config).toBe(false); }); test("an invalid art mode is refused", () => { expect(() => resolveConfig({ art: { mode: "inline" } })).toThrow(); }); }); describe("raw round-trip", () => { test("encoding a resolved config omits every defaulted key", () => { // This is what makes a UI save non-destructive: defaults never reach the file. expect(encode(resolveConfig({}))).toEqual({}); }); test("encode is LOSSY for defaulted groups — which is why we never re-encode", () => { // `encodingStrategy: "omit"` drops a defaulted key unconditionally, so a decode → // encode round-trip would silently throw away authored values. The kit's // ConfigService therefore persists the RAW payload verbatim and only *validates* // by decoding; nothing in the plugin may write `encode(config)` back to disk. const raw: RawConfig = { playniteDir: "D:\\Playnite", filter: { excludeSources: ["Xbox"] }, gameOverrides: { "11111111-1111-1111-1111-111111111111": { exclude: true }, }, }; // Undefaulted keys survive… expect(encode(resolveConfig(raw))).toEqual({ playniteDir: "D:\\Playnite" }); // …but the authored values themselves are only safe in the raw shape. expect(resolveConfig(raw).filter.excludeSources).toEqual(["Xbox"]); }); });