@wenrwa/marketplace-cli 0.8.7 → 0.8.9

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,465 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * tp-06 — previewForwarder.ts: the Team preview's tenant boundary on the
38
+ * owner's hosted machine (docs/prd/team-preview.md module 4, ADR-0014).
39
+ *
40
+ * Runs against the REAL `net` module with real upstream servers on
41
+ * 127.0.0.1. The two-source-address cases use two REAL source addresses:
42
+ * the forwarder listens dual-stack on `::`, the allowed peer dials `::1`
43
+ * and the "other tenant" dials 127.0.0.1 (which arrives as
44
+ * `::ffff:127.0.0.1`). The 6PN-address cases inject `remoteAddressOf`,
45
+ * because every real test connection necessarily comes from loopback.
46
+ */
47
+ const net = __importStar(require("net"));
48
+ const http = __importStar(require("http"));
49
+ const ws_1 = require("ws");
50
+ const previewForwarder_1 = require("../bridge/previewForwarder");
51
+ const GATEWAY_6PN = 'fdaa:0:1234:a7b:ce1:2:8a2e:2';
52
+ const OTHER_TENANT_6PN = 'fdaa:0:9999:a7b:ce1:7:1:1';
53
+ /** A real TCP "preview app" on 127.0.0.1 that greets and records bytes. */
54
+ function startUpstream() {
55
+ return new Promise((resolve) => {
56
+ const received = [];
57
+ let connections = 0;
58
+ const sockets = new Set();
59
+ const server = net.createServer((socket) => {
60
+ connections++;
61
+ sockets.add(socket);
62
+ socket.on('close', () => sockets.delete(socket));
63
+ socket.on('error', () => { });
64
+ socket.write('hello from preview\n');
65
+ socket.on('data', (d) => received.push(d));
66
+ });
67
+ server.listen(0, '127.0.0.1', () => {
68
+ const addr = server.address();
69
+ resolve({
70
+ port: addr.port,
71
+ received,
72
+ connections: () => connections,
73
+ close: () => new Promise((r) => {
74
+ for (const s of sockets)
75
+ s.destroy();
76
+ server.close(() => r());
77
+ }),
78
+ });
79
+ });
80
+ });
81
+ }
82
+ function connect(host, port) {
83
+ return new Promise((resolve, reject) => {
84
+ const client = net.createConnection({ host, port });
85
+ client.once('connect', () => resolve(client));
86
+ client.once('error', reject);
87
+ });
88
+ }
89
+ function readAll(socket, timeoutMs = 400) {
90
+ return new Promise((resolve) => {
91
+ const chunks = [];
92
+ let done = false;
93
+ const finish = () => {
94
+ if (done)
95
+ return;
96
+ done = true;
97
+ resolve(Buffer.concat(chunks).toString());
98
+ };
99
+ socket.on('data', (d) => chunks.push(d));
100
+ socket.on('close', finish);
101
+ socket.on('error', finish);
102
+ setTimeout(finish, timeoutMs);
103
+ });
104
+ }
105
+ const tick = (ms = 50) => new Promise((r) => setTimeout(r, ms));
106
+ describe('canonicalAddress (tp-06)', () => {
107
+ it('treats compressed and expanded IPv6 spellings as the same address', () => {
108
+ expect((0, previewForwarder_1.canonicalAddress)('fdaa:0:1234::2')).toBe((0, previewForwarder_1.canonicalAddress)('FDAA:0000:1234:0:0:0:0:0002'));
109
+ expect((0, previewForwarder_1.canonicalAddress)('::1')).toBe('0000:0000:0000:0000:0000:0000:0000:0001');
110
+ });
111
+ it('unwraps an IPv4-mapped peer and drops a zone id', () => {
112
+ expect((0, previewForwarder_1.canonicalAddress)('::ffff:127.0.0.1')).toBe('127.0.0.1');
113
+ expect((0, previewForwarder_1.canonicalAddress)('fe80::1%eth0')).toBe((0, previewForwarder_1.canonicalAddress)('fe80::1'));
114
+ });
115
+ it('returns null for anything that is not an IP', () => {
116
+ expect((0, previewForwarder_1.canonicalAddress)('preview-gateway.internal')).toBeNull();
117
+ expect((0, previewForwarder_1.canonicalAddress)('')).toBeNull();
118
+ expect((0, previewForwarder_1.canonicalAddress)(undefined)).toBeNull();
119
+ });
120
+ });
121
+ describe('resolvePreviewPorts (tp-06)', () => {
122
+ it('defaults to the fixed preview port and a distinct forwarder port', () => {
123
+ expect((0, previewForwarder_1.resolvePreviewPorts)({})).toEqual({
124
+ previewPort: previewForwarder_1.PREVIEW_PORT_DEFAULT,
125
+ forwarderPort: previewForwarder_1.PREVIEW_FORWARDER_PORT_DEFAULT,
126
+ });
127
+ expect(previewForwarder_1.PREVIEW_PORT_DEFAULT).not.toBe(previewForwarder_1.PREVIEW_FORWARDER_PORT_DEFAULT);
128
+ });
129
+ it('honours env overrides and ignores garbage', () => {
130
+ expect((0, previewForwarder_1.resolvePreviewPorts)({ HT_PREVIEW_PORT: '5100', HT_PREVIEW_FORWARDER_PORT: '5101' })).toEqual({
131
+ previewPort: 5100,
132
+ forwarderPort: 5101,
133
+ });
134
+ expect((0, previewForwarder_1.resolvePreviewPorts)({ HT_PREVIEW_PORT: 'nope', HT_PREVIEW_FORWARDER_PORT: '70000' })).toEqual({
135
+ previewPort: previewForwarder_1.PREVIEW_PORT_DEFAULT,
136
+ forwarderPort: previewForwarder_1.PREVIEW_FORWARDER_PORT_DEFAULT,
137
+ });
138
+ });
139
+ it('never lets the forwarder share the preview port', () => {
140
+ expect((0, previewForwarder_1.resolvePreviewPorts)({ HT_PREVIEW_PORT: '5100', HT_PREVIEW_FORWARDER_PORT: '5100' })).toEqual({
141
+ previewPort: 5100,
142
+ forwarderPort: previewForwarder_1.PREVIEW_FORWARDER_PORT_DEFAULT,
143
+ });
144
+ });
145
+ });
146
+ describe('createPreviewForwarder — two real source addresses (tp-06)', () => {
147
+ let preview;
148
+ let forwarder;
149
+ beforeEach(async () => {
150
+ preview = await startUpstream();
151
+ forwarder = (0, previewForwarder_1.createPreviewForwarder)({
152
+ listenHost: '::', // dual-stack, so the same listener sees ::1 and ::ffff:127.0.0.1
153
+ listenPort: 0,
154
+ targetPort: preview.port,
155
+ allowed: ['::1'],
156
+ });
157
+ await forwarder.start();
158
+ });
159
+ afterEach(async () => {
160
+ forwarder.stop();
161
+ await preview.close();
162
+ });
163
+ it('a connection from the allowed address reaches the preview port', async () => {
164
+ const client = await connect('::1', forwarder.port());
165
+ client.write('ping');
166
+ const reply = await readAll(client);
167
+ client.destroy();
168
+ await tick();
169
+ expect(reply).toContain('hello from preview');
170
+ expect(Buffer.concat(preview.received).toString()).toBe('ping');
171
+ });
172
+ it('a connection from any other address is closed without forwarding', async () => {
173
+ const client = await connect('127.0.0.1', forwarder.port());
174
+ client.write('should never arrive');
175
+ const reply = await readAll(client);
176
+ await tick();
177
+ expect(reply).toBe('');
178
+ expect(preview.connections()).toBe(0);
179
+ expect(preview.received).toHaveLength(0);
180
+ });
181
+ });
182
+ describe('createPreviewForwarder — another tenant on 6PN (tp-06)', () => {
183
+ it('refuses another tenant address and never dials the preview', async () => {
184
+ const preview = await startUpstream();
185
+ let peer = OTHER_TENANT_6PN;
186
+ const forwarder = (0, previewForwarder_1.createPreviewForwarder)({
187
+ listenHost: '::1',
188
+ listenPort: 0,
189
+ targetPort: preview.port,
190
+ allowed: [GATEWAY_6PN],
191
+ remoteAddressOf: () => peer,
192
+ });
193
+ try {
194
+ await forwarder.start();
195
+ const denied = await connect('::1', forwarder.port());
196
+ denied.write('GET /.env HTTP/1.1\r\nHost: x\r\n\r\n');
197
+ expect(await readAll(denied)).toBe('');
198
+ await tick();
199
+ expect(preview.connections()).toBe(0);
200
+ // Same listener, the gateway's own address (spelled differently) is let through.
201
+ peer = 'FDAA:0:1234:A7B:CE1:2:8A2E:2';
202
+ const allowed = await connect('::1', forwarder.port());
203
+ expect(await readAll(allowed)).toContain('hello from preview');
204
+ allowed.destroy();
205
+ expect(preview.connections()).toBe(1);
206
+ }
207
+ finally {
208
+ forwarder.stop();
209
+ await preview.close();
210
+ }
211
+ });
212
+ });
213
+ describe('createPreviewForwarder — one port only (tp-06)', () => {
214
+ it('opens exactly one listener and forwards only to 127.0.0.1:<preview port>', async () => {
215
+ const preview = await startUpstream();
216
+ const secret = await startUpstream(); // e.g. the owner's own dev server, or the bridge
217
+ const created = [];
218
+ const dialled = [];
219
+ const spyNet = {
220
+ createServer: ((...args) => {
221
+ created.push(1);
222
+ return net.createServer(...args);
223
+ }),
224
+ createConnection: ((opts) => {
225
+ dialled.push(opts);
226
+ return net.createConnection(opts);
227
+ }),
228
+ };
229
+ const forwarder = (0, previewForwarder_1.createPreviewForwarder)({
230
+ listenHost: '::1',
231
+ listenPort: 0,
232
+ targetPort: preview.port,
233
+ allowed: [GATEWAY_6PN],
234
+ remoteAddressOf: () => GATEWAY_6PN,
235
+ net: spyNet,
236
+ });
237
+ try {
238
+ await forwarder.start();
239
+ for (let i = 0; i < 3; i++) {
240
+ const c = await connect('::1', forwarder.port());
241
+ c.write(`try ${secret.port}`);
242
+ await readAll(c, 150);
243
+ c.destroy();
244
+ }
245
+ await tick();
246
+ expect(created).toHaveLength(1);
247
+ expect(dialled).toEqual([
248
+ { host: '127.0.0.1', port: preview.port },
249
+ { host: '127.0.0.1', port: preview.port },
250
+ { host: '127.0.0.1', port: preview.port },
251
+ ]);
252
+ expect(secret.connections()).toBe(0);
253
+ // Nothing listens on the listen host at any other port.
254
+ const outcome = await connect('::1', secret.port).then((s) => {
255
+ s.destroy();
256
+ return 'connected';
257
+ }, () => 'refused');
258
+ expect(outcome).toBe('refused');
259
+ }
260
+ finally {
261
+ forwarder.stop();
262
+ await preview.close();
263
+ await secret.close();
264
+ }
265
+ });
266
+ });
267
+ describe('createPreviewForwarder — WebSocket upgrade (tp-06)', () => {
268
+ it('passes a WebSocket upgrade through end to end', async () => {
269
+ const server = http.createServer((_req, res) => res.end('ok'));
270
+ const wss = new ws_1.WebSocketServer({ server });
271
+ wss.on('connection', (ws) => ws.on('message', (m) => ws.send(`echo:${m.toString()}`)));
272
+ await new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
273
+ const previewPort = server.address().port;
274
+ const forwarder = (0, previewForwarder_1.createPreviewForwarder)({
275
+ listenHost: '::1',
276
+ listenPort: 0,
277
+ targetPort: previewPort,
278
+ allowed: [GATEWAY_6PN],
279
+ remoteAddressOf: () => GATEWAY_6PN,
280
+ });
281
+ try {
282
+ await forwarder.start();
283
+ const ws = new ws_1.WebSocket(`ws://[::1]:${forwarder.port()}/_next/webpack-hmr`);
284
+ const reply = await new Promise((resolve, reject) => {
285
+ ws.on('open', () => ws.send('hi'));
286
+ ws.on('message', (m) => resolve(m.toString()));
287
+ ws.on('error', reject);
288
+ });
289
+ ws.close();
290
+ expect(reply).toBe('echo:hi');
291
+ }
292
+ finally {
293
+ forwarder.stop();
294
+ for (const c of wss.clients)
295
+ c.terminate();
296
+ wss.close();
297
+ await new Promise((r) => server.close(() => r()));
298
+ }
299
+ });
300
+ });
301
+ describe('createPreviewForwarder — gateway name resolved and pinned (tp-06)', () => {
302
+ it('resolves a .internal name once at start and admits only its addresses', async () => {
303
+ const preview = await startUpstream();
304
+ const lookups = [];
305
+ let peer = GATEWAY_6PN;
306
+ const forwarder = (0, previewForwarder_1.createPreviewForwarder)({
307
+ listenHost: '::1',
308
+ listenPort: 0,
309
+ targetPort: preview.port,
310
+ allowed: ['hiveop-preview-gateway.internal'],
311
+ lookup: async (name) => {
312
+ lookups.push(name);
313
+ return ['fdaa:0:1234:a7b:ce1:2:8a2e:2'];
314
+ },
315
+ refreshMs: 60_000,
316
+ setIntervalFn: () => 1,
317
+ clearIntervalFn: () => { },
318
+ remoteAddressOf: () => peer,
319
+ });
320
+ try {
321
+ await forwarder.start();
322
+ expect(forwarder.allowedAddresses()).toEqual([(0, previewForwarder_1.canonicalAddress)(GATEWAY_6PN)]);
323
+ const a = await connect('::1', forwarder.port());
324
+ expect(await readAll(a)).toContain('hello from preview');
325
+ a.destroy();
326
+ peer = OTHER_TENANT_6PN;
327
+ const b = await connect('::1', forwarder.port());
328
+ expect(await readAll(b)).toBe('');
329
+ expect(lookups).toEqual(['hiveop-preview-gateway.internal']); // pinned, not per connection
330
+ }
331
+ finally {
332
+ forwarder.stop();
333
+ await preview.close();
334
+ }
335
+ });
336
+ it('fails closed when the name does not resolve', async () => {
337
+ const preview = await startUpstream();
338
+ const forwarder = (0, previewForwarder_1.createPreviewForwarder)({
339
+ listenHost: '::1',
340
+ listenPort: 0,
341
+ targetPort: preview.port,
342
+ allowed: ['hiveop-preview-gateway.internal'],
343
+ lookup: async () => {
344
+ throw new Error('ENOTFOUND');
345
+ },
346
+ setIntervalFn: () => 1,
347
+ clearIntervalFn: () => { },
348
+ remoteAddressOf: () => GATEWAY_6PN,
349
+ });
350
+ try {
351
+ await forwarder.start();
352
+ expect(forwarder.allowedAddresses()).toEqual([]);
353
+ const c = await connect('::1', forwarder.port());
354
+ expect(await readAll(c)).toBe('');
355
+ expect(preview.connections()).toBe(0);
356
+ }
357
+ finally {
358
+ forwarder.stop();
359
+ await preview.close();
360
+ }
361
+ });
362
+ it('a refresh that fails keeps the pinned addresses', async () => {
363
+ let refresh = null;
364
+ let fail = false;
365
+ const forwarder = (0, previewForwarder_1.createPreviewForwarder)({
366
+ listenHost: '::1',
367
+ listenPort: 0,
368
+ targetPort: 1,
369
+ allowed: ['hiveop-preview-gateway.internal'],
370
+ lookup: async () => {
371
+ if (fail)
372
+ throw new Error('SERVFAIL');
373
+ return [GATEWAY_6PN];
374
+ },
375
+ setIntervalFn: (cb) => {
376
+ refresh = cb;
377
+ return 1;
378
+ },
379
+ clearIntervalFn: () => { },
380
+ });
381
+ try {
382
+ await forwarder.start();
383
+ fail = true;
384
+ refresh();
385
+ await tick();
386
+ expect(forwarder.allowedAddresses()).toEqual([(0, previewForwarder_1.canonicalAddress)(GATEWAY_6PN)]);
387
+ }
388
+ finally {
389
+ forwarder.stop();
390
+ }
391
+ });
392
+ });
393
+ describe('buildPreviewForwarder — hosted lane only (tp-06, ADR-0014)', () => {
394
+ const FLY_HOSTS = `127.0.0.1\tlocalhost\n${GATEWAY_6PN.replace(':2', ':9')}\tfly-local-6pn\n`;
395
+ function spyNet() {
396
+ const calls = { createServer: 0 };
397
+ const n = {
398
+ createServer: ((...args) => {
399
+ calls.createServer++;
400
+ return net.createServer(...args);
401
+ }),
402
+ createConnection: net.createConnection,
403
+ };
404
+ return { calls, net: n };
405
+ }
406
+ it('the local bridge (no fly-local-6pn) never opens the listener', async () => {
407
+ const spy = spyNet();
408
+ const fwd = (0, previewForwarder_1.buildPreviewForwarder)({
409
+ env: { HT_PREVIEW_GATEWAY: 'hiveop-preview-gateway.internal' },
410
+ readHosts: () => '127.0.0.1\tlocalhost\n::1\tlocalhost\n',
411
+ net: spy.net,
412
+ log: () => { },
413
+ });
414
+ await fwd.start();
415
+ fwd.stop();
416
+ expect(fwd.port()).toBeNull();
417
+ expect(spy.calls.createServer).toBe(0);
418
+ });
419
+ it('an unreadable /etc/hosts is the local lane too', async () => {
420
+ const spy = spyNet();
421
+ const fwd = (0, previewForwarder_1.buildPreviewForwarder)({
422
+ env: { HT_PREVIEW_GATEWAY: 'hiveop-preview-gateway.internal' },
423
+ readHosts: () => {
424
+ throw new Error('ENOENT');
425
+ },
426
+ net: spy.net,
427
+ log: () => { },
428
+ });
429
+ await fwd.start();
430
+ expect(spy.calls.createServer).toBe(0);
431
+ });
432
+ it('stays off on a hosted machine until a gateway is configured (default-off)', async () => {
433
+ const spy = spyNet();
434
+ const fwd = (0, previewForwarder_1.buildPreviewForwarder)({ env: {}, readHosts: () => FLY_HOSTS, net: spy.net, log: () => { } });
435
+ await fwd.start();
436
+ expect(spy.calls.createServer).toBe(0);
437
+ });
438
+ it('on a hosted machine with a gateway, binds the 6PN address at the forwarder port', async () => {
439
+ const listens = [];
440
+ const fakeServer = {
441
+ on: () => fakeServer,
442
+ once: () => fakeServer,
443
+ listen: (port, host, cb) => {
444
+ listens.push({ port, host });
445
+ cb?.();
446
+ return fakeServer;
447
+ },
448
+ address: () => ({ port: 4801 }),
449
+ close: () => fakeServer,
450
+ };
451
+ const fwd = (0, previewForwarder_1.buildPreviewForwarder)({
452
+ env: { HT_PREVIEW_GATEWAY: GATEWAY_6PN },
453
+ readHosts: () => FLY_HOSTS,
454
+ net: {
455
+ createServer: (() => fakeServer),
456
+ createConnection: net.createConnection,
457
+ },
458
+ log: () => { },
459
+ });
460
+ await fwd.start();
461
+ fwd.stop();
462
+ expect(listens).toEqual([{ port: previewForwarder_1.PREVIEW_FORWARDER_PORT_DEFAULT, host: GATEWAY_6PN.replace(':2', ':9') }]);
463
+ expect(fwd.allowedAddresses()).toEqual([(0, previewForwarder_1.canonicalAddress)(GATEWAY_6PN)]);
464
+ });
465
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * tp-05 — the preview relay frames, driven through the REAL relay client
5
+ * against a fake relay (wire behaviour, not internals).
6
+ *
7
+ * - Local lane (a laptop's crew-relay dial, onlyRelayOwnedSessions) answers
8
+ * every preview frame `unsupported / local_lane` — ADR-0014 scopes it out.
9
+ * - Hosted lane: start/update answer `accepted` at once then `done`; stop
10
+ * answers `done`; logs answer the tail. requestId is echoed every time.
11
+ */
12
+ const ws_1 = require("ws");
13
+ const relayClient_1 = require("../bridge/relayClient");
14
+ const sessionManager_1 = require("../bridge/sessionManager");
15
+ class FakePty {
16
+ opts;
17
+ constructor(opts) {
18
+ this.opts = opts;
19
+ }
20
+ write() { }
21
+ resize() { }
22
+ kill() { }
23
+ onData() { }
24
+ onExit() { }
25
+ }
26
+ const makeFactory = () => ({ spawn: (o) => new FakePty(o) });
27
+ function frameQueue(ws) {
28
+ const frames = [];
29
+ const waiters = [];
30
+ ws.on('message', (raw) => {
31
+ const f = JSON.parse(String(raw));
32
+ const w = waiters.shift();
33
+ if (w)
34
+ w(f);
35
+ else
36
+ frames.push(f);
37
+ });
38
+ return { next: () => (frames.length > 0 ? Promise.resolve(frames.shift()) : new Promise((r) => waiters.push(r))) };
39
+ }
40
+ function startFakeRelay() {
41
+ return new Promise((resolve) => {
42
+ const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
43
+ const queue = [];
44
+ const waiters = [];
45
+ wss.on('connection', (ws) => { const w = waiters.shift(); if (w)
46
+ w(ws);
47
+ else
48
+ queue.push(ws); });
49
+ wss.on('listening', () => {
50
+ const addr = wss.address();
51
+ resolve({ wss, port: typeof addr === 'object' && addr ? addr.port : 0, nextConnection: () => (queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiters.push(r))) });
52
+ });
53
+ });
54
+ }
55
+ function closeRelay(wss) {
56
+ return new Promise((resolve, reject) => {
57
+ for (const c of wss.clients)
58
+ c.terminate();
59
+ wss.close((err) => (err ? reject(err) : resolve()));
60
+ });
61
+ }
62
+ const FRAMES = [
63
+ { t: 'preview-start', requestId: 'r1', workspaceId: 'ws-1', repoUrl: 'https://github.com/o/r.git' },
64
+ { t: 'preview-update', requestId: 'r2', workspaceId: 'ws-1', repoUrl: 'https://github.com/o/r.git' },
65
+ { t: 'preview-stop', requestId: 'r3', workspaceId: 'ws-1' },
66
+ { t: 'preview-logs', requestId: 'r4', workspaceId: 'ws-1' },
67
+ ];
68
+ function fakeRunner(seen) {
69
+ return {
70
+ status: (ws) => ({ workspaceId: ws, status: 'stopped', mode: 'built' }),
71
+ start: async (req) => { seen.push(['start', req]); return { workspaceId: req.workspaceId, status: 'running', mode: 'built', sha: 'a'.repeat(40), runningSha: 'a'.repeat(40), builtSha: 'a'.repeat(40) }; },
72
+ update: async (req) => { seen.push(['update', req]); return { workspaceId: req.workspaceId, status: 'build_failed', mode: 'built', sha: 'b'.repeat(40), runningSha: 'a'.repeat(40), builtSha: 'a'.repeat(40) }; },
73
+ stop: async (req) => { seen.push(['stop', req]); return { workspaceId: req.workspaceId, status: 'stopped', mode: 'built' }; },
74
+ logs: () => ({ log: 'built ok', truncated: false }),
75
+ };
76
+ }
77
+ describe('relay client — Team preview frames (tp-05)', () => {
78
+ it('preview frames are workspace-addressed, not session-addressed', () => {
79
+ for (const f of FRAMES)
80
+ expect(relayClient_1.SESSION_ADDRESSED_FRAMES.has(f.t)).toBe(false);
81
+ });
82
+ it('LOCAL lane (ADR-0014): every preview frame is answered unsupported / local_lane', async () => {
83
+ const relay = await startFakeRelay();
84
+ const seen = [];
85
+ const handle = (0, relayClient_1.startRelayClient)({
86
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
87
+ relayToken: 'tok',
88
+ sessions: new sessionManager_1.SessionManager(makeFactory()),
89
+ onlyRelayOwnedSessions: true,
90
+ // Even a wired runner is never reached from the local lane.
91
+ preview: fakeRunner(seen),
92
+ });
93
+ try {
94
+ const ws = await relay.nextConnection();
95
+ const q = frameQueue(ws);
96
+ await q.next(); // auth
97
+ for (const f of FRAMES) {
98
+ ws.send(JSON.stringify(f));
99
+ expect(await q.next()).toMatchObject({
100
+ t: f.t === 'preview-logs' ? 'preview-logs-result' : 'preview-result',
101
+ requestId: f.requestId,
102
+ workspaceId: 'ws-1',
103
+ ok: false,
104
+ error: 'unsupported',
105
+ reason: 'local_lane',
106
+ });
107
+ }
108
+ expect(seen).toEqual([]);
109
+ }
110
+ finally {
111
+ handle.close();
112
+ await closeRelay(relay.wss);
113
+ }
114
+ });
115
+ it('HOSTED lane: start → accepted then done; update reports build_failed; stop; logs', async () => {
116
+ const relay = await startFakeRelay();
117
+ const seen = [];
118
+ const handle = (0, relayClient_1.startRelayClient)({
119
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
120
+ relayToken: 'tok',
121
+ sessions: new sessionManager_1.SessionManager(makeFactory()),
122
+ preview: fakeRunner(seen),
123
+ });
124
+ try {
125
+ const ws = await relay.nextConnection();
126
+ const q = frameQueue(ws);
127
+ await q.next(); // auth
128
+ ws.send(JSON.stringify({ ...FRAMES[0], mode: 'dev', gitCredential: { token: 't', repoFullName: 'o/r' }, branch: 'evil' }));
129
+ expect(await q.next()).toMatchObject({ t: 'preview-result', requestId: 'r1', phase: 'accepted', status: 'building' });
130
+ expect(await q.next()).toMatchObject({ t: 'preview-result', requestId: 'r1', phase: 'done', ok: true, status: 'running', runningSha: 'a'.repeat(40) });
131
+ // No branch ever reaches the runner (ADR-0014).
132
+ expect(seen[0]).toEqual(['start', { workspaceId: 'ws-1', repoUrl: 'https://github.com/o/r.git', gitCredential: { token: 't', repoFullName: 'o/r' }, mode: 'dev' }]);
133
+ ws.send(JSON.stringify(FRAMES[1]));
134
+ expect(await q.next()).toMatchObject({ requestId: 'r2', phase: 'accepted' });
135
+ expect(await q.next()).toMatchObject({ requestId: 'r2', phase: 'done', ok: false, status: 'build_failed', sha: 'b'.repeat(40), runningSha: 'a'.repeat(40) });
136
+ ws.send(JSON.stringify(FRAMES[2]));
137
+ expect(await q.next()).toMatchObject({ requestId: 'r3', phase: 'done', ok: true, status: 'stopped' });
138
+ ws.send(JSON.stringify(FRAMES[3]));
139
+ expect(await q.next()).toEqual({ t: 'preview-logs-result', requestId: 'r4', workspaceId: 'ws-1', ok: true, log: 'built ok', truncated: false });
140
+ }
141
+ finally {
142
+ handle.close();
143
+ await closeRelay(relay.wss);
144
+ }
145
+ });
146
+ it('a hosted bridge with no runner answers unsupported / bridge-cannot-preview (no timeout)', async () => {
147
+ const relay = await startFakeRelay();
148
+ const handle = (0, relayClient_1.startRelayClient)({
149
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
150
+ relayToken: 'tok',
151
+ sessions: new sessionManager_1.SessionManager(makeFactory()),
152
+ });
153
+ try {
154
+ const ws = await relay.nextConnection();
155
+ const q = frameQueue(ws);
156
+ await q.next();
157
+ ws.send(JSON.stringify(FRAMES[0]));
158
+ expect(await q.next()).toMatchObject({ requestId: 'r1', ok: false, error: 'unsupported', reason: 'bridge-cannot-preview' });
159
+ }
160
+ finally {
161
+ handle.close();
162
+ await closeRelay(relay.wss);
163
+ }
164
+ });
165
+ });