@beryl-so/cli 0.45.0 → 0.46.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/README.md CHANGED
@@ -175,6 +175,7 @@ Create and manage projects: a site Beryl explores, authors tests for, and runs.
175
175
  | `beryl projects get` | Show one project, including its current exploration state | `projects_get` |
176
176
  | `beryl projects create [url]` | Create a project. With a URL the agent starts exploring; with just --name an empty one | `projects_create` |
177
177
  | `beryl projects rename <name>` | Rename a project | `projects_rename` |
178
+ | `beryl projects quarantine [state]` | Turn auto quarantine on or off and tune when it mutes and releases a test | `projects_quarantine` |
178
179
  | `beryl projects delete` | Delete a project and all its tests and runs | `projects_delete` |
179
180
  | `beryl projects re-explore` | Send the agent back in to run/heal existing tests and discover new flows | none |
180
181
  | `beryl projects report` | Aggregate quality report across recent runs (pass rates, flaky tests, trend) | `projects_report` |
@@ -159,7 +159,17 @@ An empty \`accounts list\` means nothing is set up. In order of preference:
159
159
  1. **The user has a dedicated test account** → bank it:
160
160
  \`beryl accounts create --email <email> --password <password>\`. A DEDICATED test
161
161
  account only — never a real user's.
162
- 2. **No credentials, but the app has a signup form** → let Beryl make one.
162
+ 2. **Only Google (or another SSO) sign-in, no email+password form** → the same path:
163
+ ask for a DEDICATED Google test account and bank it with \`accounts create --email
164
+ --password\`. Its sign-in plan runs through Google's own pages — click the app's
165
+ Google button, fill \`#identifierId\` with \`{{login_email}}\`, click
166
+ \`#identifierNext\`, fill \`input[name=Passwd]\` with \`{{login_password}}\`, click
167
+ \`#passwordNext\`, then assert the app's signed-in shell. Store it with \`accounts
168
+ set-login\`, prove it with \`accounts check\`. Author its tests with \`auth_mode:
169
+ "session"\` only, so Google is signed in once per run, never per test. Google may
170
+ still challenge a scripted sign-in now and then ("Verify it's you"); the captured
171
+ session (end of §1) is the fallback for that, not the default.
172
+ 3. **No credentials, but the app has a signup form** → let Beryl make one.
163
173
  \`beryl accounts create --type beryl\`, then sign it up with
164
174
  \`beryl accounts signup <id> --file <signup-plan.json>\`. The plan is the app's own
165
175
  signup form as an ActionPlan, ending LOGGED IN, filling \`{{mailbox_address}}\` and
@@ -168,7 +178,7 @@ An empty \`accounts list\` means nothing is set up. In order of preference:
168
178
  or the record was lost) skips signup: store its sign-in with \`accounts set-login\`.
169
179
  No password sign-in? Pass \`--login-method otp\` (or \`magic_link\`) and let the plan
170
180
  \`await_email\` through; later sign-ins read the same mailbox.
171
- 3. **Neither** → the flow is not testable authenticated. Say so rather than guessing.
181
+ 4. **Neither** → the flow is not testable authenticated. Say so rather than guessing.
172
182
 
173
183
  A project with no accounts falls back to the \`LOGIN_EMAIL\` variable +
174
184
  \`LOGIN_PASSWORD\` secret with the same handle resolution, so existing tests are
@@ -222,11 +232,13 @@ pending", "the share link is issued") — a real, strong outcome. While AUTHORIN
222
232
  drive the full handshake live: a second account (\`accounts create --label member\`), or
223
233
  a second mailbox (\`mailbox create --label invitee\`, read with \`mailbox read\`).
224
234
 
225
- SSO-only sites (no email+password form at all) ride a captured session instead: a human
226
- signs in once through a live browser view in the Beryl webapp, Beryl stores the session
227
- encrypted server-side and injects it into cloud runs \`tests create\` verifies such
228
- plans server-side, \`runs trigger\` runs them, \`runs local\` skips them (the session
229
- never leaves Beryl's cloud).
235
+ SSO-only sites are not a dead end: with a dedicated Google test account they follow §1
236
+ item 2 like any password account. The captured session is the FALLBACK no test account
237
+ can be provided, or Google challenges the scripted sign-in: a human signs in once
238
+ through a live browser view in the Beryl webapp, Beryl stores the session encrypted
239
+ server-side and injects it into cloud runs — \`tests create\` verifies such plans
240
+ server-side, \`runs trigger\` runs them, \`runs local\` skips them (the session never
241
+ leaves Beryl's cloud).
230
242
 
231
243
  ## 2. Author locally over the Playwright MCP
232
244
 
@@ -349,14 +361,19 @@ Every one of these has produced a wrong plan or a red \`tests create\`. Check th
349
361
  \`text=X\` to a case-insensitive SUBSTRING — \`text=Continue\` is ambiguous. Use
350
362
  something unique (\`button[type=submit]\`, \`input[name=email]\`,
351
363
  \`input[autocomplete=one-time-code]\`).
352
- 4. **A single-page app can redirect after the first \`goto\`.** If \`/dashboard\`
364
+ 4. **Google's sign-in pages carry decoys.** The identifier page already holds a hidden
365
+ \`input[type=password]\`, so a \`wait_for\` on it fires before the real field exists —
366
+ target \`input[name=Passwd]\`. Responsive layouts often render the app's "Continue
367
+ with Google" button twice (mobile + desktop); add \`:visible\` or use a structural
368
+ selector so the click is not ambiguous.
369
+ 5. **A single-page app can redirect after the first \`goto\`.** If \`/dashboard\`
353
370
  client-side redirects to \`/dashboard/<id>\`, a click fired straight after the goto
354
371
  lands on the pre-redirect render and is discarded on re-render. Put a \`wait_for\`
355
372
  on something that exists only AFTER the redirect, then act.
356
- 5. **Never bake an id into a URL.** \`goto /projects/8ab46d63-.../settings\` breaks for
373
+ 6. **Never bake an id into a URL.** \`goto /projects/8ab46d63-.../settings\` breaks for
357
374
  any other account. Navigate to the stable entry point and click through
358
375
  (\`a[href$='/settings']\`) — the plan is about the app, not your row.
359
- 6. **Assert durable content, not the empty state.** "No tests yet" is true today and
376
+ 7. **Assert durable content, not the empty state.** "No tests yet" is true today and
360
377
  false the moment anything exists. Prefer what is structural — a section heading, a
361
378
  permanent explainer, an always-present control.
362
379
 
@@ -65,6 +65,8 @@ export const testAccountCommands = [
65
65
  "the site: pass --email and --password, and it is usable immediately. `--type beryl` " +
66
66
  "reserves one Beryl will sign up itself, addressed at the project mailbox. It starts " +
67
67
  "`pending` and becomes usable after `beryl accounts signup`. " +
68
+ "Works for Google-only apps too: bank a dedicated Google test account as user_provided " +
69
+ "and author its sign-in plan through Google's form. " +
68
70
  "The first account an environment gets is its default whatever you pass.",
69
71
  scope: "project",
70
72
  flags: [
@@ -98,6 +100,7 @@ export const testAccountCommands = [
98
100
  "beryl accounts create --email qa@acme.test --password 'hunter2'",
99
101
  "beryl accounts create --type beryl --login-method otp",
100
102
  "beryl accounts create --email admin@acme.test --password 'hunter2' --label admin",
103
+ "beryl accounts create --email qa@gmail.com --password 'hunter2' # Google-only app",
101
104
  ],
102
105
  async run(ctx, input) {
103
106
  const { workspaceId, projectId } = await ctx.requireProject(input);
@@ -176,6 +176,53 @@ export const projectCommands = [
176
176
  };
177
177
  },
178
178
  },
179
+ {
180
+ name: "projects quarantine",
181
+ summary: "Turn auto quarantine on or off and tune when it mutes and releases a test",
182
+ description: "With auto quarantine on, the end of every run checks the tests that ran: a test that " +
183
+ "failed --failures times in the last --days days with a pass in between is " +
184
+ "quarantined (straight failures are a regression and never are), and a quarantined " +
185
+ "test, manual or auto, is released after --passes first-attempt passes in a row. " +
186
+ "Pass on|off and/or any of the numbers; `projects get` shows the current values.",
187
+ scope: "project",
188
+ args: [{ name: "state", description: "on | off; omit to only tune the numbers" }],
189
+ flags: [
190
+ { name: "failures", type: "number", description: "Failures in the window that quarantine a test (1-50, default 3)" },
191
+ { name: "days", type: "number", description: "Window in days (1-90, default 7)" },
192
+ { name: "passes", type: "number", description: "Passes in a row that release a test (1-50, default 5)" },
193
+ ],
194
+ examples: [
195
+ "beryl projects quarantine on",
196
+ "beryl projects quarantine on --failures 3 --days 7 --passes 5",
197
+ "beryl projects quarantine --passes 3",
198
+ "beryl projects quarantine off",
199
+ ],
200
+ async run(ctx, input) {
201
+ const { workspaceId, projectId } = await ctx.requireProject(input);
202
+ const body = {};
203
+ const state = input.args.state;
204
+ if (typeof state === "string" && state) {
205
+ const lower = state.toLowerCase();
206
+ if (lower !== "on" && lower !== "off") {
207
+ throw new UsageError(`Expected "on" or "off", got "${state}"`);
208
+ }
209
+ body.auto_quarantine = lower === "on";
210
+ }
211
+ const failures = flagNum(input, "failures");
212
+ const days = flagNum(input, "days");
213
+ const passes = flagNum(input, "passes");
214
+ if (failures !== undefined)
215
+ body.quarantine_failures = failures;
216
+ if (days !== undefined)
217
+ body.quarantine_days = days;
218
+ if (passes !== undefined)
219
+ body.quarantine_release_passes = passes;
220
+ if (Object.keys(body).length === 0) {
221
+ throw new UsageError("Pass on|off or at least one of --failures, --days, --passes");
222
+ }
223
+ return { data: await ctx.client.patch(projectPath(workspaceId, projectId), body) };
224
+ },
225
+ },
179
226
  {
180
227
  name: "projects delete",
181
228
  summary: "Delete a project and all its tests and runs",
@@ -587,9 +587,10 @@ export const testCommands = [
587
587
  "red lands in the run's quarantined_count and gates neither the run's verdict nor exit " +
588
588
  "codes, so it can't red-light a deploy. Use " +
589
589
  "it on a persistently flaky test instead of deleting it (which destroys the history) or " +
590
- "asking support to deactivate it (which stops it running at all). After 5 consecutive " +
591
- "clean passes the test reports rehab_ready (advisory only, nothing un-quarantines " +
592
- "itself). `off` un-quarantines.",
590
+ "asking support to deactivate it (which stops it running at all). After the project's " +
591
+ "release threshold of consecutive clean passes (default 5) the test reports " +
592
+ "rehab_ready: a suggestion while auto quarantine is off, the actual release once " +
593
+ "`projects quarantine on` has turned it on. `off` un-quarantines.",
593
594
  scope: "project",
594
595
  args: [
595
596
  { name: "test-id", description: "Test id", required: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beryl-so/cli",
3
- "version": "0.45.0",
3
+ "version": "0.46.1",
4
4
  "description": "Beryl on the command line — projects, runs, the exploring agent, and an MCP server over the same commands.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,9 +40,11 @@
40
40
  "@types/node": "^26.1.1",
41
41
  "tsx": "^4.23.1",
42
42
  "typescript": "^7.0.2",
43
- "vitest": "^4.1.10"
43
+ "vitest": "^4.1.11"
44
44
  },
45
45
  "overrides": {
46
- "@hono/node-server": "2.0.11"
46
+ "@hono/node-server": "2.0.11",
47
+ "hono": "^4.13.5",
48
+ "@vitest/mocker": "^4.1.11"
47
49
  }
48
50
  }