@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.
- package/dist/__tests__/bridge-activity-parser.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-parser.test.js +0 -0
- package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-tap.test.js +178 -0
- package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-api-client.test.js +69 -0
- package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-driver.test.js +379 -0
- package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-frames.test.js +166 -0
- package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-idle.test.js +259 -0
- package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-relay.test.js +670 -0
- package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
- package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-cdp-client.test.js +133 -0
- package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
- package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
- package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
- package/dist/__tests__/bridge-config-mcp.test.js +4 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
- package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-writer.test.js +161 -0
- package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
- package/dist/__tests__/bridge-device-registration.test.js +33 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
- package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker.test.js +66 -0
- package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-registry.test.js +107 -0
- package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
- package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
- package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-add-core.test.js +48 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
- package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
- package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
- package/dist/__tests__/bridge-login-status.test.js +60 -0
- package/dist/__tests__/bridge-relay-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-client.test.js +283 -0
- package/dist/__tests__/bridge-relay-mode.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-mode.test.js +33 -0
- package/dist/__tests__/bridge-relay-pucc14.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
- package/dist/__tests__/bridge-server.test.js +22 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
- package/dist/__tests__/devices.test.d.ts +1 -0
- package/dist/__tests__/devices.test.js +66 -0
- package/dist/__tests__/local.test.d.ts +1 -0
- package/dist/__tests__/local.test.js +147 -0
- package/dist/bridge/activityParser.d.ts +53 -0
- package/dist/bridge/activityParser.js +202 -0
- package/dist/bridge/apiClient.d.ts +56 -0
- package/dist/bridge/apiClient.js +50 -0
- package/dist/bridge/browserDriver.d.ts +98 -0
- package/dist/bridge/browserDriver.js +265 -0
- package/dist/bridge/browserFrames.d.ts +182 -0
- package/dist/bridge/browserFrames.js +179 -0
- package/dist/bridge/browserIdle.d.ts +75 -0
- package/dist/bridge/browserIdle.js +120 -0
- package/dist/bridge/bundleInstaller.d.ts +31 -0
- package/dist/bridge/bundleInstaller.js +127 -0
- package/dist/bridge/cdpClient.d.ts +47 -0
- package/dist/bridge/cdpClient.js +102 -0
- package/dist/bridge/chromiumLauncher.d.ts +45 -0
- package/dist/bridge/chromiumLauncher.js +113 -0
- package/dist/bridge/config.d.ts +3 -0
- package/dist/bridge/contextWriter.d.ts +31 -0
- package/dist/bridge/contextWriter.js +139 -0
- package/dist/bridge/deviceRegistration.d.ts +28 -0
- package/dist/bridge/deviceRegistration.js +19 -0
- package/dist/bridge/folderPicker.d.ts +34 -0
- package/dist/bridge/folderPicker.js +36 -0
- package/dist/bridge/folderPickerRunner.d.ts +3 -0
- package/dist/bridge/folderPickerRunner.js +25 -0
- package/dist/bridge/folderRegistry.d.ts +19 -0
- package/dist/bridge/folderRegistry.js +100 -0
- package/dist/bridge/framePacer.d.ts +61 -0
- package/dist/bridge/framePacer.js +79 -0
- package/dist/bridge/localAdd.d.ts +28 -0
- package/dist/bridge/localAdd.js +67 -0
- package/dist/bridge/localPathValidation.d.ts +20 -0
- package/dist/bridge/localPathValidation.js +58 -0
- package/dist/bridge/localPathValidationFs.d.ts +2 -0
- package/dist/bridge/localPathValidationFs.js +98 -0
- package/dist/bridge/loginStatus.d.ts +38 -0
- package/dist/bridge/loginStatus.js +56 -0
- package/dist/bridge/realPty.js +6 -2
- package/dist/bridge/relayClient.d.ts +166 -0
- package/dist/bridge/relayClient.js +517 -0
- package/dist/bridge/repoCheckout.d.ts +31 -0
- package/dist/bridge/repoCheckout.js +18 -0
- package/dist/bridge/server.d.ts +33 -0
- package/dist/bridge/server.js +98 -10
- package/dist/bridge/sessionManager.d.ts +18 -0
- package/dist/bridge/sessionManager.js +15 -1
- package/dist/commands/bridge.d.ts +21 -0
- package/dist/commands/bridge.js +194 -1
- package/dist/commands/devices.d.ts +9 -0
- package/dist/commands/devices.js +53 -0
- package/dist/commands/local.d.ts +9 -0
- package/dist/commands/local.js +141 -0
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* pucc-int-04 — crew `.claude`-bundle installer (container writer).
|
|
5
|
+
*
|
|
6
|
+
* The bridge receives a forged crew bundle (Agent-Builder Forge `format=map`
|
|
7
|
+
* FileMap) over a relay frame and writes it into the agent's workspace checkout
|
|
8
|
+
* at /data/repos/<workspaceId>. Claude Code then picks up the subagents/skills/
|
|
9
|
+
* CLAUDE.md natively on its next run (ADR-0032) — no crewExecutionEngine.
|
|
10
|
+
*
|
|
11
|
+
* These tests exercise the PURE writer (fake fs) so path-safety + idempotency
|
|
12
|
+
* are proven without touching disk.
|
|
13
|
+
*/
|
|
14
|
+
const bundleInstaller_1 = require("../bridge/bundleInstaller");
|
|
15
|
+
const BASE = '/data/repos';
|
|
16
|
+
const makeFs = () => {
|
|
17
|
+
const files = new Map();
|
|
18
|
+
const dirs = new Set();
|
|
19
|
+
return {
|
|
20
|
+
files,
|
|
21
|
+
dirs,
|
|
22
|
+
deps: {
|
|
23
|
+
reposBaseDir: BASE,
|
|
24
|
+
mkdirp: (dir) => { dirs.add(dir); },
|
|
25
|
+
writeFile: (p, content) => { files.set(p, content); },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
describe('installBundle — writes the bundle into the workspace checkout', () => {
|
|
30
|
+
it('writes each FileMap entry under /data/repos/<workspaceId>/<path>', () => {
|
|
31
|
+
const { files, deps } = makeFs();
|
|
32
|
+
const result = (0, bundleInstaller_1.installBundle)({
|
|
33
|
+
workspaceId: 'ws-7',
|
|
34
|
+
files: {
|
|
35
|
+
'CLAUDE.md': '# crew\n',
|
|
36
|
+
'.claude/agents/reviewer.md': 'reviewer subagent',
|
|
37
|
+
'.claude/skills/run-crew/SKILL.md': 'run the crew',
|
|
38
|
+
'.claude/handoff/PROTOCOL.md': 'blackboard protocol',
|
|
39
|
+
},
|
|
40
|
+
}, deps);
|
|
41
|
+
expect(result.workspaceRoot).toBe('/data/repos/ws-7');
|
|
42
|
+
expect(files.get('/data/repos/ws-7/CLAUDE.md')).toBe('# crew\n');
|
|
43
|
+
expect(files.get('/data/repos/ws-7/.claude/agents/reviewer.md')).toBe('reviewer subagent');
|
|
44
|
+
expect(files.get('/data/repos/ws-7/.claude/skills/run-crew/SKILL.md')).toBe('run the crew');
|
|
45
|
+
expect(files.get('/data/repos/ws-7/.claude/handoff/PROTOCOL.md')).toBe('blackboard protocol');
|
|
46
|
+
expect(result.written.sort()).toEqual([
|
|
47
|
+
'.claude/agents/reviewer.md',
|
|
48
|
+
'.claude/handoff/PROTOCOL.md',
|
|
49
|
+
'.claude/skills/run-crew/SKILL.md',
|
|
50
|
+
'CLAUDE.md',
|
|
51
|
+
]);
|
|
52
|
+
expect(result.skipped).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
it('is idempotent — re-install overwrites managed files with identical result', () => {
|
|
55
|
+
const { files, deps } = makeFs();
|
|
56
|
+
const bundle = {
|
|
57
|
+
workspaceId: 'ws-7',
|
|
58
|
+
files: { 'CLAUDE.md': 'v1', '.claude/agents/a.md': 'agent-a' },
|
|
59
|
+
};
|
|
60
|
+
const first = (0, bundleInstaller_1.installBundle)(bundle, deps);
|
|
61
|
+
const firstSnapshot = new Map(files);
|
|
62
|
+
const second = (0, bundleInstaller_1.installBundle)(bundle, deps);
|
|
63
|
+
expect(second.written).toEqual(first.written);
|
|
64
|
+
expect(new Map(files)).toEqual(firstSnapshot); // byte-identical state
|
|
65
|
+
// A re-send with updated content simply overwrites the managed file.
|
|
66
|
+
(0, bundleInstaller_1.installBundle)({ workspaceId: 'ws-7', files: { 'CLAUDE.md': 'v2' } }, deps);
|
|
67
|
+
expect(files.get('/data/repos/ws-7/CLAUDE.md')).toBe('v2');
|
|
68
|
+
});
|
|
69
|
+
it('leaves pre-existing user files alone (only writes bundle entries)', () => {
|
|
70
|
+
const { files, deps } = makeFs();
|
|
71
|
+
files.set('/data/repos/ws-7/src/index.ts', 'user code'); // a user file already in the checkout
|
|
72
|
+
(0, bundleInstaller_1.installBundle)({ workspaceId: 'ws-7', files: { 'CLAUDE.md': 'crew' } }, deps);
|
|
73
|
+
expect(files.get('/data/repos/ws-7/src/index.ts')).toBe('user code'); // untouched
|
|
74
|
+
expect(files.get('/data/repos/ws-7/CLAUDE.md')).toBe('crew');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('installBundle — path safety (writes nothing outside the workspace)', () => {
|
|
78
|
+
const traversals = [
|
|
79
|
+
['parent-relative', '../evil.md'],
|
|
80
|
+
['deep parent-relative', '.claude/../../../../etc/cron.d/evil'],
|
|
81
|
+
['absolute path', '/etc/passwd'],
|
|
82
|
+
['absolute into shared login', '/data/.claude/settings.json'],
|
|
83
|
+
['sibling workspace', '../ws-OTHER/.claude/agents/steal.md'],
|
|
84
|
+
['null byte', '.claude/x\0.md'],
|
|
85
|
+
];
|
|
86
|
+
it.each(traversals)('rejects %s and writes nothing', (_label, badPath) => {
|
|
87
|
+
const { files, dirs, deps } = makeFs();
|
|
88
|
+
const result = (0, bundleInstaller_1.installBundle)({ workspaceId: 'ws-7', files: { [badPath]: 'malicious' } }, deps);
|
|
89
|
+
expect(files.size).toBe(0);
|
|
90
|
+
expect(dirs.size).toBe(0);
|
|
91
|
+
expect(result.written).toEqual([]);
|
|
92
|
+
expect(result.skipped).toEqual([{ path: badPath, reason: 'unsafe' }]);
|
|
93
|
+
});
|
|
94
|
+
it('never touches the shared /data/.claude login even via a crafted workspaceId', () => {
|
|
95
|
+
const { files, deps } = makeFs();
|
|
96
|
+
// A workspaceId that tries to climb to the shared login must be refused wholesale.
|
|
97
|
+
const result = (0, bundleInstaller_1.installBundle)({ workspaceId: '../..', files: { '.claude/settings.json': 'x' } }, deps);
|
|
98
|
+
expect(result.workspaceRoot).toBeNull();
|
|
99
|
+
expect(files.size).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
it('rejects a workspaceId containing a slash (path injection)', () => {
|
|
102
|
+
const { files, deps } = makeFs();
|
|
103
|
+
const result = (0, bundleInstaller_1.installBundle)({ workspaceId: 'ws-7/../ws-8', files: { 'CLAUDE.md': 'x' } }, deps);
|
|
104
|
+
expect(result.workspaceRoot).toBeNull();
|
|
105
|
+
expect(files.size).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
it('skips safe-but-unmanaged paths (only .claude/** + CLAUDE.md are managed)', () => {
|
|
108
|
+
const { files, deps } = makeFs();
|
|
109
|
+
const result = (0, bundleInstaller_1.installBundle)({
|
|
110
|
+
workspaceId: 'ws-7',
|
|
111
|
+
files: {
|
|
112
|
+
'CLAUDE.md': 'managed',
|
|
113
|
+
'.claude/agents/a.md': 'managed',
|
|
114
|
+
'install.mjs': 'forge installer — not needed; bridge does the install',
|
|
115
|
+
'package.json': 'do not clobber',
|
|
116
|
+
},
|
|
117
|
+
}, deps);
|
|
118
|
+
expect(result.written.sort()).toEqual(['.claude/agents/a.md', 'CLAUDE.md']);
|
|
119
|
+
expect(result.skipped).toEqual(expect.arrayContaining([
|
|
120
|
+
{ path: 'install.mjs', reason: 'unmanaged' },
|
|
121
|
+
{ path: 'package.json', reason: 'unmanaged' },
|
|
122
|
+
]));
|
|
123
|
+
expect(files.has('/data/repos/ws-7/install.mjs')).toBe(false);
|
|
124
|
+
expect(files.has('/data/repos/ws-7/package.json')).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* cbs-02 — hand-rolled CDP JSON-RPC client (id/method/params + events) over
|
|
5
|
+
* an injectable transport. No real socket: tests drive a FakeTransport that
|
|
6
|
+
* implements the same on('open'|'message'|'close'|'error') surface the real
|
|
7
|
+
* `ws`-backed transport will use, so the request/response and event-fanout
|
|
8
|
+
* logic is verified without Chromium or a network socket.
|
|
9
|
+
*/
|
|
10
|
+
const cdpClient_1 = require("../bridge/cdpClient");
|
|
11
|
+
class FakeTransport {
|
|
12
|
+
sent = [];
|
|
13
|
+
closed = false;
|
|
14
|
+
handlers = {};
|
|
15
|
+
on(event, cb) {
|
|
16
|
+
(this.handlers[event] ??= []).push(cb);
|
|
17
|
+
}
|
|
18
|
+
send(data) {
|
|
19
|
+
this.sent.push(JSON.parse(data));
|
|
20
|
+
}
|
|
21
|
+
close() {
|
|
22
|
+
this.closed = true;
|
|
23
|
+
}
|
|
24
|
+
// test-only helpers to drive the fake server side
|
|
25
|
+
emitMessage(obj) {
|
|
26
|
+
for (const cb of this.handlers.message ?? [])
|
|
27
|
+
cb(JSON.stringify(obj));
|
|
28
|
+
}
|
|
29
|
+
emitRawMessage(raw) {
|
|
30
|
+
for (const cb of this.handlers.message ?? [])
|
|
31
|
+
cb(raw);
|
|
32
|
+
}
|
|
33
|
+
emitClose(info) {
|
|
34
|
+
for (const cb of this.handlers.close ?? [])
|
|
35
|
+
cb(info);
|
|
36
|
+
}
|
|
37
|
+
replyTo(id, result) {
|
|
38
|
+
this.emitMessage({ id, result });
|
|
39
|
+
}
|
|
40
|
+
replyError(id, message) {
|
|
41
|
+
this.emitMessage({ id, error: { message } });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
describe('createCdpClient (cbs-02)', () => {
|
|
45
|
+
it('sends a request as {id, method, params} and resolves on the matching {id, result}', async () => {
|
|
46
|
+
const transport = new FakeTransport();
|
|
47
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
48
|
+
const pending = cdp.send('Target.getTargets', {});
|
|
49
|
+
expect(transport.sent).toEqual([{ id: 1, method: 'Target.getTargets', params: {} }]);
|
|
50
|
+
transport.replyTo(1, { targetInfos: [] });
|
|
51
|
+
await expect(pending).resolves.toEqual({ targetInfos: [] });
|
|
52
|
+
});
|
|
53
|
+
it('stamps sessionId on the request envelope when provided (flattened target session)', async () => {
|
|
54
|
+
const transport = new FakeTransport();
|
|
55
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
56
|
+
const pending = cdp.send('Page.enable', {}, 'sess-abc');
|
|
57
|
+
expect(transport.sent).toEqual([{ id: 1, method: 'Page.enable', params: {}, sessionId: 'sess-abc' }]);
|
|
58
|
+
transport.replyTo(1, {});
|
|
59
|
+
await pending;
|
|
60
|
+
});
|
|
61
|
+
it('assigns monotonically increasing ids across calls and resolves each independently', async () => {
|
|
62
|
+
const transport = new FakeTransport();
|
|
63
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
64
|
+
const p1 = cdp.send('A.one');
|
|
65
|
+
const p2 = cdp.send('A.two');
|
|
66
|
+
expect(transport.sent.map((m) => m.id)).toEqual([1, 2]);
|
|
67
|
+
transport.replyTo(2, { order: 'second-first' });
|
|
68
|
+
transport.replyTo(1, { order: 'first' });
|
|
69
|
+
await expect(p2).resolves.toEqual({ order: 'second-first' });
|
|
70
|
+
await expect(p1).resolves.toEqual({ order: 'first' });
|
|
71
|
+
});
|
|
72
|
+
it('rejects the pending promise on a {id, error} response', async () => {
|
|
73
|
+
const transport = new FakeTransport();
|
|
74
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
75
|
+
const pending = cdp.send('Target.attachToTarget', { targetId: 'nope' });
|
|
76
|
+
transport.replyError(1, 'No target with given id found');
|
|
77
|
+
await expect(pending).rejects.toThrow('No target with given id found');
|
|
78
|
+
});
|
|
79
|
+
it('dispatches {method, params} messages (no id) to onEvent subscribers by method name', () => {
|
|
80
|
+
const transport = new FakeTransport();
|
|
81
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
82
|
+
const received = [];
|
|
83
|
+
cdp.onEvent('Page.screencastFrame', (params, sessionId) => received.push({ params, sessionId }));
|
|
84
|
+
transport.emitMessage({ method: 'Page.screencastFrame', params: { data: 'abc', sessionId: 7 }, sessionId: 'target-sess-1' });
|
|
85
|
+
transport.emitMessage({ method: 'Target.targetInfoChanged', params: { targetInfo: {} } });
|
|
86
|
+
expect(received).toEqual([{ params: { data: 'abc', sessionId: 7 }, sessionId: 'target-sess-1' }]);
|
|
87
|
+
});
|
|
88
|
+
it('supports multiple subscribers to the same event method and unsubscribing via the returned function', () => {
|
|
89
|
+
const transport = new FakeTransport();
|
|
90
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
91
|
+
const a = [];
|
|
92
|
+
const b = [];
|
|
93
|
+
cdp.onEvent('Foo.bar', (p) => a.push(p));
|
|
94
|
+
const unsubB = cdp.onEvent('Foo.bar', (p) => b.push(p));
|
|
95
|
+
transport.emitMessage({ method: 'Foo.bar', params: { n: 1 } });
|
|
96
|
+
unsubB();
|
|
97
|
+
transport.emitMessage({ method: 'Foo.bar', params: { n: 2 } });
|
|
98
|
+
expect(a).toEqual([{ n: 1 }, { n: 2 }]);
|
|
99
|
+
expect(b).toEqual([{ n: 1 }]);
|
|
100
|
+
});
|
|
101
|
+
it('ignores malformed (non-JSON) messages instead of throwing', () => {
|
|
102
|
+
const transport = new FakeTransport();
|
|
103
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
104
|
+
cdp.onEvent('Foo.bar', () => {
|
|
105
|
+
throw new Error('should not be called');
|
|
106
|
+
});
|
|
107
|
+
expect(() => transport.emitRawMessage('not json')).not.toThrow();
|
|
108
|
+
});
|
|
109
|
+
it('on transport close: rejects all pending requests and notifies onClose listeners', async () => {
|
|
110
|
+
const transport = new FakeTransport();
|
|
111
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
112
|
+
const closes = [];
|
|
113
|
+
cdp.onClose((info) => closes.push(info));
|
|
114
|
+
const pending = cdp.send('Target.getTargets');
|
|
115
|
+
transport.emitClose({ code: 1006, reason: 'abnormal' });
|
|
116
|
+
await expect(pending).rejects.toThrow();
|
|
117
|
+
expect(closes).toEqual([{ code: 1006, reason: 'abnormal' }]);
|
|
118
|
+
});
|
|
119
|
+
it('a send() issued after close rejects immediately without touching the transport', async () => {
|
|
120
|
+
const transport = new FakeTransport();
|
|
121
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
122
|
+
transport.emitClose();
|
|
123
|
+
const sentBefore = transport.sent.length;
|
|
124
|
+
await expect(cdp.send('Page.reload')).rejects.toThrow();
|
|
125
|
+
expect(transport.sent.length).toBe(sentBefore);
|
|
126
|
+
});
|
|
127
|
+
it('close() delegates to the transport', () => {
|
|
128
|
+
const transport = new FakeTransport();
|
|
129
|
+
const cdp = (0, cdpClient_1.createCdpClient)(transport);
|
|
130
|
+
cdp.close();
|
|
131
|
+
expect(transport.closed).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* cbs-02 — Chromium child-process launcher.
|
|
5
|
+
*
|
|
6
|
+
* Spawns headless Chromium with the container-required flags (no-sandbox,
|
|
7
|
+
* persistent `/data` profile) and resolves the DevTools websocket URL
|
|
8
|
+
* parsed from stderr (standard headless behavior with
|
|
9
|
+
* `--remote-debugging-port=0`). Uses an injectable spawn function so tests
|
|
10
|
+
* never touch a real process (docs/prd/cloud-browser-session.md: "no
|
|
11
|
+
* puppeteer", container sandbox flags, profile on `/data`).
|
|
12
|
+
*/
|
|
13
|
+
const chromiumLauncher_1 = require("../bridge/chromiumLauncher");
|
|
14
|
+
class FakeChildProcess {
|
|
15
|
+
killed = false;
|
|
16
|
+
killSignal;
|
|
17
|
+
stderrHandlers = [];
|
|
18
|
+
exitHandlers = [];
|
|
19
|
+
errorHandlers = [];
|
|
20
|
+
stderr = {
|
|
21
|
+
on: (event, cb) => {
|
|
22
|
+
if (event === 'data')
|
|
23
|
+
this.stderrHandlers.push(cb);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
on(event, cb) {
|
|
27
|
+
if (event === 'exit')
|
|
28
|
+
this.exitHandlers.push(cb);
|
|
29
|
+
else if (event === 'error')
|
|
30
|
+
this.errorHandlers.push(cb);
|
|
31
|
+
}
|
|
32
|
+
kill(signal) {
|
|
33
|
+
this.killed = true;
|
|
34
|
+
this.killSignal = signal;
|
|
35
|
+
}
|
|
36
|
+
emitStderr(chunk) {
|
|
37
|
+
for (const cb of this.stderrHandlers)
|
|
38
|
+
cb(chunk);
|
|
39
|
+
}
|
|
40
|
+
emitExit(code, signal = null) {
|
|
41
|
+
for (const cb of this.exitHandlers)
|
|
42
|
+
cb(code, signal);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
describe('launchChromium (cbs-02)', () => {
|
|
46
|
+
it('spawns the binary with container-required flags and a persistent /data profile dir', () => {
|
|
47
|
+
const calls = [];
|
|
48
|
+
const fake = new FakeChildProcess();
|
|
49
|
+
(0, chromiumLauncher_1.launchChromium)({
|
|
50
|
+
spawnFn: (cmd, args) => {
|
|
51
|
+
calls.push({ cmd, args });
|
|
52
|
+
return fake;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
expect(calls).toHaveLength(1);
|
|
56
|
+
const { cmd, args } = calls[0];
|
|
57
|
+
expect(cmd).toBe('chromium'); // default binary
|
|
58
|
+
expect(args).toEqual(expect.arrayContaining(['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--hide-scrollbars', '--mute-audio']));
|
|
59
|
+
expect(args.some((a) => a.startsWith('--user-data-dir='))).toBe(true);
|
|
60
|
+
const profileArg = args.find((a) => a.startsWith('--user-data-dir='));
|
|
61
|
+
expect(profileArg).toBe('--user-data-dir=/data/chromium-profile');
|
|
62
|
+
expect(args.some((a) => a.startsWith('--remote-debugging-port='))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('honors an opts-overridable user-data-dir (for tests / non-container runs)', () => {
|
|
65
|
+
const fake = new FakeChildProcess();
|
|
66
|
+
let capturedArgs = [];
|
|
67
|
+
(0, chromiumLauncher_1.launchChromium)({
|
|
68
|
+
userDataDir: '/tmp/my-profile',
|
|
69
|
+
spawnFn: (_cmd, args) => {
|
|
70
|
+
capturedArgs = args;
|
|
71
|
+
return fake;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
expect(capturedArgs).toContain('--user-data-dir=/tmp/my-profile');
|
|
75
|
+
});
|
|
76
|
+
it('resolves the binary path from opts.chromiumPath, falling back to CHROMIUM_PATH env, then "chromium"', () => {
|
|
77
|
+
const fake = new FakeChildProcess();
|
|
78
|
+
let capturedCmd = '';
|
|
79
|
+
(0, chromiumLauncher_1.launchChromium)({
|
|
80
|
+
chromiumPath: '/usr/bin/my-chromium',
|
|
81
|
+
spawnFn: (cmd) => {
|
|
82
|
+
capturedCmd = cmd;
|
|
83
|
+
return fake;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
expect(capturedCmd).toBe('/usr/bin/my-chromium');
|
|
87
|
+
const prevEnv = process.env.CHROMIUM_PATH;
|
|
88
|
+
process.env.CHROMIUM_PATH = '/env/chromium';
|
|
89
|
+
try {
|
|
90
|
+
const fake2 = new FakeChildProcess();
|
|
91
|
+
let capturedCmd2 = '';
|
|
92
|
+
(0, chromiumLauncher_1.launchChromium)({
|
|
93
|
+
spawnFn: (cmd) => {
|
|
94
|
+
capturedCmd2 = cmd;
|
|
95
|
+
return fake2;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
expect(capturedCmd2).toBe('/env/chromium');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (prevEnv === undefined)
|
|
102
|
+
delete process.env.CHROMIUM_PATH;
|
|
103
|
+
else
|
|
104
|
+
process.env.CHROMIUM_PATH = prevEnv;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
it('includes a --window-size flag derived from opts width/height', () => {
|
|
108
|
+
const fake = new FakeChildProcess();
|
|
109
|
+
let capturedArgs = [];
|
|
110
|
+
(0, chromiumLauncher_1.launchChromium)({
|
|
111
|
+
width: 1024,
|
|
112
|
+
height: 768,
|
|
113
|
+
spawnFn: (_cmd, args) => {
|
|
114
|
+
capturedArgs = args;
|
|
115
|
+
return fake;
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
expect(capturedArgs).toContain('--window-size=1024,768');
|
|
119
|
+
});
|
|
120
|
+
it('parses the DevTools websocket URL out of stderr (standard headless behavior)', async () => {
|
|
121
|
+
const fake = new FakeChildProcess();
|
|
122
|
+
const { wsUrl } = (0, chromiumLauncher_1.launchChromium)({ spawnFn: () => fake });
|
|
123
|
+
fake.emitStderr('[1234:5678:INFO] some preamble\n');
|
|
124
|
+
fake.emitStderr('DevTools listening on ws://127.0.0.1:34567/devtools/browser/abc-123-def\n');
|
|
125
|
+
await expect(wsUrl).resolves.toBe('ws://127.0.0.1:34567/devtools/browser/abc-123-def');
|
|
126
|
+
});
|
|
127
|
+
it('assembles the ws URL even if it arrives split across multiple stderr chunks', async () => {
|
|
128
|
+
const fake = new FakeChildProcess();
|
|
129
|
+
const { wsUrl } = (0, chromiumLauncher_1.launchChromium)({ spawnFn: () => fake });
|
|
130
|
+
fake.emitStderr('DevTools listening on ws://127.0.0.1:9');
|
|
131
|
+
fake.emitStderr('222/devtools/browser/xyz\n');
|
|
132
|
+
await expect(wsUrl).resolves.toBe('ws://127.0.0.1:9222/devtools/browser/xyz');
|
|
133
|
+
});
|
|
134
|
+
it('rejects wsUrl if the process exits before printing a DevTools URL', async () => {
|
|
135
|
+
const fake = new FakeChildProcess();
|
|
136
|
+
const { wsUrl } = (0, chromiumLauncher_1.launchChromium)({ spawnFn: () => fake });
|
|
137
|
+
fake.emitExit(1, null);
|
|
138
|
+
await expect(wsUrl).rejects.toThrow();
|
|
139
|
+
});
|
|
140
|
+
it('exposes the raw child process handle so callers can kill() it', () => {
|
|
141
|
+
const fake = new FakeChildProcess();
|
|
142
|
+
const { proc } = (0, chromiumLauncher_1.launchChromium)({ spawnFn: () => fake });
|
|
143
|
+
expect(proc).toBe(fake);
|
|
144
|
+
proc.kill();
|
|
145
|
+
expect(fake.killed).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
// cbs-07: the container image does NOT bake /data/chromium-profile (it's
|
|
148
|
+
// under the /data volume mount, which the image can't pre-populate) — the
|
|
149
|
+
// launcher must create it on first launch, before spawning, so Chromium's
|
|
150
|
+
// very first run on a fresh volume doesn't depend on Chromium itself
|
|
151
|
+
// creating a nested dir under an as-yet-nonexistent parent.
|
|
152
|
+
it('creates the user-data-dir before spawning (not baked into the image)', () => {
|
|
153
|
+
const fake = new FakeChildProcess();
|
|
154
|
+
const mkdirCalls = [];
|
|
155
|
+
let spawnedAfterMkdir = false;
|
|
156
|
+
(0, chromiumLauncher_1.launchChromium)({
|
|
157
|
+
userDataDir: '/data/chromium-profile',
|
|
158
|
+
mkdirFn: (dir) => {
|
|
159
|
+
mkdirCalls.push(dir);
|
|
160
|
+
},
|
|
161
|
+
spawnFn: (_cmd, _args) => {
|
|
162
|
+
spawnedAfterMkdir = mkdirCalls.length === 1;
|
|
163
|
+
return fake;
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
expect(mkdirCalls).toEqual(['/data/chromium-profile']);
|
|
167
|
+
expect(spawnedAfterMkdir).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
it('defaults mkdirFn to a real recursive mkdirSync when not injected', () => {
|
|
170
|
+
// Exercises the real default (not just the injected fake) against a
|
|
171
|
+
// scratch dir so the default wiring itself is covered, not only the
|
|
172
|
+
// injection seam.
|
|
173
|
+
const fs = require('fs');
|
|
174
|
+
const os = require('os');
|
|
175
|
+
const path = require('path');
|
|
176
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'cbs07-launcher-'));
|
|
177
|
+
const profileDir = path.join(scratch, 'nested', 'chromium-profile');
|
|
178
|
+
const fake = new FakeChildProcess();
|
|
179
|
+
try {
|
|
180
|
+
(0, chromiumLauncher_1.launchChromium)({ userDataDir: profileDir, spawnFn: () => fake });
|
|
181
|
+
expect(fs.existsSync(profileDir)).toBe(true);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
it('never blocks the launch if mkdirFn throws (best-effort — Chromium can create its own profile dir)', () => {
|
|
188
|
+
const fake = new FakeChildProcess();
|
|
189
|
+
let spawned = false;
|
|
190
|
+
expect(() => (0, chromiumLauncher_1.launchChromium)({
|
|
191
|
+
userDataDir: '/data/chromium-profile',
|
|
192
|
+
mkdirFn: () => {
|
|
193
|
+
throw new Error('EACCES: permission denied');
|
|
194
|
+
},
|
|
195
|
+
spawnFn: () => {
|
|
196
|
+
spawned = true;
|
|
197
|
+
return fake;
|
|
198
|
+
},
|
|
199
|
+
})).not.toThrow();
|
|
200
|
+
expect(spawned).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* cbs-07 — pure resolver for the Chromium options the `wen-market bridge`
|
|
5
|
+
* (relay mode) command wires into `createBrowserDriver`. Mirrors
|
|
6
|
+
* resolveRelayMode's existing pattern in bridge.ts: the command shell itself
|
|
7
|
+
* is thin/untested, so the actual decision logic lives in a pure,
|
|
8
|
+
* env-in/options-out helper that's unit-tested directly instead of forcing a
|
|
9
|
+
* harness onto the commander action.
|
|
10
|
+
*/
|
|
11
|
+
const bridge_1 = require("../commands/bridge");
|
|
12
|
+
describe('buildBrowserDriverOptions (cbs-07)', () => {
|
|
13
|
+
it('defaults to the image convention: chromium on PATH, profile on the /data volume', () => {
|
|
14
|
+
expect((0, bridge_1.buildBrowserDriverOptions)({})).toEqual({
|
|
15
|
+
chromiumPath: 'chromium',
|
|
16
|
+
userDataDir: '/data/chromium-profile',
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it('honors CHROMIUM_PATH when set (the image bakes ENV CHROMIUM_PATH=/usr/bin/chromium)', () => {
|
|
20
|
+
expect((0, bridge_1.buildBrowserDriverOptions)({ CHROMIUM_PATH: '/usr/bin/chromium' })).toEqual({
|
|
21
|
+
chromiumPath: '/usr/bin/chromium',
|
|
22
|
+
userDataDir: '/data/chromium-profile',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it('honors an overridable CHROMIUM_USER_DATA_DIR (tests / non-container runs)', () => {
|
|
26
|
+
expect((0, bridge_1.buildBrowserDriverOptions)({ CHROMIUM_USER_DATA_DIR: '/tmp/profile' })).toEqual({
|
|
27
|
+
chromiumPath: 'chromium',
|
|
28
|
+
userDataDir: '/tmp/profile',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it('ignores unrelated env vars and blank overrides fall back to defaults', () => {
|
|
32
|
+
expect((0, bridge_1.buildBrowserDriverOptions)({ CHROMIUM_PATH: '', SOME_OTHER_VAR: 'x' })).toEqual({
|
|
33
|
+
chromiumPath: 'chromium',
|
|
34
|
+
userDataDir: '/data/chromium-profile',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -56,6 +56,10 @@ describe('bridge config', () => {
|
|
|
56
56
|
it('returns null when no config exists', () => {
|
|
57
57
|
expect((0, config_1.loadBridgeConfig)(dir)).toBeNull();
|
|
58
58
|
});
|
|
59
|
+
it('lws-03: round-trips a bound deviceId alongside the connect token', () => {
|
|
60
|
+
(0, config_1.saveBridgeConfig)({ connectToken: 'wm_ct_' + 'a'.repeat(64), deviceId: 'device-1' }, dir);
|
|
61
|
+
expect((0, config_1.loadBridgeConfig)(dir)).toEqual({ connectToken: 'wm_ct_' + 'a'.repeat(64), deviceId: 'device-1' });
|
|
62
|
+
});
|
|
59
63
|
it('writes the discovery file with port + session token, 0600', () => {
|
|
60
64
|
(0, config_1.writeDiscoveryFile)({ port: 8377, sessionToken: 'st-1' }, dir);
|
|
61
65
|
const file = path.join(dir, 'bridge.json');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* pucc-int-01 — the relay client honors the `context-manifest` frame.
|
|
5
|
+
*
|
|
6
|
+
* platform (arena) → relay → container bridge: the bridge routes a
|
|
7
|
+
* `context-manifest` frame to the injected context writer (which writes the
|
|
8
|
+
* pushed content into a fenced managed region of /data/repos/<workspaceId>/
|
|
9
|
+
* CLAUDE.md) and acks with `context-written`. No PTY / session is involved —
|
|
10
|
+
* the context reaches Claude Code as a FILE, never keystrokes (ADR-0028).
|
|
11
|
+
*
|
|
12
|
+
* Reuses the faked-relay harness (no docker, no real shell).
|
|
13
|
+
*/
|
|
14
|
+
const ws_1 = require("ws");
|
|
15
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
16
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
17
|
+
class FakePty {
|
|
18
|
+
opts;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
this.opts = opts;
|
|
21
|
+
}
|
|
22
|
+
write() { }
|
|
23
|
+
resize() { }
|
|
24
|
+
kill() { }
|
|
25
|
+
onData() { }
|
|
26
|
+
onExit() { }
|
|
27
|
+
}
|
|
28
|
+
const makeFactory = () => ({ spawn: (o) => new FakePty(o) });
|
|
29
|
+
function frameQueue(ws) {
|
|
30
|
+
const frames = [];
|
|
31
|
+
const waiters = [];
|
|
32
|
+
ws.on('message', (raw) => {
|
|
33
|
+
const f = JSON.parse(String(raw));
|
|
34
|
+
const w = waiters.shift();
|
|
35
|
+
if (w)
|
|
36
|
+
w(f);
|
|
37
|
+
else
|
|
38
|
+
frames.push(f);
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
next: () => frames.length > 0 ? Promise.resolve(frames.shift()) : new Promise((r) => waiters.push(r)),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function startFakeRelay() {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
47
|
+
const queue = [];
|
|
48
|
+
const waiters = [];
|
|
49
|
+
wss.on('connection', (ws) => { const w = waiters.shift(); if (w)
|
|
50
|
+
w(ws);
|
|
51
|
+
else
|
|
52
|
+
queue.push(ws); });
|
|
53
|
+
wss.on('listening', () => {
|
|
54
|
+
const addr = wss.address();
|
|
55
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
56
|
+
resolve({ wss, port, nextConnection: () => (queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiters.push(r))) });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function closeRelay(wss) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
for (const client of wss.clients)
|
|
63
|
+
client.terminate();
|
|
64
|
+
wss.close((err) => (err ? reject(err) : resolve()));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
describe('relay client — context-manifest frame (pucc-int-01)', () => {
|
|
68
|
+
it('routes a context-manifest frame to the injected writer and acks context-written', async () => {
|
|
69
|
+
const relay = await startFakeRelay();
|
|
70
|
+
const sessions = new sessionManager_1.SessionManager(makeFactory());
|
|
71
|
+
const calls = [];
|
|
72
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
73
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
74
|
+
relayToken: 'tok',
|
|
75
|
+
sessions,
|
|
76
|
+
writeContext: (manifest) => {
|
|
77
|
+
calls.push(manifest);
|
|
78
|
+
return { action: 'created', claudeMdPath: `/data/repos/${manifest.workspaceId}/CLAUDE.md` };
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const ws = await relay.nextConnection();
|
|
82
|
+
const q = frameQueue(ws);
|
|
83
|
+
await q.next(); // auth
|
|
84
|
+
ws.send(JSON.stringify({ t: 'context-manifest', workspaceId: 'ws-7', content: '## files\n- a.ts', sessionId: 'agent-A' }));
|
|
85
|
+
const ack = await q.next();
|
|
86
|
+
expect(calls).toEqual([{ workspaceId: 'ws-7', content: '## files\n- a.ts' }]);
|
|
87
|
+
expect(ack).toMatchObject({ t: 'context-written', workspaceId: 'ws-7', sessionId: 'agent-A', action: 'created' });
|
|
88
|
+
// Writing context must not open a PTY session (ADR-0028: a FILE, not input).
|
|
89
|
+
expect(sessions.list()).toEqual([]);
|
|
90
|
+
handle.close();
|
|
91
|
+
await closeRelay(relay.wss);
|
|
92
|
+
});
|
|
93
|
+
it('forwards empty content (region removal) verbatim to the writer', async () => {
|
|
94
|
+
const relay = await startFakeRelay();
|
|
95
|
+
const sessions = new sessionManager_1.SessionManager(makeFactory());
|
|
96
|
+
const calls = [];
|
|
97
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
98
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
99
|
+
relayToken: 'tok',
|
|
100
|
+
sessions,
|
|
101
|
+
writeContext: (manifest) => { calls.push(manifest); return { action: 'removed', claudeMdPath: null }; },
|
|
102
|
+
});
|
|
103
|
+
const ws = await relay.nextConnection();
|
|
104
|
+
const q = frameQueue(ws);
|
|
105
|
+
await q.next(); // auth
|
|
106
|
+
ws.send(JSON.stringify({ t: 'context-manifest', workspaceId: 'ws-7', content: '' }));
|
|
107
|
+
const ack = await q.next();
|
|
108
|
+
expect(calls).toEqual([{ workspaceId: 'ws-7', content: '' }]);
|
|
109
|
+
expect(ack).toMatchObject({ t: 'context-written', workspaceId: 'ws-7', action: 'removed' });
|
|
110
|
+
handle.close();
|
|
111
|
+
await closeRelay(relay.wss);
|
|
112
|
+
});
|
|
113
|
+
it('errors (never crashes) when no context writer is wired', async () => {
|
|
114
|
+
const relay = await startFakeRelay();
|
|
115
|
+
const sessions = new sessionManager_1.SessionManager(makeFactory());
|
|
116
|
+
const handle = (0, relayClient_1.startRelayClient)({ relayUrl: `ws://127.0.0.1:${relay.port}`, relayToken: 'tok', sessions });
|
|
117
|
+
const ws = await relay.nextConnection();
|
|
118
|
+
const q = frameQueue(ws);
|
|
119
|
+
await q.next(); // auth
|
|
120
|
+
ws.send(JSON.stringify({ t: 'context-manifest', workspaceId: 'ws-7', content: 'x' }));
|
|
121
|
+
expect((await q.next()).t).toBe('error');
|
|
122
|
+
handle.close();
|
|
123
|
+
await closeRelay(relay.wss);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|