@monoes/monobrowse 1.0.11 → 1.0.12
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/src/__tests__/adapters.test.js +5 -2
- package/dist/src/__tests__/adapters.test.js.map +1 -1
- package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
- package/dist/src/__tests__/box-model.test.js +9 -5
- package/dist/src/__tests__/box-model.test.js.map +1 -1
- package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
- package/dist/src/__tests__/browser-launch.test.js +17 -10
- package/dist/src/__tests__/browser-launch.test.js.map +1 -1
- package/dist/src/__tests__/cdp-client.test.js +9 -3
- package/dist/src/__tests__/cdp-client.test.js.map +1 -1
- package/dist/src/__tests__/close-browser.test.d.ts +13 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
- package/dist/src/__tests__/close-browser.test.js +5 -5
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/console-log.test.js +17 -6
- package/dist/src/__tests__/console-log.test.js.map +1 -1
- package/dist/src/__tests__/find.test.js +26 -6
- package/dist/src/__tests__/find.test.js.map +1 -1
- package/dist/src/__tests__/frame-command.test.js +9 -3
- package/dist/src/__tests__/frame-command.test.js.map +1 -1
- package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
- package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
- package/dist/src/__tests__/output.test.js +1 -1
- package/dist/src/__tests__/output.test.js.map +1 -1
- package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
- package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
- package/dist/src/__tests__/ref-cache.test.js +18 -10
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/session-state.test.d.ts +8 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
- package/dist/src/__tests__/session-state.test.js +10 -10
- package/dist/src/__tests__/session-state.test.js.map +1 -1
- package/dist/src/__tests__/snapshot.test.js +44 -14
- package/dist/src/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/__tests__/storage-eval.test.js +14 -6
- package/dist/src/__tests__/storage-eval.test.js.map +1 -1
- package/dist/src/__tests__/tabs.test.js +25 -9
- package/dist/src/__tests__/tabs.test.js.map +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +25 -9
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/action-builder/types.d.ts.map +1 -1
- package/dist/src/browser/actions.d.ts +1 -1
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +16 -6
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
- package/dist/src/browser/adapters/gemini.js.map +1 -1
- package/dist/src/browser/adapters/google.d.ts.map +1 -1
- package/dist/src/browser/adapters/google.js.map +1 -1
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +3 -3
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
- package/dist/src/browser/adapters/instagram.js.map +1 -1
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
- package/dist/src/browser/adapters/linkedin.js.map +1 -1
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
- package/dist/src/browser/adapters/microsoft.js.map +1 -1
- package/dist/src/browser/adapters/x.d.ts.map +1 -1
- package/dist/src/browser/adapters/x.js.map +1 -1
- package/dist/src/browser/browser.d.ts +30 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +122 -15
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +21 -8
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +2 -2
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +29 -19
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +153 -165
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +3 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/emulation.d.ts.map +1 -1
- package/dist/src/browser/emulation.js +21 -6
- package/dist/src/browser/emulation.js.map +1 -1
- package/dist/src/browser/find.d.ts +4 -4
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +14 -6
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +14 -7
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/index.d.ts +16 -16
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +16 -16
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +14 -4
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/pdf.d.ts.map +1 -1
- package/dist/src/browser/pdf.js +3 -3
- package/dist/src/browser/pdf.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +10 -5
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts +1 -1
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +18 -7
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +9 -4
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +11 -7
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +27 -8
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts +1 -1
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +8 -4
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts.map +1 -1
- package/dist/src/browser/storage.js.map +1 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +9 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +3 -3
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +23 -4
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts.map +1 -1
- package/dist/src/browser/vitals.js +1 -1
- package/dist/src/browser/vitals.js.map +1 -1
- package/dist/src/browser/wait.d.ts.map +1 -1
- package/dist/src/browser/wait.js +11 -5
- package/dist/src/browser/wait.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +14 -11
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +453 -159
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.js +5 -5
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/cli/platform.d.ts.map +1 -1
- package/dist/src/cli/platform.js +15 -11
- package/dist/src/cli/platform.js.map +1 -1
- package/dist/src/cli/types.d.ts +1 -1
- package/dist/src/cli/types.d.ts.map +1 -1
- package/dist/src/cli.js +21 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters.test.ts +15 -8
- package/src/__tests__/batch-eval-parsing.test.ts +4 -2
- package/src/__tests__/box-model.test.ts +9 -5
- package/src/__tests__/browser-launch.test.ts +26 -15
- package/src/__tests__/cdp-client.test.ts +25 -9
- package/src/__tests__/close-browser.test.ts +11 -6
- package/src/__tests__/console-log.test.ts +36 -13
- package/src/__tests__/find.test.ts +58 -20
- package/src/__tests__/frame-command.test.ts +12 -6
- package/src/__tests__/open-url-navigate-error.test.ts +2 -4
- package/src/__tests__/output.test.ts +1 -1
- package/src/__tests__/reap-idle-browser.test.ts +276 -0
- package/src/__tests__/ref-cache.test.ts +25 -12
- package/src/__tests__/session-state.test.ts +40 -28
- package/src/__tests__/snapshot.test.ts +60 -21
- package/src/__tests__/storage-eval.test.ts +30 -16
- package/src/__tests__/tabs.test.ts +26 -10
- package/src/browser/action-builder/analyzer.ts +39 -15
- package/src/browser/action-builder/types.ts +45 -8
- package/src/browser/actions.ts +288 -123
- package/src/browser/adapters/gemini.ts +7 -3
- package/src/browser/adapters/google.ts +5 -3
- package/src/browser/adapters/index.ts +5 -4
- package/src/browser/adapters/instagram.ts +4 -2
- package/src/browser/adapters/linkedin.ts +4 -2
- package/src/browser/adapters/microsoft.ts +9 -5
- package/src/browser/adapters/x.ts +7 -3
- package/src/browser/browser.ts +188 -42
- package/src/browser/cdp.ts +43 -11
- package/src/browser/console-log.ts +24 -7
- package/src/browser/dashboard/server.ts +44 -25
- package/src/browser/dashboard/ui.html +153 -165
- package/src/browser/dialog.ts +25 -8
- package/src/browser/emulation.ts +86 -36
- package/src/browser/find.ts +110 -47
- package/src/browser/har.ts +58 -26
- package/src/browser/index.ts +16 -16
- package/src/browser/network.ts +147 -46
- package/src/browser/pdf.ts +4 -4
- package/src/browser/profiler.ts +23 -10
- package/src/browser/record.ts +38 -19
- package/src/browser/ref-cache.ts +38 -17
- package/src/browser/screenshot.ts +58 -32
- package/src/browser/session.ts +51 -18
- package/src/browser/snapshot.ts +34 -20
- package/src/browser/storage.ts +73 -17
- package/src/browser/tabs.ts +29 -17
- package/src/browser/trace.ts +14 -10
- package/src/browser/types.ts +23 -4
- package/src/browser/vitals.ts +33 -16
- package/src/browser/wait.ts +47 -21
- package/src/cli/action.ts +52 -21
- package/src/cli/commands.ts +1093 -385
- package/src/cli/output.ts +5 -5
- package/src/cli/platform.ts +44 -23
- package/src/cli/types.ts +1 -1
- package/src/cli.ts +34 -18
- package/src/index.ts +2 -2
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* this checks that response before doing anything else.
|
|
8
8
|
*/
|
|
9
9
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
-
import type { CdpClient } from '../browser/cdp.js';
|
|
11
10
|
import { openUrl } from '../browser/browser.js';
|
|
11
|
+
import type { CdpClient } from '../browser/cdp.js';
|
|
12
12
|
|
|
13
13
|
afterEach(() => {
|
|
14
14
|
vi.restoreAllMocks();
|
|
@@ -43,9 +43,7 @@ describe('openUrl navigation-failure detection', () => {
|
|
|
43
43
|
await expect(openUrl(client, 'sess-1', 'http://127.0.0.1:1/')).rejects.toThrow(
|
|
44
44
|
/net::ERR_CONNECTION_REFUSED/,
|
|
45
45
|
);
|
|
46
|
-
expect(calls).toEqual([
|
|
47
|
-
{ method: 'Page.navigate', params: { url: 'http://127.0.0.1:1/' } },
|
|
48
|
-
]);
|
|
46
|
+
expect(calls).toEqual([{ method: 'Page.navigate', params: { url: 'http://127.0.0.1:1/' } }]);
|
|
49
47
|
});
|
|
50
48
|
|
|
51
49
|
it('rejects the 2 MB URL guard before ever calling Page.navigate', async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
// The module exports a singleton `output` which reads process.stdout.isTTY at
|
|
4
4
|
// construction time. We need to control that, so we reset modules per test.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #310: a launched Chrome outlives its session, holding its CDP port (9222 by
|
|
3
|
+
* convention, which other local tools want) with zero targets left to
|
|
4
|
+
* reattach to. reapIdleLaunchedBrowser() gives that port back.
|
|
5
|
+
*
|
|
6
|
+
* No real Chrome is spawned: the CDP HTTP endpoints (/json/version, /json/list)
|
|
7
|
+
* are served by a local http server that answers exactly like Chrome does
|
|
8
|
+
* (same approach as browser-launch.test.ts), the browser-level websocket is a
|
|
9
|
+
* fake `ws` socket (same approach as close-browser.test.ts), and process.kill
|
|
10
|
+
* is spied on rather than actually signaling anything.
|
|
11
|
+
*
|
|
12
|
+
* Fixed port range (23490-23499) chosen to avoid colliding with real services.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { createServer as createHttpServer, type Server as HttpServer } from 'node:http';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
20
|
+
import type { CdpTarget } from '../browser/types.js';
|
|
21
|
+
|
|
22
|
+
const BASE = 23490;
|
|
23
|
+
const MINUTE = 60_000;
|
|
24
|
+
|
|
25
|
+
let lastSocket: FakeWs | null = null;
|
|
26
|
+
|
|
27
|
+
/** Minimal `ws` stand-in: opens on its own and acknowledges every command,
|
|
28
|
+
* so `Browser.close` takes the graceful path without a real browser. */
|
|
29
|
+
class FakeWs {
|
|
30
|
+
handlers = new Map<string, Array<(...a: unknown[]) => void>>();
|
|
31
|
+
sent: string[] = [];
|
|
32
|
+
constructor(public url: string) {
|
|
33
|
+
lastSocket = this;
|
|
34
|
+
queueMicrotask(() => this.emit('open'));
|
|
35
|
+
}
|
|
36
|
+
on(event: string, fn: (...a: unknown[]) => void): void {
|
|
37
|
+
if (!this.handlers.has(event)) this.handlers.set(event, []);
|
|
38
|
+
this.handlers.get(event)!.push(fn);
|
|
39
|
+
}
|
|
40
|
+
emit(event: string, ...args: unknown[]): void {
|
|
41
|
+
for (const fn of [...(this.handlers.get(event) ?? [])]) fn(...args);
|
|
42
|
+
}
|
|
43
|
+
send(data: string, cb?: (err?: Error) => void): void {
|
|
44
|
+
void cb;
|
|
45
|
+
this.sent.push(data);
|
|
46
|
+
const { id } = JSON.parse(data) as { id: number };
|
|
47
|
+
queueMicrotask(() => this.emit('message', Buffer.from(JSON.stringify({ id, result: {} }))));
|
|
48
|
+
}
|
|
49
|
+
close(): void {}
|
|
50
|
+
/** Methods this socket was asked to run, e.g. ['Browser.close']. */
|
|
51
|
+
methods(): string[] {
|
|
52
|
+
return this.sent.map((s) => (JSON.parse(s) as { method: string }).method);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
vi.mock('ws', () => ({ WebSocket: FakeWs }));
|
|
57
|
+
|
|
58
|
+
let tempDir: string;
|
|
59
|
+
let servers: HttpServer[] = [];
|
|
60
|
+
let killSpy: ReturnType<typeof vi.spyOn>;
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
lastSocket = null;
|
|
64
|
+
tempDir = await mkdtemp(join(tmpdir(), 'monobrowse-reap-test-'));
|
|
65
|
+
// ref-cache.ts resolves its cache dir from process.cwd() at module load, so
|
|
66
|
+
// this must be in place before the dynamic import in each test.
|
|
67
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
68
|
+
// The browser is "already gone" by the time closeBrowser polls for its exit
|
|
69
|
+
// (ESRCH on the liveness probe) — the realistic outcome of Browser.close.
|
|
70
|
+
killSpy = vi.spyOn(process, 'kill').mockImplementation((_pid: unknown, signal?: unknown) => {
|
|
71
|
+
if (signal === 0) throw Object.assign(new Error('ESRCH'), { code: 'ESRCH' });
|
|
72
|
+
return true as never;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(async () => {
|
|
77
|
+
await Promise.all(servers.map((s) => new Promise<void>((resolve) => s.close(() => resolve()))));
|
|
78
|
+
servers = [];
|
|
79
|
+
vi.restoreAllMocks();
|
|
80
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** An http server answering /json/version and /json/list exactly like Chrome. */
|
|
84
|
+
function fakeChrome(port: number, targets: Partial<CdpTarget>[]): Promise<void> {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const s = createHttpServer((req, res) => {
|
|
87
|
+
if (req.url === '/json/version') {
|
|
88
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
89
|
+
res.end(
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
Browser: 'Chrome/999.0.0.0',
|
|
92
|
+
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/browser/fake-browser-id`,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
} else if (req.url === '/json/list') {
|
|
96
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify(targets));
|
|
98
|
+
} else {
|
|
99
|
+
res.writeHead(404);
|
|
100
|
+
res.end();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
s.once('error', reject);
|
|
104
|
+
s.listen(port, '127.0.0.1', () => {
|
|
105
|
+
servers.push(s);
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PORT_FILE = () => join(tempDir, '.monomind', 'monobrowse', 'active-port.json');
|
|
112
|
+
|
|
113
|
+
async function writePersistedPort(record: {
|
|
114
|
+
port: number;
|
|
115
|
+
pid?: number;
|
|
116
|
+
launched: boolean;
|
|
117
|
+
savedAt: number;
|
|
118
|
+
}): Promise<void> {
|
|
119
|
+
await mkdir(join(tempDir, '.monomind', 'monobrowse'), { recursive: true });
|
|
120
|
+
await writeFile(PORT_FILE(), JSON.stringify(record), 'utf-8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function portFileExists(): Promise<boolean> {
|
|
124
|
+
return readFile(PORT_FILE(), 'utf-8').then(
|
|
125
|
+
() => true,
|
|
126
|
+
() => false,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function loadBrowser() {
|
|
131
|
+
vi.resetModules();
|
|
132
|
+
return import('../browser/browser.js');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A page target as Chrome reports it when NO debugger is attached to it. */
|
|
136
|
+
function idlePageTarget(id: string, port: number): Partial<CdpTarget> {
|
|
137
|
+
return {
|
|
138
|
+
id,
|
|
139
|
+
type: 'page',
|
|
140
|
+
title: 'Example',
|
|
141
|
+
url: 'https://example.com/',
|
|
142
|
+
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/page/${id}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe('#310 reapIdleLaunchedBrowser — give back a CDP port nothing is using', () => {
|
|
147
|
+
it('reaps a launched instance that has had zero targets past the idle threshold', async () => {
|
|
148
|
+
const port = BASE + 0;
|
|
149
|
+
await fakeChrome(port, []);
|
|
150
|
+
await writePersistedPort({
|
|
151
|
+
port,
|
|
152
|
+
pid: 54321,
|
|
153
|
+
launched: true,
|
|
154
|
+
savedAt: Date.now() - 45 * MINUTE,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const { reapIdleLaunchedBrowser } = await loadBrowser();
|
|
158
|
+
await expect(reapIdleLaunchedBrowser()).resolves.toBe(port);
|
|
159
|
+
|
|
160
|
+
expect(lastSocket?.url).toBe(`ws://127.0.0.1:${port}/devtools/browser/fake-browser-id`);
|
|
161
|
+
expect(lastSocket?.methods()).toContain('Browser.close');
|
|
162
|
+
// The port file is the session handle — a reaped session must not leave
|
|
163
|
+
// one behind for the next command to chase.
|
|
164
|
+
expect(await portFileExists()).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('does NOT reap a launched instance that still has open page targets', async () => {
|
|
168
|
+
const port = BASE + 1;
|
|
169
|
+
await fakeChrome(port, [idlePageTarget('AAA', port)]);
|
|
170
|
+
await writePersistedPort({
|
|
171
|
+
port,
|
|
172
|
+
pid: 54322,
|
|
173
|
+
launched: true,
|
|
174
|
+
savedAt: Date.now() - 45 * MINUTE,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const { reapIdleLaunchedBrowser } = await loadBrowser();
|
|
178
|
+
await expect(reapIdleLaunchedBrowser()).resolves.toBeNull();
|
|
179
|
+
|
|
180
|
+
expect(lastSocket).toBeNull();
|
|
181
|
+
expect(killSpy).not.toHaveBeenCalled();
|
|
182
|
+
expect(await portFileExists()).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('does NOT reap a browser this tool never launched, however idle or old it is', async () => {
|
|
186
|
+
// `connect` records launched:false — that browser belongs to the user.
|
|
187
|
+
const port = BASE + 2;
|
|
188
|
+
await fakeChrome(port, []);
|
|
189
|
+
await writePersistedPort({
|
|
190
|
+
port,
|
|
191
|
+
pid: 54323,
|
|
192
|
+
launched: false,
|
|
193
|
+
savedAt: Date.now() - 48 * 60 * MINUTE,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const { reapIdleLaunchedBrowser } = await loadBrowser();
|
|
197
|
+
await expect(reapIdleLaunchedBrowser()).resolves.toBeNull();
|
|
198
|
+
|
|
199
|
+
expect(lastSocket).toBeNull();
|
|
200
|
+
expect(killSpy).not.toHaveBeenCalled();
|
|
201
|
+
expect(await portFileExists()).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('does NOT reap while a client is still attached to a target', async () => {
|
|
205
|
+
// Chrome omits webSocketDebuggerUrl from a target that already has a
|
|
206
|
+
// debugger attached — an in-flight session, not an abandoned browser.
|
|
207
|
+
const port = BASE + 3;
|
|
208
|
+
await fakeChrome(port, [
|
|
209
|
+
{ id: 'BBB', type: 'page', title: 'Example', url: 'https://example.com/' },
|
|
210
|
+
]);
|
|
211
|
+
await writePersistedPort({
|
|
212
|
+
port,
|
|
213
|
+
pid: 54324,
|
|
214
|
+
launched: true,
|
|
215
|
+
savedAt: Date.now() - 45 * MINUTE,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const { reapIdleLaunchedBrowser } = await loadBrowser();
|
|
219
|
+
await expect(reapIdleLaunchedBrowser()).resolves.toBeNull();
|
|
220
|
+
|
|
221
|
+
expect(lastSocket).toBeNull();
|
|
222
|
+
expect(await portFileExists()).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('does NOT reap an instance that is still inside the idle threshold', async () => {
|
|
226
|
+
// A live session can momentarily have no targets (last tab closed, next
|
|
227
|
+
// command about to open one) — reaping that would break reattach.
|
|
228
|
+
const port = BASE + 4;
|
|
229
|
+
await fakeChrome(port, []);
|
|
230
|
+
await writePersistedPort({ port, pid: 54325, launched: true, savedAt: Date.now() - MINUTE });
|
|
231
|
+
|
|
232
|
+
const { reapIdleLaunchedBrowser } = await loadBrowser();
|
|
233
|
+
await expect(reapIdleLaunchedBrowser()).resolves.toBeNull();
|
|
234
|
+
|
|
235
|
+
expect(lastSocket).toBeNull();
|
|
236
|
+
expect(await portFileExists()).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('does NOT reap when the persisted record has no savedAt (age unknowable)', async () => {
|
|
240
|
+
const port = BASE + 5;
|
|
241
|
+
await fakeChrome(port, []);
|
|
242
|
+
await mkdir(join(tempDir, '.monomind', 'monobrowse'), { recursive: true });
|
|
243
|
+
await writeFile(PORT_FILE(), JSON.stringify({ port, pid: 54326, launched: true }), 'utf-8');
|
|
244
|
+
|
|
245
|
+
const { reapIdleLaunchedBrowser } = await loadBrowser();
|
|
246
|
+
await expect(reapIdleLaunchedBrowser()).resolves.toBeNull();
|
|
247
|
+
expect(lastSocket).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('is a no-op with no persisted session at all', async () => {
|
|
251
|
+
const { reapIdleLaunchedBrowser } = await loadBrowser();
|
|
252
|
+
await expect(reapIdleLaunchedBrowser()).resolves.toBeNull();
|
|
253
|
+
expect(killSpy).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('launchBrowser reaps the stale instance before deciding what to do with the port', async () => {
|
|
257
|
+
// The wiring that makes the reaper run at all: every launch/attach is a
|
|
258
|
+
// chance to give back a port a previous session abandoned.
|
|
259
|
+
const port = BASE + 6;
|
|
260
|
+
await fakeChrome(port, []);
|
|
261
|
+
await writePersistedPort({
|
|
262
|
+
port,
|
|
263
|
+
pid: 54327,
|
|
264
|
+
launched: true,
|
|
265
|
+
savedAt: Date.now() - 45 * MINUTE,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const { launchBrowser } = await loadBrowser();
|
|
269
|
+
// The fake Chrome stays listening, so launchBrowser attaches to it after
|
|
270
|
+
// the reap rather than spawning a real browser.
|
|
271
|
+
await expect(launchBrowser({ port })).resolves.toBe(port);
|
|
272
|
+
|
|
273
|
+
expect(lastSocket?.methods()).toContain('Browser.close');
|
|
274
|
+
expect(await portFileExists()).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import {
|
|
1
|
+
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
6
|
// We need to mock process.cwd() before importing ref-cache so CACHE_DIR
|
|
7
7
|
// resolves to our temp directory.
|
|
@@ -26,7 +26,9 @@ async function importRefCache() {
|
|
|
26
26
|
return import('../browser/ref-cache.js');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function makeRefs(
|
|
29
|
+
function makeRefs(
|
|
30
|
+
count = 2,
|
|
31
|
+
): Map<string, { ref: string; role: string; name: string; nodeId: number }> {
|
|
30
32
|
const map = new Map();
|
|
31
33
|
for (let i = 1; i <= count; i++) {
|
|
32
34
|
map.set(`ref${i}`, { ref: `ref${i}`, role: 'button', name: `Button ${i}`, nodeId: i });
|
|
@@ -68,7 +70,7 @@ describe('ref-cache', () => {
|
|
|
68
70
|
|
|
69
71
|
// Now overwrite the file with invalid JSON
|
|
70
72
|
const cacheFile = join(tempDir, '.monomind', 'monobrowse', 'ax-snapshot.json');
|
|
71
|
-
const { writeFile: wf } = await import('fs/promises');
|
|
73
|
+
const { writeFile: wf } = await import('node:fs/promises');
|
|
72
74
|
await wf(cacheFile, '{{{not valid json!!!');
|
|
73
75
|
|
|
74
76
|
const loaded = await mod.loadRefCache('t');
|
|
@@ -117,7 +119,7 @@ describe('ref-cache', () => {
|
|
|
117
119
|
const cacheFile = join(tempDir, '.monomind', 'monobrowse', 'ax-snapshot.json');
|
|
118
120
|
const raw = JSON.parse(await readFile(cacheFile, 'utf8'));
|
|
119
121
|
raw.savedAt = Date.now() - mod.REF_CACHE_STALE_MS - 5000;
|
|
120
|
-
const { writeFile: wf } = await import('fs/promises');
|
|
122
|
+
const { writeFile: wf } = await import('node:fs/promises');
|
|
121
123
|
await wf(cacheFile, JSON.stringify(raw));
|
|
122
124
|
|
|
123
125
|
const loaded = await mod.loadRefCache('t1');
|
|
@@ -138,13 +140,21 @@ describe('active-port persistence', () => {
|
|
|
138
140
|
const mod = await importRefCache();
|
|
139
141
|
await mod.saveActivePort(9333);
|
|
140
142
|
expect(await mod.loadActivePort()).toBe(9333);
|
|
141
|
-
expect(await mod.loadActivePortInfo()).toEqual({
|
|
143
|
+
expect(await mod.loadActivePortInfo()).toEqual({
|
|
144
|
+
port: 9333,
|
|
145
|
+
launched: true,
|
|
146
|
+
savedAt: expect.any(Number),
|
|
147
|
+
});
|
|
142
148
|
});
|
|
143
149
|
|
|
144
150
|
it('connect provenance: launched:false survives the round-trip', async () => {
|
|
145
151
|
const mod = await importRefCache();
|
|
146
152
|
await mod.saveActivePort(9229, { launched: false });
|
|
147
|
-
expect(await mod.loadActivePortInfo()).toEqual({
|
|
153
|
+
expect(await mod.loadActivePortInfo()).toEqual({
|
|
154
|
+
port: 9229,
|
|
155
|
+
launched: false,
|
|
156
|
+
savedAt: expect.any(Number),
|
|
157
|
+
});
|
|
148
158
|
});
|
|
149
159
|
|
|
150
160
|
it('clear removes the file; load returns null afterwards', async () => {
|
|
@@ -196,11 +206,14 @@ describe('active-port persistence', () => {
|
|
|
196
206
|
|
|
197
207
|
it('#115: a non-numeric or non-positive persisted pid is dropped, not trusted', async () => {
|
|
198
208
|
const mod = await importRefCache();
|
|
199
|
-
const { writeFile, mkdir } = await import('fs/promises');
|
|
209
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
200
210
|
const dir = join(process.cwd(), '.monomind', 'monobrowse');
|
|
201
211
|
await mkdir(dir, { recursive: true });
|
|
202
212
|
for (const badPid of ['54321', -1, 0, 1.5, null]) {
|
|
203
|
-
await writeFile(
|
|
213
|
+
await writeFile(
|
|
214
|
+
join(dir, 'active-port.json'),
|
|
215
|
+
JSON.stringify({ port: 9333, launched: true, pid: badPid }),
|
|
216
|
+
);
|
|
204
217
|
const info = await mod.loadActivePortInfo();
|
|
205
218
|
expect(info!.pid).toBeUndefined();
|
|
206
219
|
}
|
|
@@ -208,7 +221,7 @@ describe('active-port persistence', () => {
|
|
|
208
221
|
|
|
209
222
|
it('rejects out-of-range or non-integer persisted ports', async () => {
|
|
210
223
|
const mod = await importRefCache();
|
|
211
|
-
const { writeFile, mkdir } = await import('fs/promises');
|
|
224
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
212
225
|
const dir = join(process.cwd(), '.monomind', 'monobrowse');
|
|
213
226
|
await mkdir(dir, { recursive: true });
|
|
214
227
|
for (const bad of [80, 70000, 1.5, '9222', null]) {
|
|
@@ -6,23 +6,21 @@
|
|
|
6
6
|
* validation logic: the CdpClient is a stub that answers the handful of CDP
|
|
7
7
|
* commands the round trip issues.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
|
|
10
|
+
import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import type { CdpClient } from '../browser/cdp.js';
|
|
14
|
-
import {
|
|
15
|
-
saveStateFile,
|
|
16
|
-
loadStateFile,
|
|
17
|
-
saveSession,
|
|
18
|
-
loadSession,
|
|
19
|
-
listSessions,
|
|
20
|
-
} from '../browser/session.js';
|
|
15
|
+
import { loadSession, loadStateFile, saveSession, saveStateFile } from '../browser/session.js';
|
|
21
16
|
|
|
22
17
|
const COOKIES = [{ name: 'sid', value: 'abc', domain: 'x.test', path: '/' }];
|
|
23
18
|
|
|
24
19
|
/** Records every CDP command and answers the ones session.ts depends on. */
|
|
25
|
-
function stubClient(): {
|
|
20
|
+
function stubClient(): {
|
|
21
|
+
client: CdpClient;
|
|
22
|
+
calls: Array<{ method: string; params: unknown; sid?: string }>;
|
|
23
|
+
} {
|
|
26
24
|
const calls: Array<{ method: string; params: unknown; sid?: string }> = [];
|
|
27
25
|
const client = {
|
|
28
26
|
send: vi.fn(async (method: string, params: unknown, sid?: string) => {
|
|
@@ -30,8 +28,10 @@ function stubClient(): { client: CdpClient; calls: Array<{ method: string; param
|
|
|
30
28
|
if (method === 'Network.getCookies') return { cookies: COOKIES };
|
|
31
29
|
if (method === 'Runtime.evaluate') {
|
|
32
30
|
const expr = (params as { expression: string }).expression;
|
|
33
|
-
if (expr.includes('Object.entries(localStorage)'))
|
|
34
|
-
|
|
31
|
+
if (expr.includes('Object.entries(localStorage)'))
|
|
32
|
+
return { result: { value: '{"k":"v"}' } };
|
|
33
|
+
if (expr.includes('Object.entries(sessionStorage)'))
|
|
34
|
+
return { result: { value: '{"s":"1"}' } };
|
|
35
35
|
}
|
|
36
36
|
return {};
|
|
37
37
|
}),
|
|
@@ -69,7 +69,7 @@ describe('validateSessionName (via saveSession/loadSession)', () => {
|
|
|
69
69
|
for (const [label, name] of bad) {
|
|
70
70
|
it(`rejects ${label}`, async () => {
|
|
71
71
|
await expect(saveSession(client, 'S1', 'T1', name, 'https://x.test', 'T')).rejects.toThrow(
|
|
72
|
-
/Invalid session name
|
|
72
|
+
/Invalid session name/,
|
|
73
73
|
);
|
|
74
74
|
await expect(loadSession(client, 'S1', name)).rejects.toThrow(/Invalid session name/);
|
|
75
75
|
});
|
|
@@ -77,7 +77,7 @@ describe('validateSessionName (via saveSession/loadSession)', () => {
|
|
|
77
77
|
|
|
78
78
|
it('accepts an ordinary name (fails later, on the missing file, not on validation)', async () => {
|
|
79
79
|
await expect(loadSession(client, 'S1', 'no-such-session-xyz')).rejects.toThrow(
|
|
80
|
-
'Session not found: no-such-session-xyz'
|
|
80
|
+
'Session not found: no-such-session-xyz',
|
|
81
81
|
);
|
|
82
82
|
});
|
|
83
83
|
|
|
@@ -103,7 +103,7 @@ describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
|
|
|
103
103
|
for (const [label, filePath] of bad) {
|
|
104
104
|
it(`rejects ${label}`, async () => {
|
|
105
105
|
await expect(
|
|
106
|
-
saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T')
|
|
106
|
+
saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T'),
|
|
107
107
|
).rejects.toThrow(/Invalid file path/);
|
|
108
108
|
await expect(loadStateFile(client, 'S1', filePath)).rejects.toThrow(/Invalid file path/);
|
|
109
109
|
});
|
|
@@ -111,7 +111,9 @@ describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
|
|
|
111
111
|
|
|
112
112
|
it('allows a plain absolute path', async () => {
|
|
113
113
|
const p = join(dir, 'state.json');
|
|
114
|
-
await expect(
|
|
114
|
+
await expect(
|
|
115
|
+
saveStateFile(client, 'S1', 'T1', p, 'https://x.test', 'T'),
|
|
116
|
+
).resolves.toBeUndefined();
|
|
115
117
|
});
|
|
116
118
|
});
|
|
117
119
|
|
|
@@ -164,38 +166,41 @@ describe('state file round trip', () => {
|
|
|
164
166
|
it('rejects a state file whose cookies field is not an array', async () => {
|
|
165
167
|
const { client } = stubClient();
|
|
166
168
|
const p = join(dir, 'bad.json');
|
|
167
|
-
const { writeFile } = await import('fs/promises');
|
|
169
|
+
const { writeFile } = await import('node:fs/promises');
|
|
168
170
|
await writeFile(p, JSON.stringify({ cookies: { name: 'sid' } }));
|
|
169
171
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
170
|
-
'Invalid state file: cookies is not an array'
|
|
172
|
+
'Invalid state file: cookies is not an array',
|
|
171
173
|
);
|
|
172
174
|
});
|
|
173
175
|
|
|
174
176
|
it('rejects a state file whose localStorage is an array rather than an object', async () => {
|
|
175
177
|
const { client } = stubClient();
|
|
176
178
|
const p = join(dir, 'bad2.json');
|
|
177
|
-
const { writeFile } = await import('fs/promises');
|
|
179
|
+
const { writeFile } = await import('node:fs/promises');
|
|
178
180
|
await writeFile(p, JSON.stringify({ cookies: [], localStorage: ['nope'] }));
|
|
179
181
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
180
|
-
'Invalid state file: localStorage is not a plain object'
|
|
182
|
+
'Invalid state file: localStorage is not a plain object',
|
|
181
183
|
);
|
|
182
184
|
});
|
|
183
185
|
|
|
184
186
|
it('rejects a state file whose sessionStorage is an array', async () => {
|
|
185
187
|
const { client } = stubClient();
|
|
186
188
|
const p = join(dir, 'bad3.json');
|
|
187
|
-
const { writeFile } = await import('fs/promises');
|
|
189
|
+
const { writeFile } = await import('node:fs/promises');
|
|
188
190
|
await writeFile(p, JSON.stringify({ cookies: [], sessionStorage: ['nope'] }));
|
|
189
191
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
190
|
-
'Invalid state file: sessionStorage is not a plain object'
|
|
192
|
+
'Invalid state file: sessionStorage is not a plain object',
|
|
191
193
|
);
|
|
192
194
|
});
|
|
193
195
|
|
|
194
196
|
it('accepts a state file with neither storage map present', async () => {
|
|
195
197
|
const { client, calls } = stubClient();
|
|
196
198
|
const p = join(dir, 'minimal.json');
|
|
197
|
-
const { writeFile } = await import('fs/promises');
|
|
198
|
-
await writeFile(
|
|
199
|
+
const { writeFile } = await import('node:fs/promises');
|
|
200
|
+
await writeFile(
|
|
201
|
+
p,
|
|
202
|
+
JSON.stringify({ targetId: 'T', sessionId: 'S', url: '', title: '', cookies: [] }),
|
|
203
|
+
);
|
|
199
204
|
await expect(loadStateFile(client, 'S1', p)).resolves.toMatchObject({ cookies: [] });
|
|
200
205
|
expect(calls.filter((c) => c.method === 'Runtime.evaluate')).toEqual([]);
|
|
201
206
|
});
|
|
@@ -224,7 +229,7 @@ describe('listSessions', () => {
|
|
|
224
229
|
it('lists only .json files, with the extension stripped', async () => {
|
|
225
230
|
const sessionDir = join(dir, '.monomind', 'browser-sessions');
|
|
226
231
|
await mkdir(sessionDir, { recursive: true });
|
|
227
|
-
const { writeFile } = await import('fs/promises');
|
|
232
|
+
const { writeFile } = await import('node:fs/promises');
|
|
228
233
|
await writeFile(join(sessionDir, 'alpha.json'), '{}');
|
|
229
234
|
await writeFile(join(sessionDir, 'beta.json'), '{}');
|
|
230
235
|
await writeFile(join(sessionDir, 'notes.txt'), 'ignore me');
|
|
@@ -236,7 +241,14 @@ describe('listSessions', () => {
|
|
|
236
241
|
it('saveSession writes into the session dir and loadSession reads it back', async () => {
|
|
237
242
|
const mod = await withHome(dir);
|
|
238
243
|
const saver = stubClient();
|
|
239
|
-
const filePath = await mod.saveSession(
|
|
244
|
+
const filePath = await mod.saveSession(
|
|
245
|
+
saver.client,
|
|
246
|
+
'S1',
|
|
247
|
+
'T1',
|
|
248
|
+
'my-session',
|
|
249
|
+
'https://x.test',
|
|
250
|
+
'T',
|
|
251
|
+
);
|
|
240
252
|
expect(filePath).toBe(join(dir, '.monomind', 'browser-sessions', 'my-session.json'));
|
|
241
253
|
expect((await stat(filePath)).mode & 0o777).toBe(0o600);
|
|
242
254
|
expect((await stat(join(dir, '.monomind', 'browser-sessions'))).mode & 0o777).toBe(0o700);
|