Initial checkin of Blueprint-MCP plugin
This commit is contained in:
225
tools/blueprint-mcp/test/bootstrap.ts
Normal file
225
tools/blueprint-mcp/test/bootstrap.ts
Normal 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;
|
||||
}
|
||||
151
tools/blueprint-mcp/test/helpers.ts
Normal file
151
tools/blueprint-mcp/test/helpers.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
36
tools/blueprint-mcp/test/setup.ts
Normal file
36
tools/blueprint-mcp/test/setup.ts
Normal 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.");
|
||||
}
|
||||
243
tools/blueprint-mcp/test/tools/add-node.test.ts
Normal file
243
tools/blueprint-mcp/test/tools/add-node.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
255
tools/blueprint-mcp/test/tools/animation-mutation.test.ts
Normal file
255
tools/blueprint-mcp/test/tools/animation-mutation.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
36
tools/blueprint-mcp/test/tools/animation-read.test.ts
Normal file
36
tools/blueprint-mcp/test/tools/animation-read.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
115
tools/blueprint-mcp/test/tools/batch-set-pin-default.test.ts
Normal file
115
tools/blueprint-mcp/test/tools/batch-set-pin-default.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
66
tools/blueprint-mcp/test/tools/blueprint-info.test.ts
Normal file
66
tools/blueprint-mcp/test/tools/blueprint-info.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
132
tools/blueprint-mcp/test/tools/class-discovery.test.ts
Normal file
132
tools/blueprint-mcp/test/tools/class-discovery.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
193
tools/blueprint-mcp/test/tools/components.test.ts
Normal file
193
tools/blueprint-mcp/test/tools/components.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
86
tools/blueprint-mcp/test/tools/connect-pins.test.ts
Normal file
86
tools/blueprint-mcp/test/tools/connect-pins.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
62
tools/blueprint-mcp/test/tools/create-blueprint.test.ts
Normal file
62
tools/blueprint-mcp/test/tools/create-blueprint.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
121
tools/blueprint-mcp/test/tools/create-graph.test.ts
Normal file
121
tools/blueprint-mcp/test/tools/create-graph.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
37
tools/blueprint-mcp/test/tools/delete-asset.test.ts
Normal file
37
tools/blueprint-mcp/test/tools/delete-asset.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
87
tools/blueprint-mcp/test/tools/delete-node.test.ts
Normal file
87
tools/blueprint-mcp/test/tools/delete-node.test.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
});
|
||||
82
tools/blueprint-mcp/test/tools/diff-blueprints.test.ts
Normal file
82
tools/blueprint-mcp/test/tools/diff-blueprints.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
53
tools/blueprint-mcp/test/tools/disconnected-pins.test.ts
Normal file
53
tools/blueprint-mcp/test/tools/disconnected-pins.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
121
tools/blueprint-mcp/test/tools/duplicate-nodes.test.ts
Normal file
121
tools/blueprint-mcp/test/tools/duplicate-nodes.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
252
tools/blueprint-mcp/test/tools/event-dispatchers.test.ts
Normal file
252
tools/blueprint-mcp/test/tools/event-dispatchers.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
39
tools/blueprint-mcp/test/tools/find-references.test.ts
Normal file
39
tools/blueprint-mcp/test/tools/find-references.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
56
tools/blueprint-mcp/test/tools/get-blueprint.test.ts
Normal file
56
tools/blueprint-mcp/test/tools/get-blueprint.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
165
tools/blueprint-mcp/test/tools/graph-management.test.ts
Normal file
165
tools/blueprint-mcp/test/tools/graph-management.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
186
tools/blueprint-mcp/test/tools/interfaces.test.ts
Normal file
186
tools/blueprint-mcp/test/tools/interfaces.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
74
tools/blueprint-mcp/test/tools/list-blueprints.test.ts
Normal file
74
tools/blueprint-mcp/test/tools/list-blueprints.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
84
tools/blueprint-mcp/test/tools/material-instance.test.ts
Normal file
84
tools/blueprint-mcp/test/tools/material-instance.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
349
tools/blueprint-mcp/test/tools/material-mutation.test.ts
Normal file
349
tools/blueprint-mcp/test/tools/material-mutation.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
149
tools/blueprint-mcp/test/tools/material-read.test.ts
Normal file
149
tools/blueprint-mcp/test/tools/material-read.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
129
tools/blueprint-mcp/test/tools/move-node.test.ts
Normal file
129
tools/blueprint-mcp/test/tools/move-node.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
370
tools/blueprint-mcp/test/tools/mutation.test.ts
Normal file
370
tools/blueprint-mcp/test/tools/mutation.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
115
tools/blueprint-mcp/test/tools/node-comments.test.ts
Normal file
115
tools/blueprint-mcp/test/tools/node-comments.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
143
tools/blueprint-mcp/test/tools/pin-introspection.test.ts
Normal file
143
tools/blueprint-mcp/test/tools/pin-introspection.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
39
tools/blueprint-mcp/test/tools/rebuild-impact.test.ts
Normal file
39
tools/blueprint-mcp/test/tools/rebuild-impact.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
40
tools/blueprint-mcp/test/tools/refresh-nodes.test.ts
Normal file
40
tools/blueprint-mcp/test/tools/refresh-nodes.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
48
tools/blueprint-mcp/test/tools/rename-asset.test.ts
Normal file
48
tools/blueprint-mcp/test/tools/rename-asset.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
58
tools/blueprint-mcp/test/tools/reparent.test.ts
Normal file
58
tools/blueprint-mcp/test/tools/reparent.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
92
tools/blueprint-mcp/test/tools/search.test.ts
Normal file
92
tools/blueprint-mcp/test/tools/search.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
17
tools/blueprint-mcp/test/tools/server-status.test.ts
Normal file
17
tools/blueprint-mcp/test/tools/server-status.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
106
tools/blueprint-mcp/test/tools/set-defaults.test.ts
Normal file
106
tools/blueprint-mcp/test/tools/set-defaults.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
112
tools/blueprint-mcp/test/tools/snapshot-restore.test.ts
Normal file
112
tools/blueprint-mcp/test/tools/snapshot-restore.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
115
tools/blueprint-mcp/test/tools/user-types.test.ts
Normal file
115
tools/blueprint-mcp/test/tools/user-types.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
140
tools/blueprint-mcp/test/tools/validate.test.ts
Normal file
140
tools/blueprint-mcp/test/tools/validate.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
157
tools/blueprint-mcp/test/tools/variable-metadata.test.ts
Normal file
157
tools/blueprint-mcp/test/tools/variable-metadata.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
210
tools/blueprint-mcp/test/tools/variables.test.ts
Normal file
210
tools/blueprint-mcp/test/tools/variables.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user