// 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); });