import assert from "node:assert/strict"; import path from "node:path"; import test from "node:test"; import { prepareUserNodecgRuntime } from "../main/nodecg/runtime-provisioner"; type FakeFsState = { paths: Set; files: Map; removed: string[]; copied: Array<{ from: string; to: string }>; }; function createFakeFs(initialPaths: string[] = [], initialFiles: Record = {}) { const state: FakeFsState = { paths: new Set(initialPaths.map((candidatePath) => path.normalize(candidatePath))), files: new Map(Object.entries(initialFiles).map(([filePath, content]) => [path.normalize(filePath), content])), removed: [], copied: [], }; for (const filePath of state.files.keys()) { state.paths.add(filePath); } return { state, deps: { existsSync: (candidatePath: string) => state.paths.has(path.normalize(candidatePath)), mkdirSync: (candidatePath: string) => { state.paths.add(path.normalize(candidatePath)); return undefined; }, rmSync: (candidatePath: string) => { state.removed.push(path.normalize(candidatePath)); state.paths.delete(path.normalize(candidatePath)); }, cpSync: (from: string, to: string) => { state.copied.push({ from: path.normalize(from), to: path.normalize(to) }); state.paths.add(path.normalize(to)); state.paths.add(path.join(path.normalize(to), "index.js")); state.paths.add(path.join(path.normalize(to), "package.json")); state.paths.add(path.join(path.normalize(to), "node_modules", "nodecg", "dist", "server", "bootstrap.js")); state.paths.add(path.join(path.normalize(to), "bundles", "scoreko-dev", "package.json")); }, readFileSync: (filePath: string) => state.files.get(path.normalize(filePath)) ?? "{}", writeFileSync: (filePath: string, content: string) => { state.files.set(path.normalize(filePath), content); state.paths.add(path.normalize(filePath)); }, }, }; } function getSourcePaths(source: string) { return [ source, path.join(source, "index.js"), path.join(source, "package.json"), path.join(source, "node_modules", "nodecg", "dist", "server", "bootstrap.js"), path.join(source, "bundles", "scoreko-dev", "package.json"), path.join(source, ".scoreko-runtime.json"), ]; } test("prepareUserNodecgRuntime copies the packaged runtime into userData", () => { const source = path.normalize("/app/lib/nodecg"); const userData = path.normalize("/user/scoreko"); const { state, deps } = createFakeFs(getSourcePaths(source), { [path.join(source, ".scoreko-runtime.json")]: JSON.stringify({ bundleVersion: "0.1.0", nodecgVersion: "2.6.4" }), }); const preparedRuntime = prepareUserNodecgRuntime({ sourceRuntimePath: source, userDataPath: userData, appVersion: "0.1.0", bundleName: "scoreko-dev", log: () => undefined, deps, }); assert.equal(preparedRuntime.runtimePath, path.join(userData, "nodecg")); assert.equal(preparedRuntime.installed, true); assert.equal(state.copied.length, 1); assert.ok(state.paths.has(path.join(userData, "nodecg", "cfg"))); assert.ok(state.paths.has(path.join(userData, "nodecg", "db"))); assert.ok(state.paths.has(path.join(userData, "nodecg", "logs"))); assert.ok(state.files.has(path.join(userData, "nodecg", ".scoreko-installed-runtime.json"))); }); test("prepareUserNodecgRuntime keeps an up-to-date runtime in place", () => { const source = path.normalize("/app/lib/nodecg"); const userData = path.normalize("/user/scoreko"); const target = path.join(userData, "nodecg"); const sourceManifest = { bundleVersion: "0.1.0", generatedAt: "2026-05-24T00:00:00.000Z", nodecgVersion: "2.6.4" }; const targetManifest = { appVersion: "0.1.0", bundleName: "scoreko-dev", sourceRuntime: sourceManifest }; const { state, deps } = createFakeFs( [ ...getSourcePaths(source), path.join(target, "node_modules", "nodecg", "dist", "server", "bootstrap.js"), path.join(target, "bundles", "scoreko-dev", "package.json"), path.join(target, ".scoreko-installed-runtime.json"), ], { [path.join(source, ".scoreko-runtime.json")]: JSON.stringify(sourceManifest), [path.join(target, ".scoreko-installed-runtime.json")]: JSON.stringify(targetManifest), }, ); const preparedRuntime = prepareUserNodecgRuntime({ sourceRuntimePath: source, userDataPath: userData, appVersion: "0.1.0", bundleName: "scoreko-dev", log: () => undefined, deps, }); assert.equal(preparedRuntime.installed, false); assert.equal(state.copied.length, 0); assert.equal(state.removed.length, 0); }); test("prepareUserNodecgRuntime refreshes managed files when the app version changes", () => { const source = path.normalize("/app/lib/nodecg"); const userData = path.normalize("/user/scoreko"); const target = path.join(userData, "nodecg"); const sourceManifest = { bundleVersion: "0.1.0", generatedAt: "2026-05-24T00:00:00.000Z", nodecgVersion: "2.6.4" }; const targetManifest = { appVersion: "0.0.9", bundleName: "scoreko-dev", sourceRuntime: sourceManifest }; const { state, deps } = createFakeFs( [ ...getSourcePaths(source), path.join(target, "node_modules", "nodecg", "dist", "server", "bootstrap.js"), path.join(target, "bundles", "scoreko-dev", "package.json"), path.join(target, ".scoreko-installed-runtime.json"), ], { [path.join(source, ".scoreko-runtime.json")]: JSON.stringify(sourceManifest), [path.join(target, ".scoreko-installed-runtime.json")]: JSON.stringify(targetManifest), }, ); const preparedRuntime = prepareUserNodecgRuntime({ sourceRuntimePath: source, userDataPath: userData, appVersion: "0.1.0", bundleName: "scoreko-dev", log: () => undefined, deps, }); assert.equal(preparedRuntime.installed, true); assert.equal(state.copied.length, 1); assert.ok(state.removed.includes(path.join(target, "node_modules"))); assert.ok(state.removed.includes(path.join(target, "bundles"))); assert.ok(!state.removed.includes(path.join(target, "db"))); }); test("prepareUserNodecgRuntime refreshes managed files when the source runtime was regenerated", () => { const source = path.normalize("/app/lib/nodecg"); const userData = path.normalize("/user/scoreko"); const target = path.join(userData, "nodecg"); const sourceManifest = { bundleVersion: "0.1.0", generatedAt: "2026-05-24T01:00:00.000Z", nodecgVersion: "2.6.4" }; const targetSourceManifest = { bundleVersion: "0.1.0", generatedAt: "2026-05-24T00:00:00.000Z", nodecgVersion: "2.6.4", }; const targetManifest = { appVersion: "0.1.0", bundleName: "scoreko-dev", sourceRuntime: targetSourceManifest }; const { state, deps } = createFakeFs( [ ...getSourcePaths(source), path.join(target, "node_modules", "nodecg", "dist", "server", "bootstrap.js"), path.join(target, "bundles", "scoreko-dev", "package.json"), path.join(target, ".scoreko-installed-runtime.json"), ], { [path.join(source, ".scoreko-runtime.json")]: JSON.stringify(sourceManifest), [path.join(target, ".scoreko-installed-runtime.json")]: JSON.stringify(targetManifest), }, ); const preparedRuntime = prepareUserNodecgRuntime({ sourceRuntimePath: source, userDataPath: userData, appVersion: "0.1.0", bundleName: "scoreko-dev", log: () => undefined, deps, }); assert.equal(preparedRuntime.installed, true); assert.equal(state.copied.length, 1); assert.ok(state.removed.includes(path.join(target, "bundles"))); assert.ok(!state.removed.includes(path.join(target, "cfg"))); });