@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.152",
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:agent-ws": "node scripts/remote-agent-ws-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: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
+ });