e84cc89222
The managed runner is de-privileged on Windows now (runs as LocalService), and %ProgramData%\punktfunk is locked read-only to it — so writing config/ cache straight under the config dir fails EPERM (proven on-glass). Move the plugin's state to <config_dir>/plugin-state/rom-manager, which `punktfunk-host plugins enable` grants the runner write on. On Linux the runner owns the config dir, so the path is writable there too — one path, no branch. Migrate a pre-0.2.1 config/cache from the old <config_dir>/rom-manager on first load (copy, guarded, best-effort) so an existing operator's roots + launch templates survive the move instead of silently resetting to defaults. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
// State persistence: the plugin writes under `<config_dir>/plugin-state/rom-manager`, and on first
|
|
// run migrates an operator config authored in the pre-0.2.1 `<config_dir>/rom-manager` location —
|
|
// the write path moved when the runner was de-privileged, and a missed migration would silently
|
|
// reset the operator's settings.
|
|
import { afterEach, beforeEach, 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 { loadConfig, saveRawConfig } from "../src/state.js";
|
|
|
|
let root: string;
|
|
let saved: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
saved = process.env.PUNKTFUNK_CONFIG_DIR;
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), "rm-state-"));
|
|
process.env.PUNKTFUNK_CONFIG_DIR = root;
|
|
});
|
|
afterEach(() => {
|
|
if (saved === undefined) delete process.env.PUNKTFUNK_CONFIG_DIR;
|
|
else process.env.PUNKTFUNK_CONFIG_DIR = saved;
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("state location + migration", () => {
|
|
test("persists under plugin-state/rom-manager", () => {
|
|
saveRawConfig({ roots: [{ dir: "/games", platform: "snes" }] });
|
|
expect(
|
|
fs.existsSync(
|
|
path.join(root, "plugin-state", "rom-manager", "config.json"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("migrates a pre-0.2.1 config from <config_dir>/rom-manager on first load", () => {
|
|
// An operator config authored in the OLD location.
|
|
const legacy = path.join(root, "rom-manager");
|
|
fs.mkdirSync(legacy, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(legacy, "config.json"),
|
|
JSON.stringify({ roots: [{ dir: "/legacy/games", platform: "snes" }] }),
|
|
);
|
|
// First load migrates it — the operator's roots survive rather than resetting to defaults.
|
|
const cfg = loadConfig();
|
|
expect(cfg.roots).toEqual([{ dir: "/legacy/games", platform: "snes" }]);
|
|
expect(
|
|
fs.existsSync(
|
|
path.join(root, "plugin-state", "rom-manager", "config.json"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("does not clobber an existing new-location config with the legacy one", () => {
|
|
const legacy = path.join(root, "rom-manager");
|
|
fs.mkdirSync(legacy, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(legacy, "config.json"),
|
|
JSON.stringify({ roots: [{ dir: "/old", platform: "snes" }] }),
|
|
);
|
|
saveRawConfig({ roots: [{ dir: "/new", platform: "snes" }] }); // new location already authored
|
|
expect(loadConfig().roots).toEqual([{ dir: "/new", platform: "snes" }]);
|
|
});
|
|
});
|