@mindexec/cli 0.2.152 → 0.2.154
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/package.json +6 -5
- package/scripts/remote-fleet-render-smoke.mjs +131 -102
- package/scripts/remote-input-ws-smoke.mjs +263 -0
- package/server.js +249 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +567 -94
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.5gltgdqyo3.dll → MindExecution.Plugins.Concept.4ncya2e794.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.cn6ppe6qoz.dll → MindExecution.Plugins.PlanMaster.0yfii1nh3p.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.d7nnsxrftv.dll → MindExecution.Plugins.YouTube.y4vxx807fo.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.mnpwk5h86c.dll → MindExecution.Shared.50ptsgk9eg.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Web.ygql0sg6b3.dll → MindExecution.Web.hztii75ar1.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +11 -11
- package/wwwroot/service-worker-assets.js +13 -13
- package/wwwroot/service-worker.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindexec/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.154",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node launch-bridge.cjs",
|
|
23
23
|
"dev": "node launch-bridge.cjs --watch",
|
|
24
|
-
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
|
|
24
|
+
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
|
|
25
25
|
"test:auth": "node scripts/auth-session-smoke.mjs",
|
|
26
26
|
"test:remote": "node scripts/remote-hub-smoke.mjs",
|
|
27
27
|
"test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
|
|
28
28
|
"test:remote:render": "node scripts/remote-fleet-render-smoke.mjs",
|
|
29
|
-
"test:remote:http": "node scripts/remote-http-smoke.mjs",
|
|
30
|
-
"test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
|
|
31
|
-
"test:remote:
|
|
29
|
+
"test:remote:http": "node scripts/remote-http-smoke.mjs",
|
|
30
|
+
"test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
|
|
31
|
+
"test:remote:input-ws": "node scripts/remote-input-ws-smoke.mjs",
|
|
32
|
+
"test:remote:agent-ws": "node scripts/remote-agent-ws-smoke.mjs",
|
|
32
33
|
"test:remote:managed": "node scripts/remote-agent-managed-smoke.mjs",
|
|
33
34
|
"test:remote:registry": "node scripts/remote-registry-follower-smoke.mjs",
|
|
34
35
|
"test:remote:package": "node scripts/remote-agent-package-smoke.mjs",
|
|
@@ -528,7 +528,8 @@ async function loadCss3DManager() {
|
|
|
528
528
|
status: 200,
|
|
529
529
|
json: async () => ({
|
|
530
530
|
bridgeToken: 'render-smoke-bridge-token',
|
|
531
|
-
remoteFrameWsPath: '/api/remote/frames/ws'
|
|
531
|
+
remoteFrameWsPath: '/api/remote/frames/ws',
|
|
532
|
+
remoteInputWsPath: '/api/remote/input/ws'
|
|
532
533
|
})
|
|
533
534
|
};
|
|
534
535
|
},
|
|
@@ -864,6 +865,18 @@ try {
|
|
|
864
865
|
};
|
|
865
866
|
}
|
|
866
867
|
|
|
868
|
+
if (methodName === 'AddRemoteFleetDeviceNodeFromJs') {
|
|
869
|
+
const deviceId = String(args[1] || '');
|
|
870
|
+
return {
|
|
871
|
+
success: true,
|
|
872
|
+
nodeId: 'remote-device-render-smoke',
|
|
873
|
+
deviceId,
|
|
874
|
+
nodeState: buildDeviceNode(
|
|
875
|
+
devices.find(device => device.DeviceId === deviceId) || focusedDevice,
|
|
876
|
+
hub.getStatus())
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
867
880
|
return { success: true };
|
|
868
881
|
}
|
|
869
882
|
};
|
|
@@ -1094,110 +1107,16 @@ try {
|
|
|
1094
1107
|
assert.equal(rerenderedPatchPreview.dataset.remoteFleetFrameSeq, String(websocketFrameSeq));
|
|
1095
1108
|
assert.equal(rerenderedPatchPreview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]')?.style.display, 'block');
|
|
1096
1109
|
|
|
1097
|
-
const liveStartCallsBeforeControl = dotNetCalls.filter(call => call.methodName === 'StartRemoteFleetLiveStreamFromJs').length;
|
|
1098
|
-
const sendCountBeforeControl = diagnostics.webSocketSends.length;
|
|
1099
|
-
const drawCountBeforeControl = document.canvasDrawCalls.length;
|
|
1100
1110
|
const controlCard = bodyView.querySelector(`article[data-device-id="${patchTarget.DeviceId}"]`);
|
|
1101
1111
|
assert.ok(controlCard);
|
|
1102
1112
|
controlCard.dispatchEvent({ type: 'dblclick', button: 0 });
|
|
1103
1113
|
await wait(30);
|
|
1104
|
-
const controlPopup = document.body.querySelector('[data-remote-fleet-control-popup="true"]');
|
|
1105
|
-
assert.ok(controlPopup, 'double-clicking an MDM tile should open the control popup');
|
|
1106
|
-
const controlShell = controlPopup.querySelector('[data-remote-fleet-control-shell="true"]');
|
|
1107
|
-
const controlStage = controlPopup.querySelector('[data-remote-fleet-control-stage="true"]');
|
|
1108
|
-
const controlCanvas = controlPopup.querySelector('[data-remote-fleet-control-canvas="true"]');
|
|
1109
|
-
assert.ok(controlShell);
|
|
1110
|
-
assert.ok(controlStage);
|
|
1111
|
-
assert.ok(controlCanvas);
|
|
1112
|
-
assert.equal(document.activeElement, controlShell);
|
|
1113
|
-
const controlStartCalls = dotNetCalls
|
|
1114
|
-
.filter(call => call.methodName === 'StartRemoteFleetLiveStreamFromJs')
|
|
1115
|
-
.slice(liveStartCallsBeforeControl);
|
|
1116
|
-
assert.equal(controlStartCalls.length, 1);
|
|
1117
|
-
assert.equal(controlStartCalls[0].args[0], 'remote-fleet-render-smoke');
|
|
1118
|
-
assert.equal(controlStartCalls[0].args[1], patchTarget.DeviceId);
|
|
1119
|
-
assert.equal(controlStartCalls[0].args[2], 20);
|
|
1120
|
-
const controlSubscribe = diagnostics.webSocketSends
|
|
1121
|
-
.slice(sendCountBeforeControl)
|
|
1122
|
-
.map(send => {
|
|
1123
|
-
try {
|
|
1124
|
-
return JSON.parse(send.payload);
|
|
1125
|
-
} catch {
|
|
1126
|
-
return null;
|
|
1127
|
-
}
|
|
1128
|
-
})
|
|
1129
|
-
.find(message =>
|
|
1130
|
-
message
|
|
1131
|
-
&& message.type === 'subscribe'
|
|
1132
|
-
&& message.nodeId === 'remote-fleet-render-smoke'
|
|
1133
|
-
&& message.autoStartLive === true
|
|
1134
|
-
&& message.fps === 20
|
|
1135
|
-
&& Array.isArray(message.deviceIds)
|
|
1136
|
-
&& message.deviceIds.includes(patchTarget.DeviceId));
|
|
1137
|
-
assert.ok(controlSubscribe, 'control popup should reuse the binary frame WebSocket with a 20fps subscription');
|
|
1138
|
-
|
|
1139
|
-
const controlFrameSeq = 20001;
|
|
1140
|
-
liveWs.onmessage?.({
|
|
1141
|
-
data: new Blob([encodeRemoteBinaryFrame({
|
|
1142
|
-
type: 'remote.frame.binary',
|
|
1143
|
-
deviceId: patchTarget.DeviceId,
|
|
1144
|
-
kind: 'live',
|
|
1145
|
-
frameSeq: controlFrameSeq,
|
|
1146
|
-
streamId: 'render-smoke-control-live',
|
|
1147
|
-
contentHash: `render-smoke-control-live-${controlFrameSeq}`,
|
|
1148
|
-
mimeType: 'image/jpeg',
|
|
1149
|
-
width: 64,
|
|
1150
|
-
height: 36,
|
|
1151
|
-
capturedAt: new Date().toISOString(),
|
|
1152
|
-
receivedAt: new Date().toISOString()
|
|
1153
|
-
})])
|
|
1154
|
-
});
|
|
1155
|
-
await wait(40);
|
|
1156
|
-
assert.equal(controlCanvas.style.display, 'block');
|
|
1157
|
-
assert.ok(
|
|
1158
|
-
document.canvasDrawCalls
|
|
1159
|
-
.slice(drawCountBeforeControl)
|
|
1160
|
-
.some(call => call.canvas === controlCanvas && call.op === 'drawImage'),
|
|
1161
|
-
'control popup should paint binary WebSocket frames directly to its canvas');
|
|
1162
|
-
|
|
1163
|
-
controlStage.dispatchEvent({
|
|
1164
|
-
type: 'pointerdown',
|
|
1165
|
-
button: 0,
|
|
1166
|
-
pointerId: 7,
|
|
1167
|
-
clientX: 160,
|
|
1168
|
-
clientY: 90
|
|
1169
|
-
});
|
|
1170
|
-
controlStage.dispatchEvent({
|
|
1171
|
-
type: 'pointerup',
|
|
1172
|
-
button: 0,
|
|
1173
|
-
pointerId: 7,
|
|
1174
|
-
clientX: 160,
|
|
1175
|
-
clientY: 90
|
|
1176
|
-
});
|
|
1177
|
-
controlShell.dispatchEvent({
|
|
1178
|
-
type: 'keydown',
|
|
1179
|
-
key: 'A',
|
|
1180
|
-
code: 'KeyA',
|
|
1181
|
-
repeat: false
|
|
1182
|
-
});
|
|
1183
|
-
await wait(20);
|
|
1184
|
-
const inputCalls = dotNetCalls.filter(call => call.methodName === 'SendRemoteFleetInputFromJs');
|
|
1185
|
-
assert.ok(inputCalls.length >= 3, 'control popup should send pointer and keyboard input through JS interop');
|
|
1186
|
-
const pointerDown = inputCalls.find(call => call.args[2]?.type === 'pointerDown');
|
|
1187
|
-
const keyDown = inputCalls.find(call => call.args[2]?.type === 'keyDown');
|
|
1188
|
-
assert.ok(pointerDown);
|
|
1189
|
-
assert.ok(keyDown);
|
|
1190
|
-
assert.equal(pointerDown.args[0], 'remote-fleet-render-smoke');
|
|
1191
|
-
assert.equal(pointerDown.args[1], patchTarget.DeviceId);
|
|
1192
|
-
assert.equal(pointerDown.args[2].button, 'left');
|
|
1193
|
-
assert.ok(pointerDown.args[2].controlLeaseId);
|
|
1194
|
-
assert.ok(pointerDown.args[2].normalizedX >= 0 && pointerDown.args[2].normalizedX <= 1);
|
|
1195
|
-
assert.ok(pointerDown.args[2].normalizedY >= 0 && pointerDown.args[2].normalizedY <= 1);
|
|
1196
|
-
assert.equal(keyDown.args[2].key, 'A');
|
|
1197
|
-
assert.equal(keyDown.args[2].code, 'KeyA');
|
|
1198
|
-
manager.closeRemoteFleetControlPopupForTest('render-smoke');
|
|
1199
|
-
await wait(20);
|
|
1200
1114
|
assert.equal(document.body.querySelector('[data-remote-fleet-control-popup="true"]'), null);
|
|
1115
|
+
const addControlNodeCall = dotNetCalls.find(call =>
|
|
1116
|
+
call.methodName === 'AddRemoteFleetDeviceNodeFromJs'
|
|
1117
|
+
&& call.args[0] === 'remote-fleet-render-smoke'
|
|
1118
|
+
&& call.args[1] === patchTarget.DeviceId);
|
|
1119
|
+
assert.ok(addControlNodeCall, 'double-clicking an MDM tile should create a remote-control canvas node');
|
|
1201
1120
|
|
|
1202
1121
|
const alternateDevice = devices.find(device =>
|
|
1203
1122
|
device.Connected === true
|
|
@@ -1281,8 +1200,7 @@ try {
|
|
|
1281
1200
|
assert.ok(devices.some(device => /^synthetic-response-/.test(device.LatestTaskResultResponseId)));
|
|
1282
1201
|
await wait(20);
|
|
1283
1202
|
const liveStartCalls = dotNetCalls
|
|
1284
|
-
.filter(call => call.methodName === 'StartRemoteFleetLiveStreamFromJs')
|
|
1285
|
-
.slice(0, liveStartCallsBeforeControl);
|
|
1203
|
+
.filter(call => call.methodName === 'StartRemoteFleetLiveStreamFromJs');
|
|
1286
1204
|
const subscribeMessages = diagnostics.webSocketSends
|
|
1287
1205
|
.map(send => {
|
|
1288
1206
|
try {
|
|
@@ -1368,9 +1286,120 @@ try {
|
|
|
1368
1286
|
child !== errorFooter && (child.textContent || '').includes('RemoteHub is unavailable')), false);
|
|
1369
1287
|
|
|
1370
1288
|
const deviceBody = document.createElement('div');
|
|
1289
|
+
deviceBody.dataset.nodeId = 'remote-device-render-smoke';
|
|
1371
1290
|
document.body.appendChild(deviceBody);
|
|
1291
|
+
const sendCountBeforeControlNode = diagnostics.webSocketSends.length;
|
|
1292
|
+
const drawCountBeforeControlNode = document.canvasDrawCalls.length;
|
|
1372
1293
|
manager.renderRemoteFleetDeviceForTest(deviceBody, buildDeviceNode(focusedDevice, hub.getStatus()));
|
|
1373
1294
|
assert.ok(deviceBody.textContent.includes(focusedDevice.Name));
|
|
1295
|
+
const deviceControlStage = deviceBody.querySelector('[data-remote-fleet-control-node-stage="true"]');
|
|
1296
|
+
const deviceControlCanvas = deviceBody.querySelector('[data-remote-fleet-control-node-canvas="true"]');
|
|
1297
|
+
assert.ok(deviceControlStage);
|
|
1298
|
+
assert.ok(deviceControlCanvas);
|
|
1299
|
+
assert.equal(document.activeElement, deviceControlStage);
|
|
1300
|
+
await wait(30);
|
|
1301
|
+
const deviceControlSubscribe = diagnostics.webSocketSends
|
|
1302
|
+
.slice(sendCountBeforeControlNode)
|
|
1303
|
+
.map(send => {
|
|
1304
|
+
try {
|
|
1305
|
+
return JSON.parse(send.payload);
|
|
1306
|
+
} catch {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
})
|
|
1310
|
+
.find(message =>
|
|
1311
|
+
message
|
|
1312
|
+
&& message.type === 'subscribe'
|
|
1313
|
+
&& message.nodeId === 'remote-device-render-smoke'
|
|
1314
|
+
&& message.autoStartLive === true
|
|
1315
|
+
&& message.fps === 20
|
|
1316
|
+
&& Array.isArray(message.deviceIds)
|
|
1317
|
+
&& message.deviceIds.includes(focusedDevice.DeviceId));
|
|
1318
|
+
assert.ok(deviceControlSubscribe, 'RemoteFleetDevice node should subscribe to 20fps binary frames');
|
|
1319
|
+
const deviceControlWs = diagnostics.webSocketConnections
|
|
1320
|
+
.filter(ws => ws.url.includes('/api/remote/frames/ws'))
|
|
1321
|
+
.at(-1);
|
|
1322
|
+
assert.ok(deviceControlWs, 'RemoteFleetDevice node should open a binary frame WebSocket');
|
|
1323
|
+
const controlNodeFrameSeq = 30001;
|
|
1324
|
+
deviceControlWs.onmessage?.({
|
|
1325
|
+
data: new Blob([encodeRemoteBinaryFrame({
|
|
1326
|
+
type: 'remote.frame.binary',
|
|
1327
|
+
deviceId: focusedDevice.DeviceId,
|
|
1328
|
+
kind: 'live',
|
|
1329
|
+
frameSeq: controlNodeFrameSeq,
|
|
1330
|
+
streamId: 'render-smoke-control-node-live',
|
|
1331
|
+
contentHash: `render-smoke-control-node-live-${controlNodeFrameSeq}`,
|
|
1332
|
+
mimeType: 'image/jpeg',
|
|
1333
|
+
width: 64,
|
|
1334
|
+
height: 36,
|
|
1335
|
+
capturedAt: new Date().toISOString(),
|
|
1336
|
+
receivedAt: new Date().toISOString()
|
|
1337
|
+
})])
|
|
1338
|
+
});
|
|
1339
|
+
await wait(40);
|
|
1340
|
+
assert.ok(
|
|
1341
|
+
document.canvasDrawCalls
|
|
1342
|
+
.slice(drawCountBeforeControlNode)
|
|
1343
|
+
.some(call => call.canvas === deviceControlCanvas && call.op === 'drawImage'),
|
|
1344
|
+
'RemoteFleetDevice node should paint binary WebSocket frames directly to its canvas');
|
|
1345
|
+
deviceControlStage.dispatchEvent({
|
|
1346
|
+
type: 'pointerdown',
|
|
1347
|
+
button: 0,
|
|
1348
|
+
pointerId: 11,
|
|
1349
|
+
clientX: 160,
|
|
1350
|
+
clientY: 90
|
|
1351
|
+
});
|
|
1352
|
+
deviceControlStage.dispatchEvent({
|
|
1353
|
+
type: 'pointerup',
|
|
1354
|
+
button: 0,
|
|
1355
|
+
pointerId: 11,
|
|
1356
|
+
clientX: 160,
|
|
1357
|
+
clientY: 90
|
|
1358
|
+
});
|
|
1359
|
+
deviceControlStage.dispatchEvent({
|
|
1360
|
+
type: 'keydown',
|
|
1361
|
+
key: 'A',
|
|
1362
|
+
code: 'KeyA',
|
|
1363
|
+
repeat: false
|
|
1364
|
+
});
|
|
1365
|
+
deviceControlStage.dispatchEvent({
|
|
1366
|
+
type: 'keyup',
|
|
1367
|
+
key: 'A',
|
|
1368
|
+
code: 'KeyA',
|
|
1369
|
+
repeat: false
|
|
1370
|
+
});
|
|
1371
|
+
await wait(50);
|
|
1372
|
+
const inputWsSends = diagnostics.webSocketSends
|
|
1373
|
+
.filter(send => send.url.includes('/api/remote/input/ws'))
|
|
1374
|
+
.map(send => {
|
|
1375
|
+
try {
|
|
1376
|
+
return JSON.parse(send.payload);
|
|
1377
|
+
} catch {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
})
|
|
1381
|
+
.filter(Boolean);
|
|
1382
|
+
assert.ok(inputWsSends.length >= 3, 'RemoteFleetDevice node should send pointer and keyboard input over the input WebSocket');
|
|
1383
|
+
const pointerDown = inputWsSends.find(message => message.input?.type === 'pointerDown');
|
|
1384
|
+
const keyDown = inputWsSends.find(message => message.input?.type === 'keyDown');
|
|
1385
|
+
const keyUp = inputWsSends.find(message => message.input?.type === 'keyUp');
|
|
1386
|
+
assert.ok(pointerDown);
|
|
1387
|
+
assert.ok(keyDown);
|
|
1388
|
+
assert.ok(keyUp);
|
|
1389
|
+
assert.equal(pointerDown.nodeId, 'remote-device-render-smoke');
|
|
1390
|
+
assert.equal(pointerDown.deviceId, focusedDevice.DeviceId);
|
|
1391
|
+
assert.equal(pointerDown.input.button, 'left');
|
|
1392
|
+
assert.ok(pointerDown.input.controlLeaseId);
|
|
1393
|
+
assert.ok(pointerDown.input.normalizedX >= 0 && pointerDown.input.normalizedX <= 1);
|
|
1394
|
+
assert.ok(pointerDown.input.normalizedY >= 0 && pointerDown.input.normalizedY <= 1);
|
|
1395
|
+
assert.equal(keyDown.input.key, 'A');
|
|
1396
|
+
assert.equal(keyDown.input.code, 'KeyA');
|
|
1397
|
+
assert.equal(keyUp.input.key, 'A');
|
|
1398
|
+
assert.equal(keyUp.input.code, 'KeyA');
|
|
1399
|
+
assert.equal(
|
|
1400
|
+
dotNetCalls.filter(call => call.methodName === 'SendRemoteFleetInputFromJs').length,
|
|
1401
|
+
0,
|
|
1402
|
+
'WebSocket input path should not use the slower DotNet input fallback in the happy path');
|
|
1374
1403
|
assert.ok(deviceBody.querySelector('[data-remote-fleet-action="refresh-device"]'));
|
|
1375
1404
|
assert.ok(deviceBody.querySelector('[data-remote-fleet-action="task-device"]'));
|
|
1376
1405
|
assert.ok(deviceBody.textContent.includes('Task timed out waiting for the agent response.'));
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { WebSocket } from 'ws';
|
|
9
|
+
|
|
10
|
+
const BRIDGE_TOKEN = 'remote-input-ws-smoke-token';
|
|
11
|
+
const PAIR_TOKEN = 'remote-input-ws-pair-token';
|
|
12
|
+
const DEVICE_ID = 'remote-input-ws-device';
|
|
13
|
+
|
|
14
|
+
function wait(ms) {
|
|
15
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function waitFor(predicate, timeoutMs = 10000, label = 'condition') {
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
21
|
+
const value = await predicate();
|
|
22
|
+
if (value) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
await wait(50);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error(`Timed out waiting for ${label}.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function findFreePort() {
|
|
32
|
+
return await new Promise((resolve, reject) => {
|
|
33
|
+
const server = net.createServer();
|
|
34
|
+
server.unref();
|
|
35
|
+
server.once('error', reject);
|
|
36
|
+
server.listen(0, '127.0.0.1', () => {
|
|
37
|
+
const address = server.address();
|
|
38
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
39
|
+
server.close(() => resolve(port));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchJson(url, options = {}) {
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
...options,
|
|
47
|
+
headers: {
|
|
48
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
49
|
+
'X-Bridge-Token': BRIDGE_TOKEN,
|
|
50
|
+
...(options.headers || {})
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const payload = await response.json().catch(() => null);
|
|
54
|
+
return { ok: response.ok, status: response.status, payload };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function waitForBridge(baseUrl, details) {
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
while (Date.now() - startedAt < 30000) {
|
|
60
|
+
try {
|
|
61
|
+
const result = await fetchJson(`${baseUrl}/api/remote/status`);
|
|
62
|
+
if (result.ok && result.payload?.started === true) {
|
|
63
|
+
return result.payload;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Server is still starting.
|
|
67
|
+
}
|
|
68
|
+
await wait(100);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(`Timed out waiting for LocalBridge.\n${details()}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spawnBridge({ bridgePort, remoteHubPort }) {
|
|
75
|
+
const bridgeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
76
|
+
const env = {
|
|
77
|
+
...process.env,
|
|
78
|
+
BRIDGE_PORT: String(bridgePort),
|
|
79
|
+
BRIDGE_TOKEN,
|
|
80
|
+
BRIDGE_REQUIRE_TOKEN: '1',
|
|
81
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
82
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
83
|
+
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
84
|
+
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
85
|
+
NO_COLOR: '1'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
89
|
+
cwd: bridgeRoot,
|
|
90
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
env
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let stdout = '';
|
|
96
|
+
let stderr = '';
|
|
97
|
+
child.stdout.on('data', chunk => {
|
|
98
|
+
stdout += chunk.toString();
|
|
99
|
+
});
|
|
100
|
+
child.stderr.on('data', chunk => {
|
|
101
|
+
stderr += chunk.toString();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const exitPromise = new Promise(resolve => child.once('exit', resolve));
|
|
105
|
+
const stop = async () => {
|
|
106
|
+
if (child.exitCode === null && !child.killed) {
|
|
107
|
+
child.kill('SIGTERM');
|
|
108
|
+
await Promise.race([exitPromise, wait(5000)]);
|
|
109
|
+
if (child.exitCode === null && !child.killed) {
|
|
110
|
+
child.kill('SIGKILL');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
baseUrl: `http://127.0.0.1:${bridgePort}`,
|
|
117
|
+
details: () => `stdout=${stdout}\nstderr=${stderr}`,
|
|
118
|
+
stop
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function openWebSocket(url) {
|
|
123
|
+
const ws = new WebSocket(url);
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
ws.once('open', resolve);
|
|
126
|
+
ws.once('error', reject);
|
|
127
|
+
});
|
|
128
|
+
return ws;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function main() {
|
|
132
|
+
const bridgePort = await findFreePort();
|
|
133
|
+
const remoteHubPort = await findFreePort();
|
|
134
|
+
const bridge = spawnBridge({ bridgePort, remoteHubPort });
|
|
135
|
+
let agentWs = null;
|
|
136
|
+
let inputWs = null;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await waitForBridge(bridge.baseUrl, bridge.details);
|
|
140
|
+
|
|
141
|
+
const receivedCommands = [];
|
|
142
|
+
agentWs = await openWebSocket(`ws://127.0.0.1:${remoteHubPort}/remote-agent`);
|
|
143
|
+
agentWs.on('message', data => {
|
|
144
|
+
const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
|
|
145
|
+
if (!text.trim().startsWith('{')) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const message = JSON.parse(text);
|
|
150
|
+
if (message.command === 'input.control') {
|
|
151
|
+
receivedCommands.push(message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
agentWs.send(JSON.stringify({
|
|
155
|
+
type: 'hello',
|
|
156
|
+
pairToken: PAIR_TOKEN,
|
|
157
|
+
deviceId: DEVICE_ID,
|
|
158
|
+
deviceName: 'Remote Input WS Smoke',
|
|
159
|
+
hostname: 'remote-input-ws-smoke',
|
|
160
|
+
platform: process.platform,
|
|
161
|
+
arch: process.arch,
|
|
162
|
+
pid: process.pid,
|
|
163
|
+
agentVersion: '0.0.0-input-ws-smoke',
|
|
164
|
+
runtime: 'node-smoke',
|
|
165
|
+
capabilities: {
|
|
166
|
+
status: true,
|
|
167
|
+
thumbnail: true,
|
|
168
|
+
liveStream: true,
|
|
169
|
+
binaryFrames: true,
|
|
170
|
+
control: true,
|
|
171
|
+
computerAgent: true,
|
|
172
|
+
taskDispatch: true,
|
|
173
|
+
aiAssist: false
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
176
|
+
agentWs.send(JSON.stringify({
|
|
177
|
+
type: 'status',
|
|
178
|
+
status: {
|
|
179
|
+
uptimeSec: 1,
|
|
180
|
+
totalMem: 2,
|
|
181
|
+
freeMem: 1,
|
|
182
|
+
timestamp: new Date().toISOString()
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
await waitFor(async () => {
|
|
187
|
+
const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
|
|
188
|
+
return result.payload?.devices?.find(device => device.deviceId === DEVICE_ID && device.connected === true);
|
|
189
|
+
}, 10000, 'device registration');
|
|
190
|
+
|
|
191
|
+
inputWs = await openWebSocket(`${bridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/input/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
|
|
192
|
+
inputWs.send(JSON.stringify({
|
|
193
|
+
type: 'input',
|
|
194
|
+
deviceId: DEVICE_ID,
|
|
195
|
+
requestId: 'pointer-move',
|
|
196
|
+
input: {
|
|
197
|
+
type: 'pointerMove',
|
|
198
|
+
normalizedX: 0.42,
|
|
199
|
+
normalizedY: 0.54,
|
|
200
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
inputWs.send(JSON.stringify({
|
|
204
|
+
type: 'input',
|
|
205
|
+
deviceId: DEVICE_ID,
|
|
206
|
+
requestId: 'key-down',
|
|
207
|
+
input: {
|
|
208
|
+
type: 'keyDown',
|
|
209
|
+
key: 'A',
|
|
210
|
+
code: 'KeyA',
|
|
211
|
+
repeat: false,
|
|
212
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
inputWs.send(JSON.stringify({
|
|
216
|
+
type: 'input',
|
|
217
|
+
deviceId: DEVICE_ID,
|
|
218
|
+
requestId: 'key-up',
|
|
219
|
+
input: {
|
|
220
|
+
type: 'keyUp',
|
|
221
|
+
key: 'A',
|
|
222
|
+
code: 'KeyA',
|
|
223
|
+
repeat: false,
|
|
224
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
await waitFor(() => receivedCommands.length >= 3, 10000, 'input.control commands');
|
|
229
|
+
const commandTypes = receivedCommands.map(command => command.payload?.type);
|
|
230
|
+
assert.deepEqual(commandTypes.slice(0, 3), ['pointerMove', 'keyDown', 'keyUp']);
|
|
231
|
+
assert.equal(receivedCommands[0].payload.normalizedX, 0.42);
|
|
232
|
+
assert.equal(receivedCommands[0].payload.normalizedY, 0.54);
|
|
233
|
+
assert.equal(receivedCommands[1].payload.key, 'A');
|
|
234
|
+
assert.equal(receivedCommands[1].payload.code, 'KeyA');
|
|
235
|
+
assert.equal(receivedCommands[2].payload.key, 'A');
|
|
236
|
+
assert.equal(receivedCommands[2].payload.code, 'KeyA');
|
|
237
|
+
|
|
238
|
+
const status = await fetchJson(`${bridge.baseUrl}/api/status?remoteInput=ws`);
|
|
239
|
+
assert.equal(status.ok, true);
|
|
240
|
+
assert.equal(status.payload?.remoteInputWsPath, '/api/remote/input/ws');
|
|
241
|
+
assert.ok(status.payload?.remoteInputWs?.clientCount >= 1, JSON.stringify(status.payload?.remoteInputWs));
|
|
242
|
+
assert.ok(status.payload?.remoteInputWs?.inputsQueued >= 3, JSON.stringify(status.payload?.remoteInputWs));
|
|
243
|
+
assert.ok(status.payload?.remoteInputWs?.pointerMovesQueued >= 1, JSON.stringify(status.payload?.remoteInputWs));
|
|
244
|
+
assert.ok(status.payload?.remoteInputWs?.keyEventsQueued >= 2, JSON.stringify(status.payload?.remoteInputWs));
|
|
245
|
+
assert.equal(status.payload?.remoteInputWs?.lastDeviceId, DEVICE_ID);
|
|
246
|
+
assert.equal(status.payload?.remoteInputWs?.lastInputType, 'keyUp');
|
|
247
|
+
|
|
248
|
+
console.log('Remote input WebSocket smoke OK');
|
|
249
|
+
} finally {
|
|
250
|
+
if (inputWs && inputWs.readyState === WebSocket.OPEN) {
|
|
251
|
+
inputWs.close();
|
|
252
|
+
}
|
|
253
|
+
if (agentWs && agentWs.readyState === WebSocket.OPEN) {
|
|
254
|
+
agentWs.close();
|
|
255
|
+
}
|
|
256
|
+
await bridge.stop();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
main().catch(error => {
|
|
261
|
+
console.error(error);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
});
|