unreal-engine-mcp-server 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +3 -2
- package/README.md +21 -5
- package/dist/index.js +124 -31
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.js +46 -62
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +29 -54
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +52 -44
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +190 -45
- package/dist/tools/consolidated-tool-definitions.js +78 -252
- package/dist/tools/consolidated-tool-handlers.js +506 -133
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +167 -31
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +97 -23
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +125 -22
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.js +28 -2
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +10 -9
- package/server.json +37 -14
- package/src/index.ts +130 -33
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +48 -51
- package/src/resources/levels.ts +35 -45
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +53 -43
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +266 -64
- package/src/tools/consolidated-tool-definitions.ts +90 -264
- package/src/tools/consolidated-tool-handlers.ts +630 -121
- package/src/tools/debug.ts +176 -33
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +102 -24
- package/src/tools/sequence.ts +136 -28
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +25 -2
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { performance } from 'node:perf_hooks';
|
|
8
|
+
|
|
9
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
10
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
11
|
+
|
|
12
|
+
const failureKeywords = [
|
|
13
|
+
'error',
|
|
14
|
+
'fail',
|
|
15
|
+
'invalid',
|
|
16
|
+
'missing',
|
|
17
|
+
'not found',
|
|
18
|
+
'reject',
|
|
19
|
+
'warning'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const successKeywords = [
|
|
23
|
+
'success',
|
|
24
|
+
'spawn',
|
|
25
|
+
'visible',
|
|
26
|
+
'applied',
|
|
27
|
+
'returns',
|
|
28
|
+
'plays',
|
|
29
|
+
'updates',
|
|
30
|
+
'created',
|
|
31
|
+
'saved'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
36
|
+
const defaultDocPath = path.resolve(repoRoot, 'docs', 'unreal-tool-test-cases.md');
|
|
37
|
+
const docPath = path.resolve(repoRoot, process.env.UNREAL_MCP_TEST_DOC ?? defaultDocPath);
|
|
38
|
+
const reportsDir = path.resolve(repoRoot, 'tests', 'reports');
|
|
39
|
+
const resultsPath = path.join(reportsDir, `unreal-tool-test-results-${new Date().toISOString().replace(/[:]/g, '-')}.json`);
|
|
40
|
+
const defaultFbxDir = normalizeWindowsPath(process.env.UNREAL_MCP_FBX_DIR ?? 'C:\\Users\\micro\\Downloads\\Compressed\\fbx');
|
|
41
|
+
const defaultFbxFile = normalizeWindowsPath(process.env.UNREAL_MCP_FBX_FILE ?? path.join(defaultFbxDir, 'test_model.fbx'));
|
|
42
|
+
|
|
43
|
+
const cliOptions = parseCliOptions(process.argv.slice(2));
|
|
44
|
+
const serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? 'node';
|
|
45
|
+
const serverArgs = parseArgsList(process.env.UNREAL_MCP_SERVER_ARGS) ?? [path.join(repoRoot, 'dist', 'cli.js')];
|
|
46
|
+
const serverCwd = process.env.UNREAL_MCP_SERVER_CWD ?? repoRoot;
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
await ensureFbxDirectory();
|
|
50
|
+
const allCases = await loadTestCasesFromDoc(docPath);
|
|
51
|
+
if (allCases.length === 0) {
|
|
52
|
+
console.error(`No test cases detected in ${docPath}.`);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const filteredCases = allCases.filter((testCase) => {
|
|
58
|
+
if (cliOptions.group && testCase.groupName !== cliOptions.group) return false;
|
|
59
|
+
if (cliOptions.caseId && testCase.caseId !== cliOptions.caseId) return false;
|
|
60
|
+
if (cliOptions.text && !testCase.scenario.toLowerCase().includes(cliOptions.text.toLowerCase())) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (filteredCases.length === 0) {
|
|
67
|
+
console.warn('No test cases matched the provided filters. Exiting.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let transport; let client;
|
|
72
|
+
const runResults = [];
|
|
73
|
+
|
|
74
|
+
if (!cliOptions.dryRun) {
|
|
75
|
+
try {
|
|
76
|
+
transport = new StdioClientTransport({
|
|
77
|
+
command: serverCommand,
|
|
78
|
+
args: serverArgs,
|
|
79
|
+
cwd: serverCwd,
|
|
80
|
+
stderr: 'inherit'
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
client = new Client({
|
|
84
|
+
name: 'unreal-mcp-tool-test-runner',
|
|
85
|
+
version: '0.1.0'
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await client.connect(transport);
|
|
89
|
+
await client.listTools({});
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('Failed to start or initialize MCP server:', err);
|
|
92
|
+
if (transport) {
|
|
93
|
+
try { await transport.close(); } catch { /* ignore */ }
|
|
94
|
+
}
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const testCase of filteredCases) {
|
|
101
|
+
if (testCase.skipReason) {
|
|
102
|
+
runResults.push({
|
|
103
|
+
...testCase,
|
|
104
|
+
status: 'skipped',
|
|
105
|
+
detail: testCase.skipReason
|
|
106
|
+
});
|
|
107
|
+
console.log(formatResultLine(testCase, 'skipped', testCase.skipReason));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (cliOptions.dryRun) {
|
|
112
|
+
runResults.push({
|
|
113
|
+
...testCase,
|
|
114
|
+
status: 'skipped',
|
|
115
|
+
detail: 'Dry run'
|
|
116
|
+
});
|
|
117
|
+
console.log(formatResultLine(testCase, 'skipped', 'Dry run'));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const started = performance.now();
|
|
122
|
+
try {
|
|
123
|
+
const response = await client.callTool({
|
|
124
|
+
name: testCase.toolName,
|
|
125
|
+
arguments: testCase.arguments
|
|
126
|
+
});
|
|
127
|
+
const duration = performance.now() - started;
|
|
128
|
+
const evaluation = evaluateExpectation(testCase, response);
|
|
129
|
+
runResults.push({
|
|
130
|
+
...testCase,
|
|
131
|
+
status: evaluation.passed ? 'passed' : 'failed',
|
|
132
|
+
durationMs: duration,
|
|
133
|
+
detail: evaluation.reason,
|
|
134
|
+
response
|
|
135
|
+
});
|
|
136
|
+
console.log(formatResultLine(testCase, evaluation.passed ? 'passed' : 'failed', evaluation.reason, duration));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const duration = performance.now() - started;
|
|
139
|
+
runResults.push({
|
|
140
|
+
...testCase,
|
|
141
|
+
status: 'failed',
|
|
142
|
+
durationMs: duration,
|
|
143
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
144
|
+
});
|
|
145
|
+
console.log(formatResultLine(testCase, 'failed', err instanceof Error ? err.message : String(err), duration));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!cliOptions.dryRun) {
|
|
150
|
+
try {
|
|
151
|
+
await client.close();
|
|
152
|
+
} catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
await transport.close();
|
|
157
|
+
} catch {
|
|
158
|
+
// ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await persistResults(runResults);
|
|
163
|
+
summarize(runResults);
|
|
164
|
+
|
|
165
|
+
if (runResults.some((result) => result.status === 'failed')) {
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseCliOptions(args) {
|
|
171
|
+
const options = {
|
|
172
|
+
dryRun: false,
|
|
173
|
+
group: undefined,
|
|
174
|
+
caseId: undefined,
|
|
175
|
+
text: undefined
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
for (const arg of args) {
|
|
179
|
+
if (arg === '--dry-run') {
|
|
180
|
+
options.dryRun = true;
|
|
181
|
+
} else if (arg.startsWith('--group=')) {
|
|
182
|
+
options.group = arg.slice('--group='.length);
|
|
183
|
+
} else if (arg.startsWith('--case=')) {
|
|
184
|
+
options.caseId = arg.slice('--case='.length);
|
|
185
|
+
} else if (arg.startsWith('--text=')) {
|
|
186
|
+
options.text = arg.slice('--text='.length);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return options;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseArgsList(value) {
|
|
194
|
+
if (!value) return undefined;
|
|
195
|
+
const trimmed = value.trim();
|
|
196
|
+
if (!trimmed) return undefined;
|
|
197
|
+
if (trimmed.startsWith('[')) {
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(trimmed);
|
|
200
|
+
if (Array.isArray(parsed)) return parsed.map(String);
|
|
201
|
+
} catch (_) {
|
|
202
|
+
// fall through
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return trimmed.split(/\s+/).filter(Boolean);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function loadTestCasesFromDoc(filePath) {
|
|
209
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
210
|
+
const lines = raw.split(/\r?\n/);
|
|
211
|
+
const cases = [];
|
|
212
|
+
let currentGroup = undefined;
|
|
213
|
+
let inLegacySection = false;
|
|
214
|
+
|
|
215
|
+
for (const line of lines) {
|
|
216
|
+
if (line.startsWith('## ')) {
|
|
217
|
+
const headerTitle = line.replace(/^##\s+/, '').trim();
|
|
218
|
+
if (headerTitle.toLowerCase().includes('legacy comprehensive matrix')) {
|
|
219
|
+
inLegacySection = true;
|
|
220
|
+
currentGroup = undefined;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (inLegacySection) {
|
|
224
|
+
currentGroup = undefined;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
currentGroup = headerTitle;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!currentGroup) continue;
|
|
231
|
+
if (!line.startsWith('|') || /^\|\s*-+/.test(line) || /^\|\s*#\s*\|/.test(line)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const columns = line.split('|').map((part) => part.trim());
|
|
236
|
+
if (columns.length < 5) continue;
|
|
237
|
+
const index = columns[1];
|
|
238
|
+
const scenario = columns[2];
|
|
239
|
+
const example = columns[3];
|
|
240
|
+
const expected = columns[4];
|
|
241
|
+
const payload = extractPayload(example);
|
|
242
|
+
const simplifiedGroup = simplifyGroupName(currentGroup);
|
|
243
|
+
|
|
244
|
+
const enriched = enrichTestCase({
|
|
245
|
+
group: currentGroup,
|
|
246
|
+
groupName: simplifiedGroup,
|
|
247
|
+
index,
|
|
248
|
+
scenario,
|
|
249
|
+
example,
|
|
250
|
+
expected,
|
|
251
|
+
payload
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
cases.push(enriched);
|
|
255
|
+
}
|
|
256
|
+
return cases;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function simplifyGroupName(groupTitle) {
|
|
260
|
+
return groupTitle
|
|
261
|
+
.replace(/`[^`]+`/g, '')
|
|
262
|
+
.replace(/\([^)]*\)/g, '')
|
|
263
|
+
.replace('Tools', 'Tools')
|
|
264
|
+
.trim();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function extractPayload(exampleColumn) {
|
|
268
|
+
if (!exampleColumn) return undefined;
|
|
269
|
+
const codeMatches = [...exampleColumn.matchAll(/`([^`]+)`/g)];
|
|
270
|
+
for (const match of codeMatches) {
|
|
271
|
+
const snippet = match[1].trim();
|
|
272
|
+
if (!snippet) continue;
|
|
273
|
+
const jsonCandidate = normalizeJsonCandidate(snippet);
|
|
274
|
+
if (!jsonCandidate) continue;
|
|
275
|
+
try {
|
|
276
|
+
return {
|
|
277
|
+
raw: snippet,
|
|
278
|
+
value: JSON.parse(jsonCandidate)
|
|
279
|
+
};
|
|
280
|
+
} catch (_) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeJsonCandidate(snippet) {
|
|
288
|
+
const trimmed = snippet.trim();
|
|
289
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
290
|
+
return trimmed;
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function enrichTestCase(rawCase) {
|
|
296
|
+
const caseIdBase = `${rawCase.groupName.toLowerCase().replace(/\s+/g, '-')}-${rawCase.index}`;
|
|
297
|
+
const base = {
|
|
298
|
+
group: rawCase.group,
|
|
299
|
+
groupName: rawCase.groupName,
|
|
300
|
+
index: rawCase.index,
|
|
301
|
+
scenario: rawCase.scenario,
|
|
302
|
+
expected: rawCase.expected,
|
|
303
|
+
example: rawCase.example,
|
|
304
|
+
caseId: caseIdBase,
|
|
305
|
+
payloadSnippet: rawCase.payload?.raw,
|
|
306
|
+
arguments: undefined,
|
|
307
|
+
toolName: undefined,
|
|
308
|
+
skipReason: undefined
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const payloadValue = rawCase.payload?.value
|
|
312
|
+
? hydratePlaceholders(rawCase.payload.value)
|
|
313
|
+
: undefined;
|
|
314
|
+
const scenarioLower = rawCase.scenario.toLowerCase();
|
|
315
|
+
|
|
316
|
+
switch (rawCase.groupName) {
|
|
317
|
+
case 'Lighting Tools': {
|
|
318
|
+
if (!payloadValue) {
|
|
319
|
+
return { ...base, skipReason: 'No JSON payload provided' };
|
|
320
|
+
}
|
|
321
|
+
if (/lightmass|ensure/i.test(rawCase.scenario)) {
|
|
322
|
+
return { ...base, skipReason: 'Scenario requires manual steps not exposed via consolidated tool' };
|
|
323
|
+
}
|
|
324
|
+
let lightType;
|
|
325
|
+
if (scenarioLower.includes('directional')) lightType = 'Directional';
|
|
326
|
+
else if (scenarioLower.includes('point')) lightType = 'Point';
|
|
327
|
+
else if (scenarioLower.includes('spot')) lightType = 'Spot';
|
|
328
|
+
else if (scenarioLower.includes('rect')) lightType = 'Rect';
|
|
329
|
+
else if (scenarioLower.includes('sky')) lightType = 'Sky';
|
|
330
|
+
else if (scenarioLower.includes('build lighting')) {
|
|
331
|
+
return {
|
|
332
|
+
...base,
|
|
333
|
+
skipReason: 'Skipping build lighting scenarios to avoid long editor runs'
|
|
334
|
+
};
|
|
335
|
+
} else {
|
|
336
|
+
return { ...base, skipReason: 'Unrecognized light type or scenario' };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const args = {
|
|
340
|
+
action: 'create_light',
|
|
341
|
+
lightType,
|
|
342
|
+
name: payloadValue.name ?? `${lightType}Light_${rawCase.index}`
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (typeof payloadValue.intensity === 'number') {
|
|
346
|
+
args.intensity = payloadValue.intensity;
|
|
347
|
+
}
|
|
348
|
+
if (Array.isArray(payloadValue.location) && payloadValue.location.length === 3) {
|
|
349
|
+
args.location = {
|
|
350
|
+
x: payloadValue.location[0],
|
|
351
|
+
y: payloadValue.location[1],
|
|
352
|
+
z: payloadValue.location[2]
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (Array.isArray(payloadValue.rotation) && payloadValue.rotation.length === 3) {
|
|
356
|
+
args.rotation = {
|
|
357
|
+
pitch: payloadValue.rotation[0],
|
|
358
|
+
yaw: payloadValue.rotation[1],
|
|
359
|
+
roll: payloadValue.rotation[2]
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(payloadValue.color) && payloadValue.color.length === 3) {
|
|
363
|
+
args.color = payloadValue.color;
|
|
364
|
+
}
|
|
365
|
+
if (payloadValue.radius !== undefined) {
|
|
366
|
+
args.radius = payloadValue.radius;
|
|
367
|
+
}
|
|
368
|
+
if (payloadValue.innerCone !== undefined) {
|
|
369
|
+
args.innerCone = payloadValue.innerCone;
|
|
370
|
+
}
|
|
371
|
+
if (payloadValue.outerCone !== undefined) {
|
|
372
|
+
args.outerCone = payloadValue.outerCone;
|
|
373
|
+
}
|
|
374
|
+
if (payloadValue.width !== undefined) {
|
|
375
|
+
args.width = payloadValue.width;
|
|
376
|
+
}
|
|
377
|
+
if (payloadValue.height !== undefined) {
|
|
378
|
+
args.height = payloadValue.height;
|
|
379
|
+
}
|
|
380
|
+
if (payloadValue.falloffExponent !== undefined) {
|
|
381
|
+
args.falloffExponent = payloadValue.falloffExponent;
|
|
382
|
+
}
|
|
383
|
+
if (typeof payloadValue.castShadows === 'boolean') {
|
|
384
|
+
args.castShadows = payloadValue.castShadows;
|
|
385
|
+
}
|
|
386
|
+
if (payloadValue.temperature !== undefined) {
|
|
387
|
+
args.temperature = payloadValue.temperature;
|
|
388
|
+
}
|
|
389
|
+
if (typeof payloadValue.sourceType === 'string') {
|
|
390
|
+
args.sourceType = payloadValue.sourceType;
|
|
391
|
+
}
|
|
392
|
+
if (typeof payloadValue.cubemapPath === 'string') {
|
|
393
|
+
args.cubemapPath = payloadValue.cubemapPath;
|
|
394
|
+
}
|
|
395
|
+
if (typeof payloadValue.recapture === 'boolean') {
|
|
396
|
+
args.recapture = payloadValue.recapture;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
...base,
|
|
401
|
+
toolName: 'manage_level',
|
|
402
|
+
arguments: args
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
case 'Actor Tools': {
|
|
406
|
+
if (!payloadValue) {
|
|
407
|
+
return { ...base, skipReason: 'No JSON payload provided' };
|
|
408
|
+
}
|
|
409
|
+
const args = { ...payloadValue };
|
|
410
|
+
if (Array.isArray(args.location) && args.location.length === 3) {
|
|
411
|
+
args.location = { x: args.location[0], y: args.location[1], z: args.location[2] };
|
|
412
|
+
}
|
|
413
|
+
if (Array.isArray(args.rotation) && args.rotation.length === 3) {
|
|
414
|
+
args.rotation = { pitch: args.rotation[0], yaw: args.rotation[1], roll: args.rotation[2] };
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
...base,
|
|
418
|
+
toolName: 'control_actor',
|
|
419
|
+
arguments: args
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
case 'Asset Tools': {
|
|
423
|
+
if (!payloadValue) {
|
|
424
|
+
return { ...base, skipReason: 'Non-JSON payload requires manual execution' };
|
|
425
|
+
}
|
|
426
|
+
if (!payloadValue.action) {
|
|
427
|
+
return { ...base, skipReason: 'Action missing; unable to route to consolidated tool' };
|
|
428
|
+
}
|
|
429
|
+
if (!['list', 'import', 'create_material'].includes(payloadValue.action)) {
|
|
430
|
+
return { ...base, skipReason: `Action '${payloadValue.action}' not supported by automated runner` };
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
...base,
|
|
434
|
+
toolName: 'manage_asset',
|
|
435
|
+
arguments: payloadValue
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
case 'Animation Tools': {
|
|
439
|
+
if (!payloadValue) {
|
|
440
|
+
return { ...base, skipReason: 'No JSON payload provided' };
|
|
441
|
+
}
|
|
442
|
+
const args = { ...payloadValue };
|
|
443
|
+
if (scenarioLower.includes('animation blueprint')) {
|
|
444
|
+
args.action = 'create_animation_bp';
|
|
445
|
+
} else if (scenarioLower.includes('montage') || scenarioLower.includes('animation asset once')) {
|
|
446
|
+
args.action = 'play_montage';
|
|
447
|
+
} else if (scenarioLower.includes('ragdoll')) {
|
|
448
|
+
args.action = 'setup_ragdoll';
|
|
449
|
+
}
|
|
450
|
+
if (!args.action) {
|
|
451
|
+
return { ...base, skipReason: 'Scenario not supported by automated runner' };
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
...base,
|
|
455
|
+
toolName: 'animation_physics',
|
|
456
|
+
arguments: args
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
case 'Blueprint Tools': {
|
|
460
|
+
if (!payloadValue) {
|
|
461
|
+
return { ...base, skipReason: 'No JSON payload provided' };
|
|
462
|
+
}
|
|
463
|
+
const args = { ...payloadValue };
|
|
464
|
+
if (!args.action) {
|
|
465
|
+
args.action = scenarioLower.includes('component') ? 'add_component' : 'create';
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
...base,
|
|
469
|
+
toolName: 'manage_blueprint',
|
|
470
|
+
arguments: args
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
case 'Material Tools': {
|
|
474
|
+
if (!payloadValue) {
|
|
475
|
+
return { ...base, skipReason: 'No JSON payload provided' };
|
|
476
|
+
}
|
|
477
|
+
const args = { ...payloadValue };
|
|
478
|
+
if (!args.action && typeof args.name === 'string' && typeof args.path === 'string') {
|
|
479
|
+
args.action = 'create_material';
|
|
480
|
+
}
|
|
481
|
+
if (args.action === 'create_material') {
|
|
482
|
+
return {
|
|
483
|
+
...base,
|
|
484
|
+
toolName: 'manage_asset',
|
|
485
|
+
arguments: {
|
|
486
|
+
action: 'create_material',
|
|
487
|
+
name: typeof args.name === 'string' ? args.name : `M_Test_${rawCase.index}`,
|
|
488
|
+
path: args.path
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return { ...base, skipReason: 'Material scenario not supported by automated runner' };
|
|
493
|
+
}
|
|
494
|
+
case 'Niagara Tools': {
|
|
495
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
496
|
+
if (!payloadValue.action) {
|
|
497
|
+
return { ...base, skipReason: 'Missing action for Niagara scenario' };
|
|
498
|
+
}
|
|
499
|
+
if (Array.isArray(payloadValue.location) && payloadValue.location.length === 3) {
|
|
500
|
+
payloadValue.location = { x: payloadValue.location[0], y: payloadValue.location[1], z: payloadValue.location[2] };
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
...base,
|
|
504
|
+
toolName: 'create_effect',
|
|
505
|
+
arguments: payloadValue
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
case 'Level Tools': {
|
|
509
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
510
|
+
if (!payloadValue.action) {
|
|
511
|
+
return { ...base, skipReason: 'Missing action for level scenario' };
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
...base,
|
|
515
|
+
toolName: 'manage_level',
|
|
516
|
+
arguments: payloadValue
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
case 'Sequence Tools': {
|
|
520
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
521
|
+
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
522
|
+
return {
|
|
523
|
+
...base,
|
|
524
|
+
toolName: 'manage_sequence',
|
|
525
|
+
arguments: payloadValue
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
case 'UI Tools': {
|
|
529
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
530
|
+
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
531
|
+
return {
|
|
532
|
+
...base,
|
|
533
|
+
toolName: 'system_control',
|
|
534
|
+
arguments: payloadValue
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
case 'Physics Tools': {
|
|
538
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
539
|
+
if (payloadValue.action === 'apply_force') {
|
|
540
|
+
return {
|
|
541
|
+
...base,
|
|
542
|
+
toolName: 'control_actor',
|
|
543
|
+
arguments: payloadValue
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return { ...base, skipReason: 'Physics scenario not mapped to consolidated tools' };
|
|
547
|
+
}
|
|
548
|
+
case 'Landscape Tools': {
|
|
549
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
550
|
+
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
551
|
+
return {
|
|
552
|
+
...base,
|
|
553
|
+
toolName: 'build_environment',
|
|
554
|
+
arguments: payloadValue
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
case 'Build Environment Tools': {
|
|
558
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
559
|
+
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
560
|
+
return {
|
|
561
|
+
...base,
|
|
562
|
+
toolName: 'build_environment',
|
|
563
|
+
arguments: payloadValue
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
case 'Performance Tools': {
|
|
567
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
568
|
+
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
569
|
+
if (scenarioLower.includes('engine quit')) {
|
|
570
|
+
return {
|
|
571
|
+
...base,
|
|
572
|
+
skipReason: 'Skipping engine quit to keep Unreal session alive during test run'
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
...base,
|
|
577
|
+
toolName: 'system_control',
|
|
578
|
+
arguments: payloadValue
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
case 'System Control Tools': {
|
|
582
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
583
|
+
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
584
|
+
if (payloadValue.action === 'engine_quit' || payloadValue.action === 'engine_start') {
|
|
585
|
+
return {
|
|
586
|
+
...base,
|
|
587
|
+
skipReason: 'Skipping engine process management during automated run'
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
...base,
|
|
592
|
+
toolName: 'system_control',
|
|
593
|
+
arguments: payloadValue
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
case 'Debug Tools': {
|
|
597
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
598
|
+
if (!payloadValue.action) {
|
|
599
|
+
payloadValue.action = 'debug_shape';
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
...base,
|
|
603
|
+
toolName: 'create_effect',
|
|
604
|
+
arguments: payloadValue
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
case 'Remote Control Preset Tools': {
|
|
608
|
+
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
609
|
+
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
610
|
+
return {
|
|
611
|
+
...base,
|
|
612
|
+
toolName: 'manage_rc',
|
|
613
|
+
arguments: payloadValue
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
default:
|
|
617
|
+
return { ...base, skipReason: `Unknown tool group '${rawCase.groupName}'` };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function evaluateExpectation(testCase, response) {
|
|
622
|
+
const lowerExpected = testCase.expected.toLowerCase();
|
|
623
|
+
const containsFailure = failureKeywords.some((word) => lowerExpected.includes(word));
|
|
624
|
+
const containsSuccess = successKeywords.some((word) => lowerExpected.includes(word));
|
|
625
|
+
|
|
626
|
+
const structuredSuccess = typeof response.structuredContent?.success === 'boolean'
|
|
627
|
+
? response.structuredContent.success
|
|
628
|
+
: undefined;
|
|
629
|
+
const actualSuccess = structuredSuccess ?? !response.isError;
|
|
630
|
+
|
|
631
|
+
const expectedFailure = containsFailure && !containsSuccess;
|
|
632
|
+
const passed = expectedFailure ? !actualSuccess : !!actualSuccess;
|
|
633
|
+
let reason;
|
|
634
|
+
if (response.isError) {
|
|
635
|
+
reason = response.content?.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
|
|
636
|
+
} else if (response.structuredContent) {
|
|
637
|
+
reason = JSON.stringify(response.structuredContent);
|
|
638
|
+
} else if (response.content?.length) {
|
|
639
|
+
reason = response.content.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
|
|
640
|
+
} else {
|
|
641
|
+
reason = 'No structured response returned';
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return { passed, reason };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function formatResultLine(testCase, status, detail, durationMs) {
|
|
648
|
+
const durationText = typeof durationMs === 'number' ? ` (${durationMs.toFixed(1)} ms)` : '';
|
|
649
|
+
return `[${status.toUpperCase()}] ${testCase.groupName} #${testCase.index} – ${testCase.scenario}${durationText}${detail ? ` => ${detail}` : ''}`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function persistResults(results) {
|
|
653
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
654
|
+
const serializable = results.map((result) => ({
|
|
655
|
+
group: result.groupName,
|
|
656
|
+
caseId: result.caseId,
|
|
657
|
+
index: result.index,
|
|
658
|
+
scenario: result.scenario,
|
|
659
|
+
toolName: result.toolName,
|
|
660
|
+
arguments: result.arguments,
|
|
661
|
+
status: result.status,
|
|
662
|
+
durationMs: result.durationMs,
|
|
663
|
+
detail: result.detail
|
|
664
|
+
}));
|
|
665
|
+
await fs.writeFile(resultsPath, JSON.stringify({
|
|
666
|
+
generatedAt: new Date().toISOString(),
|
|
667
|
+
docPath,
|
|
668
|
+
results: serializable
|
|
669
|
+
}, null, 2));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function summarize(results) {
|
|
673
|
+
const totals = results.reduce((acc, result) => {
|
|
674
|
+
acc.total += 1;
|
|
675
|
+
acc[result.status] = (acc[result.status] ?? 0) + 1;
|
|
676
|
+
return acc;
|
|
677
|
+
}, { total: 0, passed: 0, failed: 0, skipped: 0 });
|
|
678
|
+
|
|
679
|
+
console.log('\nSummary');
|
|
680
|
+
console.log('=======');
|
|
681
|
+
console.log(`Total cases processed: ${totals.total}`);
|
|
682
|
+
console.log(`Passed: ${totals.passed}`);
|
|
683
|
+
console.log(`Failed: ${totals.failed}`);
|
|
684
|
+
console.log(`Skipped: ${totals.skipped}`);
|
|
685
|
+
console.log(`Results written to: ${resultsPath}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function normalizeWindowsPath(value) {
|
|
689
|
+
if (typeof value !== 'string') return value;
|
|
690
|
+
return value.replace(/\\+/g, '\\').replace(/\/+/g, '\\');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function hydratePlaceholders(value) {
|
|
694
|
+
if (typeof value === 'string') {
|
|
695
|
+
return value
|
|
696
|
+
.replaceAll('{{FBX_DIR}}', defaultFbxDir)
|
|
697
|
+
.replaceAll('{{FBX_TEST_MODEL}}', defaultFbxFile);
|
|
698
|
+
}
|
|
699
|
+
if (Array.isArray(value)) {
|
|
700
|
+
return value.map((entry) => hydratePlaceholders(entry));
|
|
701
|
+
}
|
|
702
|
+
if (value && typeof value === 'object') {
|
|
703
|
+
return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, hydratePlaceholders(val)]));
|
|
704
|
+
}
|
|
705
|
+
return value;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function ensureFbxDirectory() {
|
|
709
|
+
if (!defaultFbxDir) return;
|
|
710
|
+
try {
|
|
711
|
+
await fs.mkdir(defaultFbxDir, { recursive: true });
|
|
712
|
+
} catch (err) {
|
|
713
|
+
console.warn(`Unable to ensure FBX directory '${defaultFbxDir}':`, err);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
main().catch((err) => {
|
|
718
|
+
console.error('Unexpected error during test execution:', err);
|
|
719
|
+
process.exitCode = 1;
|
|
720
|
+
});
|