import { test } from "node:test"; import assert from "node:assert/strict"; import { startLocalServer } from "../out/agent/localServer.js"; import { parseLoadedXml } from "../out/agent/forwardRefs.js"; const PROJECT_A = "D:/Mods/Example"; const PROJECT_B = "D:/Mods/Other"; function makeStats() { return { projectDir: "P", sdkDir: "", phase: "art", complete: true, indexedFiles: 1, parsedFiles: 1, shallowScannedFiles: 0, deferredArtFiles: 0, shallowCacheHits: 0, recordsCacheHits: 0, resolveCacheHits: 0, resolveCalls: 0, snapshotHits: 0, snapshotFallbacks: 0, candidatesMs: 0, walkMs: 0, artScanMs: 0, assetCount: 1, referenceCount: 1, defineCount: 1, manifestFiles: 0, manifestAssetCount: 0, streams: 1, sourceCandidates: 1, elapsedMs: 1, }; } const CANNON_XML = ` AthenaCannon_Die `; const BASE_XML = ` `; const FILES = { "D:/Mods/Example/Data/AthenaCannon.xml": CANNON_XML, "D:/Mods/Example/Data/BaseCannon.xml": BASE_XML, }; function makeIndex(projectDir, unitId, extraDefs = []) { const file = `${projectDir}/Data/${unitId}.xml`; const unit = { type: "GameObject", id: unitId, file, line: 2, origin: "project", stream: "static" }; const all = [unit, ...extraDefs]; const assets = new Map(); const assetsById = new Map(); for (const d of all) { if (!assets.has(d.type)) assets.set(d.type, new Map()); const byId = assets.get(d.type); if (!byId.has(d.id.toLowerCase())) byId.set(d.id.toLowerCase(), []); byId.get(d.id.toLowerCase()).push(d); const key = d.id.toLowerCase(); if (!assetsById.has(key)) assetsById.set(key, []); assetsById.get(key).push(d); } return { projectDir, sdkDir: "", complete: true, phase: "art", stale: false, assets, assetsById, defines: new Map([ ["exampledefine", [{ name: "ExampleDefine", value: "1", file, line: 2, origin: "project" }]], ]), files: new Map(), streams: [ { name: "static", entry: `${projectDir}/Data/Mod.xml`, files: new Set([file.toLowerCase().replace(/\\/g, "/")]), }, ], manifests: new Map(), sourceCandidates: [], diagnostics: [], references: new Map([ [ `GameObject\u0000${unitId.toLowerCase()}\u0000${file}\u00002`, [{ file: `${projectDir}/Data/Other.xml`, line: 3, start: 1, end: 2, kind: "attr" }], ], ]), recordsHashes: new Map(), stats: makeStats(), }; } const PROJECT_A_RELATED = [ { type: "GameObject", id: "BaseCannon", file: "D:/Mods/Example/Data/BaseCannon.xml", line: 2, origin: "project", stream: "static", }, { type: "GameObject", id: "AthenaCannon_Die", file: "D:/Mods/Example/Data/AthenaCannon_Die.xml", line: 2, origin: "project", stream: "static", }, { type: "WeaponTemplate", id: "AthenaCannonWeapon", file: "D:/Mods/Example/Data/Weapon.xml", line: 88, origin: "project", stream: "static", }, ]; /** Routes to a distinct index per requested project, like the extension does. */ function routedServerOptions() { const indexes = new Map([ [PROJECT_A, makeIndex(PROJECT_A, "AthenaCannon", PROJECT_A_RELATED)], [PROJECT_B, makeIndex(PROJECT_B, "OtherUnit")], ]); return { getIndex: (projectDir) => (projectDir ? indexes.get(projectDir) ?? null : indexes.get(PROJECT_A)), listProjects: () => [...indexes.keys()], loadFile: async (file) => { const text = FILES[file]; return text ? parseLoadedXml(text) : null; }, }; } async function withServer(fn) { const handle = await startLocalServer({ ...routedServerOptions(), token: "test-token" }); const base = `http://127.0.0.1:${handle.port}`; const headers = { authorization: "Bearer test-token" }; const get = async (path) => (await fetch(`${base}${path}`, { headers })).json(); try { await fn({ base, headers, get }); } finally { await handle.close(); } } test("local server requires the bearer token", async () => { await withServer(async ({ base }) => { const unauthorized = await fetch(`${base}/status`); assert.equal(unauthorized.status, 401); const forbidden = await fetch(`${base}/status`, { headers: { authorization: "Bearer wrong" }, }); assert.equal(forbidden.status, 401); }); }); test("local server exposes read-only queries", async () => { await withServer(async ({ get }) => { const status = await get(`/status?project=${encodeURIComponent(PROJECT_A)}`); assert.equal(status.state, "ready"); assert.equal(status.projectDir, PROJECT_A); const asset = await get( `/find_asset?project=${encodeURIComponent(PROJECT_A)}&id=AthenaCannon&type=GameObject`, ); assert.equal(asset.data.length, 1); const refs = await get( `/find_references?project=${encodeURIComponent(PROJECT_A)}&id=AthenaCannon`, ); assert.equal(refs.data.length, 1); const active = await get( `/is_file_active?project=${encodeURIComponent(PROJECT_A)}&path=D:/Mods/Example/Data/AthenaCannon.xml`, ); assert.equal(active.data.active, true); }); }); test("?project= selects the index instead of the active editor's project", async () => { await withServer(async ({ get }) => { const a = await get(`/find_asset?project=${encodeURIComponent(PROJECT_A)}&id=AthenaCannon`); assert.equal(a.index.projectDir, PROJECT_A); assert.equal(a.data.length, 1); const b = await get(`/find_asset?project=${encodeURIComponent(PROJECT_B)}&id=OtherUnit`); assert.equal(b.index.projectDir, PROJECT_B); assert.equal(b.data.length, 1); // The same id must not resolve when asked about the other project. const cross = await get( `/find_asset?project=${encodeURIComponent(PROJECT_B)}&id=AthenaCannon`, ); assert.equal(cross.data.length, 0); assert.equal(cross.index.projectDir, PROJECT_B); }); }); test("an unknown project reports no_index without faking a projectDir", async () => { await withServer(async ({ get }) => { const unknown = "D:/Mods/Unknown"; const result = await get(`/status?project=${encodeURIComponent(unknown)}`); assert.equal(result.state, "no_index"); assert.equal(result.projectDir, unknown); }); }); test("/projects lists the known roots", async () => { await withServer(async ({ get }) => { const result = await get("/projects"); assert.deepEqual(new Set(result.data), new Set([PROJECT_A, PROJECT_B])); }); }); test("/get_asset_references returns provenance-carrying edges", async () => { await withServer(async ({ get }) => { const result = await get( `/get_asset_references?project=${encodeURIComponent(PROJECT_A)}&id=AthenaCannon&type=GameObject`, ); assert.equal(result.index.state, "ready"); const data = result.data; assert.ok(data, "expected a data payload"); assert.equal(data.edges.length >= 3, true); const weapon = data.edges.find((e) => e.to?.id === "AthenaCannonWeapon"); assert.ok(weapon, "expected the inherited weapon edge"); assert.equal(weapon.via.element, "Weapon"); assert.equal(weapon.via.parent, "WeaponSlotHardpoint"); assert.equal(weapon.definedIn.id, "BaseCannon"); assert.equal(weapon.source.file, "D:/Mods/Example/Data/BaseCannon.xml"); const die = data.edges.find((e) => e.via.kind === "content"); assert.ok(die, "expected the CreateObjectDie content edge"); assert.equal(die.to.id, "AthenaCannon_Die"); }); }); test("/get_asset_references honours targetTypes and depth parameters", async () => { await withServer(async ({ get }) => { const filtered = await get( `/get_asset_references?project=${encodeURIComponent(PROJECT_A)}&id=AthenaCannon&targetTypes=WeaponTemplate`, ); for (const edge of filtered.data.edges) { if (edge.via.kind === "inheritFrom") continue; assert.equal(edge.to.type, "WeaponTemplate"); } const shallow = await get( `/get_asset_references?project=${encodeURIComponent(PROJECT_A)}&id=AthenaCannon&depth=1`, ); for (const edge of shallow.data.edges) { assert.equal(edge.depth, 1); } const deep = await get( `/get_asset_references?project=${encodeURIComponent(PROJECT_A)}&id=AthenaCannon&depth=2`, ); for (const edge of deep.data.edges) { assert.ok(edge.depth <= 2); } }); }); test("/get_asset_references explains itself when live data is unavailable", async () => { await withServer(async ({ get }) => { const missing = await get(`/get_asset_references?project=${encodeURIComponent(PROJECT_A)}&id=Nope`); assert.equal(missing.data.roots.length, 0); assert.ok(missing.data.warnings.length > 0); }); });