@flrande/browserctl 0.5.0-dev.19.1 → 0.5.0-dev.22.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.
|
@@ -17,6 +17,8 @@ type CapturedRequest = {
|
|
|
17
17
|
|
|
18
18
|
const ORIGINAL_DAEMON_PORT = process.env.BROWSERCTL_DAEMON_PORT;
|
|
19
19
|
const ORIGINAL_AUTH_TOKEN = process.env.BROWSERCTL_AUTH_TOKEN;
|
|
20
|
+
const ORIGINAL_DAEMON_STARTUP_TIMEOUT_MS = process.env.BROWSERCTL_DAEMON_STARTUP_TIMEOUT_MS;
|
|
21
|
+
const ORIGINAL_DAEMON_REQUEST_TIMEOUT_MS = process.env.BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS;
|
|
20
22
|
const TEST_PID_PORTS = new Set<number>();
|
|
21
23
|
|
|
22
24
|
function resolveRuntimeDirForTests(): string {
|
|
@@ -51,6 +53,18 @@ afterEach(() => {
|
|
|
51
53
|
process.env.BROWSERCTL_AUTH_TOKEN = ORIGINAL_AUTH_TOKEN;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
if (ORIGINAL_DAEMON_STARTUP_TIMEOUT_MS === undefined) {
|
|
57
|
+
delete process.env.BROWSERCTL_DAEMON_STARTUP_TIMEOUT_MS;
|
|
58
|
+
} else {
|
|
59
|
+
process.env.BROWSERCTL_DAEMON_STARTUP_TIMEOUT_MS = ORIGINAL_DAEMON_STARTUP_TIMEOUT_MS;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ORIGINAL_DAEMON_REQUEST_TIMEOUT_MS === undefined) {
|
|
63
|
+
delete process.env.BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS;
|
|
64
|
+
} else {
|
|
65
|
+
process.env.BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS = ORIGINAL_DAEMON_REQUEST_TIMEOUT_MS;
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
for (const port of TEST_PID_PORTS) {
|
|
55
69
|
rmSync(resolvePidFileForTests(port), { force: true });
|
|
56
70
|
}
|
|
@@ -59,8 +73,12 @@ afterEach(() => {
|
|
|
59
73
|
});
|
|
60
74
|
|
|
61
75
|
async function withDaemonHarness(
|
|
62
|
-
run: (state: { port: number; requests: CapturedRequest[] }) => Promise<void
|
|
76
|
+
run: (state: { port: number; requests: CapturedRequest[] }) => Promise<void>,
|
|
77
|
+
options: {
|
|
78
|
+
responseDelayMs?: number;
|
|
79
|
+
} = {}
|
|
63
80
|
): Promise<void> {
|
|
81
|
+
const responseDelayMs = options.responseDelayMs ?? 0;
|
|
64
82
|
const requests: CapturedRequest[] = [];
|
|
65
83
|
const server = createServer((socket) => {
|
|
66
84
|
socket.setEncoding("utf8");
|
|
@@ -81,20 +99,27 @@ async function withDaemonHarness(
|
|
|
81
99
|
|
|
82
100
|
const request = JSON.parse(line) as CapturedRequest;
|
|
83
101
|
requests.push(request);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
const respond = () => {
|
|
103
|
+
socket.write(
|
|
104
|
+
`${JSON.stringify({
|
|
105
|
+
id: request.id,
|
|
106
|
+
ok: true,
|
|
107
|
+
traceId: request.traceId,
|
|
108
|
+
sessionId:
|
|
109
|
+
typeof request.arguments.sessionId === "string"
|
|
110
|
+
? request.arguments.sessionId
|
|
111
|
+
: "cli:test",
|
|
112
|
+
data: {
|
|
113
|
+
ok: true
|
|
114
|
+
}
|
|
115
|
+
})}\n`
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
if (responseDelayMs > 0) {
|
|
119
|
+
setTimeout(respond, responseDelayMs);
|
|
120
|
+
} else {
|
|
121
|
+
respond();
|
|
122
|
+
}
|
|
98
123
|
|
|
99
124
|
lineBreakIndex = buffer.indexOf("\n");
|
|
100
125
|
}
|
|
@@ -127,6 +152,126 @@ async function withDaemonHarness(
|
|
|
127
152
|
}
|
|
128
153
|
}
|
|
129
154
|
|
|
155
|
+
async function reservePortForTests(): Promise<number> {
|
|
156
|
+
const server = createServer();
|
|
157
|
+
|
|
158
|
+
await new Promise<void>((resolve, reject) => {
|
|
159
|
+
server.once("error", reject);
|
|
160
|
+
server.listen(0, "127.0.0.1", () => {
|
|
161
|
+
server.off("error", reject);
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const address = server.address() as AddressInfo;
|
|
167
|
+
await new Promise<void>((resolve, reject) => {
|
|
168
|
+
server.close((error) => {
|
|
169
|
+
if (error !== undefined) {
|
|
170
|
+
reject(error);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
resolve();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return address.port;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function startDaemonHarnessOnPort(
|
|
181
|
+
port: number,
|
|
182
|
+
requests: CapturedRequest[],
|
|
183
|
+
options: {
|
|
184
|
+
responseDelayMs?: number;
|
|
185
|
+
} = {}
|
|
186
|
+
): Promise<() => Promise<void>> {
|
|
187
|
+
const responseDelayMs = options.responseDelayMs ?? 0;
|
|
188
|
+
const server = createServer((socket) => {
|
|
189
|
+
socket.setEncoding("utf8");
|
|
190
|
+
let buffer = "";
|
|
191
|
+
|
|
192
|
+
socket.on("data", (chunk: string) => {
|
|
193
|
+
buffer += chunk;
|
|
194
|
+
|
|
195
|
+
let lineBreakIndex = buffer.indexOf("\n");
|
|
196
|
+
while (lineBreakIndex >= 0) {
|
|
197
|
+
const line = buffer.slice(0, lineBreakIndex).trim();
|
|
198
|
+
buffer = buffer.slice(lineBreakIndex + 1);
|
|
199
|
+
|
|
200
|
+
if (line.length === 0) {
|
|
201
|
+
lineBreakIndex = buffer.indexOf("\n");
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const request = JSON.parse(line) as CapturedRequest;
|
|
206
|
+
requests.push(request);
|
|
207
|
+
const respond = () => {
|
|
208
|
+
socket.write(
|
|
209
|
+
`${JSON.stringify({
|
|
210
|
+
id: request.id,
|
|
211
|
+
ok: true,
|
|
212
|
+
traceId: request.traceId,
|
|
213
|
+
sessionId:
|
|
214
|
+
typeof request.arguments.sessionId === "string"
|
|
215
|
+
? request.arguments.sessionId
|
|
216
|
+
: "cli:test",
|
|
217
|
+
data: {
|
|
218
|
+
ok: true
|
|
219
|
+
}
|
|
220
|
+
})}\n`
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
if (responseDelayMs > 0) {
|
|
224
|
+
setTimeout(respond, responseDelayMs);
|
|
225
|
+
} else {
|
|
226
|
+
respond();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
lineBreakIndex = buffer.indexOf("\n");
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await new Promise<void>((resolve, reject) => {
|
|
235
|
+
server.once("error", reject);
|
|
236
|
+
server.listen(port, "127.0.0.1", () => {
|
|
237
|
+
server.off("error", reject);
|
|
238
|
+
resolve();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return async () =>
|
|
243
|
+
await new Promise<void>((resolve, reject) => {
|
|
244
|
+
server.close((error) => {
|
|
245
|
+
if (error !== undefined) {
|
|
246
|
+
reject(error);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
resolve();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function importDaemonClientWithMockedSpawn(
|
|
255
|
+
spawnImplementation: () => {
|
|
256
|
+
pid: number;
|
|
257
|
+
unref(): void;
|
|
258
|
+
}
|
|
259
|
+
): Promise<{
|
|
260
|
+
callDaemonToolWithMock: typeof callDaemonTool;
|
|
261
|
+
spawnMock: ReturnType<typeof vi.fn>;
|
|
262
|
+
}> {
|
|
263
|
+
vi.resetModules();
|
|
264
|
+
const spawnMock = vi.fn(spawnImplementation);
|
|
265
|
+
vi.doMock("node:child_process", () => ({
|
|
266
|
+
spawn: spawnMock
|
|
267
|
+
}));
|
|
268
|
+
const daemonClientModule = await import("./daemon-client");
|
|
269
|
+
return {
|
|
270
|
+
callDaemonToolWithMock: daemonClientModule.callDaemonTool,
|
|
271
|
+
spawnMock
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
130
275
|
describe("daemon client auth token forwarding", () => {
|
|
131
276
|
it("includes env token in daemon status probes", async () => {
|
|
132
277
|
await withDaemonHarness(async ({ port, requests }) => {
|
|
@@ -251,3 +396,117 @@ describe("daemon client auth token forwarding", () => {
|
|
|
251
396
|
expect(killSpy).not.toHaveBeenCalled();
|
|
252
397
|
});
|
|
253
398
|
});
|
|
399
|
+
|
|
400
|
+
describe("daemon client timeout env branches", () => {
|
|
401
|
+
it("applies BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS to daemon responses", async () => {
|
|
402
|
+
await withDaemonHarness(
|
|
403
|
+
async ({ port }) => {
|
|
404
|
+
process.env.BROWSERCTL_DAEMON_PORT = String(port);
|
|
405
|
+
process.env.BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS = "10";
|
|
406
|
+
|
|
407
|
+
await expect(
|
|
408
|
+
callDaemonTool("browser.tab.list", {
|
|
409
|
+
sessionId: "cli:timeout"
|
|
410
|
+
})
|
|
411
|
+
).rejects.toThrow("Timed out waiting for daemon response after 10ms");
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
responseDelayMs: 80
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("falls back to default request timeout when env value is invalid", async () => {
|
|
420
|
+
await withDaemonHarness(
|
|
421
|
+
async ({ port, requests }) => {
|
|
422
|
+
process.env.BROWSERCTL_DAEMON_PORT = String(port);
|
|
423
|
+
process.env.BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS = "-1";
|
|
424
|
+
|
|
425
|
+
await expect(
|
|
426
|
+
callDaemonTool("browser.tab.list", {
|
|
427
|
+
sessionId: "cli:timeout-fallback"
|
|
428
|
+
})
|
|
429
|
+
).resolves.toEqual({
|
|
430
|
+
ok: true
|
|
431
|
+
});
|
|
432
|
+
expect(requests).toHaveLength(1);
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
responseDelayMs: 80
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("applies BROWSERCTL_DAEMON_STARTUP_TIMEOUT_MS while polling spawned daemon readiness", async () => {
|
|
441
|
+
const unavailablePort = await reservePortForTests();
|
|
442
|
+
TEST_PID_PORTS.add(unavailablePort);
|
|
443
|
+
process.env.BROWSERCTL_DAEMON_PORT = String(unavailablePort);
|
|
444
|
+
process.env.BROWSERCTL_DAEMON_STARTUP_TIMEOUT_MS = "50";
|
|
445
|
+
process.env.BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS = "20";
|
|
446
|
+
|
|
447
|
+
const { callDaemonToolWithMock, spawnMock } = await importDaemonClientWithMockedSpawn(() => ({
|
|
448
|
+
pid: 424242,
|
|
449
|
+
unref: vi.fn()
|
|
450
|
+
}));
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const startedAt = Date.now();
|
|
454
|
+
await expect(
|
|
455
|
+
callDaemonToolWithMock("browser.status", {
|
|
456
|
+
sessionId: "cli:startup-timeout"
|
|
457
|
+
})
|
|
458
|
+
).rejects.toThrow(`Daemon did not become ready on port ${unavailablePort}:`);
|
|
459
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
460
|
+
expect(Date.now() - startedAt).toBeLessThan(1_500);
|
|
461
|
+
} finally {
|
|
462
|
+
vi.doUnmock("node:child_process");
|
|
463
|
+
vi.resetModules();
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("falls back to default startup timeout when env value is invalid", async () => {
|
|
468
|
+
const port = await reservePortForTests();
|
|
469
|
+
TEST_PID_PORTS.add(port);
|
|
470
|
+
process.env.BROWSERCTL_DAEMON_PORT = String(port);
|
|
471
|
+
process.env.BROWSERCTL_DAEMON_STARTUP_TIMEOUT_MS = "-1";
|
|
472
|
+
process.env.BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS = "50";
|
|
473
|
+
|
|
474
|
+
const requests: CapturedRequest[] = [];
|
|
475
|
+
let closeServer: (() => Promise<void>) | undefined;
|
|
476
|
+
let startupError: unknown;
|
|
477
|
+
const startupTimer = setTimeout(() => {
|
|
478
|
+
void startDaemonHarnessOnPort(port, requests)
|
|
479
|
+
.then((close) => {
|
|
480
|
+
closeServer = close;
|
|
481
|
+
})
|
|
482
|
+
.catch((error) => {
|
|
483
|
+
startupError = error;
|
|
484
|
+
});
|
|
485
|
+
}, 300);
|
|
486
|
+
|
|
487
|
+
const { callDaemonToolWithMock, spawnMock } = await importDaemonClientWithMockedSpawn(() => ({
|
|
488
|
+
pid: 434343,
|
|
489
|
+
unref: vi.fn()
|
|
490
|
+
}));
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
await expect(
|
|
494
|
+
callDaemonToolWithMock("browser.status", {
|
|
495
|
+
sessionId: "cli:startup-fallback"
|
|
496
|
+
})
|
|
497
|
+
).resolves.toEqual({
|
|
498
|
+
ok: true
|
|
499
|
+
});
|
|
500
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(requests.some((request) => request.name === "browser.status")).toBe(true);
|
|
502
|
+
expect(startupError).toBeUndefined();
|
|
503
|
+
} finally {
|
|
504
|
+
clearTimeout(startupTimer);
|
|
505
|
+
if (closeServer !== undefined) {
|
|
506
|
+
await closeServer();
|
|
507
|
+
}
|
|
508
|
+
vi.doUnmock("node:child_process");
|
|
509
|
+
vi.resetModules();
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
});
|
|
@@ -229,6 +229,42 @@ describe("browserd container", () => {
|
|
|
229
229
|
expect(config.defaultDriver).toBe("chrome-relay");
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
it("parses managed-local launch env branches for browser/headless/channel/path/timeout/args", () => {
|
|
233
|
+
const config = loadBrowserdConfig({
|
|
234
|
+
BROWSERD_MANAGED_LOCAL_BROWSER: "firefox",
|
|
235
|
+
BROWSERD_MANAGED_LOCAL_HEADLESS: "off",
|
|
236
|
+
BROWSERD_MANAGED_LOCAL_CHANNEL: "msedge",
|
|
237
|
+
BROWSERD_MANAGED_LOCAL_EXECUTABLE_PATH: "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
238
|
+
BROWSERD_MANAGED_LOCAL_LAUNCH_TIMEOUT_MS: "45000",
|
|
239
|
+
BROWSERD_MANAGED_LOCAL_ARGS: "--disable-gpu,--lang=en-US"
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(config.managedLocalLaunch).toEqual({
|
|
243
|
+
browserName: "firefox",
|
|
244
|
+
headless: false,
|
|
245
|
+
channel: "msedge",
|
|
246
|
+
executablePath: "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
247
|
+
launchTimeoutMs: 45000,
|
|
248
|
+
args: ["--disable-gpu", "--lang=en-US"]
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("falls back to managed-local defaults for invalid launch env values", () => {
|
|
253
|
+
const config = loadBrowserdConfig({
|
|
254
|
+
BROWSERD_MANAGED_LOCAL_BROWSER: "invalid-browser",
|
|
255
|
+
BROWSERD_MANAGED_LOCAL_HEADLESS: "maybe",
|
|
256
|
+
BROWSERD_MANAGED_LOCAL_LAUNCH_TIMEOUT_MS: "-10",
|
|
257
|
+
BROWSERD_MANAGED_LOCAL_ARGS: " , , "
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(config.managedLocalLaunch).toMatchObject({
|
|
261
|
+
browserName: "chromium",
|
|
262
|
+
headless: true,
|
|
263
|
+
launchTimeoutMs: undefined,
|
|
264
|
+
args: []
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
232
268
|
it("does not register managed-local driver when explicitly disabled", () => {
|
|
233
269
|
const c = createContainer(
|
|
234
270
|
loadBrowserdConfig({
|
|
@@ -292,6 +328,26 @@ describe("browserd bootstrap", () => {
|
|
|
292
328
|
runtime.close();
|
|
293
329
|
});
|
|
294
330
|
|
|
331
|
+
it("reads tcp host from BROWSERD_HOST env in tcp transport mode", () => {
|
|
332
|
+
const port = testTcpPort;
|
|
333
|
+
const runtime = bootstrapBrowserd({
|
|
334
|
+
env: createTestEnv({
|
|
335
|
+
BROWSERD_TRANSPORT: "tcp",
|
|
336
|
+
BROWSERD_HOST: "localhost",
|
|
337
|
+
BROWSERD_PORT: String(port),
|
|
338
|
+
BROWSERD_AUTH_TOKEN: "tcp-token"
|
|
339
|
+
})
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(runtime.transport).toBe("tcp");
|
|
343
|
+
expect(runtime.listening).toEqual({
|
|
344
|
+
host: "localhost",
|
|
345
|
+
port
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
runtime.close();
|
|
349
|
+
});
|
|
350
|
+
|
|
295
351
|
it("rejects tcp transport mode when auth token is not configured", () => {
|
|
296
352
|
let runtime: ReturnType<typeof bootstrapBrowserd> | undefined;
|
|
297
353
|
try {
|
|
@@ -1388,6 +1444,36 @@ describe("browserd bootstrap", () => {
|
|
|
1388
1444
|
output.end();
|
|
1389
1445
|
});
|
|
1390
1446
|
|
|
1447
|
+
it("uses legacy stdio protocol when BROWSERD_STDIO_PROTOCOL=legacy", async () => {
|
|
1448
|
+
const input = new PassThrough();
|
|
1449
|
+
const output = new PassThrough();
|
|
1450
|
+
const runtime = bootstrapBrowserd({
|
|
1451
|
+
env: createTestEnv({
|
|
1452
|
+
BROWSERD_STDIO_PROTOCOL: "legacy"
|
|
1453
|
+
}),
|
|
1454
|
+
input,
|
|
1455
|
+
output
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
const response = await sendToolRequest(input, output, {
|
|
1459
|
+
id: "request-legacy-env",
|
|
1460
|
+
name: "browser.status",
|
|
1461
|
+
traceId: "trace:legacy-env",
|
|
1462
|
+
arguments: {
|
|
1463
|
+
sessionId: "session:legacy-env"
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
expect(response.ok).toBe(true);
|
|
1468
|
+
expect(response.id).toBe("request-legacy-env");
|
|
1469
|
+
expect(response.traceId).toBe("trace:legacy-env");
|
|
1470
|
+
expect(response.sessionId).toBe("session:legacy-env");
|
|
1471
|
+
|
|
1472
|
+
runtime.close();
|
|
1473
|
+
input.end();
|
|
1474
|
+
output.end();
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1391
1477
|
it("preserves id and trace/session metadata when queue-level error handling runs", async () => {
|
|
1392
1478
|
const input = new PassThrough();
|
|
1393
1479
|
const output = new PassThrough();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flrande/browserctl",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.22.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"bin": {
|
|
6
6
|
"browserctl": "bin/browserctl.cjs",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"zod": "^4.3.6"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
39
40
|
"@types/node": "^22.15.30",
|
|
40
41
|
"typescript": "^5.6.3",
|
|
41
42
|
"vitest": "^2.1.8"
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
"test:e2e:full": "vitest run --config vitest.e2e.full.config.ts",
|
|
50
51
|
"test:e2e:stress": "vitest run --config vitest.e2e.stress.config.ts",
|
|
51
52
|
"test:smoke": "vitest run --config vitest.smoke.config.ts",
|
|
53
|
+
"test:coverage": "vitest run --config vitest.config.ts --coverage",
|
|
52
54
|
"test:all": "pnpm run test:unit && pnpm run test:contract && pnpm run test:e2e && pnpm run test:smoke",
|
|
53
55
|
"build": "npm pack --dry-run",
|
|
54
56
|
"typecheck": "pnpm exec tsc --noEmit -p tsconfig.typecheck.json",
|