@beryl-so/cli 0.48.0 → 0.50.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.
@@ -181,19 +181,35 @@ The trade is drift — run 200 has 200 of everything run 1 created. So:
181
181
  - **Assert relatively, never absolutely.** "The row I just created is present"
182
182
  (\`expect.persisted\` on a \`{{unique}}\`-named row), not "there are 3 rows" — a count
183
183
  assertion goes stale by itself.
184
- - **Own the resource lifecycle, both directions.** Creating? Delete it in \`after\`
185
- (runs on pass AND fail, unlike a trailing step inside \`steps\`). Deleting? Create
186
- your own victim first — in \`before\` or the opening steps, \`{{unique}}\`-named —
187
- then delete it and assert \`gone\`. Never aim a delete at standing data: run 2 would
188
- have nothing to delete, and it eats the account's accumulated coverage.
189
- - **Every test is \`read\` or \`write\`.** \`read\` (the default) only looks. A test
190
- that creates, saves, deletes, renames, invites, toggles or uploads anything is
191
- \`write\`: bank it with \`--access write\`, and end its \`after\` with an \`expect\`
192
- that proves the undo: \`gone\`, \`hidden\`, \`have_count\` or \`count_delta\` for a
193
- removal, \`have_text\`, \`have_value\` or \`persisted\` for a restored value.
194
- Create refuses a plan that writes without the label and a \`write\` plan
195
- without that closing proof; a \`write\` test whose teardown found nothing to remove
196
- fails the run. Flip a banked test with \`beryl tests set-access <id> write\`.
184
+ - **Cleanup lives in \`after\`, and it returns the account to the exact state you found
185
+ it.** \`after\` runs on pass AND fail; a trailing step inside \`steps\` does not, so
186
+ never put cleanup in \`steps\`.
187
+ - **Created something?** Delete it in \`after\`.
188
+ - **Deleting something?** Create your own victim first (in \`before\` or the opening
189
+ steps, \`{{unique}}\`-named), then delete it and prove \`gone\`; when the deleted thing
190
+ must persist, re-create it in \`after\` as it was. Never aim a delete at standing data:
191
+ run 2 has nothing to delete, and it eats the account's accumulated coverage.
192
+ - **Changed a value (rename, toggle)?** Restore it in \`after\`: set it back to what it
193
+ was. Capture the original BEFORE you change it with \`capture_text\` (or \`capture_value\`
194
+ for a form input), banking it under \`capture_as\`, then \`fill {{handle}}\` in \`after\`
195
+ to put it back.
196
+ - **You label each test \`read\` or \`write\`; Beryl takes your word for it.** Set it with
197
+ \`--access\` (default \`read\`). Beryl does NOT infer it from your selectors, so YOU
198
+ decide, by one test: **would the app be different after this test runs?**
199
+ - **\`write\`** = it leaves a change behind: it creates, edits, deletes, renames, moves,
200
+ uploads or toggles a record or setting that is still different once the test ends. A
201
+ \`write\` must undo itself: end \`after\` with an \`expect\` proving the undo: \`gone\`,
202
+ \`hidden\`, \`have_count\` or \`count_delta\` for a removal, \`have_text\`,
203
+ \`have_value\` or \`persisted\` for a restore.
204
+ - **\`read\`** = it only observes: navigating, searching, filtering, sorting, opening a
205
+ menu or dialog and reading it, or clicking a destructive button and then CANCELLING.
206
+ Nothing is committed. Opening a confirm dialog and cancelling is a \`read\`; a submit
207
+ that stays disabled is a \`read\`.
208
+ A flow that both changes and reads is a \`write\`. Only the \`write\` label is checked
209
+ (its closing teardown proof, and a \`write\` whose teardown removes nothing fails the
210
+ 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\`.
197
213
 
198
214
  ### If there is no account yet
199
215
 
@@ -110,8 +110,8 @@ export const testCommands = [
110
110
  {
111
111
  name: "access",
112
112
  type: "string",
113
- description: "The label the plan will be banked with, read (default) or write; a plan that " +
114
- "creates, edits, deletes or toggles something must be write",
113
+ description: "The label the plan will be banked with: read (default) or write. It is yours to " +
114
+ "set; Beryl does not infer it. Only a write is checked (its closing teardown proof)",
115
115
  },
116
116
  ],
117
117
  examples: [
@@ -580,9 +580,9 @@ export const testCommands = [
580
580
  {
581
581
  name: "tests set-access",
582
582
  summary: "Mark a test read or write",
583
- description: "read: the test only reads the app. write: any step creates, edits, renames, deletes " +
584
- "or toggles something. The server refuses a plan that writes on a read test, so flip " +
585
- "the label before re-authoring such a plan. A write test must clean up in `after`.",
583
+ description: "read: the test only reads the app. write: it creates, edits, renames, deletes or " +
584
+ "toggles something that persists. The label is yours to set; Beryl does not infer it " +
585
+ "from the plan. A write test must clean up in `after`, ending with a proof expect.",
586
586
  scope: "project",
587
587
  args: [
588
588
  { name: "test-id", description: "Test id", required: true },
package/dist/lint.js CHANGED
@@ -9,9 +9,6 @@ export const MAILBOX_ADDRESS_IN_TEST = "This plan types the standing account's a
9
9
  "and rides the account's session; one that needs an address the site can mail cites " +
10
10
  "{{inbox_address}}, an alias only this test reads. Only the account's own signup and " +
11
11
  "sign-in plans (accounts signup, accounts set-login) may fill {{mailbox_address}}.";
12
- export const WRITE_NEEDS_LABEL = (label, detail) => `${label} (${detail}) changes the app, so this test must be marked access 'write' ` +
13
- "(tests create --access write, or tests set-access <id> write) and clean up in " +
14
- "'after'. A 'read' test only reads.";
15
12
  export const WRITE_NEEDS_CLOSING_TEARDOWN = (detail) => "A 'write' test must end with an 'after' section that undoes what it changed and " +
16
13
  "proves it: the last step of 'after' is an expect with expect_kind 'gone', 'hidden', " +
17
14
  "'have_count', 'count_delta' (something removed) or 'have_text', 'have_value', " +
@@ -177,31 +174,6 @@ function identityIssues(plan, provisioning) {
177
174
  ? [{ path: "(root)", message: MINTED_IDENTITY_NEEDS_TEARDOWN }]
178
175
  : [];
179
176
  }
180
- // Same verb list as the server's write_signal (api/app/ta_shared/schemas.py).
181
- const WRITE_VERBS_RE = /\b(create|save|delete|remove|rename|revoke|enable|disable|invite|add|publish|archive|restore|quarantine|un-quarantine|turn on|turn off|join|decline)\b/i;
182
- function writeSignal(plan) {
183
- if (typeof plan !== "object" || plan === null)
184
- return null;
185
- const { before, steps: main, after } = plan;
186
- if (steps(after).length > 0)
187
- return ["after", "a teardown means something was created"];
188
- const sectioned = steps(before).length > 0 || steps(after).length > 0;
189
- const label = (section, i) => (sectioned ? `${section}[${i}]` : `step[${i}]`);
190
- const all = [
191
- ...steps(before).map((s, i) => [label("before", i), s]),
192
- ...steps(main).map((s, i) => [label("steps", i), s]),
193
- ];
194
- for (const [where, s] of all) {
195
- if (s.action === "upload")
196
- return [where, "upload"];
197
- if (s.action === "click" && typeof s.selector === "string") {
198
- const hit = WRITE_VERBS_RE.exec(s.selector);
199
- if (hit)
200
- return [where, `click on '${hit[0]}'`];
201
- }
202
- }
203
- return null;
204
- }
205
177
  const TEARDOWN_PROOF_KINDS = new Set([
206
178
  "gone",
207
179
  "hidden",
@@ -211,11 +183,11 @@ const TEARDOWN_PROOF_KINDS = new Set([
211
183
  "have_value",
212
184
  "persisted",
213
185
  ]);
186
+ // The read/write label is the author's call; Beryl no longer infers it from the plan.
187
+ // Only a `write` label is checked here: it must close its teardown with a proof.
214
188
  function accessIssues(plan, access) {
215
- if (access !== "write") {
216
- const signal = writeSignal(plan);
217
- return signal ? [{ path: "(root)", message: WRITE_NEEDS_LABEL(signal[0], signal[1]) }] : [];
218
- }
189
+ if (access !== "write")
190
+ return [];
219
191
  if (typeof plan !== "object" || plan === null)
220
192
  return [];
221
193
  const after = steps(plan.after);
@@ -232,7 +204,8 @@ function accessIssues(plan, access) {
232
204
  }
233
205
  // `provisioning`: the plan is an account's own signup or sign-in plan, the one place
234
206
  // {{mailbox_address}} is legal (mirrors the server's validate_editable_plan flag).
235
- // `access`: the label the plan will be banked with; given, a writing plan needs 'write'.
207
+ // `access`: the label the plan will be banked with. It is the author's call (Beryl no
208
+ // longer infers it from the plan); given, only a `write` is checked, for its closing proof.
236
209
  export function lintPlan(plan, opts = {}) {
237
210
  const issues = validate(plan, ACTION_PLAN_SCHEMA, ACTION_PLAN_SCHEMA, "");
238
211
  // The identity rules read a well-formed plan; a shape error is reported alone.
@@ -15,6 +15,8 @@ export const ACTION_PLAN_SCHEMA = {
15
15
  "wait_for",
16
16
  "expect",
17
17
  "capture_count",
18
+ "capture_text",
19
+ "capture_value",
18
20
  "await_email",
19
21
  "upload",
20
22
  "dialog",
@@ -325,6 +327,62 @@ export const ACTION_PLAN_SCHEMA = {
325
327
  ]
326
328
  }
327
329
  },
330
+ {
331
+ "if": {
332
+ "properties": {
333
+ "action": {
334
+ "const": "capture_text"
335
+ }
336
+ },
337
+ "required": [
338
+ "action"
339
+ ]
340
+ },
341
+ "then": {
342
+ "properties": {
343
+ "capture_as": {
344
+ "minLength": 1,
345
+ "type": "string"
346
+ },
347
+ "selector": {
348
+ "minLength": 1,
349
+ "type": "string"
350
+ }
351
+ },
352
+ "required": [
353
+ "capture_as",
354
+ "selector"
355
+ ]
356
+ }
357
+ },
358
+ {
359
+ "if": {
360
+ "properties": {
361
+ "action": {
362
+ "const": "capture_value"
363
+ }
364
+ },
365
+ "required": [
366
+ "action"
367
+ ]
368
+ },
369
+ "then": {
370
+ "properties": {
371
+ "capture_as": {
372
+ "minLength": 1,
373
+ "type": "string"
374
+ },
375
+ "selector": {
376
+ "minLength": 1,
377
+ "type": "string"
378
+ }
379
+ },
380
+ "required": [
381
+ "capture_as",
382
+ "selector"
383
+ ]
384
+ }
385
+ },
328
386
  {
329
387
  "if": {
330
388
  "properties": {
@@ -27,6 +27,8 @@ export const ACTION_DOCS = {
27
27
  wait_for: "Wait for the `selector` element to appear",
28
28
  expect: "Assert — kinds in the expect table below",
29
29
  capture_count: "Bank the live count of `selector` matches under `capture_as` for a later `count_delta`",
30
+ capture_text: "Bank the `selector` element's visible text under `capture_as`, to restore it later via `{{handle}}`",
31
+ capture_value: "Bank the `selector` input's value under `capture_as`, to restore it later via `{{handle}}`",
30
32
  await_email: "Await mail in the run inbox, bank the extracted value under `capture_as` (§5)",
31
33
  upload: "Set a file input: `value` names a project config file — see below",
32
34
  dialog: "Arm a one-shot accept/dismiss handler for the NEXT step's dialog — see below",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beryl-so/cli",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
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",