@flrande/browserctl 0.1.0 → 0.2.0-dev.12.1
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/INSTALL-CN.md +28 -0
- package/INSTALL.md +28 -0
- package/README-CN.md +27 -1131
- package/README.md +27 -1131
- package/apps/browserctl/src/commands/command-wrappers.test.ts +386 -0
- package/apps/browserctl/src/commands/network-wait-for.test.ts +90 -0
- package/apps/browserctl/src/e2e.test.ts +9 -5
- package/apps/browserctl/src/main.dispatch.test.ts +256 -0
- package/apps/browserctl/src/smoke.e2e.test.ts +97 -0
- package/apps/browserctl/src/test-port.ts +26 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +6 -31
- package/apps/browserd/src/container.ts +12 -10
- package/apps/browserd/src/main.test.ts +81 -46
- package/apps/browserd/src/test-port.ts +26 -0
- package/apps/browserd/src/tool-matrix.test.ts +398 -0
- package/extensions/chrome-relay/README-CN.md +39 -0
- package/extensions/chrome-relay/README.md +3 -0
- package/package.json +6 -4
- package/apps/browserctl/src/smoke.test.ts +0 -16
- package/apps/browserctl/src/smoke.ts +0 -5
- package/scripts/smoke.ps1 +0 -127
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { bootstrapBrowserd } from "./bootstrap";
|
|
6
|
+
import { reserveLoopbackPort } from "./test-port";
|
|
7
|
+
|
|
8
|
+
function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
let buffer = "";
|
|
11
|
+
const timeout = setTimeout(() => {
|
|
12
|
+
stream.off("data", onData);
|
|
13
|
+
reject(new Error("Timed out waiting for JSON line response."));
|
|
14
|
+
}, 1000);
|
|
15
|
+
|
|
16
|
+
const onData = (chunk: string | Buffer) => {
|
|
17
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
18
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
19
|
+
if (newlineIndex < 0) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clearTimeout(timeout);
|
|
24
|
+
stream.off("data", onData);
|
|
25
|
+
const line = buffer.slice(0, newlineIndex);
|
|
26
|
+
|
|
27
|
+
resolve(JSON.parse(line) as Record<string, unknown>);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
stream.on("data", onData);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sendToolRequest(
|
|
35
|
+
input: PassThrough,
|
|
36
|
+
output: PassThrough,
|
|
37
|
+
request: Record<string, unknown>
|
|
38
|
+
): Promise<Record<string, unknown>> {
|
|
39
|
+
const responsePromise = waitForNextJsonLine(output);
|
|
40
|
+
input.write(`${JSON.stringify(request)}\n`);
|
|
41
|
+
return responsePromise;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function createManagedLegacyRuntime() {
|
|
45
|
+
const relayPort = await reserveLoopbackPort();
|
|
46
|
+
|
|
47
|
+
const input = new PassThrough();
|
|
48
|
+
const output = new PassThrough();
|
|
49
|
+
const runtime = bootstrapBrowserd({
|
|
50
|
+
env: {
|
|
51
|
+
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
52
|
+
BROWSERD_CHROME_RELAY_URL: `http://127.0.0.1:${relayPort}`
|
|
53
|
+
},
|
|
54
|
+
input,
|
|
55
|
+
output,
|
|
56
|
+
stdioProtocol: "legacy"
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
input,
|
|
61
|
+
output,
|
|
62
|
+
runtime
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("browserd tool matrix", () => {
|
|
67
|
+
it("returns E_INVALID_ARG for missing required arguments on routed tools", async () => {
|
|
68
|
+
const { input, output, runtime } = await createManagedLegacyRuntime();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const toolNames = [
|
|
72
|
+
"browser.profile.use",
|
|
73
|
+
"browser.tab.focus",
|
|
74
|
+
"browser.tab.close",
|
|
75
|
+
"browser.snapshot",
|
|
76
|
+
"browser.dom.query",
|
|
77
|
+
"browser.dom.queryAll",
|
|
78
|
+
"browser.element.screenshot",
|
|
79
|
+
"browser.a11y.snapshot",
|
|
80
|
+
"browser.cookie.get",
|
|
81
|
+
"browser.cookie.set",
|
|
82
|
+
"browser.cookie.clear",
|
|
83
|
+
"browser.storage.get",
|
|
84
|
+
"browser.storage.set",
|
|
85
|
+
"browser.frame.list",
|
|
86
|
+
"browser.frame.snapshot",
|
|
87
|
+
"browser.act",
|
|
88
|
+
"browser.dialog.arm",
|
|
89
|
+
"browser.download.trigger",
|
|
90
|
+
"browser.network.waitFor"
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
93
|
+
for (const [index, toolName] of toolNames.entries()) {
|
|
94
|
+
const response = await sendToolRequest(input, output, {
|
|
95
|
+
id: `request-missing-arg-${index}`,
|
|
96
|
+
name: toolName,
|
|
97
|
+
traceId: `trace:missing-arg:${index}`,
|
|
98
|
+
arguments: {
|
|
99
|
+
sessionId: "session:missing-arg"
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(response.ok).toBe(false);
|
|
104
|
+
expect(response.error).toMatchObject({
|
|
105
|
+
code: "E_INVALID_ARG"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
runtime.close();
|
|
110
|
+
input.end();
|
|
111
|
+
output.end();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns E_DRIVER_UNAVAILABLE for structured action tools on managed driver", async () => {
|
|
116
|
+
const { input, output, runtime } = await createManagedLegacyRuntime();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const openResponse = await sendToolRequest(input, output, {
|
|
120
|
+
id: "request-unavailable-open",
|
|
121
|
+
name: "browser.tab.open",
|
|
122
|
+
traceId: "trace:unavailable:open",
|
|
123
|
+
arguments: {
|
|
124
|
+
sessionId: "session:unavailable",
|
|
125
|
+
url: "https://example.com/unavailable"
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
129
|
+
|
|
130
|
+
const cases: Array<{ name: string; arguments: Record<string, unknown> }> = [
|
|
131
|
+
{
|
|
132
|
+
name: "browser.dom.query",
|
|
133
|
+
arguments: {
|
|
134
|
+
sessionId: "session:unavailable",
|
|
135
|
+
targetId,
|
|
136
|
+
selector: "#root"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "browser.dom.queryAll",
|
|
141
|
+
arguments: {
|
|
142
|
+
sessionId: "session:unavailable",
|
|
143
|
+
targetId,
|
|
144
|
+
selector: ".item"
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "browser.element.screenshot",
|
|
149
|
+
arguments: {
|
|
150
|
+
sessionId: "session:unavailable",
|
|
151
|
+
targetId,
|
|
152
|
+
selector: "#hero"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "browser.a11y.snapshot",
|
|
157
|
+
arguments: {
|
|
158
|
+
sessionId: "session:unavailable",
|
|
159
|
+
targetId
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "browser.cookie.get",
|
|
164
|
+
arguments: {
|
|
165
|
+
sessionId: "session:unavailable",
|
|
166
|
+
targetId
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "browser.cookie.set",
|
|
171
|
+
arguments: {
|
|
172
|
+
sessionId: "session:unavailable",
|
|
173
|
+
targetId,
|
|
174
|
+
name: "sid",
|
|
175
|
+
value: "abc"
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "browser.cookie.clear",
|
|
180
|
+
arguments: {
|
|
181
|
+
sessionId: "session:unavailable",
|
|
182
|
+
targetId
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "browser.storage.get",
|
|
187
|
+
arguments: {
|
|
188
|
+
sessionId: "session:unavailable",
|
|
189
|
+
targetId,
|
|
190
|
+
scope: "local",
|
|
191
|
+
key: "theme"
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "browser.storage.set",
|
|
196
|
+
arguments: {
|
|
197
|
+
sessionId: "session:unavailable",
|
|
198
|
+
targetId,
|
|
199
|
+
scope: "local",
|
|
200
|
+
key: "theme",
|
|
201
|
+
value: "dark"
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "browser.frame.list",
|
|
206
|
+
arguments: {
|
|
207
|
+
sessionId: "session:unavailable",
|
|
208
|
+
targetId
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "browser.frame.snapshot",
|
|
213
|
+
arguments: {
|
|
214
|
+
sessionId: "session:unavailable",
|
|
215
|
+
targetId,
|
|
216
|
+
frameId: "frame:0"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
for (const [index, testCase] of cases.entries()) {
|
|
222
|
+
const response = await sendToolRequest(input, output, {
|
|
223
|
+
id: `request-driver-unavailable-${index}`,
|
|
224
|
+
name: testCase.name,
|
|
225
|
+
traceId: `trace:driver-unavailable:${index}`,
|
|
226
|
+
arguments: testCase.arguments
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(response.ok).toBe(false);
|
|
230
|
+
expect(response.error).toMatchObject({
|
|
231
|
+
code: "E_DRIVER_UNAVAILABLE"
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
runtime.close();
|
|
236
|
+
input.end();
|
|
237
|
+
output.end();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("routes managed-driver tools including profile.use, act, focus/close, and waitFor timeout", async () => {
|
|
242
|
+
const { input, output, runtime } = await createManagedLegacyRuntime();
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const profileUseResponse = await sendToolRequest(input, output, {
|
|
246
|
+
id: "request-profile-use",
|
|
247
|
+
name: "browser.profile.use",
|
|
248
|
+
traceId: "trace:matrix:profile-use",
|
|
249
|
+
arguments: {
|
|
250
|
+
sessionId: "session:profile-use",
|
|
251
|
+
profile: "managed"
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
expect(profileUseResponse.ok).toBe(true);
|
|
255
|
+
expect(profileUseResponse.data).toEqual({
|
|
256
|
+
profile: "managed"
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const openResponse = await sendToolRequest(input, output, {
|
|
260
|
+
id: "request-flow-open",
|
|
261
|
+
name: "browser.tab.open",
|
|
262
|
+
traceId: "trace:matrix:open",
|
|
263
|
+
arguments: {
|
|
264
|
+
sessionId: "session:flow",
|
|
265
|
+
url: "https://example.com/flow"
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
269
|
+
|
|
270
|
+
const snapshotResponse = await sendToolRequest(input, output, {
|
|
271
|
+
id: "request-flow-snapshot",
|
|
272
|
+
name: "browser.snapshot",
|
|
273
|
+
traceId: "trace:matrix:snapshot",
|
|
274
|
+
arguments: {
|
|
275
|
+
sessionId: "session:flow",
|
|
276
|
+
targetId
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
expect(snapshotResponse.ok).toBe(true);
|
|
280
|
+
expect(snapshotResponse.data).toMatchObject({
|
|
281
|
+
driver: "managed",
|
|
282
|
+
targetId
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const focusResponse = await sendToolRequest(input, output, {
|
|
286
|
+
id: "request-flow-focus",
|
|
287
|
+
name: "browser.tab.focus",
|
|
288
|
+
traceId: "trace:matrix:focus",
|
|
289
|
+
arguments: {
|
|
290
|
+
sessionId: "session:flow",
|
|
291
|
+
targetId
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
expect(focusResponse.ok).toBe(true);
|
|
295
|
+
expect(focusResponse.data).toMatchObject({
|
|
296
|
+
driver: "managed",
|
|
297
|
+
targetId,
|
|
298
|
+
focused: true
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const actResponse = await sendToolRequest(input, output, {
|
|
302
|
+
id: "request-flow-act",
|
|
303
|
+
name: "browser.act",
|
|
304
|
+
traceId: "trace:matrix:act",
|
|
305
|
+
arguments: {
|
|
306
|
+
sessionId: "session:flow",
|
|
307
|
+
targetId,
|
|
308
|
+
action: {
|
|
309
|
+
type: "click",
|
|
310
|
+
payload: {
|
|
311
|
+
selector: "#go"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
expect(actResponse.ok).toBe(true);
|
|
317
|
+
expect(actResponse.data).toMatchObject({
|
|
318
|
+
driver: "managed",
|
|
319
|
+
targetId,
|
|
320
|
+
result: {
|
|
321
|
+
actionType: "click",
|
|
322
|
+
targetId,
|
|
323
|
+
targetKnown: true,
|
|
324
|
+
ok: true
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const dialogResponse = await sendToolRequest(input, output, {
|
|
329
|
+
id: "request-flow-dialog",
|
|
330
|
+
name: "browser.dialog.arm",
|
|
331
|
+
traceId: "trace:matrix:dialog",
|
|
332
|
+
arguments: {
|
|
333
|
+
sessionId: "session:flow",
|
|
334
|
+
targetId
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
expect(dialogResponse.ok).toBe(true);
|
|
338
|
+
expect(dialogResponse.data).toMatchObject({
|
|
339
|
+
driver: "managed",
|
|
340
|
+
targetId,
|
|
341
|
+
armed: true
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const triggerResponse = await sendToolRequest(input, output, {
|
|
345
|
+
id: "request-flow-trigger",
|
|
346
|
+
name: "browser.download.trigger",
|
|
347
|
+
traceId: "trace:matrix:trigger",
|
|
348
|
+
arguments: {
|
|
349
|
+
sessionId: "session:flow",
|
|
350
|
+
targetId
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
expect(triggerResponse.ok).toBe(true);
|
|
354
|
+
expect(triggerResponse.data).toMatchObject({
|
|
355
|
+
driver: "managed",
|
|
356
|
+
targetId,
|
|
357
|
+
triggered: true
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const waitForResponse = await sendToolRequest(input, output, {
|
|
361
|
+
id: "request-flow-waitfor",
|
|
362
|
+
name: "browser.network.waitFor",
|
|
363
|
+
traceId: "trace:matrix:waitfor",
|
|
364
|
+
arguments: {
|
|
365
|
+
sessionId: "session:flow",
|
|
366
|
+
targetId,
|
|
367
|
+
urlPattern: "/never-match",
|
|
368
|
+
timeoutMs: 5,
|
|
369
|
+
pollMs: 1
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
expect(waitForResponse.ok).toBe(false);
|
|
373
|
+
expect(waitForResponse.error).toMatchObject({
|
|
374
|
+
code: "E_TIMEOUT"
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const closeResponse = await sendToolRequest(input, output, {
|
|
378
|
+
id: "request-flow-close",
|
|
379
|
+
name: "browser.tab.close",
|
|
380
|
+
traceId: "trace:matrix:close",
|
|
381
|
+
arguments: {
|
|
382
|
+
sessionId: "session:flow",
|
|
383
|
+
targetId
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
expect(closeResponse.ok).toBe(true);
|
|
387
|
+
expect(closeResponse.data).toMatchObject({
|
|
388
|
+
driver: "managed",
|
|
389
|
+
targetId,
|
|
390
|
+
closed: true
|
|
391
|
+
});
|
|
392
|
+
} finally {
|
|
393
|
+
runtime.close();
|
|
394
|
+
input.end();
|
|
395
|
+
output.end();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# BrowserCtl Relay 扩展
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
此扩展用于将 Chromium 系浏览器标签页层连接到本地 BrowserCtl 扩展桥接服务。
|
|
6
|
+
|
|
7
|
+
## 加载已解压扩展
|
|
8
|
+
|
|
9
|
+
1. 打开 `chrome://extensions`(Edge 使用 `edge://extensions`)。
|
|
10
|
+
2. 开启**开发者模式**。
|
|
11
|
+
3. 点击**加载已解压的扩展程序**。
|
|
12
|
+
4. 选择目录:`extensions/chrome-relay`。
|
|
13
|
+
|
|
14
|
+
该扩展使用 `debugger` 权限采集控制台/网络遥测,以供 agent 侧诊断与分析。
|
|
15
|
+
|
|
16
|
+
## 配置
|
|
17
|
+
|
|
18
|
+
1. 打开扩展弹窗。
|
|
19
|
+
2. 将 `Bridge URL` 设置为桥接地址。
|
|
20
|
+
3. 将 token 设置为与 `BROWSERD_CHROME_RELAY_EXTENSION_TOKEN` 一致(必填)。
|
|
21
|
+
4. 点击 **Save**,然后点击 **Reconnect**。
|
|
22
|
+
|
|
23
|
+
默认 Bridge URL:
|
|
24
|
+
|
|
25
|
+
- `ws://127.0.0.1:9223/bridge`
|
|
26
|
+
- 默认 token(当 daemon 使用默认配置时):`browserctl-relay`
|
|
27
|
+
|
|
28
|
+
## 弹窗故障排查
|
|
29
|
+
|
|
30
|
+
弹窗内置了专门的排查区域:
|
|
31
|
+
|
|
32
|
+
- **Run Diagnostics**:对比扩展侧连接状态与 relay 侧 `/browserctl/relay/status`。
|
|
33
|
+
- **Copy Report**:复制包含状态快照与建议动作的 JSON 诊断报告。
|
|
34
|
+
- **Guidance list**:给出常见问题的定向建议(URL 不匹配、token 不匹配、relay 不可达、连接陈旧等)。
|
|
35
|
+
|
|
36
|
+
## 语言切换
|
|
37
|
+
|
|
38
|
+
使用弹窗语言选择器可切换 **English** 与 **中文**。
|
|
39
|
+
所选语言会保存到扩展本地存储,并在下次打开弹窗时自动应用。
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# BrowserCtl Relay Extension
|
|
2
2
|
|
|
3
|
+
English | [简体中文](README-CN.md)
|
|
4
|
+
|
|
3
5
|
This extension connects a Chromium-based browser tab layer to the local BrowserCtl extension bridge.
|
|
4
6
|
|
|
5
7
|
## Load Unpacked
|
|
@@ -21,6 +23,7 @@ This extension uses the `debugger` permission to collect console/network telemet
|
|
|
21
23
|
Default bridge URL:
|
|
22
24
|
|
|
23
25
|
- `ws://127.0.0.1:9223/bridge`
|
|
26
|
+
- Default token (if daemon uses defaults): `browserctl-relay`
|
|
24
27
|
|
|
25
28
|
## Troubleshooting In Popup
|
|
26
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flrande/browserctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-dev.12.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"bin": {
|
|
6
6
|
"browserctl": "bin/browserctl.cjs",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"packages/protocol/src",
|
|
17
17
|
"packages/transport-mcp-stdio/src",
|
|
18
18
|
"extensions/chrome-relay",
|
|
19
|
-
"
|
|
19
|
+
"INSTALL.md",
|
|
20
|
+
"INSTALL-CN.md",
|
|
20
21
|
"bin",
|
|
21
22
|
"README.md",
|
|
22
23
|
"README-CN.md",
|
|
@@ -43,8 +44,9 @@
|
|
|
43
44
|
"test:unit": "vitest run --config vitest.config.ts",
|
|
44
45
|
"test:contract": "vitest run --config vitest.contract.config.ts",
|
|
45
46
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
46
|
-
"test:
|
|
47
|
-
"
|
|
47
|
+
"test:smoke": "vitest run --config vitest.smoke.config.ts",
|
|
48
|
+
"test:all": "pnpm run test:unit && pnpm run test:contract && pnpm run test:e2e && pnpm run test:smoke",
|
|
49
|
+
"build": "npm pack --dry-run",
|
|
48
50
|
"typecheck": "pnpm exec tsc --noEmit -p tsconfig.typecheck.json",
|
|
49
51
|
"lint": "node ./scripts/lint.mjs"
|
|
50
52
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { getSmokeCommand } from "./smoke";
|
|
4
|
-
|
|
5
|
-
describe("smoke wiring", () => {
|
|
6
|
-
it("returns a PowerShell command with required flags and default script path", () => {
|
|
7
|
-
const command = getSmokeCommand();
|
|
8
|
-
|
|
9
|
-
expect(command).toContain("pwsh");
|
|
10
|
-
expect(command).toContain("-NoLogo");
|
|
11
|
-
expect(command).toContain("-NoProfile");
|
|
12
|
-
expect(command).toContain("-NonInteractive");
|
|
13
|
-
expect(command).toContain("-File");
|
|
14
|
-
expect(command).toContain('".\\scripts\\smoke.ps1"');
|
|
15
|
-
});
|
|
16
|
-
});
|
package/scripts/smoke.ps1
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
$ErrorActionPreference = 'Stop'
|
|
2
|
-
Set-StrictMode -Version Latest
|
|
3
|
-
|
|
4
|
-
$scriptDir = Split-Path -Parent $PSCommandPath
|
|
5
|
-
$repoRoot = Resolve-Path -LiteralPath (Join-Path $scriptDir '..')
|
|
6
|
-
$browserdEntry = Join-Path $repoRoot 'apps/browserd/src/main.ts'
|
|
7
|
-
$browserctlEntry = Join-Path $repoRoot 'apps/browserctl/src/main.ts'
|
|
8
|
-
|
|
9
|
-
$daemonProcess = $null
|
|
10
|
-
$daemonPort = 41337
|
|
11
|
-
$timeoutMs = 10000
|
|
12
|
-
$pollIntervalMs = 250
|
|
13
|
-
$pnpmExecutable = if ($IsWindows) { 'pnpm.cmd' } else { 'pnpm' }
|
|
14
|
-
|
|
15
|
-
function Invoke-SmokeStatus {
|
|
16
|
-
param(
|
|
17
|
-
[Parameter(Mandatory = $true)]
|
|
18
|
-
[string]$BrowserctlEntry
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
$previousPort = $env:BROWSERCTL_DAEMON_PORT
|
|
22
|
-
$env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
|
|
23
|
-
|
|
24
|
-
$statusOutput = & pnpm exec tsx $BrowserctlEntry status --json 2>&1
|
|
25
|
-
$exitCode = $LASTEXITCODE
|
|
26
|
-
$statusText = [string]::Join([Environment]::NewLine, @($statusOutput))
|
|
27
|
-
|
|
28
|
-
if ($null -eq $previousPort) {
|
|
29
|
-
Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
|
|
30
|
-
} else {
|
|
31
|
-
$env:BROWSERCTL_DAEMON_PORT = $previousPort
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if ($exitCode -ne 0) {
|
|
35
|
-
return @{
|
|
36
|
-
Ok = $false
|
|
37
|
-
Reason = "browserctl status exited with code $exitCode. Output: $statusText"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
$statusPayload = $null
|
|
42
|
-
try {
|
|
43
|
-
$statusPayload = $statusText | ConvertFrom-Json -ErrorAction Stop
|
|
44
|
-
} catch {
|
|
45
|
-
return @{
|
|
46
|
-
Ok = $false
|
|
47
|
-
Reason = "browserctl status returned invalid JSON. Output: $statusText"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (-not $statusPayload.ok) {
|
|
52
|
-
return @{
|
|
53
|
-
Ok = $false
|
|
54
|
-
Reason = 'browserctl status returned ok=false.'
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
$errorProperty = $statusPayload.PSObject.Properties['error']
|
|
59
|
-
if ($null -ne $errorProperty -and $null -ne $errorProperty.Value) {
|
|
60
|
-
$errorJson = $errorProperty.Value | ConvertTo-Json -Compress
|
|
61
|
-
return @{
|
|
62
|
-
Ok = $false
|
|
63
|
-
Reason = "browserctl status returned error payload: $errorJson"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return @{
|
|
68
|
-
Ok = $true
|
|
69
|
-
Payload = $statusPayload
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
$previousPort = $env:BROWSERCTL_DAEMON_PORT
|
|
75
|
-
$env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
|
|
76
|
-
& pnpm exec tsx $browserctlEntry daemon-stop --json *> $null
|
|
77
|
-
if ($null -eq $previousPort) {
|
|
78
|
-
Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
|
|
79
|
-
} else {
|
|
80
|
-
$env:BROWSERCTL_DAEMON_PORT = $previousPort
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
$daemonEnv = @{
|
|
84
|
-
BROWSERD_TRANSPORT = 'tcp'
|
|
85
|
-
BROWSERD_PORT = "$daemonPort"
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
$daemonProcess = Start-Process -FilePath $pnpmExecutable `
|
|
89
|
-
-ArgumentList @('exec', 'tsx', $browserdEntry) `
|
|
90
|
-
-WorkingDirectory $repoRoot `
|
|
91
|
-
-Environment $daemonEnv `
|
|
92
|
-
-PassThru
|
|
93
|
-
|
|
94
|
-
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
|
95
|
-
$lastFailure = 'status probe did not run'
|
|
96
|
-
$statusResult = $null
|
|
97
|
-
|
|
98
|
-
while ($stopwatch.ElapsedMilliseconds -lt $timeoutMs) {
|
|
99
|
-
$statusResult = Invoke-SmokeStatus -BrowserctlEntry $browserctlEntry
|
|
100
|
-
if ($statusResult.Ok) {
|
|
101
|
-
break
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
$lastFailure = [string]$statusResult.Reason
|
|
105
|
-
Start-Sleep -Milliseconds $pollIntervalMs
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if ($null -eq $statusResult -or -not $statusResult.Ok) {
|
|
109
|
-
$elapsedMs = [int]$stopwatch.ElapsedMilliseconds
|
|
110
|
-
throw "Smoke failed: browserctl status --json was not ready within ${timeoutMs}ms (elapsed ${elapsedMs}ms). Last failure: $lastFailure"
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
Write-Host 'Smoke passed: daemon started and browserctl status --json returned success.'
|
|
114
|
-
} finally {
|
|
115
|
-
if ($null -ne $daemonProcess -and -not $daemonProcess.HasExited) {
|
|
116
|
-
Stop-Process -Id $daemonProcess.Id -ErrorAction SilentlyContinue
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
$previousPort = $env:BROWSERCTL_DAEMON_PORT
|
|
120
|
-
$env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
|
|
121
|
-
& pnpm exec tsx $browserctlEntry daemon-stop --json *> $null
|
|
122
|
-
if ($null -eq $previousPort) {
|
|
123
|
-
Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
|
|
124
|
-
} else {
|
|
125
|
-
$env:BROWSERCTL_DAEMON_PORT = $previousPort
|
|
126
|
-
}
|
|
127
|
-
}
|