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, writeInstance, writeManifest, } from "../out/agent/instances.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); });