@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.
Files changed (220) hide show
  1. package/dist/src/__tests__/adapters.test.js +5 -2
  2. package/dist/src/__tests__/adapters.test.js.map +1 -1
  3. package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
  4. package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
  5. package/dist/src/__tests__/box-model.test.js +9 -5
  6. package/dist/src/__tests__/box-model.test.js.map +1 -1
  7. package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
  8. package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
  9. package/dist/src/__tests__/browser-launch.test.js +17 -10
  10. package/dist/src/__tests__/browser-launch.test.js.map +1 -1
  11. package/dist/src/__tests__/cdp-client.test.js +9 -3
  12. package/dist/src/__tests__/cdp-client.test.js.map +1 -1
  13. package/dist/src/__tests__/close-browser.test.d.ts +13 -0
  14. package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
  15. package/dist/src/__tests__/close-browser.test.js +5 -5
  16. package/dist/src/__tests__/close-browser.test.js.map +1 -1
  17. package/dist/src/__tests__/console-log.test.js +17 -6
  18. package/dist/src/__tests__/console-log.test.js.map +1 -1
  19. package/dist/src/__tests__/find.test.js +26 -6
  20. package/dist/src/__tests__/find.test.js.map +1 -1
  21. package/dist/src/__tests__/frame-command.test.js +9 -3
  22. package/dist/src/__tests__/frame-command.test.js.map +1 -1
  23. package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
  24. package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
  25. package/dist/src/__tests__/output.test.js +1 -1
  26. package/dist/src/__tests__/output.test.js.map +1 -1
  27. package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
  28. package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
  29. package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
  30. package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
  31. package/dist/src/__tests__/ref-cache.test.js +18 -10
  32. package/dist/src/__tests__/ref-cache.test.js.map +1 -1
  33. package/dist/src/__tests__/session-state.test.d.ts +8 -0
  34. package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
  35. package/dist/src/__tests__/session-state.test.js +10 -10
  36. package/dist/src/__tests__/session-state.test.js.map +1 -1
  37. package/dist/src/__tests__/snapshot.test.js +44 -14
  38. package/dist/src/__tests__/snapshot.test.js.map +1 -1
  39. package/dist/src/__tests__/storage-eval.test.js +14 -6
  40. package/dist/src/__tests__/storage-eval.test.js.map +1 -1
  41. package/dist/src/__tests__/tabs.test.js +25 -9
  42. package/dist/src/__tests__/tabs.test.js.map +1 -1
  43. package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
  44. package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
  45. package/dist/src/browser/action-builder/analyzer.js +25 -9
  46. package/dist/src/browser/action-builder/analyzer.js.map +1 -1
  47. package/dist/src/browser/action-builder/types.d.ts.map +1 -1
  48. package/dist/src/browser/actions.d.ts +1 -1
  49. package/dist/src/browser/actions.d.ts.map +1 -1
  50. package/dist/src/browser/actions.js +16 -6
  51. package/dist/src/browser/actions.js.map +1 -1
  52. package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
  53. package/dist/src/browser/adapters/gemini.js.map +1 -1
  54. package/dist/src/browser/adapters/google.d.ts.map +1 -1
  55. package/dist/src/browser/adapters/google.js.map +1 -1
  56. package/dist/src/browser/adapters/index.d.ts.map +1 -1
  57. package/dist/src/browser/adapters/index.js +3 -3
  58. package/dist/src/browser/adapters/index.js.map +1 -1
  59. package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
  60. package/dist/src/browser/adapters/instagram.js.map +1 -1
  61. package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
  62. package/dist/src/browser/adapters/linkedin.js.map +1 -1
  63. package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
  64. package/dist/src/browser/adapters/microsoft.js.map +1 -1
  65. package/dist/src/browser/adapters/x.d.ts.map +1 -1
  66. package/dist/src/browser/adapters/x.js.map +1 -1
  67. package/dist/src/browser/browser.d.ts +30 -0
  68. package/dist/src/browser/browser.d.ts.map +1 -1
  69. package/dist/src/browser/browser.js +122 -15
  70. package/dist/src/browser/browser.js.map +1 -1
  71. package/dist/src/browser/cdp.d.ts.map +1 -1
  72. package/dist/src/browser/cdp.js +21 -8
  73. package/dist/src/browser/cdp.js.map +1 -1
  74. package/dist/src/browser/console-log.d.ts.map +1 -1
  75. package/dist/src/browser/console-log.js +2 -2
  76. package/dist/src/browser/console-log.js.map +1 -1
  77. package/dist/src/browser/dashboard/server.d.ts.map +1 -1
  78. package/dist/src/browser/dashboard/server.js +29 -19
  79. package/dist/src/browser/dashboard/server.js.map +1 -1
  80. package/dist/src/browser/dashboard/ui.html +153 -165
  81. package/dist/src/browser/dialog.d.ts.map +1 -1
  82. package/dist/src/browser/dialog.js +3 -1
  83. package/dist/src/browser/dialog.js.map +1 -1
  84. package/dist/src/browser/emulation.d.ts.map +1 -1
  85. package/dist/src/browser/emulation.js +21 -6
  86. package/dist/src/browser/emulation.js.map +1 -1
  87. package/dist/src/browser/find.d.ts +4 -4
  88. package/dist/src/browser/find.d.ts.map +1 -1
  89. package/dist/src/browser/find.js +14 -6
  90. package/dist/src/browser/find.js.map +1 -1
  91. package/dist/src/browser/har.d.ts.map +1 -1
  92. package/dist/src/browser/har.js +14 -7
  93. package/dist/src/browser/har.js.map +1 -1
  94. package/dist/src/browser/index.d.ts +16 -16
  95. package/dist/src/browser/index.d.ts.map +1 -1
  96. package/dist/src/browser/index.js +16 -16
  97. package/dist/src/browser/index.js.map +1 -1
  98. package/dist/src/browser/network.d.ts.map +1 -1
  99. package/dist/src/browser/network.js +14 -4
  100. package/dist/src/browser/network.js.map +1 -1
  101. package/dist/src/browser/pdf.d.ts.map +1 -1
  102. package/dist/src/browser/pdf.js +3 -3
  103. package/dist/src/browser/pdf.js.map +1 -1
  104. package/dist/src/browser/profiler.d.ts.map +1 -1
  105. package/dist/src/browser/profiler.js +10 -5
  106. package/dist/src/browser/profiler.js.map +1 -1
  107. package/dist/src/browser/record.d.ts +1 -1
  108. package/dist/src/browser/record.d.ts.map +1 -1
  109. package/dist/src/browser/record.js +18 -7
  110. package/dist/src/browser/record.js.map +1 -1
  111. package/dist/src/browser/ref-cache.d.ts.map +1 -1
  112. package/dist/src/browser/ref-cache.js +9 -4
  113. package/dist/src/browser/ref-cache.js.map +1 -1
  114. package/dist/src/browser/screenshot.d.ts.map +1 -1
  115. package/dist/src/browser/screenshot.js +11 -7
  116. package/dist/src/browser/screenshot.js.map +1 -1
  117. package/dist/src/browser/session.d.ts +1 -1
  118. package/dist/src/browser/session.d.ts.map +1 -1
  119. package/dist/src/browser/session.js +27 -8
  120. package/dist/src/browser/session.js.map +1 -1
  121. package/dist/src/browser/snapshot.d.ts +1 -1
  122. package/dist/src/browser/snapshot.d.ts.map +1 -1
  123. package/dist/src/browser/snapshot.js +8 -4
  124. package/dist/src/browser/snapshot.js.map +1 -1
  125. package/dist/src/browser/storage.d.ts.map +1 -1
  126. package/dist/src/browser/storage.js.map +1 -1
  127. package/dist/src/browser/tabs.d.ts.map +1 -1
  128. package/dist/src/browser/tabs.js +9 -3
  129. package/dist/src/browser/tabs.js.map +1 -1
  130. package/dist/src/browser/trace.d.ts.map +1 -1
  131. package/dist/src/browser/trace.js +3 -3
  132. package/dist/src/browser/trace.js.map +1 -1
  133. package/dist/src/browser/types.d.ts.map +1 -1
  134. package/dist/src/browser/types.js +23 -4
  135. package/dist/src/browser/types.js.map +1 -1
  136. package/dist/src/browser/vitals.d.ts.map +1 -1
  137. package/dist/src/browser/vitals.js +1 -1
  138. package/dist/src/browser/vitals.js.map +1 -1
  139. package/dist/src/browser/wait.d.ts.map +1 -1
  140. package/dist/src/browser/wait.js +11 -5
  141. package/dist/src/browser/wait.js.map +1 -1
  142. package/dist/src/cli/action.d.ts.map +1 -1
  143. package/dist/src/cli/action.js +14 -11
  144. package/dist/src/cli/action.js.map +1 -1
  145. package/dist/src/cli/commands.d.ts.map +1 -1
  146. package/dist/src/cli/commands.js +453 -159
  147. package/dist/src/cli/commands.js.map +1 -1
  148. package/dist/src/cli/output.js +5 -5
  149. package/dist/src/cli/output.js.map +1 -1
  150. package/dist/src/cli/platform.d.ts.map +1 -1
  151. package/dist/src/cli/platform.js +15 -11
  152. package/dist/src/cli/platform.js.map +1 -1
  153. package/dist/src/cli/types.d.ts +1 -1
  154. package/dist/src/cli/types.d.ts.map +1 -1
  155. package/dist/src/cli.js +21 -9
  156. package/dist/src/cli.js.map +1 -1
  157. package/dist/src/index.d.ts +2 -2
  158. package/dist/src/index.d.ts.map +1 -1
  159. package/dist/src/index.js +2 -2
  160. package/dist/src/index.js.map +1 -1
  161. package/dist/tsconfig.tsbuildinfo +1 -1
  162. package/package.json +1 -1
  163. package/src/__tests__/adapters.test.ts +15 -8
  164. package/src/__tests__/batch-eval-parsing.test.ts +4 -2
  165. package/src/__tests__/box-model.test.ts +9 -5
  166. package/src/__tests__/browser-launch.test.ts +26 -15
  167. package/src/__tests__/cdp-client.test.ts +25 -9
  168. package/src/__tests__/close-browser.test.ts +11 -6
  169. package/src/__tests__/console-log.test.ts +36 -13
  170. package/src/__tests__/find.test.ts +58 -20
  171. package/src/__tests__/frame-command.test.ts +12 -6
  172. package/src/__tests__/open-url-navigate-error.test.ts +2 -4
  173. package/src/__tests__/output.test.ts +1 -1
  174. package/src/__tests__/reap-idle-browser.test.ts +276 -0
  175. package/src/__tests__/ref-cache.test.ts +25 -12
  176. package/src/__tests__/session-state.test.ts +40 -28
  177. package/src/__tests__/snapshot.test.ts +60 -21
  178. package/src/__tests__/storage-eval.test.ts +30 -16
  179. package/src/__tests__/tabs.test.ts +26 -10
  180. package/src/browser/action-builder/analyzer.ts +39 -15
  181. package/src/browser/action-builder/types.ts +45 -8
  182. package/src/browser/actions.ts +288 -123
  183. package/src/browser/adapters/gemini.ts +7 -3
  184. package/src/browser/adapters/google.ts +5 -3
  185. package/src/browser/adapters/index.ts +5 -4
  186. package/src/browser/adapters/instagram.ts +4 -2
  187. package/src/browser/adapters/linkedin.ts +4 -2
  188. package/src/browser/adapters/microsoft.ts +9 -5
  189. package/src/browser/adapters/x.ts +7 -3
  190. package/src/browser/browser.ts +188 -42
  191. package/src/browser/cdp.ts +43 -11
  192. package/src/browser/console-log.ts +24 -7
  193. package/src/browser/dashboard/server.ts +44 -25
  194. package/src/browser/dashboard/ui.html +153 -165
  195. package/src/browser/dialog.ts +25 -8
  196. package/src/browser/emulation.ts +86 -36
  197. package/src/browser/find.ts +110 -47
  198. package/src/browser/har.ts +58 -26
  199. package/src/browser/index.ts +16 -16
  200. package/src/browser/network.ts +147 -46
  201. package/src/browser/pdf.ts +4 -4
  202. package/src/browser/profiler.ts +23 -10
  203. package/src/browser/record.ts +38 -19
  204. package/src/browser/ref-cache.ts +38 -17
  205. package/src/browser/screenshot.ts +58 -32
  206. package/src/browser/session.ts +51 -18
  207. package/src/browser/snapshot.ts +34 -20
  208. package/src/browser/storage.ts +73 -17
  209. package/src/browser/tabs.ts +29 -17
  210. package/src/browser/trace.ts +14 -10
  211. package/src/browser/types.ts +23 -4
  212. package/src/browser/vitals.ts +33 -16
  213. package/src/browser/wait.ts +47 -21
  214. package/src/cli/action.ts +52 -21
  215. package/src/cli/commands.ts +1093 -385
  216. package/src/cli/output.ts +5 -5
  217. package/src/cli/platform.ts +44 -23
  218. package/src/cli/types.ts +1 -1
  219. package/src/cli.ts +34 -18
  220. 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 { describe, it, expect, beforeEach, vi } from 'vitest';
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 { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { mkdtemp, readFile, rm, stat } from 'fs/promises';
3
- import { join } from 'path';
4
- import { tmpdir } from 'os';
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(count = 2): Map<string, { ref: string; role: string; name: string; nodeId: number }> {
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({ port: 9333, launched: true, savedAt: expect.any(Number) });
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({ port: 9229, launched: false, savedAt: expect.any(Number) });
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(join(dir, 'active-port.json'), JSON.stringify({ port: 9333, launched: true, pid: badPid }));
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
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
10
- import { mkdtemp, rm, readFile, stat, mkdir } from 'fs/promises';
11
- import { join } from 'path';
12
- import { tmpdir } from 'os';
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(): { client: CdpClient; calls: Array<{ method: string; params: unknown; sid?: string }> } {
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)')) return { result: { value: '{"k":"v"}' } };
34
- if (expr.includes('Object.entries(sessionStorage)')) return { result: { value: '{"s":"1"}' } };
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(saveStateFile(client, 'S1', 'T1', p, 'https://x.test', 'T')).resolves.toBeUndefined();
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(p, JSON.stringify({ targetId: 'T', sessionId: 'S', url: '', title: '', cookies: [] }));
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(saver.client, 'S1', 'T1', 'my-session', 'https://x.test', 'T');
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);