@sun-asterisk/sungen 3.2.23 → 3.2.24-beta.2

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.
Files changed (63) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +40 -8
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/dashboard/templates/index.html +1 -1
  5. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  6. package/dist/exporters/csv-exporter.js +3 -1
  7. package/dist/exporters/csv-exporter.js.map +1 -1
  8. package/dist/exporters/feature-parser.d.ts +7 -0
  9. package/dist/exporters/feature-parser.d.ts.map +1 -1
  10. package/dist/exporters/feature-parser.js +27 -0
  11. package/dist/exporters/feature-parser.js.map +1 -1
  12. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  13. package/dist/exporters/xlsx-report-builder.js +3 -1
  14. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  15. package/dist/harness/audit.d.ts +22 -1
  16. package/dist/harness/audit.d.ts.map +1 -1
  17. package/dist/harness/audit.js +122 -13
  18. package/dist/harness/audit.js.map +1 -1
  19. package/dist/harness/flow-contract.d.ts +104 -0
  20. package/dist/harness/flow-contract.d.ts.map +1 -1
  21. package/dist/harness/flow-contract.js +199 -12
  22. package/dist/harness/flow-contract.js.map +1 -1
  23. package/dist/harness/parse.d.ts.map +1 -1
  24. package/dist/harness/parse.js +22 -3
  25. package/dist/harness/parse.js.map +1 -1
  26. package/dist/harness/quality-gates.d.ts +2 -1
  27. package/dist/harness/quality-gates.d.ts.map +1 -1
  28. package/dist/harness/quality-gates.js +47 -5
  29. package/dist/harness/quality-gates.js.map +1 -1
  30. package/dist/harness/sensors.d.ts.map +1 -1
  31. package/dist/harness/sensors.js +48 -5
  32. package/dist/harness/sensors.js.map +1 -1
  33. package/dist/harness/spec-coverage.d.ts +20 -0
  34. package/dist/harness/spec-coverage.d.ts.map +1 -1
  35. package/dist/harness/spec-coverage.js +35 -0
  36. package/dist/harness/spec-coverage.js.map +1 -1
  37. package/dist/harness/viewpoint-ledger.d.ts +4 -0
  38. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  39. package/dist/harness/viewpoint-ledger.js +59 -1
  40. package/dist/harness/viewpoint-ledger.js.map +1 -1
  41. package/dist/orchestrator/templates/ai-src/commands/create-test.md +28 -0
  42. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +98 -5
  43. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-e-identity.md +1 -1
  44. package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
  45. package/dist/orchestrator/test-data-guide.js +8 -0
  46. package/dist/orchestrator/test-data-guide.js.map +1 -1
  47. package/package.json +3 -3
  48. package/src/cli/commands/audit.ts +37 -8
  49. package/src/dashboard/templates/index.html +1 -1
  50. package/src/exporters/csv-exporter.ts +3 -1
  51. package/src/exporters/feature-parser.ts +22 -0
  52. package/src/exporters/xlsx-report-builder.ts +3 -1
  53. package/src/harness/audit.ts +131 -17
  54. package/src/harness/flow-contract.ts +258 -12
  55. package/src/harness/parse.ts +22 -3
  56. package/src/harness/quality-gates.ts +48 -5
  57. package/src/harness/sensors.ts +47 -4
  58. package/src/harness/spec-coverage.ts +38 -0
  59. package/src/harness/viewpoint-ledger.ts +58 -1
  60. package/src/orchestrator/templates/ai-src/commands/create-test.md +28 -0
  61. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +98 -5
  62. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-e-identity.md +1 -1
  63. package/src/orchestrator/test-data-guide.ts +8 -0
@@ -23,6 +23,19 @@ export interface LedgerResult {
23
23
  }
24
24
 
25
25
  const ID_RE = /\b([A-Z]{1,5}\d{0,2}(?:[.\-][A-Za-z0-9]+)*-?\d{0,3})\b/; // VP0.Title, VP7-002, MS-HP-001, TV-01
26
+
27
+ /**
28
+ * Lines that DECLARE something about the checklist rather than being a checklist item —
29
+ * atomizing them manufactures gaps that can never be closed (QA field report: 4 of 6
30
+ * "missing items" were a `qa/bug-reports` placeholder plus three Priority-Viewpoints
31
+ * rows whose third cell is the PROSE REASON for a priority, not a claim to test).
32
+ */
33
+ // "None on file yet …", "N/A", "TBD", "Do not invent …" — an explicit statement that there
34
+ // is nothing here. A gap report built from these tells the author to test the absence.
35
+ const PLACEHOLDER_ITEM = /^(none\b|n\/a\b|tbd\b|no known\b|nothing\b|do not invent\b|-+$)/i;
36
+ /** A priority-DECLARATION row: `| VP-LOGIC | High | <reason prose> |`. The category id and its
37
+ * priority are consumed by the traceability + balance axes; the reason is rationale. */
38
+ const PRIORITY_ROW = /^(?:VP|FL)[A-Z0-9._-]*\s+—\s+(?:critical|high|medium|normal|low|deferred)\b/i;
26
39
  const GENERIC = new Set(['display', 'shown', 'value', 'field', 'input', 'page', 'screen', 'button', 'link', 'text', 'check', 'verify', 'should', 'with', 'when', 'then', 'user', 'this', 'that', 'each', 'item', 'items']);
27
40
 
28
41
  /** Extract atomic checklist items from a viewpoint file (format-tolerant). */
@@ -42,11 +55,13 @@ export function parseViewpointItems(viewpointPath: string): { id?: string; text:
42
55
  else if (line.startsWith('|')) { // table data row
43
56
  if (/^\|[\s|:-]+\|?$/.test(line)) continue; // separator
44
57
  const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
45
- if (/^(vp|id|viewpoint|priority|reason|no\.?|category|item|trigger|#|pattern|applicable|notes|field|constraint|code|description|status)$/i.test(cells[0] || '')) continue; // header
58
+ if (/^(vp|id|viewpoint|priority|reason|no\.?|category|item|trigger|#|pattern|applicable|notes|field|constraint|code|description|status|step|flow|ref|level|question|screen|actor|branches from|own steps|component|thành phần|trường|bước)$/i.test(cells[0] || '')) continue; // header
46
59
  text = cells.join(' — ');
47
60
  } else continue;
48
61
  text = text.replace(/[*`]/g, '').trim();
49
62
  if (!text) continue;
63
+ if (PLACEHOLDER_ITEM.test(text)) continue; // "None on file yet" is not a claim
64
+ if (PRIORITY_ROW.test(text)) continue; // priority declaration, not a checklist item
50
65
  const idM = text.match(ID_RE);
51
66
  const id = idM && /\d/.test(idM[1]) ? idM[1] : undefined; // require a digit so prose words aren't IDs
52
67
  const words = (text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w));
@@ -79,3 +94,45 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
79
94
 
80
95
  return { hasViewpoint: true, total: items.length, covered, ratio: items.length ? covered / items.length : 1, missing };
81
96
  }
97
+
98
+ /**
99
+ * A viewpoint item that names a BROWSER-LEVEL gesture, met by an in-app control instead.
100
+ *
101
+ * "Browser back button from Basic Info Confirmation to Basic Info — values must be restored"
102
+ * was answered with `User click [Confirmation:Back] button`. Those are different mechanisms: an
103
+ * app button runs the screen's own re-hydrate logic, browser history does not, and a defect
104
+ * that only appears on history navigation lives exactly in the gap. The ledger counted the item
105
+ * covered because the words matched, so the substitution was invisible (#595).
106
+ *
107
+ * The language has no reload/back step at all (#593), so the honest outcome is a `@manual`
108
+ * deferral naming the gesture — not a silent swap.
109
+ */
110
+ const BROWSER_GESTURES: Array<{ gesture: string; re: RegExp; appSubstitute: RegExp }> = [
111
+ { gesture: 'browser back', re: /\bbrowser(?:'s)?[ -]?(?:back|history)\b|\bhistory[ -]back\b/i,
112
+ appSubstitute: /\bclick \[[^\]]*back[^\]]*\]/i },
113
+ { gesture: 'browser reload/refresh', re: /\b(?:page |browser )?(?:reload|refresh)(?:ing|ed|es)?\b/i,
114
+ appSubstitute: /\bis on \[[^\]]+\] page\b/i },
115
+ { gesture: 'closing and reopening the tab', re: /\bclos(?:e|ing) (?:and reopen\w*\s*)?the tab\b|\breopen\w* the tab\b/i,
116
+ appSubstitute: /\bis on \[[^\]]+\] page\b/i },
117
+ ];
118
+
119
+ export function browserGestureSubstitutions(
120
+ viewpointPath: string, scenarios: ScenarioInfo[],
121
+ ): Array<{ gesture: string; item: string }> {
122
+ const out: Array<{ gesture: string; item: string }> = [];
123
+ // Judged PER SCENARIO, not over the whole file: a @manual scenario elsewhere that merely
124
+ // mentions the gesture in passing (explaining a tooling limit) silenced the check for an
125
+ // item that an automated scenario had quietly substituted.
126
+ const deferred = scenarios.filter((s) => s.manual).map((s) => s.haystack);
127
+ const automated = scenarios.filter((s) => !s.manual).map((s) => s.haystack);
128
+ for (const item of parseViewpointItems(viewpointPath)) {
129
+ for (const g of BROWSER_GESTURES) {
130
+ if (!g.re.test(item.text)) continue;
131
+ // A @manual scenario whose own subject IS the gesture is the honest answer.
132
+ if (deferred.some((h) => g.re.test(h))) break;
133
+ if (automated.some((h) => g.appSubstitute.test(h))) out.push({ gesture: g.gesture, item: item.text });
134
+ break;
135
+ }
136
+ }
137
+ return out;
138
+ }
@@ -30,6 +30,34 @@ If `spec_figma.md` exists OR the user provides a Figma URL for the PAT flow:
30
30
  **Input**: Screen or flow name (e.g., `/sungen-create-test admin-users`).
31
31
  {{/cap}}
32
32
 
33
+ ## ⛔ HARD RULE — in flow mode, AUTHOR the flow contract before generating
34
+
35
+ If `requirements/flow-contract.yaml` is missing, **write it first** — never cite a contract you
36
+ did not create, and never generate scenarios without one. Without it the flow is scored as a
37
+ generic screen (page-type themes that do not fit a journey), and the audit reports
38
+ `FLOW-CONTRACT-MISSING`.
39
+
40
+ It needs the use-case declaration ONCE — actor · trigger · goal · precondition · `outcome` —
41
+ plus **both** postconditions: `successGuarantee` (true when the goal is reached) and
42
+ `minimalGuarantee` (what must still hold when the journey FAILS). Exception flows assert against
43
+ the second one; with no `minimalGuarantee` they can only prove "an error appeared"
44
+ (`FLOW-GUARANTEE-MISSING`). Then declare `phases:` — `[BF, AF, EF]` for a use-case decomposition
45
+ (the first phase is the Basic Flow). Then enumerate the use case's flows with the step × risk
46
+ matrix in the `sungen-tc-generation` skill and **write them into the contract's `flows:`
47
+ inventory**, each with a `status:` (`covered` / `deferred` / `pending-clarification` /
48
+ `out-of-scope`) and a `reason:` for anything but `covered`. Without the inventory, coverage can
49
+ only be measured per phase — and a phase counts as covered as soon as ONE scenario carries it, so
50
+ a fifteen-flow use case scores 100% on three scenarios (`FLOW-INVENTORY-MISSING`, and the top mark
51
+ is withheld).
52
+
53
+ A risk the specs do NOT settle is `pending-clarification` with the question written down — never a
54
+ scenario asserting invented behaviour, and never a silent omission. If the flow's requirements are
55
+ restated from screen specs the project does not hold, say so: the audit reports
56
+ `SPEC-RESTATED-UNVERIFIED` because `specFR 100%` over a hand-copied list certifies the copy, not
57
+ the source.
58
+
59
+ ---
60
+
33
61
  ## Platform detection (do this FIRST)
34
62
 
35
63
  Read `qa/capabilities.yaml` and check the `platform` field — **and the verification scope** (`verification:` if set, else derived from `enabled`: `ui` always; `api`/`db` only if that driver is on). This is the project's recorded **test type**: an **E2E/UI-only** project (no `api`/`db`) must NOT get `@api`/`@query`/`@requires:api|db` verification unless the test-viewpoint explicitly asks for it — keep oracles UI-observable, and cap any in-scope API/DB-in-E2E verification at the **≤20% band** (see `sungen-tc-generation` → "Respect the project's verification scope"). `sungen audit` flags `VERIFICATION-OUT-OF-SCOPE`.
@@ -636,10 +636,21 @@ those namespaces are the **System INTEGRATION** group — keep their oracles abo
636
636
 
637
637
  **Read `requirements/flow-contract.yaml` FIRST — it is the flow's boundary and the yardstick
638
638
  `sungen audit` scores the flow against** (`flowCoverage` axis = journey phases HP/ER/EH automated;
639
- `FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). No contract yet author it with the user via the
640
- boundary checklist in `add-flow` (one business goal · clear trigger · ONE observable outcome
641
- valuable to the actor · name = "Verb + outcome"), THEN generate. **A filled contract is an INPUT —
642
- never rewrite it to match your output** (same rule as `test-viewpoint.md`).
639
+ `FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). **A filled contract is an INPUT never rewrite it
640
+ to match your output** (same rule as `test-viewpoint.md`).
641
+
642
+ > **HARD RULE the contract must be AUTHORED, never merely cited.** If
643
+ > `requirements/flow-contract.yaml` is absent, WRITE it (with the user, via the boundary checklist
644
+ > in `add-flow`: one business goal · clear trigger · ONE observable outcome valuable to the actor ·
645
+ > name = "Verb + outcome") **before** generating scenarios. Citing a contract you did not create
646
+ > leaves the flow scored as a generic screen — `flowCoverage` never applies, `coverage` falls back
647
+ > to page-type themes that do not fit a journey, and the audit reports `FLOW-CONTRACT-MISSING`.
648
+ > A contract also needs **both** guarantees, not just the happy one:
649
+ >
650
+ > - `successGuarantee` — everything true once the goal IS reached (what the Basic Flow proves).
651
+ > - `minimalGuarantee` — what must still hold when the journey **FAILS** (no duplicate record, no
652
+ > mail on a rejected submit, no half-written state). Without it an Exception Flow has nothing to
653
+ > assert against beyond "an error appeared", and `FLOW-GUARANTEE-MISSING` is reported.
643
654
 
644
655
  | Aspect | Screen | Flow |
645
656
  |---|---|---|
@@ -647,7 +658,7 @@ never rewrite it to match your output** (same rule as `test-viewpoint.md`).
647
658
  | Selector format | `[Element]` | `[Screen:Element]` (namespaced) |
648
659
  | Test data keys | `{{variable}}` | `{{phase.variable}}` |
649
660
  | Feature tag | `@auto` / `@smoke` etc. | `@flow` (required) |
650
- | Scenario ids | `VP-<CATEGORY>-NNN` | `FL-<PHASE>-NNN` — phases: `HP` (happy path), `ER` (error recovery), `EH` (guards), `UI` (journey UI states, optional) |
661
+ | Scenario ids | `VP-<CATEGORY>-NNN` | `FL-<PHASE>-NNN` — use-case phases `BF`/`AF0n`/`EF0n` (see below), or the flat `HP` (happy path) / `ER` (error recovery) / `EH` (guards) / `UI` for a short single-outcome flow |
651
662
 
652
663
  **Scenarios to generate — every phase demanded by the contract, automated:**
653
664
 
@@ -659,6 +670,88 @@ never rewrite it to match your output** (same rule as `test-viewpoint.md`).
659
670
  | Cross-screen handoff | After every screen transition, assert the CARRIED state on the new screen (the added product's name in the cart, the email echoed on the sent screen). | blind tails cap `businessDepth` (`FLOW-HANDOFF-SHALLOW`) |
660
671
  | Stateful regression (when `stateful:` declared) | Count/quantity proof · teardown (remove → empty) · multi-source add. | missing dims cap `businessDepth` (`FLOW-DEPTH`) |
661
672
 
673
+ ### Use-case decomposition — how a flow becomes 15 scenarios instead of 3
674
+
675
+ `HP`/`ER`/`EH` are phase *buckets*; they do not tell you how many flows a use case HAS. The
676
+ use-case framework does, and it is the default decomposition for a multi-screen journey:
677
+
678
+ **1 Use Case = 1 Basic Flow + N Alternate Flows + N Exception Flows.**
679
+
680
+ | Phase | Meaning | Test |
681
+ |---|---|---|
682
+ | `BF` Basic Flow | **Exactly one.** The path where nothing goes wrong, end to end. | Reaches `outcome.assertion` and satisfies `successGuarantee`. |
683
+ | `AF` Alternate Flow | A **designed-for branch** — the user does something else legitimate. NOT an error. | Its own outcome: either re-enters the BF at a named step, or ends elsewhere on purpose. |
684
+ | `EF` Exception Flow | **Blocked.** The goal is not reached. | Asserts `minimalGuarantee` — what must STILL be true now that this failed. |
685
+
686
+ `AF` is the bucket most generators skip entirely, and it is where the interesting bugs live
687
+ (going back and finding the form re-hydrated, two tabs, an escape hatch out of the journey).
688
+ Use `FL-BF-NNN` / `FL-AF0n-NNN` / `FL-EF0n-NNN` ids — `sungen audit` buckets them (BF→business
689
+ core, AF→behaviour, EF→validation/security) and traces them to the viewpoint's `FL-BF`/`FL-AF`/
690
+ `FL-EF` priority rows. Declare `phases: [BF, AF, EF]` in the contract; the FIRST phase is the
691
+ Basic Flow and is the one that must reach `outcome`.
692
+
693
+ **Declare the flow INVENTORY, not just the phases.** `phases: [BF, AF, EF]` says which
694
+ vocabulary the suite uses; it cannot say how many flows the use case has — a phase counts as
695
+ covered the moment ONE scenario carries it, so a fifteen-flow use case with three scenarios
696
+ reported full phase coverage while a reviewer counting flows read it as a third done. List every
697
+ flow in the contract, and give each one a status:
698
+
699
+ ```yaml
700
+ flows:
701
+ - id: BF
702
+ outcome: "The successGuarantee — verified, recorded, signed in"
703
+ status: covered
704
+ - id: AF02
705
+ branchFrom: "BF step 4, before submitting"
706
+ outcome: "Re-enters BF at step 4 with the buffer restored"
707
+ status: covered
708
+ - id: EF08
709
+ branchFrom: "BF step 1, the control tapped twice"
710
+ outcome: "One request, one record"
711
+ status: pending-clarification # the spec is silent — ASK, never assume a guard
712
+ reason: "The guard lives in ST_AUTH_001's spec, which this project does not hold."
713
+ ```
714
+
715
+ Four statuses, and **silence is not one of them**: `covered` · `deferred` · `pending-clarification`
716
+ (the behaviour is not agreed yet, so no scenario can be right) · `out-of-scope` (another suite owns
717
+ it). Anything but `covered` needs a `reason:` — a deferral nobody can audit is the same as a
718
+ missing flow. `sungen audit` then measures `flowCoverage` per DECLARED FLOW and reports
719
+ `FLOW-UNCOVERED` (declared, nobody wrote it), `FLOW-UNDECLARED` (a scenario claiming a flow id the
720
+ inventory never declares), `FLOW-STATUS-UNREASONED` and `FLOW-INVENTORY-MISSING`.
721
+
722
+ **One flow, one id — a viewpoint is not a flow.** Several scenarios may prove one flow: give them
723
+ the same flow id and different sequence numbers (`VP-VAL-EF01-001`, `VP-VAL-EF01-002`), never a
724
+ fresh flow id per assertion. Two shapes the audit reports as `FLOW-PHASE-MISFILED`: an `EF` that
725
+ reaches the outcome with nothing failing (that is a success-path postcondition, so it belongs to
726
+ the BF), and an `AF` with no branch point (it walks the basic path asserting extra content — also
727
+ the BF's). Both inflate the flow count while adding no branch coverage.
728
+
729
+ **Declare once, then declare only the differences.** Actor · Trigger · Goal · Precondition ·
730
+ `successGuarantee` · `minimalGuarantee` are use-case-level: they live in `flow-contract.yaml`
731
+ and are never repeated per flow. Each flow in `test-viewpoint.md` then states only three things:
732
+ **where it branches from · its own steps · its own outcome.** Repeating the precondition in
733
+ fifteen rows is noise; the branch point is the information.
734
+
735
+ **Enumerate flows with the step × risk matrix.** Walk every BF step and ask each column. A hit
736
+ with a basis in the spec becomes a flow; a hit with no basis becomes an **open question** — never
737
+ an invented behaviour:
738
+
739
+ | Risk family | The question at this step |
740
+ |---|---|
741
+ | Double submit | Is the control tapped twice guarded? One request, one record? (Spec silent → open question, not an assumed guard.) |
742
+ | Client-side buffer | Is state held client-side between this step and the next? Then test **both** directions — it survives a legitimate back, and it is **LOST** on reload. A suite proving only the surviving side has tested half the mechanism (`CONTINUITY-ONE-SIDED`). |
743
+ | Concurrency | Two tabs / two devices at this step — whose state wins? |
744
+ | Server error | This step's submit fails server-side: what is kept, what is retryable, is anything half-written? |
745
+ | Abandonment / TTL | The user stops here. Does the partial state lapse, expire, or linger forever? |
746
+ | Direct access | This step's URL opened cold, without the preceding state → refused, and the screen never renders. |
747
+ | Auth transition | Where exactly does the session begin? Before that point the actor is unauthenticated — prove it. |
748
+ | Escape hatch | Is there a documented way OUT of the journey here (back to top, cancel)? That is an `AF`, and its outcome is "goal deliberately not reached". |
749
+
750
+ **Do NOT re-derive field validation.** Per-field equivalence and boundary values belong to the
751
+ OWNING SCREEN's suite. A flow takes representative inputs (see the system-test rule above). State
752
+ that exclusion in the viewpoint's Design Decisions — and if the screen suite does not exist, that
753
+ is a gap to REPORT, not to quietly absorb.
754
+
662
755
  **Boundary discipline while generating:** every scenario must serve the contract's goal. A scenario
663
756
  that never touches `outcome.screen` and is not a guard (`EH`) or error-recovery (`ER`) belongs in a
664
757
  DIFFERENT flow — propose the split instead of writing it here (`FLOW-SCOPE-CREEP` will flag it).
@@ -107,7 +107,7 @@ See `SKILL.md` for the 4 Viewpoints, Shared Checks, and Security Tag Rules.
107
107
 
108
108
  **[VP-VAL] Edge cases**
109
109
 
110
- - [@normal] Register with a sub-address email (user+tag@gmail.com) → treated as a unique email, creation succeeds
110
+ - [@normal] Register with a sub-address email (user+tag@sun-asterisk.com) → treated as a unique email, creation succeeds
111
111
  - [@low] Browser autofill fills the fields → the form receives the correct values, no conflict with a custom input component
112
112
 
113
113
  ---
@@ -50,6 +50,14 @@ payload \`<script>alert(1)</script>\`; the exact expected error message from the
50
50
  **Self-check:** *"If I swapped this for another value of the same kind, would the
51
51
  test still mean the same thing?"* → yes = INVENTED.
52
52
 
53
+ **Email domains — never a live mailbox provider.** An invented address still gets
54
+ mail sent to it: a registration or password-reset test submits it and the system
55
+ dispatches for real. \`@gmail.com\`, \`@yahoo.com\`, \`@outlook.com\` and friends all
56
+ resolve, so that mail reaches whoever owns the address — a stranger, or someone's
57
+ personal account. Use the project domain \`@sun-asterisk.com\`, or \`@example.com\`
58
+ (RFC 2606 — reserved, guaranteed never to deliver). Make each run unique with
59
+ \`{{$timestamp}}\` in the local part: \`"signup+{{\$timestamp}}@sun-asterisk.com"\`.
60
+
53
61
  ## 2) ENV-BOUND — real records of the test environment (not secret)
54
62
 
55
63
  **What it is:** the value must EXIST in the environment for the test to work — a