import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { INSTANCE_SCHEMA_VERSION, clearInstance, instanceFileName, instancesDir, makeInstanceId, manifestPath, projectsOf, pruneInstances, readInstances, readManifest, refreshDiscovery, writeInstance, writeManifest, } from "../out/agent/instances.js"; import { endpointPath, readEndpoint } from "../out/agent/endpoint.js"; function instanceHome() { return mkdtempSync(join(tmpdir(), "ra3-instances-")); } function makeInstance(id, pid = process.pid) { return { instanceId: id, url: `http://127.0.0.1:${10000 + (pid % 1000)}`, token: `tok-${id}`, projectDir: "D:/Mods/Alpha", projects: ["D:/Mods/Alpha"], processId: pid, }; } test("instances are written and read back", async () => { const home = instanceHome(); try { const file = await writeInstance(makeInstance("a-1"), home); assert.ok(existsSync(file)); assert.ok(file.includes(instancesDir(home))); assert.equal(file.endsWith(instanceFileName("a-1")), true); const all = await readInstances(home); assert.equal(all.length, 1); assert.equal(all[0].instanceId, "a-1"); assert.equal(all[0].token, "tok-a-1"); } finally { rmSync(home, { recursive: true, force: true }); } }); test("concurrent windows do not overwrite each other", async () => { const home = instanceHome(); try { // Two windows enabling agent access at the same time: separate files, so // there is no read-modify-write race to guard. await writeInstance(makeInstance("win-a"), home); await writeInstance(makeInstance("win-b"), home); const ids = (await readInstances(home)).map((i) => i.instanceId).sort(); assert.deepEqual(ids, ["win-a", "win-b"]); } finally { rmSync(home, { recursive: true, force: true }); } }); test("pruneInstances removes entries whose PID is dead", async () => { const home = instanceHome(); try { // 0x7fffffff is not a valid Windows PID, so it cannot be alive. await writeInstance(makeInstance("alive", process.pid), home); await writeInstance(makeInstance("dead", 0x7fffffff), home); const result = await pruneInstances(home); assert.deepEqual(result.removed, ["dead"]); assert.deepEqual( result.kept.map((i) => i.instanceId), ["alive"], ); // The dead file must actually be gone from disk. assert.equal(existsSync(join(instancesDir(home), instanceFileName("dead"))), false); assert.equal((await readInstances(home)).length, 1); } finally { rmSync(home, { recursive: true, force: true }); } }); test("pruneInstances keeps entries with an unknown PID", async () => { const home = instanceHome(); try { // An older/simpler instance file without processId must never be pruned. await writeInstance( { instanceId: "no-pid", url: "http://127.0.0.1:1", token: "t" }, home, ); const result = await pruneInstances(home); assert.deepEqual(result.removed, []); assert.equal(result.kept.length, 1); } finally { rmSync(home, { recursive: true, force: true }); } }); test("clearInstance only removes its own file", async () => { const home = instanceHome(); try { await writeInstance(makeInstance("mine"), home); await writeInstance(makeInstance("theirs"), home); await clearInstance("mine", home); const ids = (await readInstances(home)).map((i) => i.instanceId); assert.deepEqual(ids, ["theirs"]); } finally { rmSync(home, { recursive: true, force: true }); } }); test("readInstances tolerates corrupt and unrelated files", async () => { const home = instanceHome(); try { await writeInstance(makeInstance("good"), home); writeFileSync(join(instancesDir(home), "broken.json"), "{ not json"); writeFileSync(join(instancesDir(home), "notes.txt"), "ignore me"); const all = await readInstances(home); assert.deepEqual(all.map((i) => i.instanceId), ["good"]); } finally { rmSync(home, { recursive: true, force: true }); } }); test("projectsOf unions projects across instances without duplicates", () => { const projects = projectsOf([ { instanceId: "a", url: "u", token: "t", projectDir: "D:/Mods/Alpha", projects: ["D:/Mods/Alpha"] }, { instanceId: "b", url: "u", token: "t", projectDir: "D:/Mods/Beta", projects: ["D:/Mods/beta", "D:/Mods/Gamma"] }, ]); // "beta" appears twice with different casing and must collapse to one entry, // keeping the first spelling seen. assert.deepEqual(projects, ["D:/Mods/Alpha", "D:/Mods/beta", "D:/Mods/Gamma"]); }); test("writeManifest produces a discovery manifest without tokens", async () => { const home = instanceHome(); try { const manifest = await writeManifest( [makeInstance("a-1"), makeInstance("b-2")], home, ); assert.equal(manifest.schemaVersion, INSTANCE_SCHEMA_VERSION); assert.deepEqual(manifest.projects, ["D:/Mods/Alpha"]); assert.equal(manifest.instances.length, 2); const raw = readFileSync(manifestPath(home), "utf8"); // The manifest is for discovery; secrets must not leak into it. assert.equal(raw.includes("tok-a-1"), false); assert.equal(raw.includes('"token"'), false); const reread = await readManifest(home); assert.equal(reread?.instances.length, 2); } finally { rmSync(home, { recursive: true, force: true }); } }); test("makeInstanceId is unique across rapid calls", () => { const ids = new Set(); for (let i = 0; i < 200; i++) ids.add(makeInstanceId(1234)); assert.equal(ids.size, 200); for (const id of ids) assert.ok(id.startsWith("1234-"), id); }); test("refreshDiscovery keeps surviving windows discoverable", async () => { const home = instanceHome(); try { const mine = makeInstance("mine"); const other = { ...makeInstance("other"), url: "http://127.0.0.1:19999", token: "tok-other", projectDir: "D:/Mods/Beta", projects: ["D:/Mods/Beta"], }; await writeInstance(mine, home); await writeInstance(other, home); // This window closes: only its own instance file goes away. await clearInstance(mine.instanceId, home); const manifest = await refreshDiscovery(home); assert.deepEqual(manifest.projects, ["D:/Mods/Beta"]); assert.deepEqual(manifest.instances.map((i) => i.instanceId), ["other"]); // The legacy global pointer must follow the survivor, not be deleted. const endpoint = await readEndpoint(home); assert.equal(endpoint?.url, other.url); assert.equal(endpoint?.token, other.token); assert.equal(existsSync(endpointPath(home)), true); // The merged manifest on disk must agree with the returned value. const reread = await readManifest(home); assert.deepEqual(reread?.instances.map((i) => i.instanceId), ["other"]); } finally { rmSync(home, { recursive: true, force: true }); } }); test("refreshDiscovery clears the global pointer when the last window closes", async () => { const home = instanceHome(); try { await writeInstance(makeInstance("only"), home); await clearInstance("only", home); const manifest = await refreshDiscovery(home); assert.deepEqual(manifest.instances, []); assert.deepEqual(manifest.projects, []); assert.equal(existsSync(endpointPath(home)), false); } finally { rmSync(home, { recursive: true, force: true }); } });