@flrande/browserctl 0.4.0-dev.15.1 → 0.5.0-dev.19.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/apps/browserctl/src/commands/act.test.ts +71 -0
- package/apps/browserctl/src/commands/act.ts +45 -1
- package/apps/browserctl/src/commands/command-wrappers.test.ts +302 -0
- package/apps/browserctl/src/commands/console-list.test.ts +102 -0
- package/apps/browserctl/src/commands/console-list.ts +89 -1
- package/apps/browserctl/src/commands/har-export.test.ts +112 -0
- package/apps/browserctl/src/commands/har-export.ts +120 -0
- package/apps/browserctl/src/commands/memory-delete.ts +20 -0
- package/apps/browserctl/src/commands/memory-inspect.ts +20 -0
- package/apps/browserctl/src/commands/memory-list.ts +90 -0
- package/apps/browserctl/src/commands/memory-mode-set.ts +29 -0
- package/apps/browserctl/src/commands/memory-purge.ts +16 -0
- package/apps/browserctl/src/commands/memory-resolve.ts +56 -0
- package/apps/browserctl/src/commands/memory-status.ts +16 -0
- package/apps/browserctl/src/commands/memory-ttl-set.ts +28 -0
- package/apps/browserctl/src/commands/memory-upsert.ts +142 -0
- package/apps/browserctl/src/commands/network-list.test.ts +110 -0
- package/apps/browserctl/src/commands/network-list.ts +112 -0
- package/apps/browserctl/src/commands/session-drop.test.ts +36 -0
- package/apps/browserctl/src/commands/session-drop.ts +16 -0
- package/apps/browserctl/src/commands/session-list.test.ts +81 -0
- package/apps/browserctl/src/commands/session-list.ts +70 -0
- package/apps/browserctl/src/commands/trace-get.test.ts +61 -0
- package/apps/browserctl/src/commands/trace-get.ts +62 -0
- package/apps/browserctl/src/commands/wait-element.test.ts +80 -0
- package/apps/browserctl/src/commands/wait-element.ts +76 -0
- package/apps/browserctl/src/commands/wait-text.test.ts +110 -0
- package/apps/browserctl/src/commands/wait-text.ts +93 -0
- package/apps/browserctl/src/commands/wait-url.test.ts +80 -0
- package/apps/browserctl/src/commands/wait-url.ts +76 -0
- package/apps/browserctl/src/main.dispatch.test.ts +206 -1
- package/apps/browserctl/src/main.test.ts +30 -0
- package/apps/browserctl/src/main.ts +246 -4
- package/apps/browserd/src/container.ts +1603 -48
- package/apps/browserd/src/main.test.ts +538 -1
- package/apps/browserd/src/tool-matrix.test.ts +492 -3
- package/package.json +5 -1
- package/packages/core/src/driver.ts +1 -1
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/navigation-memory.test.ts +259 -0
- package/packages/core/src/navigation-memory.ts +360 -0
- package/packages/core/src/session-store.test.ts +33 -0
- package/packages/core/src/session-store.ts +111 -6
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +112 -2
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +233 -10
- package/packages/driver-managed/src/managed-driver.test.ts +124 -0
- package/packages/driver-managed/src/managed-driver.ts +233 -17
- package/packages/driver-managed/src/managed-local-driver.test.ts +104 -2
- package/packages/driver-managed/src/managed-local-driver.ts +232 -10
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +112 -2
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +232 -10
- package/packages/transport-mcp-stdio/src/tool-map.ts +18 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const { callDaemonToolMock } = vi.hoisted(() => ({
|
|
4
|
+
callDaemonToolMock: vi.fn()
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("../daemon-client", () => ({
|
|
8
|
+
callDaemonTool: callDaemonToolMock
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { runActCommand } from "./act";
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
callDaemonToolMock.mockReset();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("runActCommand", () => {
|
|
18
|
+
it("forwards --payload-json into action payload", async () => {
|
|
19
|
+
callDaemonToolMock.mockResolvedValue({
|
|
20
|
+
ok: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await runActCommand([
|
|
24
|
+
"click",
|
|
25
|
+
"target:1",
|
|
26
|
+
"--payload-json",
|
|
27
|
+
"{\"selector\":\"#submit\",\"delayMs\":200}"
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(callDaemonToolMock).toHaveBeenCalledWith("browser.act", {
|
|
32
|
+
sessionId: "cli:local",
|
|
33
|
+
targetId: "target:1",
|
|
34
|
+
action: {
|
|
35
|
+
type: "click",
|
|
36
|
+
payload: {
|
|
37
|
+
selector: "#submit",
|
|
38
|
+
delayMs: 200
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("throws when --payload-json value is missing", async () => {
|
|
45
|
+
await expect(runActCommand(["click", "target:1", "--payload-json"])).rejects.toThrow(
|
|
46
|
+
"Missing value for --payload-json."
|
|
47
|
+
);
|
|
48
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("throws when --payload-json is invalid JSON", async () => {
|
|
52
|
+
await expect(
|
|
53
|
+
runActCommand(["click", "target:1", "--payload-json", "{not-json}"])
|
|
54
|
+
).rejects.toThrow("Invalid JSON for --payload-json:");
|
|
55
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("throws when --payload-json is not a JSON object", async () => {
|
|
59
|
+
await expect(runActCommand(["click", "target:1", "--payload-json", "[1,2,3]"])).rejects.toThrow(
|
|
60
|
+
"--payload-json must decode to a JSON object."
|
|
61
|
+
);
|
|
62
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("throws on unknown act options", async () => {
|
|
66
|
+
await expect(runActCommand(["click", "target:1", "--payload", "{}"])).rejects.toThrow(
|
|
67
|
+
"Unknown act option: --payload"
|
|
68
|
+
);
|
|
69
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -3,17 +3,61 @@ import { buildToolArguments, parseCommandContext, requirePositionalArg } from ".
|
|
|
3
3
|
|
|
4
4
|
export type ActCommandResult = Record<string, unknown>;
|
|
5
5
|
|
|
6
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseActPayload(tokens: string[]): Record<string, unknown> | undefined {
|
|
11
|
+
let payloadJson: string | undefined;
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
14
|
+
const token = tokens[index];
|
|
15
|
+
if (token === "--payload-json") {
|
|
16
|
+
const value = tokens[index + 1];
|
|
17
|
+
if (value === undefined) {
|
|
18
|
+
throw new Error("Missing value for --payload-json.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
payloadJson = value;
|
|
22
|
+
index += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new Error(`Unknown act option: ${token}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (payloadJson === undefined) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let parsedPayload: unknown;
|
|
34
|
+
try {
|
|
35
|
+
parsedPayload = JSON.parse(payloadJson);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
throw new Error(`Invalid JSON for --payload-json: ${message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isObjectRecord(parsedPayload)) {
|
|
42
|
+
throw new Error("--payload-json must decode to a JSON object.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parsedPayload;
|
|
46
|
+
}
|
|
47
|
+
|
|
6
48
|
export async function runActCommand(args: string[]): Promise<ActCommandResult> {
|
|
7
49
|
const context = parseCommandContext(args);
|
|
8
50
|
const actionType = requirePositionalArg(context, 0, "actionType");
|
|
9
51
|
const targetId = requirePositionalArg(context, 1, "targetId");
|
|
52
|
+
const payload = parseActPayload(context.positional.slice(2));
|
|
10
53
|
|
|
11
54
|
return await callDaemonTool<ActCommandResult>(
|
|
12
55
|
"browser.act",
|
|
13
56
|
buildToolArguments(context, {
|
|
14
57
|
targetId,
|
|
15
58
|
action: {
|
|
16
|
-
type: actionType
|
|
59
|
+
type: actionType,
|
|
60
|
+
...(payload !== undefined ? { payload } : {})
|
|
17
61
|
}
|
|
18
62
|
})
|
|
19
63
|
);
|
|
@@ -23,11 +23,24 @@ import { runDownloadWaitCommand } from "./download-wait";
|
|
|
23
23
|
import { runElementScreenshotCommand } from "./element-screenshot";
|
|
24
24
|
import { runFrameListCommand } from "./frame-list";
|
|
25
25
|
import { runFrameSnapshotCommand } from "./frame-snapshot";
|
|
26
|
+
import { runHarExportCommand } from "./har-export";
|
|
27
|
+
import { runMemoryDeleteCommand } from "./memory-delete";
|
|
28
|
+
import { runMemoryInspectCommand } from "./memory-inspect";
|
|
29
|
+
import { runMemoryListCommand } from "./memory-list";
|
|
30
|
+
import { runMemoryModeSetCommand } from "./memory-mode-set";
|
|
31
|
+
import { runMemoryPurgeCommand } from "./memory-purge";
|
|
32
|
+
import { runMemoryResolveCommand } from "./memory-resolve";
|
|
33
|
+
import { runMemoryStatusCommand } from "./memory-status";
|
|
34
|
+
import { runMemoryTtlSetCommand } from "./memory-ttl-set";
|
|
35
|
+
import { runMemoryUpsertCommand } from "./memory-upsert";
|
|
36
|
+
import { runNetworkListCommand } from "./network-list";
|
|
26
37
|
import { runNetworkWaitForCommand } from "./network-wait-for";
|
|
27
38
|
import { runProfileListCommand } from "./profile-list";
|
|
28
39
|
import { runProfileUseCommand } from "./profile-use";
|
|
29
40
|
import { runResponseBodyCommand } from "./response-body";
|
|
30
41
|
import { runScreenshotCommand } from "./screenshot";
|
|
42
|
+
import { runSessionDropCommand } from "./session-drop";
|
|
43
|
+
import { runSessionListCommand } from "./session-list";
|
|
31
44
|
import { runSnapshotCommand } from "./snapshot";
|
|
32
45
|
import { runStatusCommand } from "./status";
|
|
33
46
|
import { runStorageGetCommand } from "./storage-get";
|
|
@@ -36,7 +49,11 @@ import { runTabCloseCommand } from "./tab-close";
|
|
|
36
49
|
import { runTabFocusCommand } from "./tab-focus";
|
|
37
50
|
import { runTabOpenCommand } from "./tab-open";
|
|
38
51
|
import { runTabsCommand } from "./tabs";
|
|
52
|
+
import { runTraceGetCommand } from "./trace-get";
|
|
39
53
|
import { runUploadArmCommand } from "./upload-arm";
|
|
54
|
+
import { runWaitElementCommand } from "./wait-element";
|
|
55
|
+
import { runWaitTextCommand } from "./wait-text";
|
|
56
|
+
import { runWaitUrlCommand } from "./wait-url";
|
|
40
57
|
|
|
41
58
|
type WrapperCase = {
|
|
42
59
|
name: string;
|
|
@@ -104,6 +121,25 @@ const CASES: WrapperCase[] = [
|
|
|
104
121
|
targetId: "target:1"
|
|
105
122
|
}
|
|
106
123
|
},
|
|
124
|
+
{
|
|
125
|
+
name: "session-list",
|
|
126
|
+
run: async () => await runSessionListCommand(["--tenant", "finance", "--limit", "5"]),
|
|
127
|
+
expectedTool: "browser.session.list",
|
|
128
|
+
expectedArgs: {
|
|
129
|
+
...BASE_ARGS,
|
|
130
|
+
tenant: "finance",
|
|
131
|
+
limit: 5
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "session-drop",
|
|
136
|
+
run: async () => await runSessionDropCommand(["tenant-a:job-1"]),
|
|
137
|
+
expectedTool: "browser.session.drop",
|
|
138
|
+
expectedArgs: {
|
|
139
|
+
...BASE_ARGS,
|
|
140
|
+
sessionIdToDelete: "tenant-a:job-1"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
107
143
|
{
|
|
108
144
|
name: "snapshot",
|
|
109
145
|
run: async () => await runSnapshotCommand(["target:1"]),
|
|
@@ -162,6 +198,91 @@ const CASES: WrapperCase[] = [
|
|
|
162
198
|
selector: "#main"
|
|
163
199
|
}
|
|
164
200
|
},
|
|
201
|
+
{
|
|
202
|
+
name: "wait-element",
|
|
203
|
+
run: async () => await runWaitElementCommand(["target:1", "#ready", "--timeout-ms", "1200", "--poll-ms", "20"]),
|
|
204
|
+
expectedTool: "browser.wait.element",
|
|
205
|
+
expectedArgs: {
|
|
206
|
+
...BASE_ARGS,
|
|
207
|
+
targetId: "target:1",
|
|
208
|
+
selector: "#ready",
|
|
209
|
+
timeoutMs: 1200,
|
|
210
|
+
pollMs: 20
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "wait-text",
|
|
215
|
+
run: async () => await runWaitTextCommand(["target:1", "done", "--selector", "#status", "--timeout-ms", "1500"]),
|
|
216
|
+
expectedTool: "browser.wait.text",
|
|
217
|
+
expectedArgs: {
|
|
218
|
+
...BASE_ARGS,
|
|
219
|
+
targetId: "target:1",
|
|
220
|
+
text: "done",
|
|
221
|
+
selector: "#status",
|
|
222
|
+
timeoutMs: 1500
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "wait-url",
|
|
227
|
+
run: async () => await runWaitUrlCommand(["target:1", "/dashboard", "1800"]),
|
|
228
|
+
expectedTool: "browser.wait.url",
|
|
229
|
+
expectedArgs: {
|
|
230
|
+
...BASE_ARGS,
|
|
231
|
+
targetId: "target:1",
|
|
232
|
+
urlPattern: "/dashboard",
|
|
233
|
+
timeoutMs: 1800
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "network-list",
|
|
238
|
+
run: async () =>
|
|
239
|
+
await runNetworkListCommand([
|
|
240
|
+
"target:1",
|
|
241
|
+
"--url-contains",
|
|
242
|
+
"/api",
|
|
243
|
+
"--method",
|
|
244
|
+
"get",
|
|
245
|
+
"--status",
|
|
246
|
+
"200",
|
|
247
|
+
"--since",
|
|
248
|
+
"2026-01-01T00:00:00.000Z",
|
|
249
|
+
"--limit",
|
|
250
|
+
"15"
|
|
251
|
+
]),
|
|
252
|
+
expectedTool: "browser.network.list",
|
|
253
|
+
expectedArgs: {
|
|
254
|
+
...BASE_ARGS,
|
|
255
|
+
targetId: "target:1",
|
|
256
|
+
urlContains: "/api",
|
|
257
|
+
method: "GET",
|
|
258
|
+
status: 200,
|
|
259
|
+
since: "2026-01-01T00:00:00.000Z",
|
|
260
|
+
limit: 15
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "har-export",
|
|
265
|
+
run: async () =>
|
|
266
|
+
await runHarExportCommand([
|
|
267
|
+
"target:1",
|
|
268
|
+
"--include-bodies",
|
|
269
|
+
"--method",
|
|
270
|
+
"post",
|
|
271
|
+
"--status",
|
|
272
|
+
"201",
|
|
273
|
+
"--limit",
|
|
274
|
+
"10"
|
|
275
|
+
]),
|
|
276
|
+
expectedTool: "browser.network.harExport",
|
|
277
|
+
expectedArgs: {
|
|
278
|
+
...BASE_ARGS,
|
|
279
|
+
targetId: "target:1",
|
|
280
|
+
includeBodies: true,
|
|
281
|
+
method: "POST",
|
|
282
|
+
status: 201,
|
|
283
|
+
limit: 10
|
|
284
|
+
}
|
|
285
|
+
},
|
|
165
286
|
{
|
|
166
287
|
name: "network-wait-for",
|
|
167
288
|
run: async () =>
|
|
@@ -330,6 +451,120 @@ const CASES: WrapperCase[] = [
|
|
|
330
451
|
targetId: "target:1",
|
|
331
452
|
requestId: "request:1"
|
|
332
453
|
}
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: "memory-status",
|
|
457
|
+
run: async () => await runMemoryStatusCommand([]),
|
|
458
|
+
expectedTool: "browser.memory.status",
|
|
459
|
+
expectedArgs: {
|
|
460
|
+
...BASE_ARGS
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: "memory-resolve",
|
|
465
|
+
run: async () => await runMemoryResolveCommand(["forum.example", "open_profile"]),
|
|
466
|
+
expectedTool: "browser.memory.resolve",
|
|
467
|
+
expectedArgs: {
|
|
468
|
+
...BASE_ARGS,
|
|
469
|
+
domain: "forum.example",
|
|
470
|
+
profileId: "default",
|
|
471
|
+
intentKey: "open_profile"
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "memory-upsert",
|
|
476
|
+
run: async () =>
|
|
477
|
+
await runMemoryUpsertCommand([
|
|
478
|
+
"forum.example",
|
|
479
|
+
"open_profile",
|
|
480
|
+
"--signals-json",
|
|
481
|
+
"[{\"kind\":\"urlPattern\",\"value\":\"/profile\"}]",
|
|
482
|
+
"--confidence",
|
|
483
|
+
"0.9",
|
|
484
|
+
"--confirmed"
|
|
485
|
+
]),
|
|
486
|
+
expectedTool: "browser.memory.upsert",
|
|
487
|
+
expectedArgs: {
|
|
488
|
+
...BASE_ARGS,
|
|
489
|
+
domain: "forum.example",
|
|
490
|
+
profileId: "default",
|
|
491
|
+
intentKey: "open_profile",
|
|
492
|
+
signals: [{ kind: "urlPattern", value: "/profile" }],
|
|
493
|
+
confidence: 0.9,
|
|
494
|
+
confirmed: true
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "memory-list",
|
|
499
|
+
run: async () =>
|
|
500
|
+
await runMemoryListCommand([
|
|
501
|
+
"--domain",
|
|
502
|
+
"forum.example",
|
|
503
|
+
"--profile-id",
|
|
504
|
+
"managed",
|
|
505
|
+
"--intent-key",
|
|
506
|
+
"open_profile"
|
|
507
|
+
]),
|
|
508
|
+
expectedTool: "browser.memory.list",
|
|
509
|
+
expectedArgs: {
|
|
510
|
+
...BASE_ARGS,
|
|
511
|
+
domain: "forum.example",
|
|
512
|
+
profileId: "managed",
|
|
513
|
+
intentKey: "open_profile"
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "memory-inspect",
|
|
518
|
+
run: async () => await runMemoryInspectCommand(["memory:1"]),
|
|
519
|
+
expectedTool: "browser.memory.inspect",
|
|
520
|
+
expectedArgs: {
|
|
521
|
+
...BASE_ARGS,
|
|
522
|
+
id: "memory:1"
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: "memory-delete",
|
|
527
|
+
run: async () => await runMemoryDeleteCommand(["memory:1"]),
|
|
528
|
+
expectedTool: "browser.memory.delete",
|
|
529
|
+
expectedArgs: {
|
|
530
|
+
...BASE_ARGS,
|
|
531
|
+
id: "memory:1"
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: "memory-purge",
|
|
536
|
+
run: async () => await runMemoryPurgeCommand([]),
|
|
537
|
+
expectedTool: "browser.memory.purge",
|
|
538
|
+
expectedArgs: {
|
|
539
|
+
...BASE_ARGS
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "memory-mode-set",
|
|
544
|
+
run: async () => await runMemoryModeSetCommand(["ask"]),
|
|
545
|
+
expectedTool: "browser.memory.mode.set",
|
|
546
|
+
expectedArgs: {
|
|
547
|
+
...BASE_ARGS,
|
|
548
|
+
mode: "ask"
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: "memory-ttl-set",
|
|
553
|
+
run: async () => await runMemoryTtlSetCommand(["7"]),
|
|
554
|
+
expectedTool: "browser.memory.ttl.set",
|
|
555
|
+
expectedArgs: {
|
|
556
|
+
...BASE_ARGS,
|
|
557
|
+
ttlDays: 7
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "trace-get",
|
|
562
|
+
run: async () => await runTraceGetCommand(["--limit", "20"]),
|
|
563
|
+
expectedTool: "browser.trace.get",
|
|
564
|
+
expectedArgs: {
|
|
565
|
+
...BASE_ARGS,
|
|
566
|
+
limit: 20
|
|
567
|
+
}
|
|
333
568
|
}
|
|
334
569
|
];
|
|
335
570
|
|
|
@@ -383,4 +618,71 @@ describe("command wrappers", () => {
|
|
|
383
618
|
|
|
384
619
|
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
385
620
|
});
|
|
621
|
+
|
|
622
|
+
it.each([
|
|
623
|
+
{
|
|
624
|
+
name: "memory-resolve --profile-id",
|
|
625
|
+
run: async () => await runMemoryResolveCommand(["forum.example", "open_profile", "--profile-id", "--bad"]),
|
|
626
|
+
expectedMessage: "Missing value for --profile-id."
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "memory-list --domain",
|
|
630
|
+
run: async () => await runMemoryListCommand(["--domain", "--bad"]),
|
|
631
|
+
expectedMessage: "Missing value for --domain."
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: "memory-list --profile-id",
|
|
635
|
+
run: async () => await runMemoryListCommand(["--profile-id", "--bad"]),
|
|
636
|
+
expectedMessage: "Missing value for --profile-id."
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: "memory-list --intent-key",
|
|
640
|
+
run: async () => await runMemoryListCommand(["--intent-key", "--bad"]),
|
|
641
|
+
expectedMessage: "Missing value for --intent-key."
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
name: "memory-upsert --profile-id",
|
|
645
|
+
run: async () =>
|
|
646
|
+
await runMemoryUpsertCommand([
|
|
647
|
+
"forum.example",
|
|
648
|
+
"open_profile",
|
|
649
|
+
"--signals-json",
|
|
650
|
+
"[]",
|
|
651
|
+
"--profile-id",
|
|
652
|
+
"--bad"
|
|
653
|
+
]),
|
|
654
|
+
expectedMessage: "Missing value for --profile-id."
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
name: "memory-upsert --signals-json",
|
|
658
|
+
run: async () =>
|
|
659
|
+
await runMemoryUpsertCommand(["forum.example", "open_profile", "--signals-json", "--bad"]),
|
|
660
|
+
expectedMessage: "Missing value for --signals-json."
|
|
661
|
+
}
|
|
662
|
+
])("rejects flag token as option value for $name", async ({ run, expectedMessage }) => {
|
|
663
|
+
await expect(run()).rejects.toThrow(expectedMessage);
|
|
664
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("accepts ttlDays value 0", async () => {
|
|
668
|
+
callDaemonToolMock.mockResolvedValue({ ok: true });
|
|
669
|
+
|
|
670
|
+
await runMemoryTtlSetCommand(["0"]);
|
|
671
|
+
|
|
672
|
+
expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
|
|
673
|
+
expect(callDaemonToolMock).toHaveBeenCalledWith("browser.memory.ttl.set", {
|
|
674
|
+
...BASE_ARGS,
|
|
675
|
+
ttlDays: 0
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it.each(["1.9", "7days", "-1"])(
|
|
680
|
+
"rejects non-strict non-negative integer ttlDays value: %s",
|
|
681
|
+
async (ttlValue) => {
|
|
682
|
+
await expect(runMemoryTtlSetCommand([ttlValue]))
|
|
683
|
+
.rejects.toThrow("ttlDays must be a non-negative integer string.");
|
|
684
|
+
|
|
685
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
686
|
+
}
|
|
687
|
+
);
|
|
386
688
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const { callDaemonToolMock } = vi.hoisted(() => ({
|
|
4
|
+
callDaemonToolMock: vi.fn()
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("../daemon-client", () => ({
|
|
8
|
+
callDaemonTool: callDaemonToolMock
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { runConsoleListCommand } from "./console-list";
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
callDaemonToolMock.mockReset();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("runConsoleListCommand", () => {
|
|
18
|
+
it("forwards filter options", async () => {
|
|
19
|
+
callDaemonToolMock.mockResolvedValue({ ok: true });
|
|
20
|
+
|
|
21
|
+
await runConsoleListCommand([
|
|
22
|
+
"target:1",
|
|
23
|
+
"--type",
|
|
24
|
+
"error",
|
|
25
|
+
"--contains",
|
|
26
|
+
"timeout",
|
|
27
|
+
"--since",
|
|
28
|
+
"2026-01-01T00:00:00.000Z",
|
|
29
|
+
"--limit",
|
|
30
|
+
"20"
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(callDaemonToolMock).toHaveBeenCalledWith("browser.console.list", {
|
|
35
|
+
sessionId: "cli:local",
|
|
36
|
+
targetId: "target:1",
|
|
37
|
+
type: "error",
|
|
38
|
+
contains: "timeout",
|
|
39
|
+
since: "2026-01-01T00:00:00.000Z",
|
|
40
|
+
limit: 20
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it.each([
|
|
45
|
+
{
|
|
46
|
+
label: "--type",
|
|
47
|
+
args: ["target:1", "--type"],
|
|
48
|
+
message: "Missing value for --type."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: "--contains",
|
|
52
|
+
args: ["target:1", "--contains"],
|
|
53
|
+
message: "Missing value for --contains."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "--since",
|
|
57
|
+
args: ["target:1", "--since"],
|
|
58
|
+
message: "Missing value for --since."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: "--limit",
|
|
62
|
+
args: ["target:1", "--limit"],
|
|
63
|
+
message: "Missing value for --limit."
|
|
64
|
+
}
|
|
65
|
+
])("throws when $label is missing a value", async ({ args, message }) => {
|
|
66
|
+
await expect(runConsoleListCommand(args)).rejects.toThrow(message);
|
|
67
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it.each([
|
|
71
|
+
{
|
|
72
|
+
label: "empty --type",
|
|
73
|
+
args: ["target:1", "--type", " "],
|
|
74
|
+
message: "--type must be a non-empty string."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: "empty --contains",
|
|
78
|
+
args: ["target:1", "--contains", " "],
|
|
79
|
+
message: "--contains must be a non-empty string."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: "invalid --limit",
|
|
83
|
+
args: ["target:1", "--limit", "0"],
|
|
84
|
+
message: "--limit must be a positive integer."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: "non-strict --limit text",
|
|
88
|
+
args: ["target:1", "--limit", "20rows"],
|
|
89
|
+
message: "--limit must be a positive integer."
|
|
90
|
+
}
|
|
91
|
+
])("throws on invalid option value: $label", async ({ args, message }) => {
|
|
92
|
+
await expect(runConsoleListCommand(args)).rejects.toThrow(message);
|
|
93
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("throws on unknown option token", async () => {
|
|
97
|
+
await expect(runConsoleListCommand(["target:1", "--unknown"]))
|
|
98
|
+
.rejects.toThrow("Unknown console-list option: --unknown");
|
|
99
|
+
|
|
100
|
+
expect(callDaemonToolMock).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -7,14 +7,102 @@ export type ConsoleListCommandResult = {
|
|
|
7
7
|
entries: unknown[];
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
function parsePositiveInteger(value: string, optionName: string): number {
|
|
11
|
+
const normalized = value.trim();
|
|
12
|
+
if (!/^[1-9]\d*$/.test(normalized)) {
|
|
13
|
+
throw new Error(`${optionName} must be a positive integer.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Number.parseInt(normalized, 10);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseConsoleOptions(tokens: string[]): {
|
|
20
|
+
type?: string;
|
|
21
|
+
contains?: string;
|
|
22
|
+
since?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
} {
|
|
25
|
+
let type: string | undefined;
|
|
26
|
+
let contains: string | undefined;
|
|
27
|
+
let since: string | undefined;
|
|
28
|
+
let limit: number | undefined;
|
|
29
|
+
|
|
30
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
31
|
+
const token = tokens[index];
|
|
32
|
+
|
|
33
|
+
if (token === "--type") {
|
|
34
|
+
const value = tokens[index + 1];
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
throw new Error("Missing value for --type.");
|
|
37
|
+
}
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
if (trimmed.length === 0) {
|
|
40
|
+
throw new Error("--type must be a non-empty string.");
|
|
41
|
+
}
|
|
42
|
+
type = trimmed;
|
|
43
|
+
index += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (token === "--contains") {
|
|
48
|
+
const value = tokens[index + 1];
|
|
49
|
+
if (value === undefined) {
|
|
50
|
+
throw new Error("Missing value for --contains.");
|
|
51
|
+
}
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
if (trimmed.length === 0) {
|
|
54
|
+
throw new Error("--contains must be a non-empty string.");
|
|
55
|
+
}
|
|
56
|
+
contains = trimmed;
|
|
57
|
+
index += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (token === "--since") {
|
|
62
|
+
const value = tokens[index + 1];
|
|
63
|
+
if (value === undefined) {
|
|
64
|
+
throw new Error("Missing value for --since.");
|
|
65
|
+
}
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
if (trimmed.length === 0) {
|
|
68
|
+
throw new Error("--since must be a non-empty ISO timestamp.");
|
|
69
|
+
}
|
|
70
|
+
since = trimmed;
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (token === "--limit") {
|
|
76
|
+
const value = tokens[index + 1];
|
|
77
|
+
if (value === undefined) {
|
|
78
|
+
throw new Error("Missing value for --limit.");
|
|
79
|
+
}
|
|
80
|
+
limit = parsePositiveInteger(value, "--limit");
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new Error(`Unknown console-list option: ${token}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
...(type !== undefined ? { type } : {}),
|
|
90
|
+
...(contains !== undefined ? { contains } : {}),
|
|
91
|
+
...(since !== undefined ? { since } : {}),
|
|
92
|
+
...(limit !== undefined ? { limit } : {})
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
10
96
|
export async function runConsoleListCommand(args: string[]): Promise<ConsoleListCommandResult> {
|
|
11
97
|
const context = parseCommandContext(args);
|
|
12
98
|
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
99
|
+
const options = parseConsoleOptions(context.positional.slice(1));
|
|
13
100
|
|
|
14
101
|
return await callDaemonTool<ConsoleListCommandResult>(
|
|
15
102
|
"browser.console.list",
|
|
16
103
|
buildToolArguments(context, {
|
|
17
|
-
targetId
|
|
104
|
+
targetId,
|
|
105
|
+
...options
|
|
18
106
|
})
|
|
19
107
|
);
|
|
20
108
|
}
|