206 lines
6.2 KiB
JavaScript
206 lines
6.2 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import {
|
|
LiveClient,
|
|
findEndpoint,
|
|
liveUrlForTool,
|
|
normalizePath,
|
|
queryLive,
|
|
responseProjectMismatch,
|
|
} from "../out/agent/liveClient.js";
|
|
import { writeInstance } from "../out/agent/instances.js";
|
|
import { writeEndpoint, writeEndpointForProject } from "../out/agent/endpoint.js";
|
|
|
|
const PROJECT_A = "D:/Mods/Alpha";
|
|
const PROJECT_B = "D:/Mods/Beta";
|
|
|
|
function home() {
|
|
return mkdtempSync(join(tmpdir(), "ra3-liveclient-"));
|
|
}
|
|
|
|
test("live URLs pin the requested project for every tool", () => {
|
|
const cases = [
|
|
["get_status", {}],
|
|
["find_asset", { id: "X" }],
|
|
["find_references", { id: "X" }],
|
|
["list_assets_by_type", { type: "GameObject" }],
|
|
["is_file_active", { path: "D:/f.xml" }],
|
|
["find_define", { name: "D" }],
|
|
["resolve_include", { source: "DATA:a.xml" }],
|
|
["list_projects", {}],
|
|
];
|
|
for (const [tool, args] of cases) {
|
|
const url = liveUrlForTool("http://127.0.0.1:1234", PROJECT_A, tool, args);
|
|
assert.ok(url, `${tool} should have a live URL`);
|
|
assert.equal(
|
|
new URL(url).searchParams.get("project"),
|
|
PROJECT_A,
|
|
`${tool} must pin the project`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("get_asset_references serialises all of its options", () => {
|
|
const url = liveUrlForTool("http://127.0.0.1:1234", PROJECT_A, "get_asset_references", {
|
|
id: "AthenaCannon",
|
|
type: "GameObject",
|
|
depth: 2,
|
|
targetTypes: ["WeaponTemplate", "GameObject"],
|
|
maxEdges: 25,
|
|
includeUnresolved: true,
|
|
});
|
|
const q = new URL(url).searchParams;
|
|
assert.equal(q.get("id"), "AthenaCannon");
|
|
assert.equal(q.get("type"), "GameObject");
|
|
assert.equal(q.get("depth"), "2");
|
|
assert.equal(q.get("targetTypes"), "WeaponTemplate,GameObject");
|
|
assert.equal(q.get("maxEdges"), "25");
|
|
assert.equal(q.get("includeUnresolved"), "true");
|
|
assert.equal(q.get("project"), PROJECT_A);
|
|
});
|
|
|
|
test("unknown tools have no live URL", () => {
|
|
assert.equal(liveUrlForTool("http://127.0.0.1:1", PROJECT_A, "not_a_tool", {}), null);
|
|
});
|
|
|
|
test("responseProjectMismatch refuses another project's answer", () => {
|
|
assert.equal(
|
|
responseProjectMismatch({ index: { projectDir: "d:/mods/alpha" } }, PROJECT_A),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
responseProjectMismatch({ index: { projectDir: PROJECT_B } }, PROJECT_A),
|
|
true,
|
|
);
|
|
// Nothing to compare against: not a mismatch.
|
|
assert.equal(responseProjectMismatch({ index: { state: "ready" } }, PROJECT_A), false);
|
|
assert.equal(responseProjectMismatch({ index: { projectDir: PROJECT_B } }, null), false);
|
|
});
|
|
|
|
test("normalizePath ignores case and trailing separators", () => {
|
|
assert.equal(normalizePath("D:\\Mods\\Alpha\\"), normalizePath("d:/mods/alpha"));
|
|
});
|
|
|
|
test("findEndpoint prefers the per-project endpoint", async () => {
|
|
const h = home();
|
|
try {
|
|
await writeEndpointForProject(
|
|
PROJECT_A,
|
|
{ url: "http://127.0.0.1:1111", token: "tok-a", processId: process.pid },
|
|
h,
|
|
);
|
|
const endpoint = await findEndpoint({ projectDir: PROJECT_A, agentHome: h });
|
|
assert.equal(endpoint?.url, "http://127.0.0.1:1111");
|
|
} finally {
|
|
rmSync(h, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("findEndpoint finds a live instance when no per-project file exists", async () => {
|
|
const h = home();
|
|
try {
|
|
// A window that has not yet written per-project endpoints, only its own
|
|
// instance file. Discovery must still find it.
|
|
await writeInstance(
|
|
{
|
|
instanceId: "w1",
|
|
url: "http://127.0.0.1:2222",
|
|
token: "tok-inst",
|
|
projects: [PROJECT_A, PROJECT_B],
|
|
processId: process.pid,
|
|
},
|
|
h,
|
|
);
|
|
const endpoint = await findEndpoint({ projectDir: PROJECT_B, agentHome: h });
|
|
assert.equal(endpoint?.url, "http://127.0.0.1:2222");
|
|
} finally {
|
|
rmSync(h, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("findEndpoint ignores instances that do not serve the project", async () => {
|
|
const h = home();
|
|
try {
|
|
await writeInstance(
|
|
{
|
|
instanceId: "other",
|
|
url: "http://127.0.0.1:3333",
|
|
token: "tok",
|
|
projects: ["D:/Mods/Unrelated"],
|
|
processId: process.pid,
|
|
},
|
|
h,
|
|
);
|
|
assert.equal(await findEndpoint({ projectDir: PROJECT_A, agentHome: h }), null);
|
|
} finally {
|
|
rmSync(h, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("findEndpoint ignores dead instances", async () => {
|
|
const h = home();
|
|
try {
|
|
await writeInstance(
|
|
{
|
|
instanceId: "dead",
|
|
url: "http://127.0.0.1:4444",
|
|
token: "tok",
|
|
projects: [PROJECT_A],
|
|
processId: 0x7fffffff,
|
|
},
|
|
h,
|
|
);
|
|
assert.equal(await findEndpoint({ projectDir: PROJECT_A, agentHome: h }), null);
|
|
} finally {
|
|
rmSync(h, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("findEndpoint does not use a global endpoint recording another project", async () => {
|
|
const h = home();
|
|
try {
|
|
await writeEndpoint(
|
|
{ url: "http://127.0.0.1:5555", token: "tok", projectDir: PROJECT_B, processId: process.pid },
|
|
h,
|
|
);
|
|
assert.equal(await findEndpoint({ projectDir: PROJECT_A, agentHome: h }), null);
|
|
// It is still usable when asked for its own project.
|
|
assert.ok(await findEndpoint({ projectDir: PROJECT_B, agentHome: h }));
|
|
} finally {
|
|
rmSync(h, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("a query with no reachable live instance returns null", async () => {
|
|
const h = home();
|
|
try {
|
|
assert.equal(
|
|
await queryLive("get_status", {}, { projectDir: PROJECT_A, agentHome: h }),
|
|
null,
|
|
);
|
|
} finally {
|
|
rmSync(h, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("the negative cache suppresses repeated attempts and can be reset", async () => {
|
|
let now = 1_000_000;
|
|
const client = new LiveClient({ projectDir: PROJECT_A, now: () => now });
|
|
assert.equal(client.suppressed, false);
|
|
client.markUnavailable();
|
|
assert.equal(client.suppressed, true);
|
|
// Still suppressed just before the cooldown expires.
|
|
now += 4999;
|
|
assert.equal(client.suppressed, true);
|
|
// Expired afterwards.
|
|
now += 2;
|
|
assert.equal(client.suppressed, false);
|
|
|
|
client.markUnavailable();
|
|
client.reset();
|
|
assert.equal(client.suppressed, false);
|
|
});
|