@beryl-so/cli 0.73.2 → 0.78.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 +6 -10
- package/dist/adapters/mcp.js +1 -1
- package/dist/beryl-test-skill.js +90 -52
- package/dist/commands/accounts.js +31 -2
- package/dist/commands/config-vars.js +86 -15
- package/dist/commands/environments.js +18 -1
- package/dist/commands/init.js +32 -98
- package/dist/commands/jobs.js +4 -0
- package/dist/commands/projects.js +19 -45
- package/dist/commands/runs.js +6 -433
- package/dist/commands/tests.js +5 -107
- package/dist/commands/third-party-integrations.js +26 -3
- package/dist/commands/watch.js +21 -10
- package/dist/lint.js +120 -0
- package/dist/schema.generated.js +183 -28
- package/dist/skill-tables.js +4 -0
- package/package.json +2 -3
- package/dist/email-pump.js +0 -138
- package/dist/local-exec.js +0 -239
- package/dist/local-run.js +0 -340
- package/dist/playwright-install.js +0 -220
- package/dist/progress.js +0 -44
package/README.md
CHANGED
|
@@ -13,10 +13,9 @@ npx @beryl-so/cli@latest init
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Signs you in (browser approval by default, emailed one-time code as fallback; new
|
|
16
|
-
emails are signed up on the spot), wires the beryl
|
|
16
|
+
emails are signed up on the spot), wires the beryl MCP server into the
|
|
17
17
|
agent you pick with --claude, --cursor, --vscode or --opencode (it asks when you pass
|
|
18
|
-
none), installs the beryl-test authoring skill
|
|
19
|
-
Safe to re-run.
|
|
18
|
+
none), and installs the beryl-test authoring skill. Safe to re-run.
|
|
20
19
|
|
|
21
20
|
## Install
|
|
22
21
|
|
|
@@ -193,7 +192,7 @@ Manage a project's environments: the URLs and auth Beryl runs tests against.
|
|
|
193
192
|
| `beryl envs list` | List a project's environments | `envs_list` |
|
|
194
193
|
| `beryl envs get <env-id>` | Show one environment | `envs_get` |
|
|
195
194
|
| `beryl envs create <name> <url>` | Add an environment (e.g. staging) to a project | `envs_create` |
|
|
196
|
-
| `beryl envs update <env-id>` | Update an environment's name, URL, auth settings, Site Health screen sizes, schedule, CI and pages, or issue tracker settings | `envs_update` |
|
|
195
|
+
| `beryl envs update <env-id>` | Update an environment's name, URL, app ids, auth settings, Site Health screen sizes, schedule, CI and pages, or issue tracker settings | `envs_update` |
|
|
197
196
|
| `beryl envs delete <env-id>` | Delete an environment | `envs_delete` |
|
|
198
197
|
|
|
199
198
|
### schedule
|
|
@@ -215,11 +214,9 @@ Author, inspect, version, and heal a project's tests: the checks Beryl runs on e
|
|
|
215
214
|
|
|
216
215
|
| Command | Summary | MCP tool |
|
|
217
216
|
| --- | --- | --- |
|
|
218
|
-
| `beryl tests lint` | Validate a plan JSON file offline, before sending it to the server | `tests_lint` |
|
|
219
217
|
| `beryl tests list` | List the project's tests with their latest result | `tests_list` |
|
|
220
218
|
| `beryl tests get <test-id>` | Show one test | `tests_get` |
|
|
221
219
|
| `beryl tests plan <test-id>` | Print a test's current step plan (JSON) | `tests_plan` |
|
|
222
|
-
| `beryl tests create` | Retired: tests are now created by the cloud test-creation job (`jobs create`) | `tests_create` |
|
|
223
220
|
| `beryl tests set-plan <test-id>` | Replace a test's step plan from a JSON file (creates a new version) | `tests_set_plan` |
|
|
224
221
|
| `beryl tests rename <test-id> <title>` | Rename a test | `tests_rename` |
|
|
225
222
|
| `beryl tests set-intent <test-id> <intent>` | Edit what a test proves, the target heal judges against | `tests_set_intent` |
|
|
@@ -237,7 +234,7 @@ Author, inspect, version, and heal a project's tests: the checks Beryl runs on e
|
|
|
237
234
|
| `beryl tests reset <test-id>` | Discard user edits and return the test to its latest system-authored version | `tests_reset` |
|
|
238
235
|
| `beryl tests heal <test-id> <state>` | Turn self-healing on or off for a test | `tests_heal` |
|
|
239
236
|
| `beryl tests history <test-id>` | Pass/fail history, streak, and stability for a test | `tests_history` |
|
|
240
|
-
| `beryl tests script
|
|
237
|
+
| `beryl tests script <test-id>` | Print the rendered Playwright spec for a test | `tests_script` |
|
|
241
238
|
| `beryl tests export <test-ids...>` | Export tests as Playwright .spec.ts files in a ZIP | `tests_export` |
|
|
242
239
|
|
|
243
240
|
### groups
|
|
@@ -262,7 +259,6 @@ Trigger a run of a project's tests (e.g. in CI), then watch, inspect, and downlo
|
|
|
262
259
|
| Command | Summary | MCP tool |
|
|
263
260
|
| --- | --- | --- |
|
|
264
261
|
| `beryl runs trigger` | Trigger a test run (whole suite, a subset, or one environment) | `runs_trigger` |
|
|
265
|
-
| `beryl runs local [test-ids...]` | Run tests on your machine with Playwright; results sync to Beryl | `runs_local` |
|
|
266
262
|
| `beryl runs list` | List recent runs | `runs_list` |
|
|
267
263
|
| `beryl runs get <run-id>` | Show one run with its per-test results | `runs_get` |
|
|
268
264
|
| `beryl runs watch <run-id>` | Follow a run until it finishes | `runs_watch` |
|
|
@@ -327,9 +323,9 @@ Manage the variables, secrets, and files the agent can use while exploring and r
|
|
|
327
323
|
| `beryl config secrets set <key> <value>` | Create a secret (write-only; re-setting a key replaces it) | `config_secrets_set` |
|
|
328
324
|
| `beryl config secrets get <key>` | Show one secret's metadata, or reveal its value with --reveal | `config_secrets_get` |
|
|
329
325
|
| `beryl config secrets delete <key>` | Delete a secret | `config_secrets_delete` |
|
|
330
|
-
| `beryl config files list` | List files
|
|
326
|
+
| `beryl config files list` | List uploaded files: fixtures tests attach and Android and iOS app builds | `config_files_list` |
|
|
331
327
|
| `beryl config files get <file>` | Show one uploaded file's metadata | `config_files_get` |
|
|
332
|
-
| `beryl config files upload <file>` | Upload a file | `config_files_upload` |
|
|
328
|
+
| `beryl config files upload <file>` | Upload a file: a fixture an upload step attaches, or an Android or iOS app build | `config_files_upload` |
|
|
333
329
|
| `beryl config files download <file-id>` | Get a short-lived download URL for a file | `config_files_download` |
|
|
334
330
|
| `beryl config files delete <file-id>` | Delete an uploaded file | `config_files_delete` |
|
|
335
331
|
|
package/dist/adapters/mcp.js
CHANGED
|
@@ -282,7 +282,7 @@ export function mcpInstructions() {
|
|
|
282
282
|
"magic-link flows are self-contained, with no human login needed. " +
|
|
283
283
|
"New tests are authored only by Beryl's cloud agent: give it a document describing " +
|
|
284
284
|
"the flows with `jobs_create`; there is no local authoring. " +
|
|
285
|
-
"The guide (plan shape, outcome assertions, accounts, email/OTP wiring, the
|
|
285
|
+
"The guide (plan shape, outcome assertions, accounts, email/OTP wiring, the " +
|
|
286
286
|
"run-fix loop) ships as both the beryl-test skill and the `guide` tool, same " +
|
|
287
287
|
`content. If a beryl-test skill stating v${version} is already loaded, do not ` +
|
|
288
288
|
"call `guide`; if no beryl-test skill is available or it states another version, " +
|
package/dist/beryl-test-skill.js
CHANGED
|
@@ -23,7 +23,7 @@ ActionPlan shape, outcome assertions, test accounts and the project mailbox, and
|
|
|
23
23
|
local run-fix loop.
|
|
24
24
|
|
|
25
25
|
Then use the Beryl MCP tools: \`projects_list\` to find the project, \`jobs_create\` to
|
|
26
|
-
have Beryl's cloud agent author tests from a document, and \`
|
|
26
|
+
have Beryl's cloud agent author tests from a document, and \`runs_trigger\` to run them.
|
|
27
27
|
|
|
28
28
|
The \`guide\` tool ships with the CLI and always matches the installed version, so treat
|
|
29
29
|
it as the source of truth over anything cached here.`;
|
|
@@ -90,7 +90,7 @@ export const BERYL_TEST_SKILL_OTP_EXAMPLE_PLAN = {
|
|
|
90
90
|
};
|
|
91
91
|
export const BERYL_TEST_SKILL = `---
|
|
92
92
|
name: beryl-test
|
|
93
|
-
description: Create, run, and fix durable, healable end-to-end tests for a web app with Beryl. Use when asking Beryl's cloud agent to create tests from a document (\`jobs_create\`), running
|
|
93
|
+
description: Create, run, and fix durable, healable end-to-end tests for a web app with Beryl. Use when asking Beryl's cloud agent to create tests from a document (\`jobs_create\`), running them in Beryl's cloud with \`runs_trigger\`, setting up test accounts, or fixing a banked test's plan with your own coding agent.
|
|
94
94
|
---
|
|
95
95
|
|
|
96
96
|
# Beryl tests
|
|
@@ -98,12 +98,12 @@ description: Create, run, and fix durable, healable end-to-end tests for a web a
|
|
|
98
98
|
beryl-test v${cliVersion()}, installed by \`beryl init\`; the \`guide\` tool always returns the current version of this guide.
|
|
99
99
|
|
|
100
100
|
A Beryl test drives a real browser through a flow and asserts the flow reached its
|
|
101
|
-
meaningful outcome. \`beryl init\` wired
|
|
102
|
-
\`accounts_list\`, \`
|
|
103
|
-
|
|
104
|
-
app's markup drifts, but only when a test has the three things that make
|
|
105
|
-
real **outcome assertion**, a strong **natural-language intent**, and
|
|
106
|
-
|
|
101
|
+
meaningful outcome. \`beryl init\` wired the **beryl** MCP server (\`jobs_create\`,
|
|
102
|
+
\`accounts_list\`, \`runs_trigger\`, and the rest). You do everything below by
|
|
103
|
+
calling those tools; tests are authored and run in Beryl's cloud. Beryl **heals**
|
|
104
|
+
tests as the app's markup drifts, but only when a test has the three things that make
|
|
105
|
+
it durable: a real **outcome assertion**, a strong **natural-language intent**, and a
|
|
106
|
+
**verified plan** (every save is proven by a replay).
|
|
107
107
|
|
|
108
108
|
## 0. Start here
|
|
109
109
|
|
|
@@ -114,16 +114,16 @@ authors one test per scenario and surface. \`jobs_get\` shows the job's status,
|
|
|
114
114
|
it produced and any scenario it skipped. The document is sent to the LLM that drives the
|
|
115
115
|
agent, so it must not hold anything the user would not share with it.
|
|
116
116
|
|
|
117
|
-
What you do
|
|
117
|
+
What you do here: set up the accounts tests sign in as (§1), trigger and read runs
|
|
118
118
|
(§4), and fix a banked test's plan when it needs it (§2).
|
|
119
119
|
|
|
120
|
-
Set up (once): if you cannot call the Beryl tools yet, run \`beryl init\` in a terminal
|
|
121
|
-
|
|
120
|
+
Set up (once): if you cannot call the Beryl tools yet, run \`beryl init\` in a terminal
|
|
121
|
+
to wire them. Then, before the first job:
|
|
122
122
|
|
|
123
123
|
- \`envs_list\`: the root URL gotos resolve against (§2). If it is empty, set it with
|
|
124
124
|
\`envs_update\` to a URL Beryl's cloud can reach, never \`localhost\`, \`127.0.0.1\`
|
|
125
|
-
or a private IP (refused).
|
|
126
|
-
|
|
125
|
+
or a private IP (refused). A local dev server is not testable until it is reachable
|
|
126
|
+
from the cloud (deploy a preview, or tunnel it).
|
|
127
127
|
- \`accounts_list\`: who do authenticated tests sign in as (§1)? Store its sign-in with
|
|
128
128
|
\`accounts_set_login\`, prove it with \`accounts_check\`.
|
|
129
129
|
- \`mailbox_get\`: the address the account receives mail at (§5).
|
|
@@ -133,10 +133,16 @@ login plan is stored.
|
|
|
133
133
|
|
|
134
134
|
Then:
|
|
135
135
|
|
|
136
|
-
1. **Create tests** with \`jobs_create\` (the document, and the environment to
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
1. **Create tests** with \`jobs_create\` (the document, and the environment to
|
|
137
|
+
explore). \`jobs_get\` shows progress, the tests it banked (each one already
|
|
138
|
+
proven by a clean replay), and every scenario it skipped, with why.
|
|
139
|
+
2. A job can pause as \`awaiting_input\` when the app's login wall blocks it (no
|
|
140
|
+
self-serve signup). Its \`needs\` field says what to do: capture a signed-in
|
|
141
|
+
session with the Beryl browser extension from the webapp. The job resumes by
|
|
142
|
+
itself and keeps everything it already banked.
|
|
143
|
+
3. When the job is done, run the new ids together ONCE with \`runs_trigger\` so they
|
|
144
|
+
land as one run with real first results.
|
|
145
|
+
4. **A test is red or wrong?** Fix its plan with the run-fix loop (§4).
|
|
140
146
|
|
|
141
147
|
## 1. First: who do your tests sign in as?
|
|
142
148
|
|
|
@@ -285,18 +291,18 @@ SSO-only sites are not a dead end: with a dedicated Google test account they fol
|
|
|
285
291
|
item 2 like any password account. The captured session is the FALLBACK — no test account
|
|
286
292
|
can be provided, or Google challenges the scripted sign-in: a human signs in once
|
|
287
293
|
through a live browser view in the Beryl webapp, Beryl stores the session encrypted
|
|
288
|
-
server-side and injects it into cloud runs
|
|
289
|
-
\`runs_local\` skips them (the session never leaves Beryl's cloud).
|
|
294
|
+
server-side and injects it into cloud runs; the session never leaves Beryl's cloud.
|
|
290
295
|
|
|
291
|
-
## 2. Fixing a test's plan
|
|
296
|
+
## 2. Fixing a test's plan
|
|
292
297
|
|
|
293
298
|
New tests come from \`jobs_create\` (§0). You write plan JSON only to fix a banked test
|
|
294
299
|
(\`tests_plan\` returns its current plan) or to store an account's sign-in or signup
|
|
295
300
|
plan (§1). Either way, the rules below hold.
|
|
296
301
|
|
|
297
|
-
1. **
|
|
298
|
-
|
|
299
|
-
|
|
302
|
+
1. **Start from evidence, not memory.** Pull the failed run first: \`runs_get\` for
|
|
303
|
+
the failing step and error, \`runs_explain\` for the run's own diagnosis,
|
|
304
|
+
\`tests_events\` for how the test behaved over time. Fix what the evidence shows,
|
|
305
|
+
never what you assume the page looks like.
|
|
300
306
|
2. **Write it as an ActionPlan** — a JSON object whose \`steps\` are
|
|
301
307
|
\`{action, selector, url, value, ...}\`. Two structural rules:
|
|
302
308
|
- the **first executed step is a \`goto\`**, and
|
|
@@ -354,10 +360,10 @@ ${JSON.stringify(BERYL_TEST_SKILL_EXAMPLE_PLAN, null, 2)
|
|
|
354
360
|
.map((line) => ` ${line}`)
|
|
355
361
|
.join("\n")}
|
|
356
362
|
\`\`\`
|
|
357
|
-
3. **
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
363
|
+
3. **Save through the replay gate.** \`tests_recompile\` replays the edited plan
|
|
364
|
+
against the live site first and rejects it on a red replay; prefer it always.
|
|
365
|
+
\`tests_set_plan\` saves with no replay (the next run proves it); reserve it for
|
|
366
|
+
when the flow cannot replay right now (for example a third-party step). Pass a new
|
|
361
367
|
\`description\` only when the edit changes what the test proves (§3).
|
|
362
368
|
Full ActionPlan JSON Schema:
|
|
363
369
|
https://api.beryl.so/api/v1/schemas/action-plan.schema.json.
|
|
@@ -369,9 +375,32 @@ full Chromium device emulation, not a resize: the viewport, a mobile Chrome user
|
|
|
369
375
|
touch and the device pixel ratio all change together, so a hamburger or a nav that only
|
|
370
376
|
exists on small screens actually renders and is testable. \`mobile\` emulates a Pixel-class
|
|
371
377
|
phone, \`tablet\` a Galaxy-class tablet; both keep a Chrome user-agent so the emulated UA
|
|
372
|
-
matches the engine.
|
|
373
|
-
|
|
374
|
-
|
|
378
|
+
matches the engine. A test's surface is fixed when it is created, so when fixing a plan
|
|
379
|
+
keep \`device\` as it is: a small-screen test's selectors must come from what the
|
|
380
|
+
small-screen layout renders (a hamburger, not the desktop nav). The account, session and
|
|
381
|
+
teardown rules are unchanged; \`device\` only sets the frame it runs in.
|
|
382
|
+
|
|
383
|
+
### App tests (Android and iOS)
|
|
384
|
+
\`device: "android"\` and \`device: "ios"\` are not browsers: the plan drives the environment's
|
|
385
|
+
app (its \`app_ids.android\` from the newest APK, or \`app_ids.ios\` from the newest simulator
|
|
386
|
+
build, uploaded to Files) on an emulator or simulator in Beryl's cloud. App tests are written
|
|
387
|
+
and verified by a test-creation job (\`jobs_create\`), never run locally. When you edit one,
|
|
388
|
+
its rules differ from a web plan's:
|
|
389
|
+
|
|
390
|
+
- The first step is \`launch\` (open the app), not \`goto\`. A later \`launch\` relaunches
|
|
391
|
+
with the app's data kept, which is how a plan proves something persisted.
|
|
392
|
+
- Steps: launch, back (Android only; iOS has no back button), fill, click, press, scroll,
|
|
393
|
+
wait_for, expect, await_email.
|
|
394
|
+
- Selectors are \`id=<resource id>\` (Android) or \`id=<accessibility identifier>\` (iOS), or
|
|
395
|
+
\`text=<visible text or accessibility label>\`; no CSS. React Native's \`testID\` shows up as
|
|
396
|
+
the id on both; a Flutter icon button needs a \`Semantics\` label to have text.
|
|
397
|
+
- \`have_text\` needs an \`id=\` selector; to check text is on screen, use \`visible\` with
|
|
398
|
+
\`text=\`. No URL or title expects: an app has no URL.
|
|
399
|
+
- Keys: Enter and Backspace, plus Tab on Android. Never put \`\${\` in plan text.
|
|
400
|
+
- A signed-in app test sets \`requires_auth\` and \`auth_mode: "session"\` and rides the
|
|
401
|
+
account's sign-in on that surface. The test-creation job saves that sign-in itself when
|
|
402
|
+
the app has a login screen; \`accounts_set_login\` with \`surface: "android"\` or
|
|
403
|
+
\`surface: "ios"\` only replaces it.
|
|
375
404
|
|
|
376
405
|
### The outcome assertion is the whole game
|
|
377
406
|
|
|
@@ -396,6 +425,13 @@ observable proof the flow worked.
|
|
|
396
425
|
chrome. Target the element that carries the text (the \`h1\`, the toast), assert the
|
|
397
426
|
page with \`have_title\` (substring), or check "this string is visible somewhere"
|
|
398
427
|
with \`expect_kind: "visible"\` + a \`text=…\` selector.
|
|
428
|
+
- **For output that varies run to run** (a chatbot or AI assistant reply), assert its
|
|
429
|
+
meaning with \`expect_kind: "judged"\`: \`selector\` locates the reply (the LAST match
|
|
430
|
+
is judged, so a selector matching every assistant message reads the newest one) and
|
|
431
|
+
\`expect_text\` states the criteria in plain words ("Lists at least two decisions and
|
|
432
|
+
names the people involved; no error or refusal"). Beryl waits up to 120s for the text
|
|
433
|
+
to appear and stop changing, then a model passes or fails it and the failure carries
|
|
434
|
+
the model's reason. Write criteria a correct answer always meets, never exact wording.
|
|
399
435
|
- **If you can't name a success signal, the flow is not test-worthy.** Don't bank a
|
|
400
436
|
test that verifies nothing; explore a different flow.
|
|
401
437
|
- **Don't work around a real app failure to make a test go green.** A genuinely broken
|
|
@@ -405,13 +441,12 @@ observable proof the flow worked.
|
|
|
405
441
|
|
|
406
442
|
Every one of these has produced a wrong plan or a red run. Check them.
|
|
407
443
|
|
|
408
|
-
1. **
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
the login.
|
|
444
|
+
1. **A gated page can look public in signed-in evidence.** Run artifacts captured
|
|
445
|
+
while a session was live never show the login wall, so do not mark a flow public
|
|
446
|
+
(no \`requires_auth\`) from them; check how the page behaves logged out.
|
|
412
447
|
2. **The sign-UP flow is not the sign-IN flow.** A brand-new address often gets an
|
|
413
448
|
extra "create your account" step a returning address skips. A stored login plan must
|
|
414
|
-
be the **returning** path
|
|
449
|
+
be the **returning** path: what the app shows the SECOND time that account arrives.
|
|
415
450
|
3. **Read the DOM, not just the accessibility tree, before picking a selector.** Two
|
|
416
451
|
buttons can share a label ("Continue" / "Continue with Google"), and Beryl relaxes
|
|
417
452
|
\`text=X\` to a case-insensitive SUBSTRING — \`text=Continue\` is ambiguous. Use
|
|
@@ -453,19 +488,18 @@ Good:
|
|
|
453
488
|
Weak (describes steps + asserts nothing meaningful):
|
|
454
489
|
> "Goes to /login, fills the form, and checks the page loaded."
|
|
455
490
|
|
|
456
|
-
## 4. The
|
|
491
|
+
## 4. The run-fix loop
|
|
457
492
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
no id to run the whole suite and record the results in Beryl.
|
|
493
|
+
Every run happens in Beryl's cloud. \`runs_trigger\` against one test id (or a list)
|
|
494
|
+
starts a run; \`runs_watch\` follows it live; \`runs_get\` returns each result's
|
|
495
|
+
failing step, error and artifacts; \`runs_explain\` turns a red run into a diagnosis.
|
|
462
496
|
|
|
463
|
-
The loop: \`
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
\`runs_trigger\`
|
|
467
|
-
|
|
468
|
-
|
|
497
|
+
The loop: \`runs_trigger\` against the test id, read the failure with \`runs_get\` /
|
|
498
|
+
\`runs_explain\`, fix the plan (§2), save with \`tests_recompile\` (which already
|
|
499
|
+
replays it against the live site, so a green recompile IS the fix proven), then one
|
|
500
|
+
final \`runs_trigger\` so the suite records a clean run. If recompile keeps rejecting
|
|
501
|
+
an edit, the evidence disagrees with your theory: pull more of it rather than trying
|
|
502
|
+
variations.
|
|
469
503
|
|
|
470
504
|
## 5. Testing an OTP / signup flow (\`await_email\`)
|
|
471
505
|
|
|
@@ -521,9 +555,8 @@ For a magic-link flow, replace the code steps with
|
|
|
521
555
|
|
|
522
556
|
Two caveats:
|
|
523
557
|
|
|
524
|
-
-
|
|
525
|
-
|
|
526
|
-
end to end. It signs up / sends mail FOR REAL, like every run.
|
|
558
|
+
- Every run serves \`await_email\` from the project mailbox. It signs up / sends
|
|
559
|
+
mail FOR REAL, like every run.
|
|
527
560
|
- §2's outcome discipline still applies: the green signal is the post-verification
|
|
528
561
|
state (the welcome screen, the dashboard), not "an email arrived".
|
|
529
562
|
|
|
@@ -562,14 +595,19 @@ Two shapes:
|
|
|
562
595
|
\`third_party_integrations_target\` names, trigger the app's message, then
|
|
563
596
|
\`{ "action": "await_third_party", "provider": "slack", "expect_text": "..." }\`.
|
|
564
597
|
- **Integration intact**: no declaration. Trigger the app's message, then
|
|
565
|
-
\`await_third_party\`.
|
|
598
|
+
\`await_third_party\`. When the app only answers its bot, ping the bot instead:
|
|
599
|
+
\`{ "action": "send_third_party", "provider": "slack", "value": "hello" }\` mentions it in
|
|
600
|
+
the project's channel, then \`await_third_party\` waits for its reply (in the channel or
|
|
601
|
+
in the thread). The bot's app must be recorded for the environment first:
|
|
602
|
+
\`third_party_integrations_target\` with \`env\` and \`app\`. A reply only the sender can
|
|
603
|
+
see never reaches Beryl, so such a bot can't be tested this way.
|
|
566
604
|
|
|
567
605
|
Rules:
|
|
568
606
|
|
|
569
607
|
- Always tear down what the test connected in \`after\`: disconnect the integration in
|
|
570
608
|
the app.
|
|
571
|
-
- These steps
|
|
572
|
-
|
|
609
|
+
- These steps need Beryl's provider accounts, which live in Beryl's cloud; they run
|
|
610
|
+
like any other cloud test.
|
|
573
611
|
- A setup error saying Beryl's access "is being restored" is on Beryl's side: do not
|
|
574
612
|
edit the test.
|
|
575
613
|
- Never add steps that browse Slack itself; the actions above do all of it.
|
|
@@ -253,6 +253,10 @@ export const testAccountCommands = [
|
|
|
253
253
|
"green.\n\n" +
|
|
254
254
|
"A captured account (from the Beryl Chrome extension) takes --probe alone: it rides " +
|
|
255
255
|
"the captured session and cannot sign itself in, so there is no --file.\n\n" +
|
|
256
|
+
"--surface android stores the sign-in Android tests use instead, beside the web one; " +
|
|
257
|
+
"Beryl proves it on an Android worker the first time a signed-in android test runs. " +
|
|
258
|
+
"A test-creation job saves one itself when it meets the app's login screen, so this " +
|
|
259
|
+
"is for fixing or replacing it.\n\n" +
|
|
256
260
|
"Storing only stores. Run `beryl accounts check` to prove it against the live app.",
|
|
257
261
|
scope: "project",
|
|
258
262
|
args: [
|
|
@@ -278,11 +282,20 @@ export const testAccountCommands = [
|
|
|
278
282
|
description: "The login_plan_hash from `beryl accounts get-login`. The write is rejected if " +
|
|
279
283
|
"the stored plan changed since. Omit only when writing the first plan",
|
|
280
284
|
},
|
|
285
|
+
{
|
|
286
|
+
name: "surface",
|
|
287
|
+
type: "string",
|
|
288
|
+
enum: ["android", "ios"],
|
|
289
|
+
description: "An app surface: store (or read) the sign-in through the app's own screens. Both " +
|
|
290
|
+
"plans are plans for that device (device android or ios, starting with a launch); " +
|
|
291
|
+
"a signed-in app test restores the session this sign-in saved on the device",
|
|
292
|
+
},
|
|
281
293
|
],
|
|
282
294
|
examples: [
|
|
283
295
|
"beryl accounts set-login acc_123 --file signin.json --probe probe.json",
|
|
284
296
|
"beryl accounts set-login acc_123 --file signin.json --probe probe.json --base-hash 9f2c…",
|
|
285
297
|
"beryl accounts set-login acc_456 --probe probe.json # captured account",
|
|
298
|
+
"beryl accounts set-login acc_123 --surface android --file app-signin.json --probe app-probe.json",
|
|
286
299
|
],
|
|
287
300
|
async run(ctx, input) {
|
|
288
301
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
@@ -291,11 +304,15 @@ export const testAccountCommands = [
|
|
|
291
304
|
plan: flagStr(input, "file") ? readJsonFlag(input, "file") : null,
|
|
292
305
|
probe: readJsonFlag(input, "probe"),
|
|
293
306
|
base_hash: flagStr(input, "base-hash") ?? null,
|
|
307
|
+
...(flagStr(input, "surface") ? { surface: flagStr(input, "surface") } : {}),
|
|
294
308
|
}));
|
|
309
|
+
const surface = flagStr(input, "surface");
|
|
295
310
|
return {
|
|
296
311
|
data: account,
|
|
297
312
|
human: `${green("Stored")} — ${account.email}\n` +
|
|
298
|
-
dim(
|
|
313
|
+
dim(surface
|
|
314
|
+
? `Beryl proves it the first time a signed-in ${surface} test runs.`
|
|
315
|
+
: "Now run `beryl accounts check " + account.id + "` to prove it works."),
|
|
299
316
|
};
|
|
300
317
|
}
|
|
301
318
|
catch (err) {
|
|
@@ -314,9 +331,21 @@ export const testAccountCommands = [
|
|
|
314
331
|
"--base-hash on the next `accounts set-login`.",
|
|
315
332
|
scope: "project",
|
|
316
333
|
args: [{ name: "account-id", description: "Account id", required: true }],
|
|
334
|
+
flags: [
|
|
335
|
+
{
|
|
336
|
+
name: "surface",
|
|
337
|
+
type: "string",
|
|
338
|
+
enum: ["android", "ios"],
|
|
339
|
+
description: "An app surface: store (or read) the sign-in through the app's own screens. Both " +
|
|
340
|
+
"plans are plans for that device (device android or ios, starting with a launch); " +
|
|
341
|
+
"a signed-in app test restores the session this sign-in saved on the device",
|
|
342
|
+
},
|
|
343
|
+
],
|
|
317
344
|
async run(ctx, input) {
|
|
318
345
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
319
|
-
const
|
|
346
|
+
const surface = flagStr(input, "surface");
|
|
347
|
+
const login = (await ctx.client.get(`${accountsPath(workspaceId, projectId)}/${arg(input, "account-id")}/login` +
|
|
348
|
+
(surface ? `?surface=${encodeURIComponent(surface)}` : "")));
|
|
320
349
|
if (!login.plan && !login.probe)
|
|
321
350
|
return {
|
|
322
351
|
data: login,
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { UsageError } from "../errors.js";
|
|
3
|
+
import { CliError, UsageError } from "../errors.js";
|
|
4
|
+
import { ApiError } from "../http.js";
|
|
5
|
+
import { dim, green, table, timeAgo } from "../output.js";
|
|
4
6
|
import { arg, flagBool, flagStr, projectPath } from "./util.js";
|
|
7
|
+
const APK_RE = /\.apk$/i;
|
|
8
|
+
const BUILD_KINDS = new Set(["android_build", "ios_build"]);
|
|
9
|
+
const buildLabel = (f) => `${f.app_id ?? "?"} ${f.app_version ?? "?"} (${f.build_number ?? "?"})`;
|
|
5
10
|
const configPath = (ws, p) => `${projectPath(ws, p)}/config`;
|
|
6
11
|
function findByKey(rows, value, kind) {
|
|
7
12
|
const byId = rows.find((r) => r.id === value);
|
|
@@ -165,11 +170,20 @@ export const configCommands = [
|
|
|
165
170
|
},
|
|
166
171
|
{
|
|
167
172
|
name: "config files list",
|
|
168
|
-
summary: "List files
|
|
173
|
+
summary: "List uploaded files: fixtures tests attach and Android and iOS app builds",
|
|
169
174
|
scope: "project",
|
|
170
175
|
async run(ctx, input) {
|
|
171
176
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
172
|
-
|
|
177
|
+
const rows = (await ctx.client.get(`${configPath(workspaceId, projectId)}/files`));
|
|
178
|
+
const human = table(rows.map((f) => ({
|
|
179
|
+
name: f.name,
|
|
180
|
+
kind: f.kind ?? "fixture",
|
|
181
|
+
app: BUILD_KINDS.has(String(f.kind)) ? buildLabel(f) : "",
|
|
182
|
+
env: f.environment_id ?? "",
|
|
183
|
+
created: timeAgo(f.created_at),
|
|
184
|
+
id: f.id,
|
|
185
|
+
})));
|
|
186
|
+
return { data: rows, human };
|
|
173
187
|
},
|
|
174
188
|
},
|
|
175
189
|
{
|
|
@@ -195,32 +209,89 @@ export const configCommands = [
|
|
|
195
209
|
},
|
|
196
210
|
{
|
|
197
211
|
name: "config files upload",
|
|
198
|
-
summary: "Upload a file",
|
|
212
|
+
summary: "Upload a file: a fixture an upload step attaches, or an Android or iOS app build",
|
|
213
|
+
description: "The bytes go straight to storage over a short-lived URL, never through the API, up to " +
|
|
214
|
+
"1 GB. An .apk uploads as an Android build (or pass --kind android_build): Beryl reads " +
|
|
215
|
+
"its package name, version and native ABIs, refuses one the emulators cannot run (an " +
|
|
216
|
+
".aab, a split APK, a too-new minSdk, ARM-only native code on x86 emulators). An iOS " +
|
|
217
|
+
"simulator build (the .app folder zipped, or EAS's .tar.gz) uploads with --kind " +
|
|
218
|
+
"ios_build: Beryl reads its bundle id and version and refuses an .ipa, a device build " +
|
|
219
|
+
"or one with no arm64 slice. App tests of an environment whose app id for that surface " +
|
|
220
|
+
"matches install the newest build. Beryl keeps the newest 5 builds per app per " +
|
|
221
|
+
"environment. Upload after each build, e.g. in CI.",
|
|
199
222
|
scope: "project",
|
|
200
223
|
args: [{ name: "file", description: "Path to the local file", required: true }],
|
|
201
|
-
flags: [
|
|
224
|
+
flags: [
|
|
225
|
+
{ name: "env", type: "string", description: "Scope to one environment id" },
|
|
226
|
+
{
|
|
227
|
+
name: "kind",
|
|
228
|
+
type: "string",
|
|
229
|
+
enum: ["fixture", "android_build", "ios_build"],
|
|
230
|
+
description: "fixture: a file tests attach. android_build: an Android APK (the default for .apk). " +
|
|
231
|
+
"ios_build: a zipped or tarred iOS simulator .app",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
examples: [
|
|
235
|
+
"beryl config files upload ./fixtures/avatar.png",
|
|
236
|
+
"beryl config files upload ./android/app/build/outputs/apk/release/app-release.apk",
|
|
237
|
+
"beryl config files upload ./MyApp.zip --kind ios_build",
|
|
238
|
+
],
|
|
202
239
|
async run(ctx, input) {
|
|
203
240
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
204
241
|
const file = arg(input, "file");
|
|
205
|
-
|
|
242
|
+
const explicitKind = flagStr(input, "kind");
|
|
243
|
+
const kind = explicitKind ?? (APK_RE.test(file) ? "android_build" : "fixture");
|
|
244
|
+
const envId = flagStr(input, "env");
|
|
245
|
+
let blob;
|
|
206
246
|
try {
|
|
207
|
-
|
|
247
|
+
if (!fs.statSync(file).isFile())
|
|
248
|
+
throw new UsageError(`not a file: ${file}`);
|
|
249
|
+
blob = await fs.openAsBlob(file);
|
|
208
250
|
}
|
|
209
251
|
catch (err) {
|
|
252
|
+
if (err instanceof UsageError)
|
|
253
|
+
throw err;
|
|
210
254
|
const code = err.code;
|
|
211
255
|
if (code === "ENOENT")
|
|
212
256
|
throw new UsageError(`file not found: ${file}`);
|
|
213
257
|
throw new UsageError(`cannot read ${file} (${code ?? err.message})`);
|
|
214
258
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
259
|
+
if (blob.size === 0)
|
|
260
|
+
throw new UsageError(`${file} is empty`);
|
|
261
|
+
const base = `${configPath(workspaceId, projectId)}/files`;
|
|
262
|
+
const upload = (await ctx.client.post(`${base}/uploads`, {
|
|
263
|
+
name: path.basename(file),
|
|
264
|
+
size: blob.size,
|
|
265
|
+
kind,
|
|
266
|
+
...(envId ? { environment_id: envId } : {}),
|
|
267
|
+
}));
|
|
268
|
+
let put;
|
|
269
|
+
try {
|
|
270
|
+
put = await fetch(upload.upload_url, {
|
|
271
|
+
method: upload.method,
|
|
272
|
+
headers: upload.headers,
|
|
273
|
+
body: blob,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
throw new CliError(`Upload of ${file} failed: ${err.message}`);
|
|
278
|
+
}
|
|
279
|
+
if (!put.ok)
|
|
280
|
+
throw new CliError(`Upload of ${file} failed: storage answered HTTP ${put.status}`);
|
|
281
|
+
let done;
|
|
282
|
+
try {
|
|
283
|
+
done = (await ctx.client.post(`${base}/uploads/${upload.file_id}/complete`));
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
if (err instanceof ApiError && err.status === 422 && typeof err.detail === "string")
|
|
287
|
+
throw new CliError(`Build refused: ${err.detail}`);
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
220
290
|
return {
|
|
221
|
-
data:
|
|
222
|
-
|
|
223
|
-
|
|
291
|
+
data: done,
|
|
292
|
+
human: BUILD_KINDS.has(String(done.kind))
|
|
293
|
+
? `${green("Build uploaded")}: ${buildLabel(done)} ${dim(String(done.id))}`
|
|
294
|
+
: `${green("Uploaded")}: ${String(done.name)} ${dim(String(done.id))}`,
|
|
224
295
|
};
|
|
225
296
|
},
|
|
226
297
|
},
|
|
@@ -72,12 +72,17 @@ export const environmentCommands = [
|
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
name: "envs update",
|
|
75
|
-
summary: "Update an environment's name, URL, auth settings, Site Health screen sizes, schedule, CI and pages, or issue tracker settings",
|
|
75
|
+
summary: "Update an environment's name, URL, app ids, auth settings, Site Health screen sizes, schedule, CI and pages, or issue tracker settings",
|
|
76
76
|
scope: "project",
|
|
77
77
|
args: [{ name: "env-id", description: "Environment id", required: true }],
|
|
78
78
|
flags: [
|
|
79
79
|
{ name: "name", type: "string", description: "New name" },
|
|
80
80
|
{ name: "url", type: "string", description: "New root URL" },
|
|
81
|
+
{
|
|
82
|
+
name: "app-id",
|
|
83
|
+
type: "strings",
|
|
84
|
+
description: 'App id for an app surface, as surface=id (android=com.acme.app, ios=com.acme.App), repeatable: that surface\'s tests install the newest uploaded build of it; "ios=" clears it',
|
|
85
|
+
},
|
|
81
86
|
{ name: "auth", type: "string", enum: ["public", "gated"], description: "New auth choice" },
|
|
82
87
|
{
|
|
83
88
|
name: "viewport",
|
|
@@ -91,6 +96,7 @@ export const environmentCommands = [
|
|
|
91
96
|
],
|
|
92
97
|
examples: [
|
|
93
98
|
"beryl envs update <env-id> --tracker linear --tracker-team <team-id>",
|
|
99
|
+
"beryl envs update <env-id> --app-id android=com.acme.app --app-id ios=com.acme.App",
|
|
94
100
|
"beryl envs update <env-id> --tracker-threshold medium --tracker-max-tickets 5",
|
|
95
101
|
"beryl envs update <env-id> --tracker-priority high=urgent --tracker-pause on",
|
|
96
102
|
"beryl envs update <env-id> --health-schedule daily --health-time 06:00 --health-timezone Europe/Berlin",
|
|
@@ -104,6 +110,17 @@ export const environmentCommands = [
|
|
|
104
110
|
body.name = flagStr(input, "name");
|
|
105
111
|
if (flagStr(input, "url"))
|
|
106
112
|
body.root_url = flagStr(input, "url");
|
|
113
|
+
const appIds = flagStrings(input, "app-id");
|
|
114
|
+
if (appIds) {
|
|
115
|
+
const parsed = {};
|
|
116
|
+
for (const pair of appIds) {
|
|
117
|
+
const [surface, ...rest] = pair.split("=");
|
|
118
|
+
if (!surface || rest.length === 0)
|
|
119
|
+
throw new UsageError(`--app-id must be surface=id (android=com.acme.app), got ${pair}`);
|
|
120
|
+
parsed[surface] = rest.join("=");
|
|
121
|
+
}
|
|
122
|
+
body.app_ids = parsed;
|
|
123
|
+
}
|
|
107
124
|
if (flagStr(input, "auth"))
|
|
108
125
|
body.requires_auth_choice = flagStr(input, "auth");
|
|
109
126
|
const viewports = flagStrings(input, "viewport");
|