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>
109 lines
4.2 KiB
JavaScript
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);
|
|
});
|