@flrande/browserctl 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +34 -0
- package/dist/client.js +138 -0
- package/dist/commandRegistry.d.ts +16 -0
- package/dist/commandRegistry.js +21 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/runCli.d.ts +5 -0
- package/dist/runCli.js +170 -0
- package/package.json +32 -57
- package/INSTALL-CN.md +0 -92
- package/INSTALL.md +0 -92
- package/LICENSE +0 -21
- package/README-CN.md +0 -69
- package/README.md +0 -69
- package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
- package/apps/browserctl/src/commands/act.test.ts +0 -71
- package/apps/browserctl/src/commands/act.ts +0 -64
- package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
- package/apps/browserctl/src/commands/common.test.ts +0 -87
- package/apps/browserctl/src/commands/common.ts +0 -191
- package/apps/browserctl/src/commands/console-list.test.ts +0 -102
- package/apps/browserctl/src/commands/console-list.ts +0 -108
- package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
- package/apps/browserctl/src/commands/cookie-get.ts +0 -18
- package/apps/browserctl/src/commands/cookie-set.ts +0 -22
- package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
- package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
- package/apps/browserctl/src/commands/dom-query.ts +0 -18
- package/apps/browserctl/src/commands/download-trigger.ts +0 -22
- package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
- package/apps/browserctl/src/commands/download-wait.ts +0 -27
- package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
- package/apps/browserctl/src/commands/frame-list.ts +0 -16
- package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
- package/apps/browserctl/src/commands/har-export.test.ts +0 -112
- package/apps/browserctl/src/commands/har-export.ts +0 -120
- package/apps/browserctl/src/commands/memory-delete.ts +0 -20
- package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
- package/apps/browserctl/src/commands/memory-list.ts +0 -90
- package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
- package/apps/browserctl/src/commands/memory-purge.ts +0 -16
- package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
- package/apps/browserctl/src/commands/memory-status.ts +0 -16
- package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
- package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
- package/apps/browserctl/src/commands/network-list.test.ts +0 -110
- package/apps/browserctl/src/commands/network-list.ts +0 -112
- package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
- package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
- package/apps/browserctl/src/commands/profile-list.ts +0 -16
- package/apps/browserctl/src/commands/profile-use.ts +0 -18
- package/apps/browserctl/src/commands/response-body.ts +0 -24
- package/apps/browserctl/src/commands/screenshot.ts +0 -16
- package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
- package/apps/browserctl/src/commands/session-drop.ts +0 -16
- package/apps/browserctl/src/commands/session-list.test.ts +0 -81
- package/apps/browserctl/src/commands/session-list.ts +0 -70
- package/apps/browserctl/src/commands/snapshot.ts +0 -16
- package/apps/browserctl/src/commands/status.ts +0 -10
- package/apps/browserctl/src/commands/storage-get.ts +0 -20
- package/apps/browserctl/src/commands/storage-set.ts +0 -22
- package/apps/browserctl/src/commands/tab-close.ts +0 -20
- package/apps/browserctl/src/commands/tab-focus.ts +0 -20
- package/apps/browserctl/src/commands/tab-open.ts +0 -19
- package/apps/browserctl/src/commands/tabs.ts +0 -13
- package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
- package/apps/browserctl/src/commands/trace-get.ts +0 -62
- package/apps/browserctl/src/commands/upload-arm.ts +0 -26
- package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-element.ts +0 -76
- package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
- package/apps/browserctl/src/commands/wait-text.ts +0 -93
- package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-url.ts +0 -76
- package/apps/browserctl/src/daemon-client.test.ts +0 -253
- package/apps/browserctl/src/daemon-client.ts +0 -632
- package/apps/browserctl/src/e2e.test.ts +0 -103
- package/apps/browserctl/src/main.dispatch.test.ts +0 -461
- package/apps/browserctl/src/main.test.ts +0 -334
- package/apps/browserctl/src/main.ts +0 -957
- package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
- package/apps/browserctl/src/test-port.ts +0 -26
- package/apps/browserd/src/bootstrap.ts +0 -432
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
- package/apps/browserd/src/container.ts +0 -3088
- package/apps/browserd/src/main.test.ts +0 -1436
- package/apps/browserd/src/main.ts +0 -7
- package/apps/browserd/src/test-port.ts +0 -26
- package/apps/browserd/src/tool-matrix.test.ts +0 -887
- package/bin/browserctl.cjs +0 -21
- package/bin/browserd.cjs +0 -21
- package/extensions/chrome-relay/README-CN.md +0 -39
- package/extensions/chrome-relay/README.md +0 -39
- package/extensions/chrome-relay/background.js +0 -1687
- package/extensions/chrome-relay/manifest.json +0 -15
- package/extensions/chrome-relay/popup.html +0 -369
- package/extensions/chrome-relay/popup.js +0 -972
- package/packages/core/src/bootstrap.test.ts +0 -10
- package/packages/core/src/driver-registry.test.ts +0 -45
- package/packages/core/src/driver-registry.ts +0 -22
- package/packages/core/src/driver.ts +0 -47
- package/packages/core/src/index.ts +0 -6
- package/packages/core/src/navigation-memory.test.ts +0 -259
- package/packages/core/src/navigation-memory.ts +0 -360
- package/packages/core/src/ref-cache.test.ts +0 -61
- package/packages/core/src/ref-cache.ts +0 -28
- package/packages/core/src/session-store.test.ts +0 -82
- package/packages/core/src/session-store.ts +0 -138
- package/packages/core/src/types.ts +0 -9
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
- package/packages/driver-chrome-relay/src/index.ts +0 -26
- package/packages/driver-managed/src/index.ts +0 -22
- package/packages/driver-managed/src/managed-driver.test.ts +0 -183
- package/packages/driver-managed/src/managed-driver.ts +0 -341
- package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
- package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
- package/packages/driver-remote-cdp/src/index.ts +0 -19
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
- package/packages/protocol/src/envelope.test.ts +0 -25
- package/packages/protocol/src/envelope.ts +0 -31
- package/packages/protocol/src/errors.test.ts +0 -17
- package/packages/protocol/src/errors.ts +0 -11
- package/packages/protocol/src/index.ts +0 -3
- package/packages/protocol/src/tools.ts +0 -3
- package/packages/transport-mcp-stdio/src/index.ts +0 -3
- package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
- package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
- package/packages/transport-mcp-stdio/src/server.ts +0 -183
- package/packages/transport-mcp-stdio/src/tool-map.ts +0 -84
|
@@ -1,1436 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { PassThrough } from "node:stream";
|
|
3
|
-
import { createConnection } from "node:net";
|
|
4
|
-
|
|
5
|
-
import { createContainer, loadBrowserdConfig } from "./container";
|
|
6
|
-
import { bootstrapBrowserd } from "./bootstrap";
|
|
7
|
-
import { reserveLoopbackPort } from "./test-port";
|
|
8
|
-
|
|
9
|
-
function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
|
|
10
|
-
return new Promise((resolve, reject) => {
|
|
11
|
-
let buffer = "";
|
|
12
|
-
const timeout = setTimeout(() => {
|
|
13
|
-
stream.off("data", onData);
|
|
14
|
-
reject(new Error("Timed out waiting for JSON line response."));
|
|
15
|
-
}, 1000);
|
|
16
|
-
|
|
17
|
-
const onData = (chunk: string | Buffer) => {
|
|
18
|
-
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
19
|
-
const newlineIndex = buffer.indexOf("\n");
|
|
20
|
-
if (newlineIndex < 0) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
clearTimeout(timeout);
|
|
25
|
-
stream.off("data", onData);
|
|
26
|
-
const line = buffer.slice(0, newlineIndex);
|
|
27
|
-
|
|
28
|
-
resolve(JSON.parse(line) as Record<string, unknown>);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
stream.on("data", onData);
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function sendToolRequest(
|
|
36
|
-
input: PassThrough,
|
|
37
|
-
output: PassThrough,
|
|
38
|
-
request: Record<string, unknown>
|
|
39
|
-
): Promise<Record<string, unknown>> {
|
|
40
|
-
const responsePromise = waitForNextJsonLine(output);
|
|
41
|
-
input.write(`${JSON.stringify(request)}\n`);
|
|
42
|
-
return responsePromise;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function sendTcpToolRequest(
|
|
46
|
-
port: number,
|
|
47
|
-
request: Record<string, unknown>
|
|
48
|
-
): Promise<Record<string, unknown>> {
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
const socket = createConnection({ host: "127.0.0.1", port });
|
|
51
|
-
socket.setEncoding("utf8");
|
|
52
|
-
|
|
53
|
-
let buffer = "";
|
|
54
|
-
const timeout = setTimeout(() => {
|
|
55
|
-
socket.destroy();
|
|
56
|
-
reject(new Error("Timed out waiting for TCP response."));
|
|
57
|
-
}, 1000);
|
|
58
|
-
|
|
59
|
-
socket.on("error", (error) => {
|
|
60
|
-
clearTimeout(timeout);
|
|
61
|
-
reject(error);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
socket.on("connect", () => {
|
|
65
|
-
socket.write(`${JSON.stringify(request)}\n`);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
socket.on("data", (chunk: string) => {
|
|
69
|
-
buffer += chunk;
|
|
70
|
-
const lineBreakIndex = buffer.indexOf("\n");
|
|
71
|
-
if (lineBreakIndex < 0) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
clearTimeout(timeout);
|
|
76
|
-
socket.end();
|
|
77
|
-
resolve(JSON.parse(buffer.slice(0, lineBreakIndex)) as Record<string, unknown>);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let testRelayPort = 0;
|
|
83
|
-
let testTcpPort = 0;
|
|
84
|
-
|
|
85
|
-
beforeEach(async () => {
|
|
86
|
-
testRelayPort = await reserveLoopbackPort();
|
|
87
|
-
testTcpPort = await reserveLoopbackPort();
|
|
88
|
-
while (testTcpPort === testRelayPort) {
|
|
89
|
-
testTcpPort = await reserveLoopbackPort();
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
function createTestEnv(
|
|
94
|
-
overrides: Record<string, string | undefined> = {}
|
|
95
|
-
): Record<string, string | undefined> {
|
|
96
|
-
return {
|
|
97
|
-
BROWSERD_CHROME_RELAY_URL: `http://127.0.0.1:${testRelayPort}`,
|
|
98
|
-
...overrides
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
describe("browserd container", () => {
|
|
103
|
-
it("uses secure defaults for new security config fields", () => {
|
|
104
|
-
const config = loadBrowserdConfig({});
|
|
105
|
-
|
|
106
|
-
expect(config.defaultDriver).toBe("chrome-relay");
|
|
107
|
-
expect(config.chromeRelayMode).toBe("extension");
|
|
108
|
-
expect(config.chromeRelayExtensionToken).toBe("browserctl-relay");
|
|
109
|
-
expect(config.managedLocalEnabled).toBe(true);
|
|
110
|
-
expect(config.uploadRoot).toBeUndefined();
|
|
111
|
-
expect(config.downloadRoot).toBeUndefined();
|
|
112
|
-
expect(config.authToken).toBeUndefined();
|
|
113
|
-
expect(config.authScopes).toEqual(["read", "act", "upload", "download"]);
|
|
114
|
-
expect(config.sessionTtlMs).toBe(30 * 60 * 1_000);
|
|
115
|
-
expect(config.sessionCleanupIntervalMs).toBe(60 * 1_000);
|
|
116
|
-
expect(config.domainAllowlistMode).toBe("off");
|
|
117
|
-
expect(config.domainAllowlist).toEqual([]);
|
|
118
|
-
expect(config.sessionMaxTotal).toBe(200);
|
|
119
|
-
expect(config.sessionMaxPerTenant).toBe(50);
|
|
120
|
-
expect(config.sessionRequireTenantPrefix).toBe(false);
|
|
121
|
-
expect(config.tenantAllowlist).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("registers managed-local, managed, chrome-relay, and remote-cdp drivers by default", () => {
|
|
125
|
-
const c = createContainer(loadBrowserdConfig(createTestEnv()));
|
|
126
|
-
|
|
127
|
-
expect(c.drivers.has("managed-local")).toBe(true);
|
|
128
|
-
expect(c.drivers.has("managed")).toBe(true);
|
|
129
|
-
expect(c.drivers.has("chrome-relay")).toBe(true);
|
|
130
|
-
expect(c.drivers.has("remote-cdp")).toBe(true);
|
|
131
|
-
c.close();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("uses env override in loadBrowserdConfig", () => {
|
|
135
|
-
const config = loadBrowserdConfig({
|
|
136
|
-
BROWSERD_REMOTE_CDP_URL: "http://127.0.0.1:9333/devtools/browser/override",
|
|
137
|
-
BROWSERD_UPLOAD_ROOT: "C:\\safe\\uploads",
|
|
138
|
-
BROWSERD_DOWNLOAD_ROOT: "C:\\safe\\downloads",
|
|
139
|
-
BROWSERD_AUTH_TOKEN: "test-token",
|
|
140
|
-
BROWSERD_AUTH_SCOPES: " read, download ",
|
|
141
|
-
BROWSERD_SESSION_TTL_MS: "120000",
|
|
142
|
-
BROWSERD_SESSION_CLEANUP_INTERVAL_MS: "5000",
|
|
143
|
-
BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
|
|
144
|
-
BROWSERD_DOMAIN_ALLOWLIST: "example.com,*.corp.local",
|
|
145
|
-
BROWSERD_SESSION_MAX_TOTAL: "2",
|
|
146
|
-
BROWSERD_SESSION_MAX_PER_TENANT: "1",
|
|
147
|
-
BROWSERD_SESSION_REQUIRE_TENANT_PREFIX: "true",
|
|
148
|
-
BROWSERD_TENANT_ALLOWLIST: "finance,ops"
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
expect(config.remoteCdpUrl).toBe("http://127.0.0.1:9333/devtools/browser/override");
|
|
152
|
-
expect(config.chromeRelayUrl).toBe("http://127.0.0.1:9223");
|
|
153
|
-
expect(config.defaultDriver).toBe("chrome-relay");
|
|
154
|
-
expect(config.managedLocalEnabled).toBe(true);
|
|
155
|
-
expect(config.uploadRoot).toBe("C:\\safe\\uploads");
|
|
156
|
-
expect(config.downloadRoot).toBe("C:\\safe\\downloads");
|
|
157
|
-
expect(config.authToken).toBe("test-token");
|
|
158
|
-
expect(config.authScopes).toEqual(["read", "download"]);
|
|
159
|
-
expect(config.sessionTtlMs).toBe(120000);
|
|
160
|
-
expect(config.sessionCleanupIntervalMs).toBe(5000);
|
|
161
|
-
expect(config.domainAllowlistMode).toBe("enforce");
|
|
162
|
-
expect(config.domainAllowlist).toEqual(["example.com", "*.corp.local"]);
|
|
163
|
-
expect(config.sessionMaxTotal).toBe(2);
|
|
164
|
-
expect(config.sessionMaxPerTenant).toBe(1);
|
|
165
|
-
expect(config.sessionRequireTenantPrefix).toBe(true);
|
|
166
|
-
expect(config.tenantAllowlist).toEqual(["finance", "ops"]);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("uses memory defaults in loadBrowserdConfig", () => {
|
|
170
|
-
const config = loadBrowserdConfig({});
|
|
171
|
-
|
|
172
|
-
expect(config.memoryEnabled).toBe(true);
|
|
173
|
-
expect(config.memoryMode).toBe("ask");
|
|
174
|
-
expect(config.memoryTtlDays).toBe(30);
|
|
175
|
-
expect(config.memoryPath).toBe(".browserctl-runtime/navigation-memory.json");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("uses memory env overrides in loadBrowserdConfig", () => {
|
|
179
|
-
const config = loadBrowserdConfig({
|
|
180
|
-
BROWSERD_MEMORY_ENABLED: "false",
|
|
181
|
-
BROWSERD_MEMORY_MODE: "auto",
|
|
182
|
-
BROWSERD_MEMORY_TTL_DAYS: "7",
|
|
183
|
-
BROWSERD_MEMORY_PATH: ".browserctl-runtime/memory-custom.json"
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
expect(config.memoryEnabled).toBe(false);
|
|
187
|
-
expect(config.memoryMode).toBe("auto");
|
|
188
|
-
expect(config.memoryTtlDays).toBe(7);
|
|
189
|
-
expect(config.memoryPath).toBe(".browserctl-runtime/memory-custom.json");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("parses chrome relay extension mode env values", () => {
|
|
193
|
-
const config = loadBrowserdConfig({
|
|
194
|
-
BROWSERD_CHROME_RELAY_MODE: "extension",
|
|
195
|
-
BROWSERD_CHROME_RELAY_URL: "http://127.0.0.1:9555",
|
|
196
|
-
BROWSERD_CHROME_RELAY_EXTENSION_TOKEN: "relay-secret",
|
|
197
|
-
BROWSERD_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS: "7000"
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
expect(config.chromeRelayMode).toBe("extension");
|
|
201
|
-
expect(config.chromeRelayUrl).toBe("http://127.0.0.1:9555");
|
|
202
|
-
expect(config.chromeRelayExtensionToken).toBe("relay-secret");
|
|
203
|
-
expect(config.chromeRelayExtensionRequestTimeoutMs).toBe(7000);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("respects explicit cdp relay mode override", () => {
|
|
207
|
-
const config = loadBrowserdConfig({
|
|
208
|
-
BROWSERD_CHROME_RELAY_MODE: "cdp"
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
expect(config.chromeRelayMode).toBe("cdp");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("uses default extension relay token when extension mode is enabled", () => {
|
|
215
|
-
const config = loadBrowserdConfig({
|
|
216
|
-
BROWSERD_CHROME_RELAY_MODE: "extension"
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
expect(config.chromeRelayMode).toBe("extension");
|
|
220
|
-
expect(config.chromeRelayExtensionToken).toBe("browserctl-relay");
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("keeps chrome-relay as default when managed-local is explicitly disabled", () => {
|
|
224
|
-
const config = loadBrowserdConfig({
|
|
225
|
-
BROWSERD_MANAGED_LOCAL_ENABLED: "false"
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
expect(config.managedLocalEnabled).toBe(false);
|
|
229
|
-
expect(config.defaultDriver).toBe("chrome-relay");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("does not register managed-local driver when explicitly disabled", () => {
|
|
233
|
-
const c = createContainer(
|
|
234
|
-
loadBrowserdConfig({
|
|
235
|
-
...createTestEnv(),
|
|
236
|
-
BROWSERD_MANAGED_LOCAL_ENABLED: "false"
|
|
237
|
-
})
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
expect(c.drivers.has("managed-local")).toBe(false);
|
|
241
|
-
c.close();
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe("browserd bootstrap", () => {
|
|
246
|
-
it("starts mcp stdio runtime with container wiring", () => {
|
|
247
|
-
const input = new PassThrough();
|
|
248
|
-
const output = new PassThrough();
|
|
249
|
-
|
|
250
|
-
const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output });
|
|
251
|
-
|
|
252
|
-
expect(runtime.mcpStdioStarted).toBe(true);
|
|
253
|
-
expect(runtime.container.drivers.has("managed")).toBe(true);
|
|
254
|
-
expect(runtime.container.drivers.has("chrome-relay")).toBe(true);
|
|
255
|
-
expect(runtime.container.drivers.has("remote-cdp")).toBe(true);
|
|
256
|
-
expect(runtime.container.drivers.has("managed-local")).toBe(true);
|
|
257
|
-
|
|
258
|
-
runtime.close();
|
|
259
|
-
input.end();
|
|
260
|
-
output.end();
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("supports tcp transport mode for persistent daemon use", async () => {
|
|
264
|
-
const port = testTcpPort;
|
|
265
|
-
const runtime = bootstrapBrowserd({
|
|
266
|
-
env: createTestEnv({
|
|
267
|
-
BROWSERD_TRANSPORT: "tcp",
|
|
268
|
-
BROWSERD_PORT: String(port),
|
|
269
|
-
BROWSERD_AUTH_TOKEN: "tcp-token"
|
|
270
|
-
})
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const response = await sendTcpToolRequest(port, {
|
|
274
|
-
id: "request-tcp-status",
|
|
275
|
-
name: "browser.status",
|
|
276
|
-
traceId: "trace:tcp",
|
|
277
|
-
arguments: {
|
|
278
|
-
sessionId: "session:tcp",
|
|
279
|
-
authToken: "tcp-token"
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
expect(response.ok).toBe(true);
|
|
284
|
-
expect(response.id).toBe("request-tcp-status");
|
|
285
|
-
expect(response.traceId).toBe("trace:tcp");
|
|
286
|
-
expect(response.sessionId).toBe("session:tcp");
|
|
287
|
-
expect(response.data).toMatchObject({
|
|
288
|
-
kind: "browserd",
|
|
289
|
-
ready: true
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
runtime.close();
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("rejects tcp transport mode when auth token is not configured", () => {
|
|
296
|
-
let runtime: ReturnType<typeof bootstrapBrowserd> | undefined;
|
|
297
|
-
try {
|
|
298
|
-
expect(() => {
|
|
299
|
-
runtime = bootstrapBrowserd({
|
|
300
|
-
env: createTestEnv({
|
|
301
|
-
BROWSERD_TRANSPORT: "tcp",
|
|
302
|
-
BROWSERD_PORT: String(testTcpPort)
|
|
303
|
-
})
|
|
304
|
-
});
|
|
305
|
-
}).toThrow("BROWSERD_AUTH_TOKEN");
|
|
306
|
-
} finally {
|
|
307
|
-
runtime?.close();
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("processes line-delimited requests and writes response with id", async () => {
|
|
312
|
-
const input = new PassThrough();
|
|
313
|
-
const output = new PassThrough();
|
|
314
|
-
const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output, stdioProtocol: "legacy" });
|
|
315
|
-
|
|
316
|
-
const response = await sendToolRequest(input, output, {
|
|
317
|
-
id: "request-1",
|
|
318
|
-
name: "browser.status",
|
|
319
|
-
traceId: "trace:test-1",
|
|
320
|
-
arguments: {
|
|
321
|
-
sessionId: "session:test-1"
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
expect(response.id).toBe("request-1");
|
|
326
|
-
expect(response.ok).toBe(true);
|
|
327
|
-
expect(response.traceId).toBe("trace:test-1");
|
|
328
|
-
expect(response.sessionId).toBe("session:test-1");
|
|
329
|
-
expect(response.data).toMatchObject({
|
|
330
|
-
kind: "browserd",
|
|
331
|
-
ready: true,
|
|
332
|
-
driver: "chrome-relay"
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
runtime.close();
|
|
336
|
-
input.end();
|
|
337
|
-
output.end();
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it("supports managed-local as default driver when enabled", async () => {
|
|
341
|
-
const input = new PassThrough();
|
|
342
|
-
const output = new PassThrough();
|
|
343
|
-
const runtime = bootstrapBrowserd({
|
|
344
|
-
env: createTestEnv({
|
|
345
|
-
BROWSERD_MANAGED_LOCAL_ENABLED: "true",
|
|
346
|
-
BROWSERD_DEFAULT_DRIVER: "managed-local"
|
|
347
|
-
}),
|
|
348
|
-
input,
|
|
349
|
-
output,
|
|
350
|
-
stdioProtocol: "legacy"
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const response = await sendToolRequest(input, output, {
|
|
354
|
-
id: "request-local-default",
|
|
355
|
-
name: "browser.status",
|
|
356
|
-
traceId: "trace:local-default",
|
|
357
|
-
arguments: {
|
|
358
|
-
sessionId: "session:local-default"
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
expect(response.ok).toBe(true);
|
|
363
|
-
expect(response.data).toMatchObject({
|
|
364
|
-
kind: "browserd",
|
|
365
|
-
ready: true,
|
|
366
|
-
driver: "managed-local"
|
|
367
|
-
});
|
|
368
|
-
expect((response.data as { status: { kind: string; launched: boolean } }).status).toMatchObject({
|
|
369
|
-
kind: "managed-local",
|
|
370
|
-
launched: false
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
runtime.close();
|
|
374
|
-
input.end();
|
|
375
|
-
output.end();
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it("falls back to managed when configured default is unavailable", async () => {
|
|
379
|
-
const input = new PassThrough();
|
|
380
|
-
const output = new PassThrough();
|
|
381
|
-
const runtime = bootstrapBrowserd({
|
|
382
|
-
env: createTestEnv({
|
|
383
|
-
BROWSERD_DEFAULT_DRIVER: "managed-local",
|
|
384
|
-
BROWSERD_MANAGED_LOCAL_ENABLED: "false"
|
|
385
|
-
}),
|
|
386
|
-
input,
|
|
387
|
-
output,
|
|
388
|
-
stdioProtocol: "legacy"
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const response = await sendToolRequest(input, output, {
|
|
392
|
-
id: "request-default-fallback",
|
|
393
|
-
name: "browser.status",
|
|
394
|
-
traceId: "trace:default-fallback",
|
|
395
|
-
arguments: {
|
|
396
|
-
sessionId: "session:default-fallback"
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
expect(response.ok).toBe(true);
|
|
401
|
-
expect(response.data).toMatchObject({
|
|
402
|
-
kind: "browserd",
|
|
403
|
-
ready: true,
|
|
404
|
-
driver: "managed"
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
runtime.close();
|
|
408
|
-
input.end();
|
|
409
|
-
output.end();
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it("requires auth token when configured", async () => {
|
|
413
|
-
const input = new PassThrough();
|
|
414
|
-
const output = new PassThrough();
|
|
415
|
-
const runtime = bootstrapBrowserd({
|
|
416
|
-
env: createTestEnv({
|
|
417
|
-
BROWSERD_AUTH_TOKEN: "secret-token"
|
|
418
|
-
}),
|
|
419
|
-
input,
|
|
420
|
-
output,
|
|
421
|
-
stdioProtocol: "legacy"
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
const response = await sendToolRequest(input, output, {
|
|
425
|
-
id: "request-auth-missing",
|
|
426
|
-
name: "browser.status",
|
|
427
|
-
traceId: "trace:auth-missing",
|
|
428
|
-
arguments: {
|
|
429
|
-
sessionId: "session:auth-missing"
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
expect(response.ok).toBe(false);
|
|
434
|
-
expect(response.error).toMatchObject({
|
|
435
|
-
code: "E_PERMISSION"
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
runtime.close();
|
|
439
|
-
input.end();
|
|
440
|
-
output.end();
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it("rejects invalid auth token", async () => {
|
|
444
|
-
const input = new PassThrough();
|
|
445
|
-
const output = new PassThrough();
|
|
446
|
-
const runtime = bootstrapBrowserd({
|
|
447
|
-
env: createTestEnv({
|
|
448
|
-
BROWSERD_AUTH_TOKEN: "secret-token"
|
|
449
|
-
}),
|
|
450
|
-
input,
|
|
451
|
-
output,
|
|
452
|
-
stdioProtocol: "legacy"
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
const response = await sendToolRequest(input, output, {
|
|
456
|
-
id: "request-auth-invalid",
|
|
457
|
-
name: "browser.status",
|
|
458
|
-
traceId: "trace:auth-invalid",
|
|
459
|
-
arguments: {
|
|
460
|
-
sessionId: "session:auth-invalid",
|
|
461
|
-
authToken: "wrong-token"
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
expect(response.ok).toBe(false);
|
|
466
|
-
expect(response.error).toMatchObject({
|
|
467
|
-
code: "E_PERMISSION"
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
runtime.close();
|
|
471
|
-
input.end();
|
|
472
|
-
output.end();
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
it("denies calls when requested tool scope is not allowed", async () => {
|
|
476
|
-
const input = new PassThrough();
|
|
477
|
-
const output = new PassThrough();
|
|
478
|
-
const runtime = bootstrapBrowserd({
|
|
479
|
-
env: createTestEnv({
|
|
480
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
481
|
-
BROWSERD_AUTH_TOKEN: "secret-token",
|
|
482
|
-
BROWSERD_AUTH_SCOPES: "read"
|
|
483
|
-
}),
|
|
484
|
-
input,
|
|
485
|
-
output,
|
|
486
|
-
stdioProtocol: "legacy"
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
const response = await sendToolRequest(input, output, {
|
|
490
|
-
id: "request-scope-deny",
|
|
491
|
-
name: "browser.tab.open",
|
|
492
|
-
traceId: "trace:scope-deny",
|
|
493
|
-
arguments: {
|
|
494
|
-
sessionId: "session:scope-deny",
|
|
495
|
-
authToken: "secret-token",
|
|
496
|
-
url: "https://example.com"
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
expect(response.ok).toBe(false);
|
|
501
|
-
expect(response.error).toMatchObject({
|
|
502
|
-
code: "E_PERMISSION"
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
runtime.close();
|
|
506
|
-
input.end();
|
|
507
|
-
output.end();
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it("denies upload and download tools when scopes exclude them", async () => {
|
|
511
|
-
const input = new PassThrough();
|
|
512
|
-
const output = new PassThrough();
|
|
513
|
-
const runtime = bootstrapBrowserd({
|
|
514
|
-
env: createTestEnv({
|
|
515
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
516
|
-
BROWSERD_AUTH_TOKEN: "secret-token",
|
|
517
|
-
BROWSERD_AUTH_SCOPES: "read,act"
|
|
518
|
-
}),
|
|
519
|
-
input,
|
|
520
|
-
output,
|
|
521
|
-
stdioProtocol: "legacy"
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
525
|
-
id: "request-scope-open",
|
|
526
|
-
name: "browser.tab.open",
|
|
527
|
-
traceId: "trace:scope:open",
|
|
528
|
-
arguments: {
|
|
529
|
-
sessionId: "session:scope:file-transfer",
|
|
530
|
-
authToken: "secret-token",
|
|
531
|
-
url: "https://example.com/scopes"
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
536
|
-
|
|
537
|
-
const uploadResponse = await sendToolRequest(input, output, {
|
|
538
|
-
id: "request-scope-upload",
|
|
539
|
-
name: "browser.upload.arm",
|
|
540
|
-
traceId: "trace:scope:upload",
|
|
541
|
-
arguments: {
|
|
542
|
-
sessionId: "session:scope:file-transfer",
|
|
543
|
-
authToken: "secret-token",
|
|
544
|
-
targetId,
|
|
545
|
-
files: ["C:\\allowed\\uploads\\file.txt"]
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
expect(uploadResponse.ok).toBe(false);
|
|
550
|
-
expect(uploadResponse.error).toMatchObject({
|
|
551
|
-
code: "E_PERMISSION"
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
const downloadResponse = await sendToolRequest(input, output, {
|
|
555
|
-
id: "request-scope-download",
|
|
556
|
-
name: "browser.download.wait",
|
|
557
|
-
traceId: "trace:scope:download",
|
|
558
|
-
arguments: {
|
|
559
|
-
sessionId: "session:scope:file-transfer",
|
|
560
|
-
authToken: "secret-token",
|
|
561
|
-
targetId
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
expect(downloadResponse.ok).toBe(false);
|
|
566
|
-
expect(downloadResponse.error).toMatchObject({
|
|
567
|
-
code: "E_PERMISSION"
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
runtime.close();
|
|
571
|
-
input.end();
|
|
572
|
-
output.end();
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it("rejects upload paths that escape configured upload root", async () => {
|
|
576
|
-
const input = new PassThrough();
|
|
577
|
-
const output = new PassThrough();
|
|
578
|
-
const runtime = bootstrapBrowserd({
|
|
579
|
-
env: createTestEnv({
|
|
580
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
581
|
-
BROWSERD_UPLOAD_ROOT: "C:\\allowed\\uploads"
|
|
582
|
-
}),
|
|
583
|
-
input,
|
|
584
|
-
output,
|
|
585
|
-
stdioProtocol: "legacy"
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
589
|
-
id: "request-upload-open",
|
|
590
|
-
name: "browser.tab.open",
|
|
591
|
-
traceId: "trace:upload:open",
|
|
592
|
-
arguments: {
|
|
593
|
-
sessionId: "session:upload-root",
|
|
594
|
-
url: "https://example.com/upload"
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
599
|
-
const uploadResponse = await sendToolRequest(input, output, {
|
|
600
|
-
id: "request-upload-arm",
|
|
601
|
-
name: "browser.upload.arm",
|
|
602
|
-
traceId: "trace:upload:arm",
|
|
603
|
-
arguments: {
|
|
604
|
-
sessionId: "session:upload-root",
|
|
605
|
-
targetId,
|
|
606
|
-
files: ["..\\..\\escape.txt"]
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
expect(uploadResponse.ok).toBe(false);
|
|
611
|
-
expect(uploadResponse.error).toMatchObject({
|
|
612
|
-
code: "E_PERMISSION"
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
runtime.close();
|
|
616
|
-
input.end();
|
|
617
|
-
output.end();
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it("rejects upload-arm when upload root is not configured", async () => {
|
|
621
|
-
const input = new PassThrough();
|
|
622
|
-
const output = new PassThrough();
|
|
623
|
-
const runtime = bootstrapBrowserd({
|
|
624
|
-
env: createTestEnv({
|
|
625
|
-
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
626
|
-
}),
|
|
627
|
-
input,
|
|
628
|
-
output,
|
|
629
|
-
stdioProtocol: "legacy"
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
633
|
-
id: "request-upload-root-open",
|
|
634
|
-
name: "browser.tab.open",
|
|
635
|
-
traceId: "trace:upload-root:open",
|
|
636
|
-
arguments: {
|
|
637
|
-
sessionId: "session:upload-root:required",
|
|
638
|
-
url: "https://example.com/upload-root"
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
643
|
-
const uploadResponse = await sendToolRequest(input, output, {
|
|
644
|
-
id: "request-upload-root-required",
|
|
645
|
-
name: "browser.upload.arm",
|
|
646
|
-
traceId: "trace:upload-root:required",
|
|
647
|
-
arguments: {
|
|
648
|
-
sessionId: "session:upload-root:required",
|
|
649
|
-
targetId,
|
|
650
|
-
files: ["C:\\temp\\upload.txt"]
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
expect(uploadResponse.ok).toBe(false);
|
|
655
|
-
expect(uploadResponse.error).toMatchObject({
|
|
656
|
-
code: "E_PERMISSION"
|
|
657
|
-
});
|
|
658
|
-
expect(String(uploadResponse.error?.message ?? "")).toContain("BROWSERD_UPLOAD_ROOT");
|
|
659
|
-
|
|
660
|
-
runtime.close();
|
|
661
|
-
input.end();
|
|
662
|
-
output.end();
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
it("rejects download paths outside configured download root", async () => {
|
|
666
|
-
const input = new PassThrough();
|
|
667
|
-
const output = new PassThrough();
|
|
668
|
-
const runtime = bootstrapBrowserd({
|
|
669
|
-
env: createTestEnv({
|
|
670
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
671
|
-
BROWSERD_DOWNLOAD_ROOT: "C:\\allowed\\downloads"
|
|
672
|
-
}),
|
|
673
|
-
input,
|
|
674
|
-
output,
|
|
675
|
-
stdioProtocol: "legacy"
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
679
|
-
id: "request-download-open",
|
|
680
|
-
name: "browser.tab.open",
|
|
681
|
-
traceId: "trace:download:open",
|
|
682
|
-
arguments: {
|
|
683
|
-
sessionId: "session:download-root",
|
|
684
|
-
url: "https://example.com/download"
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
689
|
-
|
|
690
|
-
const traversalResponse = await sendToolRequest(input, output, {
|
|
691
|
-
id: "request-download-wait-traversal",
|
|
692
|
-
name: "browser.download.wait",
|
|
693
|
-
traceId: "trace:download:wait:traversal",
|
|
694
|
-
arguments: {
|
|
695
|
-
sessionId: "session:download-root",
|
|
696
|
-
targetId,
|
|
697
|
-
path: "..\\escape.bin"
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
expect(traversalResponse.ok).toBe(false);
|
|
702
|
-
expect(traversalResponse.error).toMatchObject({
|
|
703
|
-
code: "E_PERMISSION"
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
const outsideResponse = await sendToolRequest(input, output, {
|
|
707
|
-
id: "request-download-wait-outside",
|
|
708
|
-
name: "browser.download.wait",
|
|
709
|
-
traceId: "trace:download:wait:outside",
|
|
710
|
-
arguments: {
|
|
711
|
-
sessionId: "session:download-root",
|
|
712
|
-
targetId,
|
|
713
|
-
path: "D:\\outside\\escape.bin"
|
|
714
|
-
}
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
expect(outsideResponse.ok).toBe(false);
|
|
718
|
-
expect(outsideResponse.error).toMatchObject({
|
|
719
|
-
code: "E_PERMISSION"
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
runtime.close();
|
|
723
|
-
input.end();
|
|
724
|
-
output.end();
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
it("supports POSIX-style upload/download allowlist roots", async () => {
|
|
728
|
-
const input = new PassThrough();
|
|
729
|
-
const output = new PassThrough();
|
|
730
|
-
const runtime = bootstrapBrowserd({
|
|
731
|
-
env: createTestEnv({
|
|
732
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
733
|
-
BROWSERD_UPLOAD_ROOT: "/allowed/uploads",
|
|
734
|
-
BROWSERD_DOWNLOAD_ROOT: "/allowed/downloads"
|
|
735
|
-
}),
|
|
736
|
-
input,
|
|
737
|
-
output,
|
|
738
|
-
stdioProtocol: "legacy"
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
742
|
-
id: "request-posix-open",
|
|
743
|
-
name: "browser.tab.open",
|
|
744
|
-
traceId: "trace:posix:open",
|
|
745
|
-
arguments: {
|
|
746
|
-
sessionId: "session:posix-root",
|
|
747
|
-
url: "https://example.com/posix"
|
|
748
|
-
}
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
752
|
-
|
|
753
|
-
const uploadResponse = await sendToolRequest(input, output, {
|
|
754
|
-
id: "request-posix-upload",
|
|
755
|
-
name: "browser.upload.arm",
|
|
756
|
-
traceId: "trace:posix:upload",
|
|
757
|
-
arguments: {
|
|
758
|
-
sessionId: "session:posix-root",
|
|
759
|
-
targetId,
|
|
760
|
-
files: ["reports/q1.csv"]
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
expect(uploadResponse.ok).toBe(true);
|
|
765
|
-
expect(uploadResponse.data).toMatchObject({
|
|
766
|
-
files: ["/allowed/uploads/reports/q1.csv"]
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
const downloadResponse = await sendToolRequest(input, output, {
|
|
770
|
-
id: "request-posix-download",
|
|
771
|
-
name: "browser.download.wait",
|
|
772
|
-
traceId: "trace:posix:download",
|
|
773
|
-
arguments: {
|
|
774
|
-
sessionId: "session:posix-root",
|
|
775
|
-
targetId,
|
|
776
|
-
path: "reports/out.bin"
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
expect(downloadResponse.ok).toBe(true);
|
|
781
|
-
expect(downloadResponse.data).toMatchObject({
|
|
782
|
-
download: {
|
|
783
|
-
path: "/allowed/downloads/reports/out.bin"
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
const traversalResponse = await sendToolRequest(input, output, {
|
|
788
|
-
id: "request-posix-download-traversal",
|
|
789
|
-
name: "browser.download.wait",
|
|
790
|
-
traceId: "trace:posix:download:traversal",
|
|
791
|
-
arguments: {
|
|
792
|
-
sessionId: "session:posix-root",
|
|
793
|
-
targetId,
|
|
794
|
-
path: "../escape.bin"
|
|
795
|
-
}
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
expect(traversalResponse.ok).toBe(false);
|
|
799
|
-
expect(traversalResponse.error).toMatchObject({
|
|
800
|
-
code: "E_PERMISSION"
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
runtime.close();
|
|
804
|
-
input.end();
|
|
805
|
-
output.end();
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
it("rejects download-wait when download root is not configured", async () => {
|
|
809
|
-
const input = new PassThrough();
|
|
810
|
-
const output = new PassThrough();
|
|
811
|
-
const runtime = bootstrapBrowserd({
|
|
812
|
-
env: createTestEnv({
|
|
813
|
-
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
814
|
-
}),
|
|
815
|
-
input,
|
|
816
|
-
output,
|
|
817
|
-
stdioProtocol: "legacy"
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
821
|
-
id: "request-download-root-open",
|
|
822
|
-
name: "browser.tab.open",
|
|
823
|
-
traceId: "trace:download-root:open",
|
|
824
|
-
arguments: {
|
|
825
|
-
sessionId: "session:download-root:required",
|
|
826
|
-
url: "https://example.com/download-root"
|
|
827
|
-
}
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
831
|
-
const downloadResponse = await sendToolRequest(input, output, {
|
|
832
|
-
id: "request-download-root-required",
|
|
833
|
-
name: "browser.download.wait",
|
|
834
|
-
traceId: "trace:download-root:required",
|
|
835
|
-
arguments: {
|
|
836
|
-
sessionId: "session:download-root:required",
|
|
837
|
-
targetId
|
|
838
|
-
}
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
expect(downloadResponse.ok).toBe(false);
|
|
842
|
-
expect(downloadResponse.error).toMatchObject({
|
|
843
|
-
code: "E_PERMISSION"
|
|
844
|
-
});
|
|
845
|
-
expect(String(downloadResponse.error?.message ?? "")).toContain("BROWSERD_DOWNLOAD_ROOT");
|
|
846
|
-
|
|
847
|
-
runtime.close();
|
|
848
|
-
input.end();
|
|
849
|
-
output.end();
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
it("routes tab.open and tab.list through driver-backed tool handlers", async () => {
|
|
853
|
-
const input = new PassThrough();
|
|
854
|
-
const output = new PassThrough();
|
|
855
|
-
const runtime = bootstrapBrowserd({
|
|
856
|
-
env: createTestEnv({
|
|
857
|
-
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
858
|
-
}),
|
|
859
|
-
input,
|
|
860
|
-
output,
|
|
861
|
-
stdioProtocol: "legacy"
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
865
|
-
id: "request-open",
|
|
866
|
-
name: "browser.tab.open",
|
|
867
|
-
traceId: "trace:open",
|
|
868
|
-
arguments: {
|
|
869
|
-
sessionId: "session:flow",
|
|
870
|
-
url: "https://example.com"
|
|
871
|
-
}
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
expect(openResponse.ok).toBe(true);
|
|
875
|
-
expect(openResponse.data).toMatchObject({
|
|
876
|
-
driver: "managed"
|
|
877
|
-
});
|
|
878
|
-
const openedTargetId = (openResponse.data as Record<string, unknown>).targetId;
|
|
879
|
-
expect(typeof openedTargetId).toBe("string");
|
|
880
|
-
|
|
881
|
-
const listResponse = await sendToolRequest(input, output, {
|
|
882
|
-
id: "request-list",
|
|
883
|
-
name: "browser.tab.list",
|
|
884
|
-
traceId: "trace:list",
|
|
885
|
-
arguments: {
|
|
886
|
-
sessionId: "session:flow"
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
expect(listResponse.ok).toBe(true);
|
|
891
|
-
expect(listResponse.data).toMatchObject({
|
|
892
|
-
driver: "managed"
|
|
893
|
-
});
|
|
894
|
-
expect((listResponse.data as { tabs: string[] }).tabs).toContain(openedTargetId);
|
|
895
|
-
|
|
896
|
-
runtime.close();
|
|
897
|
-
input.end();
|
|
898
|
-
output.end();
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
it("routes console and network tools for known targets", async () => {
|
|
902
|
-
const input = new PassThrough();
|
|
903
|
-
const output = new PassThrough();
|
|
904
|
-
const runtime = bootstrapBrowserd({
|
|
905
|
-
env: createTestEnv({
|
|
906
|
-
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
907
|
-
}),
|
|
908
|
-
input,
|
|
909
|
-
output,
|
|
910
|
-
stdioProtocol: "legacy"
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
914
|
-
id: "request-open-tools",
|
|
915
|
-
name: "browser.tab.open",
|
|
916
|
-
traceId: "trace:tools:open",
|
|
917
|
-
arguments: {
|
|
918
|
-
sessionId: "session:tools",
|
|
919
|
-
url: "https://example.com/tools"
|
|
920
|
-
}
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
924
|
-
|
|
925
|
-
const consoleResponse = await sendToolRequest(input, output, {
|
|
926
|
-
id: "request-console",
|
|
927
|
-
name: "browser.console.list",
|
|
928
|
-
traceId: "trace:tools:console",
|
|
929
|
-
arguments: {
|
|
930
|
-
sessionId: "session:tools",
|
|
931
|
-
targetId
|
|
932
|
-
}
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
expect(consoleResponse.ok).toBe(true);
|
|
936
|
-
expect(consoleResponse.data).toEqual({
|
|
937
|
-
driver: "managed",
|
|
938
|
-
targetId,
|
|
939
|
-
entries: []
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
const consoleFilteredResponse = await sendToolRequest(input, output, {
|
|
943
|
-
id: "request-console-filtered",
|
|
944
|
-
name: "browser.console.list",
|
|
945
|
-
traceId: "trace:tools:console:filtered",
|
|
946
|
-
arguments: {
|
|
947
|
-
sessionId: "session:tools",
|
|
948
|
-
targetId,
|
|
949
|
-
type: "error",
|
|
950
|
-
contains: "timeout",
|
|
951
|
-
since: "2026-01-01T00:00:00.000Z",
|
|
952
|
-
limit: 10
|
|
953
|
-
}
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
expect(consoleFilteredResponse.ok).toBe(true);
|
|
957
|
-
expect(consoleFilteredResponse.data).toEqual({
|
|
958
|
-
driver: "managed",
|
|
959
|
-
targetId,
|
|
960
|
-
entries: []
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
const networkListResponse = await sendToolRequest(input, output, {
|
|
964
|
-
id: "request-network-list",
|
|
965
|
-
name: "browser.network.list",
|
|
966
|
-
traceId: "trace:tools:network:list",
|
|
967
|
-
arguments: {
|
|
968
|
-
sessionId: "session:tools",
|
|
969
|
-
targetId,
|
|
970
|
-
limit: 20
|
|
971
|
-
}
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
expect(networkListResponse.ok).toBe(true);
|
|
975
|
-
expect(networkListResponse.data).toEqual({
|
|
976
|
-
driver: "managed",
|
|
977
|
-
targetId,
|
|
978
|
-
requests: []
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
const harExportResponse = await sendToolRequest(input, output, {
|
|
982
|
-
id: "request-network-har-export",
|
|
983
|
-
name: "browser.network.harExport",
|
|
984
|
-
traceId: "trace:tools:network:har",
|
|
985
|
-
arguments: {
|
|
986
|
-
sessionId: "session:tools",
|
|
987
|
-
targetId
|
|
988
|
-
}
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
expect(harExportResponse.ok).toBe(true);
|
|
992
|
-
expect(harExportResponse.data).toMatchObject({
|
|
993
|
-
driver: "managed",
|
|
994
|
-
targetId,
|
|
995
|
-
har: {
|
|
996
|
-
log: {
|
|
997
|
-
entries: []
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
const networkResponse = await sendToolRequest(input, output, {
|
|
1003
|
-
id: "request-network",
|
|
1004
|
-
name: "browser.network.responseBody",
|
|
1005
|
-
traceId: "trace:tools:network",
|
|
1006
|
-
arguments: {
|
|
1007
|
-
sessionId: "session:tools",
|
|
1008
|
-
targetId,
|
|
1009
|
-
requestId: "request-42"
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
expect(networkResponse.ok).toBe(true);
|
|
1014
|
-
expect(networkResponse.data).toEqual({
|
|
1015
|
-
driver: "managed",
|
|
1016
|
-
targetId,
|
|
1017
|
-
requestId: "request-42",
|
|
1018
|
-
body: "",
|
|
1019
|
-
encoding: "utf8"
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
const traceResponse = await sendToolRequest(input, output, {
|
|
1023
|
-
id: "request-trace-get",
|
|
1024
|
-
name: "browser.trace.get",
|
|
1025
|
-
traceId: "trace:tools:trace",
|
|
1026
|
-
arguments: {
|
|
1027
|
-
sessionId: "session:tools",
|
|
1028
|
-
limit: 50
|
|
1029
|
-
}
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
expect(traceResponse.ok).toBe(true);
|
|
1033
|
-
expect(traceResponse.data).toMatchObject({
|
|
1034
|
-
sessionId: "session:tools"
|
|
1035
|
-
});
|
|
1036
|
-
const tracePayload = traceResponse.data as {
|
|
1037
|
-
steps?: Array<{ tool?: string }>;
|
|
1038
|
-
keyResponses?: Array<{ kind?: string }>;
|
|
1039
|
-
};
|
|
1040
|
-
expect(Array.isArray(tracePayload.steps)).toBe(true);
|
|
1041
|
-
expect(tracePayload.steps?.some((step) => step.tool === "browser.console.list")).toBe(true);
|
|
1042
|
-
expect(Array.isArray(tracePayload.keyResponses)).toBe(true);
|
|
1043
|
-
expect(tracePayload.keyResponses?.some((event) => event.kind === "network.list")).toBe(true);
|
|
1044
|
-
|
|
1045
|
-
const screenshotResponse = await sendToolRequest(input, output, {
|
|
1046
|
-
id: "request-screenshot",
|
|
1047
|
-
name: "browser.screenshot",
|
|
1048
|
-
traceId: "trace:tools:screenshot",
|
|
1049
|
-
arguments: {
|
|
1050
|
-
sessionId: "session:tools",
|
|
1051
|
-
targetId
|
|
1052
|
-
}
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
expect(screenshotResponse.ok).toBe(false);
|
|
1056
|
-
expect(screenshotResponse.error).toMatchObject({
|
|
1057
|
-
code: "E_DRIVER_UNAVAILABLE"
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
runtime.close();
|
|
1061
|
-
input.end();
|
|
1062
|
-
output.end();
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
it("enforces domain allowlist for tab.open", async () => {
|
|
1066
|
-
const input = new PassThrough();
|
|
1067
|
-
const output = new PassThrough();
|
|
1068
|
-
const runtime = bootstrapBrowserd({
|
|
1069
|
-
env: createTestEnv({
|
|
1070
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
1071
|
-
BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
|
|
1072
|
-
BROWSERD_DOMAIN_ALLOWLIST: "example.com,*.corp.local"
|
|
1073
|
-
}),
|
|
1074
|
-
input,
|
|
1075
|
-
output,
|
|
1076
|
-
stdioProtocol: "legacy"
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
const allowed = await sendToolRequest(input, output, {
|
|
1080
|
-
id: "request-domain-allowed",
|
|
1081
|
-
name: "browser.tab.open",
|
|
1082
|
-
traceId: "trace:domain:allowed",
|
|
1083
|
-
arguments: {
|
|
1084
|
-
sessionId: "finance:domain-allowed",
|
|
1085
|
-
url: "https://example.com/home"
|
|
1086
|
-
}
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
expect(allowed.ok).toBe(true);
|
|
1090
|
-
|
|
1091
|
-
const blocked = await sendToolRequest(input, output, {
|
|
1092
|
-
id: "request-domain-blocked",
|
|
1093
|
-
name: "browser.tab.open",
|
|
1094
|
-
traceId: "trace:domain:blocked",
|
|
1095
|
-
arguments: {
|
|
1096
|
-
sessionId: "finance:domain-blocked",
|
|
1097
|
-
url: "https://evil.test/home"
|
|
1098
|
-
}
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
expect(blocked.ok).toBe(false);
|
|
1102
|
-
expect(blocked.error).toMatchObject({
|
|
1103
|
-
code: "E_PERMISSION"
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
runtime.close();
|
|
1107
|
-
input.end();
|
|
1108
|
-
output.end();
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
it("enforces domain allowlist for act navigate/goto", async () => {
|
|
1112
|
-
const input = new PassThrough();
|
|
1113
|
-
const output = new PassThrough();
|
|
1114
|
-
const runtime = bootstrapBrowserd({
|
|
1115
|
-
env: createTestEnv({
|
|
1116
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
1117
|
-
BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
|
|
1118
|
-
BROWSERD_DOMAIN_ALLOWLIST: "example.com"
|
|
1119
|
-
}),
|
|
1120
|
-
input,
|
|
1121
|
-
output,
|
|
1122
|
-
stdioProtocol: "legacy"
|
|
1123
|
-
});
|
|
1124
|
-
|
|
1125
|
-
const openResponse = await sendToolRequest(input, output, {
|
|
1126
|
-
id: "request-domain-act-open",
|
|
1127
|
-
name: "browser.tab.open",
|
|
1128
|
-
traceId: "trace:domain:act:open",
|
|
1129
|
-
arguments: {
|
|
1130
|
-
sessionId: "finance:domain-act",
|
|
1131
|
-
url: "https://example.com"
|
|
1132
|
-
}
|
|
1133
|
-
});
|
|
1134
|
-
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
1135
|
-
|
|
1136
|
-
const actResponse = await sendToolRequest(input, output, {
|
|
1137
|
-
id: "request-domain-act-blocked",
|
|
1138
|
-
name: "browser.act",
|
|
1139
|
-
traceId: "trace:domain:act:blocked",
|
|
1140
|
-
arguments: {
|
|
1141
|
-
sessionId: "finance:domain-act",
|
|
1142
|
-
targetId,
|
|
1143
|
-
action: {
|
|
1144
|
-
type: "navigate",
|
|
1145
|
-
payload: {
|
|
1146
|
-
url: "https://unauthorized.example.net"
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
|
-
expect(actResponse.ok).toBe(false);
|
|
1153
|
-
expect(actResponse.error).toMatchObject({
|
|
1154
|
-
code: "E_PERMISSION"
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
runtime.close();
|
|
1158
|
-
input.end();
|
|
1159
|
-
output.end();
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
it("enforces max total session limit", async () => {
|
|
1163
|
-
const input = new PassThrough();
|
|
1164
|
-
const output = new PassThrough();
|
|
1165
|
-
const runtime = bootstrapBrowserd({
|
|
1166
|
-
env: createTestEnv({
|
|
1167
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
1168
|
-
BROWSERD_SESSION_MAX_TOTAL: "1"
|
|
1169
|
-
}),
|
|
1170
|
-
input,
|
|
1171
|
-
output,
|
|
1172
|
-
stdioProtocol: "legacy"
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
const first = await sendToolRequest(input, output, {
|
|
1176
|
-
id: "request-session-limit-first",
|
|
1177
|
-
name: "browser.status",
|
|
1178
|
-
traceId: "trace:session-limit:first",
|
|
1179
|
-
arguments: {
|
|
1180
|
-
sessionId: "team-a:first"
|
|
1181
|
-
}
|
|
1182
|
-
});
|
|
1183
|
-
expect(first.ok).toBe(true);
|
|
1184
|
-
|
|
1185
|
-
const second = await sendToolRequest(input, output, {
|
|
1186
|
-
id: "request-session-limit-second",
|
|
1187
|
-
name: "browser.status",
|
|
1188
|
-
traceId: "trace:session-limit:second",
|
|
1189
|
-
arguments: {
|
|
1190
|
-
sessionId: "team-b:second"
|
|
1191
|
-
}
|
|
1192
|
-
});
|
|
1193
|
-
expect(second.ok).toBe(false);
|
|
1194
|
-
expect(second.error).toMatchObject({
|
|
1195
|
-
code: "E_CONFLICT"
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
runtime.close();
|
|
1199
|
-
input.end();
|
|
1200
|
-
output.end();
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
it("enforces tenant prefix, allowlist, and per-tenant session limit", async () => {
|
|
1204
|
-
const input = new PassThrough();
|
|
1205
|
-
const output = new PassThrough();
|
|
1206
|
-
const runtime = bootstrapBrowserd({
|
|
1207
|
-
env: createTestEnv({
|
|
1208
|
-
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
1209
|
-
BROWSERD_SESSION_REQUIRE_TENANT_PREFIX: "true",
|
|
1210
|
-
BROWSERD_TENANT_ALLOWLIST: "finance,ops",
|
|
1211
|
-
BROWSERD_SESSION_MAX_TOTAL: "10",
|
|
1212
|
-
BROWSERD_SESSION_MAX_PER_TENANT: "1"
|
|
1213
|
-
}),
|
|
1214
|
-
input,
|
|
1215
|
-
output,
|
|
1216
|
-
stdioProtocol: "legacy"
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
const missingTenant = await sendToolRequest(input, output, {
|
|
1220
|
-
id: "request-tenant-missing",
|
|
1221
|
-
name: "browser.status",
|
|
1222
|
-
traceId: "trace:tenant:missing",
|
|
1223
|
-
arguments: {
|
|
1224
|
-
sessionId: "missingTenant"
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1227
|
-
expect(missingTenant.ok).toBe(false);
|
|
1228
|
-
expect(missingTenant.error).toMatchObject({
|
|
1229
|
-
code: "E_PERMISSION"
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
const disallowedTenant = await sendToolRequest(input, output, {
|
|
1233
|
-
id: "request-tenant-disallowed",
|
|
1234
|
-
name: "browser.status",
|
|
1235
|
-
traceId: "trace:tenant:disallowed",
|
|
1236
|
-
arguments: {
|
|
1237
|
-
sessionId: "personal:task-1"
|
|
1238
|
-
}
|
|
1239
|
-
});
|
|
1240
|
-
expect(disallowedTenant.ok).toBe(false);
|
|
1241
|
-
expect(disallowedTenant.error).toMatchObject({
|
|
1242
|
-
code: "E_PERMISSION"
|
|
1243
|
-
});
|
|
1244
|
-
|
|
1245
|
-
const financeFirst = await sendToolRequest(input, output, {
|
|
1246
|
-
id: "request-tenant-finance-first",
|
|
1247
|
-
name: "browser.status",
|
|
1248
|
-
traceId: "trace:tenant:finance:first",
|
|
1249
|
-
arguments: {
|
|
1250
|
-
sessionId: "finance:task-1"
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
expect(financeFirst.ok).toBe(true);
|
|
1254
|
-
|
|
1255
|
-
const financeSecond = await sendToolRequest(input, output, {
|
|
1256
|
-
id: "request-tenant-finance-second",
|
|
1257
|
-
name: "browser.status",
|
|
1258
|
-
traceId: "trace:tenant:finance:second",
|
|
1259
|
-
arguments: {
|
|
1260
|
-
sessionId: "finance:task-2"
|
|
1261
|
-
}
|
|
1262
|
-
});
|
|
1263
|
-
expect(financeSecond.ok).toBe(false);
|
|
1264
|
-
expect(financeSecond.error).toMatchObject({
|
|
1265
|
-
code: "E_CONFLICT"
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
const opsFirst = await sendToolRequest(input, output, {
|
|
1269
|
-
id: "request-tenant-ops-first",
|
|
1270
|
-
name: "browser.status",
|
|
1271
|
-
traceId: "trace:tenant:ops:first",
|
|
1272
|
-
arguments: {
|
|
1273
|
-
sessionId: "ops:task-1"
|
|
1274
|
-
}
|
|
1275
|
-
});
|
|
1276
|
-
expect(opsFirst.ok).toBe(true);
|
|
1277
|
-
|
|
1278
|
-
runtime.close();
|
|
1279
|
-
input.end();
|
|
1280
|
-
output.end();
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
it("supports session-list and session-drop governance tools", async () => {
|
|
1284
|
-
const input = new PassThrough();
|
|
1285
|
-
const output = new PassThrough();
|
|
1286
|
-
const runtime = bootstrapBrowserd({
|
|
1287
|
-
env: createTestEnv({
|
|
1288
|
-
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
1289
|
-
}),
|
|
1290
|
-
input,
|
|
1291
|
-
output,
|
|
1292
|
-
stdioProtocol: "legacy"
|
|
1293
|
-
});
|
|
1294
|
-
|
|
1295
|
-
const financeStatus = await sendToolRequest(input, output, {
|
|
1296
|
-
id: "request-session-governance-finance",
|
|
1297
|
-
name: "browser.status",
|
|
1298
|
-
traceId: "trace:session-governance:finance",
|
|
1299
|
-
arguments: {
|
|
1300
|
-
sessionId: "finance:task-1"
|
|
1301
|
-
}
|
|
1302
|
-
});
|
|
1303
|
-
expect(financeStatus.ok).toBe(true);
|
|
1304
|
-
|
|
1305
|
-
const opsStatus = await sendToolRequest(input, output, {
|
|
1306
|
-
id: "request-session-governance-ops",
|
|
1307
|
-
name: "browser.status",
|
|
1308
|
-
traceId: "trace:session-governance:ops",
|
|
1309
|
-
arguments: {
|
|
1310
|
-
sessionId: "ops:task-1"
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
expect(opsStatus.ok).toBe(true);
|
|
1314
|
-
|
|
1315
|
-
const listBeforeDrop = await sendToolRequest(input, output, {
|
|
1316
|
-
id: "request-session-list-before",
|
|
1317
|
-
name: "browser.session.list",
|
|
1318
|
-
traceId: "trace:session-governance:list:before",
|
|
1319
|
-
arguments: {
|
|
1320
|
-
sessionId: "finance:task-1",
|
|
1321
|
-
limit: 20
|
|
1322
|
-
}
|
|
1323
|
-
});
|
|
1324
|
-
expect(listBeforeDrop.ok).toBe(true);
|
|
1325
|
-
const listPayloadBeforeDrop = listBeforeDrop.data as { sessions: Array<{ sessionId: string }> };
|
|
1326
|
-
expect(Array.isArray(listPayloadBeforeDrop.sessions)).toBe(true);
|
|
1327
|
-
expect(listPayloadBeforeDrop.sessions.some((entry) => entry.sessionId === "ops:task-1")).toBe(true);
|
|
1328
|
-
|
|
1329
|
-
const dropResponse = await sendToolRequest(input, output, {
|
|
1330
|
-
id: "request-session-drop",
|
|
1331
|
-
name: "browser.session.drop",
|
|
1332
|
-
traceId: "trace:session-governance:drop",
|
|
1333
|
-
arguments: {
|
|
1334
|
-
sessionId: "finance:task-1",
|
|
1335
|
-
sessionIdToDelete: "ops:task-1"
|
|
1336
|
-
}
|
|
1337
|
-
});
|
|
1338
|
-
expect(dropResponse.ok).toBe(false);
|
|
1339
|
-
expect(dropResponse.error).toMatchObject({
|
|
1340
|
-
code: "E_PERMISSION"
|
|
1341
|
-
});
|
|
1342
|
-
|
|
1343
|
-
const listAfterDrop = await sendToolRequest(input, output, {
|
|
1344
|
-
id: "request-session-list-after",
|
|
1345
|
-
name: "browser.session.list",
|
|
1346
|
-
traceId: "trace:session-governance:list:after",
|
|
1347
|
-
arguments: {
|
|
1348
|
-
sessionId: "finance:task-1",
|
|
1349
|
-
limit: 20
|
|
1350
|
-
}
|
|
1351
|
-
});
|
|
1352
|
-
expect(listAfterDrop.ok).toBe(true);
|
|
1353
|
-
const listPayloadAfterDrop = listAfterDrop.data as { sessions: Array<{ sessionId: string }> };
|
|
1354
|
-
expect(listPayloadAfterDrop.sessions.some((entry) => entry.sessionId === "ops:task-1")).toBe(true);
|
|
1355
|
-
|
|
1356
|
-
const selfDropResponse = await sendToolRequest(input, output, {
|
|
1357
|
-
id: "request-session-drop-self",
|
|
1358
|
-
name: "browser.session.drop",
|
|
1359
|
-
traceId: "trace:session-governance:drop:self",
|
|
1360
|
-
arguments: {
|
|
1361
|
-
sessionId: "finance:task-1",
|
|
1362
|
-
sessionIdToDelete: "finance:task-1"
|
|
1363
|
-
}
|
|
1364
|
-
});
|
|
1365
|
-
expect(selfDropResponse.ok).toBe(true);
|
|
1366
|
-
expect(selfDropResponse.data).toMatchObject({
|
|
1367
|
-
sessionIdToDelete: "finance:task-1",
|
|
1368
|
-
dropped: true
|
|
1369
|
-
});
|
|
1370
|
-
|
|
1371
|
-
const listAfterSelfDrop = await sendToolRequest(input, output, {
|
|
1372
|
-
id: "request-session-list-after-self-drop",
|
|
1373
|
-
name: "browser.session.list",
|
|
1374
|
-
traceId: "trace:session-governance:list:after:self",
|
|
1375
|
-
arguments: {
|
|
1376
|
-
sessionId: "ops:task-1",
|
|
1377
|
-
limit: 20
|
|
1378
|
-
}
|
|
1379
|
-
});
|
|
1380
|
-
expect(listAfterSelfDrop.ok).toBe(true);
|
|
1381
|
-
const listPayloadAfterSelfDrop = listAfterSelfDrop.data as {
|
|
1382
|
-
sessions: Array<{ sessionId: string }>;
|
|
1383
|
-
};
|
|
1384
|
-
expect(listPayloadAfterSelfDrop.sessions.some((entry) => entry.sessionId === "finance:task-1")).toBe(false);
|
|
1385
|
-
|
|
1386
|
-
runtime.close();
|
|
1387
|
-
input.end();
|
|
1388
|
-
output.end();
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
|
-
it("preserves id and trace/session metadata when queue-level error handling runs", async () => {
|
|
1392
|
-
const input = new PassThrough();
|
|
1393
|
-
const output = new PassThrough();
|
|
1394
|
-
const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output, stdioProtocol: "legacy" });
|
|
1395
|
-
|
|
1396
|
-
const originalWrite = output.write.bind(output);
|
|
1397
|
-
let shouldThrow = true;
|
|
1398
|
-
(output as unknown as { write: (chunk: string | Buffer) => boolean }).write = (
|
|
1399
|
-
chunk: string | Buffer
|
|
1400
|
-
) => {
|
|
1401
|
-
if (shouldThrow) {
|
|
1402
|
-
shouldThrow = false;
|
|
1403
|
-
throw new Error("simulated output failure");
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
return originalWrite(chunk);
|
|
1407
|
-
};
|
|
1408
|
-
|
|
1409
|
-
const responsePromise = waitForNextJsonLine(output);
|
|
1410
|
-
input.write(
|
|
1411
|
-
`${JSON.stringify({
|
|
1412
|
-
id: "request-failure",
|
|
1413
|
-
name: "browser.status",
|
|
1414
|
-
traceId: "trace:failure",
|
|
1415
|
-
arguments: {
|
|
1416
|
-
sessionId: "session:failure"
|
|
1417
|
-
}
|
|
1418
|
-
})}\n`
|
|
1419
|
-
);
|
|
1420
|
-
|
|
1421
|
-
const response = await responsePromise;
|
|
1422
|
-
|
|
1423
|
-
expect(response.id).toBe("request-failure");
|
|
1424
|
-
expect(response.ok).toBe(false);
|
|
1425
|
-
expect(response.traceId).toBe("trace:failure");
|
|
1426
|
-
expect(response.sessionId).toBe("session:failure");
|
|
1427
|
-
expect(response.error).toEqual({
|
|
1428
|
-
code: "E_INVALID_ARG",
|
|
1429
|
-
message: "simulated output failure"
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
runtime.close();
|
|
1433
|
-
input.end();
|
|
1434
|
-
output.end();
|
|
1435
|
-
});
|
|
1436
|
-
});
|