@wenrwa/marketplace-cli 0.4.4 → 0.4.6

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.
Files changed (116) hide show
  1. package/dist/__tests__/bridge-activity-parser.test.d.ts +1 -0
  2. package/dist/__tests__/bridge-activity-parser.test.js +0 -0
  3. package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
  4. package/dist/__tests__/bridge-activity-tap.test.js +178 -0
  5. package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
  6. package/dist/__tests__/bridge-api-client.test.js +69 -0
  7. package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
  8. package/dist/__tests__/bridge-browser-driver.test.js +379 -0
  9. package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
  10. package/dist/__tests__/bridge-browser-frames.test.js +166 -0
  11. package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
  12. package/dist/__tests__/bridge-browser-idle.test.js +259 -0
  13. package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
  14. package/dist/__tests__/bridge-browser-relay.test.js +670 -0
  15. package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
  16. package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
  17. package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
  18. package/dist/__tests__/bridge-cdp-client.test.js +133 -0
  19. package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
  20. package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
  21. package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
  22. package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
  23. package/dist/__tests__/bridge-config-mcp.test.js +4 -0
  24. package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
  25. package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
  26. package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
  27. package/dist/__tests__/bridge-context-writer.test.js +161 -0
  28. package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
  29. package/dist/__tests__/bridge-device-registration.test.js +33 -0
  30. package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
  31. package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
  32. package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
  33. package/dist/__tests__/bridge-folder-picker.test.js +66 -0
  34. package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
  35. package/dist/__tests__/bridge-folder-registry.test.js +107 -0
  36. package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
  37. package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
  38. package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
  39. package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
  40. package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
  41. package/dist/__tests__/bridge-local-add-core.test.js +48 -0
  42. package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
  43. package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
  44. package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
  45. package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
  46. package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
  47. package/dist/__tests__/bridge-login-status.test.js +60 -0
  48. package/dist/__tests__/bridge-relay-client.test.d.ts +1 -0
  49. package/dist/__tests__/bridge-relay-client.test.js +283 -0
  50. package/dist/__tests__/bridge-relay-mode.test.d.ts +1 -0
  51. package/dist/__tests__/bridge-relay-mode.test.js +33 -0
  52. package/dist/__tests__/bridge-relay-pucc14.test.d.ts +1 -0
  53. package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
  54. package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
  55. package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
  56. package/dist/__tests__/bridge-server.test.js +22 -0
  57. package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
  58. package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
  59. package/dist/__tests__/devices.test.d.ts +1 -0
  60. package/dist/__tests__/devices.test.js +66 -0
  61. package/dist/__tests__/local.test.d.ts +1 -0
  62. package/dist/__tests__/local.test.js +147 -0
  63. package/dist/bridge/activityParser.d.ts +53 -0
  64. package/dist/bridge/activityParser.js +202 -0
  65. package/dist/bridge/apiClient.d.ts +56 -0
  66. package/dist/bridge/apiClient.js +50 -0
  67. package/dist/bridge/browserDriver.d.ts +98 -0
  68. package/dist/bridge/browserDriver.js +265 -0
  69. package/dist/bridge/browserFrames.d.ts +182 -0
  70. package/dist/bridge/browserFrames.js +179 -0
  71. package/dist/bridge/browserIdle.d.ts +75 -0
  72. package/dist/bridge/browserIdle.js +120 -0
  73. package/dist/bridge/bundleInstaller.d.ts +31 -0
  74. package/dist/bridge/bundleInstaller.js +127 -0
  75. package/dist/bridge/cdpClient.d.ts +47 -0
  76. package/dist/bridge/cdpClient.js +102 -0
  77. package/dist/bridge/chromiumLauncher.d.ts +45 -0
  78. package/dist/bridge/chromiumLauncher.js +113 -0
  79. package/dist/bridge/config.d.ts +3 -0
  80. package/dist/bridge/contextWriter.d.ts +31 -0
  81. package/dist/bridge/contextWriter.js +139 -0
  82. package/dist/bridge/deviceRegistration.d.ts +28 -0
  83. package/dist/bridge/deviceRegistration.js +19 -0
  84. package/dist/bridge/folderPicker.d.ts +34 -0
  85. package/dist/bridge/folderPicker.js +36 -0
  86. package/dist/bridge/folderPickerRunner.d.ts +3 -0
  87. package/dist/bridge/folderPickerRunner.js +25 -0
  88. package/dist/bridge/folderRegistry.d.ts +19 -0
  89. package/dist/bridge/folderRegistry.js +100 -0
  90. package/dist/bridge/framePacer.d.ts +61 -0
  91. package/dist/bridge/framePacer.js +79 -0
  92. package/dist/bridge/localAdd.d.ts +28 -0
  93. package/dist/bridge/localAdd.js +67 -0
  94. package/dist/bridge/localPathValidation.d.ts +20 -0
  95. package/dist/bridge/localPathValidation.js +58 -0
  96. package/dist/bridge/localPathValidationFs.d.ts +2 -0
  97. package/dist/bridge/localPathValidationFs.js +98 -0
  98. package/dist/bridge/loginStatus.d.ts +38 -0
  99. package/dist/bridge/loginStatus.js +56 -0
  100. package/dist/bridge/realPty.js +6 -2
  101. package/dist/bridge/relayClient.d.ts +166 -0
  102. package/dist/bridge/relayClient.js +517 -0
  103. package/dist/bridge/repoCheckout.d.ts +31 -0
  104. package/dist/bridge/repoCheckout.js +18 -0
  105. package/dist/bridge/server.d.ts +33 -0
  106. package/dist/bridge/server.js +98 -10
  107. package/dist/bridge/sessionManager.d.ts +18 -0
  108. package/dist/bridge/sessionManager.js +15 -1
  109. package/dist/commands/bridge.d.ts +21 -0
  110. package/dist/commands/bridge.js +194 -1
  111. package/dist/commands/devices.d.ts +9 -0
  112. package/dist/commands/devices.js +53 -0
  113. package/dist/commands/local.d.ts +9 -0
  114. package/dist/commands/local.js +141 -0
  115. package/dist/index.js +7 -1
  116. package/package.json +1 -1
@@ -0,0 +1,670 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * cbs-02 — relay client wiring for the cloud browser session.
5
+ *
6
+ * `browser-open` / `browser-input` (pane -> container) route to a single
7
+ * per-session BrowserDriver instance (one browser session per container in
8
+ * v1); driver frames/states forward up as `browser-frame` / `browser-state`
9
+ * with the sessionId stamped — following exactly the install-bundle /
10
+ * context-manifest non-PTY precedent (never touches sessions.open / PTY
11
+ * paths). Uses a FAKE driver (not the real CDP one) — this is a focused
12
+ * unit test of the new relayClient handler functions, same shape as
13
+ * bridge-install-bundle-frame.test.ts.
14
+ */
15
+ const ws_1 = require("ws");
16
+ const relayClient_1 = require("../bridge/relayClient");
17
+ const sessionManager_1 = require("../bridge/sessionManager");
18
+ const browserFrames_1 = require("../bridge/browserFrames");
19
+ class FakePty {
20
+ opts;
21
+ constructor(opts) {
22
+ this.opts = opts;
23
+ }
24
+ write() { }
25
+ resize() { }
26
+ kill() { }
27
+ onData() { }
28
+ onExit() { }
29
+ }
30
+ const makeFactory = () => ({ spawn: (o) => new FakePty(o) });
31
+ class FakeBrowserDriver {
32
+ startCalls = [];
33
+ navigateCalls = [];
34
+ inputCalls = [];
35
+ stopCalls = 0;
36
+ sleepCalls = [];
37
+ ackFrameCalls = 0;
38
+ frameCb = null;
39
+ stateCb = null;
40
+ async start(url) {
41
+ this.startCalls.push(url);
42
+ }
43
+ async navigate(url) {
44
+ this.navigateCalls.push(url);
45
+ }
46
+ input(event) {
47
+ this.inputCalls.push(event);
48
+ }
49
+ async back() { }
50
+ async forward() { }
51
+ async reload() { }
52
+ onFrame(cb) {
53
+ this.frameCb = cb;
54
+ }
55
+ onState(cb) {
56
+ this.stateCb = cb;
57
+ }
58
+ ackFrame() {
59
+ this.ackFrameCalls += 1;
60
+ }
61
+ stop() {
62
+ this.stopCalls += 1;
63
+ }
64
+ sleep(reason) {
65
+ this.sleepCalls.push(reason);
66
+ this.emitState({ state: 'asleep', reason });
67
+ }
68
+ // test-only helpers to drive the driver from outside
69
+ emitFrame(f) {
70
+ this.frameCb?.(f);
71
+ }
72
+ emitState(s) {
73
+ this.stateCb?.(s);
74
+ }
75
+ }
76
+ class FakeBrowserIdleTracker {
77
+ onIdle;
78
+ recordInputCalls = 0;
79
+ recordAckCalls = 0;
80
+ startCalls = 0;
81
+ stopCalls = 0;
82
+ constructor(onIdle) {
83
+ this.onIdle = onIdle;
84
+ }
85
+ recordInput() {
86
+ this.recordInputCalls += 1;
87
+ }
88
+ recordAck() {
89
+ this.recordAckCalls += 1;
90
+ }
91
+ start() {
92
+ this.startCalls += 1;
93
+ }
94
+ stop() {
95
+ this.stopCalls += 1;
96
+ }
97
+ // test-only helper to fire the idle condition from outside
98
+ fireIdle() {
99
+ this.onIdle();
100
+ }
101
+ }
102
+ function frameQueue(ws) {
103
+ const frames = [];
104
+ const waiters = [];
105
+ ws.on('message', (raw) => {
106
+ const f = JSON.parse(String(raw));
107
+ const w = waiters.shift();
108
+ if (w)
109
+ w(f);
110
+ else
111
+ frames.push(f);
112
+ });
113
+ return {
114
+ next: () => (frames.length > 0 ? Promise.resolve(frames.shift()) : new Promise((r) => waiters.push(r))),
115
+ };
116
+ }
117
+ function startFakeRelay() {
118
+ return new Promise((resolve) => {
119
+ const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
120
+ const queue = [];
121
+ const waiters = [];
122
+ wss.on('connection', (ws) => {
123
+ const w = waiters.shift();
124
+ if (w)
125
+ w(ws);
126
+ else
127
+ queue.push(ws);
128
+ });
129
+ wss.on('listening', () => {
130
+ const addr = wss.address();
131
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
132
+ resolve({
133
+ wss,
134
+ port,
135
+ nextConnection: () => (queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiters.push(r))),
136
+ });
137
+ });
138
+ });
139
+ }
140
+ function closeRelay(wss) {
141
+ return new Promise((resolve, reject) => {
142
+ for (const client of wss.clients)
143
+ client.terminate();
144
+ wss.close((err) => (err ? reject(err) : resolve()));
145
+ });
146
+ }
147
+ describe('relay client — browser-open/browser-input (cbs-02)', () => {
148
+ it('browser-open creates a driver via the factory, starts it, and forwards frames/states with sessionId stamped', async () => {
149
+ const relay = await startFakeRelay();
150
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
151
+ const drivers = {};
152
+ const handle = (0, relayClient_1.startRelayClient)({
153
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
154
+ relayToken: 'tok',
155
+ sessions,
156
+ createBrowserDriver: ({ sessionId }) => {
157
+ const d = new FakeBrowserDriver();
158
+ drivers[sessionId] = d;
159
+ return d;
160
+ },
161
+ });
162
+ const ws = await relay.nextConnection();
163
+ const q = frameQueue(ws);
164
+ await q.next(); // auth
165
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
166
+ await new Promise((r) => setTimeout(r, 20)); // let the async start() microtask settle
167
+ expect(drivers['browser:c1'].startCalls).toEqual(['https://example.com']);
168
+ // A browser session must never open a PTY.
169
+ expect(sessions.list()).toEqual([]);
170
+ drivers['browser:c1'].emitFrame({ seq: 1, data: 'abc', w: 1280, h: 800 });
171
+ const frameMsg = await q.next();
172
+ expect(frameMsg).toEqual({ t: 'browser-frame', sessionId: 'browser:c1', seq: 1, data: 'abc', w: 1280, h: 800 });
173
+ expect((0, browserFrames_1.isBrowserScreenFrame)(frameMsg)).toBe(true);
174
+ // cbs-06: the cbs-02 interim policy (self-ack on send) is GONE — the
175
+ // pacer is only acked by a real browser-frame-ack from the pane (see
176
+ // the dedicated describe block below). Forwarding a frame must not
177
+ // ack it on its own.
178
+ expect(drivers['browser:c1'].ackFrameCalls).toBe(0);
179
+ drivers['browser:c1'].emitState({ state: 'open', url: 'https://example.com/', title: 'Example' });
180
+ const stateMsg = await q.next();
181
+ expect(stateMsg).toEqual({ t: 'browser-state', sessionId: 'browser:c1', state: 'open', url: 'https://example.com/', title: 'Example' });
182
+ expect((0, browserFrames_1.isBrowserStateFrame)(stateMsg)).toBe(true);
183
+ handle.close();
184
+ await closeRelay(relay.wss);
185
+ });
186
+ it('browser-open on an already-open session navigates the existing driver instead of starting a new one', async () => {
187
+ const relay = await startFakeRelay();
188
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
189
+ const drivers = [];
190
+ const handle = (0, relayClient_1.startRelayClient)({
191
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
192
+ relayToken: 'tok',
193
+ sessions,
194
+ createBrowserDriver: () => {
195
+ const d = new FakeBrowserDriver();
196
+ drivers.push(d);
197
+ return d;
198
+ },
199
+ });
200
+ const ws = await relay.nextConnection();
201
+ const q = frameQueue(ws);
202
+ await q.next(); // auth
203
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
204
+ await new Promise((r) => setTimeout(r, 20));
205
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com/other' }));
206
+ await new Promise((r) => setTimeout(r, 20));
207
+ // Exactly one driver instance for this sessionId (single driver per container session).
208
+ expect(drivers).toHaveLength(1);
209
+ expect(drivers[0].startCalls).toEqual(['https://example.com']);
210
+ expect(drivers[0].navigateCalls).toEqual(['https://example.com/other']);
211
+ handle.close();
212
+ await closeRelay(relay.wss);
213
+ });
214
+ it('browser-input dispatches the event to the session-keyed driver', async () => {
215
+ const relay = await startFakeRelay();
216
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
217
+ const drivers = {};
218
+ const handle = (0, relayClient_1.startRelayClient)({
219
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
220
+ relayToken: 'tok',
221
+ sessions,
222
+ createBrowserDriver: ({ sessionId }) => {
223
+ const d = new FakeBrowserDriver();
224
+ drivers[sessionId] = d;
225
+ return d;
226
+ },
227
+ });
228
+ const ws = await relay.nextConnection();
229
+ const q = frameQueue(ws);
230
+ await q.next(); // auth
231
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
232
+ await new Promise((r) => setTimeout(r, 20));
233
+ const event = { kind: 'mouse', type: 'down', x: 10, y: 20, button: 'left' };
234
+ ws.send(JSON.stringify({ t: 'browser-input', sessionId: 'browser:c1', event }));
235
+ await new Promise((r) => setTimeout(r, 20));
236
+ expect(drivers['browser:c1'].inputCalls).toEqual([event]);
237
+ // Never opens or touches a PTY session.
238
+ expect(sessions.list()).toEqual([]);
239
+ handle.close();
240
+ await closeRelay(relay.wss);
241
+ });
242
+ it('browser-input for an unknown sessionId is a safe no-op (no driver, no crash)', async () => {
243
+ const relay = await startFakeRelay();
244
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
245
+ const handle = (0, relayClient_1.startRelayClient)({
246
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
247
+ relayToken: 'tok',
248
+ sessions,
249
+ createBrowserDriver: () => new FakeBrowserDriver(),
250
+ });
251
+ const ws = await relay.nextConnection();
252
+ const q = frameQueue(ws);
253
+ await q.next(); // auth
254
+ ws.send(JSON.stringify({ t: 'browser-input', sessionId: 'browser:never-opened', event: { kind: 'insertText', text: 'x' } }));
255
+ await new Promise((r) => setTimeout(r, 20));
256
+ expect(sessions.list()).toEqual([]);
257
+ handle.close();
258
+ await closeRelay(relay.wss);
259
+ });
260
+ it('errors (never crashes) when no browser driver factory is wired', async () => {
261
+ const relay = await startFakeRelay();
262
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
263
+ const handle = (0, relayClient_1.startRelayClient)({ relayUrl: `ws://127.0.0.1:${relay.port}`, relayToken: 'tok', sessions });
264
+ const ws = await relay.nextConnection();
265
+ const q = frameQueue(ws);
266
+ await q.next(); // auth
267
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
268
+ expect((await q.next()).t).toBe('error');
269
+ handle.close();
270
+ await closeRelay(relay.wss);
271
+ });
272
+ // cbs-07: this is the exact failure shape a bridge on an OLD image (no
273
+ // Chromium binary, or any other spawn/launch failure) hits in production —
274
+ // chromiumLauncher's spawn never throws synchronously on ENOENT, it
275
+ // rejects the driver's start() promise asynchronously instead. Proves the
276
+ // ship slice's degrade-gracefully choice (documented in
277
+ // commands/bridge.ts's createBrowserDriver wiring): a rejecting start()
278
+ // yields one `error` frame, cleans up bookkeeping, and — the actual point
279
+ // of this test — never takes the relay connection or the bridge process
280
+ // down with it (a second frame on a fresh session still works normally).
281
+ it('a driver whose start() rejects (missing/failed Chromium) errors that one session but never crashes the bridge', async () => {
282
+ class FailingStartBrowserDriver extends FakeBrowserDriver {
283
+ async start() {
284
+ throw new Error('spawn chromium ENOENT');
285
+ }
286
+ }
287
+ const relay = await startFakeRelay();
288
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
289
+ const handle = (0, relayClient_1.startRelayClient)({
290
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
291
+ relayToken: 'tok',
292
+ sessions,
293
+ createBrowserDriver: () => new FailingStartBrowserDriver(),
294
+ });
295
+ const ws = await relay.nextConnection();
296
+ const q = frameQueue(ws);
297
+ await q.next(); // auth
298
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
299
+ const errorFrame = await q.next();
300
+ expect(errorFrame.t).toBe('error');
301
+ expect(errorFrame.message).toMatch(/ENOENT/);
302
+ // The connection (and the bridge) is unaffected — an unrelated frame on
303
+ // a fresh session still gets a normal reply, proving nothing wedged.
304
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c2', url: 'https://example.com' }));
305
+ const secondFrame = await q.next();
306
+ expect(secondFrame.t).toBe('error');
307
+ handle.close();
308
+ await closeRelay(relay.wss);
309
+ });
310
+ });
311
+ describe('relay client — browser-frame-ack: true end-to-end backpressure (cbs-06)', () => {
312
+ it('a browser-frame-ack frame from the pane calls the driver ackFrame — nothing else does', async () => {
313
+ const relay = await startFakeRelay();
314
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
315
+ const drivers = {};
316
+ const handle = (0, relayClient_1.startRelayClient)({
317
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
318
+ relayToken: 'tok',
319
+ sessions,
320
+ createBrowserDriver: ({ sessionId }) => {
321
+ const d = new FakeBrowserDriver();
322
+ drivers[sessionId] = d;
323
+ return d;
324
+ },
325
+ });
326
+ const ws = await relay.nextConnection();
327
+ const q = frameQueue(ws);
328
+ await q.next(); // auth
329
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
330
+ await new Promise((r) => setTimeout(r, 20));
331
+ drivers['browser:c1'].emitFrame({ seq: 1, data: 'abc', w: 1280, h: 800 });
332
+ await q.next(); // the browser-frame forward
333
+ expect(drivers['browser:c1'].ackFrameCalls).toBe(0);
334
+ ws.send(JSON.stringify({ t: 'browser-frame-ack', sessionId: 'browser:c1', seq: 1 }));
335
+ await new Promise((r) => setTimeout(r, 20));
336
+ expect(drivers['browser:c1'].ackFrameCalls).toBe(1);
337
+ handle.close();
338
+ await closeRelay(relay.wss);
339
+ });
340
+ it('browser-frame-ack for an unknown sessionId is a safe no-op', async () => {
341
+ const relay = await startFakeRelay();
342
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
343
+ const handle = (0, relayClient_1.startRelayClient)({
344
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
345
+ relayToken: 'tok',
346
+ sessions,
347
+ createBrowserDriver: () => new FakeBrowserDriver(),
348
+ });
349
+ const ws = await relay.nextConnection();
350
+ const q = frameQueue(ws);
351
+ await q.next(); // auth
352
+ ws.send(JSON.stringify({ t: 'browser-frame-ack', sessionId: 'browser:never-opened', seq: 0 }));
353
+ await new Promise((r) => setTimeout(r, 20));
354
+ handle.close();
355
+ await closeRelay(relay.wss);
356
+ });
357
+ it('on socket close, every open driver is ack-flushed so the pacer never wedges waiting for a pane that is gone', async () => {
358
+ const relay = await startFakeRelay();
359
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
360
+ const drivers = {};
361
+ const handle = (0, relayClient_1.startRelayClient)({
362
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
363
+ relayToken: 'tok',
364
+ sessions,
365
+ createBrowserDriver: ({ sessionId }) => {
366
+ const d = new FakeBrowserDriver();
367
+ drivers[sessionId] = d;
368
+ return d;
369
+ },
370
+ });
371
+ const ws = await relay.nextConnection();
372
+ const q = frameQueue(ws);
373
+ await q.next(); // auth
374
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
375
+ await new Promise((r) => setTimeout(r, 20));
376
+ drivers['browser:c1'].emitFrame({ seq: 1, data: 'abc', w: 1280, h: 800 }); // in flight, never acked by a pane
377
+ await new Promise((r) => setTimeout(r, 20));
378
+ expect(drivers['browser:c1'].ackFrameCalls).toBe(0);
379
+ ws.terminate(); // simulate the pane/relay connection dropping mid-stream
380
+ await new Promise((r) => setTimeout(r, 30));
381
+ expect(drivers['browser:c1'].ackFrameCalls).toBe(1);
382
+ handle.close();
383
+ await closeRelay(relay.wss);
384
+ });
385
+ });
386
+ describe('relay client — browser-sleep (cbs-06)', () => {
387
+ it('stops the driver (asleep, reason "sleep") and forwards exactly one browser-state frame', async () => {
388
+ const relay = await startFakeRelay();
389
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
390
+ const drivers = {};
391
+ const handle = (0, relayClient_1.startRelayClient)({
392
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
393
+ relayToken: 'tok',
394
+ sessions,
395
+ createBrowserDriver: ({ sessionId }) => {
396
+ const d = new FakeBrowserDriver();
397
+ drivers[sessionId] = d;
398
+ return d;
399
+ },
400
+ });
401
+ const ws = await relay.nextConnection();
402
+ const q = frameQueue(ws);
403
+ await q.next(); // auth
404
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
405
+ await new Promise((r) => setTimeout(r, 20));
406
+ ws.send(JSON.stringify({ t: 'browser-sleep', sessionId: 'browser:c1' }));
407
+ const stateMsg = await q.next();
408
+ expect(stateMsg).toEqual({ t: 'browser-state', sessionId: 'browser:c1', state: 'asleep', reason: 'sleep' });
409
+ expect((0, browserFrames_1.isBrowserStateFrame)(stateMsg)).toBe(true);
410
+ expect(drivers['browser:c1'].sleepCalls).toEqual(['sleep']);
411
+ expect(drivers['browser:c1'].stopCalls).toBe(0); // sleep, not stop — distinct verb
412
+ handle.close();
413
+ await closeRelay(relay.wss);
414
+ });
415
+ it('sleep-of-nothing (never-opened session) is a safe no-op — no error, no frame', async () => {
416
+ const relay = await startFakeRelay();
417
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
418
+ const handle = (0, relayClient_1.startRelayClient)({
419
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
420
+ relayToken: 'tok',
421
+ sessions,
422
+ createBrowserDriver: () => new FakeBrowserDriver(),
423
+ });
424
+ const ws = await relay.nextConnection();
425
+ const q = frameQueue(ws);
426
+ await q.next(); // auth
427
+ ws.send(JSON.stringify({ t: 'browser-sleep', sessionId: 'browser:never-opened' }));
428
+ await new Promise((r) => setTimeout(r, 20));
429
+ // Prove no stray frame followed: open a fresh session and the next frame is its own state.
430
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c2', url: 'https://example.com' }));
431
+ await new Promise((r) => setTimeout(r, 20));
432
+ handle.close();
433
+ await closeRelay(relay.wss);
434
+ });
435
+ it('double browser-sleep is a safe no-op the second time (only one asleep frame total)', async () => {
436
+ const relay = await startFakeRelay();
437
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
438
+ const drivers = {};
439
+ const handle = (0, relayClient_1.startRelayClient)({
440
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
441
+ relayToken: 'tok',
442
+ sessions,
443
+ createBrowserDriver: ({ sessionId }) => {
444
+ const d = new FakeBrowserDriver();
445
+ drivers[sessionId] = d;
446
+ return d;
447
+ },
448
+ });
449
+ const ws = await relay.nextConnection();
450
+ const q = frameQueue(ws);
451
+ await q.next(); // auth
452
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
453
+ await new Promise((r) => setTimeout(r, 20));
454
+ ws.send(JSON.stringify({ t: 'browser-sleep', sessionId: 'browser:c1' }));
455
+ await q.next(); // asleep state
456
+ ws.send(JSON.stringify({ t: 'browser-sleep', sessionId: 'browser:c1' }));
457
+ await new Promise((r) => setTimeout(r, 20));
458
+ // Only ever one driver, one sleep() call — the second browser-sleep found
459
+ // no driver in the map (removed on the first asleep transition).
460
+ expect(Object.keys(drivers)).toEqual(['browser:c1']);
461
+ expect(drivers['browser:c1'].sleepCalls).toEqual(['sleep']);
462
+ handle.close();
463
+ await closeRelay(relay.wss);
464
+ });
465
+ it('a stopped (asleep) driver is REPLACED on the next browser-open, not navigated', async () => {
466
+ const relay = await startFakeRelay();
467
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
468
+ const drivers = [];
469
+ const handle = (0, relayClient_1.startRelayClient)({
470
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
471
+ relayToken: 'tok',
472
+ sessions,
473
+ createBrowserDriver: () => {
474
+ const d = new FakeBrowserDriver();
475
+ drivers.push(d);
476
+ return d;
477
+ },
478
+ });
479
+ const ws = await relay.nextConnection();
480
+ const q = frameQueue(ws);
481
+ await q.next(); // auth
482
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
483
+ await new Promise((r) => setTimeout(r, 20));
484
+ ws.send(JSON.stringify({ t: 'browser-sleep', sessionId: 'browser:c1' }));
485
+ await q.next(); // asleep state
486
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com/resumed' }));
487
+ await new Promise((r) => setTimeout(r, 20));
488
+ // A SECOND driver instance was created and start()ed with the resume URL
489
+ // — the first (asleep) driver was never asked to navigate().
490
+ expect(drivers).toHaveLength(2);
491
+ expect(drivers[0].startCalls).toEqual(['https://example.com']);
492
+ expect(drivers[0].navigateCalls).toEqual([]);
493
+ expect(drivers[1].startCalls).toEqual(['https://example.com/resumed']);
494
+ handle.close();
495
+ await closeRelay(relay.wss);
496
+ });
497
+ it('a closed (crashed) driver is also REPLACED on the next browser-open, not navigated', async () => {
498
+ const relay = await startFakeRelay();
499
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
500
+ const drivers = [];
501
+ const handle = (0, relayClient_1.startRelayClient)({
502
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
503
+ relayToken: 'tok',
504
+ sessions,
505
+ createBrowserDriver: () => {
506
+ const d = new FakeBrowserDriver();
507
+ drivers.push(d);
508
+ return d;
509
+ },
510
+ });
511
+ const ws = await relay.nextConnection();
512
+ const q = frameQueue(ws);
513
+ await q.next(); // auth
514
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
515
+ await new Promise((r) => setTimeout(r, 20));
516
+ drivers[0].emitState({ state: 'closed', reason: 'chromium-exit code=1' }); // crash, not sleep
517
+ await q.next(); // closed state
518
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com/again' }));
519
+ await new Promise((r) => setTimeout(r, 20));
520
+ expect(drivers).toHaveLength(2);
521
+ expect(drivers[1].startCalls).toEqual(['https://example.com/again']);
522
+ handle.close();
523
+ await closeRelay(relay.wss);
524
+ });
525
+ });
526
+ describe('relay client — browser idle timer wiring (cbs-06)', () => {
527
+ it('creates and starts an idle tracker alongside the driver on browser-open', async () => {
528
+ const relay = await startFakeRelay();
529
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
530
+ const trackers = {};
531
+ const handle = (0, relayClient_1.startRelayClient)({
532
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
533
+ relayToken: 'tok',
534
+ sessions,
535
+ createBrowserDriver: () => new FakeBrowserDriver(),
536
+ createBrowserIdleTracker: ({ sessionId, onIdle }) => {
537
+ const t = new FakeBrowserIdleTracker(onIdle);
538
+ trackers[sessionId] = t;
539
+ return t;
540
+ },
541
+ });
542
+ const ws = await relay.nextConnection();
543
+ const q = frameQueue(ws);
544
+ await q.next(); // auth
545
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
546
+ await new Promise((r) => setTimeout(r, 20));
547
+ expect(trackers['browser:c1']).toBeDefined();
548
+ expect(trackers['browser:c1'].startCalls).toBe(1);
549
+ handle.close();
550
+ await closeRelay(relay.wss);
551
+ });
552
+ it('browser-input and browser-frame-ack feed the idle tracker liveness signals', async () => {
553
+ const relay = await startFakeRelay();
554
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
555
+ const drivers = {};
556
+ const trackers = {};
557
+ const handle = (0, relayClient_1.startRelayClient)({
558
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
559
+ relayToken: 'tok',
560
+ sessions,
561
+ createBrowserDriver: ({ sessionId }) => {
562
+ const d = new FakeBrowserDriver();
563
+ drivers[sessionId] = d;
564
+ return d;
565
+ },
566
+ createBrowserIdleTracker: ({ sessionId, onIdle }) => {
567
+ const t = new FakeBrowserIdleTracker(onIdle);
568
+ trackers[sessionId] = t;
569
+ return t;
570
+ },
571
+ });
572
+ const ws = await relay.nextConnection();
573
+ const q = frameQueue(ws);
574
+ await q.next(); // auth
575
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
576
+ await new Promise((r) => setTimeout(r, 20));
577
+ const event = { kind: 'insertText', text: 'x' };
578
+ ws.send(JSON.stringify({ t: 'browser-input', sessionId: 'browser:c1', event }));
579
+ await new Promise((r) => setTimeout(r, 20));
580
+ expect(trackers['browser:c1'].recordInputCalls).toBe(1);
581
+ ws.send(JSON.stringify({ t: 'browser-frame-ack', sessionId: 'browser:c1', seq: 0 }));
582
+ await new Promise((r) => setTimeout(r, 20));
583
+ expect(trackers['browser:c1'].recordAckCalls).toBe(1);
584
+ handle.close();
585
+ await closeRelay(relay.wss);
586
+ });
587
+ it('onIdle firing closes the browser (asleep, reason idle-timeout) and stops the tracker', async () => {
588
+ const relay = await startFakeRelay();
589
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
590
+ const drivers = {};
591
+ const trackers = {};
592
+ const handle = (0, relayClient_1.startRelayClient)({
593
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
594
+ relayToken: 'tok',
595
+ sessions,
596
+ createBrowserDriver: ({ sessionId }) => {
597
+ const d = new FakeBrowserDriver();
598
+ drivers[sessionId] = d;
599
+ return d;
600
+ },
601
+ createBrowserIdleTracker: ({ sessionId, onIdle }) => {
602
+ const t = new FakeBrowserIdleTracker(onIdle);
603
+ trackers[sessionId] = t;
604
+ return t;
605
+ },
606
+ });
607
+ const ws = await relay.nextConnection();
608
+ const q = frameQueue(ws);
609
+ await q.next(); // auth
610
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
611
+ await new Promise((r) => setTimeout(r, 20));
612
+ trackers['browser:c1'].fireIdle();
613
+ const stateMsg = await q.next();
614
+ expect(stateMsg).toEqual({ t: 'browser-state', sessionId: 'browser:c1', state: 'asleep', reason: 'idle-timeout' });
615
+ expect((0, browserFrames_1.isBrowserStateFrame)(stateMsg)).toBe(true);
616
+ expect(drivers['browser:c1'].sleepCalls).toEqual(['idle-timeout']);
617
+ expect(trackers['browser:c1'].stopCalls).toBe(1);
618
+ handle.close();
619
+ await closeRelay(relay.wss);
620
+ });
621
+ it('the idle tracker is torn down (stopped) and a fresh one created when the session resumes', async () => {
622
+ const relay = await startFakeRelay();
623
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
624
+ const trackers = [];
625
+ const handle = (0, relayClient_1.startRelayClient)({
626
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
627
+ relayToken: 'tok',
628
+ sessions,
629
+ createBrowserDriver: () => new FakeBrowserDriver(),
630
+ createBrowserIdleTracker: ({ onIdle }) => {
631
+ const t = new FakeBrowserIdleTracker(onIdle);
632
+ trackers.push(t);
633
+ return t;
634
+ },
635
+ });
636
+ const ws = await relay.nextConnection();
637
+ const q = frameQueue(ws);
638
+ await q.next(); // auth
639
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
640
+ await new Promise((r) => setTimeout(r, 20));
641
+ ws.send(JSON.stringify({ t: 'browser-sleep', sessionId: 'browser:c1' }));
642
+ await q.next(); // asleep
643
+ expect(trackers[0].stopCalls).toBe(1);
644
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com/resumed' }));
645
+ await new Promise((r) => setTimeout(r, 20));
646
+ expect(trackers).toHaveLength(2);
647
+ expect(trackers[1].startCalls).toBe(1);
648
+ handle.close();
649
+ await closeRelay(relay.wss);
650
+ });
651
+ it('absent createBrowserIdleTracker: browser-open still works, no idle tracking attempted', async () => {
652
+ const relay = await startFakeRelay();
653
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
654
+ const handle = (0, relayClient_1.startRelayClient)({
655
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
656
+ relayToken: 'tok',
657
+ sessions,
658
+ createBrowserDriver: () => new FakeBrowserDriver(),
659
+ });
660
+ const ws = await relay.nextConnection();
661
+ const q = frameQueue(ws);
662
+ await q.next(); // auth
663
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
664
+ await new Promise((r) => setTimeout(r, 20));
665
+ ws.send(JSON.stringify({ t: 'browser-input', sessionId: 'browser:c1', event: { kind: 'insertText', text: 'x' } }));
666
+ await new Promise((r) => setTimeout(r, 20));
667
+ handle.close();
668
+ await closeRelay(relay.wss);
669
+ });
670
+ });
@@ -0,0 +1 @@
1
+ export {};