Initial checkin of Blueprint-MCP plugin

This commit is contained in:
2026-03-05 19:26:46 -05:00
parent 9cc1cb502b
commit 8367bd2221
4571 changed files with 1211887 additions and 7 deletions

View File

@@ -0,0 +1,225 @@
/**
* bootstrap.ts — Generate a temp UE5 project and manage the commandlet lifecycle.
*
* The temp project contains:
* TestProject.uproject — minimal JSON pointing at BlueprintMCP plugin
* Plugins/BlueprintMCP/ — directory junction to the real plugin source
* Content/ — empty; tests create Blueprints into /Game/Test/
*/
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { execSync, spawn, type ChildProcess } from "node:child_process";
/** Port used by the test commandlet (distinct from editor's 9847). */
export const TEST_PORT = 19847;
export const TEST_BASE_URL = `http://localhost:${TEST_PORT}`;
/** Absolute path to the plugin root (two levels up from test/). */
const PLUGIN_ROOT = path.resolve(import.meta.dirname, "..", "..");
let tempDir: string | null = null;
let cmdProcess: ChildProcess | null = null;
// ---------------------------------------------------------------------------
// Temp project generation
// ---------------------------------------------------------------------------
export function generateTempProject(): string {
const dir = path.join(os.tmpdir(), `BlueprintMCP_Test_${Date.now()}`);
fs.mkdirSync(dir, { recursive: true });
// Minimal .uproject — engine version must match the compiled plugin DLL
const engineVersion = detectEngineVersion();
const uproject = {
FileVersion: 3,
EngineAssociation: engineVersion,
Plugins: [{ Name: "BlueprintMCP", Enabled: true }],
};
fs.writeFileSync(
path.join(dir, "TestProject.uproject"),
JSON.stringify(uproject, null, "\t") + "\n",
);
// Content directory (blueprints land here)
fs.mkdirSync(path.join(dir, "Content"), { recursive: true });
// Plugins/BlueprintMCP → junction to real plugin
const pluginsDir = path.join(dir, "Plugins");
fs.mkdirSync(pluginsDir, { recursive: true });
const junctionTarget = path.join(pluginsDir, "BlueprintMCP");
// cmd /c mklink /J works without admin on Windows
execSync(`cmd /c mklink /J "${junctionTarget}" "${PLUGIN_ROOT}"`, {
stdio: "ignore",
});
tempDir = dir;
console.log(`[bootstrap] Temp project created at ${dir}`);
return dir;
}
// ---------------------------------------------------------------------------
// Commandlet lifecycle
// ---------------------------------------------------------------------------
/** Detect the UE engine version by scanning for installed engines (prefer newest). */
function detectEngineVersion(): string {
const base = "C:\\Program Files\\Epic Games";
try {
const dirs = fs.readdirSync(base).filter((d) => d.startsWith("UE_"));
// Sort descending so newest version is first
dirs.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
if (dirs.length > 0) {
return dirs[0].replace("UE_", "");
}
} catch { /* directory not found */ }
return "5.4"; // fallback
}
function findEditorCmd(): string | null {
if (process.env.UE_EDITOR_CMD && fs.existsSync(process.env.UE_EDITOR_CMD)) {
return process.env.UE_EDITOR_CMD;
}
// Scan for installed UE versions, preferring newest
const base = "C:\\Program Files\\Epic Games";
const bases = [base, "C:\\Program Files (x86)\\Epic Games"];
for (const b of bases) {
try {
const dirs = fs.readdirSync(b).filter((d) => d.startsWith("UE_"));
dirs.sort((a, bv) => bv.localeCompare(a, undefined, { numeric: true }));
for (const d of dirs) {
const cmd = path.join(b, d, "Engine", "Binaries", "Win64", "UnrealEditor-Cmd.exe");
if (fs.existsSync(cmd)) return cmd;
}
} catch { /* directory not found */ }
}
return null;
}
async function waitForHealth(timeoutMs: number = 240_000): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const resp = await fetch(`${TEST_BASE_URL}/api/health`, {
signal: AbortSignal.timeout(2000),
});
if (resp.ok) return true;
} catch {
// not ready yet
}
if (!cmdProcess) return false; // process died
await new Promise((r) => setTimeout(r, 2000));
}
return false;
}
export async function spawnCommandlet(projectDir: string): Promise<void> {
const editorCmd = findEditorCmd();
if (!editorCmd) {
throw new Error(
"UnrealEditor-Cmd.exe not found. Set UE_EDITOR_CMD env var.",
);
}
const uproject = path.join(projectDir, "TestProject.uproject");
const logPath = path.join(projectDir, "Saved", "Logs", "Test_server.log");
console.log(`[bootstrap] Spawning commandlet on port ${TEST_PORT}...`);
cmdProcess = spawn(
editorCmd,
[
uproject,
"-run=BlueprintMCP",
`-port=${TEST_PORT}`,
"-unattended",
"-nopause",
"-nullrhi",
`-LOG=${logPath}`,
],
{ stdio: ["ignore", "pipe", "pipe"], windowsHide: true },
);
cmdProcess.stdout?.on("data", (d: Buffer) => {
process.stderr.write(`[UE5:out] ${d.toString().trimEnd()}\n`);
});
cmdProcess.stderr?.on("data", (d: Buffer) => {
process.stderr.write(`[UE5:err] ${d.toString().trimEnd()}\n`);
});
cmdProcess.on("exit", (code) => {
console.log(`[bootstrap] Commandlet exited with code ${code}`);
cmdProcess = null;
});
console.log("[bootstrap] Waiting for health (up to 4 min)...");
const ok = await waitForHealth(240_000);
if (!ok) {
// Dump the log file if it exists
try {
const log = fs.readFileSync(logPath, "utf-8");
console.error("[bootstrap] === Commandlet log (last 80 lines) ===");
console.error(log.split("\n").slice(-80).join("\n"));
} catch { /* no log */ }
if (cmdProcess) {
cmdProcess.kill();
cmdProcess = null;
}
throw new Error("Commandlet failed to become healthy within 4 minutes.");
}
console.log("[bootstrap] Commandlet is healthy.");
}
// ---------------------------------------------------------------------------
// Shutdown & cleanup
// ---------------------------------------------------------------------------
export async function shutdownCommandlet(): Promise<void> {
if (!cmdProcess) return;
// Graceful HTTP shutdown
try {
await fetch(`${TEST_BASE_URL}/api/shutdown`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
signal: AbortSignal.timeout(3000),
});
} catch { /* may already be gone */ }
// Wait for exit
const proc = cmdProcess;
const exited = await new Promise<boolean>((resolve) => {
const timer = setTimeout(() => resolve(false), 15_000);
proc.on("exit", () => {
clearTimeout(timer);
resolve(true);
});
});
if (!exited && cmdProcess) {
console.log("[bootstrap] Force-killing commandlet.");
cmdProcess.kill();
cmdProcess = null;
}
}
export function cleanupTempProject(): void {
if (!tempDir) return;
const junctionPath = path.join(tempDir, "Plugins", "BlueprintMCP");
// Remove the junction first — rmdir removes only the junction, not the target
try {
execSync(`cmd /c rmdir "${junctionPath}"`, { stdio: "ignore" });
} catch { /* may already be gone */ }
// Remove the rest of the temp directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
console.log(`[bootstrap] Cleaned up ${tempDir}`);
} catch (e) {
console.error(`[bootstrap] Warning: could not clean up ${tempDir}:`, e);
}
tempDir = null;
}

View File

@@ -0,0 +1,151 @@
/**
* helpers.ts — HTTP wrappers and fixture management for integration tests.
*/
import { TEST_BASE_URL } from "./bootstrap.js";
// ---------------------------------------------------------------------------
// HTTP helpers (mirror the production ueGet/uePost in src/index.ts)
// ---------------------------------------------------------------------------
export async function ueGet(
endpoint: string,
params: Record<string, string> = {},
): Promise<any> {
const url = new URL(endpoint, TEST_BASE_URL);
for (const [k, v] of Object.entries(params)) {
if (v) url.searchParams.set(k, v);
}
const resp = await fetch(url.toString(), {
signal: AbortSignal.timeout(30_000),
});
return resp.json();
}
export async function uePost(
endpoint: string,
body: Record<string, any>,
): Promise<any> {
const resp = await fetch(`${TEST_BASE_URL}${endpoint}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
return resp.json();
}
// ---------------------------------------------------------------------------
// Fixture helpers
// ---------------------------------------------------------------------------
/** Generate a unique Blueprint name using a prefix and timestamp. */
export function uniqueName(prefix: string): string {
return `${prefix}_${Date.now()}`;
}
/**
* Create a test Blueprint via the HTTP API.
* Returns the full JSON response from /api/create-blueprint.
*/
export async function createTestBlueprint(opts: {
name: string;
packagePath?: string;
parentClass?: string;
blueprintType?: string;
}): Promise<any> {
return uePost("/api/create-blueprint", {
blueprintName: opts.name,
packagePath: opts.packagePath ?? "/Game/Test",
parentClass: opts.parentClass ?? "Actor",
blueprintType: opts.blueprintType ?? "Normal",
});
}
/**
* Delete a test Blueprint via the HTTP API (force = true to skip reference checks).
* Returns the full JSON response from /api/delete-asset.
*/
export async function deleteTestBlueprint(
assetPath: string,
): Promise<any> {
return uePost("/api/delete-asset", {
assetPath,
force: true,
});
}
/**
* Create a test Material via the HTTP API.
*/
export async function createTestMaterial(opts: {
name: string;
packagePath?: string;
domain?: string;
blendMode?: string;
}): Promise<any> {
return uePost("/api/create-material", {
name: opts.name,
packagePath: opts.packagePath ?? "/Game/Test",
domain: opts.domain ?? "Surface",
blendMode: opts.blendMode ?? "Opaque",
});
}
/**
* Delete a test Material via the HTTP API.
*/
export async function deleteTestMaterial(assetPath: string): Promise<any> {
return uePost("/api/delete-asset", {
assetPath,
force: true,
});
}
/**
* Create a test Material Instance via the HTTP API.
*/
export async function createTestMaterialInstance(opts: {
name: string;
packagePath?: string;
parentMaterial: string;
}): Promise<any> {
return uePost("/api/create-material-instance", {
name: opts.name,
packagePath: opts.packagePath ?? "/Game/Test",
parentMaterial: opts.parentMaterial,
});
}
/**
* Create a test Material Function via the HTTP API.
*/
export async function createTestMaterialFunction(opts: {
name: string;
packagePath?: string;
description?: string;
}): Promise<any> {
return uePost("/api/create-material-function", {
name: opts.name,
packagePath: opts.packagePath ?? "/Game/Test",
description: opts.description ?? "",
});
}
/**
* Create a test Animation Blueprint via the HTTP API.
*/
export async function createTestAnimBlueprint(opts: {
name: string;
packagePath?: string;
skeleton?: string;
parentClass?: string;
}): Promise<any> {
return uePost("/api/create-anim-blueprint", {
name: opts.name,
packagePath: opts.packagePath ?? "/Game/Test",
skeleton: opts.skeleton ?? "__create_test_skeleton__",
parentClass: opts.parentClass ?? "AnimInstance",
});
}

View File

@@ -0,0 +1,36 @@
/**
* setup.ts — Vitest globalSetup.
*
* Before any test file runs:
* 1. Generate a temp UE5 project with a junction to the real plugin
* 2. Spawn a commandlet on port 19847 and wait until healthy
*
* After all tests complete:
* 3. Shut down the commandlet
* 4. Remove the junction and temp directory
*/
import {
generateTempProject,
spawnCommandlet,
shutdownCommandlet,
cleanupTempProject,
} from "./bootstrap.js";
export async function setup(): Promise<void> {
console.log("[setup] Generating temp project...");
const projectDir = generateTempProject();
console.log("[setup] Spawning commandlet...");
await spawnCommandlet(projectDir);
console.log("[setup] Ready.");
}
export async function teardown(): Promise<void> {
console.log("[teardown] Shutting down commandlet...");
await shutdownCommandlet();
console.log("[teardown] Cleaning up temp project...");
cleanupTempProject();
console.log("[teardown] Done.");
}

View File

@@ -0,0 +1,243 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("add_node", () => {
const bpName = uniqueName("BP_AddNodeTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("adds a function call node (PrintString)", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.nodeType).toBe("CallFunction");
expect(data.saved).toBe(true);
});
it("adds an event override node (BeginPlay)", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "OverrideEvent",
functionName: "ReceiveBeginPlay",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
// A fresh Actor BP already has ReceiveBeginPlay, so the server
// returns the existing node with alreadyExists=true and no saved field.
if (data.alreadyExists) {
expect(data.alreadyExists).toBe(true);
} else {
expect(data.saved).toBe(true);
}
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
// missing graph and nodeType
});
expect(data.error).toBeDefined();
});
it("rejects non-existent blueprint", async () => {
const data = await uePost("/api/add-node", {
blueprint: "BP_Nonexistent_XYZ_999",
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(data.error).toBeDefined();
});
it("rejects non-existent graph", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "FakeGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(data.error).toBeDefined();
expect(data.availableGraphs).toBeDefined();
});
// --- New node types ---
it("adds a Branch node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Branch",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
// Branch should have Condition input pin and True/False output pins
const pins = data.node?.pins || data.pins || [];
const pinNames = pins.map((p: any) => p.name);
expect(pinNames).toContain("Condition");
});
it("adds a Sequence node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Sequence",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a CustomEvent node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CustomEvent",
eventName: "TestCustomEvent",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("rejects CustomEvent without eventName", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CustomEvent",
});
expect(data.error).toBeDefined();
});
it("adds a ForEachLoop node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "ForEachLoop",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a ForLoop node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "ForLoop",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a ForLoopWithBreak node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "ForLoopWithBreak",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a WhileLoop node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "WhileLoop",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a SpawnActorFromClass node without actorClass", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "SpawnActorFromClass",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a Select node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Select",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a Comment node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Comment",
comment: "Test Section",
width: 500,
height: 300,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a Comment node with defaults", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Comment",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("adds a Reroute node", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Reroute",
posX: 300,
posY: 200,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
});

View File

@@ -0,0 +1,255 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestAnimBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("animation blueprint mutation tools", () => {
const bpName = uniqueName("ABP_MutationTest");
const packagePath = "/Game/Test";
let stateMachineGraph: string;
beforeAll(async () => {
const result = await createTestAnimBlueprint({ name: bpName });
expect(result.error).toBeUndefined();
expect(result.success).toBe(true);
// Add a state machine to get a state machine graph
const smResult = await uePost("/api/add-state-machine", { blueprint: bpName });
expect(smResult.error).toBeUndefined();
stateMachineGraph = smResult.stateMachineGraph;
expect(stateMachineGraph).toBeDefined();
}, 60_000);
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
}, 30_000);
describe("create_anim_blueprint", () => {
it("creates animation blueprint", async () => {
const name = uniqueName("ABP_CreateTest");
const result = await uePost("/api/create-anim-blueprint", {
name,
packagePath,
skeleton: "__create_test_skeleton__",
});
expect(result.error).toBeUndefined();
expect(result.success).toBe(true);
expect(result.isAnimBlueprint).toBe(true);
expect(result.targetSkeleton).toBeDefined();
// Cleanup
await deleteTestBlueprint(`${packagePath}/${name}`);
});
it("rejects missing required fields", async () => {
const result = await uePost("/api/create-anim-blueprint", {});
expect(result.error).toBeDefined();
});
it("rejects non-existent skeleton", async () => {
const result = await uePost("/api/create-anim-blueprint", {
name: uniqueName("ABP_BadSkel"),
packagePath,
skeleton: "NonExistentSkeleton_XYZ_999",
});
expect(result.error).toBeDefined();
});
});
describe("add_anim_state", () => {
it("adds a state to the state machine", async () => {
const data = await uePost("/api/add-anim-state", {
blueprint: bpName,
graph: stateMachineGraph,
stateName: "IdleState",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.stateName).toBe("IdleState");
expect(data.nodeId).toBeDefined();
});
it("rejects duplicate state name", async () => {
const data = await uePost("/api/add-anim-state", {
blueprint: bpName,
graph: stateMachineGraph,
stateName: "IdleState",
});
expect(data.error).toBeDefined();
});
it("rejects non-ABP blueprint", async () => {
const data = await uePost("/api/add-anim-state", {
blueprint: "BP_NonexistentXYZ_999",
graph: "SomeGraph",
stateName: "Test",
});
expect(data.error).toBeDefined();
});
});
describe("add_anim_transition", () => {
beforeAll(async () => {
// Add a second state so we can create a transition
const data = await uePost("/api/add-anim-state", {
blueprint: bpName,
graph: stateMachineGraph,
stateName: "RunState",
posX: 400,
});
expect(data.error).toBeUndefined();
});
it("adds a transition between states", async () => {
const data = await uePost("/api/add-anim-transition", {
blueprint: bpName,
graph: stateMachineGraph,
fromState: "IdleState",
toState: "RunState",
crossfadeDuration: 0.25,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.fromState).toBe("IdleState");
expect(data.toState).toBe("RunState");
expect(data.nodeId).toBeDefined();
});
it("rejects non-existent states", async () => {
const data = await uePost("/api/add-anim-transition", {
blueprint: bpName,
graph: stateMachineGraph,
fromState: "NonExistentState",
toState: "RunState",
});
expect(data.error).toBeDefined();
});
});
describe("set_transition_rule", () => {
it("updates transition properties", async () => {
const data = await uePost("/api/set-transition-rule", {
blueprint: bpName,
graph: stateMachineGraph,
fromState: "IdleState",
toState: "RunState",
crossfadeDuration: 0.5,
priorityOrder: 2,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.crossfadeDuration).toBe(0.5);
expect(data.priorityOrder).toBe(2);
});
it("rejects non-existent transition", async () => {
const data = await uePost("/api/set-transition-rule", {
blueprint: bpName,
graph: stateMachineGraph,
fromState: "RunState",
toState: "IdleState",
crossfadeDuration: 0.1,
});
expect(data.error).toBeDefined();
});
});
describe("remove_anim_state", () => {
it("removes state and connected transitions", async () => {
// Add a temporary state and transition
await uePost("/api/add-anim-state", {
blueprint: bpName,
graph: stateMachineGraph,
stateName: "TempState",
posX: 600,
});
await uePost("/api/add-anim-transition", {
blueprint: bpName,
graph: stateMachineGraph,
fromState: "IdleState",
toState: "TempState",
});
const data = await uePost("/api/remove-anim-state", {
blueprint: bpName,
graph: stateMachineGraph,
stateName: "TempState",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.removedState).toBe("TempState");
expect(data.removedTransitions).toBeGreaterThanOrEqual(1);
});
});
describe("add_anim_node", () => {
it("adds a SequencePlayer node", async () => {
const data = await uePost("/api/add-anim-node", {
blueprint: bpName,
nodeType: "SequencePlayer",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeType).toBe("SequencePlayer");
expect(data.nodeId).toBeDefined();
});
it("adds a StateMachine node with sub-graph", async () => {
const data = await uePost("/api/add-anim-node", {
blueprint: bpName,
nodeType: "StateMachine",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.stateMachineGraph).toBeDefined();
});
it("rejects unsupported nodeType", async () => {
const data = await uePost("/api/add-anim-node", {
blueprint: bpName,
nodeType: "InvalidType",
});
expect(data.error).toBeDefined();
});
});
describe("add_state_machine", () => {
it("adds state machine to AnimGraph", async () => {
const data = await uePost("/api/add-state-machine", {
blueprint: bpName,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
});
});
describe("list_anim_slots", () => {
it("returns slot list", async () => {
const data = await uePost("/api/list-anim-slots", {
blueprint: bpName,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.slots).toBeDefined();
expect(Array.isArray(data.slots)).toBe(true);
});
it("rejects non-ABP", async () => {
const data = await uePost("/api/list-anim-slots", {
blueprint: "BP_NonexistentXYZ_999",
});
expect(data.error).toBeDefined();
});
});
describe("list_sync_groups", () => {
it("returns sync group list", async () => {
const data = await uePost("/api/list-sync-groups", {
blueprint: bpName,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.syncGroups).toBeDefined();
expect(Array.isArray(data.syncGroups)).toBe(true);
});
});
});

View File

@@ -0,0 +1,36 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { ueGet, uePost, createTestAnimBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("animation blueprint read tools", () => {
const bpName = uniqueName("ABP_ReadTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const result = await createTestAnimBlueprint({ name: bpName });
expect(result.error).toBeUndefined();
expect(result.success).toBe(true);
expect(result.isAnimBlueprint).toBe(true);
}, 60_000);
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
}, 30_000);
it("get_blueprint returns isAnimBlueprint and targetSkeleton", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
expect(data.isAnimBlueprint).toBe(true);
expect(data.targetSkeleton).toBeDefined();
});
it("get_blueprint_graph returns graphType for AnimGraph", async () => {
const data = await ueGet("/api/graph", { name: bpName, graph: "AnimGraph" });
expect(data.error).toBeUndefined();
expect(data.graphType).toBe("AnimGraph");
});
it("returns error for non-existent blueprint", async () => {
const data = await ueGet("/api/blueprint", { name: "ABP_NonexistentXYZ_999" });
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,115 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("batch set_pin_default", () => {
const bpName = uniqueName("BP_BatchPinTest");
const packagePath = "/Game/Test";
let printNodeId1: string;
let printNodeId2: string;
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
// Add two PrintString nodes
const n1 = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(n1.success).toBe(true);
printNodeId1 = n1.nodeId;
const n2 = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(n2.success).toBe(true);
printNodeId2 = n2.nodeId;
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
// Verify single mode still works
it("sets a single pin default (backwards compatible)", async () => {
const data = await uePost("/api/set-pin-default", {
blueprint: bpName,
nodeId: printNodeId1,
pinName: "InString",
value: "Hello World",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.newValue).toBe("Hello World");
expect(data.saved).toBe(true);
});
it("sets multiple pin defaults in batch", async () => {
const data = await uePost("/api/set-pin-default", {
batch: [
{ blueprint: bpName, nodeId: printNodeId1, pinName: "InString", value: "Batch Value 1" },
{ blueprint: bpName, nodeId: printNodeId2, pinName: "InString", value: "Batch Value 2" },
],
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.successCount).toBe(2);
expect(data.totalCount).toBe(2);
expect(data.results).toHaveLength(2);
expect(data.results[0].success).toBe(true);
expect(data.results[1].success).toBe(true);
expect(data.saved).toBe(true);
});
it("handles errors in batch gracefully", async () => {
const data = await uePost("/api/set-pin-default", {
batch: [
{ blueprint: bpName, nodeId: printNodeId1, pinName: "InString", value: "Good" },
{ blueprint: bpName, nodeId: "00000000-0000-0000-0000-000000000000", pinName: "InString", value: "Bad" },
],
});
expect(data.error).toBeUndefined();
expect(data.successCount).toBe(1);
expect(data.totalCount).toBe(2);
expect(data.results[0].success).toBe(true);
expect(data.results[1].error).toBeDefined();
});
it("handles empty batch gracefully", async () => {
// Empty batch should fall through to single mode which will error on missing fields
const data = await uePost("/api/set-pin-default", {
batch: [],
});
// With empty batch, it should fall through to single mode error
expect(data.error).toBeDefined();
});
it("verifies batch values persist", async () => {
// First set values in batch
await uePost("/api/set-pin-default", {
batch: [
{ blueprint: bpName, nodeId: printNodeId1, pinName: "InString", value: "Persisted1" },
{ blueprint: bpName, nodeId: printNodeId2, pinName: "InString", value: "Persisted2" },
],
});
// Verify by reading the graph
const graph = await ueGet("/api/graph", { name: bpName, graph: "EventGraph" });
expect(graph.error).toBeUndefined();
const node1 = graph.nodes.find((n: any) => n.id === printNodeId1);
const node2 = graph.nodes.find((n: any) => n.id === printNodeId2);
expect(node1).toBeDefined();
expect(node2).toBeDefined();
const pin1 = node1.pins?.find((p: any) => p.name === "InString");
const pin2 = node2.pins?.find((p: any) => p.name === "InString");
if (pin1) expect(pin1.default).toBe("Persisted1");
if (pin2) expect(pin2.default).toBe("Persisted2");
});
});

View File

@@ -0,0 +1,66 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { ueGet, uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("get_blueprint_summary / describe_graph", () => {
const bpName = uniqueName("BP_InfoTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
// Add a PrintString node for describe_graph to walk
await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
describe("get_blueprint_summary (via /api/blueprint)", () => {
it("returns blueprint data with graphs and variables", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
expect(data.name).toBe(bpName);
expect(data.parentClass).toBeDefined();
expect(Array.isArray(data.graphs)).toBe(true);
expect(data.graphs.length).toBeGreaterThanOrEqual(1);
});
it("returns error for non-existent blueprint", async () => {
const data = await ueGet("/api/blueprint", {
name: "BP_Nonexistent_XYZ_999",
});
expect(data.error).toBeDefined();
});
});
describe("describe_graph (via /api/graph)", () => {
it("returns graph data with nodes", async () => {
const data = await ueGet("/api/graph", {
name: bpName,
graph: "EventGraph",
});
expect(data.error).toBeUndefined();
expect(Array.isArray(data.nodes)).toBe(true);
// Should have at least the PrintString node and default event nodes
expect(data.nodes.length).toBeGreaterThanOrEqual(1);
});
it("nodes have expected structure", async () => {
const data = await ueGet("/api/graph", {
name: bpName,
graph: "EventGraph",
});
expect(data.error).toBeUndefined();
for (const node of data.nodes) {
expect(node.id).toBeDefined();
expect(Array.isArray(node.pins)).toBe(true);
}
});
});
});

View File

@@ -0,0 +1,132 @@
import { describe, it, expect } from "vitest";
import { uePost } from "../helpers.js";
describe("class_discovery", () => {
// --- list_classes ---
it("lists classes with no filter", async () => {
const data = await uePost("/api/list-classes", { limit: 10 });
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.count).toBeGreaterThan(0);
expect(data.classes).toBeDefined();
expect(data.classes.length).toBeGreaterThan(0);
// Verify structure
const cls = data.classes[0];
expect(cls.name).toBeDefined();
expect(cls.isBlueprint).toBeDefined();
});
it("filters classes by name", async () => {
const data = await uePost("/api/list-classes", { filter: "Actor", limit: 20 });
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.count).toBeGreaterThan(0);
// Every returned class should contain "Actor" in the name
for (const cls of data.classes) {
expect(cls.name.toLowerCase()).toContain("actor");
}
});
it("filters classes by parent class", async () => {
const data = await uePost("/api/list-classes", { parentClass: "Actor", limit: 20 });
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.count).toBeGreaterThan(0);
});
it("returns error for non-existent parent class", async () => {
const data = await uePost("/api/list-classes", { parentClass: "FakeClass_XYZ_999" });
expect(data.error).toBeDefined();
});
it("respects limit parameter", async () => {
const data = await uePost("/api/list-classes", { limit: 5 });
expect(data.error).toBeUndefined();
expect(data.classes.length).toBeLessThanOrEqual(5);
});
// --- list_functions ---
it("lists functions on KismetSystemLibrary", async () => {
const data = await uePost("/api/list-functions", { className: "KismetSystemLibrary" });
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.count).toBeGreaterThan(0);
// Should contain PrintString
const names = data.functions.map((f: any) => f.name);
expect(names).toContain("PrintString");
});
it("filters functions by name", async () => {
const data = await uePost("/api/list-functions", {
className: "KismetSystemLibrary",
filter: "Print",
});
expect(data.error).toBeUndefined();
expect(data.count).toBeGreaterThan(0);
for (const fn of data.functions) {
expect(fn.name.toLowerCase()).toContain("print");
}
});
it("includes parameter info in function listing", async () => {
const data = await uePost("/api/list-functions", {
className: "KismetSystemLibrary",
filter: "PrintString",
});
expect(data.error).toBeUndefined();
const printFn = data.functions.find((f: any) => f.name === "PrintString");
expect(printFn).toBeDefined();
expect(printFn.parameters).toBeDefined();
expect(printFn.parameters.length).toBeGreaterThan(0);
// Verify parameter structure
const param = printFn.parameters[0];
expect(param.name).toBeDefined();
expect(param.type).toBeDefined();
});
it("returns error for non-existent class", async () => {
const data = await uePost("/api/list-functions", { className: "FakeClass_XYZ_999" });
expect(data.error).toBeDefined();
});
it("returns error for missing className", async () => {
const data = await uePost("/api/list-functions", {});
expect(data.error).toBeDefined();
});
// --- list_properties ---
it("lists properties on Actor", async () => {
const data = await uePost("/api/list-properties", { className: "Actor" });
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.count).toBeGreaterThan(0);
// Verify structure
const prop = data.properties[0];
expect(prop.name).toBeDefined();
expect(prop.type).toBeDefined();
});
it("filters properties by name", async () => {
const data = await uePost("/api/list-properties", {
className: "Actor",
filter: "Root",
});
expect(data.error).toBeUndefined();
for (const prop of data.properties) {
expect(prop.name.toLowerCase()).toContain("root");
}
});
it("returns error for non-existent class", async () => {
const data = await uePost("/api/list-properties", { className: "FakeClass_XYZ_999" });
expect(data.error).toBeDefined();
});
it("returns error for missing className", async () => {
const data = await uePost("/api/list-properties", {});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,193 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("add_component / remove_component / list_components", () => {
const bpName = uniqueName("BP_ComponentTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName, parentClass: "Actor" });
expect(res.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
// --- list_components tests ---
it("lists components on an Actor blueprint", async () => {
const data = await uePost("/api/list-components", { blueprint: bpName });
expect(data.error).toBeUndefined();
expect(data.blueprint).toBe(bpName);
expect(data.components).toBeDefined();
expect(Array.isArray(data.components)).toBe(true);
// An Actor BP should have at least a DefaultSceneRoot
expect(data.components.length).toBeGreaterThanOrEqual(1);
});
it("first component is the scene root", async () => {
const data = await uePost("/api/list-components", { blueprint: bpName });
expect(data.error).toBeUndefined();
const root = data.components.find((c: any) => c.isSceneRoot === true);
expect(root).toBeDefined();
});
// --- add_component tests ---
it("adds a StaticMeshComponent", async () => {
const data = await uePost("/api/add-component", {
blueprint: bpName,
componentClass: "StaticMeshComponent",
name: "TestMesh",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.name).toBe("TestMesh");
expect(data.componentClass).toContain("StaticMeshComponent");
expect(data.saved).toBe(true);
});
it("lists the added component", async () => {
const data = await uePost("/api/list-components", { blueprint: bpName });
expect(data.error).toBeUndefined();
const found = data.components.find((c: any) => c.name === "TestMesh");
expect(found).toBeDefined();
expect(found.componentClass).toContain("StaticMeshComponent");
});
it("adds a component with a parent", async () => {
const data = await uePost("/api/add-component", {
blueprint: bpName,
componentClass: "SceneComponent",
name: "ChildScene",
parentComponent: "TestMesh",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.parentComponent).toBeDefined();
});
it("verifies parent-child relationship", async () => {
const data = await uePost("/api/list-components", { blueprint: bpName });
expect(data.error).toBeUndefined();
const child = data.components.find((c: any) => c.name === "ChildScene");
expect(child).toBeDefined();
expect(child.parentComponent).toBe("TestMesh");
});
it("rejects duplicate component name", async () => {
const data = await uePost("/api/add-component", {
blueprint: bpName,
componentClass: "SceneComponent",
name: "TestMesh",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("already exists");
});
it("rejects non-existent component class", async () => {
const data = await uePost("/api/add-component", {
blueprint: bpName,
componentClass: "FakeComponentClass_XYZ_999",
name: "TestFake",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("not found");
});
it("rejects non-existent parent component", async () => {
const data = await uePost("/api/add-component", {
blueprint: bpName,
componentClass: "SceneComponent",
name: "TestOrphan",
parentComponent: "NonExistentParent_XYZ_999",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("not found");
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/add-component", {});
expect(data.error).toBeDefined();
});
it("rejects add on non-existent blueprint", async () => {
const data = await uePost("/api/add-component", {
blueprint: "BP_Nonexistent_XYZ_999",
componentClass: "StaticMeshComponent",
name: "Test",
});
expect(data.error).toBeDefined();
});
// --- remove_component tests ---
it("removes the child component first", async () => {
const data = await uePost("/api/remove-component", {
blueprint: bpName,
name: "ChildScene",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.saved).toBe(true);
});
it("removes the TestMesh component", async () => {
const data = await uePost("/api/remove-component", {
blueprint: bpName,
name: "TestMesh",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.saved).toBe(true);
});
it("verifies components are removed", async () => {
const data = await uePost("/api/list-components", { blueprint: bpName });
expect(data.error).toBeUndefined();
const mesh = data.components.find((c: any) => c.name === "TestMesh");
expect(mesh).toBeUndefined();
const child = data.components.find((c: any) => c.name === "ChildScene");
expect(child).toBeUndefined();
});
it("rejects removing non-existent component", async () => {
const data = await uePost("/api/remove-component", {
blueprint: bpName,
name: "NonExistent_XYZ_999",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("not found");
expect(data.existingComponents).toBeDefined();
});
it("rejects remove with missing required fields", async () => {
const data = await uePost("/api/remove-component", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("rejects remove on non-existent blueprint", async () => {
const data = await uePost("/api/remove-component", {
blueprint: "BP_Nonexistent_XYZ_999",
name: "SomeComponent",
});
expect(data.error).toBeDefined();
});
// --- list_components error cases ---
it("rejects list on non-existent blueprint", async () => {
const data = await uePost("/api/list-components", {
blueprint: "BP_Nonexistent_XYZ_999",
});
expect(data.error).toBeDefined();
});
it("rejects list with missing required fields", async () => {
const data = await uePost("/api/list-components", {});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,86 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("connect_pins / disconnect_pin", () => {
const bpName = uniqueName("BP_ConnectTest");
const packagePath = "/Game/Test";
let eventNodeId: string;
let printNodeId: string;
beforeAll(async () => {
// Create Blueprint
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
// Add an event override node (BeginPlay)
const eventRes = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "OverrideEvent",
functionName: "ReceiveBeginPlay",
});
expect(eventRes.success).toBe(true);
eventNodeId = eventRes.nodeId;
// Add a PrintString call
const printRes = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(printRes.success).toBe(true);
printNodeId = printRes.nodeId;
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("connects execution pins between two nodes", async () => {
const data = await uePost("/api/connect-pins", {
blueprint: bpName,
sourceNodeId: eventNodeId,
sourcePinName: "then",
targetNodeId: printNodeId,
targetPinName: "execute",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.saved).toBe(true);
expect(data.updatedSourceNode).toBeDefined();
expect(data.updatedTargetNode).toBeDefined();
});
it("rejects connection to non-existent pin", async () => {
const data = await uePost("/api/connect-pins", {
blueprint: bpName,
sourceNodeId: eventNodeId,
sourcePinName: "FakePin",
targetNodeId: printNodeId,
targetPinName: "execute",
});
expect(data.error).toBeDefined();
expect(data.availablePins).toBeDefined();
});
it("disconnects a pin", async () => {
const data = await uePost("/api/disconnect-pin", {
blueprint: bpName,
nodeId: eventNodeId,
pinName: "then",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.saved).toBe(true);
});
it("rejects disconnect on non-existent node", async () => {
const data = await uePost("/api/disconnect-pin", {
blueprint: bpName,
nodeId: "00000000-0000-0000-0000-000000000000",
pinName: "then",
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,62 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("create_blueprint", () => {
const bpName = uniqueName("BP_CreateTest");
const packagePath = "/Game/Test";
let createResult: any;
beforeAll(async () => {
createResult = await createTestBlueprint({ name: bpName });
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("returns success fields", () => {
expect(createResult.error).toBeUndefined();
expect(createResult.blueprintName).toBe(bpName);
expect(createResult.packagePath).toBe(packagePath);
expect(createResult.assetPath).toContain(bpName);
expect(createResult.parentClass).toBe("Actor");
expect(createResult.blueprintType).toBe("Normal");
expect(createResult.saved).toBe(true);
});
it("creates at least one graph (EventGraph)", () => {
expect(createResult.graphs).toBeDefined();
expect(createResult.graphs.length).toBeGreaterThanOrEqual(1);
});
it("rejects duplicate creation", async () => {
const dup = await createTestBlueprint({ name: bpName });
expect(dup.error).toBeDefined();
expect(dup.error).toContain("already exists");
});
it("appears in list_blueprints", async () => {
const list = await ueGet("/api/list", { filter: bpName });
expect(list.count).toBeGreaterThanOrEqual(1);
const names = list.blueprints.map((b: any) => b.name);
expect(names).toContain(bpName);
});
it("rejects invalid packagePath", async () => {
const bad = await createTestBlueprint({
name: uniqueName("BP_BadPath"),
packagePath: "/Invalid/Path",
});
expect(bad.error).toBeDefined();
expect(bad.error).toContain("/Game");
});
it("rejects unknown parent class", async () => {
const bad = await createTestBlueprint({
name: uniqueName("BP_BadParent"),
parentClass: "NonExistentClass_XYZ",
});
expect(bad.error).toBeDefined();
expect(bad.error).toContain("Could not find parent class");
});
});

View File

@@ -0,0 +1,121 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("create_graph", () => {
const bpName = uniqueName("BP_CreateGraphTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("creates a function graph", async () => {
const data = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "MyFunction",
graphType: "function",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.graphName).toBe("MyFunction");
expect(data.graphType).toBe("function");
expect(data.saved).toBe(true);
});
it("function graph appears in get_blueprint", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
const graphNames = data.graphs.map((g: any) => g.name);
expect(graphNames).toContain("MyFunction");
});
it("creates a macro graph", async () => {
const data = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "MyMacro",
graphType: "macro",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.graphName).toBe("MyMacro");
expect(data.graphType).toBe("macro");
expect(data.saved).toBe(true);
});
it("creates a custom event", async () => {
const data = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "MyCustomEvent",
graphType: "customEvent",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.graphName).toBe("MyCustomEvent");
expect(data.graphType).toBe("customEvent");
expect(data.nodeId).toBeDefined();
expect(data.saved).toBe(true);
});
it("rejects duplicate graph name", async () => {
const data = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "MyFunction",
graphType: "function",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("already exists");
});
it("rejects duplicate custom event name", async () => {
const data = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "MyCustomEvent",
graphType: "customEvent",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("already exists");
});
it("rejects non-existent blueprint", async () => {
const data = await uePost("/api/create-graph", {
blueprint: "BP_Nonexistent_XYZ_999",
graphName: "SomeGraph",
graphType: "function",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/create-graph", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("rejects invalid graphType", async () => {
const data = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "BadType",
graphType: "invalid",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("Invalid graphType");
});
it("can add a node to the new function graph", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "MyFunction",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
});
});

View File

@@ -0,0 +1,37 @@
import { describe, it, expect, beforeAll } from "vitest";
import { ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("delete_asset", () => {
it("deletes a Blueprint that was just created", async () => {
const bpName = uniqueName("BP_DeleteTest");
const packagePath = "/Game/Test";
const assetPath = `${packagePath}/${bpName}`;
// Create
const created = await createTestBlueprint({ name: bpName });
expect(created.error).toBeUndefined();
expect(created.saved).toBe(true);
// Verify it exists in the list
const listBefore = await ueGet("/api/list", { filter: bpName });
expect(listBefore.count).toBeGreaterThanOrEqual(1);
// Delete
const deleted = await deleteTestBlueprint(assetPath);
expect(deleted.error).toBeUndefined();
expect(deleted.success).toBe(true);
expect(deleted.assetPath).toBe(assetPath);
});
it("returns error for non-existent asset", async () => {
const data = await deleteTestBlueprint("/Game/Test/BP_DoesNotExist_999999");
expect(data.error).toBeDefined();
});
it("requires assetPath field", async () => {
const { uePost } = await import("../helpers.js");
const data = await uePost("/api/delete-asset", {});
expect(data.error).toBeDefined();
expect(data.error).toContain("assetPath");
});
});

View File

@@ -0,0 +1,87 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("delete_node", () => {
const bpName = uniqueName("BP_DeleteNodeTest");
const packagePath = "/Game/Test";
let printNodeId: string;
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
// Add a PrintString node to delete later
const res = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(res.success).toBe(true);
printNodeId = res.nodeId;
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("deletes a function call node", async () => {
const data = await uePost("/api/delete-node", {
blueprint: bpName,
nodeId: printNodeId,
});
expect(data.error).toBeUndefined();
expect(data.blueprint).toBe(bpName);
expect(data.nodeId).toBe(printNodeId);
expect(data.nodeClass).toBeDefined();
expect(data.saved).toBe(true);
});
it("verifies node is gone from graph", async () => {
const graph = await ueGet("/api/graph", {
name: bpName,
graph: "EventGraph",
});
expect(graph.error).toBeUndefined();
const found = graph.nodes.find((n: any) => n.id === printNodeId);
expect(found).toBeUndefined();
});
it("rejects deletion of non-existent node", async () => {
const data = await uePost("/api/delete-node", {
blueprint: bpName,
nodeId: "00000000-0000-0000-0000-000000000000",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/delete-node", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("protects entry/event nodes from deletion", async () => {
// Get the EventGraph to find the entry node
const graph = await ueGet("/api/graph", {
name: bpName,
graph: "EventGraph",
});
expect(graph.error).toBeUndefined();
// Find an event node (ReceiveBeginPlay or similar)
const eventNode = graph.nodes.find(
(n: any) =>
n.class?.includes("Event") || n.class?.includes("FunctionEntry"),
);
if (eventNode) {
const data = await uePost("/api/delete-node", {
blueprint: bpName,
nodeId: eventNode.id,
});
expect(data.error).toBeDefined();
expect(data.error).toContain("Cannot delete");
}
});
});

View File

@@ -0,0 +1,82 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("diff_blueprints", () => {
const bpNameA = uniqueName("BP_DiffTestA");
const bpNameB = uniqueName("BP_DiffTestB");
const packagePath = "/Game/Test";
beforeAll(async () => {
const a = await createTestBlueprint({ name: bpNameA });
expect(a.error).toBeUndefined();
const b = await createTestBlueprint({ name: bpNameB });
expect(b.error).toBeUndefined();
// Add a node to A but not B to create a difference
const node = await uePost("/api/add-node", {
blueprint: bpNameA,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(node.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpNameA}`);
await deleteTestBlueprint(`${packagePath}/${bpNameB}`);
});
it("diffs two blueprints and finds differences", async () => {
const data = await uePost("/api/diff-blueprints", {
blueprintA: bpNameA,
blueprintB: bpNameB,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.blueprintA).toBe(bpNameA);
expect(data.blueprintB).toBe(bpNameB);
expect(data.graphs).toBeDefined();
expect(data.totalDifferences).toBeGreaterThan(0);
});
it("diffs identical blueprints", async () => {
const data = await uePost("/api/diff-blueprints", {
blueprintA: bpNameB,
blueprintB: bpNameB,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
// Same BP compared to itself should be identical
for (const g of data.graphs) {
expect(g.status).toBe("identical");
}
});
it("filters by graph name", async () => {
const data = await uePost("/api/diff-blueprints", {
blueprintA: bpNameA,
blueprintB: bpNameB,
graph: "EventGraph",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.graphs.length).toBe(1);
expect(data.graphs[0].graph).toBe("EventGraph");
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/diff-blueprints", {
blueprintA: bpNameA,
});
expect(data.error).toBeDefined();
});
it("rejects non-existent blueprint", async () => {
const data = await uePost("/api/diff-blueprints", {
blueprintA: bpNameA,
blueprintB: "BP_Nonexistent_XYZ_999",
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,53 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("find_disconnected_pins", () => {
const bpName = uniqueName("BP_DisconnPinsTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("scans a single Blueprint for disconnected pins", async () => {
const data = await uePost("/api/find-disconnected-pins", {
blueprint: bpName,
sensitivity: "all",
});
expect(data.error).toBeUndefined();
// A fresh Actor BP should have no disconnected struct pins
expect(data.summary).toBeDefined();
expect(typeof data.summary.blueprintsScanned).toBe("number");
});
it("scans by path filter", async () => {
const data = await uePost("/api/find-disconnected-pins", {
filter: "/Game/Test",
sensitivity: "medium",
});
expect(data.error).toBeUndefined();
expect(data.summary).toBeDefined();
});
it("supports high sensitivity (broken types only)", async () => {
const data = await uePost("/api/find-disconnected-pins", {
blueprint: bpName,
sensitivity: "high",
});
expect(data.error).toBeUndefined();
});
it("rejects when no scope is provided", async () => {
const data = await uePost("/api/find-disconnected-pins", {
sensitivity: "medium",
});
// Should return error or empty results (no blueprint/filter specified)
// The exact behavior depends on the implementation
expect(data).toBeDefined();
});
});

View File

@@ -0,0 +1,121 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("duplicate_nodes", () => {
const bpName = uniqueName("BP_DuplicateNodesTest");
const packagePath = "/Game/Test";
let printNodeId: string;
let branchNodeId: string;
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
const n1 = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
posX: 100,
posY: 100,
});
expect(n1.success).toBe(true);
printNodeId = n1.nodeId;
const n2 = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Branch",
posX: 300,
posY: 100,
});
expect(n2.success).toBe(true);
branchNodeId = n2.nodeId;
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("duplicates a single node", async () => {
const data = await uePost("/api/duplicate-nodes", {
blueprint: bpName,
graph: "EventGraph",
nodeIds: [printNodeId],
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.duplicatedCount).toBe(1);
expect(data.nodes).toHaveLength(1);
expect(data.nodes[0].newNodeId).toBeDefined();
expect(data.nodes[0].sourceNodeId).toBe(printNodeId);
expect(data.saved).toBe(true);
});
it("duplicates multiple nodes", async () => {
const data = await uePost("/api/duplicate-nodes", {
blueprint: bpName,
graph: "EventGraph",
nodeIds: [printNodeId, branchNodeId],
offsetX: 200,
offsetY: 100,
});
expect(data.error).toBeUndefined();
expect(data.duplicatedCount).toBe(2);
expect(data.nodes).toHaveLength(2);
expect(data.saved).toBe(true);
});
it("duplicated nodes appear in graph", async () => {
const graph = await ueGet("/api/graph", { name: bpName, graph: "EventGraph" });
expect(graph.error).toBeUndefined();
// Should have original nodes plus duplicated ones
expect(graph.nodes.length).toBeGreaterThanOrEqual(4);
});
it("handles invalid node ID gracefully", async () => {
const data = await uePost("/api/duplicate-nodes", {
blueprint: bpName,
graph: "EventGraph",
nodeIds: [printNodeId, "00000000-0000-0000-0000-000000000000"],
});
expect(data.error).toBeUndefined();
expect(data.duplicatedCount).toBe(1);
expect(data.notFound).toBeDefined();
expect(data.notFound.length).toBeGreaterThan(0);
});
it("rejects empty nodeIds array", async () => {
const data = await uePost("/api/duplicate-nodes", {
blueprint: bpName,
graph: "EventGraph",
nodeIds: [],
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/duplicate-nodes", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("rejects non-existent blueprint", async () => {
const data = await uePost("/api/duplicate-nodes", {
blueprint: "BP_Nonexistent_XYZ_999",
graph: "EventGraph",
nodeIds: ["00000000-0000-0000-0000-000000000000"],
});
expect(data.error).toBeDefined();
});
it("rejects non-existent graph", async () => {
const data = await uePost("/api/duplicate-nodes", {
blueprint: bpName,
graph: "NonExistentGraph_XYZ",
nodeIds: [printNodeId],
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,252 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("add_event_dispatcher / list_event_dispatchers / add_function_parameter", () => {
const bpName = uniqueName("BP_EventDispTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
// --- list_event_dispatchers tests (empty) ---
it("lists dispatchers on a BP with none (empty)", async () => {
const data = await uePost("/api/list-event-dispatchers", { blueprint: bpName });
expect(data.error).toBeUndefined();
expect(data.blueprint).toBe(bpName);
expect(data.count).toBe(0);
expect(data.dispatchers).toEqual([]);
});
// --- add_event_dispatcher tests ---
it("adds an event dispatcher with no parameters", async () => {
const data = await uePost("/api/add-event-dispatcher", {
blueprint: bpName,
dispatcherName: "OnSimpleEvent",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.blueprint).toBe(bpName);
expect(data.dispatcherName).toBe("OnSimpleEvent");
expect(data.parameters).toEqual([]);
expect(data.saved).toBe(true);
});
it("adds an event dispatcher with parameters", async () => {
const data = await uePost("/api/add-event-dispatcher", {
blueprint: bpName,
dispatcherName: "OnDamageTaken",
parameters: [
{ name: "Damage", type: "float" },
{ name: "Instigator", type: "object" },
],
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.dispatcherName).toBe("OnDamageTaken");
expect(data.parameters).toHaveLength(2);
expect(data.parameters[0].name).toBe("Damage");
expect(data.parameters[0].type).toBe("float");
expect(data.parameters[1].name).toBe("Instigator");
expect(data.parameters[1].type).toBe("object");
expect(data.saved).toBe(true);
});
it("lists dispatchers after adding two", async () => {
const data = await uePost("/api/list-event-dispatchers", { blueprint: bpName });
expect(data.error).toBeUndefined();
expect(data.count).toBe(2);
expect(data.dispatchers).toHaveLength(2);
const names = data.dispatchers.map((d: any) => d.name);
expect(names).toContain("OnSimpleEvent");
expect(names).toContain("OnDamageTaken");
// Check that OnDamageTaken has parameters
const damageTaken = data.dispatchers.find((d: any) => d.name === "OnDamageTaken");
expect(damageTaken.parameters).toHaveLength(2);
expect(damageTaken.parameters[0].name).toBe("Damage");
expect(damageTaken.parameters[1].name).toBe("Instigator");
});
it("rejects adding duplicate dispatcher", async () => {
const data = await uePost("/api/add-event-dispatcher", {
blueprint: bpName,
dispatcherName: "OnSimpleEvent",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("already exists");
});
it("rejects adding dispatcher to non-existent blueprint", async () => {
const data = await uePost("/api/add-event-dispatcher", {
blueprint: "BP_Nonexistent_XYZ_999",
dispatcherName: "OnFoo",
});
expect(data.error).toBeDefined();
});
it("rejects adding dispatcher with missing required fields", async () => {
const data = await uePost("/api/add-event-dispatcher", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("rejects dispatcher with invalid parameter type", async () => {
const data = await uePost("/api/add-event-dispatcher", {
blueprint: bpName,
dispatcherName: "OnBadType",
parameters: [{ name: "BadParam", type: "NonExistentType_XYZ" }],
});
expect(data.error).toBeDefined();
});
// --- add_function_parameter tests ---
it("adds a parameter to a function", async () => {
// First create a function graph
const createRes = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "TestFunction",
graphType: "function",
});
expect(createRes.error).toBeUndefined();
const data = await uePost("/api/add-function-parameter", {
blueprint: bpName,
functionName: "TestFunction",
paramName: "InputValue",
paramType: "float",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.blueprint).toBe(bpName);
expect(data.functionName).toBe("TestFunction");
expect(data.paramName).toBe("InputValue");
expect(data.paramType).toBe("float");
expect(data.nodeType).toBe("FunctionEntry");
expect(data.saved).toBe(true);
});
it("adds a parameter to a custom event", async () => {
// Create a custom event
const createRes = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "TestCustomEvent",
graphType: "customEvent",
});
expect(createRes.error).toBeUndefined();
const data = await uePost("/api/add-function-parameter", {
blueprint: bpName,
functionName: "TestCustomEvent",
paramName: "EventData",
paramType: "string",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.functionName).toBe("TestCustomEvent");
expect(data.paramName).toBe("EventData");
expect(data.nodeType).toBe("CustomEvent");
expect(data.saved).toBe(true);
});
it("adds a parameter to an event dispatcher", async () => {
const data = await uePost("/api/add-function-parameter", {
blueprint: bpName,
functionName: "OnSimpleEvent",
paramName: "NewParam",
paramType: "bool",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.functionName).toBe("OnSimpleEvent");
expect(data.paramName).toBe("NewParam");
expect(data.nodeType).toBe("EventDispatcher");
expect(data.saved).toBe(true);
});
it("verifies dispatcher parameter was added via list", async () => {
const data = await uePost("/api/list-event-dispatchers", { blueprint: bpName });
expect(data.error).toBeUndefined();
const simple = data.dispatchers.find((d: any) => d.name === "OnSimpleEvent");
expect(simple).toBeDefined();
expect(simple.parameters).toHaveLength(1);
expect(simple.parameters[0].name).toBe("NewParam");
});
it("rejects duplicate parameter name", async () => {
const data = await uePost("/api/add-function-parameter", {
blueprint: bpName,
functionName: "TestFunction",
paramName: "InputValue",
paramType: "int",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("already exists");
});
it("rejects adding parameter to non-existent function", async () => {
const data = await uePost("/api/add-function-parameter", {
blueprint: bpName,
functionName: "NonExistentFunc_XYZ",
paramName: "Foo",
paramType: "bool",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("not found");
expect(data.availableFunctions).toBeDefined();
expect(data.availableFunctions.length).toBeGreaterThan(0);
});
it("rejects adding parameter to non-existent blueprint", async () => {
const data = await uePost("/api/add-function-parameter", {
blueprint: "BP_Nonexistent_XYZ_999",
functionName: "SomeFunc",
paramName: "Foo",
paramType: "bool",
});
expect(data.error).toBeDefined();
});
it("rejects adding parameter with missing required fields", async () => {
const data = await uePost("/api/add-function-parameter", {
blueprint: bpName,
functionName: "TestFunction",
});
expect(data.error).toBeDefined();
});
it("rejects adding parameter with unknown type", async () => {
const data = await uePost("/api/add-function-parameter", {
blueprint: bpName,
functionName: "TestFunction",
paramName: "BadTypeParam",
paramType: "NonExistentType_XYZ",
});
expect(data.error).toBeDefined();
});
// --- list error cases ---
it("rejects listing dispatchers on non-existent blueprint", async () => {
const data = await uePost("/api/list-event-dispatchers", {
blueprint: "BP_Nonexistent_XYZ_999",
});
expect(data.error).toBeDefined();
});
it("rejects listing dispatchers with missing required fields", async () => {
const data = await uePost("/api/list-event-dispatchers", {});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,39 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("find_asset_references", () => {
const bpName = uniqueName("BP_RefTest");
const packagePath = "/Game/Test";
const assetPath = `${packagePath}/${bpName}`;
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(assetPath);
});
it("returns reference data for an existing asset", async () => {
const data = await ueGet("/api/references", { assetPath });
expect(data.error).toBeUndefined();
expect(data.assetPath).toBe(assetPath);
expect(typeof data.totalReferencers).toBe("number");
});
it("returns zero referencers for an isolated test asset", async () => {
const data = await ueGet("/api/references", { assetPath });
expect(data.totalReferencers).toBe(0);
});
it("returns zero referencers for non-existent asset path", async () => {
// The API queries the asset registry which returns empty for unknown paths
// (it does not validate asset existence — it just reports zero refs)
const data = await ueGet("/api/references", {
assetPath: "/Game/Test/NonExistent_XYZ_999",
});
expect(data.error).toBeUndefined();
expect(data.totalReferencers).toBe(0);
});
});

View File

@@ -0,0 +1,56 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("get_blueprint / get_blueprint_graph", () => {
const bpName = uniqueName("BP_GetTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
describe("get_blueprint", () => {
it("returns blueprint metadata", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
expect(data.name).toBe(bpName);
expect(data.parentClass).toBeDefined();
expect(data.graphs).toBeDefined();
expect(Array.isArray(data.graphs)).toBe(true);
});
it("returns error for non-existent blueprint", async () => {
const data = await ueGet("/api/blueprint", {
name: "BP_DoesNotExist_999999",
});
expect(data.error).toBeDefined();
});
});
describe("get_blueprint_graph", () => {
it("returns EventGraph with nodes and pins", async () => {
const data = await ueGet("/api/graph", {
name: bpName,
graph: "EventGraph",
});
expect(data.error).toBeUndefined();
expect(data.graphName || data.name).toBeDefined();
// A fresh Actor BP should have at least default event nodes
expect(data.nodes).toBeDefined();
expect(Array.isArray(data.nodes)).toBe(true);
});
it("returns error for non-existent graph", async () => {
const data = await ueGet("/api/graph", {
name: bpName,
graph: "NonExistentGraph",
});
expect(data.error).toBeDefined();
});
});
});

View File

@@ -0,0 +1,165 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("delete_graph / rename_graph", () => {
const bpName = uniqueName("BP_GraphMgmtTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
// Create a function graph to test with
const fn = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "MyTestFunction",
graphType: "function",
});
expect(fn.error).toBeUndefined();
// Create a macro graph to test with
const macro = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "MyTestMacro",
graphType: "macro",
});
expect(macro.error).toBeUndefined();
// Create another function for rename testing
const fn2 = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: "FunctionToRename",
graphType: "function",
});
expect(fn2.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
// --- rename_graph tests ---
it("renames a function graph", async () => {
const data = await uePost("/api/rename-graph", {
blueprint: bpName,
graphName: "FunctionToRename",
newName: "RenamedFunction",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.oldName).toBe("FunctionToRename");
expect(data.newName).toBe("RenamedFunction");
expect(data.graphType).toBe("function");
expect(data.saved).toBe(true);
});
it("verifies renamed graph appears in blueprint", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
const graphNames = data.graphs.map((g: any) => g.name || g);
expect(graphNames).toContain("RenamedFunction");
expect(graphNames).not.toContain("FunctionToRename");
});
it("rejects renaming EventGraph", async () => {
const data = await uePost("/api/rename-graph", {
blueprint: bpName,
graphName: "EventGraph",
newName: "MyEventGraph",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("Cannot rename");
});
it("rejects rename to existing name", async () => {
const data = await uePost("/api/rename-graph", {
blueprint: bpName,
graphName: "RenamedFunction",
newName: "MyTestMacro",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("already exists");
});
it("rejects rename of non-existent graph", async () => {
const data = await uePost("/api/rename-graph", {
blueprint: bpName,
graphName: "NonExistentGraph_XYZ",
newName: "NewName",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields for rename", async () => {
const data = await uePost("/api/rename-graph", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
// --- delete_graph tests ---
it("deletes a function graph", async () => {
const data = await uePost("/api/delete-graph", {
blueprint: bpName,
graphName: "MyTestFunction",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.graphName).toBe("MyTestFunction");
expect(data.graphType).toBe("function");
expect(data.nodeCount).toBeDefined();
expect(data.saved).toBe(true);
});
it("verifies deleted graph is gone", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
const graphNames = data.graphs.map((g: any) => g.name || g);
expect(graphNames).not.toContain("MyTestFunction");
});
it("deletes a macro graph", async () => {
const data = await uePost("/api/delete-graph", {
blueprint: bpName,
graphName: "MyTestMacro",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.graphType).toBe("macro");
expect(data.saved).toBe(true);
});
it("rejects deleting EventGraph", async () => {
const data = await uePost("/api/delete-graph", {
blueprint: bpName,
graphName: "EventGraph",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("Cannot delete");
});
it("rejects deleting non-existent graph", async () => {
const data = await uePost("/api/delete-graph", {
blueprint: bpName,
graphName: "NonExistentGraph_XYZ",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields for delete", async () => {
const data = await uePost("/api/delete-graph", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("rejects non-existent blueprint", async () => {
const data = await uePost("/api/delete-graph", {
blueprint: "BP_Nonexistent_XYZ_999",
graphName: "SomeGraph",
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,186 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("add_interface / remove_interface / list_interfaces", () => {
const bpName = uniqueName("BP_InterfacesTest");
const ifaceName = uniqueName("BPI_TestIface");
const packagePath = "/Game/Test";
beforeAll(async () => {
// Create a regular Actor blueprint
const bpRes = await createTestBlueprint({ name: bpName });
expect(bpRes.error).toBeUndefined();
// Create a Blueprint Interface asset
const ifaceRes = await createTestBlueprint({
name: ifaceName,
blueprintType: "Interface",
});
expect(ifaceRes.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
await deleteTestBlueprint(`${packagePath}/${ifaceName}`);
});
// --- list_interfaces tests ---
it("lists interfaces on a BP with none implemented (empty)", async () => {
const data = await uePost("/api/list-interfaces", { blueprint: bpName });
expect(data.error).toBeUndefined();
expect(data.blueprint).toBe(bpName);
expect(data.count).toBe(0);
expect(data.interfaces).toEqual([]);
});
// --- add_interface tests ---
it("adds an interface to a blueprint", async () => {
const data = await uePost("/api/add-interface", {
blueprint: bpName,
interfaceName: ifaceName,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.blueprint).toBe(bpName);
expect(data.interfaceName).toBeDefined();
expect(data.interfacePath).toBeDefined();
expect(data.functionGraphsAdded).toBeDefined();
expect(data.saved).toBe(true);
});
it("lists interfaces after adding one", async () => {
const data = await uePost("/api/list-interfaces", { blueprint: bpName });
expect(data.error).toBeUndefined();
expect(data.count).toBe(1);
expect(data.interfaces).toHaveLength(1);
expect(data.interfaces[0].name).toBeDefined();
expect(data.interfaces[0].classPath).toBeDefined();
expect(data.interfaces[0].functions).toBeDefined();
});
it("interface appears in get_blueprint response", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
expect(data.interfaces).toBeDefined();
expect(data.interfaces.length).toBeGreaterThanOrEqual(1);
});
it("rejects adding duplicate interface", async () => {
const data = await uePost("/api/add-interface", {
blueprint: bpName,
interfaceName: ifaceName,
});
expect(data.error).toBeDefined();
expect(data.error).toContain("already implemented");
});
it("rejects adding non-existent interface", async () => {
const data = await uePost("/api/add-interface", {
blueprint: bpName,
interfaceName: "BPI_Nonexistent_XYZ_999",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("not found");
});
it("rejects adding interface to non-existent blueprint", async () => {
const data = await uePost("/api/add-interface", {
blueprint: "BP_Nonexistent_XYZ_999",
interfaceName: ifaceName,
});
expect(data.error).toBeDefined();
});
it("rejects add with missing required fields", async () => {
const data = await uePost("/api/add-interface", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
// --- remove_interface tests ---
it("removes an interface from a blueprint", async () => {
const data = await uePost("/api/remove-interface", {
blueprint: bpName,
interfaceName: ifaceName,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.blueprint).toBe(bpName);
expect(data.interfaceName).toBeDefined();
expect(data.preservedFunctions).toBe(false);
expect(data.saved).toBe(true);
});
it("lists interfaces after removal (empty again)", async () => {
const data = await uePost("/api/list-interfaces", { blueprint: bpName });
expect(data.error).toBeUndefined();
expect(data.count).toBe(0);
expect(data.interfaces).toEqual([]);
});
it("rejects removing non-implemented interface", async () => {
const data = await uePost("/api/remove-interface", {
blueprint: bpName,
interfaceName: "BPI_NotImplemented_XYZ_999",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("not implemented");
expect(data.implementedInterfaces).toBeDefined();
});
it("rejects removing interface from non-existent blueprint", async () => {
const data = await uePost("/api/remove-interface", {
blueprint: "BP_Nonexistent_XYZ_999",
interfaceName: ifaceName,
});
expect(data.error).toBeDefined();
});
it("rejects remove with missing required fields", async () => {
const data = await uePost("/api/remove-interface", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
// --- list_interfaces error cases ---
it("rejects list on non-existent blueprint", async () => {
const data = await uePost("/api/list-interfaces", {
blueprint: "BP_Nonexistent_XYZ_999",
});
expect(data.error).toBeDefined();
});
it("rejects list with missing required fields", async () => {
const data = await uePost("/api/list-interfaces", {});
expect(data.error).toBeDefined();
});
// --- preserveFunctions option ---
it("add and remove with preserveFunctions=true", async () => {
// Re-add the interface
const addData = await uePost("/api/add-interface", {
blueprint: bpName,
interfaceName: ifaceName,
});
expect(addData.error).toBeUndefined();
expect(addData.success).toBe(true);
// Remove with preserveFunctions=true
const removeData = await uePost("/api/remove-interface", {
blueprint: bpName,
interfaceName: ifaceName,
preserveFunctions: true,
});
expect(removeData.error).toBeUndefined();
expect(removeData.success).toBe(true);
expect(removeData.preservedFunctions).toBe(true);
expect(removeData.saved).toBe(true);
});
});

View File

@@ -0,0 +1,74 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("list_blueprints", () => {
const bpName = uniqueName("BP_ListTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("returns count and blueprints array", async () => {
const data = await ueGet("/api/list");
expect(data.error).toBeUndefined();
expect(typeof data.count).toBe("number");
expect(typeof data.total).toBe("number");
expect(Array.isArray(data.blueprints)).toBe(true);
expect(data.count).toBeGreaterThan(0);
});
it("filters by name substring", async () => {
const data = await ueGet("/api/list", { filter: bpName });
expect(data.count).toBe(1);
expect(data.blueprints[0].name).toBe(bpName);
});
it("returns empty results for non-matching filter", async () => {
const data = await ueGet("/api/list", { filter: "ZZZ_NonExistent_XYZ" });
expect(data.count).toBe(0);
expect(data.blueprints).toHaveLength(0);
});
it("blueprint entries have expected fields", async () => {
const data = await ueGet("/api/list", { filter: bpName });
const bp = data.blueprints[0];
expect(bp.name).toBeDefined();
expect(bp.path).toBeDefined();
});
it("type=regular excludes level blueprints", async () => {
const data = await ueGet("/api/list", { type: "regular" });
expect(data.error).toBeUndefined();
const levelBPs = data.blueprints.filter((bp: any) => bp.isLevelBlueprint);
expect(levelBPs).toHaveLength(0);
});
it("type=level returns only level blueprints", async () => {
const data = await ueGet("/api/list", { type: "level" });
expect(data.error).toBeUndefined();
// Every entry should be a level blueprint
for (const bp of data.blueprints) {
expect(bp.isLevelBlueprint).toBe(true);
}
});
it("type=all returns both regular and level blueprints", async () => {
const all = await ueGet("/api/list", { type: "all" });
const regular = await ueGet("/api/list", { type: "regular" });
const level = await ueGet("/api/list", { type: "level" });
expect(all.error).toBeUndefined();
expect(all.count).toBe(regular.count + level.count);
});
it("regular blueprint entries do not have isLevelBlueprint", async () => {
const data = await ueGet("/api/list", { filter: bpName });
expect(data.count).toBe(1);
expect(data.blueprints[0].isLevelBlueprint).toBeUndefined();
});
});

View File

@@ -0,0 +1,84 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestMaterial, createTestMaterialInstance, deleteTestMaterial, uniqueName } from "../helpers.js";
describe("material instance tools", () => {
const parentName = uniqueName("M_MIParent");
const miName = uniqueName("MI_Test");
const packagePath = "/Game/Test";
beforeAll(async () => {
// Create parent material first
const parentResult = await createTestMaterial({ name: parentName });
expect(parentResult.error).toBeUndefined();
// Add a scalar parameter to the parent so we can test instance overrides
await uePost("/api/add-material-expression", {
material: parentName,
expressionClass: "ScalarParameter",
posX: -200,
posY: 0,
});
});
afterAll(async () => {
await deleteTestMaterial(`${packagePath}/${miName}`);
await deleteTestMaterial(`${packagePath}/${parentName}`);
});
describe("create_material_instance", () => {
it("creates a material instance", async () => {
const data = await uePost("/api/create-material-instance", {
name: miName,
packagePath,
parentMaterial: parentName,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.name).toBe(miName);
});
it("rejects missing parent", async () => {
const data = await uePost("/api/create-material-instance", {
name: "MI_NoParent",
packagePath,
parentMaterial: "M_NonExistent_XYZ_999",
});
expect(data.error).toBeDefined();
});
});
describe("get_material_instance_parameters", () => {
it("returns parameter info", async () => {
const data = await ueGet("/api/material-instance-params", { name: miName });
expect(data.error).toBeUndefined();
expect(data.name).toBe(miName);
expect(data.parentChain).toBeDefined();
});
it("returns error for non-existent MI", async () => {
const data = await ueGet("/api/material-instance-params", { name: "MI_NonExistent_XYZ_999" });
expect(data.error).toBeDefined();
});
});
describe("reparent_material_instance", () => {
const newParentName = uniqueName("M_NewParent");
beforeAll(async () => {
await createTestMaterial({ name: newParentName });
});
afterAll(async () => {
await deleteTestMaterial(`${packagePath}/${newParentName}`);
});
it("changes the parent material", async () => {
const data = await uePost("/api/reparent-material-instance", {
materialInstance: miName,
newParent: newParentName,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
});
});

View File

@@ -0,0 +1,349 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestMaterial, createTestMaterialFunction, deleteTestMaterial, uniqueName } from "../helpers.js";
describe("material mutation tools", () => {
const matName = uniqueName("M_MutTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const result = await createTestMaterial({ name: matName });
expect(result.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestMaterial(`${packagePath}/${matName}`);
});
describe("create_material", () => {
const createName = uniqueName("M_CreateTest");
afterAll(async () => {
await deleteTestMaterial(`/Game/Test/${createName}`);
});
it("creates a new material", async () => {
const data = await uePost("/api/create-material", {
name: createName,
packagePath: "/Game/Test",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.name).toBe(createName);
});
it("rejects missing name", async () => {
const data = await uePost("/api/create-material", { packagePath: "/Game/Test" });
expect(data.error).toBeDefined();
});
});
describe("add_material_expression", () => {
it("adds a constant expression", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Constant",
posX: 100,
posY: 100,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
});
it("adds a scalar parameter", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "ScalarParameter",
posX: -200,
posY: 0,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("rejects invalid expression class", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "InvalidExpressionClass",
});
expect(data.error).toBeDefined();
});
it("supports dry run", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Constant",
dryRun: true,
});
expect(data.error).toBeUndefined();
expect(data.dryRun).toBe(true);
});
// Dynamic expression lookup tests (#1)
it("adds Subtract via dynamic lookup", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Subtract",
posX: 200,
posY: 200,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
});
it("adds Fresnel via dynamic lookup", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Fresnel",
posX: 300,
posY: 200,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("adds Comment via dynamic lookup", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Comment",
posX: 400,
posY: 200,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("adds If via dynamic lookup", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "If",
posX: 500,
posY: 200,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("supports Lerp alias for LinearInterpolate", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Lerp",
posX: 600,
posY: 200,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("rejects abstract expression classes", async () => {
// UMaterialExpressionParameter is abstract
const data = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Parameter",
});
expect(data.error).toBeDefined();
});
});
describe("set_material_property (new properties)", () => {
it("sets opacityMaskClipValue", async () => {
const data = await uePost("/api/set-material-property", {
material: matName,
property: "opacityMaskClipValue",
value: 0.5,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("sets bUsedWithSkeletalMesh", async () => {
const data = await uePost("/api/set-material-property", {
material: matName,
property: "bUsedWithSkeletalMesh",
value: true,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.newValue).toBe("true");
});
it("sets ditheredLODTransition", async () => {
const data = await uePost("/api/set-material-property", {
material: matName,
property: "ditheredLODTransition",
value: true,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("sets bAllowNegativeEmissiveColor", async () => {
const data = await uePost("/api/set-material-property", {
material: matName,
property: "bAllowNegativeEmissiveColor",
value: true,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
});
describe("set_expression_value", () => {
let constantNodeId: string;
beforeAll(async () => {
const result = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Constant",
posX: 300,
posY: 300,
});
constantNodeId = result.nodeId;
});
it("sets constant value", async () => {
const data = await uePost("/api/set-expression-value", {
material: matName,
nodeId: constantNodeId,
value: 0.75,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
});
describe("move_material_expression", () => {
let nodeId: string;
beforeAll(async () => {
const result = await uePost("/api/add-material-expression", {
material: matName,
expressionClass: "Constant",
posX: 0,
posY: 0,
});
nodeId = result.nodeId;
});
it("moves expression to new position", async () => {
const data = await uePost("/api/move-material-expression", {
material: matName,
nodeId,
posX: 500,
posY: 250,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
});
describe("material function editing", () => {
const mfName = uniqueName("MF_EditTest");
beforeAll(async () => {
const result = await createTestMaterialFunction({ name: mfName });
expect(result.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestMaterial(`/Game/Test/${mfName}`);
});
it("adds expression to material function", async () => {
const data = await uePost("/api/add-material-expression", {
materialFunction: mfName,
expressionClass: "Constant",
posX: 0,
posY: 0,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("adds FunctionInput to material function", async () => {
const data = await uePost("/api/add-material-expression", {
materialFunction: mfName,
expressionClass: "FunctionInput",
posX: -200,
posY: 0,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("adds FunctionOutput to material function", async () => {
const data = await uePost("/api/add-material-expression", {
materialFunction: mfName,
expressionClass: "FunctionOutput",
posX: 200,
posY: 0,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("rejects when both material and materialFunction are provided", async () => {
const data = await uePost("/api/add-material-expression", {
material: matName,
materialFunction: mfName,
expressionClass: "Constant",
});
expect(data.error).toBeDefined();
});
});
describe("validate_material", () => {
it("validates a material", async () => {
const data = await uePost("/api/validate-material", {
material: matName,
});
expect(data.error).toBeUndefined();
expect(data.valid).toBeDefined();
expect(data.material).toBe(matName);
expect(data.expressionCount).toBeGreaterThanOrEqual(0);
});
it("returns error for non-existent material", async () => {
const data = await uePost("/api/validate-material", {
material: "M_NonExistent_XYZ_999",
});
expect(data.error).toBeDefined();
});
it("rejects missing material field", async () => {
const data = await uePost("/api/validate-material", {});
expect(data.error).toBeDefined();
});
});
describe("snapshot and diff", () => {
let snapshotId: string;
it("takes a snapshot", async () => {
const data = await uePost("/api/snapshot-material-graph", {
material: matName,
});
expect(data.error).toBeUndefined();
expect(data.snapshotId).toBeDefined();
snapshotId = data.snapshotId;
});
it("diffs against snapshot", async () => {
const data = await uePost("/api/diff-material-graph", {
material: matName,
snapshotId,
});
expect(data.error).toBeUndefined();
expect(data.summary).toBeDefined();
});
it("returns error for invalid snapshot", async () => {
const data = await uePost("/api/diff-material-graph", {
material: matName,
snapshotId: "nonexistent_snapshot_id",
});
expect(data.error).toBeDefined();
});
});
});

View File

@@ -0,0 +1,149 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestMaterial, deleteTestMaterial, uniqueName } from "../helpers.js";
describe("material read tools", () => {
const matName = uniqueName("M_ReadTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const result = await createTestMaterial({ name: matName });
expect(result.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestMaterial(`${packagePath}/${matName}`);
});
describe("list_materials", () => {
it("returns materials list", async () => {
const data = await ueGet("/api/materials", {});
expect(data.error).toBeUndefined();
expect(data.count).toBeGreaterThanOrEqual(1);
expect(data.materials).toBeDefined();
expect(Array.isArray(data.materials)).toBe(true);
});
it("filters by name", async () => {
const data = await ueGet("/api/materials", { filter: matName });
expect(data.error).toBeUndefined();
expect(data.count).toBeGreaterThanOrEqual(1);
const found = data.materials.some((m: any) => m.name === matName);
expect(found).toBe(true);
});
it("filters by type", async () => {
const data = await ueGet("/api/materials", { type: "material" });
expect(data.error).toBeUndefined();
for (const m of data.materials) {
expect(m.type).toBe("Material");
}
});
});
describe("get_material", () => {
it("returns material details", async () => {
const data = await ueGet("/api/material", { name: matName });
expect(data.error).toBeUndefined();
expect(data.name).toBe(matName);
expect(data.domain).toBeDefined();
expect(data.blendMode).toBeDefined();
});
it("returns usage flags", async () => {
const data = await ueGet("/api/material", { name: matName });
expect(data.error).toBeUndefined();
expect(data.usageFlags).toBeDefined();
expect(typeof data.usageFlags.bUsedWithSkeletalMesh).toBe("boolean");
expect(typeof data.usageFlags.bUsedWithMorphTargets).toBe("boolean");
expect(typeof data.usageFlags.bUsedWithNiagaraSprites).toBe("boolean");
expect(typeof data.usageFlags.bUsedWithParticleSprites).toBe("boolean");
expect(typeof data.usageFlags.bUsedWithStaticLighting).toBe("boolean");
});
it("returns opacityMaskClipValue", async () => {
const data = await ueGet("/api/material", { name: matName });
expect(data.error).toBeUndefined();
expect(typeof data.opacityMaskClipValue).toBe("number");
});
it("returns additional settings", async () => {
const data = await ueGet("/api/material", { name: matName });
expect(data.error).toBeUndefined();
expect(typeof data.ditheredLODTransition).toBe("boolean");
expect(typeof data.bAllowNegativeEmissiveColor).toBe("boolean");
});
it("returns textureSampleCount", async () => {
const data = await ueGet("/api/material", { name: matName });
expect(data.error).toBeUndefined();
expect(typeof data.textureSampleCount).toBe("number");
});
it("returns error for non-existent material", async () => {
const data = await ueGet("/api/material", { name: "M_NonExistent_XYZ_999" });
expect(data.error).toBeDefined();
});
it("rejects missing name", async () => {
const data = await ueGet("/api/material", {});
expect(data.error).toBeDefined();
});
});
describe("get_material_graph", () => {
it("returns graph data", async () => {
const data = await ueGet("/api/material-graph", { name: matName });
expect(data.error).toBeUndefined();
expect(data.nodes).toBeDefined();
});
it("returns error for non-existent material", async () => {
const data = await ueGet("/api/material-graph", { name: "M_NonExistent_XYZ_999" });
expect(data.error).toBeDefined();
});
});
describe("describe_material", () => {
it("returns description", async () => {
const data = await uePost("/api/describe-material", { material: matName });
expect(data.error).toBeUndefined();
expect(data.material).toBe(matName);
});
it("returns error for missing material field", async () => {
const data = await uePost("/api/describe-material", {});
expect(data.error).toBeDefined();
});
});
describe("search_materials", () => {
it("returns results for query", async () => {
const data = await ueGet("/api/search-materials", { query: matName });
expect(data.error).toBeUndefined();
expect(data.resultCount).toBeGreaterThanOrEqual(0);
expect(data.results).toBeDefined();
});
});
describe("find_material_references", () => {
it("returns references data", async () => {
const data = await uePost("/api/material-references", { material: matName });
expect(data.error).toBeUndefined();
expect(data.totalReferencers).toBeDefined();
});
it("returns error for missing material field", async () => {
const data = await uePost("/api/material-references", {});
expect(data.error).toBeDefined();
});
});
describe("list_material_functions", () => {
it("returns function list", async () => {
const data = await ueGet("/api/material-functions", {});
expect(data.error).toBeUndefined();
expect(data.count).toBeDefined();
expect(data.functions).toBeDefined();
});
});
});

View File

@@ -0,0 +1,129 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("move_node", () => {
const bpName = uniqueName("BP_MoveNodeTest");
const packagePath = "/Game/Test";
let nodeId1: string;
let nodeId2: string;
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
// Add two nodes to reposition
const n1 = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
posX: 0,
posY: 0,
});
expect(n1.success).toBe(true);
nodeId1 = n1.nodeId;
const n2 = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Branch",
posX: 100,
posY: 100,
});
expect(n2.success).toBe(true);
nodeId2 = n2.nodeId;
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("moves a single node", async () => {
const data = await uePost("/api/move-node", {
blueprint: bpName,
nodeId: nodeId1,
x: 500,
y: 300,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBe(nodeId1);
expect(data.newX).toBe(500);
expect(data.newY).toBe(300);
expect(data.saved).toBe(true);
});
it("verifies node position in graph", async () => {
const graph = await ueGet("/api/graph", { name: bpName, graph: "EventGraph" });
expect(graph.error).toBeUndefined();
const node = graph.nodes.find((n: any) => n.id === nodeId1);
expect(node).toBeDefined();
expect(node.posX).toBe(500);
expect(node.posY).toBe(300);
});
it("moves multiple nodes in batch", async () => {
const data = await uePost("/api/move-node", {
blueprint: bpName,
nodes: [
{ nodeId: nodeId1, x: 100, y: 200 },
{ nodeId: nodeId2, x: 400, y: 500 },
],
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.movedCount).toBe(2);
expect(data.results).toHaveLength(2);
expect(data.saved).toBe(true);
});
it("handles invalid node in batch gracefully", async () => {
const data = await uePost("/api/move-node", {
blueprint: bpName,
nodes: [
{ nodeId: nodeId1, x: 0, y: 0 },
{ nodeId: "00000000-0000-0000-0000-000000000000", x: 100, y: 100 },
],
});
expect(data.error).toBeUndefined();
expect(data.movedCount).toBe(1);
expect(data.results[1].error).toBeDefined();
});
it("rejects missing nodeId in single mode", async () => {
const data = await uePost("/api/move-node", {
blueprint: bpName,
x: 100,
y: 100,
});
expect(data.error).toBeDefined();
});
it("rejects missing coordinates in single mode", async () => {
const data = await uePost("/api/move-node", {
blueprint: bpName,
nodeId: nodeId1,
});
expect(data.error).toBeDefined();
});
it("rejects non-existent blueprint", async () => {
const data = await uePost("/api/move-node", {
blueprint: "BP_Nonexistent_XYZ_999",
nodeId: "00000000-0000-0000-0000-000000000000",
x: 0,
y: 0,
});
expect(data.error).toBeDefined();
});
it("rejects non-existent node", async () => {
const data = await uePost("/api/move-node", {
blueprint: bpName,
nodeId: "00000000-0000-0000-0000-000000000000",
x: 0,
y: 0,
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,370 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
/**
* Tests for mutation tools that operate on variables, function parameters,
* and struct node types. These tools require pre-existing variables/functions
* that can't be created via the current API, so we primarily test error cases
* and verify the response structure.
*/
describe("replace_function_calls", () => {
const bpName = uniqueName("BP_ReplaceFnTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("returns error when newClass does not exist", async () => {
// The API validates newClass exists before scanning — returns error if not found
const data = await uePost("/api/replace-function-calls", {
blueprint: bpName,
oldClass: "FakeOldLibrary_XYZ",
newClass: "FakeNewLibrary_XYZ",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("FakeNewLibrary_XYZ");
});
it("returns error in dryRun mode when newClass does not exist", async () => {
const data = await uePost("/api/replace-function-calls", {
blueprint: bpName,
oldClass: "FakeOldLibrary_XYZ",
newClass: "FakeNewLibrary_XYZ",
dryRun: true,
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/replace-function-calls", {
blueprint: "BP_DoesNotExist_XYZ_999",
oldClass: "SomeClass",
newClass: "OtherClass",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/replace-function-calls", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
});
describe("change_variable_type", () => {
const bpName = uniqueName("BP_ChangeVarTest");
const packagePath = "/Game/Test";
const testVarName = "TestVar";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
// Add a test variable to the blueprint
const addVar = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: testVarName,
variableType: "bool",
});
expect(addVar.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("returns error for non-existent variable", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
variable: "NonExistentVar_XYZ",
newType: "FVector",
typeCategory: "struct",
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: "BP_DoesNotExist_XYZ_999",
variable: "SomeVar",
newType: "FVector",
typeCategory: "struct",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("changes variable to object:Actor via colon syntax", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
variable: testVarName,
newType: "object:Actor",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.typeCategory).toBe("object");
expect(data.updatedVariable).toBeDefined();
expect(data.updatedVariable.type).toBe("object");
expect(data.updatedVariable.subtype).toBe("Actor");
});
it("changes variable to object:Actor via typeCategory param", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
variable: testVarName,
newType: "Actor",
typeCategory: "object",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.typeCategory).toBe("object");
});
it("changes variable to class:Actor (TSubclassOf)", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
variable: testVarName,
newType: "class:Actor",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.typeCategory).toBe("class");
});
it("changes variable to softobject:Actor", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
variable: testVarName,
newType: "softobject:Actor",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.typeCategory).toBe("softobject");
});
it("auto-detects struct type without typeCategory", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
variable: testVarName,
newType: "FVector",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.typeCategory).toBe("struct");
});
it("returns error for non-existent class in object reference", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
variable: testVarName,
newType: "object:NonExistentClass_XYZ_999",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("NonExistentClass_XYZ_999");
});
it("dry run with object reference type", async () => {
const data = await uePost("/api/change-variable-type", {
blueprint: bpName,
variable: testVarName,
newType: "object:Actor",
dryRun: true,
});
expect(data.error).toBeUndefined();
expect(data.dryRun).toBe(true);
expect(data.typeCategory).toBe("object");
});
});
describe("change_function_parameter_type", () => {
const bpName = uniqueName("BP_ChangeFnParamTest");
const packagePath = "/Game/Test";
const funcName = "TestFunc";
const paramName = "TestParam";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
// Create a function graph with a parameter to test type changes
const graph = await uePost("/api/create-graph", {
blueprint: bpName,
graphName: funcName,
graphType: "function",
});
expect(graph.error).toBeUndefined();
const param = await uePost("/api/add-function-parameter", {
blueprint: bpName,
functionName: funcName,
paramName: paramName,
paramType: "bool",
});
expect(param.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("returns error for non-existent function", async () => {
const data = await uePost("/api/change-function-param-type", {
blueprint: bpName,
functionName: "NonExistentFunc_XYZ",
paramName: "SomeParam",
newType: "FVector",
});
expect(data.error).toBeDefined();
expect(data.availableFunctionsAndEvents).toBeDefined();
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/change-function-param-type", {
blueprint: "BP_DoesNotExist_XYZ_999",
functionName: "SomeFunc",
paramName: "SomeParam",
newType: "FVector",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/change-function-param-type", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("changes param to object:Actor", async () => {
const data = await uePost("/api/change-function-param-type", {
blueprint: bpName,
functionName: funcName,
paramName: paramName,
newType: "object:Actor",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("changes param to class:Actor (TSubclassOf)", async () => {
const data = await uePost("/api/change-function-param-type", {
blueprint: bpName,
functionName: funcName,
paramName: paramName,
newType: "class:Actor",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("changes param to enum type", async () => {
const data = await uePost("/api/change-function-param-type", {
blueprint: bpName,
functionName: funcName,
paramName: paramName,
newType: "ECollisionChannel",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("returns error for non-existent class in object reference", async () => {
const data = await uePost("/api/change-function-param-type", {
blueprint: bpName,
functionName: funcName,
paramName: paramName,
newType: "object:NonExistentClass_XYZ_999",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("NonExistentClass_XYZ_999");
});
});
describe("remove_function_parameter", () => {
const bpName = uniqueName("BP_RemoveParamTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("returns error for non-existent function", async () => {
const data = await uePost("/api/remove-function-parameter", {
blueprint: bpName,
functionName: "NonExistentFunc_XYZ",
paramName: "SomeParam",
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/remove-function-parameter", {
blueprint: "BP_DoesNotExist_XYZ_999",
functionName: "SomeFunc",
paramName: "SomeParam",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/remove-function-parameter", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
});
describe("change_struct_node_type", () => {
const bpName = uniqueName("BP_ChangeStructTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("returns error for non-existent node", async () => {
const data = await uePost("/api/change-struct-node-type", {
blueprint: bpName,
nodeId: "00000000-0000-0000-0000-000000000000",
newType: "FVector",
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/change-struct-node-type", {
blueprint: "BP_DoesNotExist_XYZ_999",
nodeId: "some-node-id",
newType: "FVector",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/change-struct-node-type", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,115 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("node_comments", () => {
const bpName = uniqueName("BP_NodeCommentTest");
const packagePath = "/Game/Test";
let testNodeId: string;
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
// Add a node to test comments on
const node = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(node.error).toBeUndefined();
testNodeId = node.nodeId;
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("gets an empty comment on a new node", async () => {
const data = await uePost("/api/get-node-comment", {
blueprint: bpName,
nodeId: testNodeId,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.comment).toBe("");
});
it("sets a comment on a node", async () => {
const data = await uePost("/api/set-node-comment", {
blueprint: bpName,
nodeId: testNodeId,
comment: "This prints a debug message",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.oldComment).toBe("");
expect(data.newComment).toBe("This prints a debug message");
expect(data.saved).toBe(true);
});
it("reads back the comment that was set", async () => {
const data = await uePost("/api/get-node-comment", {
blueprint: bpName,
nodeId: testNodeId,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.comment).toBe("This prints a debug message");
expect(data.commentBubbleVisible).toBe(true);
});
it("clears a comment by setting empty string", async () => {
const data = await uePost("/api/set-node-comment", {
blueprint: bpName,
nodeId: testNodeId,
comment: "",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.oldComment).toBe("This prints a debug message");
expect(data.newComment).toBe("");
expect(data.saved).toBe(true);
});
it("rejects missing blueprint field", async () => {
const data = await uePost("/api/set-node-comment", {
nodeId: testNodeId,
comment: "test",
});
expect(data.error).toBeDefined();
});
it("rejects missing nodeId field", async () => {
const data = await uePost("/api/set-node-comment", {
blueprint: bpName,
comment: "test",
});
expect(data.error).toBeDefined();
});
it("rejects missing comment field", async () => {
const data = await uePost("/api/set-node-comment", {
blueprint: bpName,
nodeId: testNodeId,
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent node", async () => {
const data = await uePost("/api/set-node-comment", {
blueprint: bpName,
nodeId: "00000000-0000-0000-0000-000000000000",
comment: "test",
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/get-node-comment", {
blueprint: "BP_Nonexistent_XYZ_999",
nodeId: testNodeId,
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,143 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("pin_introspection", () => {
const bpName = uniqueName("BP_PinInfoTest");
const packagePath = "/Game/Test";
let printStringNodeId: string;
let branchNodeId: string;
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
// Add a PrintString node
const ps = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(ps.error).toBeUndefined();
printStringNodeId = ps.nodeId;
// Add a Branch node
const br = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "Branch",
});
expect(br.error).toBeUndefined();
branchNodeId = br.nodeId;
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
// --- get_pin_info ---
it("gets pin info for an exec pin", async () => {
const data = await uePost("/api/get-pin-info", {
blueprint: bpName,
nodeId: printStringNodeId,
pinName: "execute",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.direction).toBe("Input");
expect(data.type).toBe("exec");
expect(data.isArray).toBe(false);
});
it("gets pin info for a data pin", async () => {
const data = await uePost("/api/get-pin-info", {
blueprint: bpName,
nodeId: branchNodeId,
pinName: "Condition",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.direction).toBe("Input");
expect(data.type).toBe("bool");
});
it("returns available pins when pin not found", async () => {
const data = await uePost("/api/get-pin-info", {
blueprint: bpName,
nodeId: printStringNodeId,
pinName: "FakePin",
});
expect(data.error).toBeDefined();
expect(data.availablePins).toBeDefined();
expect(data.availablePins.length).toBeGreaterThan(0);
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/get-pin-info", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("rejects non-existent node", async () => {
const data = await uePost("/api/get-pin-info", {
blueprint: bpName,
nodeId: "00000000-0000-0000-0000-000000000000",
pinName: "execute",
});
expect(data.error).toBeDefined();
});
// --- check_pin_compatibility ---
it("checks compatible exec pins", async () => {
const data = await uePost("/api/check-pin-compatibility", {
blueprint: bpName,
sourceNodeId: branchNodeId,
sourcePinName: "then",
targetNodeId: printStringNodeId,
targetPinName: "execute",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.compatible).toBe(true);
});
it("checks incompatible pins", async () => {
const data = await uePost("/api/check-pin-compatibility", {
blueprint: bpName,
sourceNodeId: branchNodeId,
sourcePinName: "Condition",
targetNodeId: printStringNodeId,
targetPinName: "InString",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
// bool -> string: may or may not be compatible depending on UE5's auto-conversion
// Just verify the response structure
expect(data.compatible).toBeDefined();
expect(data.connectionType).toBeDefined();
expect(data.sourcePinType).toBeDefined();
expect(data.targetPinType).toBeDefined();
});
it("rejects missing fields for compatibility check", async () => {
const data = await uePost("/api/check-pin-compatibility", {
blueprint: bpName,
sourceNodeId: branchNodeId,
});
expect(data.error).toBeDefined();
});
it("rejects non-existent source node", async () => {
const data = await uePost("/api/check-pin-compatibility", {
blueprint: bpName,
sourceNodeId: "00000000-0000-0000-0000-000000000000",
sourcePinName: "then",
targetNodeId: printStringNodeId,
targetPinName: "execute",
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest";
import { uePost } from "../helpers.js";
describe("analyze_rebuild_impact", () => {
it("analyzes impact for the Engine module", async () => {
const data = await uePost("/api/analyze-rebuild-impact", {
moduleName: "Engine",
});
expect(data.error).toBeUndefined();
expect(data.summary).toBeDefined();
expect(typeof data.summary.totalBlueprints).toBe("number");
expect(typeof data.summary.totalBreakMakeNodes).toBe("number");
expect(typeof data.summary.totalConnectionsAtRisk).toBe("number");
});
it("analyzes impact with specific struct names", async () => {
const data = await uePost("/api/analyze-rebuild-impact", {
moduleName: "Engine",
structNames: ["Vector"],
});
expect(data.error).toBeUndefined();
expect(data.summary).toBeDefined();
});
it("handles non-existent module gracefully", async () => {
const data = await uePost("/api/analyze-rebuild-impact", {
moduleName: "NonExistentModule_XYZ_999",
});
// Should succeed with zero affected (module name is used as a package name filter)
expect(data.error).toBeUndefined();
expect(data.summary).toBeDefined();
expect(data.summary.totalBlueprints).toBe(0);
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/analyze-rebuild-impact", {});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,40 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("refresh_all_nodes", () => {
const bpName = uniqueName("BP_RefreshTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("refreshes all nodes in a Blueprint", async () => {
const data = await uePost("/api/refresh-all-nodes", {
blueprint: bpName,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.blueprint).toBe(bpName);
expect(typeof data.graphCount).toBe("number");
expect(typeof data.nodeCount).toBe("number");
expect(data.saved).toBe(true);
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/refresh-all-nodes", {
blueprint: "BP_DoesNotExist_XYZ_999",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/refresh-all-nodes", {});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,48 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("rename_asset", () => {
const bpName = uniqueName("BP_RenameTest");
const packagePath = "/Game/Test";
const newName = uniqueName("BP_Renamed");
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
// Clean up the renamed asset (or original if rename failed)
await deleteTestBlueprint(`${packagePath}/${newName}`);
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("renames a Blueprint asset", async () => {
const data = await uePost("/api/rename-asset", {
assetPath: `${packagePath}/${bpName}`,
newPath: `${packagePath}/${newName}`,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.oldPath).toBe(`${packagePath}/${bpName}`);
expect(data.newPath).toBe(`${packagePath}/${newName}`);
});
it("verifies renamed asset exists under new name", async () => {
const list = await ueGet("/api/list", { filter: newName });
expect(list.count).toBeGreaterThanOrEqual(1);
});
it("returns error for non-existent source asset", async () => {
const data = await uePost("/api/rename-asset", {
assetPath: "/Game/Test/BP_DoesNotExist_XYZ_999",
newPath: "/Game/Test/BP_Whatever",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/rename-asset", {});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,58 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("reparent_blueprint", () => {
const bpName = uniqueName("BP_ReparentTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
// Create an Actor-based BP
const bp = await createTestBlueprint({ name: bpName, parentClass: "Actor" });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("reparents a Blueprint to a new parent class", async () => {
const data = await uePost("/api/reparent-blueprint", {
blueprint: bpName,
newParentClass: "Pawn",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.blueprint).toBe(bpName);
expect(data.newParentClass).toContain("Pawn");
expect(data.saved).toBe(true);
});
it("verifies parent class changed via get_blueprint", async () => {
const info = await ueGet("/api/blueprint", { name: bpName });
expect(info.error).toBeUndefined();
expect(info.parentClass).toContain("Pawn");
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/reparent-blueprint", {
blueprint: "BP_DoesNotExist_XYZ_999",
newParentClass: "Pawn",
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent parent class", async () => {
const data = await uePost("/api/reparent-blueprint", {
blueprint: bpName,
newParentClass: "NonExistentClass_XYZ_999",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/reparent-blueprint", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,92 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { ueGet, uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("search_blueprints", () => {
const bpName = uniqueName("BP_SearchTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
// Add a PrintString node so we have something to search for
await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("finds nodes matching a query", async () => {
const data = await ueGet("/api/search", {
query: "PrintString",
path: "/Game/Test",
});
expect(data.error).toBeUndefined();
expect(data.resultCount).toBeGreaterThanOrEqual(1);
expect(Array.isArray(data.results)).toBe(true);
});
it("returns empty results for non-matching query", async () => {
const data = await ueGet("/api/search", {
query: "ZZZ_NonExistentFunction_XYZ",
path: "/Game/Test",
});
expect(data.error).toBeUndefined();
expect(data.resultCount).toBe(0);
});
it("rejects missing query parameter", async () => {
const data = await ueGet("/api/search", {});
expect(data.error).toBeDefined();
});
it("regular BP search results do not have isLevelBlueprint", async () => {
const data = await ueGet("/api/search", {
query: "PrintString",
path: "/Game/Test",
});
expect(data.error).toBeUndefined();
for (const r of data.results) {
expect(r.isLevelBlueprint).toBeUndefined();
}
});
});
describe("search_by_type", () => {
it("returns flat results array with usage field", async () => {
const data = await ueGet("/api/search-by-type", { typeName: "Vector" });
expect(data.error).toBeUndefined();
expect(Array.isArray(data.results)).toBe(true);
expect(typeof data.resultCount).toBe("number");
// If results exist, each should have a usage field
for (const r of data.results) {
expect(r.usage).toBeDefined();
expect(r.blueprint).toBeDefined();
expect(r.blueprintPath).toBeDefined();
}
});
it("returns empty for non-existent type", async () => {
const data = await ueGet("/api/search-by-type", {
typeName: "FNonExistentType_XYZ_999",
});
expect(data.error).toBeUndefined();
expect(data.resultCount).toBe(0);
});
it("regular BP results do not have isLevelBlueprint", async () => {
const data = await ueGet("/api/search-by-type", {
typeName: "Vector",
filter: "/Game/Test",
});
expect(data.error).toBeUndefined();
for (const r of data.results) {
expect(r.isLevelBlueprint).toBeUndefined();
}
});
});

View File

@@ -0,0 +1,17 @@
import { describe, it, expect } from "vitest";
import { ueGet } from "../helpers.js";
describe("server_status", () => {
it("returns health information", async () => {
const data = await ueGet("/api/health");
expect(data.status).toBe("ok");
expect(typeof data.blueprintCount).toBe("number");
expect(typeof data.mapCount).toBe("number");
expect(data.mode).toBeDefined();
});
it("reports commandlet mode in test environment", async () => {
const data = await ueGet("/api/health");
expect(data.mode).toBe("commandlet");
});
});

View File

@@ -0,0 +1,106 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("set_blueprint_default", () => {
const bpName = uniqueName("BP_SetDefaultTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("returns error for non-existent property", async () => {
const data = await uePost("/api/set-blueprint-default", {
blueprint: bpName,
property: "NonExistentProperty_XYZ",
value: "true",
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/set-blueprint-default", {
blueprint: "BP_DoesNotExist_XYZ_999",
property: "SomeProperty",
value: "true",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/set-blueprint-default", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
});
describe("set_pin_default", () => {
const bpName = uniqueName("BP_SetPinTest");
const packagePath = "/Game/Test";
let printNodeId: string;
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
// Add a PrintString node — it has an "InString" pin with a default value
const res = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
expect(res.success).toBe(true);
printNodeId = res.nodeId;
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("sets a pin default value on PrintString's InString pin", async () => {
const data = await uePost("/api/set-pin-default", {
blueprint: bpName,
nodeId: printNodeId,
pinName: "InString",
value: "Hello Test",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.newValue).toBe("Hello Test");
expect(data.saved).toBe(true);
});
it("returns error for non-existent pin", async () => {
const data = await uePost("/api/set-pin-default", {
blueprint: bpName,
nodeId: printNodeId,
pinName: "FakePin_XYZ",
value: "test",
});
expect(data.error).toBeDefined();
});
it("returns error for non-existent node", async () => {
const data = await uePost("/api/set-pin-default", {
blueprint: bpName,
nodeId: "00000000-0000-0000-0000-000000000000",
pinName: "InString",
value: "test",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/set-pin-default", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,112 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("snapshot_graph / diff_graph / restore_graph", () => {
const bpName = uniqueName("BP_SnapshotTest");
const packagePath = "/Game/Test";
let snapshotId: string;
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
// Add a PrintString node so there's some graph content
await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "CallFunction",
functionName: "PrintString",
});
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
describe("snapshot_graph", () => {
it("creates a snapshot of EventGraph", async () => {
const data = await uePost("/api/snapshot-graph", {
blueprint: bpName,
graph: "EventGraph",
});
expect(data.error).toBeUndefined();
expect(data.snapshotId).toBeDefined();
expect(data.blueprint).toBe(bpName);
expect(data.totalConnections).toBeDefined();
snapshotId = data.snapshotId;
});
it("creates a snapshot of all graphs when graph is omitted", async () => {
const data = await uePost("/api/snapshot-graph", {
blueprint: bpName,
});
expect(data.error).toBeUndefined();
expect(data.snapshotId).toBeDefined();
expect(Array.isArray(data.graphs)).toBe(true);
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/snapshot-graph", {
blueprint: "BP_DoesNotExist_XYZ_999",
});
expect(data.error).toBeDefined();
});
});
describe("diff_graph", () => {
it("diffs current state against snapshot (no changes)", async () => {
expect(snapshotId).toBeDefined();
const data = await uePost("/api/diff-graph", {
blueprint: bpName,
snapshotId,
});
expect(data.error).toBeUndefined();
expect(data.summary).toBeDefined();
// No changes since snapshot
expect(data.summary.severedConnections).toBe(0);
expect(data.summary.missingNodes).toBe(0);
});
it("returns error for invalid snapshot ID", async () => {
const data = await uePost("/api/diff-graph", {
blueprint: bpName,
snapshotId: "invalid-snapshot-id-xyz",
});
expect(data.error).toBeDefined();
});
});
describe("restore_graph", () => {
it("restores from snapshot in dry-run mode", async () => {
expect(snapshotId).toBeDefined();
const data = await uePost("/api/restore-graph", {
blueprint: bpName,
snapshotId,
dryRun: true,
});
expect(data.error).toBeUndefined();
expect(data.status).toBe("ok");
expect(typeof data.reconnected).toBe("number");
expect(typeof data.failed).toBe("number");
});
it("restores from snapshot (actual)", async () => {
expect(snapshotId).toBeDefined();
const data = await uePost("/api/restore-graph", {
blueprint: bpName,
snapshotId,
});
expect(data.error).toBeUndefined();
expect(data.status).toBe("ok");
expect(typeof data.reconnected).toBe("number");
});
it("returns error for invalid snapshot ID", async () => {
const data = await uePost("/api/restore-graph", {
blueprint: bpName,
snapshotId: "invalid-snapshot-id-xyz",
});
expect(data.error).toBeDefined();
});
});
});

View File

@@ -0,0 +1,115 @@
import { describe, it, expect, afterAll } from "vitest";
import { uePost, uniqueName } from "../helpers.js";
describe("user_types", () => {
const structPath = `/Game/Test/${uniqueName("S_TestStruct")}`;
const enumPath = `/Game/Test/${uniqueName("E_TestEnum")}`;
afterAll(async () => {
// Clean up created assets
await uePost("/api/delete-asset", { assetPath: structPath });
await uePost("/api/delete-asset", { assetPath: enumPath });
});
// --- create_struct ---
it("creates a struct with properties", async () => {
const data = await uePost("/api/create-struct", {
assetPath: structPath,
properties: [
{ name: "Health", type: "float" },
{ name: "Name", type: "string" },
{ name: "IsAlive", type: "bool" },
],
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.assetPath).toBe(structPath);
expect(data.propertiesAdded).toBe(3);
expect(data.saved).toBe(true);
});
it("rejects creating struct at existing path", async () => {
const data = await uePost("/api/create-struct", {
assetPath: structPath,
});
expect(data.error).toBeDefined();
});
it("rejects missing assetPath for struct", async () => {
const data = await uePost("/api/create-struct", {});
expect(data.error).toBeDefined();
});
// --- add_struct_property ---
it("adds a property to existing struct", async () => {
const data = await uePost("/api/add-struct-property", {
assetPath: structPath,
name: "Score",
type: "int",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.propertyName).toBe("Score");
expect(data.saved).toBe(true);
});
it("rejects adding to non-existent struct", async () => {
const data = await uePost("/api/add-struct-property", {
assetPath: "/Game/Test/S_Nonexistent_XYZ_999",
name: "Foo",
type: "bool",
});
expect(data.error).toBeDefined();
});
// --- remove_struct_property ---
it("removes a property from struct", async () => {
const data = await uePost("/api/remove-struct-property", {
assetPath: structPath,
name: "Score",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.removedProperty).toBe("Score");
expect(data.saved).toBe(true);
});
it("returns error for non-existent property", async () => {
const data = await uePost("/api/remove-struct-property", {
assetPath: structPath,
name: "FakeProperty_XYZ",
});
expect(data.error).toBeDefined();
expect(data.availableProperties).toBeDefined();
});
// --- create_enum ---
it("creates an enum with values", async () => {
const data = await uePost("/api/create-enum", {
assetPath: enumPath,
values: ["Low", "Medium", "High", "Critical"],
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.assetPath).toBe(enumPath);
expect(data.valueCount).toBe(4);
expect(data.saved).toBe(true);
});
it("rejects enum with no values", async () => {
const data = await uePost("/api/create-enum", {
assetPath: "/Game/Test/E_Empty",
values: [],
});
expect(data.error).toBeDefined();
});
it("rejects missing assetPath for enum", async () => {
const data = await uePost("/api/create-enum", { values: ["A"] });
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,140 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("validate_blueprint", () => {
const bpName = uniqueName("BP_ValidateTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const bp = await createTestBlueprint({ name: bpName });
expect(bp.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("validates a clean Blueprint successfully", async () => {
const data = await uePost("/api/validate-blueprint", {
blueprint: bpName,
});
expect(data.error).toBeUndefined();
expect(data.blueprint).toBe(bpName);
expect(data.isValid).toBe(true);
});
it("returns error for non-existent blueprint", async () => {
const data = await uePost("/api/validate-blueprint", {
blueprint: "BP_DoesNotExist_XYZ_999",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/validate-blueprint", {});
expect(data.error).toBeDefined();
});
});
describe("validate_all_blueprints", () => {
it("validates all blueprints in /Game/Test path", async () => {
const data = await uePost("/api/validate-all-blueprints", {
filter: "/Game/Test",
});
expect(data.error).toBeUndefined();
expect(typeof data.totalChecked).toBe("number");
expect(typeof data.totalPassed).toBe("number");
expect(typeof data.totalFailed).toBe("number");
expect(typeof data.totalMatching).toBe("number");
});
it("validates with no filter (may take longer)", async () => {
const data = await uePost("/api/validate-all-blueprints", {});
expect(data.error).toBeUndefined();
expect(data.totalChecked).toBeGreaterThanOrEqual(0);
expect(typeof data.totalMatching).toBe("number");
});
});
describe("validate_all_blueprints pagination", () => {
const bpNames = [
uniqueName("BP_ValidatePag_A"),
uniqueName("BP_ValidatePag_B"),
uniqueName("BP_ValidatePag_C"),
];
const packagePath = "/Game/Test";
beforeAll(async () => {
for (const name of bpNames) {
const bp = await createTestBlueprint({ name });
expect(bp.error).toBeUndefined();
}
});
afterAll(async () => {
for (const name of bpNames) {
await deleteTestBlueprint(`${packagePath}/${name}`);
}
});
it("countOnly returns totalMatching without validation fields", async () => {
const data = await uePost("/api/validate-all-blueprints", {
filter: "BP_ValidatePag_",
countOnly: true,
});
expect(data.error).toBeUndefined();
expect(data.totalMatching).toBeGreaterThanOrEqual(3);
// countOnly should NOT include validation result fields
expect(data.totalChecked).toBeUndefined();
expect(data.totalPassed).toBeUndefined();
expect(data.totalFailed).toBeUndefined();
expect(data.failed).toBeUndefined();
});
it("countOnly with non-matching filter returns 0", async () => {
const data = await uePost("/api/validate-all-blueprints", {
filter: "BP_NoSuchBlueprint_ZZZ_999",
countOnly: true,
});
expect(data.error).toBeUndefined();
expect(data.totalMatching).toBe(0);
});
it("offset + limit respects limit on totalChecked", async () => {
const data = await uePost("/api/validate-all-blueprints", {
filter: "BP_ValidatePag_",
offset: 0,
limit: 2,
});
expect(data.error).toBeUndefined();
expect(data.totalMatching).toBeGreaterThanOrEqual(3);
expect(data.totalChecked).toBeLessThanOrEqual(2);
});
it("offset beyond total returns totalChecked: 0", async () => {
const data = await uePost("/api/validate-all-blueprints", {
filter: "BP_ValidatePag_",
offset: 9999,
limit: 10,
});
expect(data.error).toBeUndefined();
expect(data.totalMatching).toBeGreaterThanOrEqual(3);
expect(data.totalChecked).toBe(0);
});
it("offset=0, limit=0 matches unparameterized call (backward compat)", async () => {
const withParams = await uePost("/api/validate-all-blueprints", {
filter: "BP_ValidatePag_",
offset: 0,
limit: 0,
});
const withoutParams = await uePost("/api/validate-all-blueprints", {
filter: "BP_ValidatePag_",
});
expect(withParams.error).toBeUndefined();
expect(withoutParams.error).toBeUndefined();
expect(withParams.totalChecked).toBe(withoutParams.totalChecked);
expect(withParams.totalPassed).toBe(withoutParams.totalPassed);
expect(withParams.totalFailed).toBe(withoutParams.totalFailed);
});
});

View File

@@ -0,0 +1,157 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("set_variable_metadata", () => {
const bpName = uniqueName("BP_VarMetadataTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
// Add test variables
const v1 = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "Health",
variableType: "float",
});
expect(v1.error).toBeUndefined();
const v2 = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "bIsAlive",
variableType: "bool",
});
expect(v2.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
it("sets category on a variable", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "Health",
category: "Stats",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.changes).toHaveLength(1);
expect(data.changes[0].field).toBe("category");
expect(data.changes[0].newValue).toBe("Stats");
expect(data.saved).toBe(true);
});
it("sets tooltip on a variable", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "Health",
tooltip: "Current health points",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.changes).toHaveLength(1);
expect(data.changes[0].field).toBe("tooltip");
expect(data.saved).toBe(true);
});
it("sets multiple fields at once", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "bIsAlive",
category: "State",
tooltip: "Whether the character is alive",
editability: "editAnywhere",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.changes.length).toBeGreaterThanOrEqual(3);
expect(data.saved).toBe(true);
});
it("sets replication to replicated", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "Health",
replication: "replicated",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
const repChange = data.changes.find((c: any) => c.field === "replication");
expect(repChange).toBeDefined();
expect(repChange.newValue).toBe("replicated");
});
it("sets exposeOnSpawn", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "Health",
exposeOnSpawn: true,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("sets editability to editDefaultsOnly", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "Health",
editability: "editDefaultsOnly",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
});
it("rejects invalid replication value", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "Health",
replication: "invalid_value",
});
expect(data.error).toBeDefined();
});
it("rejects invalid editability value", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "Health",
editability: "invalid_value",
});
expect(data.error).toBeDefined();
});
it("rejects no metadata fields provided", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "Health",
});
expect(data.error).toBeDefined();
});
it("rejects non-existent variable", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
variable: "NonExistentVar_XYZ",
category: "Test",
});
expect(data.error).toBeDefined();
expect(data.availableVariables).toBeDefined();
});
it("rejects non-existent blueprint", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: "BP_Nonexistent_XYZ_999",
variable: "Health",
category: "Test",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/set-variable-metadata", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
});

View File

@@ -0,0 +1,210 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { uePost, ueGet, createTestBlueprint, deleteTestBlueprint, uniqueName } from "../helpers.js";
describe("add_variable / remove_variable", () => {
const bpName = uniqueName("BP_VariablesTest");
const packagePath = "/Game/Test";
beforeAll(async () => {
const res = await createTestBlueprint({ name: bpName });
expect(res.error).toBeUndefined();
});
afterAll(async () => {
await deleteTestBlueprint(`${packagePath}/${bpName}`);
});
// --- add_variable tests ---
it("adds a bool variable", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "bIsActive",
variableType: "bool",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.variableName).toBe("bIsActive");
expect(data.variableType).toBe("bool");
expect(data.saved).toBe(true);
});
it("adds a float variable with category", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "Health",
variableType: "float",
category: "Stats",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.variableName).toBe("Health");
expect(data.category).toBe("Stats");
expect(data.saved).toBe(true);
});
it("adds an int variable", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "Score",
variableType: "int",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.variableName).toBe("Score");
expect(data.saved).toBe(true);
});
it("adds a string variable", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "PlayerName",
variableType: "string",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.variableName).toBe("PlayerName");
expect(data.saved).toBe(true);
});
it("adds a vector variable (built-in struct)", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "SpawnLocation",
variableType: "vector",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.variableName).toBe("SpawnLocation");
expect(data.saved).toBe(true);
});
it("adds an array variable", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "Inventory",
variableType: "string",
isArray: true,
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.variableName).toBe("Inventory");
expect(data.isArray).toBe(true);
expect(data.saved).toBe(true);
});
it("variable appears in get_blueprint response", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
const varNames = data.variables.map((v: any) => v.name);
expect(varNames).toContain("bIsActive");
expect(varNames).toContain("Health");
expect(varNames).toContain("Score");
});
it("rejects duplicate variable name", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "bIsActive",
variableType: "bool",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("already exists");
});
it("rejects unknown type", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
variableName: "BadVar",
variableType: "NonExistentType_XYZ",
});
expect(data.error).toBeDefined();
});
it("rejects non-existent blueprint", async () => {
const data = await uePost("/api/add-variable", {
blueprint: "BP_Nonexistent_XYZ_999",
variableName: "Foo",
variableType: "bool",
});
expect(data.error).toBeDefined();
});
it("rejects missing required fields", async () => {
const data = await uePost("/api/add-variable", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
// --- VariableGet/VariableSet nodes for the new variable ---
it("can add a VariableGet node for the new variable", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "VariableGet",
variableName: "Health",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
});
it("can add a VariableSet node for the new variable", async () => {
const data = await uePost("/api/add-node", {
blueprint: bpName,
graph: "EventGraph",
nodeType: "VariableSet",
variableName: "Health",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.nodeId).toBeDefined();
});
// --- remove_variable tests ---
it("removes a variable", async () => {
const data = await uePost("/api/remove-variable", {
blueprint: bpName,
variableName: "Score",
});
expect(data.error).toBeUndefined();
expect(data.success).toBe(true);
expect(data.variableName).toBe("Score");
expect(data.saved).toBe(true);
});
it("removed variable is gone from get_blueprint", async () => {
const data = await ueGet("/api/blueprint", { name: bpName });
expect(data.error).toBeUndefined();
const varNames = data.variables.map((v: any) => v.name);
expect(varNames).not.toContain("Score");
});
it("rejects removal of non-existent variable", async () => {
const data = await uePost("/api/remove-variable", {
blueprint: bpName,
variableName: "NonExistentVar_XYZ",
});
expect(data.error).toBeDefined();
expect(data.error).toContain("not found");
expect(data.availableVariables).toBeDefined();
});
it("rejects removal with missing required fields", async () => {
const data = await uePost("/api/remove-variable", {
blueprint: bpName,
});
expect(data.error).toBeDefined();
});
it("rejects removal from non-existent blueprint", async () => {
const data = await uePost("/api/remove-variable", {
blueprint: "BP_Nonexistent_XYZ_999",
variableName: "Foo",
});
expect(data.error).toBeDefined();
});
});