unreal-engine-mcp-server 0.5.2 → 0.5.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/CHANGELOG.md +195 -0
- package/README.md +9 -6
- package/dist/automation/bridge.d.ts +1 -0
- package/dist/automation/bridge.js +62 -4
- package/dist/automation/types.d.ts +1 -0
- package/dist/config/class-aliases.d.ts +5 -0
- package/dist/config/class-aliases.js +30 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/graphql/server.d.ts +0 -1
- package/dist/graphql/server.js +15 -16
- package/dist/index.js +1 -1
- package/dist/services/metrics-server.d.ts +2 -1
- package/dist/services/metrics-server.js +29 -4
- package/dist/tools/consolidated-tool-definitions.js +3 -3
- package/dist/tools/debug.d.ts +5 -0
- package/dist/tools/debug.js +7 -0
- package/dist/tools/handlers/actor-handlers.js +4 -27
- package/dist/tools/handlers/asset-handlers.js +13 -1
- package/dist/tools/handlers/blueprint-handlers.d.ts +4 -1
- package/dist/tools/handlers/common-handlers.d.ts +11 -11
- package/dist/tools/handlers/common-handlers.js +6 -4
- package/dist/tools/handlers/editor-handlers.d.ts +2 -1
- package/dist/tools/handlers/editor-handlers.js +6 -6
- package/dist/tools/handlers/effect-handlers.js +3 -0
- package/dist/tools/handlers/graph-handlers.d.ts +2 -1
- package/dist/tools/handlers/graph-handlers.js +1 -1
- package/dist/tools/handlers/input-handlers.d.ts +5 -1
- package/dist/tools/handlers/level-handlers.d.ts +2 -1
- package/dist/tools/handlers/level-handlers.js +3 -3
- package/dist/tools/handlers/lighting-handlers.d.ts +2 -1
- package/dist/tools/handlers/lighting-handlers.js +3 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts +2 -1
- package/dist/tools/handlers/pipeline-handlers.js +64 -10
- package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
- package/dist/tools/handlers/system-handlers.d.ts +1 -1
- package/dist/tools/input.d.ts +5 -1
- package/dist/tools/input.js +37 -1
- package/dist/tools/lighting.d.ts +1 -0
- package/dist/tools/lighting.js +7 -0
- package/dist/tools/physics.d.ts +1 -1
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +7 -0
- package/dist/types/handler-types.d.ts +343 -0
- package/dist/types/handler-types.js +2 -0
- package/dist/unreal-bridge.d.ts +1 -1
- package/dist/unreal-bridge.js +8 -6
- package/dist/utils/command-validator.d.ts +1 -0
- package/dist/utils/command-validator.js +11 -1
- package/dist/utils/error-handler.js +3 -1
- package/dist/utils/response-validator.js +2 -2
- package/dist/utils/safe-json.d.ts +1 -1
- package/dist/utils/safe-json.js +3 -6
- package/dist/utils/unreal-command-queue.js +1 -1
- package/dist/utils/validation.js +6 -2
- package/docs/handler-mapping.md +6 -1
- package/package.json +2 -2
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +25 -1
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +40 -58
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +27 -46
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +16 -1
- package/server.json +2 -2
- package/src/automation/bridge.ts +80 -10
- package/src/automation/types.ts +1 -0
- package/src/config/class-aliases.ts +65 -0
- package/src/constants.ts +10 -0
- package/src/graphql/server.ts +23 -23
- package/src/index.ts +1 -1
- package/src/services/metrics-server.ts +40 -6
- package/src/tools/consolidated-tool-definitions.ts +3 -3
- package/src/tools/debug.ts +8 -0
- package/src/tools/handlers/actor-handlers.ts +5 -31
- package/src/tools/handlers/asset-handlers.ts +19 -1
- package/src/tools/handlers/blueprint-handlers.ts +1 -1
- package/src/tools/handlers/common-handlers.ts +32 -11
- package/src/tools/handlers/editor-handlers.ts +8 -7
- package/src/tools/handlers/effect-handlers.ts +4 -0
- package/src/tools/handlers/graph-handlers.ts +7 -6
- package/src/tools/handlers/level-handlers.ts +5 -4
- package/src/tools/handlers/lighting-handlers.ts +5 -1
- package/src/tools/handlers/pipeline-handlers.ts +83 -16
- package/src/tools/input.ts +60 -1
- package/src/tools/lighting.ts +11 -0
- package/src/tools/physics.ts +1 -1
- package/src/tools/sequence.ts +11 -0
- package/src/types/handler-types.ts +442 -0
- package/src/unreal-bridge.ts +8 -6
- package/src/utils/command-validator.ts +23 -1
- package/src/utils/error-handler.ts +4 -1
- package/src/utils/response-validator.ts +7 -9
- package/src/utils/safe-json.ts +20 -15
- package/src/utils/unreal-command-queue.ts +3 -1
- package/src/utils/validation.test.ts +3 -3
- package/src/utils/validation.ts +36 -26
- package/tests/test-console-command.mjs +1 -1
- package/tests/test-runner.mjs +63 -3
- package/tests/run-unreal-tool-tests.mjs +0 -948
- package/tests/test-asset-errors.mjs +0 -35
|
@@ -1,948 +0,0 @@
|
|
|
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 ?? path.join(repoRoot, 'tests', 'assets', '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
|
-
const stressTestMode = process.env.STRESS_TEST_MODE === '1';
|
|
48
|
-
const benchmarkMode = process.env.BENCHMARK_MODE === '1';
|
|
49
|
-
|
|
50
|
-
async function main() {
|
|
51
|
-
await ensureFbxDirectory();
|
|
52
|
-
const allCases = await loadTestCasesFromDoc(docPath);
|
|
53
|
-
if (allCases.length === 0) {
|
|
54
|
-
console.error(`No test cases detected in ${docPath}.`);
|
|
55
|
-
process.exitCode = 1;
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const filteredCases = allCases.filter((testCase) => {
|
|
60
|
-
if (cliOptions.group && testCase.groupName !== cliOptions.group) return false;
|
|
61
|
-
if (cliOptions.caseId && testCase.caseId !== cliOptions.caseId) return false;
|
|
62
|
-
if (cliOptions.text && !testCase.scenario.toLowerCase().includes(cliOptions.text.toLowerCase())) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
return true;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (filteredCases.length === 0) {
|
|
69
|
-
console.warn('No test cases matched the provided filters. Exiting.');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let transport; let client;
|
|
74
|
-
const runResults = [];
|
|
75
|
-
let automationBridgeStatus = { connected: false, summary: null };
|
|
76
|
-
let automationBridgeTestsEnabled = process.env.UNREAL_MCP_AUTOMATION_BRIDGE === '1';
|
|
77
|
-
|
|
78
|
-
if (!cliOptions.dryRun) {
|
|
79
|
-
try {
|
|
80
|
-
transport = new StdioClientTransport({
|
|
81
|
-
command: serverCommand,
|
|
82
|
-
args: serverArgs,
|
|
83
|
-
cwd: serverCwd,
|
|
84
|
-
stderr: 'inherit'
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
client = new Client({
|
|
88
|
-
name: 'unreal-mcp-tool-test-runner',
|
|
89
|
-
version: '0.1.0'
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
await client.connect(transport);
|
|
93
|
-
await client.listTools({});
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const bridgeResource = await client.readResource({ uri: 'ue://automation-bridge' });
|
|
97
|
-
const text = bridgeResource.contents?.[0]?.text;
|
|
98
|
-
if (text) {
|
|
99
|
-
const parsed = JSON.parse(text);
|
|
100
|
-
if (parsed && typeof parsed === 'object') {
|
|
101
|
-
const summary = parsed.summary ?? parsed;
|
|
102
|
-
const connected = Boolean(summary?.connected);
|
|
103
|
-
automationBridgeStatus = { connected, summary };
|
|
104
|
-
if (connected) {
|
|
105
|
-
automationBridgeTestsEnabled = true;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch (err) {
|
|
110
|
-
// Resource may not exist on older servers; treat as unavailable without failing run
|
|
111
|
-
console.warn('[warn] Unable to query ue://automation-bridge resource:', err?.message ?? String(err));
|
|
112
|
-
}
|
|
113
|
-
} catch (err) {
|
|
114
|
-
console.error('Failed to start or initialize MCP server:', err);
|
|
115
|
-
if (transport) {
|
|
116
|
-
try { await transport.close(); } catch { /* ignore */ }
|
|
117
|
-
}
|
|
118
|
-
process.exitCode = 1;
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
for (const testCase of filteredCases) {
|
|
124
|
-
let skipReason = testCase.skipReason;
|
|
125
|
-
|
|
126
|
-
if (!skipReason && testCase.groupName === 'Automation Bridge') {
|
|
127
|
-
if (!automationBridgeTestsEnabled) {
|
|
128
|
-
skipReason = 'Automation bridge tests disabled (set UNREAL_MCP_AUTOMATION_BRIDGE=1 or connect the plugin).';
|
|
129
|
-
} else {
|
|
130
|
-
const requestedTransport = typeof testCase.arguments?.transport === 'string'
|
|
131
|
-
? testCase.arguments.transport.trim().toLowerCase()
|
|
132
|
-
: '';
|
|
133
|
-
const wantsBridge = ['automation_bridge', 'automation', 'bridge'].includes(requestedTransport);
|
|
134
|
-
if (wantsBridge && !automationBridgeStatus.connected) {
|
|
135
|
-
skipReason = 'Automation bridge transport requested but plugin is not connected.';
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (testCase.skipReason) {
|
|
141
|
-
runResults.push({
|
|
142
|
-
...testCase,
|
|
143
|
-
status: 'skipped',
|
|
144
|
-
detail: testCase.skipReason
|
|
145
|
-
});
|
|
146
|
-
console.log(formatResultLine(testCase, 'skipped', testCase.skipReason));
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (skipReason) {
|
|
151
|
-
runResults.push({
|
|
152
|
-
...testCase,
|
|
153
|
-
status: 'skipped',
|
|
154
|
-
detail: skipReason
|
|
155
|
-
});
|
|
156
|
-
console.log(formatResultLine(testCase, 'skipped', skipReason));
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (cliOptions.dryRun) {
|
|
161
|
-
runResults.push({
|
|
162
|
-
...testCase,
|
|
163
|
-
status: 'skipped',
|
|
164
|
-
detail: 'Dry run'
|
|
165
|
-
});
|
|
166
|
-
console.log(formatResultLine(testCase, 'skipped', 'Dry run'));
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const started = performance.now();
|
|
171
|
-
try {
|
|
172
|
-
const response = await client.callTool({
|
|
173
|
-
name: testCase.toolName,
|
|
174
|
-
arguments: testCase.arguments
|
|
175
|
-
});
|
|
176
|
-
const duration = performance.now() - started;
|
|
177
|
-
const evaluation = evaluateExpectation(testCase, response);
|
|
178
|
-
runResults.push({
|
|
179
|
-
...testCase,
|
|
180
|
-
status: evaluation.passed ? 'passed' : 'failed',
|
|
181
|
-
durationMs: duration,
|
|
182
|
-
detail: evaluation.reason,
|
|
183
|
-
response
|
|
184
|
-
});
|
|
185
|
-
console.log(formatResultLine(testCase, evaluation.passed ? 'passed' : 'failed', evaluation.reason, duration));
|
|
186
|
-
} catch (err) {
|
|
187
|
-
const duration = performance.now() - started;
|
|
188
|
-
runResults.push({
|
|
189
|
-
...testCase,
|
|
190
|
-
status: 'failed',
|
|
191
|
-
durationMs: duration,
|
|
192
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
193
|
-
});
|
|
194
|
-
console.log(formatResultLine(testCase, 'failed', err instanceof Error ? err.message : String(err), duration));
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (!cliOptions.dryRun) {
|
|
199
|
-
try {
|
|
200
|
-
await client.close();
|
|
201
|
-
} catch {
|
|
202
|
-
// ignore
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
await transport.close();
|
|
206
|
-
} catch {
|
|
207
|
-
// ignore
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
await persistResults(runResults);
|
|
212
|
-
summarize(runResults);
|
|
213
|
-
|
|
214
|
-
// Performance statistics if benchmarking
|
|
215
|
-
if (benchmarkMode) {
|
|
216
|
-
generateBenchmarkReport(runResults);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (runResults.some((result) => result.status === 'failed')) {
|
|
220
|
-
process.exitCode = 1;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function parseCliOptions(args) {
|
|
225
|
-
const options = {
|
|
226
|
-
dryRun: false,
|
|
227
|
-
group: undefined,
|
|
228
|
-
caseId: undefined,
|
|
229
|
-
text: undefined
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
for (const arg of args) {
|
|
233
|
-
if (arg === '--dry-run') {
|
|
234
|
-
options.dryRun = true;
|
|
235
|
-
} else if (arg.startsWith('--group=')) {
|
|
236
|
-
options.group = arg.slice('--group='.length);
|
|
237
|
-
} else if (arg.startsWith('--case=')) {
|
|
238
|
-
options.caseId = arg.slice('--case='.length);
|
|
239
|
-
} else if (arg.startsWith('--text=')) {
|
|
240
|
-
options.text = arg.slice('--text='.length);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return options;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function parseArgsList(value) {
|
|
248
|
-
if (!value) return undefined;
|
|
249
|
-
const trimmed = value.trim();
|
|
250
|
-
if (!trimmed) return undefined;
|
|
251
|
-
if (trimmed.startsWith('[')) {
|
|
252
|
-
try {
|
|
253
|
-
const parsed = JSON.parse(trimmed);
|
|
254
|
-
if (Array.isArray(parsed)) return parsed.map(String);
|
|
255
|
-
} catch (_) {
|
|
256
|
-
// fall through
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return trimmed.split(/\s+/).filter(Boolean);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function loadTestCasesFromDoc(filePath) {
|
|
263
|
-
const raw = await fs.readFile(filePath, 'utf8');
|
|
264
|
-
const lines = raw.split(/\r?\n/);
|
|
265
|
-
const cases = [];
|
|
266
|
-
let currentGroup = undefined;
|
|
267
|
-
let inLegacySection = false;
|
|
268
|
-
|
|
269
|
-
for (const line of lines) {
|
|
270
|
-
if (line.startsWith('## ')) {
|
|
271
|
-
const headerTitle = line.replace(/^##\s+/, '').trim();
|
|
272
|
-
if (headerTitle.toLowerCase().includes('legacy comprehensive matrix')) {
|
|
273
|
-
inLegacySection = true;
|
|
274
|
-
currentGroup = undefined;
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
if (inLegacySection) {
|
|
278
|
-
currentGroup = undefined;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
currentGroup = headerTitle;
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (!currentGroup) continue;
|
|
285
|
-
if (!line.startsWith('|') || /^\|\s*-+/.test(line) || /^\|\s*#\s*\|/.test(line)) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const columns = line.split('|').map((part) => part.trim());
|
|
290
|
-
if (columns.length < 5) continue;
|
|
291
|
-
const index = columns[1];
|
|
292
|
-
const scenario = columns[2];
|
|
293
|
-
const example = columns[3];
|
|
294
|
-
const expected = columns[4];
|
|
295
|
-
const payload = extractPayload(example);
|
|
296
|
-
const simplifiedGroup = simplifyGroupName(currentGroup);
|
|
297
|
-
|
|
298
|
-
const enriched = enrichTestCase({
|
|
299
|
-
group: currentGroup,
|
|
300
|
-
groupName: simplifiedGroup,
|
|
301
|
-
index,
|
|
302
|
-
scenario,
|
|
303
|
-
example,
|
|
304
|
-
expected,
|
|
305
|
-
payload
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
cases.push(enriched);
|
|
309
|
-
}
|
|
310
|
-
return cases;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function simplifyGroupName(groupTitle) {
|
|
314
|
-
return groupTitle
|
|
315
|
-
.replace(/`[^`]+`/g, '')
|
|
316
|
-
.replace(/\([^)]*\)/g, '')
|
|
317
|
-
.replace('Tools', 'Tools')
|
|
318
|
-
.trim();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function extractPayload(exampleColumn) {
|
|
322
|
-
if (!exampleColumn) return undefined;
|
|
323
|
-
const codeMatches = [...exampleColumn.matchAll(/`([^`]+)`/g)];
|
|
324
|
-
for (const match of codeMatches) {
|
|
325
|
-
const snippet = match[1].trim();
|
|
326
|
-
if (!snippet) continue;
|
|
327
|
-
const jsonCandidate = normalizeJsonCandidate(snippet);
|
|
328
|
-
if (!jsonCandidate) continue;
|
|
329
|
-
try {
|
|
330
|
-
return {
|
|
331
|
-
raw: snippet,
|
|
332
|
-
value: JSON.parse(jsonCandidate)
|
|
333
|
-
};
|
|
334
|
-
} catch (_) {
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return undefined;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function normalizeJsonCandidate(snippet) {
|
|
342
|
-
const trimmed = snippet.trim();
|
|
343
|
-
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
344
|
-
return trimmed;
|
|
345
|
-
}
|
|
346
|
-
return undefined;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function enrichTestCase(rawCase) {
|
|
350
|
-
const caseIdBase = `${rawCase.groupName.toLowerCase().replace(/\s+/g, '-')}-${rawCase.index}`;
|
|
351
|
-
const base = {
|
|
352
|
-
group: rawCase.group,
|
|
353
|
-
groupName: rawCase.groupName,
|
|
354
|
-
index: rawCase.index,
|
|
355
|
-
scenario: rawCase.scenario,
|
|
356
|
-
expected: rawCase.expected,
|
|
357
|
-
example: rawCase.example,
|
|
358
|
-
caseId: caseIdBase,
|
|
359
|
-
payloadSnippet: rawCase.payload?.raw,
|
|
360
|
-
arguments: undefined,
|
|
361
|
-
toolName: undefined,
|
|
362
|
-
skipReason: undefined
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
const payloadValue = rawCase.payload?.value
|
|
366
|
-
? hydratePlaceholders(rawCase.payload.value)
|
|
367
|
-
: undefined;
|
|
368
|
-
const scenarioLower = rawCase.scenario.toLowerCase();
|
|
369
|
-
|
|
370
|
-
switch (rawCase.groupName) {
|
|
371
|
-
case 'Lighting Tools': {
|
|
372
|
-
if (!payloadValue) {
|
|
373
|
-
return { ...base, skipReason: 'No JSON payload provided' };
|
|
374
|
-
}
|
|
375
|
-
if (/lightmass|ensure/i.test(rawCase.scenario)) {
|
|
376
|
-
return { ...base, skipReason: 'Scenario requires manual steps not exposed via consolidated tool' };
|
|
377
|
-
}
|
|
378
|
-
let lightType;
|
|
379
|
-
if (scenarioLower.includes('directional')) lightType = 'Directional';
|
|
380
|
-
else if (scenarioLower.includes('point')) lightType = 'Point';
|
|
381
|
-
else if (scenarioLower.includes('spot')) lightType = 'Spot';
|
|
382
|
-
else if (scenarioLower.includes('rect')) lightType = 'Rect';
|
|
383
|
-
else if (scenarioLower.includes('sky')) lightType = 'Sky';
|
|
384
|
-
else if (scenarioLower.includes('build lighting')) {
|
|
385
|
-
return {
|
|
386
|
-
...base,
|
|
387
|
-
skipReason: 'Skipping build lighting scenarios to avoid long editor runs'
|
|
388
|
-
};
|
|
389
|
-
} else {
|
|
390
|
-
return { ...base, skipReason: 'Unrecognized light type or scenario' };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const args = {
|
|
394
|
-
action: 'create_light',
|
|
395
|
-
lightType,
|
|
396
|
-
name: payloadValue.name ?? `${lightType}Light_${rawCase.index}`
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
if (typeof payloadValue.intensity === 'number') {
|
|
400
|
-
args.intensity = payloadValue.intensity;
|
|
401
|
-
}
|
|
402
|
-
if (Array.isArray(payloadValue.location) && payloadValue.location.length === 3) {
|
|
403
|
-
args.location = {
|
|
404
|
-
x: payloadValue.location[0],
|
|
405
|
-
y: payloadValue.location[1],
|
|
406
|
-
z: payloadValue.location[2]
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
if (Array.isArray(payloadValue.rotation) && payloadValue.rotation.length === 3) {
|
|
410
|
-
args.rotation = {
|
|
411
|
-
pitch: payloadValue.rotation[0],
|
|
412
|
-
yaw: payloadValue.rotation[1],
|
|
413
|
-
roll: payloadValue.rotation[2]
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
if (Array.isArray(payloadValue.color) && payloadValue.color.length === 3) {
|
|
417
|
-
args.color = payloadValue.color;
|
|
418
|
-
}
|
|
419
|
-
if (payloadValue.radius !== undefined) {
|
|
420
|
-
args.radius = payloadValue.radius;
|
|
421
|
-
}
|
|
422
|
-
if (payloadValue.innerCone !== undefined) {
|
|
423
|
-
args.innerCone = payloadValue.innerCone;
|
|
424
|
-
}
|
|
425
|
-
if (payloadValue.outerCone !== undefined) {
|
|
426
|
-
args.outerCone = payloadValue.outerCone;
|
|
427
|
-
}
|
|
428
|
-
if (payloadValue.width !== undefined) {
|
|
429
|
-
args.width = payloadValue.width;
|
|
430
|
-
}
|
|
431
|
-
if (payloadValue.height !== undefined) {
|
|
432
|
-
args.height = payloadValue.height;
|
|
433
|
-
}
|
|
434
|
-
if (payloadValue.falloffExponent !== undefined) {
|
|
435
|
-
args.falloffExponent = payloadValue.falloffExponent;
|
|
436
|
-
}
|
|
437
|
-
if (typeof payloadValue.castShadows === 'boolean') {
|
|
438
|
-
args.castShadows = payloadValue.castShadows;
|
|
439
|
-
}
|
|
440
|
-
if (payloadValue.temperature !== undefined) {
|
|
441
|
-
args.temperature = payloadValue.temperature;
|
|
442
|
-
}
|
|
443
|
-
if (typeof payloadValue.sourceType === 'string') {
|
|
444
|
-
args.sourceType = payloadValue.sourceType;
|
|
445
|
-
}
|
|
446
|
-
if (typeof payloadValue.cubemapPath === 'string') {
|
|
447
|
-
args.cubemapPath = payloadValue.cubemapPath;
|
|
448
|
-
}
|
|
449
|
-
if (typeof payloadValue.recapture === 'boolean') {
|
|
450
|
-
args.recapture = payloadValue.recapture;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return {
|
|
454
|
-
...base,
|
|
455
|
-
toolName: 'manage_level',
|
|
456
|
-
arguments: args
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
case 'Actor Tools': {
|
|
460
|
-
if (!payloadValue) {
|
|
461
|
-
return { ...base, skipReason: 'No JSON payload provided' };
|
|
462
|
-
}
|
|
463
|
-
const args = { ...payloadValue };
|
|
464
|
-
if (Array.isArray(args.location) && args.location.length === 3) {
|
|
465
|
-
args.location = { x: args.location[0], y: args.location[1], z: args.location[2] };
|
|
466
|
-
}
|
|
467
|
-
if (Array.isArray(args.rotation) && args.rotation.length === 3) {
|
|
468
|
-
args.rotation = { pitch: args.rotation[0], yaw: args.rotation[1], roll: args.rotation[2] };
|
|
469
|
-
}
|
|
470
|
-
return {
|
|
471
|
-
...base,
|
|
472
|
-
toolName: 'control_actor',
|
|
473
|
-
arguments: args
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
case 'Asset Tools': {
|
|
477
|
-
if (!payloadValue) {
|
|
478
|
-
return { ...base, skipReason: 'Non-JSON payload requires manual execution' };
|
|
479
|
-
}
|
|
480
|
-
if (!payloadValue.action) {
|
|
481
|
-
return { ...base, skipReason: 'Action missing; unable to route to consolidated tool' };
|
|
482
|
-
}
|
|
483
|
-
if (!['list', 'import', 'create_material'].includes(payloadValue.action)) {
|
|
484
|
-
return { ...base, skipReason: `Action '${payloadValue.action}' not supported by automated runner` };
|
|
485
|
-
}
|
|
486
|
-
return {
|
|
487
|
-
...base,
|
|
488
|
-
toolName: 'manage_asset',
|
|
489
|
-
arguments: payloadValue
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
case 'Animation Tools': {
|
|
493
|
-
if (!payloadValue) {
|
|
494
|
-
return { ...base, skipReason: 'No JSON payload provided' };
|
|
495
|
-
}
|
|
496
|
-
const args = { ...payloadValue };
|
|
497
|
-
if (scenarioLower.includes('animation blueprint')) {
|
|
498
|
-
args.action = 'create_animation_bp';
|
|
499
|
-
} else if (scenarioLower.includes('montage') || scenarioLower.includes('animation asset once')) {
|
|
500
|
-
args.action = 'play_montage';
|
|
501
|
-
} else if (scenarioLower.includes('ragdoll')) {
|
|
502
|
-
args.action = 'setup_ragdoll';
|
|
503
|
-
}
|
|
504
|
-
if (!args.action) {
|
|
505
|
-
return { ...base, skipReason: 'Scenario not supported by automated runner' };
|
|
506
|
-
}
|
|
507
|
-
return {
|
|
508
|
-
...base,
|
|
509
|
-
toolName: 'animation_physics',
|
|
510
|
-
arguments: args
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
case 'Blueprint Tools': {
|
|
514
|
-
if (!payloadValue) {
|
|
515
|
-
return { ...base, skipReason: 'No JSON payload provided' };
|
|
516
|
-
}
|
|
517
|
-
const args = { ...payloadValue };
|
|
518
|
-
if (!args.action) {
|
|
519
|
-
args.action = scenarioLower.includes('component') ? 'add_component' : 'create';
|
|
520
|
-
}
|
|
521
|
-
return {
|
|
522
|
-
...base,
|
|
523
|
-
toolName: 'manage_blueprint',
|
|
524
|
-
arguments: args
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
case 'Material Tools': {
|
|
528
|
-
if (!payloadValue) {
|
|
529
|
-
return { ...base, skipReason: 'No JSON payload provided' };
|
|
530
|
-
}
|
|
531
|
-
const args = { ...payloadValue };
|
|
532
|
-
if (!args.action && typeof args.name === 'string' && typeof args.path === 'string') {
|
|
533
|
-
args.action = 'create_material';
|
|
534
|
-
}
|
|
535
|
-
if (args.action === 'create_material') {
|
|
536
|
-
return {
|
|
537
|
-
...base,
|
|
538
|
-
toolName: 'manage_asset',
|
|
539
|
-
arguments: {
|
|
540
|
-
action: 'create_material',
|
|
541
|
-
name: typeof args.name === 'string' ? args.name : `M_Test_${rawCase.index}`,
|
|
542
|
-
path: args.path
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
return { ...base, skipReason: 'Material scenario not supported by automated runner' };
|
|
547
|
-
}
|
|
548
|
-
case 'Niagara Tools': {
|
|
549
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
550
|
-
if (!payloadValue.action) {
|
|
551
|
-
return { ...base, skipReason: 'Missing action for Niagara scenario' };
|
|
552
|
-
}
|
|
553
|
-
if (Array.isArray(payloadValue.location) && payloadValue.location.length === 3) {
|
|
554
|
-
payloadValue.location = { x: payloadValue.location[0], y: payloadValue.location[1], z: payloadValue.location[2] };
|
|
555
|
-
}
|
|
556
|
-
return {
|
|
557
|
-
...base,
|
|
558
|
-
toolName: 'create_effect',
|
|
559
|
-
arguments: payloadValue
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
case 'Level Tools': {
|
|
563
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
564
|
-
if (!payloadValue.action) {
|
|
565
|
-
return { ...base, skipReason: 'Missing action for level scenario' };
|
|
566
|
-
}
|
|
567
|
-
return {
|
|
568
|
-
...base,
|
|
569
|
-
toolName: 'manage_level',
|
|
570
|
-
arguments: payloadValue
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
case 'Sequence Tools': {
|
|
574
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
575
|
-
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
576
|
-
return {
|
|
577
|
-
...base,
|
|
578
|
-
toolName: 'manage_sequence',
|
|
579
|
-
arguments: payloadValue
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
case 'UI Tools': {
|
|
583
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
584
|
-
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
585
|
-
return {
|
|
586
|
-
...base,
|
|
587
|
-
toolName: 'system_control',
|
|
588
|
-
arguments: payloadValue
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
case 'Physics Tools': {
|
|
592
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
593
|
-
if (payloadValue.action === 'apply_force') {
|
|
594
|
-
return {
|
|
595
|
-
...base,
|
|
596
|
-
toolName: 'control_actor',
|
|
597
|
-
arguments: payloadValue
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
return { ...base, skipReason: 'Physics scenario not mapped to consolidated tools' };
|
|
601
|
-
}
|
|
602
|
-
case 'Landscape Tools': {
|
|
603
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
604
|
-
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
605
|
-
return {
|
|
606
|
-
...base,
|
|
607
|
-
toolName: 'build_environment',
|
|
608
|
-
arguments: payloadValue
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
case 'Build Environment Tools': {
|
|
612
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
613
|
-
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
614
|
-
return {
|
|
615
|
-
...base,
|
|
616
|
-
toolName: 'build_environment',
|
|
617
|
-
arguments: payloadValue
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
case 'Performance Tools': {
|
|
621
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
622
|
-
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
623
|
-
if (scenarioLower.includes('engine quit')) {
|
|
624
|
-
return {
|
|
625
|
-
...base,
|
|
626
|
-
skipReason: 'Skipping engine quit to keep Unreal session alive during test run'
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
return {
|
|
630
|
-
...base,
|
|
631
|
-
toolName: 'system_control',
|
|
632
|
-
arguments: payloadValue
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
case 'System Control Tools': {
|
|
636
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
637
|
-
if (!payloadValue.action) return { ...base, skipReason: 'Missing action in payload' };
|
|
638
|
-
return {
|
|
639
|
-
...base,
|
|
640
|
-
toolName: 'system_control',
|
|
641
|
-
arguments: payloadValue
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
case 'Debug Tools': {
|
|
645
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
646
|
-
if (!payloadValue.action) {
|
|
647
|
-
payloadValue.action = 'debug_shape';
|
|
648
|
-
}
|
|
649
|
-
return {
|
|
650
|
-
...base,
|
|
651
|
-
toolName: 'create_effect',
|
|
652
|
-
arguments: payloadValue
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
case 'Asset Boundary Tests': {
|
|
656
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
657
|
-
return { ...base, toolName: 'manage_asset', arguments: payloadValue };
|
|
658
|
-
}
|
|
659
|
-
case 'Actor Boundary Tests': {
|
|
660
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
661
|
-
const args = { ...payloadValue };
|
|
662
|
-
if (Array.isArray(args.location) && args.location.length === 3) {
|
|
663
|
-
args.location = { x: args.location[0], y: args.location[1], z: args.location[2] };
|
|
664
|
-
}
|
|
665
|
-
if (Array.isArray(args.rotation) && args.rotation.length === 3) {
|
|
666
|
-
args.rotation = { pitch: args.rotation[0], yaw: args.rotation[1], roll: args.rotation[2] };
|
|
667
|
-
}
|
|
668
|
-
if (Array.isArray(args.scale) && args.scale.length === 3) {
|
|
669
|
-
args.scale = { x: args.scale[0], y: args.scale[1], z: args.scale[2] };
|
|
670
|
-
}
|
|
671
|
-
if (Array.isArray(args.force) && args.force.length === 3) {
|
|
672
|
-
args.force = { x: args.force[0], y: args.force[1], z: args.force[2] };
|
|
673
|
-
}
|
|
674
|
-
return { ...base, toolName: 'control_actor', arguments: args };
|
|
675
|
-
}
|
|
676
|
-
case 'Editor Boundary Tests':
|
|
677
|
-
case 'Editor Control Boundary Tests': {
|
|
678
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
679
|
-
return { ...base, toolName: 'control_editor', arguments: payloadValue };
|
|
680
|
-
}
|
|
681
|
-
case 'Level Boundary Tests':
|
|
682
|
-
case 'Level Management Boundary Tests': {
|
|
683
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
684
|
-
return { ...base, toolName: 'manage_level', arguments: payloadValue };
|
|
685
|
-
}
|
|
686
|
-
case 'Animation Boundary Tests': {
|
|
687
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
688
|
-
return { ...base, toolName: 'animation_physics', arguments: payloadValue };
|
|
689
|
-
}
|
|
690
|
-
case 'Blueprint Boundary Tests':
|
|
691
|
-
case 'Blueprint Control Boundary Tests': {
|
|
692
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
693
|
-
return { ...base, toolName: 'manage_blueprint', arguments: payloadValue };
|
|
694
|
-
}
|
|
695
|
-
case 'Effects Boundary Tests':
|
|
696
|
-
case 'Effects Control Boundary Tests': {
|
|
697
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
698
|
-
const args = { ...payloadValue };
|
|
699
|
-
if (Array.isArray(args.location) && args.location.length === 3) {
|
|
700
|
-
args.location = { x: args.location[0], y: args.location[1], z: args.location[2] };
|
|
701
|
-
}
|
|
702
|
-
if (Array.isArray(args.start) && args.start.length === 3) {
|
|
703
|
-
args.start = { x: args.start[0], y: args.start[1], z: args.start[2] };
|
|
704
|
-
}
|
|
705
|
-
if (Array.isArray(args.end) && args.end.length === 3) {
|
|
706
|
-
args.end = { x: args.end[0], y: args.end[1], z: args.end[2] };
|
|
707
|
-
}
|
|
708
|
-
return { ...base, toolName: 'create_effect', arguments: args };
|
|
709
|
-
}
|
|
710
|
-
case 'Environment Boundary Tests':
|
|
711
|
-
case 'Environment Building Boundary Tests': {
|
|
712
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
713
|
-
const args = { ...payloadValue };
|
|
714
|
-
if (Array.isArray(args.location) && args.location.length === 3) {
|
|
715
|
-
args.location = { x: args.location[0], y: args.location[1], z: args.location[2] };
|
|
716
|
-
}
|
|
717
|
-
if (args.bounds) {
|
|
718
|
-
const bounds = { ...args.bounds };
|
|
719
|
-
if (Array.isArray(bounds.location) && bounds.location.length === 3) {
|
|
720
|
-
bounds.location = { x: bounds.location[0], y: bounds.location[1], z: bounds.location[2] };
|
|
721
|
-
}
|
|
722
|
-
if (Array.isArray(bounds.size) && bounds.size.length === 3) {
|
|
723
|
-
bounds.size = { x: bounds.size[0], y: bounds.size[1], z: bounds.size[2] };
|
|
724
|
-
}
|
|
725
|
-
args.bounds = bounds;
|
|
726
|
-
}
|
|
727
|
-
return { ...base, toolName: 'build_environment', arguments: args };
|
|
728
|
-
}
|
|
729
|
-
case 'System Boundary Tests':
|
|
730
|
-
case 'System Control Boundary Tests': {
|
|
731
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
732
|
-
return { ...base, toolName: 'system_control', arguments: payloadValue };
|
|
733
|
-
}
|
|
734
|
-
case 'Sequence Boundary Tests':
|
|
735
|
-
case 'Sequence Control Boundary Tests': {
|
|
736
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
737
|
-
return { ...base, toolName: 'manage_sequence', arguments: payloadValue };
|
|
738
|
-
}
|
|
739
|
-
case 'Remote Control Boundary Tests':
|
|
740
|
-
case 'Remote Control Preset Boundary Tests': {
|
|
741
|
-
// No consolidated remote control tool mapping; treated as unsupported group
|
|
742
|
-
return { ...base, skipReason: `Unknown tool group '${rawCase.groupName}'` };
|
|
743
|
-
}
|
|
744
|
-
case 'Python Execution Boundary Tests': {
|
|
745
|
-
// No consolidated Python execution tool; treated as unsupported group
|
|
746
|
-
return { ...base, skipReason: `Unknown tool group '${rawCase.groupName}'` };
|
|
747
|
-
}
|
|
748
|
-
case 'Inspection Boundary Tests': {
|
|
749
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
750
|
-
return { ...base, toolName: 'inspect', arguments: payloadValue };
|
|
751
|
-
}
|
|
752
|
-
case 'Cross-Tool Integration Tests':
|
|
753
|
-
case 'Stress Test Scenarios':
|
|
754
|
-
case 'Error Recovery Tests': {
|
|
755
|
-
if (!payloadValue) return { ...base, skipReason: 'No JSON payload provided' };
|
|
756
|
-
// These require multi-step execution or special handling
|
|
757
|
-
return { ...base, skipReason: 'Multi-step test scenario - requires custom test implementation' };
|
|
758
|
-
}
|
|
759
|
-
default:
|
|
760
|
-
return { ...base, skipReason: `Unknown tool group '${rawCase.groupName}'` };
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function evaluateExpectation(testCase, response) {
|
|
765
|
-
const lowerExpected = testCase.expected.toLowerCase();
|
|
766
|
-
const containsFailure = failureKeywords.some((word) => lowerExpected.includes(word));
|
|
767
|
-
const containsSuccess = successKeywords.some((word) => lowerExpected.includes(word));
|
|
768
|
-
|
|
769
|
-
const structuredSuccess = typeof response.structuredContent?.success === 'boolean'
|
|
770
|
-
? response.structuredContent.success
|
|
771
|
-
: undefined;
|
|
772
|
-
const actualSuccess = structuredSuccess ?? !response.isError;
|
|
773
|
-
|
|
774
|
-
// Extract actual error/message from response
|
|
775
|
-
let actualError = null;
|
|
776
|
-
let actualMessage = null;
|
|
777
|
-
if (response.structuredContent) {
|
|
778
|
-
actualError = response.structuredContent.error;
|
|
779
|
-
actualMessage = response.structuredContent.message;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// CRITICAL FIX: UE_NOT_CONNECTED errors should ALWAYS fail tests unless explicitly expected
|
|
783
|
-
if (actualError === 'UE_NOT_CONNECTED') {
|
|
784
|
-
const explicitlyExpectsDisconnection = lowerExpected.includes('not connected') ||
|
|
785
|
-
lowerExpected.includes('ue_not_connected') ||
|
|
786
|
-
lowerExpected.includes('disconnected');
|
|
787
|
-
if (!explicitlyExpectsDisconnection) {
|
|
788
|
-
return {
|
|
789
|
-
passed: false,
|
|
790
|
-
reason: `Test requires Unreal Engine connection, but got: ${actualError} - ${actualMessage}`
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// For tests that expect specific error types, validate the actual error matches
|
|
796
|
-
const expectedFailure = containsFailure && !containsSuccess;
|
|
797
|
-
if (expectedFailure && !actualSuccess) {
|
|
798
|
-
// Test expects failure and got failure - but verify it's the RIGHT kind of failure
|
|
799
|
-
const lowerReason = actualMessage?.toLowerCase() || actualError?.toLowerCase() || '';
|
|
800
|
-
const errorTypeMatch = failureKeywords.some(keyword => lowerExpected.includes(keyword) && lowerReason.includes(keyword));
|
|
801
|
-
|
|
802
|
-
// If expected outcome specifies an error type, actual error should match it
|
|
803
|
-
if (lowerExpected.includes('not found') || lowerExpected.includes('invalid') ||
|
|
804
|
-
lowerExpected.includes('missing') || lowerExpected.includes('already exists')) {
|
|
805
|
-
const passed = errorTypeMatch;
|
|
806
|
-
let reason;
|
|
807
|
-
if (response.isError) {
|
|
808
|
-
reason = response.content?.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
|
|
809
|
-
} else if (response.structuredContent) {
|
|
810
|
-
reason = JSON.stringify(response.structuredContent);
|
|
811
|
-
} else {
|
|
812
|
-
reason = 'No structured response returned';
|
|
813
|
-
}
|
|
814
|
-
return { passed, reason };
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Default evaluation logic
|
|
819
|
-
const passed = expectedFailure ? !actualSuccess : !!actualSuccess;
|
|
820
|
-
let reason;
|
|
821
|
-
if (response.isError) {
|
|
822
|
-
reason = response.content?.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
|
|
823
|
-
} else if (response.structuredContent) {
|
|
824
|
-
reason = JSON.stringify(response.structuredContent);
|
|
825
|
-
} else if (response.content?.length) {
|
|
826
|
-
reason = response.content.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
|
|
827
|
-
} else {
|
|
828
|
-
reason = 'No structured response returned';
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
return { passed, reason };
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function formatResultLine(testCase, status, detail, durationMs) {
|
|
835
|
-
const durationText = typeof durationMs === 'number' ? ` (${durationMs.toFixed(1)} ms)` : '';
|
|
836
|
-
return `[${status.toUpperCase()}] ${testCase.groupName} #${testCase.index} – ${testCase.scenario}${durationText}${detail ? ` => ${detail}` : ''}`;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
async function persistResults(results) {
|
|
840
|
-
await fs.mkdir(reportsDir, { recursive: true });
|
|
841
|
-
const serializable = results.map((result) => ({
|
|
842
|
-
group: result.groupName,
|
|
843
|
-
caseId: result.caseId,
|
|
844
|
-
index: result.index,
|
|
845
|
-
scenario: result.scenario,
|
|
846
|
-
toolName: result.toolName,
|
|
847
|
-
arguments: result.arguments,
|
|
848
|
-
status: result.status,
|
|
849
|
-
durationMs: result.durationMs,
|
|
850
|
-
detail: result.detail
|
|
851
|
-
}));
|
|
852
|
-
await fs.writeFile(resultsPath, JSON.stringify({
|
|
853
|
-
generatedAt: new Date().toISOString(),
|
|
854
|
-
docPath,
|
|
855
|
-
results: serializable
|
|
856
|
-
}, null, 2));
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
function summarize(results) {
|
|
860
|
-
const totals = results.reduce((acc, result) => {
|
|
861
|
-
acc.total += 1;
|
|
862
|
-
acc[result.status] = (acc[result.status] ?? 0) + 1;
|
|
863
|
-
return acc;
|
|
864
|
-
}, { total: 0, passed: 0, failed: 0, skipped: 0 });
|
|
865
|
-
|
|
866
|
-
console.log('\nSummary');
|
|
867
|
-
console.log('=======');
|
|
868
|
-
console.log(`Total cases processed: ${totals.total}`);
|
|
869
|
-
console.log(`Passed: ${totals.passed}`);
|
|
870
|
-
console.log(`Failed: ${totals.failed}`);
|
|
871
|
-
console.log(`Skipped: ${totals.skipped}`);
|
|
872
|
-
console.log(`Results written to: ${resultsPath}`);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function generateBenchmarkReport(results) {
|
|
876
|
-
const passedResults = results.filter(r => r.status === 'passed' && r.durationMs);
|
|
877
|
-
|
|
878
|
-
if (passedResults.length === 0) {
|
|
879
|
-
console.log('\nNo performance data available for benchmarking.');
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const durations = passedResults.map(r => r.durationMs).sort((a, b) => a - b);
|
|
884
|
-
const sum = durations.reduce((a, b) => a + b, 0);
|
|
885
|
-
const avg = sum / durations.length;
|
|
886
|
-
const median = durations[Math.floor(durations.length / 2)];
|
|
887
|
-
const min = durations[0];
|
|
888
|
-
const max = durations[durations.length - 1];
|
|
889
|
-
const p95 = durations[Math.floor(durations.length * 0.95)];
|
|
890
|
-
const p99 = durations[Math.floor(durations.length * 0.99)];
|
|
891
|
-
|
|
892
|
-
console.log('\nPerformance Benchmark');
|
|
893
|
-
console.log('====================');
|
|
894
|
-
console.log(`Total operations: ${passedResults.length}`);
|
|
895
|
-
console.log(`Average: ${avg.toFixed(2)} ms`);
|
|
896
|
-
console.log(`Median: ${median.toFixed(2)} ms`);
|
|
897
|
-
console.log(`Min: ${min.toFixed(2)} ms`);
|
|
898
|
-
console.log(`Max: ${max.toFixed(2)} ms`);
|
|
899
|
-
console.log(`95th percentile: ${p95.toFixed(2)} ms`);
|
|
900
|
-
console.log(`99th percentile: ${p99.toFixed(2)} ms`);
|
|
901
|
-
|
|
902
|
-
// Group by tool
|
|
903
|
-
const byTool = {};
|
|
904
|
-
passedResults.forEach(r => {
|
|
905
|
-
if (!byTool[r.toolName]) byTool[r.toolName] = [];
|
|
906
|
-
byTool[r.toolName].push(r.durationMs);
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
console.log('\nBy Tool:');
|
|
910
|
-
Object.entries(byTool).forEach(([tool, times]) => {
|
|
911
|
-
const toolAvg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
912
|
-
console.log(` ${tool}: ${toolAvg.toFixed(2)} ms avg (${times.length} ops)`);
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
function normalizeWindowsPath(value) {
|
|
917
|
-
if (typeof value !== 'string') return value;
|
|
918
|
-
return value.replace(/\\+/g, '\\').replace(/\/+/g, '\\');
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function hydratePlaceholders(value) {
|
|
922
|
-
if (typeof value === 'string') {
|
|
923
|
-
return value
|
|
924
|
-
.replaceAll('{{FBX_DIR}}', defaultFbxDir)
|
|
925
|
-
.replaceAll('{{FBX_TEST_MODEL}}', defaultFbxFile);
|
|
926
|
-
}
|
|
927
|
-
if (Array.isArray(value)) {
|
|
928
|
-
return value.map((entry) => hydratePlaceholders(entry));
|
|
929
|
-
}
|
|
930
|
-
if (value && typeof value === 'object') {
|
|
931
|
-
return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, hydratePlaceholders(val)]));
|
|
932
|
-
}
|
|
933
|
-
return value;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
async function ensureFbxDirectory() {
|
|
937
|
-
if (!defaultFbxDir) return;
|
|
938
|
-
try {
|
|
939
|
-
await fs.mkdir(defaultFbxDir, { recursive: true });
|
|
940
|
-
} catch (err) {
|
|
941
|
-
console.warn(`Unable to ensure FBX directory '${defaultFbxDir}':`, err);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
main().catch((err) => {
|
|
946
|
-
console.error('Unexpected error during test execution:', err);
|
|
947
|
-
process.exitCode = 1;
|
|
948
|
-
});
|