@wenrwa/marketplace-cli 0.4.6 → 0.5.0-lws.0
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-agent-launch.test.d.ts +1 -0
- package/dist/__tests__/bridge-agent-launch.test.js +47 -0
- package/dist/__tests__/bridge-browser-driver.test.js +65 -0
- package/dist/__tests__/bridge-browser-relay.test.js +70 -0
- package/dist/__tests__/bridge-cdp-transport-race.test.d.ts +1 -0
- package/dist/__tests__/bridge-cdp-transport-race.test.js +79 -0
- package/dist/__tests__/bridge-dev-server-detect.test.d.ts +1 -0
- package/dist/__tests__/bridge-dev-server-detect.test.js +94 -0
- package/dist/__tests__/bridge-fail-fast.test.d.ts +1 -0
- package/dist/__tests__/bridge-fail-fast.test.js +597 -0
- package/dist/__tests__/bridge-gitignore-matcher.test.d.ts +1 -0
- package/dist/__tests__/bridge-gitignore-matcher.test.js +55 -0
- package/dist/__tests__/bridge-local-context-no-self-echo.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-context-no-self-echo.test.js +106 -0
- package/dist/__tests__/bridge-local-context-writer.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-context-writer.test.js +120 -0
- package/dist/__tests__/bridge-local-tree-service.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-tree-service.test.js +121 -0
- package/dist/__tests__/bridge-publish-preflight.test.d.ts +1 -0
- package/dist/__tests__/bridge-publish-preflight.test.js +194 -0
- package/dist/__tests__/bridge-relay-client.test.js +356 -2
- package/dist/__tests__/bridge-server-dev-server-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-dev-server-frame.test.js +111 -0
- package/dist/__tests__/bridge-server-local-context.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-local-context.test.js +158 -0
- package/dist/__tests__/bridge-server-local-publish.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-local-publish.test.js +102 -0
- package/dist/__tests__/bridge-server-publish-preflight.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-publish-preflight.test.js +133 -0
- package/dist/__tests__/bridge-server-verify-origin.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-verify-origin.test.js +107 -0
- package/dist/__tests__/bridge-server.test.js +301 -2
- package/dist/__tests__/bridge-session-manager.test.js +270 -23
- package/dist/__tests__/bridge-tree-scanner.test.d.ts +1 -0
- package/dist/__tests__/bridge-tree-scanner.test.js +174 -0
- package/dist/__tests__/bridge-tree-watcher.test.d.ts +1 -0
- package/dist/__tests__/bridge-tree-watcher.test.js +111 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.js +109 -0
- package/dist/__tests__/localPublish.test.d.ts +1 -0
- package/dist/__tests__/localPublish.test.js +256 -0
- package/dist/__tests__/originVerification.test.d.ts +1 -0
- package/dist/__tests__/originVerification.test.js +160 -0
- package/dist/__tests__/term-snapshot.test.d.ts +1 -0
- package/dist/__tests__/term-snapshot.test.js +80 -0
- package/dist/__tests__/writeGitignore.test.d.ts +1 -0
- package/dist/__tests__/writeGitignore.test.js +59 -0
- package/dist/bridge/agentLaunch.d.ts +31 -0
- package/dist/bridge/agentLaunch.js +18 -0
- package/dist/bridge/browserDriver.d.ts +25 -0
- package/dist/bridge/browserDriver.js +70 -3
- package/dist/bridge/devServerDetect.d.ts +41 -0
- package/dist/bridge/devServerDetect.js +123 -0
- package/dist/bridge/gitignoreMatcher.d.ts +19 -0
- package/dist/bridge/gitignoreMatcher.js +0 -0
- package/dist/bridge/localContextWriter.d.ts +30 -0
- package/dist/bridge/localContextWriter.js +80 -0
- package/dist/bridge/localPublish.d.ts +15 -0
- package/dist/bridge/localPublish.js +82 -0
- package/dist/bridge/localTreeService.d.ts +38 -0
- package/dist/bridge/localTreeService.js +51 -0
- package/dist/bridge/originVerification.d.ts +17 -0
- package/dist/bridge/originVerification.js +55 -0
- package/dist/bridge/publishPreflight.d.ts +7 -0
- package/dist/bridge/publishPreflight.js +152 -0
- package/dist/bridge/realPty.js +6 -2
- package/dist/bridge/relayClient.d.ts +55 -0
- package/dist/bridge/relayClient.js +232 -33
- package/dist/bridge/repoCheckout.js +46 -10
- package/dist/bridge/server.d.ts +103 -0
- package/dist/bridge/server.js +299 -21
- package/dist/bridge/sessionManager.d.ts +110 -6
- package/dist/bridge/sessionManager.js +202 -13
- package/dist/bridge/termSnapshot.d.ts +36 -0
- package/dist/bridge/termSnapshot.js +66 -0
- package/dist/bridge/treeScanner.d.ts +25 -0
- package/dist/bridge/treeScanner.js +125 -0
- package/dist/bridge/treeWatcher.d.ts +17 -0
- package/dist/bridge/treeWatcher.js +116 -0
- package/dist/bridge/writeGitignore.d.ts +15 -0
- package/dist/bridge/writeGitignore.js +73 -0
- package/dist/commands/bridge.d.ts +13 -0
- package/dist/commands/bridge.js +187 -1
- package/dist/index.js +0 -0
- package/package.json +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const agentLaunch_1 = require("../bridge/agentLaunch");
|
|
4
|
+
/**
|
|
5
|
+
* TDD — lws-06: pure decision of what the bridge spawns for an agent-scoped
|
|
6
|
+
* PTY (ADR-0035 D4). `claude`, never $SHELL — autonomous maps to
|
|
7
|
+
* --dangerously-skip-permissions ONLY when the agent opts in; default OFF.
|
|
8
|
+
*
|
|
9
|
+
* lws-08 (PRD D9) — every agent-scoped launch also carries a minimal env
|
|
10
|
+
* pointer at `.hiveop/context.md` (relative to the spawn cwd, the project
|
|
11
|
+
* root). This is the ONLY mechanism used to point a spawned `claude` at the
|
|
12
|
+
* manifest: no args, no stdin injection, no CLAUDE.md edit. `claude` itself
|
|
13
|
+
* doesn't read this var natively today — it exists so the context file's
|
|
14
|
+
* location is a documented, stable contract other tooling (a future MCP
|
|
15
|
+
* resource, a slash command, a system-prompt append) can rely on instead of
|
|
16
|
+
* hardcoding the path, and so a human/agent inspecting `env` in the PTY can
|
|
17
|
+
* discover it. Deliberately NOT wired into --append-system-prompt or similar
|
|
18
|
+
* — keeping this additive-only avoids coupling the launch decision to
|
|
19
|
+
* `claude` CLI flag churn.
|
|
20
|
+
*/
|
|
21
|
+
describe('resolveAgentLaunch (lws-06)', () => {
|
|
22
|
+
it('defaults to a bare `claude` invocation (autonomous OFF)', () => {
|
|
23
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({ autonomous: false })).toEqual({
|
|
24
|
+
command: 'claude',
|
|
25
|
+
args: [],
|
|
26
|
+
env: { [agentLaunch_1.HIVEOP_CONTEXT_ENV_VAR]: agentLaunch_1.HIVEOP_CONTEXT_ENV_VALUE },
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it('an agent-scoped launch with autonomous unset also defaults OFF', () => {
|
|
30
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({})).toEqual({
|
|
31
|
+
command: 'claude',
|
|
32
|
+
args: [],
|
|
33
|
+
env: { [agentLaunch_1.HIVEOP_CONTEXT_ENV_VAR]: agentLaunch_1.HIVEOP_CONTEXT_ENV_VALUE },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('autonomous:true maps to --dangerously-skip-permissions', () => {
|
|
37
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true })).toEqual({
|
|
38
|
+
command: 'claude',
|
|
39
|
+
args: ['--dangerously-skip-permissions'],
|
|
40
|
+
env: { [agentLaunch_1.HIVEOP_CONTEXT_ENV_VAR]: agentLaunch_1.HIVEOP_CONTEXT_ENV_VALUE },
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it('the context env var always points at the relative .hiveop/context.md path', () => {
|
|
44
|
+
expect(agentLaunch_1.HIVEOP_CONTEXT_ENV_VALUE).toBe('.hiveop/context.md');
|
|
45
|
+
expect((0, agentLaunch_1.resolveAgentLaunch)({}).env).toEqual({ HIVEOP_CONTEXT_FILE: '.hiveop/context.md' });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -210,6 +210,71 @@ describe('createBrowserDriver (cbs-02)', () => {
|
|
|
210
210
|
expect(calls[0].params).toEqual({ text: 'hunter2' });
|
|
211
211
|
expect(calls[0].sessionId).toBe('sess-1');
|
|
212
212
|
});
|
|
213
|
+
// Live bug fix: Page.startScreencast only emits Page.screencastFrame on
|
|
214
|
+
// an actual repaint, so an idle/already-loaded page (or a pane that
|
|
215
|
+
// reattaches to one) gets nothing new. refresh() is the forcing
|
|
216
|
+
// function: re-emit whatever we last delivered, then restart the
|
|
217
|
+
// screencast so Chromium produces a genuinely fresh frame.
|
|
218
|
+
describe('refresh()', () => {
|
|
219
|
+
it('re-emits the cached last-delivered frame synchronously', async () => {
|
|
220
|
+
const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
|
|
221
|
+
const frames = [];
|
|
222
|
+
driver.onFrame((f) => frames.push(f));
|
|
223
|
+
await driver.start('https://example.com');
|
|
224
|
+
endpoint.emitEvent('Page.screencastFrame', { data: 'first', sessionId: 1, metadata: {} }, 'sess-1');
|
|
225
|
+
expect(frames.map((f) => f.data)).toEqual(['first']);
|
|
226
|
+
void driver.refresh();
|
|
227
|
+
// The cached frame is re-emitted synchronously, before any CDP
|
|
228
|
+
// round-trip for the forced-fresh frame completes.
|
|
229
|
+
expect(frames.map((f) => f.data)).toEqual(['first', 'first']);
|
|
230
|
+
});
|
|
231
|
+
it('restarts the screencast (stopScreencast then startScreencast, same params) on the attached session', async () => {
|
|
232
|
+
const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
|
|
233
|
+
await driver.start('https://example.com');
|
|
234
|
+
endpoint.sent.length = 0;
|
|
235
|
+
await driver.refresh();
|
|
236
|
+
const methods = endpoint.sent.map((m) => m.method);
|
|
237
|
+
expect(methods).toEqual(['Page.stopScreencast', 'Page.startScreencast']);
|
|
238
|
+
const stop = endpoint.sentCalls('Page.stopScreencast')[0];
|
|
239
|
+
expect(stop.sessionId).toBe('sess-1');
|
|
240
|
+
expect(stop.params).toEqual({});
|
|
241
|
+
const start = endpoint.sentCalls('Page.startScreencast')[0];
|
|
242
|
+
expect(start.sessionId).toBe('sess-1');
|
|
243
|
+
expect(start.params).toEqual({ format: 'jpeg', quality: 60, maxWidth: 1280, maxHeight: 800 });
|
|
244
|
+
});
|
|
245
|
+
it('a restarted screencast delivers the forced-fresh frame through onFrame once Chromium emits it', async () => {
|
|
246
|
+
const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
|
|
247
|
+
const frames = [];
|
|
248
|
+
driver.onFrame((f) => frames.push(f));
|
|
249
|
+
await driver.start('https://example.com');
|
|
250
|
+
endpoint.emitEvent('Page.screencastFrame', { data: 'stale', sessionId: 1, metadata: {} }, 'sess-1');
|
|
251
|
+
await driver.refresh();
|
|
252
|
+
endpoint.emitEvent('Page.screencastFrame', { data: 'fresh', sessionId: 2, metadata: {} }, 'sess-1');
|
|
253
|
+
expect(frames.map((f) => f.data)).toEqual(['stale', 'stale', 'fresh']);
|
|
254
|
+
});
|
|
255
|
+
it('called before start() resolves is a safe no-op (nothing cached, no session to restart)', async () => {
|
|
256
|
+
const { driver } = setup({ existingPageTargetId: 't1' });
|
|
257
|
+
await expect(driver.refresh()).resolves.toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
it('un-wedges a pacer stuck on a previous consumer\'s unacked frame, so the next pushed frame delivers immediately', async () => {
|
|
260
|
+
const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
|
|
261
|
+
const frames = [];
|
|
262
|
+
driver.onFrame((f) => frames.push(f));
|
|
263
|
+
await driver.start('https://example.com');
|
|
264
|
+
// One frame lands and is delivered; nobody ever acks it (the pane
|
|
265
|
+
// that received it is gone) and — matching the real idle-page bug —
|
|
266
|
+
// nothing further is pushed, so nothing is queued behind it either.
|
|
267
|
+
// The pacer is left permanently "awaitingAck" with no live consumer.
|
|
268
|
+
endpoint.emitEvent('Page.screencastFrame', { data: 'stale', sessionId: 1, metadata: {} }, 'sess-1');
|
|
269
|
+
expect(frames.map((f) => f.data)).toEqual(['stale']);
|
|
270
|
+
await driver.refresh();
|
|
271
|
+
// A newly (re)attached pane must see the next captured frame land
|
|
272
|
+
// immediately — WITHOUT anyone having called ackFrame() — instead of
|
|
273
|
+
// it silently queuing behind a phantom ack that will never come.
|
|
274
|
+
endpoint.emitEvent('Page.screencastFrame', { data: 'new-attach-frame', sessionId: 2, metadata: {} }, 'sess-1');
|
|
275
|
+
expect(frames[frames.length - 1].data).toBe('new-attach-frame');
|
|
276
|
+
});
|
|
277
|
+
});
|
|
213
278
|
it('navigate(url) after start sends Page.navigate again on the attached target session', async () => {
|
|
214
279
|
const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
|
|
215
280
|
await driver.start('https://example.com');
|
|
@@ -31,6 +31,7 @@ const makeFactory = () => ({ spawn: (o) => new FakePty(o) });
|
|
|
31
31
|
class FakeBrowserDriver {
|
|
32
32
|
startCalls = [];
|
|
33
33
|
navigateCalls = [];
|
|
34
|
+
refreshCalls = 0;
|
|
34
35
|
inputCalls = [];
|
|
35
36
|
stopCalls = 0;
|
|
36
37
|
sleepCalls = [];
|
|
@@ -43,6 +44,9 @@ class FakeBrowserDriver {
|
|
|
43
44
|
async navigate(url) {
|
|
44
45
|
this.navigateCalls.push(url);
|
|
45
46
|
}
|
|
47
|
+
async refresh() {
|
|
48
|
+
this.refreshCalls += 1;
|
|
49
|
+
}
|
|
46
50
|
input(event) {
|
|
47
51
|
this.inputCalls.push(event);
|
|
48
52
|
}
|
|
@@ -208,6 +212,72 @@ describe('relay client — browser-open/browser-input (cbs-02)', () => {
|
|
|
208
212
|
expect(drivers).toHaveLength(1);
|
|
209
213
|
expect(drivers[0].startCalls).toEqual(['https://example.com']);
|
|
210
214
|
expect(drivers[0].navigateCalls).toEqual(['https://example.com/other']);
|
|
215
|
+
expect(drivers[0].refreshCalls).toBe(0);
|
|
216
|
+
handle.close();
|
|
217
|
+
await closeRelay(relay.wss);
|
|
218
|
+
});
|
|
219
|
+
// Live bug fix: a pane that closes and reopens the Browser view for the
|
|
220
|
+
// SAME session/url is a reattach, not a navigation — navigate()-ing an
|
|
221
|
+
// already-loaded page is a correct-but-useless no-visible-effect call
|
|
222
|
+
// (see browserDriver's refresh() doc comment for why). refresh() is what
|
|
223
|
+
// actually gets the returning pane a frame.
|
|
224
|
+
it('browser-open on an already-open session with the SAME url calls refresh(), not navigate()', async () => {
|
|
225
|
+
const relay = await startFakeRelay();
|
|
226
|
+
const sessions = new sessionManager_1.SessionManager(makeFactory());
|
|
227
|
+
const drivers = [];
|
|
228
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
229
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
230
|
+
relayToken: 'tok',
|
|
231
|
+
sessions,
|
|
232
|
+
createBrowserDriver: () => {
|
|
233
|
+
const d = new FakeBrowserDriver();
|
|
234
|
+
drivers.push(d);
|
|
235
|
+
return d;
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const ws = await relay.nextConnection();
|
|
239
|
+
const q = frameQueue(ws);
|
|
240
|
+
await q.next(); // auth
|
|
241
|
+
ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
|
|
242
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
243
|
+
// Reattach: same sessionId, same url.
|
|
244
|
+
ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
|
|
245
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
246
|
+
expect(drivers).toHaveLength(1);
|
|
247
|
+
expect(drivers[0].startCalls).toEqual(['https://example.com']);
|
|
248
|
+
expect(drivers[0].navigateCalls).toEqual([]);
|
|
249
|
+
expect(drivers[0].refreshCalls).toBe(1);
|
|
250
|
+
handle.close();
|
|
251
|
+
await closeRelay(relay.wss);
|
|
252
|
+
});
|
|
253
|
+
// A url change after a same-url reattach must still navigate — refresh()
|
|
254
|
+
// is only for the no-op-navigation case.
|
|
255
|
+
it('browser-open with a DIFFERENT url after a same-url reattach still navigates', async () => {
|
|
256
|
+
const relay = await startFakeRelay();
|
|
257
|
+
const sessions = new sessionManager_1.SessionManager(makeFactory());
|
|
258
|
+
const drivers = [];
|
|
259
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
260
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
261
|
+
relayToken: 'tok',
|
|
262
|
+
sessions,
|
|
263
|
+
createBrowserDriver: () => {
|
|
264
|
+
const d = new FakeBrowserDriver();
|
|
265
|
+
drivers.push(d);
|
|
266
|
+
return d;
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
const ws = await relay.nextConnection();
|
|
270
|
+
const q = frameQueue(ws);
|
|
271
|
+
await q.next(); // auth
|
|
272
|
+
ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
|
|
273
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
274
|
+
ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' })); // reattach
|
|
275
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
276
|
+
ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com/other' })); // real nav
|
|
277
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
278
|
+
expect(drivers).toHaveLength(1);
|
|
279
|
+
expect(drivers[0].refreshCalls).toBe(1);
|
|
280
|
+
expect(drivers[0].navigateCalls).toEqual(['https://example.com/other']);
|
|
211
281
|
handle.close();
|
|
212
282
|
await closeRelay(relay.wss);
|
|
213
283
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const browserDriver_1 = require("../bridge/browserDriver");
|
|
4
|
+
/**
|
|
5
|
+
* Regression test for the first-light hang: the driver sends its first CDP
|
|
6
|
+
* command (Target.getTargets) the moment the transport is constructed, while
|
|
7
|
+
* the DevTools socket is still CONNECTING. The original transport dropped
|
|
8
|
+
* anything sent before OPEN, so that command never reached Chromium and
|
|
9
|
+
* start() hung forever with no error. Frames never flowed.
|
|
10
|
+
*
|
|
11
|
+
* The real `ws` path is otherwise untested by design (mocks are injected at
|
|
12
|
+
* wsFactory), which is exactly why the bug survived to production — so this
|
|
13
|
+
* test drives a fake WebSocket through the real transport instead.
|
|
14
|
+
*/
|
|
15
|
+
class FakeSocket {
|
|
16
|
+
url;
|
|
17
|
+
static CONNECTING = 0;
|
|
18
|
+
static OPEN = 1;
|
|
19
|
+
static CLOSING = 2;
|
|
20
|
+
static CLOSED = 3;
|
|
21
|
+
readyState = FakeSocket.CONNECTING;
|
|
22
|
+
sent = [];
|
|
23
|
+
handlers = new Map();
|
|
24
|
+
constructor(url) {
|
|
25
|
+
this.url = url;
|
|
26
|
+
}
|
|
27
|
+
send(d) { this.sent.push(d); }
|
|
28
|
+
close() { this.readyState = FakeSocket.CLOSED; }
|
|
29
|
+
on(event, cb) {
|
|
30
|
+
const set = this.handlers.get(event) ?? [];
|
|
31
|
+
set.push(cb);
|
|
32
|
+
this.handlers.set(event, set);
|
|
33
|
+
}
|
|
34
|
+
/** simulate the socket finishing its handshake */
|
|
35
|
+
fireOpen() {
|
|
36
|
+
this.readyState = FakeSocket.OPEN;
|
|
37
|
+
for (const cb of this.handlers.get('open') ?? [])
|
|
38
|
+
cb();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// The transport reads OPEN/CONNECTING off the constructor it is given.
|
|
42
|
+
function makeTransport() {
|
|
43
|
+
let created;
|
|
44
|
+
class Tracked extends FakeSocket {
|
|
45
|
+
constructor(url) { super(url); created = this; }
|
|
46
|
+
}
|
|
47
|
+
const transport = (0, browserDriver_1.createDefaultTransport)('ws://127.0.0.1:1/devtools/browser/x', Tracked);
|
|
48
|
+
return { transport, socket: created };
|
|
49
|
+
}
|
|
50
|
+
describe('CDP default transport — connect race', () => {
|
|
51
|
+
it('buffers a command sent while CONNECTING and flushes it on open', () => {
|
|
52
|
+
const { transport, socket } = makeTransport();
|
|
53
|
+
transport.send('{"id":1,"method":"Target.getTargets"}');
|
|
54
|
+
expect(socket.sent).toEqual([]); // still connecting — must NOT be lost
|
|
55
|
+
socket.fireOpen();
|
|
56
|
+
expect(socket.sent).toEqual(['{"id":1,"method":"Target.getTargets"}']);
|
|
57
|
+
});
|
|
58
|
+
it('preserves order across the open boundary', () => {
|
|
59
|
+
const { transport, socket } = makeTransport();
|
|
60
|
+
transport.send('a');
|
|
61
|
+
transport.send('b');
|
|
62
|
+
socket.fireOpen();
|
|
63
|
+
transport.send('c');
|
|
64
|
+
expect(socket.sent).toEqual(['a', 'b', 'c']);
|
|
65
|
+
});
|
|
66
|
+
it('sends straight through once open', () => {
|
|
67
|
+
const { transport, socket } = makeTransport();
|
|
68
|
+
socket.fireOpen();
|
|
69
|
+
transport.send('x');
|
|
70
|
+
expect(socket.sent).toEqual(['x']);
|
|
71
|
+
});
|
|
72
|
+
it('drops (without throwing) once the socket is closed', () => {
|
|
73
|
+
const { transport, socket } = makeTransport();
|
|
74
|
+
socket.fireOpen();
|
|
75
|
+
socket.close();
|
|
76
|
+
expect(() => transport.send('late')).not.toThrow();
|
|
77
|
+
expect(socket.sent).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* TDD — lws-08 (PRD D7 nice-to-have): pure dev-server-banner matcher over
|
|
5
|
+
* agent PTY output. Table-driven per common framework so the false-positive
|
|
6
|
+
* surface stays small (a bare "listening on" fallback would false-trigger on
|
|
7
|
+
* ordinary chatter); every framework line must ALSO resolve to a
|
|
8
|
+
* localhost/127.0.0.1/0.0.0.0/[::1] URL, since that is the whole point
|
|
9
|
+
* (offer a Browser pane at a port on THIS machine).
|
|
10
|
+
*/
|
|
11
|
+
const devServerDetect_1 = require("../bridge/devServerDetect");
|
|
12
|
+
describe('matchDevServerLine — per-framework banner table', () => {
|
|
13
|
+
it('Next.js — "- Local: http://localhost:3000"', () => {
|
|
14
|
+
expect((0, devServerDetect_1.matchDevServerLine)(' - Local: http://localhost:3000')).toEqual({
|
|
15
|
+
framework: 'Next.js', port: 3000, url: 'http://localhost:3000',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
it('Vite — "➜ Local: http://localhost:5173/"', () => {
|
|
19
|
+
expect((0, devServerDetect_1.matchDevServerLine)(' ➜ Local: http://localhost:5173/')).toEqual({
|
|
20
|
+
framework: 'Vite', port: 5173, url: 'http://localhost:5173/',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
it('Create React App — "Local: http://localhost:3000"', () => {
|
|
24
|
+
expect((0, devServerDetect_1.matchDevServerLine)('You can now view app in the browser.\n Local: http://localhost:3000')).toEqual({
|
|
25
|
+
framework: 'CRA', port: 3000, url: 'http://localhost:3000',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it('Rails/Puma — "Listening on http://127.0.0.1:3000"', () => {
|
|
29
|
+
expect((0, devServerDetect_1.matchDevServerLine)('* Listening on http://127.0.0.1:3000')).toEqual({
|
|
30
|
+
framework: 'Rails', port: 3000, url: 'http://127.0.0.1:3000',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it('Django — "Starting development server at http://127.0.0.1:8000/"', () => {
|
|
34
|
+
expect((0, devServerDetect_1.matchDevServerLine)('Starting development server at http://127.0.0.1:8000/')).toEqual({
|
|
35
|
+
framework: 'Django', port: 8000, url: 'http://127.0.0.1:8000/',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('generic fallback — "Server ready on http://localhost:4000"', () => {
|
|
39
|
+
expect((0, devServerDetect_1.matchDevServerLine)('Server ready on http://localhost:4000')).toEqual({
|
|
40
|
+
framework: 'generic', port: 4000, url: 'http://localhost:4000',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it('generic fallback — "App listening on 0.0.0.0:8080"', () => {
|
|
44
|
+
expect((0, devServerDetect_1.matchDevServerLine)('App listening on 0.0.0.0:8080')).toEqual({
|
|
45
|
+
framework: 'generic', port: 8080, url: 'http://0.0.0.0:8080',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('strips ANSI color codes before matching', () => {
|
|
49
|
+
const line = '\x1b[32m - Local:\x1b[39m \x1b[1mhttp://localhost:3000\x1b[22m';
|
|
50
|
+
expect((0, devServerDetect_1.matchDevServerLine)(line)).toEqual({ framework: 'Next.js', port: 3000, url: 'http://localhost:3000' });
|
|
51
|
+
});
|
|
52
|
+
it('a non-localhost URL never matches (the point is offering a pane on THIS machine)', () => {
|
|
53
|
+
expect((0, devServerDetect_1.matchDevServerLine)('- Local: http://example.com:3000')).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
it('no false positives on ordinary agent chatter', () => {
|
|
56
|
+
expect((0, devServerDetect_1.matchDevServerLine)('Running tests...')).toBeNull();
|
|
57
|
+
expect((0, devServerDetect_1.matchDevServerLine)('$ npm install')).toBeNull();
|
|
58
|
+
expect((0, devServerDetect_1.matchDevServerLine)('Reading file src/index.ts')).toBeNull();
|
|
59
|
+
expect((0, devServerDetect_1.matchDevServerLine)('I will now start the server for you.')).toBeNull();
|
|
60
|
+
expect((0, devServerDetect_1.matchDevServerLine)('See http://localhost:3000 for more info in the README')).toBeNull();
|
|
61
|
+
expect((0, devServerDetect_1.matchDevServerLine)('')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('createDevServerWatcher — feed(chunk), dedupe/debounce per session', () => {
|
|
65
|
+
it('emits a match once a full banner line arrives', () => {
|
|
66
|
+
const w = (0, devServerDetect_1.createDevServerWatcher)();
|
|
67
|
+
const matches = w.feed('some output\n- Local: http://localhost:3000\nmore output\n');
|
|
68
|
+
expect(matches).toEqual([{ framework: 'Next.js', port: 3000, url: 'http://localhost:3000' }]);
|
|
69
|
+
});
|
|
70
|
+
it('dedupes — the same port is never emitted twice for one watcher instance', () => {
|
|
71
|
+
const w = (0, devServerDetect_1.createDevServerWatcher)();
|
|
72
|
+
expect(w.feed('- Local: http://localhost:3000\n')).toHaveLength(1);
|
|
73
|
+
expect(w.feed('- Local: http://localhost:3000\n')).toHaveLength(0);
|
|
74
|
+
// Even framed differently (Vite banner, same port) — still a dupe on port.
|
|
75
|
+
expect(w.feed('➜ Local: http://localhost:3000/\n')).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
it('emits separately for genuinely different ports', () => {
|
|
78
|
+
const w = (0, devServerDetect_1.createDevServerWatcher)();
|
|
79
|
+
expect(w.feed('- Local: http://localhost:3000\n')).toHaveLength(1);
|
|
80
|
+
expect(w.feed('- Local: http://localhost:5173\n')).toEqual([
|
|
81
|
+
{ framework: 'Next.js', port: 5173, url: 'http://localhost:5173' },
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
it('handles a banner split across two chunks (buffers the incomplete line)', () => {
|
|
85
|
+
const w = (0, devServerDetect_1.createDevServerWatcher)();
|
|
86
|
+
expect(w.feed('- Local: http://localhost:')).toHaveLength(0);
|
|
87
|
+
expect(w.feed('3000\n')).toEqual([{ framework: 'Next.js', port: 3000, url: 'http://localhost:3000' }]);
|
|
88
|
+
});
|
|
89
|
+
it('no false positives from a burst of ordinary PTY output', () => {
|
|
90
|
+
const w = (0, devServerDetect_1.createDevServerWatcher)();
|
|
91
|
+
const chunk = ['$ npm install', 'added 200 packages', '$ npm run build', 'Compiled successfully.'].join('\n');
|
|
92
|
+
expect(w.feed(chunk)).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|