Files
unreal-engine-mcp-system-pl…/test/insights.test.js
Bonchellon eba71c4ca8 Unified, portable Unreal Engine MCP system plugin
Merge of the two project copies into one self-contained plugin (the superset:
variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the
CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring).

Made fully project- and machine-agnostic — no hardcoded paths:
- New src/projectPaths.js auto-detects the host .uproject (walk-up), project
  name, Editor build target, log file, and engine install (EngineAssociation
  via launcher manifest/registry, else installed-engine scan). All overridable
  via UE_* env vars.
- Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the
  Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old
  C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver.
- Voxel made optional: Build.cs auto-detects the Voxel plugin (env
  UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL,
  so the module builds in projects without Voxel; .uplugin marks Voxel optional.
- De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded
  node_modules/Binaries/Intermediate/__pycache__/secrets from the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 01:07:24 +03:00

109 lines
4.2 KiB
JavaScript

// Structural tests for the Insights MCP layer.
// We synthesise minimal Insights-style TSVs in a tmp dir, run the ingester
// + analytics handlers directly, and assert the shapes. The full e2e path
// (UnrealInsights.exe export) is not exercised here — that needs a real
// .utrace and an installed Insights binary.
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { dbAvailable, openDb, ingestRawDir, listTables, tableColumns, parseTabular } from "../src/insightsDb.js";
function mkTmp() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "insights-mcp-test-"));
return dir;
}
function writeTsv(dir, name, header, rows) {
const text = [header.join("\t"), ...rows.map((r) => r.join("\t"))].join("\n");
fs.writeFileSync(path.join(dir, name), text);
}
test("parseTabular: TSV with mixed types", async () => {
const dir = mkTmp();
const raw = path.join(dir, "raw");
fs.mkdirSync(raw);
writeTsv(raw, "events.tsv",
["Name", "ThreadName", "StartTime", "Duration"],
[
["UWorld::Tick", "GameThread", "0.0", "0.008"],
["BPP_Foo::Tick", "GameThread", "0.008", "0.003"],
["FrameTime", "GameThread", "0.0", "0.016"],
["FrameTime", "GameThread", "0.016", "0.052"], // a hitch
]
);
const parsed = await parseTabular(path.join(raw, "events.tsv"));
assert.deepEqual(parsed.headers, ["Name", "ThreadName", "StartTime", "Duration"]);
assert.equal(parsed.types[2], "REAL");
assert.equal(parsed.rows.length, 4);
});
test("ingestRawDir: creates tables, picks indexes", async (t) => {
if (!dbAvailable()) return t.skip("better-sqlite3 not installed yet (run npm install)");
const dir = mkTmp();
const raw = path.join(dir, "raw");
fs.mkdirSync(raw);
writeTsv(raw, "events.tsv",
["Name", "ThreadName", "StartTime", "Duration"],
[
["UWorld::Tick", "GameThread", "0.0", "0.008"],
["FrameTime", "GameThread", "0.0", "0.016"],
["FrameTime", "GameThread", "0.016", "0.052"],
]
);
writeTsv(raw, "memtags.tsv",
["Name", "Peak", "Current"],
[["Niagara", "180000000", "120000000"], ["Audio", "40000000", "30000000"]]
);
const db = openDb(dir);
try {
const summary = await ingestRawDir(db, raw);
assert.ok(summary.find((s) => s.table === "events" && s.rows === 3));
assert.ok(summary.find((s) => s.table === "memtags" && s.rows === 2));
const tables = listTables(db);
assert.ok(tables.includes("events"));
assert.ok(tables.includes("memtags"));
const cols = tableColumns(db, "events").map((c) => c.name);
assert.deepEqual(cols, ["Name", "ThreadName", "StartTime", "Duration"]);
} finally { db.close(); }
});
test("handleFrameStats: detects hitches over threshold", async (t) => {
if (!dbAvailable()) return t.skip("better-sqlite3 not installed yet");
const { handleOpenTrace, handleFrameStats } = await import("../src/insights.js");
// We bypass open_trace's real-export by manually staging the cache
// structure that open_trace would have produced.
const projectDir = mkTmp();
const traceId = "abcd1234deadbeef";
const dir = path.join(projectDir, "Saved/InsightsMCP", traceId);
fs.mkdirSync(path.join(dir, "raw"), { recursive: true });
writeTsv(path.join(dir, "raw"), "events.tsv",
["Name", "ThreadName", "StartTime", "Duration"],
[
["FrameTime", "GameThread", "0.000", "0.016"],
["FrameTime", "GameThread", "0.016", "0.017"],
["FrameTime", "GameThread", "0.033", "0.052"], // hitch
["FrameTime", "GameThread", "0.085", "0.018"],
["FrameTime", "GameThread", "0.103", "0.045"], // hitch
]
);
const db = openDb(dir);
try { await ingestRawDir(db, path.join(dir, "raw")); } finally { db.close(); }
// Write a minimal trace index entry.
fs.writeFileSync(
path.join(projectDir, "Saved/InsightsMCP/traces.json"),
JSON.stringify({ [traceId]: { label: "synthetic", trace_path: "synthetic.utrace" } })
);
const res = await handleFrameStats({ trace_id: traceId, hitch_ms: 33, project_dir: projectDir });
assert.equal(res.isError, undefined, res.content?.[0]?.text);
const out = JSON.parse(res.content[0].text);
assert.equal(out.frames, 5);
assert.equal(out.hitch_count, 2);
assert.ok(out.worst_hitches[0].ms >= 50);
});