unreal-engine-mcp-server 0.5.0 → 0.5.2
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.example +1 -1
- package/.github/release-drafter-config.yml +51 -0
- package/.github/workflows/greetings.yml +5 -1
- package/.github/workflows/labeler.yml +2 -1
- package/.github/workflows/publish-mcp.yml +2 -4
- package/.github/workflows/release-drafter.yml +3 -2
- package/.github/workflows/release.yml +3 -3
- package/CHANGELOG.md +109 -0
- package/CONTRIBUTING.md +1 -1
- package/GEMINI.md +115 -0
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/README.md +166 -200
- package/dist/automation/bridge.d.ts +1 -2
- package/dist/automation/bridge.js +24 -23
- package/dist/automation/connection-manager.d.ts +1 -0
- package/dist/automation/connection-manager.js +10 -0
- package/dist/automation/message-handler.js +5 -4
- package/dist/automation/request-tracker.d.ts +4 -0
- package/dist/automation/request-tracker.js +11 -3
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/graphql/loaders.d.ts +64 -0
- package/dist/graphql/loaders.js +117 -0
- package/dist/graphql/resolvers.d.ts +3 -3
- package/dist/graphql/resolvers.js +33 -30
- package/dist/graphql/server.js +3 -1
- package/dist/graphql/types.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -2
- package/dist/server-setup.d.ts +0 -1
- package/dist/server-setup.js +0 -40
- package/dist/tools/actors.d.ts +58 -24
- package/dist/tools/actors.js +22 -6
- package/dist/tools/assets.d.ts +19 -71
- package/dist/tools/assets.js +28 -22
- package/dist/tools/base-tool.d.ts +4 -4
- package/dist/tools/base-tool.js +1 -1
- package/dist/tools/blueprint.d.ts +45 -61
- package/dist/tools/blueprint.js +43 -14
- package/dist/tools/consolidated-tool-definitions.js +2 -1
- package/dist/tools/consolidated-tool-handlers.js +96 -110
- package/dist/tools/dynamic-handler-registry.d.ts +11 -9
- package/dist/tools/dynamic-handler-registry.js +17 -95
- package/dist/tools/editor.d.ts +19 -193
- package/dist/tools/editor.js +11 -2
- package/dist/tools/environment.d.ts +8 -14
- package/dist/tools/foliage.d.ts +18 -143
- package/dist/tools/foliage.js +4 -2
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.js +14 -13
- package/dist/tools/handlers/asset-handlers.js +454 -454
- package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
- package/dist/tools/handlers/sequence-handlers.js +24 -13
- package/dist/tools/introspection.d.ts +1 -1
- package/dist/tools/introspection.js +1 -1
- package/dist/tools/landscape.d.ts +16 -116
- package/dist/tools/landscape.js +7 -3
- package/dist/tools/level.d.ts +22 -103
- package/dist/tools/level.js +26 -18
- package/dist/tools/lighting.d.ts +54 -7
- package/dist/tools/lighting.js +9 -5
- package/dist/tools/materials.d.ts +1 -1
- package/dist/tools/materials.js +5 -1
- package/dist/tools/niagara.js +37 -2
- package/dist/tools/performance.d.ts +0 -1
- package/dist/tools/performance.js +0 -1
- package/dist/tools/physics.js +5 -1
- package/dist/tools/sequence.d.ts +24 -24
- package/dist/tools/sequence.js +13 -0
- package/dist/tools/ui.d.ts +0 -2
- package/dist/types/automation-responses.d.ts +115 -0
- package/dist/types/automation-responses.js +2 -0
- package/dist/types/responses.d.ts +249 -0
- package/dist/types/responses.js +2 -0
- package/dist/types/tool-interfaces.d.ts +135 -135
- package/dist/types/tool-types.d.ts +2 -0
- package/dist/unreal-bridge.js +4 -4
- package/dist/utils/command-validator.js +7 -5
- package/dist/utils/error-handler.d.ts +24 -2
- package/dist/utils/error-handler.js +58 -23
- package/dist/utils/normalize.d.ts +7 -4
- package/dist/utils/normalize.js +12 -10
- package/dist/utils/path-security.d.ts +2 -0
- package/dist/utils/path-security.js +24 -0
- package/dist/utils/response-factory.d.ts +4 -4
- package/dist/utils/response-factory.js +15 -21
- package/dist/utils/response-validator.js +88 -73
- package/dist/utils/unreal-command-queue.d.ts +2 -0
- package/dist/utils/unreal-command-queue.js +8 -1
- package/docs/Migration-Guide-v0.5.0.md +1 -9
- package/docs/handler-mapping.md +4 -2
- package/docs/testing-guide.md +2 -2
- package/package.json +12 -6
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
- package/scripts/run-all-tests.mjs +25 -20
- package/server.json +3 -2
- package/src/automation/bridge.ts +27 -25
- package/src/automation/connection-manager.ts +18 -0
- package/src/automation/message-handler.ts +33 -8
- package/src/automation/request-tracker.ts +39 -7
- package/src/config.ts +1 -1
- package/src/constants.ts +7 -0
- package/src/graphql/loaders.ts +244 -0
- package/src/graphql/resolvers.ts +47 -49
- package/src/graphql/server.ts +3 -1
- package/src/graphql/types.ts +3 -0
- package/src/index.ts +15 -2
- package/src/resources/assets.ts +5 -4
- package/src/server/tool-registry.ts +3 -3
- package/src/server-setup.ts +3 -37
- package/src/tools/actors.ts +77 -44
- package/src/tools/animation.ts +1 -0
- package/src/tools/assets.ts +76 -65
- package/src/tools/base-tool.ts +3 -3
- package/src/tools/blueprint.ts +170 -104
- package/src/tools/consolidated-tool-definitions.ts +2 -1
- package/src/tools/consolidated-tool-handlers.ts +129 -150
- package/src/tools/dynamic-handler-registry.ts +22 -140
- package/src/tools/editor.ts +43 -29
- package/src/tools/environment.ts +21 -27
- package/src/tools/foliage.ts +28 -25
- package/src/tools/handlers/actor-handlers.ts +16 -17
- package/src/tools/handlers/asset-handlers.ts +484 -484
- package/src/tools/handlers/sequence-handlers.ts +85 -62
- package/src/tools/introspection.ts +7 -7
- package/src/tools/landscape.ts +34 -28
- package/src/tools/level.ts +100 -80
- package/src/tools/lighting.ts +25 -20
- package/src/tools/materials.ts +9 -3
- package/src/tools/niagara.ts +44 -2
- package/src/tools/performance.ts +1 -2
- package/src/tools/physics.ts +7 -1
- package/src/tools/sequence.ts +42 -26
- package/src/tools/ui.ts +1 -3
- package/src/types/automation-responses.ts +119 -0
- package/src/types/responses.ts +355 -0
- package/src/types/tool-interfaces.ts +135 -135
- package/src/types/tool-types.ts +4 -0
- package/src/unreal-bridge.ts +71 -26
- package/src/utils/command-validator.ts +47 -5
- package/src/utils/error-handler.ts +128 -45
- package/src/utils/normalize.test.ts +162 -0
- package/src/utils/normalize.ts +38 -16
- package/src/utils/path-security.ts +43 -0
- package/src/utils/response-factory.ts +29 -24
- package/src/utils/response-validator.ts +103 -87
- package/src/utils/safe-json.test.ts +90 -0
- package/src/utils/unreal-command-queue.ts +13 -1
- package/src/utils/validation.test.ts +184 -0
- package/tests/test-animation.mjs +358 -33
- package/tests/test-asset-graph.mjs +311 -0
- package/tests/test-audio.mjs +314 -116
- package/tests/test-behavior-tree.mjs +327 -144
- package/tests/test-blueprint-graph.mjs +343 -12
- package/tests/test-control-editor.mjs +85 -53
- package/tests/test-graphql.mjs +58 -8
- package/tests/test-input.mjs +349 -0
- package/tests/test-inspect.mjs +291 -61
- package/tests/test-landscape.mjs +304 -48
- package/tests/test-lighting.mjs +428 -0
- package/tests/test-manage-level.mjs +70 -51
- package/tests/test-performance.mjs +539 -0
- package/tests/test-sequence.mjs +82 -46
- package/tests/test-system.mjs +72 -33
- package/tests/test-wasm.mjs +98 -8
- package/vitest.config.ts +35 -0
- package/.github/release-drafter.yml +0 -148
- package/dist/prompts/index.d.ts +0 -21
- package/dist/prompts/index.js +0 -217
- package/dist/tools/blueprint/helpers.d.ts +0 -29
- package/dist/tools/blueprint/helpers.js +0 -182
- package/src/prompts/index.ts +0 -249
- package/src/tools/blueprint/helpers.ts +0 -189
- package/tests/test-blueprint-events.mjs +0 -35
- package/tests/test-extra-tools.mjs +0 -38
- package/tests/test-render.mjs +0 -33
- package/tests/test-search-assets.mjs +0 -66
|
@@ -27,114 +27,130 @@ function buildSummaryText(toolName: string, payload: unknown): string {
|
|
|
27
27
|
return `${toolName} responded`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// Recursively flatten data/result wrappers into effective payload
|
|
31
31
|
const effectivePayload: Record<string, any> = { ...(payload as object) };
|
|
32
|
-
if (isRecord(effectivePayload.data)) {
|
|
33
|
-
Object.assign(effectivePayload, effectivePayload.data);
|
|
34
|
-
}
|
|
35
|
-
if (isRecord(effectivePayload.result)) {
|
|
36
|
-
Object.assign(effectivePayload, effectivePayload.result);
|
|
37
|
-
}
|
|
38
32
|
|
|
39
|
-
const
|
|
33
|
+
const flattenWrappers = (obj: Record<string, any>, depth = 0): void => {
|
|
34
|
+
if (depth > 5) return; // Prevent infinite loops
|
|
35
|
+
if (isRecord(obj.data)) {
|
|
36
|
+
Object.assign(obj, obj.data);
|
|
37
|
+
delete obj.data;
|
|
38
|
+
flattenWrappers(obj, depth + 1);
|
|
39
|
+
}
|
|
40
|
+
if (isRecord(obj.result)) {
|
|
41
|
+
Object.assign(obj, obj.result);
|
|
42
|
+
delete obj.result;
|
|
43
|
+
flattenWrappers(obj, depth + 1);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
flattenWrappers(effectivePayload);
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
const addedKeys = new Set<string>();
|
|
50
|
+
|
|
51
|
+
// Keys to skip (internal/redundant)
|
|
52
|
+
const skipKeys = new Set(['requestId', 'type', 'data', 'result', 'warnings']);
|
|
53
|
+
|
|
54
|
+
// Helper to format a value for display
|
|
55
|
+
const formatValue = (val: unknown): string => {
|
|
56
|
+
if (val === null || val === undefined) return '';
|
|
57
|
+
if (typeof val === 'string') return val.length > 150 ? val.slice(0, 150) + '...' : val;
|
|
58
|
+
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
59
|
+
|
|
60
|
+
// Handle arrays - show items with names/paths
|
|
61
|
+
if (Array.isArray(val)) {
|
|
62
|
+
if (val.length === 0) return '[] (0)';
|
|
63
|
+
const items = val.slice(0, 30).map(v => {
|
|
64
|
+
if (isRecord(v)) {
|
|
65
|
+
// Try common identifier fields
|
|
66
|
+
return v.name || v.path || v.id || v.nodeId || v.nodeName || v.className ||
|
|
67
|
+
v.displayName || v.type || v.assetPath || v.objectPath ||
|
|
68
|
+
JSON.stringify(v).slice(0, 50);
|
|
69
|
+
}
|
|
70
|
+
return String(v);
|
|
71
|
+
});
|
|
72
|
+
const suffix = val.length > 30 ? `, ... (+${val.length - 30} more)` : '';
|
|
73
|
+
return `[${items.join(', ')}${suffix}] (${val.length})`;
|
|
51
74
|
}
|
|
52
|
-
}
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
76
|
+
// Handle transform-like objects (location/rotation/scale)
|
|
77
|
+
if (isRecord(val)) {
|
|
78
|
+
const keys = Object.keys(val);
|
|
79
|
+
// Check if it looks like a 3D vector/transform
|
|
80
|
+
if (keys.some(k => ['x', 'y', 'z', 'pitch', 'yaw', 'roll'].includes(k))) {
|
|
81
|
+
const x = val.x ?? val.pitch ?? 0;
|
|
82
|
+
const y = val.y ?? val.yaw ?? 0;
|
|
83
|
+
const z = val.z ?? val.roll ?? 0;
|
|
84
|
+
return `[${x}, ${y}, ${z}]`;
|
|
85
|
+
}
|
|
86
|
+
// Generic object - show key=value pairs
|
|
87
|
+
const entries = Object.entries(val).slice(0, 8);
|
|
88
|
+
const formatted = entries.map(([k, v]) => {
|
|
89
|
+
const vStr = typeof v === 'object' ? JSON.stringify(v).slice(0, 40) : String(v);
|
|
90
|
+
return `${k}=${vStr}`;
|
|
91
|
+
});
|
|
92
|
+
return `{ ${formatted.join(', ')}${keys.length > 8 ? ' ...' : ''} }`;
|
|
93
|
+
}
|
|
61
94
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
return String(val);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Process all keys in priority order
|
|
99
|
+
// 1. First add 'success' and 'error' at the start
|
|
100
|
+
for (const key of ['success', 'error']) {
|
|
101
|
+
if (effectivePayload[key] !== undefined && !addedKeys.has(key)) {
|
|
102
|
+
const formatted = formatValue(effectivePayload[key]);
|
|
103
|
+
if (formatted) {
|
|
104
|
+
parts.push(`${key}: ${formatted}`);
|
|
105
|
+
addedKeys.add(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
66
108
|
}
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
110
|
+
// 2. Then add ALL other keys dynamically
|
|
111
|
+
let hasArrays = false;
|
|
112
|
+
for (const [key, val] of Object.entries(effectivePayload)) {
|
|
113
|
+
if (addedKeys.has(key)) continue;
|
|
114
|
+
if (skipKeys.has(key)) continue;
|
|
115
|
+
if (val === undefined || val === null) continue;
|
|
116
|
+
if (typeof val === 'string' && val.trim() === '') continue;
|
|
73
117
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const name = isRecord(seq) ? (seq.name || seq.path) : seq;
|
|
77
|
-
parts.push(`Sequence: ${name}`);
|
|
78
|
-
}
|
|
118
|
+
// Skip 'message' for now - handle later to avoid duplication
|
|
119
|
+
if (key === 'message') continue;
|
|
79
120
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const usefulKeys = [
|
|
83
|
-
'success', 'error', 'message', 'assets', 'folders', 'count', 'totalCount',
|
|
84
|
-
'saved', 'valid', 'issues', 'class', 'skeleton', 'parent',
|
|
85
|
-
'package', 'dependencies', 'graph', 'tags', 'metadata', 'properties'
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
for (const key of usefulKeys) {
|
|
89
|
-
if (effectivePayload[key] !== undefined && effectivePayload[key] !== null) {
|
|
90
|
-
const val = effectivePayload[key];
|
|
91
|
-
// Special handling for objects like metadata
|
|
92
|
-
if (typeof val === 'object') {
|
|
93
|
-
if (key === 'metadata' || key === 'properties' || key === 'tags') {
|
|
94
|
-
const entries = Object.entries(val as object);
|
|
95
|
-
// Format as "Key=Value", skip generic types if possible, or just show raw
|
|
96
|
-
const formatted = entries.map(([k, v]) => `${k}=${v}`);
|
|
97
|
-
const limit = 50; // Show more items as requested
|
|
98
|
-
parts.push(`${key}: { ${formatted.slice(0, limit).join(', ')}${formatted.length > limit ? '...' : ''} }`);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
// Try to find a name if it's an object
|
|
102
|
-
// Skip complex objects unless handled above
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
121
|
+
// Track if we have arrays (to skip duplicate count/totalCount later)
|
|
122
|
+
if (Array.isArray(val) && val.length > 0) hasArrays = true;
|
|
105
123
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (strVal.length > 100) continue;
|
|
124
|
+
// Skip count/totalCount if we already have arrays showing counts
|
|
125
|
+
if ((key === 'count' || key === 'totalCount') && hasArrays) continue;
|
|
109
126
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
127
|
+
const formatted = formatValue(val);
|
|
128
|
+
if (formatted) {
|
|
129
|
+
parts.push(`${key}: ${formatted}`);
|
|
130
|
+
addedKeys.add(key);
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
133
|
|
|
116
|
-
//
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
// 3. Handle message last - but skip if it duplicates existing info
|
|
135
|
+
const message = typeof effectivePayload.message === 'string' ? normalizeText(effectivePayload.message) : '';
|
|
136
|
+
if (message && message.toLowerCase() !== 'success') {
|
|
137
|
+
// Skip if message duplicates count info
|
|
138
|
+
const isDuplicateInfo = /^(found|listed|retrieved|got|loaded|created|deleted|saved|spawned)\s+\d+/i.test(message) ||
|
|
139
|
+
/Folders:\s*\[/.test(message) ||
|
|
140
|
+
/\d+\s+(assets?|folders?|items?|actors?|components?)\s+(and|in|at)/i.test(message);
|
|
120
141
|
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
// Also skip if message content is already represented in parts
|
|
143
|
+
const messageInParts = parts.some(p => p.toLowerCase().includes(message.toLowerCase().slice(0, 30)));
|
|
144
|
+
|
|
145
|
+
if (!isDuplicateInfo && !messageInParts) {
|
|
123
146
|
parts.push(message);
|
|
124
147
|
}
|
|
125
|
-
} else {
|
|
126
|
-
// No data parts, rely on message/error
|
|
127
|
-
if (message) parts.push(message);
|
|
128
|
-
if (error) parts.push(`Error: ${error}`);
|
|
129
|
-
if (parts.length === 0 && success !== undefined) {
|
|
130
|
-
parts.push(success ? 'Success' : 'Failed');
|
|
131
|
-
}
|
|
132
148
|
}
|
|
133
149
|
|
|
134
|
-
//
|
|
135
|
-
const warnings = Array.isArray(
|
|
150
|
+
// 4. Warnings at end
|
|
151
|
+
const warnings = Array.isArray(effectivePayload.warnings) ? effectivePayload.warnings : [];
|
|
136
152
|
if (warnings.length > 0) {
|
|
137
|
-
parts.push(`Warnings: ${warnings.
|
|
153
|
+
parts.push(`Warnings: ${warnings.map((w: any) => typeof w === 'string' ? w : JSON.stringify(w)).join('; ')}`);
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
return parts.length > 0 ? parts.join(' | ') : `${toolName} responded`;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for safe-json utility
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { cleanObject } from './safe-json.js';
|
|
6
|
+
|
|
7
|
+
describe('cleanObject', () => {
|
|
8
|
+
it('removes undefined values', () => {
|
|
9
|
+
const input = { a: 1, b: undefined, c: 3 };
|
|
10
|
+
const result = cleanObject(input);
|
|
11
|
+
expect(result).toEqual({ a: 1, c: 3 });
|
|
12
|
+
expect('b' in result).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('preserves null values (only removes undefined)', () => {
|
|
16
|
+
const input = { a: 1, b: null, c: 3 };
|
|
17
|
+
const result = cleanObject(input);
|
|
18
|
+
// cleanObject preserves null, only removes undefined
|
|
19
|
+
expect(result).toEqual({ a: 1, b: null, c: 3 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('handles nested objects', () => {
|
|
23
|
+
const input = { a: 1, nested: { b: 2, c: undefined } };
|
|
24
|
+
const result = cleanObject(input);
|
|
25
|
+
expect(result.nested).toEqual({ b: 2 });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('handles arrays with undefined (preserves null)', () => {
|
|
29
|
+
const input = { arr: [1, undefined, 3, null] };
|
|
30
|
+
const result = cleanObject(input);
|
|
31
|
+
// Arrays preserve their structure, undefined becomes undefined in array
|
|
32
|
+
expect(result.arr).toEqual([1, undefined, 3, null]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('preserves falsy values that are not null/undefined', () => {
|
|
36
|
+
const input = { a: 0, b: '', c: false };
|
|
37
|
+
const result = cleanObject(input);
|
|
38
|
+
expect(result).toEqual({ a: 0, b: '', c: false });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('handles empty objects', () => {
|
|
42
|
+
expect(cleanObject({})).toEqual({});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('handles primitive inputs', () => {
|
|
46
|
+
expect(cleanObject('string')).toBe('string');
|
|
47
|
+
expect(cleanObject(42)).toBe(42);
|
|
48
|
+
expect(cleanObject(true)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('respects max depth limit', () => {
|
|
52
|
+
// Create deeply nested object
|
|
53
|
+
let deep: any = { value: 'deep' };
|
|
54
|
+
for (let i = 0; i < 15; i++) {
|
|
55
|
+
deep = { nested: deep };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Should not throw when hitting depth limit
|
|
59
|
+
expect(() => cleanObject(deep, 10)).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('handles circular reference prevention at max depth', () => {
|
|
63
|
+
const obj: any = { a: 1 };
|
|
64
|
+
obj.self = obj; // circular reference
|
|
65
|
+
|
|
66
|
+
// Should not throw - depth limiting should prevent infinite recursion
|
|
67
|
+
expect(() => cleanObject(obj, 5)).not.toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('handles Date objects (converts to empty object)', () => {
|
|
71
|
+
const date = new Date('2024-01-01');
|
|
72
|
+
const input = { created: date };
|
|
73
|
+
const result = cleanObject(input);
|
|
74
|
+
// Date objects are processed as objects using Object.keys, which returns []
|
|
75
|
+
expect(result.created).toEqual({});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('handles empty arrays', () => {
|
|
79
|
+
const input = { arr: [] };
|
|
80
|
+
const result = cleanObject(input);
|
|
81
|
+
expect(result.arr).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('handles NaN values', () => {
|
|
85
|
+
const input = { a: 1, b: NaN };
|
|
86
|
+
const result = cleanObject(input);
|
|
87
|
+
// NaN handling depends on implementation
|
|
88
|
+
expect(result.a).toBe(1);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -14,6 +14,7 @@ export class UnrealCommandQueue {
|
|
|
14
14
|
private isProcessing = false;
|
|
15
15
|
private lastCommandTime = 0;
|
|
16
16
|
private lastStatCommandTime = 0;
|
|
17
|
+
private processorInterval?: ReturnType<typeof setInterval>;
|
|
17
18
|
|
|
18
19
|
// Config
|
|
19
20
|
private readonly MIN_COMMAND_DELAY = 100;
|
|
@@ -139,13 +140,24 @@ export class UnrealCommandQueue {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
private startProcessor(): void {
|
|
142
|
-
setInterval(() => {
|
|
143
|
+
this.processorInterval = setInterval(() => {
|
|
143
144
|
if (!this.isProcessing && this.queue.length > 0) {
|
|
144
145
|
this.processQueue();
|
|
145
146
|
}
|
|
146
147
|
}, 1000);
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Stop the command queue processor and clean up the interval.
|
|
152
|
+
* Should be called during shutdown to allow clean process exit.
|
|
153
|
+
*/
|
|
154
|
+
stopProcessor(): void {
|
|
155
|
+
if (this.processorInterval) {
|
|
156
|
+
clearInterval(this.processorInterval);
|
|
157
|
+
this.processorInterval = undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
149
161
|
private delay(ms: number): Promise<void> {
|
|
150
162
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
151
163
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for validation utility functions
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
sanitizeAssetName,
|
|
7
|
+
sanitizePath,
|
|
8
|
+
validatePathLength,
|
|
9
|
+
validateAssetParams,
|
|
10
|
+
validateNumber,
|
|
11
|
+
ensureVector3,
|
|
12
|
+
ensureRotation
|
|
13
|
+
} from './validation.js';
|
|
14
|
+
|
|
15
|
+
describe('sanitizeAssetName', () => {
|
|
16
|
+
it('removes invalid characters', () => {
|
|
17
|
+
// ! is replaced with _, then trailing _ is stripped
|
|
18
|
+
expect(sanitizeAssetName('My Asset!')).toBe('My_Asset');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('removes leading/trailing whitespace', () => {
|
|
22
|
+
expect(sanitizeAssetName(' MyAsset ')).toBe('MyAsset');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('preserves valid names', () => {
|
|
26
|
+
expect(sanitizeAssetName('ValidName_123')).toBe('ValidName_123');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('replaces spaces with underscores', () => {
|
|
30
|
+
expect(sanitizeAssetName('My Cool Asset')).toBe('My_Cool_Asset');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('handles empty strings', () => {
|
|
34
|
+
const result = sanitizeAssetName('');
|
|
35
|
+
expect(result.length).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('removes consecutive underscores', () => {
|
|
39
|
+
const result = sanitizeAssetName('My__Asset');
|
|
40
|
+
expect(result).not.toContain('__');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('sanitizePath', () => {
|
|
45
|
+
it('normalizes forward slashes', () => {
|
|
46
|
+
const result = sanitizePath('/Game/MyAsset');
|
|
47
|
+
expect(result).toContain('/');
|
|
48
|
+
expect(result).not.toContain('\\\\');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('removes double slashes', () => {
|
|
52
|
+
const result = sanitizePath('/Game//MyAsset');
|
|
53
|
+
expect(result).not.toContain('//');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('handles backslashes', () => {
|
|
57
|
+
const result = sanitizePath('\\Game\\MyAsset');
|
|
58
|
+
expect(result).toContain('/');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('sanitizes path segments with dots', () => {
|
|
62
|
+
const result = sanitizePath('/Game/../MyAsset');
|
|
63
|
+
// Each segment is sanitized, .. becomes a segment that gets sanitized
|
|
64
|
+
expect(result).toContain('/');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('validatePathLength', () => {
|
|
69
|
+
it('accepts valid short paths', () => {
|
|
70
|
+
const result = validatePathLength('/Game/MyAsset');
|
|
71
|
+
expect(result.valid).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts empty paths (length 0 < 260)', () => {
|
|
75
|
+
const result = validatePathLength('');
|
|
76
|
+
// Empty string has length 0, which is < 260, so it's valid
|
|
77
|
+
expect(result.valid).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rejects excessively long paths', () => {
|
|
81
|
+
const longPath = '/Game/' + 'a'.repeat(300);
|
|
82
|
+
const result = validatePathLength(longPath);
|
|
83
|
+
expect(result.valid).toBe(false);
|
|
84
|
+
expect(result.error).toBeDefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('validateAssetParams', () => {
|
|
89
|
+
it('validates correct create params', () => {
|
|
90
|
+
const result = validateAssetParams({
|
|
91
|
+
action: 'create',
|
|
92
|
+
name: 'MyAsset',
|
|
93
|
+
path: '/Game/Assets'
|
|
94
|
+
});
|
|
95
|
+
expect(result.valid).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('handles names that need sanitization', () => {
|
|
99
|
+
const result = validateAssetParams({
|
|
100
|
+
name: '',
|
|
101
|
+
savePath: '/Game'
|
|
102
|
+
});
|
|
103
|
+
// Empty name gets sanitized to 'Asset', so this should be valid
|
|
104
|
+
expect(result.valid).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('validates path params', () => {
|
|
108
|
+
const result = validateAssetParams({
|
|
109
|
+
name: 'Target',
|
|
110
|
+
savePath: '/Game/Source'
|
|
111
|
+
});
|
|
112
|
+
expect(result.valid).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('validateNumber', () => {
|
|
117
|
+
it('accepts valid finite numbers', () => {
|
|
118
|
+
expect(() => validateNumber(42, 'test')).not.toThrow();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('rejects NaN', () => {
|
|
122
|
+
expect(() => validateNumber(NaN, 'test')).toThrow('expected a finite number');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('rejects Infinity', () => {
|
|
126
|
+
expect(() => validateNumber(Infinity, 'test')).toThrow();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('respects minimum constraint', () => {
|
|
130
|
+
expect(() => validateNumber(5, 'test', { min: 10 })).toThrow('must be >=');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('respects maximum constraint', () => {
|
|
134
|
+
expect(() => validateNumber(15, 'test', { max: 10 })).toThrow('must be <=');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('allows zero by default', () => {
|
|
138
|
+
expect(() => validateNumber(0, 'test')).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects zero when allowZero is false', () => {
|
|
142
|
+
expect(() => validateNumber(0, 'test', { allowZero: false })).toThrow('zero is not allowed');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('ensureVector3', () => {
|
|
147
|
+
it('accepts object format with x, y, z', () => {
|
|
148
|
+
const result = ensureVector3({ x: 1, y: 2, z: 3 }, 'location');
|
|
149
|
+
expect(result).toEqual([1, 2, 3]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('accepts array format', () => {
|
|
153
|
+
const result = ensureVector3([1, 2, 3], 'location');
|
|
154
|
+
expect(result).toEqual([1, 2, 3]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('throws on invalid object', () => {
|
|
158
|
+
expect(() => ensureVector3({ x: 1 }, 'location')).toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('throws on wrong array length', () => {
|
|
162
|
+
expect(() => ensureVector3([1, 2], 'location')).toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('throws on non-number values', () => {
|
|
166
|
+
expect(() => ensureVector3({ x: 'a', y: 2, z: 3 }, 'location')).toThrow();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('ensureRotation', () => {
|
|
171
|
+
it('accepts object format with pitch, yaw, roll', () => {
|
|
172
|
+
const result = ensureRotation({ pitch: 0, yaw: 90, roll: 0 }, 'rotation');
|
|
173
|
+
expect(result).toEqual([0, 90, 0]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('accepts array format', () => {
|
|
177
|
+
const result = ensureRotation([0, 90, 0], 'rotation');
|
|
178
|
+
expect(result).toEqual([0, 90, 0]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('throws on invalid input', () => {
|
|
182
|
+
expect(() => ensureRotation('invalid', 'rotation')).toThrow();
|
|
183
|
+
});
|
|
184
|
+
});
|