@livedesk/hub 0.1.59 → 0.1.61

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.
@@ -0,0 +1,999 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawn } from 'node:child_process';
3
+ import { once } from 'node:events';
4
+ import net from 'node:net';
5
+ import test from 'node:test';
6
+ import { fileURLToPath } from 'node:url';
7
+ import WebSocket from 'ws';
8
+
9
+ import { createRemoteHub } from './remote-hub.js';
10
+ import {
11
+ READ_ONLY_CONTROL_PRESENTATION_MAX_IDLE_MS,
12
+ READ_ONLY_CONTROL_STOP_RECONCILE_GRACE_MS,
13
+ createReadOnlyControlPresentationReconcileCoordinator,
14
+ resolveReadOnlyControlPresentationBorrow
15
+ } from './live-stream-monitor-contract.js';
16
+
17
+ const NOW_EPOCH_MS = Date.parse('2026-08-29T12:00:00.000Z');
18
+
19
+ function createHealthyDevice() {
20
+ return {
21
+ sessionId: 'session-current',
22
+ activeLiveStream: {
23
+ active: true,
24
+ open: true,
25
+ stopPending: false,
26
+ stopCommandId: '',
27
+ pendingDescriptor: null,
28
+ sessionId: 'session-current',
29
+ streamId: 'control-stream-current',
30
+ commandId: 'control-command-current',
31
+ captureGeneration: 7,
32
+ monitorIndex: 1,
33
+ streamPurpose: 'control',
34
+ mode: 'mode3-h264-hw',
35
+ frameMode: 'mode3-h264-hw',
36
+ readyFrameReceived: true,
37
+ framesReceived: 120,
38
+ lastFrameAt: new Date(NOW_EPOCH_MS - 250).toISOString(),
39
+ fps: 30,
40
+ maxWidth: 1920,
41
+ maxHeight: 1080,
42
+ quality: 72,
43
+ latestFrame: {
44
+ currentGenerationVerified: true,
45
+ sessionId: 'session-current',
46
+ streamId: 'control-stream-current',
47
+ commandId: 'control-command-current',
48
+ captureGeneration: 7,
49
+ monitorIndex: 1,
50
+ streamPurpose: 'control'
51
+ }
52
+ }
53
+ };
54
+ }
55
+
56
+ function createWallOptions(overrides = {}) {
57
+ return {
58
+ allowReadOnlyControlBorrow: true,
59
+ streamPurpose: 'wall',
60
+ frameMode: 'mode3-h264-hw',
61
+ fps: 12,
62
+ maxWidth: 960,
63
+ maxHeight: 540,
64
+ quality: 55,
65
+ ...overrides
66
+ };
67
+ }
68
+
69
+ function resolve(device = createHealthyDevice(), liveOptions = createWallOptions(), monitorIndex = 1) {
70
+ return resolveReadOnlyControlPresentationBorrow({
71
+ device,
72
+ liveOptions,
73
+ monitorIndex,
74
+ nowEpochMs: NOW_EPOCH_MS
75
+ });
76
+ }
77
+
78
+ test('a Wall observer borrows only the exact healthy Control H.264 binding', () => {
79
+ assert.deepEqual(resolve(), {
80
+ ok: true,
81
+ commandId: 'control-command-current',
82
+ sessionId: 'session-current',
83
+ streamId: 'control-stream-current',
84
+ streamPurpose: 'control',
85
+ fps: 30,
86
+ mode: 'mode3-h264-hw',
87
+ frameMode: 'mode3-h264-hw',
88
+ monitorIndex: 1,
89
+ captureGeneration: 7,
90
+ ready: true,
91
+ reused: true,
92
+ readOnlyControlBorrow: true,
93
+ presentationPurpose: 'wall',
94
+ requestedProfile: {
95
+ fps: 12,
96
+ maxWidth: 960,
97
+ maxHeight: 540,
98
+ quality: 55
99
+ },
100
+ effectiveProfile: {
101
+ fps: 30,
102
+ maxWidth: 1920,
103
+ maxHeight: 1080,
104
+ quality: 72
105
+ }
106
+ });
107
+ });
108
+
109
+ test('borrowing requires an explicit Wall observer request', () => {
110
+ assert.equal(resolve(createHealthyDevice(), createWallOptions({
111
+ allowReadOnlyControlBorrow: false
112
+ })), null);
113
+ assert.equal(resolve(createHealthyDevice(), createWallOptions({
114
+ streamPurpose: 'control'
115
+ })), null);
116
+ });
117
+
118
+ test('borrowing rejects a different monitor and an idle Control frame', () => {
119
+ assert.equal(resolve(createHealthyDevice(), createWallOptions(), 0), null);
120
+
121
+ const staleDevice = createHealthyDevice();
122
+ staleDevice.activeLiveStream.lastFrameAt = new Date(
123
+ NOW_EPOCH_MS - READ_ONLY_CONTROL_PRESENTATION_MAX_IDLE_MS - 1
124
+ ).toISOString();
125
+ assert.equal(resolve(staleDevice), null);
126
+ });
127
+
128
+ test('borrowing rejects a stopping or replacement-pending Control stream', () => {
129
+ for (const activePatch of [
130
+ { stopPending: true },
131
+ { stopCommandId: 'stop-command-current' },
132
+ { pendingDescriptor: { commandId: 'replacement-command' } }
133
+ ]) {
134
+ const device = createHealthyDevice();
135
+ Object.assign(device.activeLiveStream, activePatch);
136
+ assert.equal(resolve(device), null);
137
+ }
138
+ });
139
+
140
+ test('borrowing rejects non-Control purpose and non-hardware-H.264 modes', () => {
141
+ const wrongPurpose = createHealthyDevice();
142
+ wrongPurpose.activeLiveStream.streamPurpose = 'wall';
143
+ assert.equal(resolve(wrongPurpose), null);
144
+
145
+ const wrongActiveMode = createHealthyDevice();
146
+ wrongActiveMode.activeLiveStream.frameMode = 'mode2-jpeg';
147
+ wrongActiveMode.activeLiveStream.mode = 'mode2-jpeg';
148
+ assert.equal(resolve(wrongActiveMode), null);
149
+
150
+ assert.equal(resolve(createHealthyDevice(), createWallOptions({
151
+ frameMode: 'mode2-jpeg'
152
+ })), null);
153
+ });
154
+
155
+ test('borrowing rejects a latest frame owned by another exact binding', () => {
156
+ for (const [field, value] of [
157
+ ['sessionId', 'session-old'],
158
+ ['streamId', 'control-stream-old'],
159
+ ['commandId', 'control-command-old'],
160
+ ['captureGeneration', 6],
161
+ ['monitorIndex', 0],
162
+ ['streamPurpose', 'wall']
163
+ ]) {
164
+ const device = createHealthyDevice();
165
+ device.activeLiveStream.latestFrame[field] = value;
166
+ assert.equal(resolve(device), null, `latest frame ${field} mismatch must be rejected`);
167
+ }
168
+ });
169
+
170
+ function createFakeReconcileTimers() {
171
+ const owners = [];
172
+ return {
173
+ owners,
174
+ setTimeoutFn(callback, delayMs) {
175
+ const owner = {
176
+ callback,
177
+ delayMs,
178
+ cleared: false,
179
+ unrefCalls: 0,
180
+ unref() {
181
+ owner.unrefCalls += 1;
182
+ }
183
+ };
184
+ owners.push(owner);
185
+ return owner;
186
+ },
187
+ clearTimeoutFn(owner) {
188
+ owner.cleared = true;
189
+ },
190
+ fire(owner) {
191
+ if (!owner.cleared) owner.callback();
192
+ }
193
+ };
194
+ }
195
+
196
+ test('a replacement Control cancels the one deferred stop reconcile and Ready rebinds immediately', () => {
197
+ const timers = createFakeReconcileTimers();
198
+ const reconciledDeviceIds = [];
199
+ const coordinator = createReadOnlyControlPresentationReconcileCoordinator({
200
+ onReconcile: deviceId => reconciledDeviceIds.push(deviceId),
201
+ setTimeoutFn: timers.setTimeoutFn,
202
+ clearTimeoutFn: timers.clearTimeoutFn
203
+ });
204
+
205
+ assert.equal(coordinator.scheduleAfterConfirmedStop(' mac-current '), true);
206
+ assert.equal(coordinator.scheduleAfterConfirmedStop('mac-current'), false);
207
+ assert.equal(coordinator.getPendingCount(), 1);
208
+ assert.equal(timers.owners.length, 1);
209
+ assert.equal(timers.owners[0].delayMs, READ_ONLY_CONTROL_STOP_RECONCILE_GRACE_MS);
210
+ assert.equal(timers.owners[0].unrefCalls, 1);
211
+
212
+ assert.equal(coordinator.cancelForControlTransition('mac-current'), true);
213
+ assert.equal(coordinator.getPendingCount(), 0);
214
+ assert.equal(timers.owners[0].cleared, true);
215
+ timers.fire(timers.owners[0]);
216
+ assert.deepEqual(reconciledDeviceIds, []);
217
+
218
+ assert.equal(coordinator.scheduleAfterConfirmedStop('mac-current'), true);
219
+ assert.equal(coordinator.reconcileReadyControl('mac-current'), true);
220
+ assert.equal(timers.owners[1].cleared, true);
221
+ assert.equal(coordinator.getPendingCount(), 0);
222
+ assert.deepEqual(reconciledDeviceIds, ['mac-current']);
223
+ timers.fire(timers.owners[1]);
224
+ assert.deepEqual(reconciledDeviceIds, ['mac-current']);
225
+ });
226
+
227
+ test('closing the reconcile coordinator releases every timer and rejects later work', () => {
228
+ const timers = createFakeReconcileTimers();
229
+ const reconciledDeviceIds = [];
230
+ const coordinator = createReadOnlyControlPresentationReconcileCoordinator({
231
+ onReconcile: deviceId => reconciledDeviceIds.push(deviceId),
232
+ setTimeoutFn: timers.setTimeoutFn,
233
+ clearTimeoutFn: timers.clearTimeoutFn
234
+ });
235
+
236
+ coordinator.scheduleAfterConfirmedStop('mac-a');
237
+ coordinator.scheduleAfterConfirmedStop('mac-b');
238
+ assert.equal(coordinator.getPendingCount(), 2);
239
+ coordinator.close();
240
+ assert.equal(coordinator.getPendingCount(), 0);
241
+ assert.deepEqual(timers.owners.map(owner => owner.cleared), [true, true]);
242
+ assert.equal(coordinator.scheduleAfterConfirmedStop('mac-c'), false);
243
+ assert.equal(coordinator.reconcileReadyControl('mac-c'), false);
244
+ for (const owner of timers.owners) timers.fire(owner);
245
+ assert.deepEqual(reconciledDeviceIds, []);
246
+ });
247
+
248
+ test('the authoritative Hub borrows without stream.start and still enforces current policy', async () => {
249
+ let remoteAccessBlocked = false;
250
+ const hub = createRemoteHub({
251
+ env: {
252
+ ...process.env,
253
+ LIVEDESK_REMOTE_HUB: '1',
254
+ REMOTE_HUB_HOST: '127.0.0.1',
255
+ REMOTE_HUB_PORT: '0'
256
+ },
257
+ pairToken: 'control-presentation-borrow-token',
258
+ getEffectiveDevicePolicy: () => remoteAccessBlocked
259
+ ? { accessMode: 'block-remote-access' }
260
+ : {}
261
+ });
262
+ let socket;
263
+ try {
264
+ const status = await hub.start();
265
+ socket = net.createConnection({ host: '127.0.0.1', port: status.port });
266
+ await once(socket, 'connect');
267
+
268
+ const messages = [];
269
+ let incoming = '';
270
+ socket.on('data', chunk => {
271
+ incoming += chunk.toString('utf8');
272
+ let newline = incoming.indexOf('\n');
273
+ while (newline >= 0) {
274
+ const line = incoming.slice(0, newline).trim();
275
+ incoming = incoming.slice(newline + 1);
276
+ if (line) messages.push(JSON.parse(line));
277
+ newline = incoming.indexOf('\n');
278
+ }
279
+ });
280
+ const waitUntil = async predicate => {
281
+ const deadline = Date.now() + 2_000;
282
+ while (Date.now() < deadline) {
283
+ if (predicate()) return;
284
+ await new Promise(resolveWait => setTimeout(resolveWait, 10));
285
+ }
286
+ throw new Error('timed out waiting for authoritative Control borrow fixture');
287
+ };
288
+ const send = message => socket.write(`${JSON.stringify(message)}\n`);
289
+
290
+ send({
291
+ type: 'hello',
292
+ pairToken: 'control-presentation-borrow-token',
293
+ deviceId: 'control-presentation-device',
294
+ deviceName: 'Control presentation fixture',
295
+ hostname: 'control-presentation-fixture',
296
+ platform: 'darwin',
297
+ arch: 'arm64',
298
+ protocol: 'mindexec.remote.agent',
299
+ protocolVersion: 2,
300
+ capabilities: { liveStream: true, frameProtocol: {}, frameModes: [] }
301
+ });
302
+ await waitUntil(() => messages.some(message => message.type === 'welcome'));
303
+
304
+ const controlOptions = {
305
+ streamId: 'control-control-presentation-device',
306
+ streamPurpose: 'control',
307
+ mode: 'mode3-h264-hw',
308
+ frameMode: 'mode3-h264-hw',
309
+ fps: 30,
310
+ maxWidth: 1920,
311
+ maxHeight: 1080,
312
+ quality: 72,
313
+ monitorIndex: 0
314
+ };
315
+ const control = hub.startLiveStream('control-presentation-device', {
316
+ ...controlOptions,
317
+ commandId: 'control-presentation-command'
318
+ });
319
+ assert.equal(control.ok, true);
320
+ await waitUntil(() => messages.some(message => (
321
+ message.type === 'command'
322
+ && message.command === 'stream.start'
323
+ && message.commandId === control.commandId
324
+ )));
325
+
326
+ send({
327
+ type: 'stream.open',
328
+ streamId: control.streamId,
329
+ commandId: control.commandId,
330
+ captureGeneration: control.captureGeneration,
331
+ monitorIndex: 0,
332
+ streamPurpose: 'control',
333
+ mode: 'mode3-h264-hw',
334
+ frameMode: 'mode3-h264-hw',
335
+ codec: 'h264',
336
+ width: 1920,
337
+ height: 1080
338
+ });
339
+ send({
340
+ type: 'stream.frame',
341
+ streamId: control.streamId,
342
+ commandId: control.commandId,
343
+ captureGeneration: control.captureGeneration,
344
+ monitorIndex: 0,
345
+ streamPurpose: 'control',
346
+ frameSeq: 1,
347
+ frameMode: 'mode3-h264-hw',
348
+ codec: 'h264',
349
+ mimeType: 'video/h264',
350
+ isKeyFrame: true,
351
+ chunkType: 'key',
352
+ width: 1920,
353
+ height: 1080,
354
+ data: Buffer.from([
355
+ 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1f,
356
+ 0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x06, 0xe2,
357
+ 0x00, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84
358
+ ]).toString('base64')
359
+ });
360
+ await waitUntil(() => hub.getDeviceLiveFrame('control-presentation-device')?.currentGenerationVerified === true);
361
+
362
+ const streamStartCount = messages.filter(message => message.command === 'stream.start').length;
363
+ const wallRequest = {
364
+ streamPurpose: 'wall',
365
+ mode: 'mode3-h264-hw',
366
+ frameMode: 'mode3-h264-hw',
367
+ fps: 5,
368
+ maxWidth: 960,
369
+ maxHeight: 540,
370
+ quality: 55,
371
+ monitorIndex: 0
372
+ };
373
+ const borrowed = hub.startLiveStream('control-presentation-device', {
374
+ ...wallRequest,
375
+ allowReadOnlyControlBorrow: true,
376
+ reuseExisting: true
377
+ });
378
+ assert.equal(borrowed.ok, true);
379
+ assert.equal(borrowed.readOnlyControlBorrow, true);
380
+ assert.equal(borrowed.presentationPurpose, 'wall');
381
+ assert.equal(borrowed.streamPurpose, 'control');
382
+ assert.equal(borrowed.streamId, control.streamId);
383
+ assert.equal(borrowed.commandId, control.commandId);
384
+ assert.equal(borrowed.captureGeneration, control.captureGeneration);
385
+ assert.deepEqual(borrowed.effectiveProfile, {
386
+ fps: 30,
387
+ maxWidth: 1920,
388
+ maxHeight: 1080,
389
+ quality: 72
390
+ });
391
+ await new Promise(resolveWait => setTimeout(resolveWait, 20));
392
+ assert.equal(messages.filter(message => message.command === 'stream.start').length, streamStartCount);
393
+
394
+ const unflagged = hub.startLiveStream('control-presentation-device', wallRequest);
395
+ assert.equal(unflagged.ok, false);
396
+ assert.equal(unflagged.error, 'CONTROL_CAPTURE_OWNS_DEVICE');
397
+
398
+ remoteAccessBlocked = true;
399
+ const blocked = hub.startLiveStream('control-presentation-device', {
400
+ ...wallRequest,
401
+ allowReadOnlyControlBorrow: true
402
+ });
403
+ assert.equal(blocked.ok, false);
404
+ assert.equal(blocked.error, 'remote-access-blocked-by-settings');
405
+ assert.equal(messages.filter(message => message.command === 'stream.start').length, streamStartCount);
406
+ } finally {
407
+ socket?.destroy();
408
+ await hub.close();
409
+ }
410
+ });
411
+
412
+ async function reserveBorrowLifecyclePort(excluded = new Set()) {
413
+ for (let attempt = 0; attempt < 8; attempt += 1) {
414
+ const server = net.createServer();
415
+ await new Promise((resolveListen, rejectListen) => {
416
+ server.once('error', rejectListen);
417
+ server.listen(0, '127.0.0.1', resolveListen);
418
+ });
419
+ const port = Number(server.address()?.port || 0);
420
+ await new Promise(resolveClose => server.close(resolveClose));
421
+ if (port > 0 && !excluded.has(port)) return port;
422
+ }
423
+ throw new Error('Could not reserve an isolated Control borrow lifecycle port.');
424
+ }
425
+
426
+ async function waitForBorrowLifecycle(read, label, timeoutMs = 4_000, intervalMs = 10) {
427
+ const deadline = Date.now() + timeoutMs;
428
+ let lastError = null;
429
+ while (Date.now() < deadline) {
430
+ try {
431
+ const value = await read();
432
+ if (value) return value;
433
+ } catch (error) {
434
+ lastError = error;
435
+ }
436
+ await new Promise(resolveWait => setTimeout(resolveWait, intervalMs));
437
+ }
438
+ throw new Error(
439
+ `Timed out waiting for ${label}.`
440
+ + (lastError ? ` Last error: ${lastError.message}` : '')
441
+ );
442
+ }
443
+
444
+ function collectBorrowLifecycleProcessOutput(child, limit = 100) {
445
+ const lines = [];
446
+ const collect = chunk => {
447
+ lines.push(...String(chunk || '').split(/\r?\n/).filter(Boolean));
448
+ while (lines.length > limit) lines.shift();
449
+ };
450
+ child.stdout?.on('data', collect);
451
+ child.stderr?.on('data', collect);
452
+ return lines;
453
+ }
454
+
455
+ async function terminateBorrowLifecycleProcess(child) {
456
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
457
+ child.kill();
458
+ const exited = await Promise.race([
459
+ once(child, 'exit').then(() => true),
460
+ new Promise(resolveWait => setTimeout(() => resolveWait(false), 3_000))
461
+ ]);
462
+ if (!exited && child.exitCode === null && child.signalCode === null) {
463
+ child.kill('SIGKILL');
464
+ await Promise.race([
465
+ once(child, 'exit'),
466
+ new Promise(resolveWait => setTimeout(resolveWait, 1_000))
467
+ ]);
468
+ }
469
+ }
470
+
471
+ function parseBorrowLifecycleFramePacket(data) {
472
+ if (!Buffer.isBuffer(data) || data.length < 5) return null;
473
+ const metadataLength = data.readUInt32BE(0);
474
+ if (metadataLength <= 0 || metadataLength > data.length - 4) return null;
475
+ try {
476
+ return JSON.parse(data.subarray(4, 4 + metadataLength).toString('utf8'));
477
+ } catch {
478
+ return null;
479
+ }
480
+ }
481
+
482
+ test('the browser Wall borrower follows exact Control key frames without owning native capture', {
483
+ timeout: 20_000
484
+ }, async () => {
485
+ const {
486
+ READ_ONLY_CONTROL_STOP_RECONCILE_GRACE_MS
487
+ } = await import('./live-stream-monitor-contract.js');
488
+ const usedPorts = new Set();
489
+ const httpPort = await reserveBorrowLifecyclePort(usedPorts);
490
+ usedPorts.add(httpPort);
491
+ const remotePort = await reserveBorrowLifecyclePort(usedPorts);
492
+ const pairToken = `control-borrow-ws-${process.pid}-${Date.now().toString(36)}`;
493
+ const deviceId = `control-borrow-ws-device-${process.pid}`;
494
+ const baseUrl = `http://127.0.0.1:${httpPort}`;
495
+ const hubServerPath = fileURLToPath(new URL('./server.js', import.meta.url));
496
+ const rootDir = fileURLToPath(new URL('../../../', import.meta.url));
497
+ let hubProcess = null;
498
+ let agentSocket = null;
499
+ const browserSockets = [];
500
+ const agentMessages = [];
501
+ const stopAcknowledgedAtByCommandId = new Map();
502
+ let hubOutput = [];
503
+
504
+ const postJson = async (path, body) => {
505
+ const response = await fetch(`${baseUrl}${path}`, {
506
+ method: 'POST',
507
+ headers: { 'Content-Type': 'application/json' },
508
+ body: JSON.stringify(body)
509
+ });
510
+ const payload = await response.json();
511
+ assert.equal(response.ok, true, `${path} HTTP request failed: ${JSON.stringify(payload)}`);
512
+ return payload;
513
+ };
514
+
515
+ const sendAgent = payload => {
516
+ assert.equal(agentSocket?.destroyed, false, 'The fake macOS Agent socket must still be open.');
517
+ agentSocket.write(`${JSON.stringify(payload)}\n`);
518
+ };
519
+
520
+ const waitForAgentCommand = (predicate, label, fromIndex = 0) => waitForBorrowLifecycle(
521
+ () => agentMessages.slice(fromIndex).find(record => (
522
+ record.message?.type === 'command' && predicate(record.message)
523
+ )),
524
+ label
525
+ );
526
+
527
+ const sendOpen = binding => sendAgent({
528
+ type: 'stream.open',
529
+ streamId: binding.streamId,
530
+ commandId: binding.commandId,
531
+ captureGeneration: binding.captureGeneration,
532
+ monitorIndex: 0,
533
+ streamPurpose: 'control',
534
+ mode: 'mode3-h264-hw',
535
+ frameMode: 'mode3-h264-hw',
536
+ codec: 'h264',
537
+ width: 1920,
538
+ height: 1080
539
+ });
540
+
541
+ const sendControlFrame = (binding, frameSeq, isKeyFrame) => sendAgent({
542
+ type: 'stream.frame',
543
+ streamId: binding.streamId,
544
+ commandId: binding.commandId,
545
+ captureGeneration: binding.captureGeneration,
546
+ monitorIndex: 0,
547
+ streamPurpose: 'control',
548
+ frameSeq,
549
+ frameMode: 'mode3-h264-hw',
550
+ codec: 'h264',
551
+ mimeType: 'video/h264',
552
+ isKeyFrame,
553
+ chunkType: isKeyFrame ? 'key' : 'delta',
554
+ width: 1920,
555
+ height: 1080,
556
+ data: Buffer.from(isKeyFrame
557
+ ? [
558
+ 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1f,
559
+ 0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x06, 0xe2,
560
+ 0x00, 0x00, 0x00, 0x01, 0x65, frameSeq & 0xff, 0x84
561
+ ]
562
+ : [0x00, 0x00, 0x00, 0x01, 0x41, frameSeq & 0xff, 0x11]
563
+ ).toString('base64')
564
+ });
565
+
566
+ const connectBorrower = async label => {
567
+ const socket = new WebSocket(
568
+ `ws://127.0.0.1:${httpPort}/api/remote/frames/ws?devices=${encodeURIComponent(deviceId)}`,
569
+ { headers: { Origin: baseUrl }, perMessageDeflate: false }
570
+ );
571
+ const observed = { json: [], frames: [] };
572
+ socket.on('message', (data, isBinary) => {
573
+ if (isBinary) {
574
+ const metadata = parseBorrowLifecycleFramePacket(data);
575
+ if (metadata) observed.frames.push(metadata);
576
+ return;
577
+ }
578
+ try {
579
+ observed.json.push(JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data)));
580
+ } catch {
581
+ // This isolated contract records only valid Hub JSON control messages.
582
+ }
583
+ });
584
+ browserSockets.push(socket);
585
+ await Promise.race([
586
+ once(socket, 'open'),
587
+ once(socket, 'error').then(([error]) => Promise.reject(error))
588
+ ]);
589
+ await waitForBorrowLifecycle(
590
+ () => observed.json.find(message => message.type === 'RemoteFrameSocketReady'),
591
+ `${label} frame socket ready`
592
+ );
593
+ const subscribeJsonIndex = observed.json.length;
594
+ socket.send(JSON.stringify({
595
+ type: 'subscribe',
596
+ deviceIds: [deviceId],
597
+ autoStartLive: true,
598
+ allowReadOnlyControlBorrow: true,
599
+ streamPurpose: 'wall',
600
+ mode: 'mode3-h264-hw',
601
+ frameMode: 'mode3-h264-hw',
602
+ fps: 5,
603
+ maxWidth: 960,
604
+ maxHeight: 540,
605
+ quality: 55,
606
+ monitorIndex: 0
607
+ }));
608
+ return { socket, observed, subscribeJsonIndex };
609
+ };
610
+
611
+ const waitForBorrowStart = (observed, commandId, fromIndex, label) => waitForBorrowLifecycle(
612
+ () => observed.json.slice(fromIndex).find(message => (
613
+ message.type === 'RemoteFrameLiveAutoStart'
614
+ && message.started?.some(item => (
615
+ item.deviceId === deviceId
616
+ && item.commandId === commandId
617
+ && item.streamPurpose === 'control'
618
+ && item.readOnlyControlBorrow === true
619
+ && item.presentationPurpose === 'wall'
620
+ ))
621
+ )),
622
+ label
623
+ );
624
+
625
+ const waitForExactReady = (observed, binding, fromIndex, label) => waitForBorrowLifecycle(
626
+ () => observed.json.slice(fromIndex).find(message => (
627
+ message.type === 'RemoteFrameStreamReady'
628
+ && message.deviceId === deviceId
629
+ && message.sessionId === binding.sessionId
630
+ && message.streamId === binding.streamId
631
+ && message.commandId === binding.commandId
632
+ && message.captureGeneration === binding.captureGeneration
633
+ && message.streamPurpose === 'control'
634
+ && message.monitorIndex === 0
635
+ && message.readOnlyControlBorrow === true
636
+ && message.presentationPurpose === 'wall'
637
+ )),
638
+ label
639
+ );
640
+
641
+ try {
642
+ hubProcess = spawn(process.execPath, [hubServerPath], {
643
+ cwd: rootDir,
644
+ env: {
645
+ ...process.env,
646
+ LIVEDESK_HUB_HTTP_HOST: '127.0.0.1',
647
+ LIVEDESK_HUB_HTTP_PORT: String(httpPort),
648
+ REMOTE_HUB_HOST: '127.0.0.1',
649
+ REMOTE_HUB_PORT: String(remotePort),
650
+ REMOTE_HUB_PAIR_TOKEN: pairToken,
651
+ REMOTE_HUB_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS: '750',
652
+ LIVEDESK_FRAME_STREAM_STOP_GRACE_MS: '25',
653
+ LIVEDESK_PUBLIC_IP_DISCOVERY_DISABLED: '1',
654
+ REMOTE_HUB_PUBLIC_IP_DISCOVERY_DISABLED: '1',
655
+ LIVEDESK_TEST_MODE: '1',
656
+ LIVEDESK_TEST_LICENSE_PLAN: 'pro',
657
+ LIVEDESK_UDP_ENABLED: '0'
658
+ },
659
+ stdio: ['ignore', 'pipe', 'pipe'],
660
+ windowsHide: true
661
+ });
662
+ hubOutput = collectBorrowLifecycleProcessOutput(hubProcess);
663
+ await waitForBorrowLifecycle(async () => {
664
+ if (hubProcess.exitCode !== null) {
665
+ throw new Error(`isolated Hub exited ${hubProcess.exitCode}: ${hubOutput.join('\n')}`);
666
+ }
667
+ const response = await fetch(`${baseUrl}/api/health`, { cache: 'no-store' });
668
+ return response.ok;
669
+ }, 'isolated Hub health', 10_000, 50);
670
+
671
+ agentSocket = net.createConnection({ host: '127.0.0.1', port: remotePort });
672
+ await once(agentSocket, 'connect');
673
+ let incoming = '';
674
+ agentSocket.on('data', chunk => {
675
+ incoming += chunk.toString('utf8');
676
+ let newlineIndex = incoming.indexOf('\n');
677
+ while (newlineIndex >= 0) {
678
+ const line = incoming.slice(0, newlineIndex).trim();
679
+ incoming = incoming.slice(newlineIndex + 1);
680
+ newlineIndex = incoming.indexOf('\n');
681
+ if (!line) continue;
682
+ const message = JSON.parse(line);
683
+ const record = { message, at: Date.now() };
684
+ agentMessages.push(record);
685
+ if (message.type === 'command' && message.command === 'stream.stop') {
686
+ setTimeout(() => {
687
+ if (agentSocket?.destroyed) return;
688
+ stopAcknowledgedAtByCommandId.set(message.commandId, Date.now());
689
+ sendAgent({
690
+ type: 'command.result',
691
+ commandId: message.commandId,
692
+ result: {
693
+ stream: true,
694
+ stopped: true,
695
+ streamId: String(message.payload?.streamId || '')
696
+ }
697
+ });
698
+ }, 5);
699
+ }
700
+ }
701
+ });
702
+ sendAgent({
703
+ type: 'hello',
704
+ pairToken,
705
+ deviceId,
706
+ deviceName: 'Control borrow browser fixture',
707
+ hostname: 'control-borrow-browser-fixture',
708
+ platform: 'darwin',
709
+ arch: 'arm64',
710
+ protocol: 'mindexec.remote.agent',
711
+ protocolVersion: 2,
712
+ capabilities: {
713
+ liveStream: true,
714
+ liveCaptureConcurrency: 'exclusive',
715
+ frameProtocol: {},
716
+ frameModes: []
717
+ }
718
+ });
719
+ await waitForBorrowLifecycle(
720
+ () => agentMessages.find(record => record.message?.type === 'welcome'),
721
+ 'fake macOS Agent welcome'
722
+ );
723
+ await waitForBorrowLifecycle(async () => {
724
+ const response = await fetch(`${baseUrl}/api/remote/devices`, { cache: 'no-store' });
725
+ const payload = await response.json();
726
+ return payload.devices?.find(device => device.deviceId === deviceId && device.connected);
727
+ }, 'fake macOS Agent registration');
728
+
729
+ const controlOptions = {
730
+ streamPurpose: 'control',
731
+ mode: 'mode3-h264-hw',
732
+ frameMode: 'mode3-h264-hw',
733
+ fps: 30,
734
+ maxWidth: 1920,
735
+ maxHeight: 1080,
736
+ quality: 72,
737
+ monitorIndex: 0
738
+ };
739
+ const initialControl = await postJson(
740
+ `/api/remote/devices/${encodeURIComponent(deviceId)}/live/start`,
741
+ controlOptions
742
+ );
743
+ assert.equal(initialControl.ok, true);
744
+ const initialStartCommand = await waitForAgentCommand(
745
+ message => message.command === 'stream.start' && message.commandId === initialControl.commandId,
746
+ 'initial Control start command'
747
+ );
748
+ assert.equal(initialStartCommand.message.payload.streamPurpose, 'control');
749
+ sendOpen(initialControl);
750
+ sendControlFrame(initialControl, 1, true);
751
+ await waitForBorrowLifecycle(async () => {
752
+ const response = await fetch(`${baseUrl}/api/remote/devices`, { cache: 'no-store' });
753
+ const payload = await response.json();
754
+ const device = payload.devices?.find(item => item.deviceId === deviceId);
755
+ return device?.activeLiveStream?.commandId === initialControl.commandId
756
+ && device.activeLiveStream.readyFrameReceived === true;
757
+ }, 'initial exact Control readiness');
758
+
759
+ const firstBorrower = await connectBorrower('first borrower');
760
+ const firstBorrowStart = await waitForBorrowStart(
761
+ firstBorrower.observed,
762
+ initialControl.commandId,
763
+ firstBorrower.subscribeJsonIndex,
764
+ 'first read-only Control binding'
765
+ );
766
+ const firstBorrowBinding = firstBorrowStart.started.find(item => item.commandId === initialControl.commandId);
767
+ assert.equal(firstBorrowBinding.ready, false, 'A new borrower must wait for its own exact key frame.');
768
+ assert.equal(firstBorrowBinding.captureGeneration, initialControl.captureGeneration);
769
+ assert.equal(firstBorrowBinding.effectiveProfile?.fps, 30);
770
+ const firstReadyIndex = firstBorrower.observed.json.length;
771
+ const firstFrameIndex = firstBorrower.observed.frames.length;
772
+ sendControlFrame(initialControl, 2, false);
773
+ await new Promise(resolveWait => setTimeout(resolveWait, 60));
774
+ assert.equal(
775
+ firstBorrower.observed.json.slice(firstReadyIndex).some(message => message.type === 'RemoteFrameStreamReady'),
776
+ false,
777
+ 'A dependent delta must not declare a new borrower ready.'
778
+ );
779
+ assert.equal(
780
+ firstBorrower.observed.frames.length,
781
+ firstFrameIndex,
782
+ 'A dependent delta must not cross the new borrower frame lane before a key frame.'
783
+ );
784
+ sendControlFrame(initialControl, 3, true);
785
+ await waitForExactReady(
786
+ firstBorrower.observed,
787
+ initialControl,
788
+ firstReadyIndex,
789
+ 'first borrower exact Control key readiness'
790
+ );
791
+ const firstFrame = await waitForBorrowLifecycle(
792
+ () => firstBorrower.observed.frames.slice(firstFrameIndex).find(frame => (
793
+ frame.commandId === initialControl.commandId
794
+ && frame.captureGeneration === initialControl.captureGeneration
795
+ && frame.frameSeq === 3
796
+ )),
797
+ 'first borrower exact Control key packet'
798
+ );
799
+ assert.equal(firstFrame.streamPurpose, 'control');
800
+ assert.equal(firstFrame.monitorIndex, 0);
801
+
802
+ const firstStopCommandIndex = agentMessages.length;
803
+ const stopInitialPromise = postJson(
804
+ `/api/remote/devices/${encodeURIComponent(deviceId)}/live/stop`,
805
+ {
806
+ streamId: initialControl.streamId,
807
+ streamPurpose: 'control',
808
+ reason: 'replace-control-generation'
809
+ }
810
+ );
811
+ const firstStopCommand = await waitForAgentCommand(
812
+ message => message.command === 'stream.stop' && message.payload?.streamId === initialControl.streamId,
813
+ 'initial Control confirmed stop',
814
+ firstStopCommandIndex
815
+ );
816
+ const initialStop = await stopInitialPromise;
817
+ assert.equal(initialStop.captureStopConfirmed, true);
818
+ assert.ok(stopAcknowledgedAtByCommandId.has(firstStopCommand.message.commandId));
819
+
820
+ const replacementControl = await postJson(
821
+ `/api/remote/devices/${encodeURIComponent(deviceId)}/live/start`,
822
+ controlOptions
823
+ );
824
+ assert.equal(replacementControl.ok, true);
825
+ assert.ok(replacementControl.captureGeneration > initialControl.captureGeneration);
826
+ const replacementStartCommand = await waitForAgentCommand(
827
+ message => message.command === 'stream.start' && message.commandId === replacementControl.commandId,
828
+ 'replacement Control start command',
829
+ firstStopCommandIndex
830
+ );
831
+ assert.equal(replacementStartCommand.message.payload.streamPurpose, 'control');
832
+ const replacementJsonIndex = firstBorrower.observed.json.length;
833
+ await new Promise(resolveWait => setTimeout(
834
+ resolveWait,
835
+ READ_ONLY_CONTROL_STOP_RECONCILE_GRACE_MS + 40
836
+ ));
837
+ assert.equal(
838
+ agentMessages.slice(firstStopCommandIndex).some(record => (
839
+ record.message?.type === 'command'
840
+ && record.message.command === 'stream.start'
841
+ && record.message.payload?.streamPurpose === 'wall'
842
+ )),
843
+ false,
844
+ 'The stopped-Control grace must not let Wall steal a prompt replacement Control claim.'
845
+ );
846
+ assert.equal(
847
+ firstBorrower.observed.json.slice(replacementJsonIndex).some(message => (
848
+ message.type === 'RemoteFrameLiveAutoStart'
849
+ && message.started?.some(item => item.commandId === replacementControl.commandId)
850
+ )),
851
+ false,
852
+ 'A borrower must not rebind to a replacement Control before that generation is ready.'
853
+ );
854
+
855
+ const replacementFrameIndex = firstBorrower.observed.frames.length;
856
+ sendOpen(replacementControl);
857
+ sendControlFrame(replacementControl, 1, true);
858
+ const replacementBorrowStart = await waitForBorrowStart(
859
+ firstBorrower.observed,
860
+ replacementControl.commandId,
861
+ replacementJsonIndex,
862
+ 'replacement Control borrower reconciliation'
863
+ );
864
+ assert.equal(replacementBorrowStart.reason, 'control-borrow-reconcile');
865
+ assert.equal(
866
+ replacementBorrowStart.started.find(item => item.commandId === replacementControl.commandId)?.ready,
867
+ false
868
+ );
869
+ assert.equal(
870
+ firstBorrower.observed.frames.slice(replacementFrameIndex).some(frame => (
871
+ frame.commandId === replacementControl.commandId
872
+ )),
873
+ false,
874
+ 'The key that precedes replacement binding installation must not leak into the new lane.'
875
+ );
876
+ const replacementReadyIndex = firstBorrower.observed.json.length;
877
+ sendControlFrame(replacementControl, 2, false);
878
+ await new Promise(resolveWait => setTimeout(resolveWait, 60));
879
+ assert.equal(firstBorrower.observed.frames.length, replacementFrameIndex);
880
+ sendControlFrame(replacementControl, 3, true);
881
+ await waitForExactReady(
882
+ firstBorrower.observed,
883
+ replacementControl,
884
+ replacementReadyIndex,
885
+ 'replacement borrower exact Control key readiness'
886
+ );
887
+ await waitForBorrowLifecycle(
888
+ () => firstBorrower.observed.frames.slice(replacementFrameIndex).find(frame => (
889
+ frame.commandId === replacementControl.commandId
890
+ && frame.captureGeneration === replacementControl.captureGeneration
891
+ && frame.frameSeq === 3
892
+ )),
893
+ 'replacement borrower exact Control key packet'
894
+ );
895
+ assert.equal(
896
+ firstBorrower.observed.frames.slice(replacementFrameIndex).some(frame => (
897
+ frame.commandId === initialControl.commandId
898
+ || frame.captureGeneration === initialControl.captureGeneration
899
+ )),
900
+ false,
901
+ 'A replacement binding must reject every stale Control generation.'
902
+ );
903
+
904
+ const stopCountBeforeBorrowerClose = agentMessages.filter(record => (
905
+ record.message?.type === 'command' && record.message.command === 'stream.stop'
906
+ )).length;
907
+ const firstBorrowerClosed = once(firstBorrower.socket, 'close');
908
+ firstBorrower.socket.close(1000, 'borrower-release-regression');
909
+ await firstBorrowerClosed;
910
+ await new Promise(resolveWait => setTimeout(resolveWait, 90));
911
+ assert.equal(
912
+ agentMessages.filter(record => (
913
+ record.message?.type === 'command' && record.message.command === 'stream.stop'
914
+ )).length,
915
+ stopCountBeforeBorrowerClose,
916
+ 'Closing a read-only borrower must never stop the native Control owner.'
917
+ );
918
+
919
+ const fallbackBorrower = await connectBorrower('fallback borrower');
920
+ await waitForBorrowStart(
921
+ fallbackBorrower.observed,
922
+ replacementControl.commandId,
923
+ fallbackBorrower.subscribeJsonIndex,
924
+ 'fallback borrower Control binding'
925
+ );
926
+ const fallbackReadyIndex = fallbackBorrower.observed.json.length;
927
+ sendControlFrame(replacementControl, 4, true);
928
+ await waitForExactReady(
929
+ fallbackBorrower.observed,
930
+ replacementControl,
931
+ fallbackReadyIndex,
932
+ 'fallback borrower exact Control key readiness'
933
+ );
934
+
935
+ const finalStopCommandIndex = agentMessages.length;
936
+ const stopReplacementPromise = postJson(
937
+ `/api/remote/devices/${encodeURIComponent(deviceId)}/live/stop`,
938
+ {
939
+ streamId: replacementControl.streamId,
940
+ streamPurpose: 'control',
941
+ reason: 'confirmed-control-stop-wall-fallback'
942
+ }
943
+ );
944
+ const finalStopCommand = await waitForAgentCommand(
945
+ message => message.command === 'stream.stop' && message.payload?.streamId === replacementControl.streamId,
946
+ 'replacement Control confirmed stop',
947
+ finalStopCommandIndex
948
+ );
949
+ const replacementStop = await stopReplacementPromise;
950
+ assert.equal(replacementStop.captureStopConfirmed, true);
951
+ const finalStopAcknowledgedAt = stopAcknowledgedAtByCommandId.get(finalStopCommand.message.commandId);
952
+ assert.ok(finalStopAcknowledgedAt > 0);
953
+ assert.equal(
954
+ agentMessages.slice(finalStopCommandIndex).some(record => (
955
+ record.message?.type === 'command'
956
+ && record.message.command === 'stream.start'
957
+ && record.message.payload?.streamPurpose === 'wall'
958
+ )),
959
+ false,
960
+ 'Confirmed Control stop must not reacquire Wall synchronously.'
961
+ );
962
+
963
+ const fallbackWallCommand = await waitForAgentCommand(
964
+ message => message.command === 'stream.start' && message.payload?.streamPurpose === 'wall',
965
+ 'deferred ordinary Wall fallback',
966
+ finalStopCommandIndex
967
+ );
968
+ assert.ok(
969
+ fallbackWallCommand.at - finalStopAcknowledgedAt
970
+ >= READ_ONLY_CONTROL_STOP_RECONCILE_GRACE_MS - 40,
971
+ `Wall fallback started before the ${READ_ONLY_CONTROL_STOP_RECONCILE_GRACE_MS}ms Control replacement grace.`
972
+ );
973
+ const fallbackWallStart = await waitForBorrowLifecycle(
974
+ () => fallbackBorrower.observed.json.find(message => (
975
+ message.type === 'RemoteFrameLiveAutoStart'
976
+ && message.reason === 'control-borrow-reconcile'
977
+ && message.started?.some(item => (
978
+ item.deviceId === deviceId
979
+ && item.commandId === fallbackWallCommand.message.commandId
980
+ && item.streamPurpose === 'wall'
981
+ && item.readOnlyControlBorrow !== true
982
+ ))
983
+ )),
984
+ 'ordinary Wall browser binding after confirmed Control stop'
985
+ );
986
+ assert.equal(fallbackWallStart.started[0].presentationPurpose, '');
987
+ } catch (error) {
988
+ error.message += hubOutput.length > 0
989
+ ? `\nIsolated Hub output:\n${hubOutput.join('\n')}`
990
+ : '';
991
+ throw error;
992
+ } finally {
993
+ for (const socket of browserSockets) {
994
+ try { socket.terminate(); } catch { /* already closed */ }
995
+ }
996
+ agentSocket?.destroy();
997
+ await terminateBorrowLifecycleProcess(hubProcess);
998
+ }
999
+ });