@akiojin/unity-mcp-server 2.33.0 → 2.37.1
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/README.md +30 -5
- package/package.json +9 -4
- package/src/core/config.js +241 -242
- package/src/core/projectInfo.js +15 -0
- package/src/core/transports/HybridStdioServerTransport.js +78 -75
- package/src/handlers/addressables/AddressablesAnalyzeToolHandler.js +45 -47
- package/src/handlers/addressables/AddressablesBuildToolHandler.js +32 -33
- package/src/handlers/addressables/AddressablesManageToolHandler.js +74 -75
- package/src/handlers/component/ComponentFieldSetToolHandler.js +419 -419
- package/src/handlers/index.js +437 -437
- package/src/handlers/input/InputGamepadToolHandler.js +162 -0
- package/src/handlers/input/InputKeyboardToolHandler.js +127 -0
- package/src/handlers/input/InputMouseToolHandler.js +188 -0
- package/src/handlers/input/InputSystemControlToolHandler.js +63 -64
- package/src/handlers/input/InputTouchToolHandler.js +178 -0
- package/src/handlers/playmode/PlaymodePlayToolHandler.js +36 -23
- package/src/handlers/playmode/PlaymodeStopToolHandler.js +32 -21
- package/src/handlers/test/TestGetStatusToolHandler.js +37 -10
- package/src/handlers/test/TestRunToolHandler.js +36 -35
- package/src/lsp/LspProcessManager.js +18 -12
- package/src/utils/editorState.js +42 -0
- package/src/utils/testResultsCache.js +70 -0
- package/src/handlers/input/InputGamepadSimulateToolHandler.js +0 -116
- package/src/handlers/input/InputKeyboardSimulateToolHandler.js +0 -79
- package/src/handlers/input/InputMouseSimulateToolHandler.js +0 -107
- package/src/handlers/input/InputTouchSimulateToolHandler.js +0 -142
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
|
|
3
|
+
const actionProperties = {
|
|
4
|
+
action: {
|
|
5
|
+
type: 'string',
|
|
6
|
+
enum: ['tap', 'swipe', 'pinch', 'multi'],
|
|
7
|
+
description: 'The touch action to perform'
|
|
8
|
+
},
|
|
9
|
+
x: {
|
|
10
|
+
type: 'number',
|
|
11
|
+
description: 'X coordinate for tap action'
|
|
12
|
+
},
|
|
13
|
+
y: {
|
|
14
|
+
type: 'number',
|
|
15
|
+
description: 'Y coordinate for tap action'
|
|
16
|
+
},
|
|
17
|
+
touchId: {
|
|
18
|
+
type: 'number',
|
|
19
|
+
minimum: 0,
|
|
20
|
+
maximum: 9,
|
|
21
|
+
description: 'Touch ID (0-9)'
|
|
22
|
+
},
|
|
23
|
+
startX: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
description: 'Start X for swipe action'
|
|
26
|
+
},
|
|
27
|
+
startY: {
|
|
28
|
+
type: 'number',
|
|
29
|
+
description: 'Start Y for swipe action'
|
|
30
|
+
},
|
|
31
|
+
endX: {
|
|
32
|
+
type: 'number',
|
|
33
|
+
description: 'End X for swipe action'
|
|
34
|
+
},
|
|
35
|
+
endY: {
|
|
36
|
+
type: 'number',
|
|
37
|
+
description: 'End Y for swipe action'
|
|
38
|
+
},
|
|
39
|
+
duration: {
|
|
40
|
+
type: 'number',
|
|
41
|
+
description: 'Swipe duration in milliseconds'
|
|
42
|
+
},
|
|
43
|
+
centerX: {
|
|
44
|
+
type: 'number',
|
|
45
|
+
description: 'Center X for pinch gesture'
|
|
46
|
+
},
|
|
47
|
+
centerY: {
|
|
48
|
+
type: 'number',
|
|
49
|
+
description: 'Center Y for pinch gesture'
|
|
50
|
+
},
|
|
51
|
+
startDistance: {
|
|
52
|
+
type: 'number',
|
|
53
|
+
description: 'Start distance between fingers for pinch'
|
|
54
|
+
},
|
|
55
|
+
endDistance: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
description: 'End distance between fingers for pinch'
|
|
58
|
+
},
|
|
59
|
+
touches: {
|
|
60
|
+
type: 'array',
|
|
61
|
+
description: 'Array of touch points for multi-touch',
|
|
62
|
+
items: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
x: { type: 'number' },
|
|
66
|
+
y: { type: 'number' },
|
|
67
|
+
phase: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
enum: ['began', 'moved', 'stationary', 'ended']
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
required: ['x', 'y']
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function validateTouchAction(params, context = 'action') {
|
|
78
|
+
if (!params || typeof params !== 'object') {
|
|
79
|
+
throw new Error(`${context} must be an object`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { action, x, y, startX, startY, endX, endY, touches } = params;
|
|
83
|
+
|
|
84
|
+
if (!action) {
|
|
85
|
+
throw new Error(`${context}: action is required`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
switch (action) {
|
|
89
|
+
case 'tap':
|
|
90
|
+
if (x === undefined || y === undefined) {
|
|
91
|
+
throw new Error(`${context}: x and y coordinates are required for tap action`);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'swipe':
|
|
95
|
+
if (
|
|
96
|
+
startX === undefined ||
|
|
97
|
+
startY === undefined ||
|
|
98
|
+
endX === undefined ||
|
|
99
|
+
endY === undefined
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(`${context}: startX, startY, endX, and endY are required for swipe action`);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case 'pinch':
|
|
105
|
+
// Optional parameters with defaults handled downstream
|
|
106
|
+
break;
|
|
107
|
+
case 'multi':
|
|
108
|
+
if (!touches || !Array.isArray(touches) || touches.length === 0) {
|
|
109
|
+
throw new Error(`${context}: touches array is required for multi action`);
|
|
110
|
+
}
|
|
111
|
+
if (touches.length > 10) {
|
|
112
|
+
throw new Error(`${context}: maximum 10 simultaneous touches supported`);
|
|
113
|
+
}
|
|
114
|
+
touches.forEach((touch, i) => {
|
|
115
|
+
if (touch.x === undefined || touch.y === undefined) {
|
|
116
|
+
throw new Error(`${context}: touches[${i}] must include x and y`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`${context}: invalid action ${action}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handler for the input_touch tool
|
|
127
|
+
*/
|
|
128
|
+
export class InputTouchToolHandler extends BaseToolHandler {
|
|
129
|
+
constructor(unityConnection) {
|
|
130
|
+
super('input_touch', 'Touch input (tap/swipe/pinch/multi) with batching.', {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
...actionProperties,
|
|
134
|
+
actions: {
|
|
135
|
+
type: 'array',
|
|
136
|
+
description: 'Batch multiple touch actions executed in order',
|
|
137
|
+
items: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: { ...actionProperties },
|
|
140
|
+
required: ['action']
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
anyOf: [{ required: ['action'] }, { required: ['actions'] }]
|
|
145
|
+
});
|
|
146
|
+
this.unityConnection = unityConnection;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
validate(params) {
|
|
150
|
+
const { actions } = params;
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(actions)) {
|
|
153
|
+
if (actions.length === 0) {
|
|
154
|
+
throw new Error('actions must contain at least one entry');
|
|
155
|
+
}
|
|
156
|
+
actions.forEach((action, index) => validateTouchAction(action, `actions[${index}]`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (actions !== undefined && !Array.isArray(actions)) {
|
|
161
|
+
throw new Error('actions must be an array');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
validateTouchAction(params);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async execute(params) {
|
|
168
|
+
if (!this.unityConnection.isConnected()) {
|
|
169
|
+
await this.unityConnection.connect();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const hasBatch = Array.isArray(params.actions) && params.actions.length > 0;
|
|
173
|
+
const payload = hasBatch ? { actions: params.actions } : params;
|
|
174
|
+
|
|
175
|
+
const result = await this.unityConnection.sendCommand('input_touch', payload);
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { extractEditorState } from '../../utils/editorState.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Handler for the playmode_play tool
|
|
5
6
|
*/
|
|
6
7
|
export class PlaymodePlayToolHandler extends BaseToolHandler {
|
|
7
8
|
constructor(unityConnection) {
|
|
8
|
-
super(
|
|
9
|
-
'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
properties: {},
|
|
14
|
-
required: []
|
|
15
|
-
}
|
|
16
|
-
);
|
|
9
|
+
super('playmode_play', 'Enter Play Mode.', {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {},
|
|
12
|
+
required: []
|
|
13
|
+
});
|
|
17
14
|
this.unityConnection = unityConnection;
|
|
18
15
|
}
|
|
19
16
|
|
|
@@ -23,11 +20,14 @@ export class PlaymodePlayToolHandler extends BaseToolHandler {
|
|
|
23
20
|
* @returns {Promise<object>} Play mode state
|
|
24
21
|
*/
|
|
25
22
|
async execute(params) {
|
|
26
|
-
const initialDelayMs =
|
|
27
|
-
|
|
23
|
+
const initialDelayMs =
|
|
24
|
+
typeof params?.initialDelayMs === 'number' ? params.initialDelayMs : 3000;
|
|
25
|
+
const reconnectIntervalMs =
|
|
26
|
+
typeof params?.reconnectIntervalMs === 'number' ? params.reconnectIntervalMs : 500;
|
|
28
27
|
const pollIntervalMs = typeof params?.pollIntervalMs === 'number' ? params.pollIntervalMs : 800;
|
|
29
28
|
const maxWaitMs = typeof params?.maxWaitMs === 'number' ? params.maxWaitMs : null; // null = unlimited
|
|
30
29
|
|
|
30
|
+
let lastPlayMessage = 'Entered play mode';
|
|
31
31
|
try {
|
|
32
32
|
if (!this.unityConnection.isConnected()) {
|
|
33
33
|
await this.unityConnection.connect();
|
|
@@ -38,31 +38,37 @@ export class PlaymodePlayToolHandler extends BaseToolHandler {
|
|
|
38
38
|
error.code = 'UNITY_ERROR';
|
|
39
39
|
throw error;
|
|
40
40
|
}
|
|
41
|
+
lastPlayMessage = result?.message || lastPlayMessage;
|
|
41
42
|
const startOk = Date.now();
|
|
42
43
|
for (;;) {
|
|
43
44
|
try {
|
|
44
|
-
const state = await this.unityConnection.sendCommand('
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const state = await this.unityConnection.sendCommand('get_editor_state', {});
|
|
46
|
+
const editorState = extractEditorState(state);
|
|
47
|
+
if (editorState?.isPlaying) {
|
|
48
|
+
return { status: 'success', message: lastPlayMessage, state: editorState };
|
|
47
49
|
}
|
|
48
50
|
} catch {}
|
|
49
|
-
await sleep(pollIntervalMs);
|
|
51
|
+
await sleep(Math.max(0, pollIntervalMs));
|
|
50
52
|
if (maxWaitMs != null && Date.now() - startOk > maxWaitMs) {
|
|
51
53
|
return result;
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
} catch (err) {
|
|
55
57
|
const msg = err?.message || '';
|
|
56
|
-
const transient = /(Connection closed|timeout|ECONNRESET|EPIPE|socket|Not connected)/i.test(
|
|
58
|
+
const transient = /(Connection closed|timeout|ECONNRESET|EPIPE|socket|Not connected)/i.test(
|
|
59
|
+
msg
|
|
60
|
+
);
|
|
57
61
|
if (!transient) throw err;
|
|
58
62
|
|
|
59
63
|
const start = Date.now();
|
|
60
|
-
await sleep(initialDelayMs);
|
|
64
|
+
await sleep(Math.max(0, initialDelayMs));
|
|
61
65
|
// Reconnect until connected (unlimited unless maxWaitMs specified)
|
|
62
66
|
for (;;) {
|
|
63
67
|
if (this.unityConnection.isConnected()) break;
|
|
64
|
-
try {
|
|
65
|
-
|
|
68
|
+
try {
|
|
69
|
+
await this.unityConnection.connect();
|
|
70
|
+
} catch {}
|
|
71
|
+
await sleep(Math.max(0, reconnectIntervalMs));
|
|
66
72
|
if (maxWaitMs != null && Date.now() - start > maxWaitMs) {
|
|
67
73
|
const e = new Error('Timed out waiting to reconnect after Play start');
|
|
68
74
|
e.code = 'RECONNECT_TIMEOUT';
|
|
@@ -73,11 +79,16 @@ export class PlaymodePlayToolHandler extends BaseToolHandler {
|
|
|
73
79
|
for (;;) {
|
|
74
80
|
try {
|
|
75
81
|
const state = await this.unityConnection.sendCommand('get_editor_state', {});
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
const editorState = extractEditorState(state);
|
|
83
|
+
if (editorState?.isPlaying) {
|
|
84
|
+
return {
|
|
85
|
+
status: 'success',
|
|
86
|
+
message: `${lastPlayMessage} (reconnected after reload)`,
|
|
87
|
+
state: editorState
|
|
88
|
+
};
|
|
78
89
|
}
|
|
79
90
|
} catch {}
|
|
80
|
-
await sleep(pollIntervalMs);
|
|
91
|
+
await sleep(Math.max(0, pollIntervalMs));
|
|
81
92
|
if (maxWaitMs != null && Date.now() - start > maxWaitMs) {
|
|
82
93
|
const e = new Error('Timed out waiting for Play mode after reconnect');
|
|
83
94
|
e.code = 'PLAY_WAIT_TIMEOUT';
|
|
@@ -88,4 +99,6 @@ export class PlaymodePlayToolHandler extends BaseToolHandler {
|
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
function sleep(ms) {
|
|
102
|
+
function sleep(ms) {
|
|
103
|
+
return new Promise(r => setTimeout(r, Math.max(0, ms || 0)));
|
|
104
|
+
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { extractEditorState } from '../../utils/editorState.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Handler for the playmode_stop tool
|
|
5
6
|
*/
|
|
6
7
|
export class PlaymodeStopToolHandler extends BaseToolHandler {
|
|
7
8
|
constructor(unityConnection) {
|
|
8
|
-
super(
|
|
9
|
-
'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
properties: {},
|
|
14
|
-
required: []
|
|
15
|
-
}
|
|
16
|
-
);
|
|
9
|
+
super('playmode_stop', 'Exit Play Mode and return to Edit Mode.', {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {},
|
|
12
|
+
required: []
|
|
13
|
+
});
|
|
17
14
|
this.unityConnection = unityConnection;
|
|
18
15
|
}
|
|
19
16
|
|
|
@@ -23,7 +20,8 @@ export class PlaymodeStopToolHandler extends BaseToolHandler {
|
|
|
23
20
|
* @returns {Promise<object>} Play mode state
|
|
24
21
|
*/
|
|
25
22
|
async execute(params) {
|
|
26
|
-
const reconnectIntervalMs =
|
|
23
|
+
const reconnectIntervalMs =
|
|
24
|
+
typeof params?.reconnectIntervalMs === 'number' ? params.reconnectIntervalMs : 500;
|
|
27
25
|
const pollIntervalMs = typeof params?.pollIntervalMs === 'number' ? params.pollIntervalMs : 500;
|
|
28
26
|
const maxWaitMs = typeof params?.maxWaitMs === 'number' ? params.maxWaitMs : null; // null = unlimited
|
|
29
27
|
|
|
@@ -37,33 +35,44 @@ export class PlaymodeStopToolHandler extends BaseToolHandler {
|
|
|
37
35
|
error.code = 'UNITY_ERROR';
|
|
38
36
|
throw error;
|
|
39
37
|
}
|
|
38
|
+
const message = result?.message || 'Exited play mode';
|
|
40
39
|
const startOk = Date.now();
|
|
41
40
|
for (;;) {
|
|
42
41
|
try {
|
|
43
|
-
const state = await this.unityConnection.sendCommand('
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const state = await this.unityConnection.sendCommand('get_editor_state', {});
|
|
43
|
+
const editorState = extractEditorState(state);
|
|
44
|
+
if (editorState && !editorState.isPlaying) {
|
|
45
|
+
return { status: 'success', message, state: editorState };
|
|
46
46
|
}
|
|
47
47
|
} catch {}
|
|
48
|
-
await sleep(pollIntervalMs);
|
|
48
|
+
await sleep(Math.max(0, pollIntervalMs));
|
|
49
49
|
if (maxWaitMs != null && Date.now() - startOk > maxWaitMs) return result;
|
|
50
50
|
}
|
|
51
51
|
} catch (err) {
|
|
52
|
-
const transient = /(Connection closed|timeout|ECONNRESET|EPIPE|socket|Not connected)/i.test(
|
|
52
|
+
const transient = /(Connection closed|timeout|ECONNRESET|EPIPE|socket|Not connected)/i.test(
|
|
53
|
+
err?.message || ''
|
|
54
|
+
);
|
|
53
55
|
if (!transient) throw err;
|
|
54
56
|
const start = Date.now();
|
|
55
57
|
for (;;) {
|
|
56
58
|
if (!this.unityConnection.isConnected()) {
|
|
57
|
-
try {
|
|
58
|
-
|
|
59
|
+
try {
|
|
60
|
+
await this.unityConnection.connect();
|
|
61
|
+
} catch {}
|
|
62
|
+
await sleep(Math.max(0, reconnectIntervalMs));
|
|
59
63
|
}
|
|
60
64
|
try {
|
|
61
65
|
const state = await this.unityConnection.sendCommand('get_editor_state', {});
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const editorState = extractEditorState(state);
|
|
67
|
+
if (editorState && !editorState.isPlaying) {
|
|
68
|
+
return {
|
|
69
|
+
status: 'success',
|
|
70
|
+
message: 'Exited play mode (reconnected after reload)',
|
|
71
|
+
state: editorState
|
|
72
|
+
};
|
|
64
73
|
}
|
|
65
74
|
} catch {}
|
|
66
|
-
await sleep(pollIntervalMs);
|
|
75
|
+
await sleep(Math.max(0, pollIntervalMs));
|
|
67
76
|
if (maxWaitMs != null && Date.now() - start > maxWaitMs) {
|
|
68
77
|
const e = new Error('Timed out waiting for stop');
|
|
69
78
|
e.code = 'PLAY_WAIT_TIMEOUT';
|
|
@@ -74,4 +83,6 @@ export class PlaymodeStopToolHandler extends BaseToolHandler {
|
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
function sleep(ms) {
|
|
86
|
+
function sleep(ms) {
|
|
87
|
+
return new Promise(r => setTimeout(r, Math.max(0, ms || 0)));
|
|
88
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import * as testResultsCache from '../../utils/testResultsCache.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Handler for getting Unity test execution status
|
|
@@ -6,16 +7,24 @@ import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
|
6
7
|
*/
|
|
7
8
|
export class TestGetStatusToolHandler extends BaseToolHandler {
|
|
8
9
|
constructor(unityConnection) {
|
|
9
|
-
super(
|
|
10
|
-
'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
super('test_get_status', 'Get current Unity test execution status and results', {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
includeTestResults: {
|
|
14
|
+
type: 'boolean',
|
|
15
|
+
default: false,
|
|
16
|
+
description: 'Include the summary of the latest exported test results'
|
|
17
|
+
},
|
|
18
|
+
includeFileContent: {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
default: false,
|
|
21
|
+
description: 'When includeTestResults is true, also include the JSON file contents'
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
|
-
);
|
|
24
|
+
});
|
|
17
25
|
|
|
18
26
|
this.unityConnection = unityConnection;
|
|
27
|
+
this.testResultsCache = testResultsCache;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
/**
|
|
@@ -30,7 +39,7 @@ export class TestGetStatusToolHandler extends BaseToolHandler {
|
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
// Send command to Unity
|
|
33
|
-
const response = await this.unityConnection.sendCommand('get_test_status', params);
|
|
42
|
+
const response = await this.unityConnection.sendCommand('get_test_status', params || {});
|
|
34
43
|
|
|
35
44
|
// Handle Unity response
|
|
36
45
|
if (response.error) {
|
|
@@ -38,10 +47,15 @@ export class TestGetStatusToolHandler extends BaseToolHandler {
|
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
// Return status directly if still running or idle
|
|
41
|
-
if (response.status === 'running'
|
|
50
|
+
if (response.status === 'running') {
|
|
42
51
|
return response;
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
if (response.status === 'idle') {
|
|
55
|
+
const cached = await this.testResultsCache.loadCachedTestResults();
|
|
56
|
+
return cached || response;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
// Format completed results
|
|
46
60
|
if (response.status === 'completed') {
|
|
47
61
|
const result = {
|
|
@@ -57,7 +71,14 @@ export class TestGetStatusToolHandler extends BaseToolHandler {
|
|
|
57
71
|
tests: response.tests || []
|
|
58
72
|
};
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
if (response.latestResult) {
|
|
75
|
+
result.latestResult = response.latestResult;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const artifactPath = await this.testResultsCache.persistTestResults(result);
|
|
79
|
+
const cachedResult = await this.testResultsCache.loadCachedTestResults(artifactPath);
|
|
80
|
+
|
|
81
|
+
return cachedResult || result;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
// Error status
|
|
@@ -73,6 +94,12 @@ export class TestGetStatusToolHandler extends BaseToolHandler {
|
|
|
73
94
|
checkStatus: {
|
|
74
95
|
description: 'Check current test execution status',
|
|
75
96
|
params: {}
|
|
97
|
+
},
|
|
98
|
+
checkWithResults: {
|
|
99
|
+
description: 'Check status and include last exported test results summary',
|
|
100
|
+
params: {
|
|
101
|
+
includeTestResults: true
|
|
102
|
+
}
|
|
76
103
|
}
|
|
77
104
|
};
|
|
78
105
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { resetTestResultsCache } from '../../utils/testResultsCache.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Handler for running Unity NUnit tests via Test Runner API
|
|
@@ -6,42 +7,39 @@ import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
|
6
7
|
*/
|
|
7
8
|
export class TestRunToolHandler extends BaseToolHandler {
|
|
8
9
|
constructor(unityConnection) {
|
|
9
|
-
super(
|
|
10
|
-
'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
type: 'string',
|
|
40
|
-
description: 'Export test results to NUnit XML file at specified path'
|
|
41
|
-
}
|
|
10
|
+
super('test_run', 'Run Unity NUnit tests in the current project', {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
testMode: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
enum: ['EditMode', 'PlayMode', 'All'],
|
|
16
|
+
default: 'EditMode',
|
|
17
|
+
description:
|
|
18
|
+
'Test mode to run (EditMode: editor tests, PlayMode: runtime tests, All: both)'
|
|
19
|
+
},
|
|
20
|
+
filter: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Filter tests by class name (e.g., "PlayerControllerTests")'
|
|
23
|
+
},
|
|
24
|
+
category: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Filter tests by category attribute (e.g., "Integration")'
|
|
27
|
+
},
|
|
28
|
+
namespace: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Filter tests by namespace (e.g., "MyGame.Tests.Player")'
|
|
31
|
+
},
|
|
32
|
+
includeDetails: {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
default: false,
|
|
35
|
+
description: 'Include detailed test results for each individual test'
|
|
36
|
+
},
|
|
37
|
+
exportPath: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Export test results to NUnit XML file at specified path'
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
|
-
);
|
|
42
|
+
});
|
|
45
43
|
|
|
46
44
|
this.unityConnection = unityConnection;
|
|
47
45
|
}
|
|
@@ -57,6 +55,8 @@ export class TestRunToolHandler extends BaseToolHandler {
|
|
|
57
55
|
await this.unityConnection.connect();
|
|
58
56
|
}
|
|
59
57
|
|
|
58
|
+
await resetTestResultsCache();
|
|
59
|
+
|
|
60
60
|
// Send command to Unity
|
|
61
61
|
const response = await this.unityConnection.sendCommand('run_tests', params);
|
|
62
62
|
|
|
@@ -69,7 +69,8 @@ export class TestRunToolHandler extends BaseToolHandler {
|
|
|
69
69
|
if (response.status === 'running') {
|
|
70
70
|
return {
|
|
71
71
|
status: 'running',
|
|
72
|
-
message:
|
|
72
|
+
message:
|
|
73
|
+
response.message || 'Test execution started. Use get_test_status to check progress.'
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -2,44 +2,50 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import { logger } from '../core/config.js';
|
|
3
3
|
import { CSharpLspUtils } from './CSharpLspUtils.js';
|
|
4
4
|
|
|
5
|
+
const sharedState = {
|
|
6
|
+
proc: null,
|
|
7
|
+
starting: null
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
export class LspProcessManager {
|
|
6
11
|
constructor() {
|
|
7
|
-
this.
|
|
8
|
-
this.starting = null;
|
|
12
|
+
this.state = sharedState;
|
|
9
13
|
this.utils = new CSharpLspUtils();
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
async ensureStarted() {
|
|
13
|
-
if (this.proc && !this.proc.killed) return this.proc;
|
|
14
|
-
if (this.starting) return this.starting;
|
|
15
|
-
this.starting = (async () => {
|
|
17
|
+
if (this.state.proc && !this.state.proc.killed) return this.state.proc;
|
|
18
|
+
if (this.state.starting) return this.state.starting;
|
|
19
|
+
this.state.starting = (async () => {
|
|
16
20
|
const rid = this.utils.detectRid();
|
|
17
21
|
const bin = await this.utils.ensureLocal(rid);
|
|
18
22
|
const proc = spawn(bin, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
19
23
|
proc.on('error', e => logger.error(`[csharp-lsp] process error: ${e.message}`));
|
|
20
24
|
proc.on('close', (code, sig) => {
|
|
21
25
|
logger.warn(`[csharp-lsp] exited code=${code} signal=${sig || ''}`);
|
|
22
|
-
this.proc
|
|
26
|
+
if (this.state.proc === proc) {
|
|
27
|
+
this.state.proc = null;
|
|
28
|
+
}
|
|
23
29
|
});
|
|
24
30
|
proc.stderr.on('data', d => {
|
|
25
31
|
const s = String(d || '').trim();
|
|
26
32
|
if (s) logger.debug(`[csharp-lsp] ${s}`);
|
|
27
33
|
});
|
|
28
|
-
this.proc = proc;
|
|
34
|
+
this.state.proc = proc;
|
|
29
35
|
logger.info(`[csharp-lsp] started (pid=${proc.pid})`);
|
|
30
36
|
return proc;
|
|
31
37
|
})();
|
|
32
38
|
try {
|
|
33
|
-
return await this.starting;
|
|
39
|
+
return await this.state.starting;
|
|
34
40
|
} finally {
|
|
35
|
-
this.starting = null;
|
|
41
|
+
this.state.starting = null;
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
async stop(graceMs = 3000) {
|
|
40
|
-
if (!this.proc || this.proc.killed) return;
|
|
41
|
-
const p = this.proc;
|
|
42
|
-
this.proc = null;
|
|
46
|
+
if (!this.state.proc || this.state.proc.killed) return;
|
|
47
|
+
const p = this.state.proc;
|
|
48
|
+
this.state.proc = null;
|
|
43
49
|
try {
|
|
44
50
|
// Send LSP shutdown/exit if possible
|
|
45
51
|
const shutdown = obj => {
|