Files
punktfunk-plugin-playnite/test/playnite.test.ts
T
enricobuehler e832201669
CI / publish (push) Successful in 17s
CI / exporter (push) Successful in 11s
CI / build (push) Successful in 20s
fix: read/write the library via the host ingest inbox (de-privileged runner)
The host plugin runner is de-privileged now (LocalService) and can't read the
interactive user's %APPDATA%\Playnite\ExtensionsData — so it never saw the
export. Bridge it through the host's user-writable ingest inbox:

- exporter (C#): also drop punktfunk-library.json into
  %ProgramData%\punktfunk\ingest\playnite (best-effort, only when a punktfunk
  host is present), alongside its ExtensionsData home.
- plugin (TS): locateExport() reads <config_dir>/ingest/playnite first, then
  falls back to the ExtensionsData scan (same-user/SYSTEM runner, Linux). The
  watcher also watches the inbox so a drop reconciles within seconds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:40:28 +02:00

92 lines
3.3 KiB
TypeScript

import { afterAll, describe, expect, test } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { resolveConfig } from "../src/config.js";
import { EXPORT_FILE } from "../src/export-schema.js";
import { ExportError, locateExport, readExport } from "../src/playnite.js";
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pf-playnite-loc-"));
afterAll(() => fs.rmSync(root, { recursive: true, force: true }));
/** Lay out a fake Playnite data dir with an export under ExtensionsData/<guid>/. */
const layout = (name: string, doc: unknown): string => {
const dir = path.join(root, name);
const ext = path.join(dir, "ExtensionsData", "abc-guid");
fs.mkdirSync(ext, { recursive: true });
if (doc !== undefined)
fs.writeFileSync(path.join(ext, EXPORT_FILE), JSON.stringify(doc));
return dir;
};
const validDoc = {
schema: 1,
generatedAt: "2026-01-01T00:00:00Z",
playnite: { version: "10", mode: "Desktop" },
games: [],
};
describe("locateExport", () => {
test("finds the export under an overridden Playnite dir", () => {
const dir = layout("found", validDoc);
const loc = locateExport(resolveConfig({ playniteDir: dir }));
expect(loc.dir).toBe(dir);
expect(loc.file).toBe(
path.join(dir, "ExtensionsData", "abc-guid", EXPORT_FILE),
);
expect(loc.mtime).toBeGreaterThan(0);
});
test("reports the dir but no file when the exporter hasn't written yet", () => {
const dir = path.join(root, "empty");
fs.mkdirSync(dir, { recursive: true });
const loc = locateExport(resolveConfig({ playniteDir: dir }));
expect(loc.dir).toBe(dir);
expect(loc.file).toBeNull();
});
test("reads the ingest inbox first — the de-privileged runner's source", () => {
const cfg = fs.mkdtempSync(path.join(os.tmpdir(), "pf-cfg-"));
const prev = process.env.PUNKTFUNK_CONFIG_DIR;
process.env.PUNKTFUNK_CONFIG_DIR = cfg;
try {
const inbox = path.join(cfg, "ingest", "playnite");
fs.mkdirSync(inbox, { recursive: true });
fs.writeFileSync(path.join(inbox, EXPORT_FILE), JSON.stringify(validDoc));
// Even with a Playnite ExtensionsData dir present, the ingest drop wins.
const pd = layout("with-ingest", validDoc);
const loc = locateExport(resolveConfig({ playniteDir: pd }));
expect(loc.dir).toBe(inbox);
expect(loc.file).toBe(path.join(inbox, EXPORT_FILE));
} finally {
if (prev === undefined) delete process.env.PUNKTFUNK_CONFIG_DIR;
else process.env.PUNKTFUNK_CONFIG_DIR = prev;
fs.rmSync(cfg, { recursive: true, force: true });
}
});
});
describe("readExport", () => {
test("reads a valid export", () => {
const dir = layout("valid", validDoc);
const file = path.join(dir, "ExtensionsData", "abc-guid", EXPORT_FILE);
expect(readExport(file).schema).toBe(1);
});
test("rejects a future schema", () => {
const dir = layout("future", { ...validDoc, schema: 99 });
const file = path.join(dir, "ExtensionsData", "abc-guid", EXPORT_FILE);
expect(() => readExport(file)).toThrow(ExportError);
});
test("rejects non-JSON and foreign shapes", () => {
const bad = path.join(root, "bad.json");
fs.writeFileSync(bad, "{not json");
expect(() => readExport(bad)).toThrow(ExportError);
const foreign = path.join(root, "foreign.json");
fs.writeFileSync(foreign, JSON.stringify({ hello: "world" }));
expect(() => readExport(foreign)).toThrow(ExportError);
});
});