Files
punktfunk-plugin-playnite/plugin/test/config.test.ts
T
enricobuehler b81e03685a refactor: rewrite on @punktfunk/plugin-kit — contract + plugin workspaces
The framework half of this plugin (engine lifecycle, config/state, host
reconcile, UI server, CLI) was ~80% identical to rom-manager's. That code is
now @punktfunk/plugin-kit; this replaces the local copy with it and adopts the
rom-manager blueprint layout.

  contract/  Effect Schemas: the cross-process export contract (the C#
             exporter's other half, with the schema-version guard as a decode
             check), the config schema (one schema, Encoded = authored file
             shape, defaults only via withDecodingDefaultKey), domain DTOs, the
             HttpApi contract, API errors.
  plugin/    src/domain = the pure core (locate/read, art, reconcile), ported
             tests green before any Effect wiring; src/services = the kit's
             ConfigService/CacheStore/SyncEngine/ProviderClient; definePluginKit
             entry, kit CLI, auth-free dev API.

Preserved, because they are what makes playnite work at all: the ingest inbox
is still read FIRST (the de-privileged LocalService runner cannot reach the
interactive user's %APPDATA%), the host/dataurl/off art modes, and the
playnite:// launch hand-back. The C# exporter is untouched.

New: per-game include/exclude overrides, and an allow-listed /api/art proxy so
the SPA can render local Playnite covers — it serves only paths the currently
ingested export references.

Dropped: the standalone password UI server (console surface + CLI only, as in
rom-manager 0.3.0) and the devEntry flag.

Trap found and fixed here: the HttpApi encoder serialises `undefined` as
`null`, so a `Schema.optional` field the server omits cannot be decoded by the
client — silently in the SSE feed, loudly in the typed client. Every optional
EngineStatus field is `Schema.NullOr` with an explicit value on the wire.
2026-07-20 20:56:12 +02:00

74 lines
2.8 KiB
TypeScript

// 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"]);
});
});