@sun-asterisk/sungen 3.2.21 → 3.2.22-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 (188) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +8 -0
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/delivery.d.ts +3 -0
  5. package/dist/cli/commands/delivery.d.ts.map +1 -1
  6. package/dist/cli/commands/delivery.js +15 -0
  7. package/dist/cli/commands/delivery.js.map +1 -1
  8. package/dist/cli/commands/inspect.d.ts +41 -0
  9. package/dist/cli/commands/inspect.d.ts.map +1 -0
  10. package/dist/cli/commands/inspect.js +134 -0
  11. package/dist/cli/commands/inspect.js.map +1 -0
  12. package/dist/cli/commands/trace.d.ts.map +1 -1
  13. package/dist/cli/commands/trace.js +9 -0
  14. package/dist/cli/commands/trace.js.map +1 -1
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/exporters/matrix/build.d.ts +10 -0
  18. package/dist/exporters/matrix/build.d.ts.map +1 -1
  19. package/dist/exporters/matrix/build.js +38 -0
  20. package/dist/exporters/matrix/build.js.map +1 -1
  21. package/dist/exporters/matrix/export.d.ts.map +1 -1
  22. package/dist/exporters/matrix/export.js +11 -0
  23. package/dist/exporters/matrix/export.js.map +1 -1
  24. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  25. package/dist/exporters/matrix/render-xlsx.js +44 -1
  26. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  27. package/dist/exporters/matrix/types.d.ts +10 -0
  28. package/dist/exporters/matrix/types.d.ts.map +1 -1
  29. package/dist/exporters/matrix/types.js.map +1 -1
  30. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  31. package/dist/exporters/playwright-report-parser.js +1 -0
  32. package/dist/exporters/playwright-report-parser.js.map +1 -1
  33. package/dist/exporters/types.d.ts +2 -0
  34. package/dist/exporters/types.d.ts.map +1 -1
  35. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
  36. package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
  37. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
  38. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
  39. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
  40. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
  41. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
  42. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  43. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
  44. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
  45. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
  46. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
  47. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
  48. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
  49. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
  50. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
  51. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
  52. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
  53. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
  54. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
  55. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
  56. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
  57. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
  58. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
  59. package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
  60. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
  61. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
  62. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
  63. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
  64. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  65. package/dist/generators/test-generator/code-generator.js +29 -8
  66. package/dist/generators/test-generator/code-generator.js.map +1 -1
  67. package/dist/generators/test-generator/diagnostics.d.ts +25 -1
  68. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  69. package/dist/generators/test-generator/diagnostics.js +24 -0
  70. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  71. package/dist/generators/test-generator/patterns/index.d.ts +45 -0
  72. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  73. package/dist/generators/test-generator/patterns/index.js +159 -20
  74. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  75. package/dist/generators/test-generator/patterns/types.d.ts +36 -0
  76. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  77. package/dist/generators/test-generator/step-mapper.d.ts +33 -0
  78. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  79. package/dist/generators/test-generator/step-mapper.js +100 -24
  80. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  81. package/dist/harness/audit.d.ts +2 -0
  82. package/dist/harness/audit.d.ts.map +1 -1
  83. package/dist/harness/audit.js +101 -10
  84. package/dist/harness/audit.js.map +1 -1
  85. package/dist/harness/flow-contract.d.ts +87 -0
  86. package/dist/harness/flow-contract.d.ts.map +1 -0
  87. package/dist/harness/flow-contract.js +259 -0
  88. package/dist/harness/flow-contract.js.map +1 -0
  89. package/dist/harness/flow-plan.d.ts +3 -0
  90. package/dist/harness/flow-plan.d.ts.map +1 -1
  91. package/dist/harness/flow-plan.js +6 -2
  92. package/dist/harness/flow-plan.js.map +1 -1
  93. package/dist/harness/parse.d.ts +5 -0
  94. package/dist/harness/parse.d.ts.map +1 -1
  95. package/dist/harness/parse.js +29 -1
  96. package/dist/harness/parse.js.map +1 -1
  97. package/dist/harness/perf.d.ts +40 -0
  98. package/dist/harness/perf.d.ts.map +1 -0
  99. package/dist/harness/perf.js +136 -0
  100. package/dist/harness/perf.js.map +1 -0
  101. package/dist/harness/sensors.d.ts.map +1 -1
  102. package/dist/harness/sensors.js +13 -1
  103. package/dist/harness/sensors.js.map +1 -1
  104. package/dist/harness/spec-coverage.d.ts +8 -0
  105. package/dist/harness/spec-coverage.d.ts.map +1 -1
  106. package/dist/harness/spec-coverage.js +60 -6
  107. package/dist/harness/spec-coverage.js.map +1 -1
  108. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +51 -3
  109. package/dist/orchestrator/templates/ai-src/commands/create-test.md +10 -0
  110. package/dist/orchestrator/templates/ai-src/commands/run-test.md +23 -0
  111. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
  112. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  113. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +61 -12
  114. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
  115. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +65 -17
  116. package/dist/orchestrator/templates/qa-context.md +14 -1
  117. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  118. package/dist/orchestrator/templates/specs-api.js +104 -29
  119. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  120. package/dist/orchestrator/templates/specs-api.ts +104 -26
  121. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  122. package/dist/orchestrator/templates/specs-db.js +18 -5
  123. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  124. package/dist/orchestrator/templates/specs-db.ts +19 -5
  125. package/package.json +3 -3
  126. package/src/cli/commands/audit.ts +8 -0
  127. package/src/cli/commands/delivery.ts +14 -2
  128. package/src/cli/commands/inspect.ts +128 -0
  129. package/src/cli/commands/trace.ts +9 -0
  130. package/src/cli/index.ts +2 -0
  131. package/src/exporters/matrix/build.ts +40 -0
  132. package/src/exporters/matrix/export.ts +11 -0
  133. package/src/exporters/matrix/render-xlsx.ts +45 -1
  134. package/src/exporters/matrix/types.ts +10 -0
  135. package/src/exporters/playwright-report-parser.ts +2 -0
  136. package/src/exporters/types.ts +2 -0
  137. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +9 -0
  138. package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +23 -1
  139. package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-row-column.hbs +2 -0
  140. package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +10 -0
  141. package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-with-alert-action.hbs +7 -0
  142. package/src/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +14 -2
  143. package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +3 -0
  144. package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  145. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +17 -0
  146. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +13 -0
  147. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +26 -0
  148. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +24 -0
  149. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-variable-assertion.hbs +9 -0
  150. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +10 -0
  151. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +14 -0
  152. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +21 -0
  153. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-variable-assertion.hbs +10 -0
  154. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/row-scoped-column-assertion.hbs +2 -0
  155. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +23 -0
  156. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/storage-key-assertion.hbs +2 -0
  157. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/tab-order-assertion.hbs +3 -0
  158. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-dialog-heading-assertion.hbs +10 -0
  159. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +17 -0
  160. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +13 -0
  161. package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-table-refresh.hbs +13 -0
  162. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -1
  163. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/frame-enter-action.hbs +1 -1
  164. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +5 -5
  165. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +1 -0
  166. package/src/generators/test-generator/code-generator.ts +34 -9
  167. package/src/generators/test-generator/diagnostics.ts +25 -1
  168. package/src/generators/test-generator/patterns/index.ts +172 -25
  169. package/src/generators/test-generator/patterns/types.ts +35 -0
  170. package/src/generators/test-generator/step-mapper.ts +106 -23
  171. package/src/harness/audit.ts +104 -11
  172. package/src/harness/flow-contract.ts +261 -0
  173. package/src/harness/flow-plan.ts +10 -3
  174. package/src/harness/parse.ts +31 -1
  175. package/src/harness/perf.ts +112 -0
  176. package/src/harness/sensors.ts +13 -1
  177. package/src/harness/spec-coverage.ts +55 -5
  178. package/src/orchestrator/templates/ai-src/commands/add-flow.md +51 -3
  179. package/src/orchestrator/templates/ai-src/commands/create-test.md +10 -0
  180. package/src/orchestrator/templates/ai-src/commands/run-test.md +23 -0
  181. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +2 -2
  182. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  183. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +61 -12
  184. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +22 -8
  185. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +65 -17
  186. package/src/orchestrator/templates/qa-context.md +14 -1
  187. package/src/orchestrator/templates/specs-api.ts +104 -26
  188. package/src/orchestrator/templates/specs-db.ts +19 -5
@@ -479,7 +479,11 @@ Add cleanup tags per the `sungen-gherkin-syntax` Cleanup table. Key rules:
479
479
 
480
480
  **Files:** `qa/screens/<screen>/features/<screen>.feature` + `qa/screens/<screen>/test-data/<screen>.yaml`
481
481
 
482
- Use step patterns and element types from `sungen-gherkin-syntax`.
482
+ Use step patterns and element types from `sungen-gherkin-syntax`. For a `@platform:mobile/android/ios`
483
+ screen or flow, first check that skill's **Platform Support** section — never author a step marked
484
+ `[web]` there (tables, browser storage, tab order, `@mock`, uploader, `Then User is on [T] page`) for
485
+ a mobile unit, and read the divergence table before relying on `see [T] page` or `hover` to prove
486
+ anything on mobile. Reach for `sungen-mobile-gestures` for the gesture vocabulary instead.
483
487
  **Naming**: reuse the **project's `test-viewpoint.md` IDs** when it declares them (e.g. `VP0`, `MS-HP-001`); otherwise `VP-<CATEGORY>-<NNN>`. Scenario name must use the **same element type** as the steps.
484
488
 
485
489
  **Test data** — grouped by section, loaded at runtime:
@@ -603,24 +607,67 @@ error:
603
607
 
604
608
  ## Flow Test Generation
605
609
 
610
+ > **Terminology**: many QA teams call this level **System Test** (ISTQB: verify the fully
611
+ > integrated system against its specified requirements, designed around business
612
+ > processes/use cases). In sungen a *flow* is that system-test suite run through the real UI;
613
+ > its automation then serves as the regression suite on later runs. The names are
614
+ > interchangeable in conversation; the artifacts stay `qa/flows/<name>/`.
615
+
616
+ **Representative, not exhaustive (ISTQB system-test design rule).** A flow case takes ONE
617
+ representative input per branch — one valid value for the main flow, one error value that
618
+ triggers the exception branch and proves the journey recovers. The full equivalence/boundary
619
+ matrix belongs to the OWNING SCREEN (as `@cases` there), never re-run inside the flow: if the
620
+ screen already proved "the field rejects negatives", the flow only proves "the journey survives
621
+ one rejected input". `sungen audit` reports `FLOW-EXHAUSTIVE-INPUTS` when ≥3 same-shape flow
622
+ scenarios differ only in data. **Mocked dependencies**: a flow scenario using `@mock` is
623
+ delivered as *Pass (Mocked)* — verified handling logic, not the real dependency; the delivery's
624
+ External Dependency Tracking table lists it until re-verified real.
625
+
626
+ **Release selection & external legs**: when the contract declares `golden: true`, tag the
627
+ happy-path scenario(s) that prove the outcome with **`@golden`** — that is the set
628
+ `sungen inspect` (Final Inspection, Go/No-Go) runs before a release; `GOLDEN-MISSING` fires
629
+ until it exists, and a @golden scenario that passes only against mocks BLOCKS the release.
630
+ When the contract declares `external:` legs (another team/vendor's system), scenarios touching
631
+ those namespaces are the **System INTEGRATION** group — keep their oracles about the boundary
632
+ (contract/format/handoff), and expect `SIT-MOCK-ONLY` until they are re-verified real.
633
+
634
+
606
635
  > **Auto-detect**: if path is `qa/flows/<name>/` → use this section. Skip Steps 1–4 above.
607
636
 
637
+ **Read `requirements/flow-contract.yaml` FIRST — it is the flow's boundary and the yardstick
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`).
643
+
608
644
  | Aspect | Screen | Flow |
609
645
  |---|---|---|
610
- | Section focus | UI patterns per section | Journey phases across screens |
646
+ | Section focus | UI patterns per section | Journey phases toward ONE declared outcome |
611
647
  | Selector format | `[Element]` | `[Screen:Element]` (namespaced) |
612
648
  | Test data keys | `{{variable}}` | `{{phase.variable}}` |
613
649
  | Feature tag | `@auto` / `@smoke` etc. | `@flow` (required) |
614
- | Viewpoints | VP-UI/VAL/LOGIC/SEC per section | VP-LOGIC (transitions), VP-SEC (auth persistence), VP-VAL (cross-screen data) |
650
+ | Scenario ids | `VP-<CATEGORY>-NNN` | `FL-<PHASE>-NNN` phases: `HP` (happy path), `ER` (error recovery), `EH` (guards), `UI` (journey UI states, optional) |
615
651
 
616
- **Scenarios to generate:**
652
+ **Scenarios to generate — every phase demanded by the contract, automated:**
617
653
 
618
- | Category | What to test |
619
- |---|---|
620
- | Happy path | Complete flow end-to-end with valid data |
621
- | Auth persistence | Auth state maintained across screen transitions |
622
- | Error recovery | Invalid input mid-flow fixcontinue |
623
- | Cross-screen data | Data entered on screen A visible on screen B |
654
+ | Phase | What to test | Scoring |
655
+ |---|---|---|
656
+ | `FL-HP` happy path | The complete journey ending in the contract's `outcome.assertion` — an AUTOMATED **data** assertion on `outcome.screen` (an order number, a summed total — not just "page visible"). This scenario is WHY the flow exists: it is the regression proof after a lib/framework upgrade. | uncovered → `flowCoverage` drops + `FLOW-OUTCOME-UNPROVEN` |
657
+ | `FL-ER` error recovery | Invalid input mid-flow error shown → fix → the journey still completes. Validation must not trap the journey. | uncovered → `flowCoverage` drops |
658
+ | `FL-EH` guards | Direct URL access without the precondition · browser back · refresh · expired context — each ends in a safe observable state. | uncovered `flowCoverage` drops |
659
+ | 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
+ | Stateful regression (when `stateful:` declared) | Count/quantity proof · teardown (remove → empty) · multi-source add. | missing dims cap `businessDepth` (`FLOW-DEPTH`) |
661
+
662
+ **Boundary discipline while generating:** every scenario must serve the contract's goal. A scenario
663
+ that never touches `outcome.screen` and is not a guard (`EH`) or error-recovery (`ER`) belongs in a
664
+ DIFFERENT flow — propose the split instead of writing it here (`FLOW-SCOPE-CREEP` will flag it).
665
+ Auth persistence across transitions is part of `EH` unless the project declares it its own phase.
666
+
667
+ **Manual in flows**: always `@manual:Mx` with the reason code — bare `@manual` is flagged
668
+ (`MANUAL-CODE-MISSING`) because the capability planner cannot route it. Typical flow deferrals:
669
+ inbox/mail oracle → `M5`, network-request count → `M3`, context expiry control → `M7`. A
670
+ cross-screen scenario inside the flow's own goal is NOT manual — automate it here.
624
671
 
625
672
  ```gherkin
626
673
  @flow @auth:user
@@ -630,19 +677,20 @@ Feature: Award Submission Flow
630
677
  Given User is on [Login] page
631
678
 
632
679
  @high
633
- Scenario: User logs in successfully
680
+ Scenario: FL-HP-001 A signed-in user's nomination is submitted and confirmed
634
681
  When User fill [Login:Email] field with {{login.email}}
635
682
  And User fill [Login:Password] field with {{login.password}}
636
683
  And User click [Login:Submit] button
637
684
  Then User see [Dashboard] page
638
-
639
- @high
640
- Scenario: User submits nomination
641
685
  When User click [Dashboard:Awards] link
642
- Then User see [Awards] page
643
- When User fill [Awards:Nominee] field with {{submission.nominee}}
686
+ And User fill [Awards:Nominee] field with {{submission.nominee}}
644
687
  And User click [Awards:Submit] button
645
- Then User see {{success_message}} message
688
+ Then User see [Awards:Success Message] text with {{success_message}}
689
+
690
+ @high
691
+ Scenario: FL-EH-001 Direct access to the award form without login redirects to login
692
+ When User go to [Awards] page
693
+ Then User see [Login] page
646
694
  ```
647
695
 
648
696
  ```yaml
@@ -45,8 +45,21 @@ Example:
45
45
 
46
46
  ## Testing Strategy
47
47
 
48
+ Machine-readable intent — `sungen audit` reads these keys (Intent Profile). Values here are
49
+ live even when the surrounding text changes; an invalid value silently falls back to the default.
50
+
51
+ focus: functional
52
+ <!-- focus: functional | e-commerce | security | smoke — drives the audit's depth threshold -->
53
+
54
+ risk_tier: normal
55
+ <!-- risk_tier: high | normal | low -->
56
+
57
+ To silence driver suggestions in audit findings, add a line: capability_suggestions with value off.
58
+
48
59
  **Focus areas** — what to cover thoroughly:
49
- <!-- List from: functional, security, ui, accessibility, performance -->
60
+ <!-- Prose for humans; the parseable value is the `focus:` key above.
61
+ Response-time budgets are NOT a focus value — declare them in qa/perf.yaml
62
+ (percentile + scenario_ms budgets; reported by `sungen delivery`). -->
50
63
  <!-- Example: functional, security -->
51
64
 
52
65
  **Mandatory coverage:**
@@ -7,12 +7,16 @@
7
7
  * `{{name.body.<path>}}`. Base URL + auth come from a `kind: api` datasource in datasources.yaml,
8
8
  * with `${VAR}` resolved from .env.qa / process.env — never inline.
9
9
  *
10
+ * Runs on Node's global `fetch` (Node 18+) — no `@playwright/test` import, so this driver loads in
11
+ * mobile (Appium/Mocha) specs, which carry no Playwright dependency. Previously built on
12
+ * `APIRequestContext`; ported method-by-method (see `ApiClient.call`) to keep behaviour — including
13
+ * the `@hybrid` storageState cookie reuse — equivalent on web.
14
+ *
10
15
  * Safety: a datasource flagged `env: production` is refused unless SUNGEN_ALLOW_PROD=1.
11
16
  * DO NOT EDIT — regenerated by `sungen generate`.
12
17
  */
13
18
  import * as fs from 'fs';
14
19
  import * as path from 'path';
15
- import { request, type APIRequestContext } from '@playwright/test';
16
20
 
17
21
  interface ApiDataSource {
18
22
  kind?: string;
@@ -56,11 +60,53 @@ function substituteRaw(text: string, params: Record<string, any>): string {
56
60
  }
57
61
  /** Delete any case-insensitive variant of a header (HTTP header names are case-insensitive, but a
58
62
  * plain object keys on exact case — so a datasource `Content-Type` and our `content-type` would
59
- * both be sent). Call before setting, or to let Playwright own the header (multipart boundary). */
63
+ * both be sent). Call before setting, or to let fetch own the header (multipart boundary). */
60
64
  function deleteHeader(headers: Record<string, string>, name: string): void {
61
65
  const lower = name.toLowerCase();
62
66
  for (const k of Object.keys(headers)) if (k.toLowerCase() === lower) delete headers[k];
63
67
  }
68
+ /** True when `headers` already carries a case-insensitive match for `name` — used to respect an
69
+ * explicit catalog header instead of overriding it with our own content-type default. */
70
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
71
+ const lower = name.toLowerCase();
72
+ return Object.keys(headers).some((k) => k.toLowerCase() === lower);
73
+ }
74
+
75
+ /**
76
+ * Build a `Cookie:` header value from a Playwright storageState JSON file (written by
77
+ * `sungen makeauth` / `context.storageState()` — `{ cookies: [...], origins: [...] }`), scoped to
78
+ * the cookies that apply to `url` the way a browser would send them (domain + path + not expired).
79
+ * Powers `@hybrid`: the API call reuses the UI session's cookies without a browser/Playwright
80
+ * context. Returns undefined when the file has no cookie that matches `url`.
81
+ */
82
+ function loadStorageStateCookieHeader(storageStatePath: string, url: string): string | undefined {
83
+ const abs = path.isAbsolute(storageStatePath) ? storageStatePath : path.join(process.cwd(), storageStatePath);
84
+ if (!fs.existsSync(abs)) {
85
+ throw new Error(`API Driver: storageState "${storageStatePath}" not found — run \`sungen makeauth\` first.`);
86
+ }
87
+ const state = JSON.parse(fs.readFileSync(abs, 'utf-8')) as {
88
+ cookies?: Array<{ name: string; value: string; domain?: string; path?: string; expires?: number; secure?: boolean }>;
89
+ };
90
+ const cookies = state.cookies || [];
91
+ if (!cookies.length) return undefined;
92
+ const target = new URL(url);
93
+ const nowSec = Date.now() / 1000;
94
+ const targetIsSecure = target.protocol === 'https:';
95
+ const matches = cookies.filter((c) => {
96
+ const domain = (c.domain || '').replace(/^\./, '');
97
+ const domainOk = !domain || target.hostname === domain || target.hostname.endsWith(`.${domain}`);
98
+ const pathOk = target.pathname.startsWith(c.path || '/');
99
+ const notExpired = c.expires === undefined || c.expires < 0 || c.expires > nowSec;
100
+ // A Secure cookie must never leave over plaintext. Playwright's own cookie jar enforces this,
101
+ // and this function exists to be EQUIVALENT to it — omitting the check would make a mixed
102
+ // @hybrid setup (https browser session, http API datasource) leak the session cookie in clear,
103
+ // a downgrade introduced by us rather than by anything the author wrote.
104
+ const secureOk = !c.secure || targetIsSecure;
105
+ return domainOk && pathOk && notExpired && secureOk;
106
+ });
107
+ if (!matches.length) return undefined;
108
+ return matches.map((c) => `${c.name}=${c.value}`).join('; ');
109
+ }
64
110
 
65
111
  /** A multipart file field embedded by the compiler (string = shorthand for { path }). */
66
112
  type FileSpec = string | { path: string; mimeType?: string; filename?: string; name?: string };
@@ -173,11 +219,13 @@ class ApiClient {
173
219
  if (req.body !== undefined && req.body !== null) {
174
220
  body = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
175
221
  }
176
- // Map the wire format to the right Playwright option (#345): json → data (application/json,
177
- // default), form → form (application/x-www-form-urlencoded), multipart → multipart (form-data).
178
- // A `files` block is a real upload: read each fixture into a { name, mimeType, buffer } part and
179
- // send multipart, with any `body` values riding along as text fields.
180
- const bodyOpt: Record<string, unknown> = {};
222
+ // Map the wire format to a fetch request body (#345): json → JSON string (application/json,
223
+ // default), form → URL-encoded string, multipart → FormData. A `files` block is a real upload:
224
+ // read each fixture into a Blob part and send multipart, with any `body` values riding along
225
+ // as text fields. (Typed as `any`, not fetch's DOM-only `BodyInit`: this package's tsconfig has
226
+ // no "dom" lib, so that alias isn't in scope even though the global runtime values are.)
227
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- see comment above
228
+ let fetchBody: any;
181
229
  if (req.bodyFile) {
182
230
  // Raw-binary body: the whole request body is the fixture's bytes. Content-Type comes from the
183
231
  // spec's mimeType (default application/octet-stream) — this is what octet-stream upload
@@ -186,40 +234,70 @@ class ApiClient {
186
234
  const fpath = substituteRaw(String(spec.path), params);
187
235
  if (fpath) {
188
236
  const resolved = resolveFixture(fpath, label);
189
- bodyOpt.data = fs.readFileSync(resolved);
237
+ fetchBody = fs.readFileSync(resolved);
190
238
  // Own the Content-Type: drop any datasource default (case-insensitive) so we don't send both.
191
239
  deleteHeader(headers, 'content-type');
192
240
  headers['content-type'] = spec.mimeType ?? 'application/octet-stream';
193
241
  }
194
242
  } else if (req.files && Object.keys(req.files).length) {
195
- // Playwright generates the multipart/form-data Content-Type (with boundary); drop any datasource
196
- // default Content-Type so it doesn't conflict with / duplicate the generated one.
243
+ // fetch derives the multipart/form-data Content-Type (with boundary) from the FormData body;
244
+ // drop any datasource default Content-Type so it doesn't conflict with / duplicate it.
197
245
  deleteHeader(headers, 'content-type');
198
- bodyOpt.multipart = buildMultipart(req.files, body, params, label);
246
+ fetchBody = buildMultipart(req.files, body, params, label);
199
247
  } else if (body !== undefined) {
200
248
  const enc = req.encoding ?? 'json';
201
- if (enc === 'form') bodyOpt.form = body;
202
- else if (enc === 'multipart') bodyOpt.multipart = body;
203
- else bodyOpt.data = body;
249
+ if (enc === 'form') {
250
+ if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/x-www-form-urlencoded';
251
+ fetchBody = new URLSearchParams(body as Record<string, string>).toString();
252
+ } else if (enc === 'multipart') {
253
+ deleteHeader(headers, 'content-type');
254
+ fetchBody = buildMultipart({}, body, params, label); // no file fields — text fields only
255
+ } else {
256
+ if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json';
257
+ fetchBody = JSON.stringify(body);
258
+ }
259
+ }
260
+
261
+ // GET/HEAD requests may not carry a body under fetch (undici throws). Refuse LOUDLY rather than
262
+ // send a request quietly shorter than the catalog says it is — a dropped body reads as a server
263
+ // bug, not an authoring one. `validateApiEntry` catches this at generate time; this is the
264
+ // backstop for a catalog that reached the runtime some other way.
265
+ if (fetchBody !== undefined && /^(GET|HEAD)$/i.test(req.method)) {
266
+ throw new Error(`API Driver: ${label} — a ${req.method.toUpperCase()} request cannot carry a body. Remove body:/files:/bodyFile: from the catalog entry, or change its method to POST/PUT/PATCH.`);
267
+ }
268
+
269
+ // Full URL (not a baseURL-relative path) so a base path component like /api/v3 is preserved.
270
+ const fullUrl = joinApiUrl(base, urlPath);
271
+
272
+ // @hybrid: `storageState` is the @auth role's saved session (Playwright's `context.storageState()`
273
+ // JSON) — reuse its cookies (scoped to `fullUrl`'s domain/path) so the request shares the
274
+ // browser's authenticated session without a browser/Playwright context.
275
+ if (opts.storageState) {
276
+ const cookieHeader = loadStorageStateCookieHeader(opts.storageState, fullUrl);
277
+ if (cookieHeader) {
278
+ const existingKey = Object.keys(headers).find((k) => k.toLowerCase() === 'cookie');
279
+ headers[existingKey || 'cookie'] = existingKey ? `${headers[existingKey]}; ${cookieHeader}` : cookieHeader;
280
+ }
204
281
  }
205
282
 
206
- // Playwright APIRequestContext: same runner/report/retries as UI tests. @hybrid passes
207
- // `storageState` (the @auth role's saved session) so the request shares the browser's
208
- // authenticated cookies. Disposed per call so no request context lingers and hangs the process.
209
- const ctx: APIRequestContext = await request.newContext({
210
- extraHTTPHeaders: headers,
211
- timeout: req.timeout ?? conf.timeout_ms ?? 15000, // per-endpoint override → datasource → 15s
212
- ...(opts.storageState ? { storageState: opts.storageState } : {}),
213
- });
283
+ // Timeout: fetch has no built-in per-request timeout, so abort it manually — mirrors the prior
284
+ // per-endpoint-override datasource 15s default precedence.
285
+ const timeoutMs = req.timeout ?? conf.timeout_ms ?? 15000;
286
+ const controller = new AbortController();
287
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
214
288
  try {
215
- // Full URL (not a baseURL-relative path) so a base path component like /api/v3 is preserved.
216
- const res = await ctx.fetch(joinApiUrl(base, urlPath), { method: req.method, ...bodyOpt });
289
+ const res = await fetch(fullUrl, {
290
+ method: req.method,
291
+ headers,
292
+ ...(fetchBody !== undefined ? { body: fetchBody } : {}),
293
+ signal: controller.signal,
294
+ });
217
295
  const text = await res.text();
218
296
  let parsed: any = text;
219
297
  try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
220
- return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
298
+ return { status: res.status, ok: res.ok, body: parsed, headers: Object.fromEntries(res.headers.entries()) };
221
299
  } finally {
222
- await ctx.dispose();
300
+ clearTimeout(timer);
223
301
  }
224
302
  }
225
303
 
@@ -9,9 +9,12 @@
9
9
  * Engines: PostgreSQL (`pg`), MySQL (`mysql2`), and SQLite (`better-sqlite3`), lazy-loaded on first use.
10
10
  * Config: a `datasources.yaml` at the project root (or qa/), with ${VAR} resolved from env.
11
11
  *
12
+ * Assertions are plain thrown Errors — no `@playwright/test` import — so this helper loads in
13
+ * mobile (Appium/Mocha) specs too, which carry no Playwright dependency (same reason
14
+ * `specs/api.ts` runs on Node's global `fetch`).
15
+ *
12
16
  * DO NOT EDIT — regenerated by `sungen generate`.
13
17
  */
14
- import { expect } from '@playwright/test';
15
18
  import * as fs from 'fs';
16
19
  import * as path from 'path';
17
20
 
@@ -647,12 +650,13 @@ class DataSource {
647
650
  async assertRow(table: string, filter: Record<string, any>, expected?: Record<string, any>, datasource?: string): Promise<void> {
648
651
  const { engine } = await this.engine(datasource);
649
652
  const rows = await engine.findRows(table, filter, 50);
650
- expect(rows.length, `Expected a row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBeGreaterThanOrEqual(1);
653
+ assert(rows.length >= 1, `Expected a row in "${table}" where ${desc(filter)} — found ${rows.length}`);
651
654
  if (expected) {
652
655
  const row = rows[0];
653
656
  for (const [col, val] of Object.entries(expected)) {
654
657
  ident(col);
655
- expect(String(row[col]), `Column "${col}" of "${table}" where ${desc(filter)}`).toBe(String(val));
658
+ assert(String(row[col]) === String(val),
659
+ `Column "${col}" of "${table}" where ${desc(filter)} — expected ${JSON.stringify(String(val))}, found ${JSON.stringify(String(row[col]))}`);
656
660
  }
657
661
  }
658
662
  }
@@ -661,7 +665,7 @@ class DataSource {
661
665
  async assertNoRow(table: string, filter: Record<string, any>, datasource?: string): Promise<void> {
662
666
  const { engine } = await this.engine(datasource);
663
667
  const rows = await engine.findRows(table, filter, 50);
664
- expect(rows.length, `Expected NO row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBe(0);
668
+ assert(rows.length === 0, `Expected NO row in "${table}" where ${desc(filter)} — found ${rows.length}`);
665
669
  }
666
670
 
667
671
  /** Exactly `count` rows must match `filter`. */
@@ -669,7 +673,7 @@ class DataSource {
669
673
  const { engine } = await this.engine(datasource);
670
674
  const n = await engine.countRows(table, filter);
671
675
  const cols = Object.keys(filter);
672
- expect(n, `Expected ${count} row(s) in "${table}"${cols.length ? ' where ' + desc(filter) : ''} — found ${n}`).toBe(Number(count));
676
+ assert(n === Number(count), `Expected ${count} row(s) in "${table}"${cols.length ? ' where ' + desc(filter) : ''} — found ${n}`);
673
677
  }
674
678
 
675
679
  /**
@@ -694,6 +698,16 @@ class DataSource {
694
698
  }
695
699
  }
696
700
 
701
+ /**
702
+ * Fail a DB assertion. A thrown Error is exactly what BOTH runners report as a test failure
703
+ * (Playwright and Mocha/WDIO), so this replaces Playwright's `expect(...)` without importing it —
704
+ * the coupling that kept `specs/db.ts` out of mobile specs. Messages carry expected AND found,
705
+ * since there is no matcher to print them.
706
+ */
707
+ function assert(cond: boolean, message: string): void {
708
+ if (!cond) throw new Error(`[sungen] ${message}`);
709
+ }
710
+
697
711
  function desc(filter: Record<string, any>): string {
698
712
  return Object.entries(filter).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ');
699
713
  }