ai agent
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user