@beryl-so/cli 0.44.0 → 0.45.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 +18 -19
- package/dist/commands/mailboxes.js +5 -3
- package/dist/commands/tests.js +17 -5
- package/dist/lint.js +22 -17
- package/package.json +1 -1
package/dist/beryl-test-skill.js
CHANGED
|
@@ -402,28 +402,27 @@ A flow that emails the user — a signup verification code, a magic sign-in link
|
|
|
402
402
|
receipt — is testable with the \`await_email\` action. No setup, no configuration, no
|
|
403
403
|
flag to turn on.
|
|
404
404
|
|
|
405
|
-
**The project has one permanent mailbox and all of its mail arrives there.**
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
that mints but never reads mail is flagged by \`tests lint\`. Mail is kept for 48 hours.
|
|
405
|
+
**The project has one permanent mailbox and all of its mail arrives there.** The
|
|
406
|
+
standing account is used one way only, \`requires_auth: true\` (its session is injected
|
|
407
|
+
before the first step); a test never types its address (the save refuses it), only the
|
|
408
|
+
account's own \`signup\` / \`set-login\` plans fill \`{{mailbox_address}}\`. Every flow that
|
|
409
|
+
sends mail (a signup, a sign-in code, a wrong code, a lockout, a resend timer, an
|
|
410
|
+
invitation) cites \`{{inbox_address}}\`, a \`+tag\` alias minted per execution and read by
|
|
411
|
+
this test alone.
|
|
412
|
+
|
|
413
|
+
| The test... | Then |
|
|
414
|
+
|---|---|
|
|
415
|
+
| 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 |
|
|
416
|
+
| only makes mail arrive, read or not, no account created | nothing to clean up; mail is kept for 48 hours |
|
|
417
|
+
|
|
418
|
+
Every alias minted is listed by \`beryl mailbox identities\` with how its teardown ended.
|
|
420
419
|
|
|
421
420
|
The wiring is a three-part chain:
|
|
422
421
|
|
|
423
|
-
1. **Type the address into the app
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
422
|
+
1. **Type the address into the app**: a \`fill\` with \`value: "{{inbox_address}}"\`.
|
|
423
|
+
The alias is fresh every run, so a signup is repeatable by construction, no
|
|
424
|
+
\`{{unique}}\` needed for the email itself; use it for other must-not-collide
|
|
425
|
+
values like a username. The generator handles
|
|
427
426
|
\`{{unique}}\`, \`{{uuid}}\`, \`{{timestamp}}\` are each minted once per execution;
|
|
428
427
|
\`{{timestamp}}\` is second-resolution (a time, NOT a uniqueness guarantee —
|
|
429
428
|
\`{{unique}}\` is).
|
|
@@ -9,9 +9,11 @@ export const mailboxCommands = [
|
|
|
9
9
|
groupDefault: true,
|
|
10
10
|
groupSummary: "The project's standing email addresses, where its tests receive sign-in mail.",
|
|
11
11
|
description: "Returns the address {{mailbox_address}} resolves to, creating it on first ask. " +
|
|
12
|
-
"Every test that reads mail receives here
|
|
13
|
-
"
|
|
14
|
-
"
|
|
12
|
+
"Every test that reads mail receives here, at its own `+tag` alias of this mailbox: " +
|
|
13
|
+
"a plan cites {{inbox_address}}, minted per execution and read by that test alone, " +
|
|
14
|
+
"so parallel tests never consume each other's codes. A test never types the bare " +
|
|
15
|
+
"address (the save refuses it); only an account's own signup and sign-in plans " +
|
|
16
|
+
"(`accounts signup`, `accounts set-login`) may fill it. " +
|
|
15
17
|
"Every alias minted is listed by `mailbox identities`, and a plan that signs up " +
|
|
16
18
|
"with one must delete the account in its `after` section.",
|
|
17
19
|
scope: "project",
|
package/dist/commands/tests.js
CHANGED
|
@@ -84,17 +84,29 @@ export const testCommands = [
|
|
|
84
84
|
"fields, plus the two structural rules (the first EXECUTED step must be a goto, and at " +
|
|
85
85
|
"least one step across before + steps must be an expect). Also the identity rules: a " +
|
|
86
86
|
"plan that signs up with {{inbox_address}} and reads the mail must delete the account " +
|
|
87
|
-
"in `after
|
|
88
|
-
"
|
|
89
|
-
"malformed plan fails here instead of
|
|
87
|
+
"in `after`, and no test plan may type {{mailbox_address}} (errors; the server refuses " +
|
|
88
|
+
"both too). Pass --provisioning for an account's signup or sign-in plan, which may fill " +
|
|
89
|
+
"{{mailbox_address}}. Runs entirely locally, so a malformed plan fails here instead of " +
|
|
90
|
+
"costing a server round-trip. " +
|
|
90
91
|
`Schema: ${ACTION_PLAN_SCHEMA.$id}`,
|
|
91
92
|
scope: "none",
|
|
92
93
|
flags: [
|
|
93
94
|
{ name: "file", type: "string", required: true, description: "Plan JSON file, or - for stdin" },
|
|
95
|
+
{
|
|
96
|
+
name: "provisioning",
|
|
97
|
+
type: "boolean",
|
|
98
|
+
description: "The file is an account's signup or sign-in plan (for `accounts signup` / " +
|
|
99
|
+
"`accounts set-login`), so it may fill {{mailbox_address}}",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
examples: [
|
|
103
|
+
"beryl tests lint --file plan.json",
|
|
104
|
+
"beryl tests lint --file signup-plan.json --provisioning",
|
|
94
105
|
],
|
|
95
|
-
examples: ["beryl tests lint --file plan.json"],
|
|
96
106
|
async run(_ctx, input) {
|
|
97
|
-
const issues = lintPlan(readJsonFlag(input, "file")
|
|
107
|
+
const issues = lintPlan(readJsonFlag(input, "file"), {
|
|
108
|
+
provisioning: flagBool(input, "provisioning"),
|
|
109
|
+
});
|
|
98
110
|
const lines = issues.map((i) => `${i.severity === "warning" ? "warning" : "error"} ${i.path}: ${i.message}`);
|
|
99
111
|
if (issues.every((i) => i.severity === "warning")) {
|
|
100
112
|
return {
|
package/dist/lint.js
CHANGED
|
@@ -3,14 +3,15 @@ import { ACTION_PLAN_SCHEMA } from "./schema.generated.js";
|
|
|
3
3
|
// keep them identical.
|
|
4
4
|
export const MINTED_IDENTITY_NEEDS_TEARDOWN = "This plan signs up with {{inbox_address}} and reads the mail it triggers, so it " +
|
|
5
5
|
"leaves an account behind on the site. Add an 'after' section that deletes it " +
|
|
6
|
-
"through the app (mark steps optional so a missing account ends the teardown "
|
|
7
|
-
|
|
6
|
+
"through the app (mark steps optional so a missing account ends the teardown quietly).";
|
|
7
|
+
export const MAILBOX_ADDRESS_IN_TEST = "This plan types the standing account's address ({{mailbox_address}}) into the app. " +
|
|
8
|
+
"A test never signs in that way: one that needs to be signed in sets requires_auth " +
|
|
9
|
+
"and rides the account's session; one that needs an address the site can mail cites " +
|
|
10
|
+
"{{inbox_address}}, an alias only this test reads. Only the account's own signup and " +
|
|
11
|
+
"sign-in plans (accounts signup, accounts set-login) may fill {{mailbox_address}}.";
|
|
8
12
|
export const OPTIONAL_TEARDOWN_ACTION_MUST_BE_LAST = (label, action) => `${label} (${action}) is optional but not the last step of 'after'. An optional step ` +
|
|
9
13
|
"that misses ends the teardown, so the steps after it would silently not run. Guard " +
|
|
10
14
|
"them with an optional 'wait_for' or 'expect' instead, or move this step to the end.";
|
|
11
|
-
export const MINTED_IDENTITY_UNUSED = "This plan cites {{inbox_address}} but never reads the mail it triggers, so it mints " +
|
|
12
|
-
"an identity it never uses. Cite {{mailbox_address}} instead, unless the site must be " +
|
|
13
|
-
"handed an address it has never seen.";
|
|
14
15
|
const typeOf = (value) => {
|
|
15
16
|
if (value === null)
|
|
16
17
|
return "null";
|
|
@@ -135,7 +136,9 @@ function validate(value, raw, root, path) {
|
|
|
135
136
|
const steps = (section) => Array.isArray(section)
|
|
136
137
|
? section.filter((s) => typeof s === "object" && s !== null)
|
|
137
138
|
: [];
|
|
138
|
-
const
|
|
139
|
+
const cites = (handle) => (step) => Object.values(step).some((v) => typeof v === "string" && v.includes(`{{${handle}}}`));
|
|
140
|
+
const citesInboxAddress = cites("inbox_address");
|
|
141
|
+
const citesMailboxAddress = cites("mailbox_address");
|
|
139
142
|
const TEARDOWN_GUARD_ACTIONS = new Set(["wait_for", "expect"]);
|
|
140
143
|
function optionalPlacementIssues(after) {
|
|
141
144
|
return after.flatMap((s, i) => s.optional === true && !TEARDOWN_GUARD_ACTIONS.has(String(s.action)) && i !== after.length - 1
|
|
@@ -147,7 +150,7 @@ function optionalPlacementIssues(after) {
|
|
|
147
150
|
]
|
|
148
151
|
: []);
|
|
149
152
|
}
|
|
150
|
-
function
|
|
153
|
+
function identityIssues(plan, provisioning) {
|
|
151
154
|
if (typeof plan !== "object" || plan === null)
|
|
152
155
|
return [];
|
|
153
156
|
const { before, steps: main, after } = plan;
|
|
@@ -155,20 +158,22 @@ function mintedIdentityIssues(plan) {
|
|
|
155
158
|
if (placement.length > 0)
|
|
156
159
|
return placement;
|
|
157
160
|
const body = [...steps(before), ...steps(main)];
|
|
158
|
-
|
|
161
|
+
const all = [...body, ...steps(after)];
|
|
162
|
+
if (!provisioning && all.some(citesMailboxAddress)) {
|
|
163
|
+
return [{ path: "(root)", message: MAILBOX_ADDRESS_IN_TEST }];
|
|
164
|
+
}
|
|
165
|
+
if (!all.some(citesInboxAddress))
|
|
159
166
|
return [];
|
|
160
167
|
const readsMail = body.some((s) => s.action === "await_email");
|
|
161
168
|
const tearsDown = steps(after).some((s) => s.action !== "expect");
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!readsMail) {
|
|
166
|
-
return [{ path: "(root)", message: MINTED_IDENTITY_UNUSED, severity: "warning" }];
|
|
167
|
-
}
|
|
168
|
-
return [];
|
|
169
|
+
return readsMail && !tearsDown
|
|
170
|
+
? [{ path: "(root)", message: MINTED_IDENTITY_NEEDS_TEARDOWN }]
|
|
171
|
+
: [];
|
|
169
172
|
}
|
|
170
|
-
|
|
173
|
+
// `provisioning`: the plan is an account's own signup or sign-in plan, the one place
|
|
174
|
+
// {{mailbox_address}} is legal (mirrors the server's validate_editable_plan flag).
|
|
175
|
+
export function lintPlan(plan, opts = {}) {
|
|
171
176
|
const issues = validate(plan, ACTION_PLAN_SCHEMA, ACTION_PLAN_SCHEMA, "");
|
|
172
177
|
// The identity rules read a well-formed plan; a shape error is reported alone.
|
|
173
|
-
return issues.length > 0 ? issues :
|
|
178
|
+
return issues.length > 0 ? issues : identityIssues(plan, opts.provisioning === true);
|
|
174
179
|
}
|
package/package.json
CHANGED