local-browser-bridge 0.1.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.
Files changed (92) hide show
  1. package/README.md +724 -0
  2. package/dist/package.json +61 -0
  3. package/dist/src/browser/chrome.d.ts +19 -0
  4. package/dist/src/browser/chrome.js +778 -0
  5. package/dist/src/browser/index.d.ts +3 -0
  6. package/dist/src/browser/index.js +25 -0
  7. package/dist/src/browser/safari.d.ts +41 -0
  8. package/dist/src/browser/safari.js +827 -0
  9. package/dist/src/browser-attach-ux-helper.d.ts +39 -0
  10. package/dist/src/browser-attach-ux-helper.js +157 -0
  11. package/dist/src/capabilities.d.ts +3 -0
  12. package/dist/src/capabilities.js +182 -0
  13. package/dist/src/chrome-relay-error-helper.d.ts +19 -0
  14. package/dist/src/chrome-relay-error-helper.js +78 -0
  15. package/dist/src/chrome-relay-helper-cli.d.ts +2 -0
  16. package/dist/src/chrome-relay-helper-cli.js +97 -0
  17. package/dist/src/chrome-relay-helper.d.ts +29 -0
  18. package/dist/src/chrome-relay-helper.js +151 -0
  19. package/dist/src/chrome-relay-state.d.ts +23 -0
  20. package/dist/src/chrome-relay-state.js +108 -0
  21. package/dist/src/claude-code.d.ts +20 -0
  22. package/dist/src/claude-code.js +66 -0
  23. package/dist/src/cli-reference-adapter.d.ts +13 -0
  24. package/dist/src/cli-reference-adapter.js +48 -0
  25. package/dist/src/cli.d.ts +3 -0
  26. package/dist/src/cli.js +200 -0
  27. package/dist/src/codex.d.ts +17 -0
  28. package/dist/src/codex.js +25 -0
  29. package/dist/src/connection-ux.d.ts +61 -0
  30. package/dist/src/connection-ux.js +256 -0
  31. package/dist/src/errors.d.ts +12 -0
  32. package/dist/src/errors.js +58 -0
  33. package/dist/src/http-reference-adapter.d.ts +34 -0
  34. package/dist/src/http-reference-adapter.js +61 -0
  35. package/dist/src/http.d.ts +3 -0
  36. package/dist/src/http.js +161 -0
  37. package/dist/src/index.d.ts +17 -0
  38. package/dist/src/index.js +43 -0
  39. package/dist/src/mcp-stdio.d.ts +2 -0
  40. package/dist/src/mcp-stdio.js +10 -0
  41. package/dist/src/mcp.d.ts +25 -0
  42. package/dist/src/mcp.js +483 -0
  43. package/dist/src/reference-adapter.d.ts +32 -0
  44. package/dist/src/reference-adapter.js +42 -0
  45. package/dist/src/service/attach-service.d.ts +28 -0
  46. package/dist/src/service/attach-service.js +272 -0
  47. package/dist/src/session-metadata.d.ts +4 -0
  48. package/dist/src/session-metadata.js +88 -0
  49. package/dist/src/store/session-store.d.ts +14 -0
  50. package/dist/src/store/session-store.js +52 -0
  51. package/dist/src/target.d.ts +9 -0
  52. package/dist/src/target.js +61 -0
  53. package/dist/src/types.d.ts +397 -0
  54. package/dist/src/types.js +2 -0
  55. package/dist/tests/attach-service.test.d.ts +1 -0
  56. package/dist/tests/attach-service.test.js +1367 -0
  57. package/dist/tests/browser-attach-ux-helper.test.d.ts +1 -0
  58. package/dist/tests/browser-attach-ux-helper.test.js +139 -0
  59. package/dist/tests/chrome-relay-error-helper.test.d.ts +1 -0
  60. package/dist/tests/chrome-relay-error-helper.test.js +67 -0
  61. package/dist/tests/chrome-relay-helper.test.d.ts +1 -0
  62. package/dist/tests/chrome-relay-helper.test.js +142 -0
  63. package/dist/tests/chrome-relay-state-schema.test.d.ts +1 -0
  64. package/dist/tests/chrome-relay-state-schema.test.js +96 -0
  65. package/dist/tests/claude-code-wrapper.test.d.ts +1 -0
  66. package/dist/tests/claude-code-wrapper.test.js +170 -0
  67. package/dist/tests/codex.test.d.ts +1 -0
  68. package/dist/tests/codex.test.js +210 -0
  69. package/dist/tests/demo-client-smoke.test.d.ts +1 -0
  70. package/dist/tests/demo-client-smoke.test.js +405 -0
  71. package/dist/tests/docs-fixtures.test.d.ts +1 -0
  72. package/dist/tests/docs-fixtures.test.js +255 -0
  73. package/dist/tests/doctor-connect-wrapper.test.d.ts +1 -0
  74. package/dist/tests/doctor-connect-wrapper.test.js +62 -0
  75. package/dist/tests/fixtures/doctor-connect-cli-stub.d.ts +1 -0
  76. package/dist/tests/fixtures/doctor-connect-cli-stub.js +93 -0
  77. package/dist/tests/fixtures/public-root-cli-stub.d.ts +210 -0
  78. package/dist/tests/fixtures/public-root-cli-stub.js +143 -0
  79. package/dist/tests/fixtures/public-root-consumer.js +67 -0
  80. package/dist/tests/mcp.test.d.ts +1 -0
  81. package/dist/tests/mcp.test.js +345 -0
  82. package/dist/tests/public-consumer-helpers.test.d.ts +1 -0
  83. package/dist/tests/public-consumer-helpers.test.js +33 -0
  84. package/dist/tests/public-package-git-consumption.test.d.ts +1 -0
  85. package/dist/tests/public-package-git-consumption.test.js +56 -0
  86. package/dist/tests/public-root-consumer-smoke.test.d.ts +1 -0
  87. package/dist/tests/public-root-consumer-smoke.test.js +214 -0
  88. package/dist/tests/reference-adapter.test.d.ts +1 -0
  89. package/dist/tests/reference-adapter.test.js +220 -0
  90. package/dist/tests/transport-reference-adapters.test.d.ts +1 -0
  91. package/dist/tests/transport-reference-adapters.test.js +214 -0
  92. package/package.json +61 -0
@@ -0,0 +1,1367 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_http_1 = require("node:http");
10
+ const node_path_1 = require("node:path");
11
+ const browser_1 = require("../src/browser");
12
+ const safari_1 = require("../src/browser/safari");
13
+ const http_1 = require("../src/http");
14
+ const attach_service_1 = require("../src/service/attach-service");
15
+ const session_store_1 = require("../src/store/session-store");
16
+ const cli_1 = require("../src/cli");
17
+ const errors_1 = require("../src/errors");
18
+ class FakeAdapter {
19
+ browser = "safari";
20
+ mode = "stable";
21
+ setMode(mode) {
22
+ this.mode = mode;
23
+ }
24
+ async listTabs() {
25
+ if (this.mode === "moved") {
26
+ return [
27
+ this.makeTab(1, 1, "Front", "https://front.example.com", true, true, "front-signature"),
28
+ this.makeTab(5, 9, "Example", "https://example.com", false, false, "example-signature")
29
+ ];
30
+ }
31
+ return [
32
+ this.makeTab(1, 1, "Front", "https://front.example.com", true, true, "front-signature"),
33
+ this.makeTab(2, 3, "Example", "https://example.com", false, false, "example-signature")
34
+ ];
35
+ }
36
+ async resolveTab(target) {
37
+ const tabs = await this.listTabs();
38
+ if (target.type === "front") {
39
+ return tabs[0];
40
+ }
41
+ if (target.type === "signature") {
42
+ const bySignature = tabs.find((tab) => tab.identity.signature === target.signature);
43
+ if (bySignature) {
44
+ return bySignature;
45
+ }
46
+ }
47
+ const matched = tabs.find((tab) => tab.windowIndex === target.windowIndex && tab.tabIndex === target.tabIndex);
48
+ strict_1.default.ok(matched);
49
+ return matched;
50
+ }
51
+ async performSessionAction(action) {
52
+ const tab = await this.resolveTab(action.target);
53
+ if (action.action === "activate") {
54
+ return {
55
+ action: "activate",
56
+ browser: this.browser,
57
+ tab,
58
+ activatedAt: "2026-01-02T00:00:00.000Z",
59
+ implementation: {
60
+ browserNative: false,
61
+ engine: "fake-adapter",
62
+ selectedTarget: true,
63
+ broughtAppToFront: true,
64
+ reorderedWindowToFront: true
65
+ }
66
+ };
67
+ }
68
+ if (action.action === "navigate") {
69
+ const requestedUrl = String(action.options.url);
70
+ return {
71
+ action: "navigate",
72
+ browser: this.browser,
73
+ requestedUrl,
74
+ previousTab: tab,
75
+ tab: this.makeTab(tab.windowIndex, tab.tabIndex, "Navigated", requestedUrl, tab.isFrontWindow, tab.isActiveInWindow),
76
+ navigatedAt: "2026-01-02T00:00:00.000Z",
77
+ implementation: {
78
+ browserNative: false,
79
+ engine: "fake-adapter",
80
+ selectedTarget: true,
81
+ broughtAppToFront: true,
82
+ reusedExistingTab: true
83
+ }
84
+ };
85
+ }
86
+ strict_1.default.equal(action.action, "screenshot");
87
+ const outputPath = action.options && typeof action.options === "object" && "outputPath" in action.options
88
+ ? String(action.options.outputPath)
89
+ : "";
90
+ strict_1.default.ok(outputPath);
91
+ await (0, promises_1.writeFile)(outputPath, "fake-png", "utf8");
92
+ return {
93
+ action: "screenshot",
94
+ browser: this.browser,
95
+ tab,
96
+ outputPath,
97
+ format: "png",
98
+ capturedAt: "2026-01-02T00:00:00.000Z",
99
+ implementation: {
100
+ browserNative: false,
101
+ engine: "fake-adapter",
102
+ scope: "window",
103
+ activatedTarget: true,
104
+ includesBrowserChrome: true
105
+ }
106
+ };
107
+ }
108
+ async getDiagnostics() {
109
+ return {
110
+ browser: this.browser,
111
+ checkedAt: "2026-01-02T00:00:00.000Z",
112
+ runtime: {
113
+ platform: process.platform,
114
+ arch: process.arch,
115
+ nodeVersion: process.version,
116
+ safariRunning: true
117
+ },
118
+ host: {
119
+ osascriptAvailable: true,
120
+ screencaptureAvailable: true,
121
+ safariApplicationAvailable: true
122
+ },
123
+ supportedFeatures: {
124
+ inspectTabs: true,
125
+ attach: true,
126
+ activate: true,
127
+ navigate: true,
128
+ screenshot: true,
129
+ savedSessions: true,
130
+ cli: true,
131
+ httpApi: true
132
+ },
133
+ constraints: ["Fake adapter constraint"],
134
+ preflight: {
135
+ inspect: {
136
+ ready: true,
137
+ checks: ["inspect"],
138
+ blockers: []
139
+ },
140
+ automation: {
141
+ ready: true,
142
+ checks: ["automation"],
143
+ blockers: []
144
+ },
145
+ screenshot: {
146
+ ready: true,
147
+ checks: ["screenshot"],
148
+ blockers: []
149
+ }
150
+ }
151
+ };
152
+ }
153
+ makeTab(windowIndex, tabIndex, title, url, isFrontWindow = false, isActiveInWindow = false, signature) {
154
+ return {
155
+ browser: "safari",
156
+ windowIndex,
157
+ tabIndex,
158
+ title,
159
+ url,
160
+ attachedAt: "2026-01-01T00:00:00.000Z",
161
+ identity: {
162
+ signature: signature ?? `${title.toLowerCase()}-${tabIndex}`,
163
+ urlKey: `${url}/`.replace(/\/\/$/, "/"),
164
+ titleKey: title.toLowerCase(),
165
+ origin: new URL(url).origin,
166
+ pathname: new URL(url).pathname
167
+ },
168
+ isFrontWindow,
169
+ isActiveInWindow
170
+ };
171
+ }
172
+ }
173
+ function createTestService(baseDir, adapter = new FakeAdapter()) {
174
+ const store = new session_store_1.SessionStore({ filePath: (0, node_path_1.resolve)(baseDir, "sessions.json") });
175
+ const service = new attach_service_1.AttachService({
176
+ store,
177
+ adapterFactory: (_browser) => adapter
178
+ });
179
+ return { service, adapter };
180
+ }
181
+ async function withCapturedStreams(run) {
182
+ let stdout = "";
183
+ let stderr = "";
184
+ const stdoutWrite = process.stdout.write.bind(process.stdout);
185
+ const stderrWrite = process.stderr.write.bind(process.stderr);
186
+ process.stdout.write = ((chunk) => {
187
+ stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
188
+ return true;
189
+ });
190
+ process.stderr.write = ((chunk) => {
191
+ stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
192
+ return true;
193
+ });
194
+ try {
195
+ await run();
196
+ return { stdout, stderr };
197
+ }
198
+ finally {
199
+ process.stdout.write = stdoutWrite;
200
+ process.stderr.write = stderrWrite;
201
+ }
202
+ }
203
+ (0, node_test_1.default)("attach persists stronger signature targets and list returns newest-first sessions", async () => {
204
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "service");
205
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
206
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
207
+ const { service } = createTestService(baseDir);
208
+ const first = await service.attach("safari");
209
+ const second = await service.attach("safari", { type: "indexed", windowIndex: 2, tabIndex: 3 });
210
+ const sessions = await service.listSessions();
211
+ strict_1.default.equal(sessions.length, 2);
212
+ strict_1.default.equal(sessions[0].id, second.id);
213
+ strict_1.default.equal(sessions[1].id, first.id);
214
+ strict_1.default.equal(sessions[0].tab.url, "https://example.com");
215
+ strict_1.default.equal(sessions[0].target.type, "signature");
216
+ strict_1.default.equal((sessions[0].target.type === "signature" && sessions[0].target.signature) || "", "example-signature");
217
+ strict_1.default.equal(sessions[0].schemaVersion, 1);
218
+ strict_1.default.equal(sessions[0].kind, "safari-actionable");
219
+ strict_1.default.equal(sessions[0].status.state, "actionable");
220
+ strict_1.default.equal(sessions[0].status.canAct, true);
221
+ strict_1.default.equal(sessions[0].capabilities.resume, true);
222
+ strict_1.default.equal(sessions[0].capabilities.activate, true);
223
+ strict_1.default.equal(sessions[0].capabilities.navigate, true);
224
+ strict_1.default.equal(sessions[0].capabilities.screenshot, true);
225
+ const persisted = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(baseDir, "sessions.json"), "utf8"));
226
+ strict_1.default.equal(persisted[0].schemaVersion, 1);
227
+ strict_1.default.equal(persisted[0].kind, "safari-actionable");
228
+ strict_1.default.equal(persisted[0].status.state, "actionable");
229
+ strict_1.default.equal(persisted[0].capabilities.activate, true);
230
+ });
231
+ (0, node_test_1.default)("resume falls back to stronger identity when tabs moved", async () => {
232
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "resume");
233
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
234
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
235
+ const { service, adapter } = createTestService(baseDir);
236
+ const session = await service.attach("safari", { type: "indexed", windowIndex: 2, tabIndex: 3 });
237
+ adapter.setMode("moved");
238
+ const resumed = await service.resumeSession(session.id);
239
+ strict_1.default.equal(resumed.session.id, session.id);
240
+ strict_1.default.equal(resumed.tab.windowIndex, 5);
241
+ strict_1.default.equal(resumed.resolution.strategy, "signature");
242
+ });
243
+ (0, node_test_1.default)("store hydrates additive session metadata for older saved payloads", async () => {
244
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "session-hydration");
245
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
246
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
247
+ const sessionPath = (0, node_path_1.resolve)(baseDir, "sessions.json");
248
+ await (0, promises_1.writeFile)(sessionPath, JSON.stringify([
249
+ {
250
+ id: "legacy-session",
251
+ browser: "chrome",
252
+ target: {
253
+ type: "signature",
254
+ signature: "example-two",
255
+ url: "https://example.com/two",
256
+ title: "Example Two"
257
+ },
258
+ tab: {
259
+ browser: "chrome",
260
+ windowIndex: 1,
261
+ tabIndex: 2,
262
+ title: "Example Two",
263
+ url: "https://example.com/two",
264
+ attachedAt: "2026-01-01T00:00:00.000Z",
265
+ identity: {
266
+ signature: "example-two",
267
+ urlKey: "https://example.com/two",
268
+ titleKey: "example two",
269
+ origin: "https://example.com",
270
+ pathname: "/two"
271
+ }
272
+ },
273
+ frontTab: {
274
+ browser: "chrome",
275
+ windowIndex: 1,
276
+ tabIndex: 2,
277
+ title: "Example Two",
278
+ url: "https://example.com/two",
279
+ attachedAt: "2026-01-01T00:00:00.000Z",
280
+ identity: {
281
+ signature: "example-two",
282
+ urlKey: "https://example.com/two",
283
+ titleKey: "example two",
284
+ origin: "https://example.com",
285
+ pathname: "/two"
286
+ }
287
+ },
288
+ createdAt: "2026-01-01T00:00:00.000Z"
289
+ }
290
+ ], null, 2) + "\n", "utf8");
291
+ const store = new session_store_1.SessionStore({ filePath: sessionPath });
292
+ const [session] = await store.list();
293
+ strict_1.default.equal(session.schemaVersion, 1);
294
+ strict_1.default.equal(session.kind, "chrome-readonly");
295
+ strict_1.default.equal(session.attach.mode, "direct");
296
+ strict_1.default.equal(session.attach.source, "user-browser");
297
+ strict_1.default.equal(session.attach.scope, "browser");
298
+ strict_1.default.equal(session.status.state, "read-only");
299
+ strict_1.default.equal(session.status.canAct, false);
300
+ strict_1.default.equal(session.capabilities.resume, true);
301
+ strict_1.default.equal(session.capabilities.activate, false);
302
+ strict_1.default.equal(session.capabilities.navigate, false);
303
+ strict_1.default.equal(session.capabilities.screenshot, false);
304
+ });
305
+ (0, node_test_1.default)("activate, navigate, diagnostics, and session navigation work through the service", async () => {
306
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "actions");
307
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
308
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
309
+ const { service, adapter } = createTestService(baseDir);
310
+ const activation = await service.activate("safari", { type: "indexed", windowIndex: 2, tabIndex: 3 });
311
+ strict_1.default.equal(activation.tab.url, "https://example.com");
312
+ const navigation = await service.navigate("safari", { type: "indexed", windowIndex: 2, tabIndex: 3 }, { url: "https://example.com/next" });
313
+ strict_1.default.equal(navigation.previousTab.url, "https://example.com");
314
+ strict_1.default.equal(navigation.tab.url, "https://example.com/next");
315
+ const diagnostics = await service.diagnostics("safari");
316
+ strict_1.default.equal(diagnostics.supportedFeatures.navigate, true);
317
+ strict_1.default.equal(diagnostics.preflight?.automation.ready, true);
318
+ const session = await service.attach("safari", { type: "indexed", windowIndex: 2, tabIndex: 3 });
319
+ adapter.setMode("moved");
320
+ const sessionNavigation = await service.navigateSession(session.id, { url: "https://example.com/renamed" });
321
+ strict_1.default.equal(sessionNavigation.navigation.previousTab.windowIndex, 5);
322
+ strict_1.default.equal(sessionNavigation.navigation.tab.url, "https://example.com/renamed");
323
+ strict_1.default.equal(sessionNavigation.session.target.type, "signature");
324
+ strict_1.default.equal(sessionNavigation.session.tab.url, "https://example.com/renamed");
325
+ });
326
+ (0, node_test_1.default)("screenshot writes a file for a target and session screenshot resolves from saved session", async () => {
327
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "screenshot");
328
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
329
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
330
+ const { service, adapter } = createTestService(baseDir);
331
+ const screenshotPath = (0, node_path_1.resolve)(baseDir, "target.png");
332
+ const targetScreenshot = await service.screenshot("safari", { type: "indexed", windowIndex: 2, tabIndex: 3 }, { outputPath: screenshotPath });
333
+ strict_1.default.equal(targetScreenshot.outputPath, screenshotPath);
334
+ strict_1.default.equal(await (0, promises_1.readFile)(screenshotPath, "utf8"), "fake-png");
335
+ const session = await service.attach("safari", { type: "indexed", windowIndex: 2, tabIndex: 3 });
336
+ adapter.setMode("moved");
337
+ const sessionScreenshotPath = (0, node_path_1.resolve)(baseDir, "session.png");
338
+ const sessionScreenshot = await service.screenshotSession(session.id, { outputPath: sessionScreenshotPath });
339
+ strict_1.default.equal(sessionScreenshot.screenshot.tab.windowIndex, 5);
340
+ strict_1.default.equal(await (0, promises_1.readFile)(sessionScreenshotPath, "utf8"), "fake-png");
341
+ });
342
+ (0, node_test_1.default)("http server exposes diagnostics, navigation, session navigation, and structured errors", async () => {
343
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "http");
344
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
345
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
346
+ const { service, adapter } = createTestService(baseDir);
347
+ const server = (0, http_1.createApiServer)(service);
348
+ await new Promise((resolvePromise) => server.listen(0, "127.0.0.1", resolvePromise));
349
+ const address = server.address();
350
+ strict_1.default.ok(address && typeof address === "object");
351
+ const baseUrl = `http://127.0.0.1:${address.port}`;
352
+ const diagnostics = await fetch(`${baseUrl}/v1/diagnostics?browser=safari`);
353
+ strict_1.default.equal(diagnostics.status, 200);
354
+ const diagnosticsPayload = (await diagnostics.json());
355
+ strict_1.default.equal(diagnosticsPayload.diagnostics.supportedFeatures.navigate, true);
356
+ strict_1.default.equal(diagnosticsPayload.diagnostics.preflight?.automation.ready, true);
357
+ const capabilities = await fetch(`${baseUrl}/v1/capabilities?browser=safari`);
358
+ strict_1.default.equal(capabilities.status, 200);
359
+ const capabilitiesPayload = (await capabilities.json());
360
+ strict_1.default.equal(capabilitiesPayload.capabilities.schemaVersion, 1);
361
+ strict_1.default.equal(capabilitiesPayload.capabilities.schema.path, "schema/capabilities.schema.json");
362
+ strict_1.default.equal(capabilitiesPayload.capabilities.schema.version, "1.0.0");
363
+ strict_1.default.equal(capabilitiesPayload.capabilities.product.manifestoPath, "docs/product-direction.md");
364
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].kind, "safari-actionable");
365
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].browser, "safari");
366
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].operations.screenshot, true);
367
+ const attach = await fetch(`${baseUrl}/v1/attach`, {
368
+ method: "POST",
369
+ headers: { "content-type": "application/json" },
370
+ body: JSON.stringify({ browser: "safari", target: { windowIndex: 2, tabIndex: 3 } })
371
+ });
372
+ const attachPayload = (await attach.json());
373
+ strict_1.default.equal(attachPayload.session.schemaVersion, 1);
374
+ strict_1.default.equal(attachPayload.session.kind, "safari-actionable");
375
+ strict_1.default.equal(attachPayload.session.status.state, "actionable");
376
+ strict_1.default.equal(attachPayload.session.capabilities.navigate, true);
377
+ strict_1.default.equal(attachPayload.session.capabilities.screenshot, true);
378
+ const navigate = await fetch(`${baseUrl}/v1/navigate`, {
379
+ method: "POST",
380
+ headers: { "content-type": "application/json" },
381
+ body: JSON.stringify({
382
+ browser: "safari",
383
+ target: { signature: "example-signature", url: "https://example.com", title: "Example" },
384
+ url: "https://example.com/next"
385
+ })
386
+ });
387
+ strict_1.default.equal(navigate.status, 201);
388
+ const navigatePayload = (await navigate.json());
389
+ strict_1.default.equal(navigatePayload.navigation.requestedUrl, "https://example.com/next");
390
+ strict_1.default.equal(navigatePayload.navigation.tab.url, "https://example.com/next");
391
+ adapter.setMode("moved");
392
+ const sessionNavigate = await fetch(`${baseUrl}/v1/sessions/${attachPayload.session.id}/navigate`, {
393
+ method: "POST",
394
+ headers: { "content-type": "application/json" },
395
+ body: JSON.stringify({ url: "https://example.com/from-session" })
396
+ });
397
+ strict_1.default.equal(sessionNavigate.status, 201);
398
+ const sessionNavigatePayload = (await sessionNavigate.json());
399
+ strict_1.default.equal(sessionNavigatePayload.sessionNavigation.navigation.previousTab.windowIndex, 5);
400
+ strict_1.default.equal(sessionNavigatePayload.sessionNavigation.navigation.tab.url, "https://example.com/from-session");
401
+ strict_1.default.equal(sessionNavigatePayload.sessionNavigation.session.tab.url, "https://example.com/from-session");
402
+ strict_1.default.equal(sessionNavigatePayload.sessionNavigation.session.status.state, "actionable");
403
+ const missingUrl = await fetch(`${baseUrl}/v1/navigate`, {
404
+ method: "POST",
405
+ headers: { "content-type": "application/json" },
406
+ body: JSON.stringify({ browser: "safari", target: { windowIndex: 2, tabIndex: 3 } })
407
+ });
408
+ strict_1.default.equal(missingUrl.status, 400);
409
+ strict_1.default.deepEqual(await missingUrl.json(), {
410
+ error: {
411
+ code: "missing_url",
412
+ message: "url is required.",
413
+ statusCode: 400
414
+ }
415
+ });
416
+ await new Promise((resolvePromise, reject) => server.close((error) => (error ? reject(error) : resolvePromise())));
417
+ });
418
+ (0, node_test_1.default)("cli prints diagnostics JSON and machine-readable errors", async () => {
419
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "cli");
420
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
421
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
422
+ const { service } = createTestService(baseDir);
423
+ const diagnosticsResult = await withCapturedStreams(async () => {
424
+ await (0, cli_1.runCli)(["diagnostics", "--browser", "safari"], service);
425
+ });
426
+ const diagnosticsPayload = JSON.parse(diagnosticsResult.stdout);
427
+ strict_1.default.equal(diagnosticsPayload.diagnostics.browser, "safari");
428
+ strict_1.default.equal(diagnosticsPayload.diagnostics.preflight?.inspect.ready, true);
429
+ const capabilitiesResult = await withCapturedStreams(async () => {
430
+ await (0, cli_1.runCli)(["capabilities", "--browser", "safari"], service);
431
+ });
432
+ const capabilitiesPayload = JSON.parse(capabilitiesResult.stdout);
433
+ strict_1.default.equal(capabilitiesPayload.capabilities.schemaVersion, 1);
434
+ strict_1.default.equal(capabilitiesPayload.capabilities.product.name, "local-browser-bridge");
435
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].kind, "safari-actionable");
436
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].browser, "safari");
437
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].operations.attach, true);
438
+ const doctorResult = await withCapturedStreams(async () => {
439
+ await (0, cli_1.runCli)(["doctor", "--route", "safari"], service);
440
+ });
441
+ const doctorPayload = JSON.parse(doctorResult.stdout);
442
+ strict_1.default.equal(doctorPayload.ok, true);
443
+ strict_1.default.equal(doctorPayload.blocked, false);
444
+ strict_1.default.equal(doctorPayload.route.name, "safari");
445
+ strict_1.default.equal(doctorPayload.route.browser, "safari");
446
+ strict_1.default.equal(doctorPayload.route.attachMode, "direct");
447
+ strict_1.default.equal(doctorPayload.routeUx.label, "Safari (actionable)");
448
+ strict_1.default.equal(doctorPayload.routeUx.readOnly, false);
449
+ strict_1.default.equal(doctorPayload.nextStep.action, "connect");
450
+ strict_1.default.equal(doctorPayload.nextStep.command, "local-browser-bridge connect --route safari");
451
+ strict_1.default.match(doctorPayload.summary, /Safari \(actionable\) is ready/i);
452
+ const attachResult = await withCapturedStreams(async () => {
453
+ await (0, cli_1.runCli)(["attach", "--browser", "safari", "--window-index", "2", "--tab-index", "3"], service);
454
+ });
455
+ const attachPayload = JSON.parse(attachResult.stdout);
456
+ strict_1.default.equal(attachPayload.session.schemaVersion, 1);
457
+ strict_1.default.equal(attachPayload.session.kind, "safari-actionable");
458
+ strict_1.default.equal(attachPayload.session.status.state, "actionable");
459
+ strict_1.default.equal(attachPayload.session.capabilities.navigate, true);
460
+ strict_1.default.equal(attachPayload.session.capabilities.screenshot, true);
461
+ const connectResult = await withCapturedStreams(async () => {
462
+ await (0, cli_1.runCli)(["connect", "--route", "safari"], service);
463
+ });
464
+ const connectPayload = JSON.parse(connectResult.stdout);
465
+ strict_1.default.equal(connectPayload.ok, true);
466
+ strict_1.default.equal(connectPayload.connected, true);
467
+ strict_1.default.equal(connectPayload.routeUx.label, "Safari (actionable)");
468
+ strict_1.default.equal(connectPayload.sessionUx.label, "Safari (actionable)");
469
+ strict_1.default.equal(connectPayload.sessionUx.readOnly, false);
470
+ strict_1.default.equal(connectPayload.session.kind, "safari-actionable");
471
+ strict_1.default.equal(connectPayload.session.status.state, "actionable");
472
+ strict_1.default.equal(connectPayload.nextStep.action, "session-ready");
473
+ strict_1.default.match(connectPayload.nextStep.prompt, /activate, navigate, or screenshot/i);
474
+ strict_1.default.match(connectPayload.summary, /Connected Safari \(actionable\) session/i);
475
+ const errorResult = await withCapturedStreams(async () => {
476
+ try {
477
+ await (0, cli_1.runCli)(["navigate", "--browser", "safari"], service);
478
+ }
479
+ catch (error) {
480
+ const { payload } = (0, errors_1.toErrorPayload)(error);
481
+ process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
482
+ }
483
+ });
484
+ const errorPayload = JSON.parse(errorResult.stderr);
485
+ strict_1.default.equal(errorPayload.error.code, "missing_url");
486
+ strict_1.default.equal(errorPayload.error.statusCode, 400);
487
+ });
488
+ async function withChromeDevtoolsFixture(run) {
489
+ let scenario = "stable";
490
+ const server = (0, node_http_1.createServer)((request, response) => {
491
+ if (request.url === "/json/version") {
492
+ response.writeHead(200, { "content-type": "application/json" });
493
+ response.end(JSON.stringify({ Browser: "Chrome/123.0.0.0", ProtocolVersion: "1.3" }));
494
+ return;
495
+ }
496
+ if (request.url === "/json/list") {
497
+ const payload = scenario === "moved"
498
+ ? [
499
+ {
500
+ id: "page-1",
501
+ type: "page",
502
+ title: "Example One",
503
+ url: "https://example.com/one"
504
+ },
505
+ {
506
+ id: "page-2",
507
+ type: "page",
508
+ title: "Retitled Two",
509
+ url: "https://example.com/two-renamed",
510
+ attached: true,
511
+ openerId: "page-1",
512
+ browserContextId: "context-1"
513
+ }
514
+ ]
515
+ : [
516
+ { id: "page-1", type: "page", title: "Example One", url: "https://example.com/one" },
517
+ {
518
+ id: "page-2",
519
+ type: "page",
520
+ title: "Example Two",
521
+ url: "https://example.com/two",
522
+ attached: false,
523
+ openerId: "page-1",
524
+ browserContextId: "context-1"
525
+ },
526
+ { id: "worker-1", type: "service_worker", title: "Worker", url: "https://example.com/sw.js" }
527
+ ];
528
+ response.writeHead(200, { "content-type": "application/json" });
529
+ response.end(JSON.stringify(payload));
530
+ return;
531
+ }
532
+ response.writeHead(404, { "content-type": "application/json" });
533
+ response.end(JSON.stringify({ error: "not found" }));
534
+ });
535
+ await new Promise((resolvePromise) => server.listen(0, "127.0.0.1", resolvePromise));
536
+ const address = server.address();
537
+ strict_1.default.ok(address && typeof address === "object");
538
+ const baseUrl = `http://127.0.0.1:${address.port}`;
539
+ const previous = process.env.LOCAL_BROWSER_BRIDGE_CHROME_DEBUG_URL;
540
+ process.env.LOCAL_BROWSER_BRIDGE_CHROME_DEBUG_URL = baseUrl;
541
+ try {
542
+ return await run(baseUrl, (nextScenario) => {
543
+ scenario = nextScenario;
544
+ });
545
+ }
546
+ finally {
547
+ if (previous === undefined) {
548
+ delete process.env.LOCAL_BROWSER_BRIDGE_CHROME_DEBUG_URL;
549
+ }
550
+ else {
551
+ process.env.LOCAL_BROWSER_BRIDGE_CHROME_DEBUG_URL = previous;
552
+ }
553
+ await new Promise((resolvePromise, reject) => server.close((error) => (error ? reject(error) : resolvePromise())));
554
+ }
555
+ }
556
+ async function withChromeRelayStateFixture(state, run) {
557
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "chrome-relay");
558
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
559
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
560
+ const statePath = (0, node_path_1.resolve)(baseDir, "chrome-relay-state.json");
561
+ await (0, promises_1.writeFile)(statePath, JSON.stringify(state, null, 2), "utf8");
562
+ const previous = process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH;
563
+ process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH = statePath;
564
+ try {
565
+ return await run(statePath);
566
+ }
567
+ finally {
568
+ if (previous === undefined) {
569
+ delete process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH;
570
+ }
571
+ else {
572
+ process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH = previous;
573
+ }
574
+ }
575
+ }
576
+ async function withChromeRelayStatePathOverride(value, run) {
577
+ const previous = process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH;
578
+ if (value === undefined) {
579
+ delete process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH;
580
+ }
581
+ else {
582
+ process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH = value;
583
+ }
584
+ try {
585
+ return await run();
586
+ }
587
+ finally {
588
+ if (previous === undefined) {
589
+ delete process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH;
590
+ }
591
+ else {
592
+ process.env.LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH = previous;
593
+ }
594
+ }
595
+ }
596
+ (0, node_test_1.default)("safari runtime errors classify permission, availability, and target-loss states truthfully", async () => {
597
+ const automationDenied = (0, safari_1.classifySafariRuntimeError)("activate", new Error("execution error: Not authorized to send Apple events to Safari. (-1743)"));
598
+ strict_1.default.equal(automationDenied.code, "automation_permission_denied");
599
+ strict_1.default.equal(automationDenied.statusCode, 403);
600
+ strict_1.default.match(automationDenied.message, /automation\/apple events permission/i);
601
+ const screenshotDenied = (0, safari_1.classifySafariRuntimeError)("screenshot", new Error("screencapture: not permitted to capture screen"));
602
+ strict_1.default.equal(screenshotDenied.code, "screen_recording_permission_denied");
603
+ strict_1.default.equal(screenshotDenied.statusCode, 403);
604
+ strict_1.default.match(screenshotDenied.message, /screen recording permission/i);
605
+ const preflightDenied = (0, safari_1.classifySafariRuntimeError)("screenshot", new Error("CGPreflightScreenCaptureAccess returned false. Screen recording permission is required before Safari screenshots can be captured."));
606
+ strict_1.default.equal(preflightDenied.code, "screen_recording_permission_denied");
607
+ strict_1.default.equal(preflightDenied.statusCode, 403);
608
+ strict_1.default.match(preflightDenied.message, /screen recording permission/i);
609
+ const notRunning = (0, safari_1.classifySafariRuntimeError)("inspect", new Error("Safari is not running."));
610
+ strict_1.default.equal(notRunning.code, "browser_not_running");
611
+ strict_1.default.equal(notRunning.statusCode, 503);
612
+ strict_1.default.match(notRunning.message, /not running/i);
613
+ const noWindows = (0, safari_1.classifySafariRuntimeError)("inspect", new Error("Safari has no open windows."));
614
+ strict_1.default.equal(noWindows.code, "browser_unavailable");
615
+ strict_1.default.equal(noWindows.statusCode, 503);
616
+ strict_1.default.match(noWindows.message, /no open windows/i);
617
+ const missingTarget = (0, safari_1.classifySafariRuntimeError)("navigate", new Error("Safari target tab is no longer available."));
618
+ strict_1.default.equal(missingTarget.code, "tab_not_found");
619
+ strict_1.default.equal(missingTarget.statusCode, 404);
620
+ strict_1.default.match(missingTarget.message, /attach or resume/i);
621
+ const invalidBounds = (0, safari_1.classifySafariRuntimeError)("screenshot", new Error("Safari target window bounds are unavailable or invalid for screenshot capture."));
622
+ strict_1.default.equal(invalidBounds.code, "window_bounds_unavailable");
623
+ strict_1.default.equal(invalidBounds.statusCode, 503);
624
+ strict_1.default.match(invalidBounds.message, /aborted before calling screencapture/i);
625
+ const rejectedRect = (0, safari_1.classifySafariRuntimeError)("screenshot", new Error("Command failed: screencapture -x -R 0,31,1440,869 out.png\ncould not create image from rect\n"));
626
+ strict_1.default.equal(rejectedRect.code, "screenshot_capture_failed");
627
+ strict_1.default.equal(rejectedRect.statusCode, 503);
628
+ strict_1.default.match(rejectedRect.message, /rejected the safari window region/i);
629
+ });
630
+ (0, node_test_1.default)("safari window bounds validator accepts only finite positive screenshot regions", () => {
631
+ strict_1.default.equal((0, safari_1.isValidSafariWindowBounds)({ x: 10, y: 20, width: 1200, height: 800, reorderedWindowToFront: true }), true);
632
+ strict_1.default.equal((0, safari_1.isValidSafariWindowBounds)({ x: 10, y: 20, width: 0, height: 800, reorderedWindowToFront: true }), false);
633
+ strict_1.default.equal((0, safari_1.isValidSafariWindowBounds)({ x: 10, y: 20, width: Number.NaN, height: 800, reorderedWindowToFront: true }), false);
634
+ strict_1.default.equal((0, safari_1.isValidSafariWindowBounds)(null), false);
635
+ });
636
+ (0, node_test_1.default)("safari tab resolution errors distinguish no windows, no inspectable tabs, and missing targets", () => {
637
+ const noWindows = (0, safari_1.classifySafariTabResolutionError)({ type: "front" }, {
638
+ tabs: [],
639
+ windowCount: 0,
640
+ inspectableWindowCount: 0,
641
+ tabCount: 0
642
+ });
643
+ strict_1.default.equal(noWindows.code, "browser_no_windows");
644
+ strict_1.default.equal(noWindows.statusCode, 503);
645
+ strict_1.default.match(noWindows.message, /no open windows/i);
646
+ const noInspectableTabs = (0, safari_1.classifySafariTabResolutionError)({ type: "front" }, {
647
+ tabs: [],
648
+ windowCount: 2,
649
+ inspectableWindowCount: 0,
650
+ tabCount: 0
651
+ });
652
+ strict_1.default.equal(noInspectableTabs.code, "browser_no_tabs");
653
+ strict_1.default.equal(noInspectableTabs.statusCode, 503);
654
+ strict_1.default.match(noInspectableTabs.message, /no inspectable tabs/i);
655
+ const indexedNoInspectableTabs = (0, safari_1.classifySafariTabResolutionError)({ type: "indexed", windowIndex: 1, tabIndex: 1 }, {
656
+ tabs: [],
657
+ windowCount: 1,
658
+ inspectableWindowCount: 0,
659
+ tabCount: 0
660
+ });
661
+ strict_1.default.equal(indexedNoInspectableTabs.code, "browser_no_tabs");
662
+ strict_1.default.equal(indexedNoInspectableTabs.statusCode, 503);
663
+ strict_1.default.match(indexedNoInspectableTabs.message, /resolve/i);
664
+ const missingIndexed = (0, safari_1.classifySafariTabResolutionError)({ type: "indexed", windowIndex: 9, tabIndex: 4 }, {
665
+ tabs: [
666
+ {
667
+ browser: "safari",
668
+ windowIndex: 1,
669
+ tabIndex: 1,
670
+ title: "Front",
671
+ url: "https://example.com",
672
+ isFrontWindow: true,
673
+ isActiveInWindow: true
674
+ }
675
+ ],
676
+ windowCount: 1,
677
+ inspectableWindowCount: 1,
678
+ tabCount: 1
679
+ });
680
+ strict_1.default.equal(missingIndexed.code, "tab_not_found");
681
+ strict_1.default.equal(missingIndexed.statusCode, 404);
682
+ strict_1.default.match(missingIndexed.message, /window 9, tab 4/i);
683
+ });
684
+ (0, node_test_1.default)("safari inspection snapshot parsing preserves inspectable tabs and skips broken windows", () => {
685
+ const snapshot = (0, safari_1.parseSafariInspectionSnapshot)(JSON.stringify({
686
+ tabs: [
687
+ {
688
+ browser: "safari",
689
+ windowIndex: 2,
690
+ tabIndex: 1,
691
+ title: "Inspectable",
692
+ url: "https://example.com",
693
+ isFrontWindow: false,
694
+ isActiveInWindow: true
695
+ }
696
+ ],
697
+ windowCount: 3,
698
+ inspectableWindowCount: 1,
699
+ tabCount: 1
700
+ }));
701
+ strict_1.default.equal(snapshot.windowCount, 3);
702
+ strict_1.default.equal(snapshot.inspectableWindowCount, 1);
703
+ strict_1.default.equal(snapshot.tabCount, 1);
704
+ strict_1.default.equal(snapshot.tabs.length, 1);
705
+ strict_1.default.equal(snapshot.tabs[0]?.windowIndex, 2);
706
+ strict_1.default.equal(snapshot.tabs[0]?.title, "Inspectable");
707
+ });
708
+ (0, node_test_1.default)("safari diagnostics preflight exposes machine-readable readiness and blockers", async () => {
709
+ const ready = (0, safari_1.buildSafariPreflight)({
710
+ osascriptAvailable: true,
711
+ screencaptureAvailable: true,
712
+ applicationAvailable: true,
713
+ safariRunning: true,
714
+ windowCount: 2,
715
+ inspectableWindowCount: 2,
716
+ tabCount: 4
717
+ });
718
+ strict_1.default.equal(ready.inspect.ready, true);
719
+ strict_1.default.equal(ready.automation.ready, true);
720
+ strict_1.default.equal(ready.screenshot.ready, true);
721
+ strict_1.default.equal(ready.inspect.blockers.length, 0);
722
+ const noWindows = (0, safari_1.buildSafariPreflight)({
723
+ osascriptAvailable: true,
724
+ screencaptureAvailable: true,
725
+ applicationAvailable: true,
726
+ safariRunning: true,
727
+ windowCount: 0,
728
+ inspectableWindowCount: 0,
729
+ tabCount: 0
730
+ });
731
+ strict_1.default.equal(noWindows.inspect.ready, false);
732
+ strict_1.default.equal(noWindows.automation.ready, false);
733
+ strict_1.default.equal(noWindows.screenshot.ready, false);
734
+ strict_1.default.equal(noWindows.inspect.blockers[0]?.code, "browser_no_windows");
735
+ const noInspectableTabs = (0, safari_1.buildSafariPreflight)({
736
+ osascriptAvailable: true,
737
+ screencaptureAvailable: true,
738
+ applicationAvailable: true,
739
+ safariRunning: true,
740
+ windowCount: 2,
741
+ inspectableWindowCount: 0,
742
+ tabCount: 0
743
+ });
744
+ strict_1.default.equal(noInspectableTabs.inspect.ready, false);
745
+ strict_1.default.equal(noInspectableTabs.automation.ready, false);
746
+ strict_1.default.equal(noInspectableTabs.screenshot.ready, false);
747
+ strict_1.default.equal(noInspectableTabs.inspect.blockers[0]?.code, "browser_no_tabs");
748
+ strict_1.default.match(noInspectableTabs.inspect.blockers[0]?.message ?? "", /special or transient windows/i);
749
+ const permissionDenied = (0, safari_1.buildSafariPreflight)({
750
+ osascriptAvailable: true,
751
+ screencaptureAvailable: true,
752
+ applicationAvailable: true,
753
+ safariRunning: true,
754
+ probeError: new Error("execution error: Not authorized to send Apple events to Safari. (-1743)")
755
+ });
756
+ strict_1.default.equal(permissionDenied.inspect.ready, false);
757
+ strict_1.default.equal(permissionDenied.automation.blockers[0]?.code, "automation_permission_denied");
758
+ strict_1.default.equal(permissionDenied.screenshot.blockers[0]?.code, "automation_permission_denied");
759
+ const screenshotHostBlocked = (0, safari_1.buildSafariPreflight)({
760
+ osascriptAvailable: true,
761
+ screencaptureAvailable: false,
762
+ applicationAvailable: true,
763
+ safariRunning: true,
764
+ windowCount: 1,
765
+ inspectableWindowCount: 1,
766
+ tabCount: 1
767
+ });
768
+ strict_1.default.equal(screenshotHostBlocked.inspect.ready, true);
769
+ strict_1.default.equal(screenshotHostBlocked.automation.ready, true);
770
+ strict_1.default.equal(screenshotHostBlocked.screenshot.ready, false);
771
+ strict_1.default.equal(screenshotHostBlocked.screenshot.blockers[0]?.code, "host_tool_missing");
772
+ const screenRecordingDenied = (0, safari_1.buildSafariPreflight)({
773
+ osascriptAvailable: true,
774
+ screencaptureAvailable: true,
775
+ applicationAvailable: true,
776
+ safariRunning: true,
777
+ screenRecordingPermissionGranted: false,
778
+ windowCount: 1,
779
+ inspectableWindowCount: 1,
780
+ tabCount: 1
781
+ });
782
+ strict_1.default.equal(screenRecordingDenied.inspect.ready, true);
783
+ strict_1.default.equal(screenRecordingDenied.automation.ready, true);
784
+ strict_1.default.equal(screenRecordingDenied.screenshot.ready, false);
785
+ strict_1.default.equal(screenRecordingDenied.screenshot.blockers[0]?.code, "screen_recording_permission_denied");
786
+ strict_1.default.match(screenRecordingDenied.screenshot.blockers[0]?.message ?? "", /screen recording/i);
787
+ });
788
+ (0, node_test_1.default)("chrome capabilities expose read-only inspection and chromium normalizes to chrome", async () => {
789
+ strict_1.default.equal((0, browser_1.normalizeBrowser)("chromium"), "chrome");
790
+ const service = new attach_service_1.AttachService();
791
+ const capabilities = service.getCapabilities();
792
+ const chrome = capabilities.browsers.find((browser) => browser.browser === "chrome");
793
+ strict_1.default.ok(chrome);
794
+ strict_1.default.equal(chrome.kind, "chrome-readonly");
795
+ strict_1.default.equal(chrome.maturity, "experimental-readonly");
796
+ strict_1.default.equal(chrome.attachModes?.[0]?.mode, "direct");
797
+ strict_1.default.equal(chrome.attachModes?.[0]?.source, "user-browser");
798
+ strict_1.default.equal(chrome.attachModes?.[1]?.mode, "relay");
799
+ strict_1.default.equal(chrome.attachModes?.[1]?.scope, "tab");
800
+ strict_1.default.equal(chrome.operations.capabilities, true);
801
+ strict_1.default.equal(chrome.operations.diagnostics, true);
802
+ strict_1.default.equal(chrome.operations.inspectFrontTab, true);
803
+ strict_1.default.equal(chrome.operations.inspectTab, true);
804
+ strict_1.default.equal(chrome.operations.listTabs, true);
805
+ strict_1.default.equal(chrome.operations.attach, true);
806
+ strict_1.default.equal(chrome.operations.resumeSession, true);
807
+ strict_1.default.equal(chrome.operations.navigate, false);
808
+ strict_1.default.equal(chrome.operations.screenshot, false);
809
+ });
810
+ (0, node_test_1.default)("chrome diagnostics expose discovery candidates and selected endpoint", async () => {
811
+ await withChromeRelayStatePathOverride((0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "missing-relay-state.json"), async () => {
812
+ await withChromeDevtoolsFixture(async (baseUrl) => {
813
+ const service = new attach_service_1.AttachService();
814
+ const diagnostics = await service.diagnostics("chrome");
815
+ strict_1.default.equal(diagnostics.browser, "chrome");
816
+ strict_1.default.equal(diagnostics.supportedFeatures.inspectTabs, true);
817
+ strict_1.default.equal(diagnostics.supportedFeatures.attach, true);
818
+ strict_1.default.equal(diagnostics.supportedFeatures.navigate, false);
819
+ strict_1.default.equal(diagnostics.supportedFeatures.savedSessions, true);
820
+ strict_1.default.equal(diagnostics.adapter?.mode, "chrome-devtools-readonly");
821
+ strict_1.default.equal(diagnostics.attach?.direct.mode, "direct");
822
+ strict_1.default.equal(diagnostics.attach?.direct.scope, "browser");
823
+ strict_1.default.equal(diagnostics.attach?.direct.ready, true);
824
+ strict_1.default.match(String(diagnostics.attach?.direct.state), /ready|degraded/);
825
+ strict_1.default.equal(diagnostics.attach?.relay.mode, "relay");
826
+ strict_1.default.equal(diagnostics.attach?.relay.ready, false);
827
+ strict_1.default.equal(diagnostics.attach?.relay.state, "unavailable");
828
+ strict_1.default.equal(diagnostics.attach?.relay.blockers[0]?.code, "relay_probe_not_configured");
829
+ strict_1.default.equal(diagnostics.adapter?.discovery?.selectedBaseUrl, baseUrl);
830
+ strict_1.default.match(diagnostics.constraints.join(" "), /read-only/i);
831
+ strict_1.default.ok((diagnostics.adapter?.discovery?.candidates.length ?? 0) >= 1);
832
+ });
833
+ });
834
+ });
835
+ (0, node_test_1.default)("chrome relay diagnostics differentiate local probe states and report relay readiness truthfully", async () => {
836
+ await withChromeDevtoolsFixture(async () => {
837
+ await withChromeRelayStateFixture({
838
+ version: "1.0.0",
839
+ updatedAt: "2026-03-28T10:00:00.000Z",
840
+ extensionInstalled: false
841
+ }, async (statePath) => {
842
+ const service = new attach_service_1.AttachService();
843
+ const diagnostics = await service.diagnostics("chrome");
844
+ strict_1.default.equal(diagnostics.attach?.relay.ready, false);
845
+ strict_1.default.equal(diagnostics.attach?.relay.state, "unavailable");
846
+ strict_1.default.equal(diagnostics.attach?.relay.blockers[0]?.code, "relay_extension_not_installed");
847
+ strict_1.default.match(diagnostics.attach?.relay.notes?.join(" ") ?? "", new RegExp(statePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
848
+ });
849
+ await withChromeRelayStateFixture({
850
+ extensionInstalled: true,
851
+ connected: false
852
+ }, async () => {
853
+ const service = new attach_service_1.AttachService();
854
+ const diagnostics = await service.diagnostics("chrome");
855
+ strict_1.default.equal(diagnostics.attach?.relay.state, "unavailable");
856
+ strict_1.default.equal(diagnostics.attach?.relay.blockers[0]?.code, "relay_extension_disconnected");
857
+ });
858
+ await withChromeRelayStateFixture({
859
+ extensionInstalled: true,
860
+ connected: true,
861
+ shareRequired: true,
862
+ sharedTab: {
863
+ id: "tab-contradiction"
864
+ }
865
+ }, async () => {
866
+ const service = new attach_service_1.AttachService();
867
+ const diagnostics = await service.diagnostics("chrome");
868
+ strict_1.default.equal(diagnostics.attach?.relay.state, "unavailable");
869
+ strict_1.default.equal(diagnostics.attach?.relay.blockers[0]?.code, "relay_probe_invalid");
870
+ });
871
+ await withChromeRelayStateFixture({
872
+ extensionInstalled: true,
873
+ connected: true,
874
+ userGestureRequired: true
875
+ }, async () => {
876
+ const service = new attach_service_1.AttachService();
877
+ const diagnostics = await service.diagnostics("chrome");
878
+ strict_1.default.equal(diagnostics.attach?.relay.state, "attention-required");
879
+ strict_1.default.equal(diagnostics.attach?.relay.blockers[0]?.code, "relay_toolbar_not_clicked");
880
+ });
881
+ await withChromeRelayStateFixture({
882
+ extensionInstalled: true,
883
+ connected: true,
884
+ shareRequired: true
885
+ }, async () => {
886
+ const service = new attach_service_1.AttachService();
887
+ const diagnostics = await service.diagnostics("chrome");
888
+ strict_1.default.equal(diagnostics.attach?.relay.state, "attention-required");
889
+ strict_1.default.equal(diagnostics.attach?.relay.blockers[0]?.code, "relay_share_required");
890
+ });
891
+ await withChromeRelayStateFixture({
892
+ extensionInstalled: true,
893
+ connected: true,
894
+ sharedTab: null
895
+ }, async () => {
896
+ const service = new attach_service_1.AttachService();
897
+ const diagnostics = await service.diagnostics("chrome");
898
+ strict_1.default.equal(diagnostics.attach?.relay.state, "unavailable");
899
+ strict_1.default.equal(diagnostics.attach?.relay.blockers[0]?.code, "relay_no_shared_tab");
900
+ });
901
+ await withChromeRelayStateFixture({
902
+ version: "1.1.0",
903
+ updatedAt: "2026-03-28T11:00:00.000Z",
904
+ extensionInstalled: true,
905
+ connected: true,
906
+ sharedTab: {
907
+ id: "tab-123",
908
+ title: "Relay Example",
909
+ url: "https://example.com/shared"
910
+ }
911
+ }, async () => {
912
+ const service = new attach_service_1.AttachService();
913
+ const diagnostics = await service.diagnostics("chrome");
914
+ strict_1.default.equal(diagnostics.attach?.relay.ready, true);
915
+ strict_1.default.equal(diagnostics.attach?.relay.state, "ready");
916
+ strict_1.default.equal(diagnostics.attach?.relay.blockers.length, 0);
917
+ strict_1.default.match(diagnostics.attach?.relay.notes?.join(" ") ?? "", /shared tab detected/i);
918
+ });
919
+ });
920
+ });
921
+ (0, node_test_1.default)("chrome relay attach creates a read-only tab-scoped session and resumes when relay state still matches", async () => {
922
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "chrome-relay-attach-session");
923
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
924
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
925
+ await withChromeRelayStateFixture({
926
+ version: "1.1.0",
927
+ updatedAt: "2026-03-28T11:00:00.000Z",
928
+ extensionInstalled: true,
929
+ connected: true,
930
+ resumable: true,
931
+ expiresAt: "2099-03-28T12:00:00.000Z",
932
+ sharedTab: {
933
+ id: "tab-123",
934
+ title: "Relay Example",
935
+ url: "https://example.com/shared"
936
+ }
937
+ }, async (statePath) => {
938
+ const service = new attach_service_1.AttachService({
939
+ store: new session_store_1.SessionStore({ filePath: (0, node_path_1.resolve)(baseDir, "sessions.json") })
940
+ });
941
+ const session = await service.attach("chrome", {
942
+ target: { type: "front" },
943
+ attach: { mode: "relay" }
944
+ });
945
+ strict_1.default.equal(session.kind, "chrome-readonly");
946
+ strict_1.default.equal(session.attach.mode, "relay");
947
+ strict_1.default.equal(session.attach.source, "extension-relay");
948
+ strict_1.default.equal(session.attach.scope, "tab");
949
+ strict_1.default.equal(session.attach.resumable, true);
950
+ strict_1.default.equal(session.attach.expiresAt, "2099-03-28T12:00:00.000Z");
951
+ strict_1.default.equal(session.semantics.inspect, "shared-tab-only");
952
+ strict_1.default.equal(session.semantics.resume, "current-shared-tab");
953
+ strict_1.default.equal(session.semantics.tabReference.windowIndex, "synthetic-shared-tab-position");
954
+ strict_1.default.equal(session.status.state, "read-only");
955
+ strict_1.default.equal(session.capabilities.navigate, false);
956
+ strict_1.default.equal(session.tab.url, "https://example.com/shared");
957
+ strict_1.default.equal(session.target.type, "signature");
958
+ await (0, promises_1.writeFile)(statePath, JSON.stringify({
959
+ version: "1.1.0",
960
+ updatedAt: "2026-03-28T11:30:00.000Z",
961
+ extensionInstalled: true,
962
+ connected: true,
963
+ resumable: false,
964
+ resumeRequiresUserGesture: false,
965
+ expiresAt: "2099-03-28T13:00:00.000Z",
966
+ sharedTab: {
967
+ id: "tab-123",
968
+ title: "Relay Example",
969
+ url: "https://example.com/shared"
970
+ }
971
+ }, null, 2), "utf8");
972
+ const resumed = await service.resumeSession(session.id);
973
+ strict_1.default.equal(resumed.resolution.strategy, "signature");
974
+ strict_1.default.equal(resumed.resolution.attachMode, "relay");
975
+ strict_1.default.equal(resumed.resolution.semantics, "current-shared-tab");
976
+ strict_1.default.equal(resumed.tab.url, "https://example.com/shared");
977
+ strict_1.default.equal(resumed.session.kind, "chrome-readonly");
978
+ strict_1.default.equal(resumed.session.attach.mode, "relay");
979
+ strict_1.default.equal(resumed.session.attach.source, "extension-relay");
980
+ strict_1.default.equal(resumed.session.attach.scope, "tab");
981
+ strict_1.default.equal(resumed.session.attach.trustedAt, "2026-03-28T11:30:00.000Z");
982
+ strict_1.default.equal(resumed.session.attach.resumable, false);
983
+ strict_1.default.equal(resumed.session.attach.resumeRequiresUserGesture, false);
984
+ strict_1.default.equal(resumed.session.attach.expiresAt, "2099-03-28T13:00:00.000Z");
985
+ strict_1.default.equal(resumed.session.semantics.inspect, "shared-tab-only");
986
+ strict_1.default.equal(resumed.session.semantics.resume, "current-shared-tab");
987
+ strict_1.default.equal(resumed.session.tab.title, "Relay Example");
988
+ strict_1.default.equal(resumed.session.frontTab.url, "https://example.com/shared");
989
+ strict_1.default.equal(resumed.session.target.type, "signature");
990
+ strict_1.default.equal(resumed.session.target.url, "https://example.com/shared");
991
+ const refreshed = await service.getSession(session.id);
992
+ strict_1.default.equal(refreshed.attach.trustedAt, "2026-03-28T11:30:00.000Z");
993
+ strict_1.default.equal(refreshed.attach.expiresAt, "2099-03-28T13:00:00.000Z");
994
+ strict_1.default.equal(refreshed.tab.title, "Relay Example");
995
+ });
996
+ });
997
+ (0, node_test_1.default)("chrome relay resume failures expose structured relay details for consumer branching", async () => {
998
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "chrome-relay-resume-errors");
999
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
1000
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
1001
+ await withChromeRelayStateFixture({
1002
+ version: "1.1.0",
1003
+ updatedAt: "2026-03-28T11:00:00.000Z",
1004
+ extensionInstalled: true,
1005
+ connected: true,
1006
+ resumable: false,
1007
+ resumeRequiresUserGesture: true,
1008
+ expiresAt: "2099-03-28T12:00:00.000Z",
1009
+ sharedTab: {
1010
+ id: "tab-123",
1011
+ title: "Relay Example",
1012
+ url: "https://example.com/shared"
1013
+ }
1014
+ }, async () => {
1015
+ const service = new attach_service_1.AttachService({
1016
+ store: new session_store_1.SessionStore({ filePath: (0, node_path_1.resolve)(baseDir, "sessions.json") })
1017
+ });
1018
+ const session = await service.attach("chrome", {
1019
+ target: { type: "front" },
1020
+ attach: { mode: "relay" }
1021
+ });
1022
+ await strict_1.default.rejects(() => service.resumeSession(session.id), (error) => {
1023
+ strict_1.default.ok(error instanceof errors_1.AppError);
1024
+ strict_1.default.equal(error.code, "relay_share_required");
1025
+ strict_1.default.equal(error.statusCode, 409);
1026
+ strict_1.default.deepEqual(error.details?.context, {
1027
+ browser: "chrome",
1028
+ attachMode: "relay",
1029
+ operation: "resumeSession"
1030
+ });
1031
+ strict_1.default.equal(error.details?.relay?.branch, "share-original-tab-again");
1032
+ strict_1.default.equal(error.details?.relay?.retryable, true);
1033
+ strict_1.default.equal(error.details?.relay?.userActionRequired, true);
1034
+ strict_1.default.equal(error.details?.relay?.phase, "session-precondition");
1035
+ strict_1.default.equal(error.details?.relay?.sharedTabScope, "current-shared-tab");
1036
+ strict_1.default.equal(error.details?.relay?.resumable, false);
1037
+ strict_1.default.equal(error.details?.relay?.resumeRequiresUserGesture, true);
1038
+ strict_1.default.equal(error.details?.relay?.sessionId, session.id);
1039
+ return true;
1040
+ });
1041
+ });
1042
+ });
1043
+ (0, node_test_1.default)("chrome relay attach fails with relay-aware errors for unshared or out-of-scope requests", async () => {
1044
+ await withChromeRelayStateFixture({
1045
+ extensionInstalled: true,
1046
+ connected: true,
1047
+ shareRequired: true
1048
+ }, async () => {
1049
+ const service = new attach_service_1.AttachService();
1050
+ await strict_1.default.rejects(() => service.attach("chrome", { target: { type: "front" }, attach: { mode: "relay" } }), (error) => {
1051
+ strict_1.default.ok(error instanceof errors_1.AppError);
1052
+ strict_1.default.equal(error.code, "relay_share_required");
1053
+ strict_1.default.equal(error.statusCode, 503);
1054
+ strict_1.default.deepEqual(error.details?.context, {
1055
+ browser: "chrome",
1056
+ attachMode: "relay",
1057
+ operation: "attach"
1058
+ });
1059
+ strict_1.default.equal(error.details?.relay?.branch, "share-tab");
1060
+ strict_1.default.equal(error.details?.relay?.retryable, true);
1061
+ strict_1.default.equal(error.details?.relay?.userActionRequired, true);
1062
+ strict_1.default.equal(error.details?.relay?.phase, "diagnostics");
1063
+ strict_1.default.equal(error.details?.relay?.sharedTabScope, "current-shared-tab");
1064
+ return true;
1065
+ });
1066
+ });
1067
+ await withChromeRelayStateFixture({
1068
+ extensionInstalled: true,
1069
+ connected: true,
1070
+ sharedTab: {
1071
+ id: "tab-123",
1072
+ title: "Relay Example",
1073
+ url: "https://example.com/shared"
1074
+ }
1075
+ }, async () => {
1076
+ const service = new attach_service_1.AttachService();
1077
+ await strict_1.default.rejects(() => service.attach("chrome", {
1078
+ target: { type: "indexed", windowIndex: 1, tabIndex: 2 },
1079
+ attach: { mode: "relay" }
1080
+ }), (error) => {
1081
+ strict_1.default.ok(error instanceof errors_1.AppError);
1082
+ strict_1.default.equal(error.code, "relay_attach_target_out_of_scope");
1083
+ strict_1.default.equal(error.statusCode, 409);
1084
+ strict_1.default.deepEqual(error.details?.context, {
1085
+ browser: "chrome",
1086
+ attachMode: "relay",
1087
+ operation: "attach"
1088
+ });
1089
+ strict_1.default.equal(error.details?.relay?.branch, "use-current-shared-tab");
1090
+ strict_1.default.equal(error.details?.relay?.retryable, true);
1091
+ strict_1.default.equal(error.details?.relay?.userActionRequired, true);
1092
+ strict_1.default.equal(error.details?.relay?.phase, "target-selection");
1093
+ strict_1.default.equal(error.details?.relay?.sharedTabScope, "current-shared-tab");
1094
+ strict_1.default.equal(error.details?.relay?.currentSharedTabMatches, false);
1095
+ return true;
1096
+ });
1097
+ });
1098
+ });
1099
+ (0, node_test_1.default)("chrome read-only inspection works while session actions still fail clearly", async () => {
1100
+ await withChromeDevtoolsFixture(async () => {
1101
+ const service = new attach_service_1.AttachService();
1102
+ const tabs = await service.listTabs("chrome");
1103
+ strict_1.default.equal(tabs.length, 2);
1104
+ strict_1.default.equal(tabs[0].title, "Example One");
1105
+ strict_1.default.equal(tabs[1].url, "https://example.com/two");
1106
+ strict_1.default.equal(tabs[1].identity.native?.kind, "chrome-devtools-target");
1107
+ strict_1.default.equal(tabs[1].identity.native?.targetId, "page-2");
1108
+ const front = await service.inspectFrontTab("chrome");
1109
+ strict_1.default.equal(front.url, "https://example.com/one");
1110
+ const resolved = await service.inspectTab("chrome", {
1111
+ type: "signature",
1112
+ signature: tabs[1].identity.signature,
1113
+ url: tabs[1].url,
1114
+ title: tabs[1].title
1115
+ });
1116
+ strict_1.default.equal(resolved.title, "Example Two");
1117
+ await strict_1.default.rejects(() => service.activate("chrome"), (error) => {
1118
+ strict_1.default.ok(error instanceof errors_1.AppError);
1119
+ strict_1.default.equal(error.code, "activation_unavailable");
1120
+ strict_1.default.equal(error.statusCode, 501);
1121
+ strict_1.default.match(error.message, /not implemented/i);
1122
+ return true;
1123
+ });
1124
+ await strict_1.default.rejects(() => service.navigate("chrome", { type: "front" }, { url: "https://example.com" }), (error) => {
1125
+ strict_1.default.ok(error instanceof errors_1.AppError);
1126
+ strict_1.default.equal(error.code, "navigation_unavailable");
1127
+ strict_1.default.equal(error.statusCode, 501);
1128
+ return true;
1129
+ });
1130
+ });
1131
+ });
1132
+ (0, node_test_1.default)("chrome sessions persist native target identity and resume read-only when metadata changes", async () => {
1133
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "chrome-resume");
1134
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
1135
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
1136
+ await withChromeDevtoolsFixture(async (_baseUrl, setScenario) => {
1137
+ const service = new attach_service_1.AttachService({
1138
+ store: new session_store_1.SessionStore({ filePath: (0, node_path_1.resolve)(baseDir, "sessions.json") })
1139
+ });
1140
+ const session = await service.attach("chrome", { type: "indexed", windowIndex: 1, tabIndex: 2 });
1141
+ strict_1.default.equal(session.target.type, "signature");
1142
+ strict_1.default.equal(session.target.type === "signature" ? session.target.native?.targetId : "", "page-2");
1143
+ strict_1.default.equal(session.schemaVersion, 1);
1144
+ strict_1.default.equal(session.kind, "chrome-readonly");
1145
+ strict_1.default.equal(session.attach.mode, "direct");
1146
+ strict_1.default.equal(session.attach.source, "user-browser");
1147
+ strict_1.default.equal(session.attach.scope, "browser");
1148
+ strict_1.default.equal(session.semantics.inspect, "browser-tabs");
1149
+ strict_1.default.equal(session.semantics.resume, "saved-browser-target");
1150
+ strict_1.default.equal(session.semantics.tabReference.tabIndex, "browser-position");
1151
+ strict_1.default.equal(session.status.state, "read-only");
1152
+ strict_1.default.equal(session.status.canAct, false);
1153
+ strict_1.default.equal(session.capabilities.resume, true);
1154
+ strict_1.default.equal(session.capabilities.activate, false);
1155
+ strict_1.default.equal(session.capabilities.navigate, false);
1156
+ strict_1.default.equal(session.capabilities.screenshot, false);
1157
+ setScenario("moved");
1158
+ const resumed = await service.resumeSession(session.id);
1159
+ strict_1.default.equal(resumed.resolution.strategy, "native_identity");
1160
+ strict_1.default.equal(resumed.resolution.attachMode, "direct");
1161
+ strict_1.default.equal(resumed.resolution.semantics, "saved-browser-target");
1162
+ strict_1.default.equal(resumed.tab.url, "https://example.com/two-renamed");
1163
+ strict_1.default.equal(resumed.tab.title, "Retitled Two");
1164
+ strict_1.default.equal(resumed.tab.identity.native?.targetId, "page-2");
1165
+ });
1166
+ });
1167
+ (0, node_test_1.default)("http and cli surfaces expose chrome read-only details", async () => {
1168
+ const baseDir = (0, node_path_1.resolve)(process.cwd(), ".tmp-tests", "chrome-surfaces");
1169
+ await (0, promises_1.rm)(baseDir, { recursive: true, force: true });
1170
+ await (0, promises_1.mkdir)(baseDir, { recursive: true });
1171
+ await withChromeDevtoolsFixture(async (debugBaseUrl) => {
1172
+ await withChromeRelayStateFixture({
1173
+ version: "1.1.0",
1174
+ updatedAt: "2026-03-28T11:00:00.000Z",
1175
+ extensionInstalled: true,
1176
+ connected: true,
1177
+ resumable: false,
1178
+ resumeRequiresUserGesture: true,
1179
+ expiresAt: "2099-03-28T12:00:00.000Z",
1180
+ sharedTab: {
1181
+ id: "tab-123",
1182
+ title: "Relay Example",
1183
+ url: "https://example.com/shared"
1184
+ }
1185
+ }, async () => {
1186
+ const service = new attach_service_1.AttachService({
1187
+ store: new session_store_1.SessionStore({ filePath: (0, node_path_1.resolve)(baseDir, "sessions.json") })
1188
+ });
1189
+ const server = (0, http_1.createApiServer)(service);
1190
+ await new Promise((resolvePromise) => server.listen(0, "127.0.0.1", resolvePromise));
1191
+ const address = server.address();
1192
+ strict_1.default.ok(address && typeof address === "object");
1193
+ const baseUrl = `http://127.0.0.1:${address.port}`;
1194
+ const capabilities = await fetch(`${baseUrl}/v1/capabilities?browser=chrome`);
1195
+ strict_1.default.equal(capabilities.status, 200);
1196
+ const capabilitiesPayload = (await capabilities.json());
1197
+ strict_1.default.equal(capabilitiesPayload.capabilities.schemaVersion, 1);
1198
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].kind, "chrome-readonly");
1199
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].browser, "chrome");
1200
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].maturity, "experimental-readonly");
1201
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].attachModes?.[0]?.mode, "direct");
1202
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].attachModes?.[1]?.mode, "relay");
1203
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].operations.inspectTab, true);
1204
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].operations.diagnostics, true);
1205
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].operations.attach, true);
1206
+ strict_1.default.equal(capabilitiesPayload.capabilities.browsers[0].operations.resumeSession, true);
1207
+ const frontTab = await fetch(`${baseUrl}/v1/front-tab?browser=chrome`);
1208
+ strict_1.default.equal(frontTab.status, 200);
1209
+ const frontTabPayload = (await frontTab.json());
1210
+ strict_1.default.equal(frontTabPayload.frontTab.url, "https://example.com/one");
1211
+ const diagnostics = await fetch(`${baseUrl}/v1/diagnostics?browser=chrome`);
1212
+ strict_1.default.equal(diagnostics.status, 200);
1213
+ const diagnosticsPayload = (await diagnostics.json());
1214
+ strict_1.default.equal(diagnosticsPayload.diagnostics.adapter?.discovery?.selectedBaseUrl, debugBaseUrl);
1215
+ strict_1.default.equal(diagnosticsPayload.diagnostics.attach?.direct?.mode, "direct");
1216
+ strict_1.default.equal(diagnosticsPayload.diagnostics.attach?.direct?.ready, true);
1217
+ strict_1.default.equal(diagnosticsPayload.diagnostics.attach?.relay?.mode, "relay");
1218
+ strict_1.default.equal(diagnosticsPayload.diagnostics.attach?.relay?.state, "ready");
1219
+ strict_1.default.equal(diagnosticsPayload.diagnostics.attach?.relay?.ready, true);
1220
+ strict_1.default.equal(diagnosticsPayload.diagnostics.supportedFeatures.attach, true);
1221
+ strict_1.default.equal(diagnosticsPayload.diagnostics.supportedFeatures.savedSessions, true);
1222
+ const attach = await fetch(`${baseUrl}/v1/attach`, {
1223
+ method: "POST",
1224
+ headers: { "content-type": "application/json" },
1225
+ body: JSON.stringify({ browser: "chrome", target: { windowIndex: 1, tabIndex: 2 } })
1226
+ });
1227
+ strict_1.default.equal(attach.status, 201);
1228
+ const attachPayload = (await attach.json());
1229
+ strict_1.default.equal(attachPayload.session.schemaVersion, 1);
1230
+ strict_1.default.equal(attachPayload.session.kind, "chrome-readonly");
1231
+ strict_1.default.equal(attachPayload.session.attach.mode, "direct");
1232
+ strict_1.default.equal(attachPayload.session.attach.scope, "browser");
1233
+ strict_1.default.equal(attachPayload.session.status.state, "read-only");
1234
+ strict_1.default.equal(attachPayload.session.status.canAct, false);
1235
+ strict_1.default.equal(attachPayload.session.capabilities.activate, false);
1236
+ strict_1.default.equal(attachPayload.session.capabilities.navigate, false);
1237
+ strict_1.default.equal(attachPayload.session.capabilities.screenshot, false);
1238
+ const relayAttach = await fetch(`${baseUrl}/v1/attach`, {
1239
+ method: "POST",
1240
+ headers: { "content-type": "application/json" },
1241
+ body: JSON.stringify({ browser: "chrome", attach: { mode: "relay" } })
1242
+ });
1243
+ strict_1.default.equal(relayAttach.status, 201);
1244
+ const relayAttachPayload = (await relayAttach.json());
1245
+ strict_1.default.equal(relayAttachPayload.session.attach.mode, "relay");
1246
+ strict_1.default.equal(relayAttachPayload.session.attach.source, "extension-relay");
1247
+ strict_1.default.equal(relayAttachPayload.session.attach.scope, "tab");
1248
+ strict_1.default.equal(relayAttachPayload.session.attach.resumable, false);
1249
+ strict_1.default.equal(relayAttachPayload.session.attach.resumeRequiresUserGesture, true);
1250
+ strict_1.default.equal(relayAttachPayload.session.attach.expiresAt, "2099-03-28T12:00:00.000Z");
1251
+ strict_1.default.equal(relayAttachPayload.session.semantics.inspect, "shared-tab-only");
1252
+ strict_1.default.equal(relayAttachPayload.session.semantics.resume, "current-shared-tab");
1253
+ strict_1.default.equal(relayAttachPayload.session.semantics.tabReference.windowIndex, "synthetic-shared-tab-position");
1254
+ strict_1.default.equal(relayAttachPayload.session.tab.url, "https://example.com/shared");
1255
+ await new Promise((resolvePromise, reject) => server.close((error) => (error ? reject(error) : resolvePromise())));
1256
+ const cliCapabilities = await withCapturedStreams(async () => {
1257
+ await (0, cli_1.runCli)(["capabilities", "--browser", "chromium"], service);
1258
+ });
1259
+ const cliCapabilitiesPayload = JSON.parse(cliCapabilities.stdout);
1260
+ strict_1.default.equal(cliCapabilitiesPayload.capabilities.schemaVersion, 1);
1261
+ strict_1.default.equal(cliCapabilitiesPayload.capabilities.browsers[0].kind, "chrome-readonly");
1262
+ strict_1.default.equal(cliCapabilitiesPayload.capabilities.browsers[0].browser, "chrome");
1263
+ strict_1.default.equal(cliCapabilitiesPayload.capabilities.browsers[0].maturity, "experimental-readonly");
1264
+ strict_1.default.equal(cliCapabilitiesPayload.capabilities.browsers[0].attachModes?.[0]?.mode, "direct");
1265
+ const cliRelayAttach = await withCapturedStreams(async () => {
1266
+ await (0, cli_1.runCli)(["attach", "--browser", "chrome", "--attach-mode", "relay"], service);
1267
+ });
1268
+ const cliRelayAttachPayload = JSON.parse(cliRelayAttach.stdout);
1269
+ strict_1.default.equal(cliRelayAttachPayload.session.attach.mode, "relay");
1270
+ strict_1.default.equal(cliRelayAttachPayload.session.attach.scope, "tab");
1271
+ strict_1.default.equal(cliRelayAttachPayload.session.semantics.inspect, "shared-tab-only");
1272
+ strict_1.default.equal(cliRelayAttachPayload.session.semantics.resume, "current-shared-tab");
1273
+ strict_1.default.equal(cliRelayAttachPayload.session.tab.url, "https://example.com/shared");
1274
+ const cliSessions = await withCapturedStreams(async () => {
1275
+ await (0, cli_1.runCli)(["sessions"], service);
1276
+ });
1277
+ const cliSessionsPayload = JSON.parse(cliSessions.stdout);
1278
+ strict_1.default.equal(cliSessionsPayload.sessions[0].schemaVersion, 1);
1279
+ strict_1.default.equal(cliSessionsPayload.sessions[0].kind, "chrome-readonly");
1280
+ strict_1.default.equal(cliSessionsPayload.sessions[0].attach.mode, "relay");
1281
+ strict_1.default.equal(cliSessionsPayload.sessions[0].semantics.inspect, "shared-tab-only");
1282
+ strict_1.default.equal(cliSessionsPayload.sessions[0].semantics.resume, "current-shared-tab");
1283
+ strict_1.default.equal(cliSessionsPayload.sessions[0].semantics.tabReference.tabIndex, "synthetic-shared-tab-position");
1284
+ strict_1.default.equal(cliSessionsPayload.sessions[0].status.state, "read-only");
1285
+ strict_1.default.equal(cliSessionsPayload.sessions[0].capabilities.activate, false);
1286
+ strict_1.default.equal(cliSessionsPayload.sessions[0].capabilities.navigate, false);
1287
+ strict_1.default.equal(cliSessionsPayload.sessions[0].capabilities.screenshot, false);
1288
+ });
1289
+ });
1290
+ });
1291
+ (0, node_test_1.default)("chrome relay CLI and HTTP errors expose the same structured failure details", async () => {
1292
+ await withChromeRelayStateFixture({
1293
+ extensionInstalled: true,
1294
+ connected: true,
1295
+ shareRequired: true
1296
+ }, async () => {
1297
+ const service = new attach_service_1.AttachService();
1298
+ const server = (0, http_1.createApiServer)(service);
1299
+ await new Promise((resolvePromise, reject) => server.listen(0, "127.0.0.1", (error) => (error ? reject(error) : resolvePromise())));
1300
+ const address = server.address();
1301
+ strict_1.default.ok(address && typeof address === "object");
1302
+ const baseUrl = `http://127.0.0.1:${address.port}`;
1303
+ try {
1304
+ const httpResponse = await fetch(`${baseUrl}/v1/attach`, {
1305
+ method: "POST",
1306
+ headers: { "content-type": "application/json" },
1307
+ body: JSON.stringify({ browser: "chrome", attach: { mode: "relay" } })
1308
+ });
1309
+ strict_1.default.equal(httpResponse.status, 503);
1310
+ const httpPayload = (await httpResponse.json());
1311
+ strict_1.default.equal(httpPayload.error.code, "relay_share_required");
1312
+ strict_1.default.equal(httpPayload.error.details?.context?.operation, "attach");
1313
+ strict_1.default.equal(httpPayload.error.details?.relay?.branch, "share-tab");
1314
+ strict_1.default.equal(httpPayload.error.details?.relay?.phase, "diagnostics");
1315
+ strict_1.default.equal(httpPayload.error.details?.relay?.sharedTabScope, "current-shared-tab");
1316
+ const cliResult = await withCapturedStreams(async () => {
1317
+ try {
1318
+ await (0, cli_1.runCli)(["attach", "--browser", "chrome", "--attach-mode", "relay"], service);
1319
+ }
1320
+ catch (error) {
1321
+ const { payload } = (0, errors_1.toErrorPayload)(error);
1322
+ process.stderr.write(JSON.stringify(payload, null, 2) + "\n");
1323
+ }
1324
+ });
1325
+ const cliPayload = JSON.parse(cliResult.stderr);
1326
+ strict_1.default.equal(cliPayload.error.code, httpPayload.error.code);
1327
+ strict_1.default.deepEqual(cliPayload.error.details, httpPayload.error.details);
1328
+ const cliDoctor = await withCapturedStreams(async () => {
1329
+ await (0, cli_1.runCli)(["doctor", "--route", "chrome-relay"], service);
1330
+ });
1331
+ const cliDoctorPayload = JSON.parse(cliDoctor.stdout);
1332
+ strict_1.default.equal(cliDoctorPayload.ok, false);
1333
+ strict_1.default.equal(cliDoctorPayload.blocked, true);
1334
+ strict_1.default.equal(cliDoctorPayload.outcome, "blocked");
1335
+ strict_1.default.equal(cliDoctorPayload.status, "blocked");
1336
+ strict_1.default.equal(cliDoctorPayload.category, "route-blocked");
1337
+ strict_1.default.equal(cliDoctorPayload.reason?.code, "relay_share_required");
1338
+ strict_1.default.equal(cliDoctorPayload.routeUx.label, "Chrome (shared tab, read-only)");
1339
+ strict_1.default.equal(cliDoctorPayload.routeUx.readOnly, true);
1340
+ strict_1.default.equal(cliDoctorPayload.routeUx.sharedTabScoped, true);
1341
+ strict_1.default.match(cliDoctorPayload.routeUx.prompt ?? "", /Share the tab first/i);
1342
+ strict_1.default.equal(cliDoctorPayload.nextStep.action, "fix-blocker");
1343
+ strict_1.default.match(cliDoctorPayload.summary, /currently shared tab/i);
1344
+ const cliConnect = await withCapturedStreams(async () => {
1345
+ await (0, cli_1.runCli)(["connect", "--route", "chrome-relay"], service);
1346
+ });
1347
+ const cliConnectPayload = JSON.parse(cliConnect.stdout);
1348
+ strict_1.default.equal(cliConnectPayload.ok, false);
1349
+ strict_1.default.equal(cliConnectPayload.blocked, true);
1350
+ strict_1.default.equal(cliConnectPayload.connected, false);
1351
+ strict_1.default.equal(cliConnectPayload.outcome, "blocked");
1352
+ strict_1.default.equal(cliConnectPayload.status, "blocked");
1353
+ strict_1.default.equal(cliConnectPayload.category, "connection-blocked");
1354
+ strict_1.default.equal(cliConnectPayload.reason?.code, "relay_share_required");
1355
+ strict_1.default.equal(cliConnectPayload.error, undefined);
1356
+ strict_1.default.equal(cliConnectPayload.routeUx?.state, "blocked");
1357
+ strict_1.default.equal(cliConnectPayload.routeUx?.sharedTabScoped, true);
1358
+ strict_1.default.equal(cliConnectPayload.routeUx?.readOnly, true);
1359
+ strict_1.default.match(cliConnectPayload.routeUx?.prompt ?? "", /Share the tab first/i);
1360
+ strict_1.default.equal(cliConnectPayload.nextStep.action, "fix-blocker");
1361
+ strict_1.default.match(cliConnectPayload.summary, /currently shared tab/i);
1362
+ }
1363
+ finally {
1364
+ await new Promise((resolvePromise, reject) => server.close((error) => (error ? reject(error) : resolvePromise())));
1365
+ }
1366
+ });
1367
+ });