@beryl-so/cli 0.50.0 → 0.52.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/dist/beryl-test-skill.js +99 -103
- package/dist/commands/init.js +45 -8
- package/dist/schema.generated.js +15 -0
- package/package.json +1 -1
package/dist/beryl-test-skill.js
CHANGED
|
@@ -48,7 +48,7 @@ export const BERYL_COPILOT_SECTION = `${BERYL_COPILOT_MARK_BEGIN}
|
|
|
48
48
|
${BERYL_GUIDE_POINTER}
|
|
49
49
|
${BERYL_COPILOT_MARK_END}`;
|
|
50
50
|
// The example plan the skill shows verbatim. Exported so the test suite lints it with
|
|
51
|
-
// lintPlan
|
|
51
|
+
// lintPlan; the skill's documented shape must pass `tests_lint` on the first try.
|
|
52
52
|
export const BERYL_TEST_SKILL_EXAMPLE_PLAN = {
|
|
53
53
|
steps: [
|
|
54
54
|
{ action: "goto", url: "/pricing" },
|
|
@@ -63,7 +63,7 @@ export const BERYL_TEST_SKILL_EXAMPLE_PLAN = {
|
|
|
63
63
|
};
|
|
64
64
|
// The OTP/signup example the skill shows verbatim (§5). Exported so the test suite lints
|
|
65
65
|
// it with lintPlan, same as the minimal example — the documented await_email shape must
|
|
66
|
-
// pass `
|
|
66
|
+
// pass `tests_lint` on the first try.
|
|
67
67
|
export const BERYL_TEST_SKILL_OTP_EXAMPLE_PLAN = {
|
|
68
68
|
steps: [
|
|
69
69
|
{ action: "goto", url: "/signup" },
|
|
@@ -89,7 +89,7 @@ export const BERYL_TEST_SKILL_OTP_EXAMPLE_PLAN = {
|
|
|
89
89
|
};
|
|
90
90
|
export const BERYL_TEST_SKILL = `---
|
|
91
91
|
name: beryl-test
|
|
92
|
-
description: Author durable, healable end-to-end tests for a web app with Beryl. Use when writing, running, or fixing a Beryl test locally with your own coding agent
|
|
92
|
+
description: Author durable, healable end-to-end tests for a web app with Beryl. Use when writing, running, or fixing a Beryl test locally with your own coding agent, drafting the plan over the Playwright MCP, writing the natural-language intent, and running it with \`runs_local\`.
|
|
93
93
|
---
|
|
94
94
|
|
|
95
95
|
# Authoring Beryl tests
|
|
@@ -97,52 +97,50 @@ description: Author durable, healable end-to-end tests for a web app with Beryl.
|
|
|
97
97
|
beryl-test v${cliVersion()} — installed by \`beryl init\`; the \`guide\` tool always returns the current version of this guide.
|
|
98
98
|
|
|
99
99
|
A Beryl test drives a real browser through a flow and asserts the flow reached its
|
|
100
|
-
meaningful outcome. \`beryl init\` wired two MCP servers: **beryl** (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
meaningful outcome. \`beryl init\` wired two MCP servers: **beryl** (the tools you author
|
|
101
|
+
with: \`accounts_list\`, \`tests_create\`, \`runs_local\`, and the rest) and **playwright**
|
|
102
|
+
(drive a real browser). You do everything below by calling those tools. Beryl **heals**
|
|
103
|
+
tests as the app's markup drifts, but only when you supply the three things that make a
|
|
104
|
+
test durable: a real **outcome assertion**, a strong **natural-language intent**, and the
|
|
105
|
+
**local run-fix loop**.
|
|
105
106
|
|
|
106
107
|
## 0. Start here
|
|
107
108
|
|
|
108
|
-
**Everything happens on your machine first
|
|
109
|
-
\`
|
|
110
|
-
|
|
111
|
-
exception are in \`
|
|
109
|
+
**Everything happens on your machine first**: you drive the flow here, and
|
|
110
|
+
\`tests_create\` proves the plan here too, so a working local browser IS the loop.
|
|
111
|
+
Keep \`tests_create\`'s verify on (banking unproven is a trap); replay mechanics and the
|
|
112
|
+
cloud-session exception are in the \`tests_create\` tool's own description.
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
Set up (once): if you cannot call the Beryl tools yet, run \`beryl init\` in a terminal to
|
|
115
|
+
install the browser and wire the tools. Then, before the first plan:
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
beryl accounts set-login <id> --file … --probe … # store its sign-in — required (§1)
|
|
121
|
-
beryl accounts check <id> # does that stored sign-in still work? (§1)
|
|
122
|
-
beryl mailbox get # the address they receive mail at (§5)
|
|
123
|
-
\`\`\`
|
|
117
|
+
- \`envs_list\`: the root URL gotos resolve against (§2). If it is empty, set it with
|
|
118
|
+
\`envs_update\`.
|
|
119
|
+
- \`accounts_list\`: who do authenticated tests sign in as (§1)? Store its sign-in with
|
|
120
|
+
\`accounts_set_login\`, prove it with \`accounts_check\`.
|
|
121
|
+
- \`mailbox_get\`: the address the account receives mail at (§5).
|
|
124
122
|
|
|
125
|
-
\`
|
|
123
|
+
\`accounts_list\` comes FIRST: standing account vs new identity changes the plan you
|
|
126
124
|
write (discovering it afterwards means rewriting), and an authenticated test cannot be
|
|
127
125
|
created until its account's login plan is stored.
|
|
128
126
|
|
|
129
127
|
Then, per test:
|
|
130
128
|
|
|
131
|
-
1. **Drive the flow** in a real browser over the Playwright MCP
|
|
132
|
-
imagination (§2).
|
|
129
|
+
1. **Drive the flow** in a real browser over the Playwright MCP, never from imagination (§2).
|
|
133
130
|
2. **Write the ActionPlan**, with one real outcome assertion (§2).
|
|
134
|
-
3.
|
|
135
|
-
|
|
136
|
-
4. \`
|
|
137
|
-
|
|
138
|
-
replay hands back the failure
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
3. **Lint it** with \`tests_lint\` (the plan plus its \`read\`/\`write\` access): schema and
|
|
132
|
+
access check, offline.
|
|
133
|
+
4. **Bank it** with \`tests_create\` (the title, the plan, its \`access\`, and the
|
|
134
|
+
\`description\` intent): it replays the plan locally, banks only on green, records no
|
|
135
|
+
run; a red replay hands back the failure to fix and re-run (§3 for the intent, §4 for
|
|
136
|
+
the loop).
|
|
137
|
+
5. When the batch is banked, run the new ids together ONCE with \`runs_local\` (or
|
|
138
|
+
\`runs_trigger\` for the cloud) so they land as one run with real first results.
|
|
141
139
|
|
|
142
140
|
## 1. First: who do your tests sign in as?
|
|
143
141
|
|
|
144
142
|
An authenticated test **signs in as an account that already exists** — it does not sign
|
|
145
|
-
one up. Start every authoring session with \`
|
|
143
|
+
one up. Start every authoring session with \`accounts_list\`:
|
|
146
144
|
|
|
147
145
|
\`\`\`
|
|
148
146
|
label email type login status
|
|
@@ -161,9 +159,9 @@ one plan stays correct across environments.
|
|
|
161
159
|
|
|
162
160
|
**The account needs a stored login plan before any authenticated test can be created.**
|
|
163
161
|
A run replays that plan to produce the session its tests ride, and replays it again
|
|
164
|
-
when the session expires
|
|
162
|
+
when the session expires, so \`tests_create\` rejects a \`requires_auth\` plan whose
|
|
165
163
|
account has none, naming the fix. Store it once per account with
|
|
166
|
-
\`
|
|
164
|
+
\`accounts_set_login\` (§ Session mode).
|
|
167
165
|
|
|
168
166
|
**NEVER paste a real email or password into a plan.** Handles resolve at run time —
|
|
169
167
|
the password never lands in the rendered spec and is scrubbed from artifacts. A pasted
|
|
@@ -193,8 +191,8 @@ The trade is drift — run 200 has 200 of everything run 1 created. So:
|
|
|
193
191
|
was. Capture the original BEFORE you change it with \`capture_text\` (or \`capture_value\`
|
|
194
192
|
for a form input), banking it under \`capture_as\`, then \`fill {{handle}}\` in \`after\`
|
|
195
193
|
to put it back.
|
|
196
|
-
- **You label each test \`read\` or \`write\`; Beryl takes your word for it.** Set
|
|
197
|
-
|
|
194
|
+
- **You label each test \`read\` or \`write\`; Beryl takes your word for it.** Set its
|
|
195
|
+
\`access\` (default \`read\`). Beryl does NOT infer it from your selectors, so YOU
|
|
198
196
|
decide, by one test: **would the app be different after this test runs?**
|
|
199
197
|
- **\`write\`** = it leaves a change behind: it creates, edits, deletes, renames, moves,
|
|
200
198
|
uploads or toggles a record or setting that is still different once the test ends. A
|
|
@@ -208,35 +206,33 @@ The trade is drift — run 200 has 200 of everything run 1 created. So:
|
|
|
208
206
|
A flow that both changes and reads is a \`write\`. Only the \`write\` label is checked
|
|
209
207
|
(its closing teardown proof, and a \`write\` whose teardown removes nothing fails the
|
|
210
208
|
run); \`read\` is taken at your word, so a real write you mislabel \`read\` silently
|
|
211
|
-
drifts the account. Label deliberately, and flip a banked test with
|
|
212
|
-
\`beryl tests set-access <id> read|write\`.
|
|
209
|
+
drifts the account. Label deliberately, and flip a banked test with \`tests_set_access\`.
|
|
213
210
|
|
|
214
211
|
### If there is no account yet
|
|
215
212
|
|
|
216
|
-
An empty \`
|
|
213
|
+
An empty \`accounts_list\` means nothing is set up. In order of preference:
|
|
217
214
|
|
|
218
|
-
1. **The user has a dedicated test account** → bank it
|
|
219
|
-
|
|
220
|
-
account only — never a real user's.
|
|
215
|
+
1. **The user has a dedicated test account** → bank it with \`accounts_create\` (email +
|
|
216
|
+
password). A DEDICATED test account only, never a real user's.
|
|
221
217
|
2. **Only Google (or another SSO) sign-in, no email+password form** → the same path:
|
|
222
|
-
ask for a DEDICATED Google test account and bank it with \`
|
|
223
|
-
|
|
218
|
+
ask for a DEDICATED Google test account and bank it with \`accounts_create\`. Its
|
|
219
|
+
sign-in plan runs through Google's own pages: click the app's
|
|
224
220
|
Google button, fill \`#identifierId\` with \`{{login_email}}\`, click
|
|
225
221
|
\`#identifierNext\`, fill \`input[name=Passwd]\` with \`{{login_password}}\`, click
|
|
226
|
-
\`#passwordNext\`, then assert the app's signed-in shell. Store it with
|
|
227
|
-
|
|
222
|
+
\`#passwordNext\`, then assert the app's signed-in shell. Store it with
|
|
223
|
+
\`accounts_set_login\`, prove it with \`accounts_check\`. Author its tests with \`auth_mode:
|
|
228
224
|
"session"\` only, so Google is signed in once per run, never per test. Google may
|
|
229
225
|
still challenge a scripted sign-in now and then ("Verify it's you"); the captured
|
|
230
226
|
session (end of §1) is the fallback for that, not the default.
|
|
231
227
|
3. **No credentials, but the app has a signup form** → let Beryl make one.
|
|
232
|
-
\`
|
|
233
|
-
|
|
228
|
+
\`accounts_create\` (type \`beryl\`), then sign it up with \`accounts_signup\` (its
|
|
229
|
+
signup plan). The plan is the app's own
|
|
234
230
|
signup form as an ActionPlan, ending LOGGED IN, filling \`{{mailbox_address}}\` and
|
|
235
231
|
\`{{login_password}}\`. It stays on the account, so a new environment signs the same
|
|
236
232
|
identity up again by itself. An account that already exists on the site (hand-made,
|
|
237
|
-
or the record was lost) skips signup: store its sign-in with \`
|
|
238
|
-
No password sign-in?
|
|
239
|
-
\`await_email\` through; later sign-ins read the same mailbox.
|
|
233
|
+
or the record was lost) skips signup: store its sign-in with \`accounts_set_login\`.
|
|
234
|
+
No password sign-in? Set the account's login method to \`otp\` (or \`magic_link\`) and
|
|
235
|
+
let the plan \`await_email\` through; later sign-ins read the same mailbox.
|
|
240
236
|
4. **Neither** → the flow is not testable authenticated. Say so rather than guessing.
|
|
241
237
|
|
|
242
238
|
A project with no accounts falls back to the \`LOGIN_EMAIL\` variable +
|
|
@@ -254,21 +250,17 @@ or the failure evidence; nothing to turn on. A session-mode test has no sign-in
|
|
|
254
250
|
to fall back to, so an account with no stored login plan fails at setup with
|
|
255
251
|
\`SESSION_NO_LOGIN_PLAN\`.
|
|
256
252
|
|
|
257
|
-
Set it up once per account:
|
|
258
|
-
|
|
259
|
-
\`\`\`
|
|
260
|
-
beryl accounts set-login <id> --file signin.json --probe probe.json
|
|
261
|
-
beryl accounts check <id> # signs in NOW and proves it — do not skip this
|
|
262
|
-
\`\`\`
|
|
253
|
+
Set it up once per account: \`accounts_set_login\` (the sign-in plan and a probe), then
|
|
254
|
+
\`accounts_check\` (signs in NOW and proves it; do not skip this).
|
|
263
255
|
|
|
264
|
-
What
|
|
265
|
-
|
|
256
|
+
What the sign-in plan and the probe must contain, and the read-before-write rule for
|
|
257
|
+
later edits, are in the \`accounts_set_login\` / \`accounts_get_login\` tools' own
|
|
266
258
|
descriptions.
|
|
267
259
|
|
|
268
260
|
Then write the tests with \`requires_auth: true\`, \`auth_mode: "session"\`, and NO
|
|
269
261
|
sign-in steps.
|
|
270
262
|
|
|
271
|
-
**Creates share the sign-in.** \`
|
|
263
|
+
**Creates share the sign-in.** \`tests_create\` replays session-mode plans against the
|
|
272
264
|
account's established session (reused while it stays live), so a batch of creates costs
|
|
273
265
|
at most one sign-in — but still create one at a time: racing creates race the same
|
|
274
266
|
mailbox for mail.
|
|
@@ -277,7 +269,7 @@ An expired session is never your problem: every run proves the stored session an
|
|
|
277
269
|
miss, signs in again from the login plan. A "reconnect" message only appears when there
|
|
278
270
|
is no stored login plan to refresh from.
|
|
279
271
|
|
|
280
|
-
When \`
|
|
272
|
+
When \`accounts_check\` fails, its description explains which half broke and each
|
|
281
273
|
\`SESSION_*\` reason. The one that changes your authoring: an account marked
|
|
282
274
|
unsupported (its session cannot survive a fresh browser) is not your bug, but it IS
|
|
283
275
|
your move — re-author its session-mode tests with \`auth_mode: "inline"\` and their own
|
|
@@ -288,15 +280,15 @@ sign-in steps.
|
|
|
288
280
|
Invite-a-teammate-and-accept-as-them needs two identities mid-flow — not bankable as
|
|
289
281
|
one test. Bank the half the app shows to account A ("the invitation is listed as
|
|
290
282
|
pending", "the share link is issued") — a real, strong outcome. While AUTHORING you can
|
|
291
|
-
drive the full handshake live: a second account (\`
|
|
292
|
-
|
|
283
|
+
drive the full handshake live: a second account (\`accounts_create\`, labelled), or a
|
|
284
|
+
second mailbox (\`mailbox_create\`, read with \`mailbox_read\`).
|
|
293
285
|
|
|
294
286
|
SSO-only sites are not a dead end: with a dedicated Google test account they follow §1
|
|
295
287
|
item 2 like any password account. The captured session is the FALLBACK — no test account
|
|
296
288
|
can be provided, or Google challenges the scripted sign-in: a human signs in once
|
|
297
289
|
through a live browser view in the Beryl webapp, Beryl stores the session encrypted
|
|
298
|
-
server-side and injects it into cloud runs
|
|
299
|
-
server-side, \`
|
|
290
|
+
server-side and injects it into cloud runs, and \`tests_create\` verifies such plans
|
|
291
|
+
server-side, \`runs_trigger\` runs them, \`runs_local\` skips them (the session never
|
|
300
292
|
leaves Beryl's cloud).
|
|
301
293
|
|
|
302
294
|
## 2. Author locally over the Playwright MCP
|
|
@@ -310,9 +302,9 @@ leaves Beryl's cloud).
|
|
|
310
302
|
- **at least one step is an \`expect\`** (a test that asserts nothing is not a test).
|
|
311
303
|
**A \`goto\` at your own app is a PATH, never a full URL** — \`/pricing\`, not
|
|
312
304
|
\`https://app.example.com/pricing\`. The origin comes from the environment's root
|
|
313
|
-
URL, so one plan runs against prod, staging and a preview; bake the origin in and
|
|
314
|
-
|
|
315
|
-
handoff, a magic link on another domain).
|
|
305
|
+
URL, so one plan runs against prod, staging and a preview; bake the origin in and the
|
|
306
|
+
run's environment is silently ignored. Absolute URLs stay legal for OTHER origins (an
|
|
307
|
+
OAuth handoff, a magic link on another domain).
|
|
316
308
|
Optional \`before\` / \`after\` arrays hold setup and teardown; \`after\` runs even
|
|
317
309
|
when a main step fails, so a create flow can clean up its record — and a deletion
|
|
318
310
|
flow creates its own record in \`before\` first. A step in \`after\` may carry
|
|
@@ -361,21 +353,28 @@ ${JSON.stringify(BERYL_TEST_SKILL_EXAMPLE_PLAN, null, 2)
|
|
|
361
353
|
.map((line) => ` ${line}`)
|
|
362
354
|
.join("\n")}
|
|
363
355
|
\`\`\`
|
|
364
|
-
3. **Validate offline, then create
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
\`create\` banks the plan only on a green local replay; a red one hands back the
|
|
371
|
-
failure — fix and re-run. It records no run. Replay mechanics (where it runs over
|
|
372
|
-
MCP, \`--no-verify\`, the cloud-session exception) are in \`tests create\`'s own
|
|
373
|
-
description. Inspect the exact spec with \`beryl tests script --file plan.json\`.
|
|
356
|
+
3. **Validate offline, then create.** Lint with \`tests_lint\` (schema check, no network),
|
|
357
|
+
then bank with \`tests_create\` (the title, the plan, and the \`description\` intent, see
|
|
358
|
+
§3). \`tests_create\` banks the plan only on a green local replay; a red one hands back
|
|
359
|
+
the failure to fix and re-run. It records no run. Replay mechanics (where it runs over
|
|
360
|
+
MCP, its verify option, the cloud-session exception) are in the \`tests_create\` tool's
|
|
361
|
+
own description. Inspect the exact spec with \`tests_script\`.
|
|
374
362
|
Full ActionPlan JSON Schema:
|
|
375
363
|
https://api.beryl.so/api/v1/schemas/action-plan.schema.json.
|
|
376
|
-
Groups are optional and never block: after the test is banked, if \`
|
|
377
|
-
shows any, offer them once (\`
|
|
378
|
-
|
|
364
|
+
Groups are optional and never block: after the test is banked, if \`groups_list\`
|
|
365
|
+
shows any, offer them once (\`tests_set_groups\`); no answer = leave it ungrouped and
|
|
366
|
+
move on. Never create a group yourself, that is the user's call.
|
|
367
|
+
|
|
368
|
+
### Testing a mobile or tablet viewport
|
|
369
|
+
A test runs at a desktop viewport unless you say otherwise. To cover a responsive layout,
|
|
370
|
+
set \`device\` on the plan: \`"mobile"\` or \`"tablet"\` (default \`"desktop"\`). This is
|
|
371
|
+
full Chromium device emulation, not a resize: the viewport, a mobile Chrome user-agent,
|
|
372
|
+
touch and the device pixel ratio all change together, so a hamburger or a nav that only
|
|
373
|
+
exists on small screens actually renders and is testable. \`mobile\` emulates a Pixel-class
|
|
374
|
+
phone, \`tablet\` a Galaxy-class tablet; both keep a Chrome user-agent so the emulated UA
|
|
375
|
+
matches the engine. Author the flow at that size over the Playwright MCP (resize the MCP
|
|
376
|
+
browser to match) so the refs you act on are the ones the small-screen layout renders. The
|
|
377
|
+
account, session and teardown rules are unchanged; \`device\` only sets the frame it runs in.
|
|
379
378
|
|
|
380
379
|
### The outcome assertion is the whole game
|
|
381
380
|
|
|
@@ -407,7 +406,7 @@ observable proof the flow worked.
|
|
|
407
406
|
|
|
408
407
|
### Traps when authoring against a real app
|
|
409
408
|
|
|
410
|
-
Every one of these has produced a wrong plan or a red \`
|
|
409
|
+
Every one of these has produced a wrong plan or a red \`tests_create\`. Check them.
|
|
411
410
|
|
|
412
411
|
1. **Your browser may already be signed in.** The Playwright MCP keeps a persistent
|
|
413
412
|
profile. Author while signed in and you never see the gate — you will mark gated
|
|
@@ -439,8 +438,8 @@ Every one of these has produced a wrong plan or a red \`tests create\`. Check th
|
|
|
439
438
|
|
|
440
439
|
## 3. Writing the natural-language intent
|
|
441
440
|
|
|
442
|
-
|
|
443
|
-
when you re-author). 1
|
|
441
|
+
Give the intent as the \`description\` of \`tests_create\` (or \`tests_set_plan\`
|
|
442
|
+
when you re-author). 1-3 sentences. This is the immutable anchor from §6.
|
|
444
443
|
|
|
445
444
|
- **State the purpose, not the steps.** Not "clicks Sign in, types email and password,
|
|
446
445
|
clicks submit" — the trajectory will change. Instead: *what does a green run prove is
|
|
@@ -458,20 +457,17 @@ Weak (describes steps + asserts nothing meaningful):
|
|
|
458
457
|
|
|
459
458
|
## 4. The local run-fix loop
|
|
460
459
|
|
|
461
|
-
Iterate on your machine before you rely on the cloud. \`
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
beryl runs local <test-id> --no-sync --url-override http://localhost:3000 --dir ./beryl-local
|
|
466
|
-
beryl runs local # the whole suite, results recorded in Beryl
|
|
467
|
-
\`\`\`
|
|
460
|
+
Iterate on your machine before you rely on the cloud. \`runs_local\` runs banked tests
|
|
461
|
+
with the Playwright runner bundled in the CLI: no cloud, no waiting for a scheduled run.
|
|
462
|
+
Run it un-synced with a report directory against one test id while you iterate, or with
|
|
463
|
+
no id to run the whole suite and record the results in Beryl.
|
|
468
464
|
|
|
469
|
-
The loop: draft → \`
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
465
|
+
The loop: draft → \`tests_lint\` → \`tests_create\` (proves, records nothing) → \`runs_local\`
|
|
466
|
+
un-synced while iterating → read the run's \`report.json\` to see which step or assertion
|
|
467
|
+
failed and why → fix the plan → \`tests_set_plan\` → run again. When the batch is done, one
|
|
468
|
+
synced \`runs_local\` (or \`runs_trigger\`) records it as a single run. Its options,
|
|
469
|
+
session-mode behavior, and \`await_email\` / authenticated-test handling are in the
|
|
470
|
+
\`runs_local\` tool's own description.
|
|
475
471
|
|
|
476
472
|
## 5. Testing an OTP / signup flow (\`await_email\`)
|
|
477
473
|
|
|
@@ -482,7 +478,7 @@ flag to turn on.
|
|
|
482
478
|
**The project has one permanent mailbox and all of its mail arrives there.** The
|
|
483
479
|
standing account is used one way only, \`requires_auth: true\` (its session is injected
|
|
484
480
|
before the first step); a test never types its address (the save refuses it), only the
|
|
485
|
-
account's own
|
|
481
|
+
account's own signup and sign-in plans fill \`{{mailbox_address}}\`. Every flow that
|
|
486
482
|
sends mail (a signup, a sign-in code, a wrong code, a lockout, a resend timer, an
|
|
487
483
|
invitation) cites \`{{inbox_address}}\`, a \`+tag\` alias minted per execution and read by
|
|
488
484
|
this test alone.
|
|
@@ -492,7 +488,7 @@ this test alone.
|
|
|
492
488
|
| creates an account, or signs in as one it created (a returning-user test: \`before\` signs up the alias and logs out, the steps sign in as it) | an \`after\` section that deletes it through the app, its steps marked \`optional\`; a plan that reads its signup mail is refused at save time without one |
|
|
493
489
|
| only makes mail arrive, read or not, no account created | nothing to clean up; mail is kept for 48 hours |
|
|
494
490
|
|
|
495
|
-
Every alias minted is listed by \`
|
|
491
|
+
Every alias minted is listed by \`mailbox_identities\` with how its teardown
|
|
496
492
|
ended; the run detail shows the same outcome on each result.
|
|
497
493
|
|
|
498
494
|
The wiring is a three-part chain:
|
|
@@ -527,11 +523,11 @@ For a magic-link flow, replace the code steps with
|
|
|
527
523
|
|
|
528
524
|
Two caveats:
|
|
529
525
|
|
|
530
|
-
- \`
|
|
526
|
+
- \`tests_create\` verifies an \`await_email\` plan like any other: the local
|
|
531
527
|
replay receives at the project mailbox and answers each step over the API, so the
|
|
532
528
|
app's mail really is received and extracted before the test is accepted. The replay
|
|
533
|
-
signs up / sends mail FOR REAL
|
|
534
|
-
unwanted. \`
|
|
529
|
+
signs up / sends mail FOR REAL, so turn \`tests_create\`'s verify off only if that side
|
|
530
|
+
effect is unwanted. \`runs_local\` serves \`await_email\` the same way, so the local loop
|
|
535
531
|
covers OTP/signup flows end to end.
|
|
536
532
|
- §2's outcome discipline still applies: the green signal is the post-verification
|
|
537
533
|
state (the welcome screen, the dashboard), not "an email arrived".
|
package/dist/commands/init.js
CHANGED
|
@@ -89,6 +89,22 @@ function claudeAddHint(name, entry) {
|
|
|
89
89
|
function cursorUserConfigPath() {
|
|
90
90
|
return path.join(os.homedir(), ".cursor", "mcp.json");
|
|
91
91
|
}
|
|
92
|
+
// VS Code's user-scope MCP config lives in the profile dir, NOT ~/.vscode (which VS Code
|
|
93
|
+
// ignores): macOS ~/Library/Application Support/Code/User, Linux ~/.config/Code/User,
|
|
94
|
+
// Windows %APPDATA%/Code/User. Pure resolver so all three platforms are testable.
|
|
95
|
+
export function vscodeUserConfigDirFor(platform, home, env) {
|
|
96
|
+
return platform === "darwin"
|
|
97
|
+
? path.join(home, "Library", "Application Support", "Code", "User")
|
|
98
|
+
: platform === "win32"
|
|
99
|
+
? path.join(env.APPDATA ?? path.join(home, "AppData", "Roaming"), "Code", "User")
|
|
100
|
+
: path.join(env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "Code", "User");
|
|
101
|
+
}
|
|
102
|
+
// Returns null when that dir is absent (VS Code not installed there, Insiders / VSCodium,
|
|
103
|
+
// portable mode) so the caller falls back to a repo-local file.
|
|
104
|
+
function vscodeUserConfigDir() {
|
|
105
|
+
const dir = vscodeUserConfigDirFor(process.platform, os.homedir(), process.env);
|
|
106
|
+
return fs.existsSync(dir) ? dir : null;
|
|
107
|
+
}
|
|
92
108
|
// Write generated content that ships with the CLI and changes every release, so anything
|
|
93
109
|
// that differs is overwritten: a stale copy silently feeds agents months-old guidance,
|
|
94
110
|
// which is worse than losing a local edit the user can redo (and `beryl guide` still prints
|
|
@@ -218,7 +234,8 @@ export const initCommands = [
|
|
|
218
234
|
"instead. With --cursor the same two servers are also wired into Cursor and a repo-local " +
|
|
219
235
|
".cursor/rules/beryl.mdc authoring rule is written (Cursor's equivalent of the skill, " +
|
|
220
236
|
"since it reads neither skills dir); --vscode does the same for VS Code / GitHub Copilot " +
|
|
221
|
-
"(.vscode/mcp.json plus a marked
|
|
237
|
+
"(VS Code's user mcp.json, or .vscode/mcp.json with --scope project, plus a marked " +
|
|
238
|
+
"section in .github/copilot-instructions.md). No " +
|
|
222
239
|
"workspace/project pin and no URL prompt: ask Claude to write tests for " +
|
|
223
240
|
"your site and it resolves the workspace, project, and URL. Safe to re-run: every run " +
|
|
224
241
|
"refreshes the authoring skill to this CLI's version (overwriting an older or edited " +
|
|
@@ -244,8 +261,8 @@ export const initCommands = [
|
|
|
244
261
|
{
|
|
245
262
|
name: "vscode",
|
|
246
263
|
type: "boolean",
|
|
247
|
-
description: "Also wire the two MCP servers into .vscode/mcp.json
|
|
248
|
-
".github/copilot-instructions.md for
|
|
264
|
+
description: "Also wire the two MCP servers into VS Code's user config (or .vscode/mcp.json with " +
|
|
265
|
+
"--scope project) and add a Beryl section to .github/copilot-instructions.md for Copilot",
|
|
249
266
|
},
|
|
250
267
|
],
|
|
251
268
|
examples: [
|
|
@@ -341,16 +358,30 @@ export const initCommands = [
|
|
|
341
358
|
// marked section in `.github/copilot-instructions.md` for the guide pointer. VS Code's
|
|
342
359
|
// per-user MCP config is managed through its UI, not a portable file, so scope is moot.
|
|
343
360
|
if (input.flags.vscode) {
|
|
344
|
-
|
|
361
|
+
// User scope (default) targets VS Code's real profile mcp.json, so it works in
|
|
362
|
+
// every project. Project scope, or a missing profile dir, falls back to the repo.
|
|
363
|
+
const userDir = scope === "user" ? vscodeUserConfigDir() : null;
|
|
364
|
+
const file = userDir
|
|
365
|
+
? path.join(userDir, "mcp.json")
|
|
366
|
+
: path.join(cwd, ".vscode", "mcp.json");
|
|
345
367
|
const wrote = mergeMcpConfig(file, "servers", {
|
|
346
368
|
beryl: VSCODE_MCP_SERVER_ENTRY,
|
|
347
369
|
playwright: VSCODE_PLAYWRIGHT_SERVER_ENTRY,
|
|
348
370
|
});
|
|
349
|
-
const where = path.relative(cwd, file);
|
|
371
|
+
const where = userDir ? file : path.relative(cwd, file);
|
|
350
372
|
report("vscode beryl MCP", wrote.beryl, where);
|
|
351
373
|
report("vscode playwright MCP", wrote.playwright, where);
|
|
374
|
+
if (scope === "user" && !userDir) {
|
|
375
|
+
ctx.err(dim(" • VS Code's user config dir was not found, so this went to a repo-local " +
|
|
376
|
+
".vscode/mcp.json. Run --vscode from your project, or add the server via " +
|
|
377
|
+
'VS Code\'s "MCP: Add Server".'));
|
|
378
|
+
}
|
|
352
379
|
const inst = writeCopilotInstructions(cwd);
|
|
353
380
|
ctx.err(`${green("✓")} copilot instructions ${outcomeLabel(inst.outcome)} ${dim(path.relative(cwd, inst.file))}`);
|
|
381
|
+
if (!fs.existsSync(path.join(cwd, ".git"))) {
|
|
382
|
+
ctx.err(dim(" • copilot-instructions is repo-local; this dir is not a git repo, so " +
|
|
383
|
+
"Copilot may not read it. Run --vscode from your project root."));
|
|
384
|
+
}
|
|
354
385
|
}
|
|
355
386
|
const skills = writeSkills(cwd, scope);
|
|
356
387
|
for (const skill of skills) {
|
|
@@ -367,10 +398,16 @@ export const initCommands = [
|
|
|
367
398
|
ctx.err(`${green("✓")} Beryl authoring skill ${what} ${dim(where)}`);
|
|
368
399
|
}
|
|
369
400
|
await ensureLocalPlaywright(ctx, cwd);
|
|
370
|
-
|
|
371
|
-
|
|
401
|
+
// Name the editor's own agent, so the message speaks to what the user just set up.
|
|
402
|
+
const ed = input.flags.vscode
|
|
403
|
+
? { open: "open VS Code", agent: "Copilot" }
|
|
404
|
+
: input.flags.cursor
|
|
405
|
+
? { open: "open Cursor", agent: "Cursor" }
|
|
406
|
+
: { open: "open your editor", agent: "Claude" };
|
|
407
|
+
const nextSteps = `\n${bold(`Beryl is set up. Now ${ed.open} and ask ${ed.agent} to write tests.`)}\n` +
|
|
408
|
+
` ${dim(`• Say: "write tests for https://your-app.com" and ${ed.agent} picks your workspace/project`)}\n` +
|
|
372
409
|
` ${dim(" and sets the URL for you (no pin, no prompt).")}\n` +
|
|
373
|
-
` ${dim(
|
|
410
|
+
` ${dim(`• Playwright MCP is wired, so ${ed.agent} can drive a real browser to author from your plan.`)}\n` +
|
|
374
411
|
`\nAuthor against the ActionPlan JSON Schema: ${cyan(ACTION_PLAN_SCHEMA_URL)}`;
|
|
375
412
|
// The .mcp.json entry is pinned to @latest, but a global install / old npx cache
|
|
376
413
|
// still wins resolution — so tell the user when the CLI they just ran is stale.
|
package/dist/schema.generated.js
CHANGED
|
@@ -36,6 +36,16 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
36
36
|
"title": "AuthMode",
|
|
37
37
|
"type": "string"
|
|
38
38
|
},
|
|
39
|
+
"DeviceClass": {
|
|
40
|
+
"description": "The viewport and device a test runs at. The AUTHOR sets it when a flow is specific\nto a form factor (a mobile-only nav, a hamburger, a stacked layout); omit it and the\ntest runs at the default desktop viewport.\n\n``mobile`` / ``tablet`` are full Chromium device emulation, not a bare resize: the\nviewport, a mobile Chrome user-agent, touch, and the device pixel ratio all change\ntogether, so a control gated behind a touch or mobile-UA media query actually appears.",
|
|
41
|
+
"enum": [
|
|
42
|
+
"desktop",
|
|
43
|
+
"tablet",
|
|
44
|
+
"mobile"
|
|
45
|
+
],
|
|
46
|
+
"title": "DeviceClass",
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
39
49
|
"DialogChoice": {
|
|
40
50
|
"enum": [
|
|
41
51
|
"accept",
|
|
@@ -1344,6 +1354,11 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
1344
1354
|
"title": "Before",
|
|
1345
1355
|
"type": "array"
|
|
1346
1356
|
},
|
|
1357
|
+
"device": {
|
|
1358
|
+
"$ref": "#/$defs/DeviceClass",
|
|
1359
|
+
"default": "desktop",
|
|
1360
|
+
"description": "Viewport and device to run at. 'mobile' and 'tablet' apply full Chromium device emulation (viewport, mobile user-agent, touch, pixel ratio); 'desktop' (default) runs at the standard desktop viewport."
|
|
1361
|
+
},
|
|
1347
1362
|
"requires_auth": {
|
|
1348
1363
|
"default": false,
|
|
1349
1364
|
"title": "Requires Auth",
|
package/package.json
CHANGED