@beryl-so/cli 0.29.0 → 0.33.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/README.md +70 -55
- package/dist/beryl-test-skill.js +3 -0
- package/dist/commands/accounts.js +6 -6
- package/dist/commands/auth.js +5 -5
- package/dist/commands/config-vars.js +5 -5
- package/dist/commands/environments.js +91 -25
- package/dist/commands/explorations.js +3 -3
- package/dist/commands/groups.js +69 -0
- package/dist/commands/health.js +1 -1
- package/dist/commands/init.js +8 -8
- package/dist/commands/mailboxes.js +6 -6
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/projects.js +5 -5
- package/dist/commands/runs.js +83 -23
- package/dist/commands/tests.js +112 -32
- package/dist/commands/util.js +20 -0
- package/dist/commands/watch.js +2 -1
- package/dist/registry/index.js +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { UsageError } from "../errors.js";
|
|
2
|
+
import { arg, findGroup, flagBool, projectPath } from "./util.js";
|
|
3
|
+
async function resolveGroup(ctx, workspaceId, projectId, ref) {
|
|
4
|
+
const groups = (await ctx.client.get(`${projectPath(workspaceId, projectId)}/groups`));
|
|
5
|
+
const match = findGroup(groups, ref);
|
|
6
|
+
if (!match) {
|
|
7
|
+
const names = groups.map((g) => g.name).join(", ") || "none yet";
|
|
8
|
+
throw new UsageError(`No group '${ref}' in this project (existing: ${names}).`);
|
|
9
|
+
}
|
|
10
|
+
return match;
|
|
11
|
+
}
|
|
12
|
+
export const groupCommands = [
|
|
13
|
+
{
|
|
14
|
+
name: "groups list",
|
|
15
|
+
summary: "List the project's test groups and how many tests each holds",
|
|
16
|
+
scope: "project",
|
|
17
|
+
groupDefault: true,
|
|
18
|
+
groupSummary: "Manage a project's test groups: labels a test can carry any number of, used to filter, run, or schedule a slice of the suite. Groups are created only here (or in Settings → Groups); assigning a test to an unknown name is an error.",
|
|
19
|
+
async run(ctx, input) {
|
|
20
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
21
|
+
return { data: await ctx.client.get(`${projectPath(workspaceId, projectId)}/groups`) };
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "groups create",
|
|
26
|
+
summary: "Create a group",
|
|
27
|
+
scope: "project",
|
|
28
|
+
args: [{ name: "name", description: "Group name (unique per project)", required: true }],
|
|
29
|
+
examples: ["beryl groups create Smoke"],
|
|
30
|
+
async run(ctx, input) {
|
|
31
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
32
|
+
return {
|
|
33
|
+
data: await ctx.client.post(`${projectPath(workspaceId, projectId)}/groups`, {
|
|
34
|
+
name: arg(input, "name"),
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "groups rename",
|
|
41
|
+
summary: "Rename a group (tests keep their membership)",
|
|
42
|
+
scope: "project",
|
|
43
|
+
args: [
|
|
44
|
+
{ name: "group", description: "Current group name or id", required: true },
|
|
45
|
+
{ name: "name", description: "New name", required: true },
|
|
46
|
+
],
|
|
47
|
+
async run(ctx, input) {
|
|
48
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
49
|
+
const group = await resolveGroup(ctx, workspaceId, projectId, arg(input, "group"));
|
|
50
|
+
return {
|
|
51
|
+
data: await ctx.client.patch(`${projectPath(workspaceId, projectId)}/groups/${group.id}`, { name: arg(input, "name") }),
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "groups delete",
|
|
57
|
+
summary: "Delete a group: its tests stay; a schedule targeting only this group goes with it",
|
|
58
|
+
scope: "project",
|
|
59
|
+
args: [{ name: "group", description: "Group name or id", required: true }],
|
|
60
|
+
flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
|
|
61
|
+
async run(ctx, input) {
|
|
62
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
63
|
+
const group = await resolveGroup(ctx, workspaceId, projectId, arg(input, "group"));
|
|
64
|
+
await ctx.confirm(`Delete group '${group.name}' (${group.test_count ?? 0} tests leave it)?`, flagBool(input, "force"));
|
|
65
|
+
await ctx.client.del(`${projectPath(workspaceId, projectId)}/groups/${group.id}`);
|
|
66
|
+
return { human: "Deleted." };
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
package/dist/commands/health.js
CHANGED
|
@@ -8,7 +8,7 @@ export const healthCommands = [
|
|
|
8
8
|
{
|
|
9
9
|
name: "health get",
|
|
10
10
|
groupDefault: true,
|
|
11
|
-
groupSummary: "Site Health
|
|
11
|
+
groupSummary: "Site Health, or how your site reads to search engines and visitors: content, " +
|
|
12
12
|
"speed, mobile, links and security, graded from a real check of your live pages.",
|
|
13
13
|
summary: "Show the latest Site Health report for a project environment",
|
|
14
14
|
description: "A check runs automatically when a project or environment gets its URL. While one " +
|
package/dist/commands/init.js
CHANGED
|
@@ -147,20 +147,20 @@ async function ensureLocalPlaywright(ctx, cwd) {
|
|
|
147
147
|
export const initCommands = [
|
|
148
148
|
{
|
|
149
149
|
name: "init",
|
|
150
|
-
summary: "Set up Beryl in this repo
|
|
150
|
+
summary: "Set up Beryl in this repo: sign in and wire up your coding agent",
|
|
151
151
|
description: "One-command onboarding: signs you in (browser confirm, or an emailed one-time code) " +
|
|
152
152
|
"and wires up your coding " +
|
|
153
|
-
"agent. Nothing is detected and nothing is conditional
|
|
153
|
+
"agent. Nothing is detected and nothing is conditional: every run wires the beryl AND " +
|
|
154
154
|
"playwright MCP servers, writes the authoring skill (user scope: your home " +
|
|
155
155
|
".agents/skills/ + .claude/skills/, so it follows you into every session; --scope " +
|
|
156
156
|
"project: the repo's own dirs, committed for teammates), and installs @playwright/test " +
|
|
157
|
-
"+ chromium if missing
|
|
157
|
+
"+ chromium if missing, since browser authoring and local runs depend on it. By default the " +
|
|
158
158
|
"servers are wired per-user (matching where your login token lives) via `claude mcp add " +
|
|
159
159
|
"-s user`; pass --scope project to write a committed .mcp.json for a shared repo " +
|
|
160
|
-
"instead. No workspace/project pin and no URL prompt
|
|
160
|
+
"instead. No workspace/project pin and no URL prompt: ask Claude to write tests for " +
|
|
161
161
|
"your site and it resolves the workspace, project, and URL. Safe to re-run: every run " +
|
|
162
162
|
"refreshes the authoring skill to this CLI's version (overwriting an older or edited " +
|
|
163
|
-
"copy
|
|
163
|
+
"copy; `beryl guide` prints the same content), and skips whatever else is already set " +
|
|
164
164
|
"up.",
|
|
165
165
|
interactive: true,
|
|
166
166
|
flags: [
|
|
@@ -170,7 +170,7 @@ export const initCommands = [
|
|
|
170
170
|
enum: ["user", "project"],
|
|
171
171
|
description: "Where to wire the MCP servers. `user` (default) configures them per-user (matching " +
|
|
172
172
|
"where your Beryl login token lives) via `claude mcp add -s user`. `project` writes a " +
|
|
173
|
-
"committed .mcp.json for a shared repo
|
|
173
|
+
"committed .mcp.json for a shared repo. Every teammate still runs `beryl login` to " +
|
|
174
174
|
"authenticate",
|
|
175
175
|
},
|
|
176
176
|
{
|
|
@@ -285,8 +285,8 @@ export const initCommands = [
|
|
|
285
285
|
description: "The full guide to authoring durable, healable tests: the ActionPlan shape, outcome " +
|
|
286
286
|
"assertions, natural-language intent, test accounts and the project mailbox " +
|
|
287
287
|
"({{login_email}}, {{mailbox_address}}, {{inbox_address}} + await_email), and the " +
|
|
288
|
-
"local run-fix loop. Same content as the beryl-test skill `beryl init` installs
|
|
289
|
-
`
|
|
288
|
+
"local run-fix loop. Same content as the beryl-test skill `beryl init` installs. " +
|
|
289
|
+
`Skip if a loaded beryl-test skill states v${cliVersion()}; else call this first ` +
|
|
290
290
|
"(works without logging in).",
|
|
291
291
|
examples: ["beryl guide"],
|
|
292
292
|
async run() {
|
|
@@ -7,10 +7,10 @@ export const mailboxCommands = [
|
|
|
7
7
|
name: "mailbox get",
|
|
8
8
|
summary: "The project's mailbox address",
|
|
9
9
|
groupDefault: true,
|
|
10
|
-
groupSummary: "The project's standing email addresses
|
|
10
|
+
groupSummary: "The project's standing email addresses, where its tests receive sign-in mail.",
|
|
11
11
|
description: "Returns the address {{mailbox_address}} resolves to, creating it on first ask. " +
|
|
12
12
|
"Every test that reads mail receives here. A test needing an address the site has " +
|
|
13
|
-
"never seen cites {{inbox_address}} instead
|
|
13
|
+
"never seen cites {{inbox_address}} instead. That renders a `+tag` alias of this " +
|
|
14
14
|
"same mailbox, so a signup stays repeatable without a second address to manage.",
|
|
15
15
|
scope: "project",
|
|
16
16
|
async run(ctx, input) {
|
|
@@ -46,7 +46,7 @@ export const mailboxCommands = [
|
|
|
46
46
|
name: "mailbox create",
|
|
47
47
|
summary: "Add a second mailbox to the project",
|
|
48
48
|
description: "Only needed when a flow requires two genuinely separate inboxes at the same time " +
|
|
49
|
-
"
|
|
49
|
+
"(both sides of an invite handshake). For an address the site has not seen before, " +
|
|
50
50
|
"cite {{inbox_address}} in the plan instead: it aliases the existing mailbox and " +
|
|
51
51
|
"costs nothing to manage.",
|
|
52
52
|
scope: "project",
|
|
@@ -88,12 +88,12 @@ export const mailboxCommands = [
|
|
|
88
88
|
name: "mailbox read",
|
|
89
89
|
summary: "Read the latest email in a mailbox (waits for one to arrive)",
|
|
90
90
|
description: "Waits up to --timeout-s for a matching email and returns it (one blocking request; " +
|
|
91
|
-
"the server caps the wait at 50s
|
|
91
|
+
"the server caps the wait at 50s, so re-run to keep waiting). With --extract-code, " +
|
|
92
92
|
"also asks the server to pull the one-time code out of the email (AI-assisted when " +
|
|
93
93
|
"the email is ambiguous; `code` is null if none was found). Use " +
|
|
94
94
|
"--recipient-contains to read only one `+tag` alias's mail when several identities " +
|
|
95
95
|
"share the mailbox. Exits non-zero if nothing arrives before the timeout. Waits for " +
|
|
96
|
-
"and returns ONE latest matching email
|
|
96
|
+
"and returns ONE latest matching email. `mailbox emails` lists what has already " +
|
|
97
97
|
"arrived, without waiting.",
|
|
98
98
|
scope: "project",
|
|
99
99
|
args: [{ name: "mailbox-id", description: "Mailbox id from `beryl mailbox list`", required: true }],
|
|
@@ -154,7 +154,7 @@ export const mailboxCommands = [
|
|
|
154
154
|
{
|
|
155
155
|
name: "mailbox emails",
|
|
156
156
|
summary: "List the emails a mailbox has received",
|
|
157
|
-
description: "Returns the already-received emails without waiting
|
|
157
|
+
description: "Returns the already-received emails without waiting. `mailbox read` blocks for " +
|
|
158
158
|
"a matching one and returns just it.",
|
|
159
159
|
scope: "project",
|
|
160
160
|
args: [{ name: "mailbox-id", description: "Mailbox id from `beryl mailbox list`", required: true }],
|
package/dist/commands/mcp.js
CHANGED
|
@@ -3,9 +3,9 @@ export const mcpCommands = [
|
|
|
3
3
|
{
|
|
4
4
|
name: "version",
|
|
5
5
|
summary: "Show the running CLI version, API URL, and Node version",
|
|
6
|
-
description: "Answers \"which build am I actually talking to?\"
|
|
6
|
+
description: "Answers \"which build am I actually talking to?\" (the one question a long-lived " +
|
|
7
7
|
"`beryl mcp` process can't otherwise answer, since it loads source at spawn and never " +
|
|
8
|
-
"hot-reloads. Needs no login, so it still works when a token is missing or broken. " +
|
|
8
|
+
"hot-reloads). Needs no login, so it still works when a token is missing or broken. " +
|
|
9
9
|
"Also reports whether the running build is behind npm's `latest` (best-effort: " +
|
|
10
10
|
"`update_available` is null when the registry can't be reached, and the check is " +
|
|
11
11
|
"skipped entirely under BERYL_NO_UPDATE_CHECK).",
|
|
@@ -26,7 +26,7 @@ export const mcpCommands = [
|
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
name: "mcp",
|
|
29
|
-
summary: "Run the Beryl MCP server (stdio)
|
|
29
|
+
summary: "Run the Beryl MCP server (stdio): every CLI command as an agent tool",
|
|
30
30
|
description: "Exposes the CLI's commands as MCP tools over stdio, so coding agents (Claude Code, " +
|
|
31
31
|
"Cursor, …) can create projects, trigger runs, watch the agent, and edit tests. " +
|
|
32
32
|
"Authenticate via BERYL_API_KEY or a prior `beryl login`.",
|
|
@@ -37,7 +37,7 @@ export const projectCommands = [
|
|
|
37
37
|
summary: "List projects in the workspace",
|
|
38
38
|
scope: "workspace",
|
|
39
39
|
groupDefault: true,
|
|
40
|
-
groupSummary: "Create and manage projects
|
|
40
|
+
groupSummary: "Create and manage projects: a site Beryl explores, authors tests for, and runs.",
|
|
41
41
|
async run(ctx, input) {
|
|
42
42
|
const ws = await ctx.requireWorkspace(input);
|
|
43
43
|
const rows = (await ctx.client.get(`/workspaces/${ws}/projects`));
|
|
@@ -70,7 +70,7 @@ export const projectCommands = [
|
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
name: "projects create",
|
|
73
|
-
summary: "Create a project
|
|
73
|
+
summary: "Create a project. With a URL the agent starts exploring; with just --name an empty one",
|
|
74
74
|
scope: "workspace",
|
|
75
75
|
args: [
|
|
76
76
|
{
|
|
@@ -91,13 +91,13 @@ export const projectCommands = [
|
|
|
91
91
|
type: "string",
|
|
92
92
|
enum: ["public", "gated"],
|
|
93
93
|
description: "Whether the site needs a login (gated) or not (public). Default: detected from the " +
|
|
94
|
-
"site
|
|
94
|
+
"site, and only asked when detection is genuinely unsure",
|
|
95
95
|
},
|
|
96
96
|
{ name: "force-new-login", type: "boolean", description: "Ignore any reusable saved login" },
|
|
97
97
|
{
|
|
98
98
|
name: "no-explore",
|
|
99
99
|
type: "boolean",
|
|
100
|
-
description: "Create the project without starting the cloud exploration
|
|
100
|
+
description: "Create the project without starting the cloud exploration. Author tests yourself " +
|
|
101
101
|
"via `beryl tests create` or your coding agent over MCP",
|
|
102
102
|
},
|
|
103
103
|
{ name: "watch", type: "boolean", description: "Stream the agent's exploration live" },
|
|
@@ -190,7 +190,7 @@ export const projectCommands = [
|
|
|
190
190
|
},
|
|
191
191
|
{
|
|
192
192
|
name: "projects re-explore",
|
|
193
|
-
summary: "Send the agent back in
|
|
193
|
+
summary: "Send the agent back in to run/heal existing tests and discover new flows",
|
|
194
194
|
scope: "project",
|
|
195
195
|
flags: [
|
|
196
196
|
{ name: "watch", type: "boolean", description: "Stream the agent's exploration live" },
|
package/dist/commands/runs.js
CHANGED
|
@@ -7,7 +7,7 @@ import { countPlannedFrames, countWrittenFrames, PlaywrightMissingError, } from
|
|
|
7
7
|
import { dim, green, red, yellow } from "../output.js";
|
|
8
8
|
import { anyGap, confirmInstall, describeGaps, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
|
|
9
9
|
import { ProgressBar } from "../progress.js";
|
|
10
|
-
import { arg, flagBool, flagNum, flagStr, projectPath } from "./util.js";
|
|
10
|
+
import { arg, flagBool, flagNum, flagStr, idsInGroup, projectPath } from "./util.js";
|
|
11
11
|
import { watchRun } from "./watch.js";
|
|
12
12
|
const MAX_FAILURE_SCREENSHOTS = 5;
|
|
13
13
|
// One filesystem check before a single spec is fetched. A missing browser binary otherwise
|
|
@@ -50,10 +50,19 @@ export const runCommands = [
|
|
|
50
50
|
name: "runs trigger",
|
|
51
51
|
summary: "Trigger a test run (whole suite, a subset, or one environment)",
|
|
52
52
|
description: "Runs execute in Beryl's cloud. With --watch the CLI streams live progress and " +
|
|
53
|
-
"exits 0 only if every test passed
|
|
53
|
+
"exits 0 only if every test passed, so wire it straight into CI. A run with a " +
|
|
54
|
+
"heal-eligible failure completes only after Beryl has tried to heal it: a repaired " +
|
|
55
|
+
"test is re-run inside the same run and counts as passed (reported as `healed`), so " +
|
|
56
|
+
"the final counts, the report and the completion email all reflect the repair.",
|
|
54
57
|
scope: "project",
|
|
55
58
|
flags: [
|
|
56
59
|
{ name: "test", type: "strings", description: "Run only these test ids (repeatable)" },
|
|
60
|
+
{
|
|
61
|
+
name: "group",
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Run only the active tests in this group (by name), resolved to ids before the run, " +
|
|
64
|
+
"so the run records exactly what it ran. Cannot be combined with --test.",
|
|
65
|
+
},
|
|
57
66
|
{ name: "env", type: "string", description: "Environment id to run against" },
|
|
58
67
|
{ name: "url-override", type: "string", description: "Replace the base URL (preview deploys)" },
|
|
59
68
|
{
|
|
@@ -75,11 +84,22 @@ export const runCommands = [
|
|
|
75
84
|
"beryl runs trigger --url-override https://preview-123.example.com --watch --timeout 30",
|
|
76
85
|
"beryl runs trigger --url-override https://preview-123.example.com --header x-vercel-protection-bypass=<token> --watch",
|
|
77
86
|
"beryl runs trigger --test 4f… --test 9a…",
|
|
87
|
+
"beryl runs trigger --group Smoke --watch",
|
|
78
88
|
"beryl runs trigger --retries 0 --watch",
|
|
79
89
|
],
|
|
80
90
|
async run(ctx, input) {
|
|
81
91
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
82
|
-
|
|
92
|
+
let tests = input.flags.test;
|
|
93
|
+
const group = flagStr(input, "group");
|
|
94
|
+
if (group && tests && tests.length > 0)
|
|
95
|
+
throw new UsageError("--group cannot be combined with --test");
|
|
96
|
+
if (group) {
|
|
97
|
+
const listed = (await ctx.client.get(`${projectPath(workspaceId, projectId)}/tests`));
|
|
98
|
+
tests = idsInGroup(listed, group);
|
|
99
|
+
// Exiting 0 here would be a green CI run that tested nothing.
|
|
100
|
+
if (tests.length === 0)
|
|
101
|
+
throw new UsageError(`No active tests in group '${group}'.`);
|
|
102
|
+
}
|
|
83
103
|
const extraHeaders = parseHeaders(input.flags.header);
|
|
84
104
|
const created = (await ctx.client.post(`${projectPath(workspaceId, projectId)}/runs`, {
|
|
85
105
|
test_case_ids: tests && tests.length > 0 ? tests : null,
|
|
@@ -96,7 +116,7 @@ export const runCommands = [
|
|
|
96
116
|
{
|
|
97
117
|
name: "runs local",
|
|
98
118
|
summary: "Run tests on your machine with your own Playwright; results sync to Beryl",
|
|
99
|
-
description: "Unlike `runs trigger`, the browser runs on YOUR machine
|
|
119
|
+
description: "Unlike `runs trigger`, the browser runs on YOUR machine: each test's rendered spec is " +
|
|
100
120
|
"fetched and run with your local @playwright/test and the Chromium binary it drives. " +
|
|
101
121
|
"Both are checked once before any spec is fetched, so a machine that can't run tests " +
|
|
102
122
|
"says so once instead of failing every test (on a terminal the CLI offers to install " +
|
|
@@ -109,12 +129,12 @@ export const runCommands = [
|
|
|
109
129
|
"does, and scrubbed from any error text or DOM snapshot before results upload; a test " +
|
|
110
130
|
"whose account has no stored password is skipped with the exact fix-it command. When the " +
|
|
111
131
|
"run finishes, the results and replay artifacts are imported into Beryl as a normal run " +
|
|
112
|
-
"(trigger source `local`)
|
|
132
|
+
"(trigger source `local`), so history, replay, and reports all work; pass --no-sync to " +
|
|
113
133
|
"keep a run entirely off the record while iterating. Session-mode tests behave as " +
|
|
114
134
|
"in the cloud: their account signs in once per invocation and every session-mode " +
|
|
115
135
|
"test rides that session; a failed sign-in fails those tests with the same " +
|
|
116
136
|
"SESSION_* reason a cloud run reports. Only a test that depends on a session Beryl " +
|
|
117
|
-
"captured server-side is skipped, with a note
|
|
137
|
+
"captured server-side is skipped, with a note. Run those with `runs trigger`. Point " +
|
|
118
138
|
"--url-override at a local dev server or preview, and --dir to keep specs, artifacts, " +
|
|
119
139
|
"and reports on disk. Exits 0 only if every executed test passed.",
|
|
120
140
|
scope: "project",
|
|
@@ -131,6 +151,11 @@ export const runCommands = [
|
|
|
131
151
|
type: "boolean",
|
|
132
152
|
description: "Run every active test in the project (the default when no ids are given)",
|
|
133
153
|
},
|
|
154
|
+
{
|
|
155
|
+
name: "group",
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "Run only the active tests in this group (by name). Cannot be combined with test ids.",
|
|
158
|
+
},
|
|
134
159
|
{
|
|
135
160
|
name: "url-override",
|
|
136
161
|
type: "string",
|
|
@@ -139,7 +164,7 @@ export const runCommands = [
|
|
|
139
164
|
{
|
|
140
165
|
name: "env",
|
|
141
166
|
type: "string",
|
|
142
|
-
description: "Environment id to run against and attach the imported run to
|
|
167
|
+
description: "Environment id to run against and attach the imported run to. Use for a " +
|
|
143
168
|
"standing environment; --url-override is for a throwaway host",
|
|
144
169
|
},
|
|
145
170
|
{
|
|
@@ -162,15 +187,23 @@ export const runCommands = [
|
|
|
162
187
|
"beryl runs local 4f… --no-sync --dir ./beryl-local",
|
|
163
188
|
],
|
|
164
189
|
async run(ctx, input) {
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
// Argument validation runs BEFORE the Chromium/Playwright precondition: a bad flag
|
|
191
|
+
// combination should say so on any machine, not report a missing browser.
|
|
167
192
|
// Deduped: a repeated id would put the same test twice in one imported run,
|
|
168
193
|
// which the import manifest rejects.
|
|
169
194
|
const explicitIds = [...new Set(input.args["test-ids"] ?? [])];
|
|
170
|
-
|
|
171
|
-
|
|
195
|
+
const group = flagStr(input, "group");
|
|
196
|
+
if (group && explicitIds.length > 0)
|
|
197
|
+
throw new UsageError("--group cannot be combined with explicit test ids");
|
|
172
198
|
if (explicitIds.length > 0 && flagBool(input, "all"))
|
|
173
199
|
throw new UsageError("--all cannot be combined with explicit test ids");
|
|
200
|
+
if (group && flagBool(input, "all"))
|
|
201
|
+
throw new UsageError("--all cannot be combined with --group");
|
|
202
|
+
// No ids and no group means the whole suite — `beryl runs local` alone is a
|
|
203
|
+
// complete local run.
|
|
204
|
+
const all = explicitIds.length === 0 && !group;
|
|
205
|
+
await ensureRunnableLocally(ctx);
|
|
206
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
174
207
|
const sync = input.flags.sync !== false;
|
|
175
208
|
const urlOverride = flagStr(input, "url-override");
|
|
176
209
|
const dir = flagStr(input, "dir");
|
|
@@ -180,9 +213,15 @@ export const runCommands = [
|
|
|
180
213
|
// The list rows carry the authored name as nl_title (title is the customer's
|
|
181
214
|
// rename, usually unset) — fall through so the terminal shows names, not ids.
|
|
182
215
|
const titles = new Map(listed.map((t) => [t.id, t.title || t.nl_title || t.id.slice(0, 8)]));
|
|
183
|
-
const ids = all
|
|
216
|
+
const ids = all
|
|
217
|
+
? listed.filter((t) => t.is_active !== false).map((t) => t.id)
|
|
218
|
+
: group
|
|
219
|
+
? idsInGroup(listed, group)
|
|
220
|
+
: explicitIds;
|
|
184
221
|
if (ids.length === 0)
|
|
185
|
-
throw
|
|
222
|
+
throw group
|
|
223
|
+
? new UsageError(`No active tests in group '${group}'.`)
|
|
224
|
+
: new CliError("This project has no tests to run.");
|
|
186
225
|
const fetchScript = (id) => ctx.client.get(`${projectPath(workspaceId, projectId)}/tests/${id}/script`, {
|
|
187
226
|
// frames only matter when the run will be imported: they become the replay.
|
|
188
227
|
// environment_id keeps the baked login_email on the same environment the
|
|
@@ -454,15 +493,36 @@ export const runCommands = [
|
|
|
454
493
|
{
|
|
455
494
|
name: "runs list",
|
|
456
495
|
summary: "List recent runs",
|
|
496
|
+
description: "Returns the 50 most recent runs unless --page is given; pass --page to walk the " +
|
|
497
|
+
"full history a slice at a time.",
|
|
457
498
|
scope: "project",
|
|
458
499
|
groupDefault: true,
|
|
459
500
|
groupSummary: "Trigger a run of a project's tests (e.g. in CI), then watch, inspect, and download results.",
|
|
460
|
-
flags: [
|
|
501
|
+
flags: [
|
|
502
|
+
{ name: "env", type: "string", description: "Filter by environment id" },
|
|
503
|
+
{
|
|
504
|
+
name: "page",
|
|
505
|
+
type: "number",
|
|
506
|
+
description: "Return only this 1-indexed page instead of the 50 most recent runs",
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: "page-size",
|
|
510
|
+
type: "number",
|
|
511
|
+
description: "Runs per page when --page is given (default 8, max 100)",
|
|
512
|
+
},
|
|
513
|
+
],
|
|
461
514
|
async run(ctx, input) {
|
|
462
515
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
516
|
+
const page = flagNum(input, "page");
|
|
517
|
+
const rows = page === undefined
|
|
518
|
+
? (await ctx.client.get(`${projectPath(workspaceId, projectId)}/runs`, {
|
|
519
|
+
environment_id: flagStr(input, "env"),
|
|
520
|
+
}))
|
|
521
|
+
: (await ctx.client.get(`${projectPath(workspaceId, projectId)}/runs/page`, {
|
|
522
|
+
page,
|
|
523
|
+
page_size: flagNum(input, "page-size"),
|
|
524
|
+
environment_id: flagStr(input, "env"),
|
|
525
|
+
})).items;
|
|
466
526
|
return { data: rows.map(({ test_results: _omit, ...row }) => row) };
|
|
467
527
|
},
|
|
468
528
|
},
|
|
@@ -471,8 +531,8 @@ export const runCommands = [
|
|
|
471
531
|
summary: "Show one run with its per-test results",
|
|
472
532
|
description: "Over MCP the failure screenshots come back as viewable image content, so an agent can " +
|
|
473
533
|
"look at the page that broke instead of guessing from the error string. Set screenshots " +
|
|
474
|
-
"to false to skip fetching them. Ignored outside MCP
|
|
475
|
-
"Returns the run row with its per-test results
|
|
534
|
+
"to false to skip fetching them. Ignored outside MCP (the terminal cannot show an image). " +
|
|
535
|
+
"Returns the run row with its per-test results. `runs report` returns the generated " +
|
|
476
536
|
"report document, `runs explain` an AI explanation of one failed result.",
|
|
477
537
|
scope: "project",
|
|
478
538
|
args: [{ name: "run-id", description: "Run id", required: true }],
|
|
@@ -525,7 +585,7 @@ export const runCommands = [
|
|
|
525
585
|
{
|
|
526
586
|
name: "runs report",
|
|
527
587
|
summary: "Show the generated report for a run",
|
|
528
|
-
description: "Returns the run's stored generated report (404 until it has been generated)
|
|
588
|
+
description: "Returns the run's stored generated report (404 until it has been generated). " +
|
|
529
589
|
"`runs get` returns the raw run row with per-test results.",
|
|
530
590
|
scope: "project",
|
|
531
591
|
args: [{ name: "run-id", description: "Run id", required: true }],
|
|
@@ -539,8 +599,8 @@ export const runCommands = [
|
|
|
539
599
|
{
|
|
540
600
|
name: "runs download",
|
|
541
601
|
summary: "Download a run's results, with its artifacts, to disk",
|
|
542
|
-
description: "With --dir, fetches the artifact bytes
|
|
543
|
-
"trace zip, and the filmstrip frames of the failing tests
|
|
602
|
+
description: "With --dir, fetches the artifact bytes (screenshots, DOM snapshots, the Playwright " +
|
|
603
|
+
"trace zip, and the filmstrip frames of the failing tests) into <dir>/<test-result-id>/ " +
|
|
544
604
|
"alongside a run.json manifest. Artifact URLs are short-lived, so download rather than " +
|
|
545
605
|
"stash them. With --out (or neither), writes only the JSON manifest.",
|
|
546
606
|
scope: "project",
|
|
@@ -607,7 +667,7 @@ export const runCommands = [
|
|
|
607
667
|
name: "runs explain",
|
|
608
668
|
summary: "Explain, with AI, why a test result failed",
|
|
609
669
|
description: "Takes a single test-RESULT id (not a run id) and returns an AI failure " +
|
|
610
|
-
"explanation for that result
|
|
670
|
+
"explanation for that result. `runs get` lists a run's results and their ids.",
|
|
611
671
|
scope: "project",
|
|
612
672
|
args: [{ name: "result-id", description: "Test result id (from `beryl runs get`)", required: true }],
|
|
613
673
|
async run(ctx, input) {
|