@sun-asterisk/sungen 3.2.18 → 3.2.19

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 (249) hide show
  1. package/dist/capabilities/discover.d.ts.map +1 -1
  2. package/dist/capabilities/discover.js +4 -1
  3. package/dist/capabilities/discover.js.map +1 -1
  4. package/dist/cli/commands/add.d.ts.map +1 -1
  5. package/dist/cli/commands/add.js +4 -2
  6. package/dist/cli/commands/add.js.map +1 -1
  7. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  8. package/dist/cli/commands/dashboard.js +10 -1
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/cli/commands/delivery.d.ts.map +1 -1
  11. package/dist/cli/commands/delivery.js +19 -1
  12. package/dist/cli/commands/delivery.js.map +1 -1
  13. package/dist/cli/commands/generate.d.ts.map +1 -1
  14. package/dist/cli/commands/generate.js +4 -1
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/dashboard/html-renderer.d.ts.map +1 -1
  17. package/dist/dashboard/html-renderer.js +7 -2
  18. package/dist/dashboard/html-renderer.js.map +1 -1
  19. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  20. package/dist/dashboard/snapshot-builder.js +3 -0
  21. package/dist/dashboard/snapshot-builder.js.map +1 -1
  22. package/dist/dashboard/types.d.ts +4 -0
  23. package/dist/dashboard/types.d.ts.map +1 -1
  24. package/dist/exporters/api-environment.d.ts.map +1 -1
  25. package/dist/exporters/api-environment.js +4 -24
  26. package/dist/exporters/api-environment.js.map +1 -1
  27. package/dist/exporters/api-testcase-formatter.d.ts +6 -0
  28. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  29. package/dist/exporters/api-testcase-formatter.js +11 -4
  30. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  31. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  32. package/dist/exporters/csv-exporter.js +40 -9
  33. package/dist/exporters/csv-exporter.js.map +1 -1
  34. package/dist/exporters/env-qa.d.ts +13 -0
  35. package/dist/exporters/env-qa.d.ts.map +1 -0
  36. package/dist/exporters/env-qa.js +82 -0
  37. package/dist/exporters/env-qa.js.map +1 -0
  38. package/dist/exporters/feature-parser.d.ts.map +1 -1
  39. package/dist/exporters/feature-parser.js +16 -33
  40. package/dist/exporters/feature-parser.js.map +1 -1
  41. package/dist/exporters/json-exporter.d.ts.map +1 -1
  42. package/dist/exporters/json-exporter.js +57 -20
  43. package/dist/exporters/json-exporter.js.map +1 -1
  44. package/dist/exporters/matrix/build.d.ts.map +1 -1
  45. package/dist/exporters/matrix/build.js +6 -5
  46. package/dist/exporters/matrix/build.js.map +1 -1
  47. package/dist/exporters/matrix/gates.d.ts.map +1 -1
  48. package/dist/exporters/matrix/gates.js +3 -2
  49. package/dist/exporters/matrix/gates.js.map +1 -1
  50. package/dist/exporters/matrix/step-buckets.d.ts +13 -0
  51. package/dist/exporters/matrix/step-buckets.d.ts.map +1 -0
  52. package/dist/exporters/matrix/step-buckets.js +7 -0
  53. package/dist/exporters/matrix/step-buckets.js.map +1 -0
  54. package/dist/exporters/qa-env-secret-denylist.d.ts +43 -0
  55. package/dist/exporters/qa-env-secret-denylist.d.ts.map +1 -0
  56. package/dist/exporters/qa-env-secret-denylist.js +143 -0
  57. package/dist/exporters/qa-env-secret-denylist.js.map +1 -0
  58. package/dist/exporters/scenario-merger.d.ts +20 -13
  59. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  60. package/dist/exporters/scenario-merger.js +55 -30
  61. package/dist/exporters/scenario-merger.js.map +1 -1
  62. package/dist/exporters/step-formatter.d.ts +42 -33
  63. package/dist/exporters/step-formatter.d.ts.map +1 -1
  64. package/dist/exporters/step-formatter.js +68 -68
  65. package/dist/exporters/step-formatter.js.map +1 -1
  66. package/dist/exporters/types.d.ts +9 -8
  67. package/dist/exporters/types.d.ts.map +1 -1
  68. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
  69. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  70. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +1 -0
  71. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  72. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  73. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
  74. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  75. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  76. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  77. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  78. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
  79. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
  80. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
  81. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
  83. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
  84. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
  85. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  86. package/dist/generators/test-generator/code-generator.js +70 -7
  87. package/dist/generators/test-generator/code-generator.js.map +1 -1
  88. package/dist/generators/test-generator/diagnostics.d.ts +7 -1
  89. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  90. package/dist/generators/test-generator/diagnostics.js +6 -0
  91. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  92. package/dist/generators/test-generator/recipe-expander.d.ts +1 -1
  93. package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -1
  94. package/dist/generators/test-generator/recipe-expander.js +1 -1
  95. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  96. package/dist/generators/test-generator/step-mapper.d.ts +17 -0
  97. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  98. package/dist/generators/test-generator/step-mapper.js +108 -17
  99. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  100. package/dist/generators/test-generator/template-engine.d.ts +1 -0
  101. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  102. package/dist/generators/test-generator/template-engine.js +1 -1
  103. package/dist/generators/test-generator/template-engine.js.map +1 -1
  104. package/dist/generators/test-generator/utils/data-resolver.d.ts +7 -1
  105. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  106. package/dist/generators/test-generator/utils/data-resolver.js +8 -4
  107. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  108. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +2 -2
  109. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  110. package/dist/generators/test-generator/utils/runtime-data-transformer.js +56 -11
  111. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  112. package/dist/generators/test-generator/utils/selector-resolver.d.ts +18 -2
  113. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  114. package/dist/generators/test-generator/utils/selector-resolver.js +29 -6
  115. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  116. package/dist/harness/audit.d.ts.map +1 -1
  117. package/dist/harness/audit.js +8 -7
  118. package/dist/harness/audit.js.map +1 -1
  119. package/dist/harness/capability-plan.d.ts.map +1 -1
  120. package/dist/harness/capability-plan.js +2 -3
  121. package/dist/harness/capability-plan.js.map +1 -1
  122. package/dist/harness/catalog/drivers.yaml +1 -1
  123. package/dist/harness/challenge.d.ts.map +1 -1
  124. package/dist/harness/challenge.js +1 -55
  125. package/dist/harness/challenge.js.map +1 -1
  126. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  127. package/dist/harness/data-driven-lint.js +14 -9
  128. package/dist/harness/data-driven-lint.js.map +1 -1
  129. package/dist/harness/depth-lint.d.ts.map +1 -1
  130. package/dist/harness/depth-lint.js +1 -3
  131. package/dist/harness/depth-lint.js.map +1 -1
  132. package/dist/harness/flow-check.d.ts.map +1 -1
  133. package/dist/harness/flow-check.js +9 -7
  134. package/dist/harness/flow-check.js.map +1 -1
  135. package/dist/harness/flow-plan.d.ts.map +1 -1
  136. package/dist/harness/flow-plan.js +4 -3
  137. package/dist/harness/flow-plan.js.map +1 -1
  138. package/dist/harness/manifest.js +2 -2
  139. package/dist/harness/manifest.js.map +1 -1
  140. package/dist/harness/parse.d.ts +12 -0
  141. package/dist/harness/parse.d.ts.map +1 -1
  142. package/dist/harness/parse.js +28 -1
  143. package/dist/harness/parse.js.map +1 -1
  144. package/dist/harness/script-check.d.ts.map +1 -1
  145. package/dist/harness/script-check.js +52 -30
  146. package/dist/harness/script-check.js.map +1 -1
  147. package/dist/harness/secret-scan.d.ts +3 -1
  148. package/dist/harness/secret-scan.d.ts.map +1 -1
  149. package/dist/harness/secret-scan.js +14 -10
  150. package/dist/harness/secret-scan.js.map +1 -1
  151. package/dist/harness/sensors.d.ts.map +1 -1
  152. package/dist/harness/sensors.js +15 -4
  153. package/dist/harness/sensors.js.map +1 -1
  154. package/dist/harness/trace.js +1 -1
  155. package/dist/harness/trace.js.map +1 -1
  156. package/dist/harness/unit-paths.d.ts +16 -0
  157. package/dist/harness/unit-paths.d.ts.map +1 -1
  158. package/dist/harness/unit-paths.js +39 -2
  159. package/dist/harness/unit-paths.js.map +1 -1
  160. package/dist/index.d.ts +1 -0
  161. package/dist/index.d.ts.map +1 -1
  162. package/dist/index.js +3 -1
  163. package/dist/index.js.map +1 -1
  164. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  165. package/dist/orchestrator/screen-manager.js +8 -2
  166. package/dist/orchestrator/screen-manager.js.map +1 -1
  167. package/dist/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
  168. package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  169. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
  170. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
  171. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
  172. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
  173. package/dist/orchestrator/templates/playwright.config.ts +33 -2
  174. package/dist/orchestrator/templates/specs-mock.d.ts +66 -0
  175. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -0
  176. package/dist/orchestrator/templates/specs-mock.js +137 -0
  177. package/dist/orchestrator/templates/specs-mock.js.map +1 -0
  178. package/dist/orchestrator/templates/specs-mock.ts +179 -0
  179. package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
  180. package/dist/orchestrator/test-data-guide.js +4 -0
  181. package/dist/orchestrator/test-data-guide.js.map +1 -1
  182. package/package.json +3 -3
  183. package/src/capabilities/discover.ts +4 -1
  184. package/src/cli/commands/add.ts +4 -2
  185. package/src/cli/commands/dashboard.ts +10 -1
  186. package/src/cli/commands/delivery.ts +19 -1
  187. package/src/cli/commands/generate.ts +4 -1
  188. package/src/dashboard/html-renderer.ts +8 -2
  189. package/src/dashboard/snapshot-builder.ts +3 -0
  190. package/src/dashboard/types.ts +4 -0
  191. package/src/exporters/api-environment.ts +3 -20
  192. package/src/exporters/api-testcase-formatter.ts +8 -2
  193. package/src/exporters/csv-exporter.ts +47 -8
  194. package/src/exporters/env-qa.ts +41 -0
  195. package/src/exporters/feature-parser.ts +17 -38
  196. package/src/exporters/json-exporter.ts +71 -24
  197. package/src/exporters/matrix/build.ts +6 -5
  198. package/src/exporters/matrix/gates.ts +3 -2
  199. package/src/exporters/matrix/step-buckets.ts +15 -0
  200. package/src/exporters/qa-env-secret-denylist.ts +118 -0
  201. package/src/exporters/scenario-merger.ts +80 -42
  202. package/src/exporters/step-formatter.ts +67 -66
  203. package/src/exporters/types.ts +9 -8
  204. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  205. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
  206. package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
  207. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  208. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  209. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
  210. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
  211. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
  212. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
  213. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
  214. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
  215. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
  216. package/src/generators/test-generator/code-generator.ts +82 -7
  217. package/src/generators/test-generator/diagnostics.ts +7 -1
  218. package/src/generators/test-generator/recipe-expander.ts +2 -2
  219. package/src/generators/test-generator/step-mapper.ts +106 -15
  220. package/src/generators/test-generator/template-engine.ts +2 -2
  221. package/src/generators/test-generator/utils/data-resolver.ts +8 -5
  222. package/src/generators/test-generator/utils/runtime-data-transformer.ts +57 -11
  223. package/src/generators/test-generator/utils/selector-resolver.ts +29 -5
  224. package/src/harness/audit.ts +10 -9
  225. package/src/harness/capability-plan.ts +2 -2
  226. package/src/harness/catalog/drivers.yaml +1 -1
  227. package/src/harness/challenge.ts +2 -3
  228. package/src/harness/data-driven-lint.ts +13 -9
  229. package/src/harness/depth-lint.ts +2 -4
  230. package/src/harness/flow-check.ts +9 -7
  231. package/src/harness/flow-plan.ts +4 -3
  232. package/src/harness/manifest.ts +3 -3
  233. package/src/harness/parse.ts +28 -1
  234. package/src/harness/script-check.ts +52 -32
  235. package/src/harness/secret-scan.ts +11 -7
  236. package/src/harness/sensors.ts +15 -4
  237. package/src/harness/trace.ts +2 -2
  238. package/src/harness/unit-paths.ts +39 -2
  239. package/src/index.ts +1 -0
  240. package/src/orchestrator/screen-manager.ts +7 -2
  241. package/src/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
  242. package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  243. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
  244. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
  245. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
  246. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
  247. package/src/orchestrator/templates/playwright.config.ts +33 -2
  248. package/src/orchestrator/templates/specs-mock.ts +179 -0
  249. package/src/orchestrator/test-data-guide.ts +4 -0
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mock = void 0;
4
+ /** Resource types the fail-on-unmocked guard judges; everything else passes through.
5
+ * NOTE: page.route() does NOT intercept WebSocket connections (that is
6
+ * page.routeWebSocket, a separate API) — the shipped posture is fetch/XHR only. */
7
+ const GUARDED_TYPES = new Set(['fetch', 'xhr']);
8
+ class MockRouter {
9
+ constructor() {
10
+ this.state = new WeakMap();
11
+ }
12
+ stateFor(page) {
13
+ let st = this.state.get(page);
14
+ if (!st) {
15
+ st = { counts: new Map(), installed: new Set(), routes: [], guardInstalled: false, passthrough: false, violations: [], backgroundRouteCount: -1, backgroundNames: new Set() };
16
+ this.state.set(page, st);
17
+ }
18
+ return st;
19
+ }
20
+ /** `@mock:passthrough` — unmatched requests reach the real backend for this page/scenario.
21
+ * Also forgives violations already recorded (Background installs + goto run BEFORE the
22
+ * scenario body can set this flag — boot-time requests of an opted-out scenario must not
23
+ * fail its assertNoUnmocked verdict). The compiler additionally arms the flag inside
24
+ * beforeEach via test.info().tags, before the Background installs, so boot requests are
25
+ * not even aborted on the default path. */
26
+ setPassthrough(page) {
27
+ const st = this.stateFor(page);
28
+ st.passthrough = true;
29
+ st.violations.length = 0;
30
+ }
31
+ /** Install one catalog entry. The first install also arms the fail-on-unmocked guard. */
32
+ async install(page, cfg) {
33
+ const st = this.stateFor(page);
34
+ if (!st.guardInstalled) {
35
+ st.guardInstalled = true;
36
+ const guard = async (route) => {
37
+ const req = route.request();
38
+ if (!GUARDED_TYPES.has(req.resourceType()) || st.passthrough)
39
+ return route.fallback();
40
+ // Record + abort — NEVER throw here: a route handler runs outside any test
41
+ // stack, so a throw surfaces as an unhandled rejection (a hard process crash
42
+ // outside @playwright/test, flaky attribution inside it). The compiler emits
43
+ // `await mock.assertNoUnmocked(page)` at the end of every @mock scenario,
44
+ // which fails the test deterministically with the FULL violation list.
45
+ st.violations.push(`${req.method()} ${req.url()}`);
46
+ await route.abort('failed');
47
+ };
48
+ await page.route('**/*', guard); // FIRST → matched last → every named mock wins
49
+ st.routes.push({ pattern: '**/*', handler: guard });
50
+ }
51
+ const pattern = cfg.url.startsWith('re:') ? new RegExp(cfg.url.slice(3)) : cfg.url;
52
+ const handler = async (route) => {
53
+ if (cfg.method !== '*' && route.request().method() !== cfg.method)
54
+ return route.fallback();
55
+ st.counts.set(cfg.name, (st.counts.get(cfg.name) ?? 0) + 1);
56
+ if (cfg.delay_ms)
57
+ await new Promise((r) => setTimeout(r, cfg.delay_ms));
58
+ // A delayed fulfill can race a page close / reset at teardown — that race must
59
+ // not surface as an unhandled rejection from inside a route handler.
60
+ try {
61
+ await route.fulfill({ status: cfg.status, headers: cfg.headers, body: cfg.body });
62
+ }
63
+ catch { /* page closing */ }
64
+ };
65
+ await page.route(pattern, handler);
66
+ st.routes.push({ pattern, handler });
67
+ st.installed.add(cfg.name);
68
+ if (!st.counts.has(cfg.name))
69
+ st.counts.set(cfg.name, 0);
70
+ }
71
+ /** Handled-request count for a mock installed in THIS scenario — throws for a name
72
+ * that was never installed here (an exact 0 on an uninstalled mock must be a loud
73
+ * error, not a passing assertion). */
74
+ callCount(page, name) {
75
+ const st = this.state.get(page);
76
+ if (!st || !st.installed.has(name)) {
77
+ throw new Error(`mock "${name}" was never installed in this scenario — add \`Given [${name}] mock is active\``);
78
+ }
79
+ return st.counts.get(name) ?? 0;
80
+ }
81
+ /** Fail-on-unmocked verdict — emitted by the compiler as the LAST statement of every
82
+ * @mock scenario. Throws with the full list of aborted unmatched requests; a scenario
83
+ * whose app swallowed those failed fetches must not stay green for the wrong reason. */
84
+ async assertNoUnmocked(page) {
85
+ const st = this.state.get(page);
86
+ if (!st || st.violations.length === 0)
87
+ return;
88
+ const list = st.violations.slice();
89
+ st.violations.length = 0;
90
+ throw new Error(`${list.length} unmocked request(s) hit the fail-on-unmocked guard:\n - ${list.join('\n - ')}\n` +
91
+ 'Add entries to mock/mocks.yaml, or opt out with @mock:passthrough.');
92
+ }
93
+ /** @serial only, emitted by the compiler at the END of the Background block (beforeAll):
94
+ * everything installed so far — Background mocks and the guard — persists across the
95
+ * feature's scenarios; reset() removes only what each scenario added on top. Without
96
+ * this, scenario 1's reset would kill the Background mocks and scenarios 2+ would run
97
+ * against the REAL backend with no guard — the silent-green class this driver exists
98
+ * to prevent. */
99
+ snapshotBackground(page) {
100
+ const st = this.stateFor(page);
101
+ st.backgroundRouteCount = st.routes.length;
102
+ st.backgroundNames = new Set(st.installed);
103
+ }
104
+ /** Scoped teardown for the @serial shared page: unroute exactly what THIS scenario
105
+ * installed (Background routes captured by snapshotBackground persist), zero every
106
+ * counter, clear flags/violations. Emitted at the end of each @mock scenario on the
107
+ * serial path; the default per-test path never needs it (page dies with the test). */
108
+ async reset(page) {
109
+ const st = this.state.get(page);
110
+ if (!st)
111
+ return;
112
+ if (st.backgroundRouteCount < 0) {
113
+ // No Background snapshot → drop everything (the pre-snapshot / default behavior).
114
+ for (const r of st.routes) {
115
+ try {
116
+ await page.unroute(r.pattern, r.handler);
117
+ }
118
+ catch { /* page may be closing */ }
119
+ }
120
+ this.state.delete(page);
121
+ return;
122
+ }
123
+ for (const r of st.routes.slice(st.backgroundRouteCount)) {
124
+ try {
125
+ await page.unroute(r.pattern, r.handler);
126
+ }
127
+ catch { /* page may be closing */ }
128
+ }
129
+ st.routes.length = st.backgroundRouteCount;
130
+ st.installed = new Set(st.backgroundNames);
131
+ st.counts = new Map();
132
+ st.violations.length = 0;
133
+ st.passthrough = false;
134
+ }
135
+ }
136
+ exports.mock = new MockRouter();
137
+ //# sourceMappingURL=specs-mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specs-mock.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-mock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,179 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Sungen Mock Driver — runtime network-mocking helper (auto-generated into specs/mock.ts).
4
+ *
5
+ * One module singleton (same import shape as db/api); ALL state lives per-page in a
6
+ * WeakMap<Page, MockState>, so routes and call counters follow the page's lifetime on
7
+ * both isolation paths: a fresh page per test destroys them for free, and the @serial
8
+ * shared page is reset by the compiler-emitted mock.reset(page) at the end of each
9
+ * @mock scenario (scoped — only routes THIS router installed are unrouted; a blanket
10
+ * page.unrouteAll() would also kill Background-installed mocks and third-party routes).
11
+ *
12
+ * Blocking posture (fail-on-unmocked, the project default): the first install() also
13
+ * registers a catch-all route. Playwright matches routes last-registered-first, so
14
+ * registering the catch-all FIRST makes every named mock win over it. The catch-all
15
+ * only judges fetch/XHR/websocket traffic — documents/scripts/styles/images pass, the
16
+ * app shell must still load; what is being isolated is API traffic. An unmatched API
17
+ * request aborts and throws (fails the test) unless the scenario opted out via
18
+ * @mock:passthrough (compiled to mock.setPassthrough(page)).
19
+ *
20
+ * DO NOT EDIT — regenerated by `sungen generate`.
21
+ */
22
+ import type { Page, Route } from '@playwright/test';
23
+
24
+ export interface MockConfig {
25
+ name: string;
26
+ method: string; // HTTP verb or '*'
27
+ url: string; // page.route glob, or 're:<pattern>' for a regex
28
+ status: number;
29
+ headers?: Record<string, string>;
30
+ body?: string; // pre-serialized by the compiler (JSON string)
31
+ delay_ms?: number;
32
+ }
33
+
34
+ interface InstalledRoute {
35
+ pattern: string | RegExp;
36
+ handler: (route: Route) => Promise<void>;
37
+ }
38
+
39
+ interface MockState {
40
+ counts: Map<string, number>; // per-mock handled-request counters
41
+ installed: Set<string>; // names installed in this scenario (callCount guard)
42
+ routes: InstalledRoute[]; // exactly what WE routed (scoped reset)
43
+ guardInstalled: boolean;
44
+ passthrough: boolean;
45
+ violations: string[]; // unmocked requests the guard aborted (fail-on-unmocked)
46
+ /** @serial only: routes[0..N) / names installed by Background (beforeAll) persist across
47
+ * scenarios — reset() removes only what a scenario added on top. -1 = no snapshot
48
+ * (the default per-test path), where reset() drops the whole state. */
49
+ backgroundRouteCount: number;
50
+ backgroundNames: Set<string>;
51
+ }
52
+
53
+ /** Resource types the fail-on-unmocked guard judges; everything else passes through.
54
+ * NOTE: page.route() does NOT intercept WebSocket connections (that is
55
+ * page.routeWebSocket, a separate API) — the shipped posture is fetch/XHR only. */
56
+ const GUARDED_TYPES = new Set(['fetch', 'xhr']);
57
+
58
+ class MockRouter {
59
+ private state = new WeakMap<Page, MockState>();
60
+
61
+ private stateFor(page: Page): MockState {
62
+ let st = this.state.get(page);
63
+ if (!st) {
64
+ st = { counts: new Map(), installed: new Set(), routes: [], guardInstalled: false, passthrough: false, violations: [], backgroundRouteCount: -1, backgroundNames: new Set() };
65
+ this.state.set(page, st);
66
+ }
67
+ return st;
68
+ }
69
+
70
+ /** `@mock:passthrough` — unmatched requests reach the real backend for this page/scenario.
71
+ * Also forgives violations already recorded (Background installs + goto run BEFORE the
72
+ * scenario body can set this flag — boot-time requests of an opted-out scenario must not
73
+ * fail its assertNoUnmocked verdict). The compiler additionally arms the flag inside
74
+ * beforeEach via test.info().tags, before the Background installs, so boot requests are
75
+ * not even aborted on the default path. */
76
+ setPassthrough(page: Page): void {
77
+ const st = this.stateFor(page);
78
+ st.passthrough = true;
79
+ st.violations.length = 0;
80
+ }
81
+
82
+ /** Install one catalog entry. The first install also arms the fail-on-unmocked guard. */
83
+ async install(page: Page, cfg: MockConfig): Promise<void> {
84
+ const st = this.stateFor(page);
85
+ if (!st.guardInstalled) {
86
+ st.guardInstalled = true;
87
+ const guard = async (route: Route) => {
88
+ const req = route.request();
89
+ if (!GUARDED_TYPES.has(req.resourceType()) || st.passthrough) return route.fallback();
90
+ // Record + abort — NEVER throw here: a route handler runs outside any test
91
+ // stack, so a throw surfaces as an unhandled rejection (a hard process crash
92
+ // outside @playwright/test, flaky attribution inside it). The compiler emits
93
+ // `await mock.assertNoUnmocked(page)` at the end of every @mock scenario,
94
+ // which fails the test deterministically with the FULL violation list.
95
+ st.violations.push(`${req.method()} ${req.url()}`);
96
+ await route.abort('failed');
97
+ };
98
+ await page.route('**/*', guard); // FIRST → matched last → every named mock wins
99
+ st.routes.push({ pattern: '**/*', handler: guard });
100
+ }
101
+
102
+ const pattern = cfg.url.startsWith('re:') ? new RegExp(cfg.url.slice(3)) : cfg.url;
103
+ const handler = async (route: Route) => {
104
+ if (cfg.method !== '*' && route.request().method() !== cfg.method) return route.fallback();
105
+ st.counts.set(cfg.name, (st.counts.get(cfg.name) ?? 0) + 1);
106
+ if (cfg.delay_ms) await new Promise((r) => setTimeout(r, cfg.delay_ms));
107
+ // A delayed fulfill can race a page close / reset at teardown — that race must
108
+ // not surface as an unhandled rejection from inside a route handler.
109
+ try { await route.fulfill({ status: cfg.status, headers: cfg.headers, body: cfg.body }); } catch { /* page closing */ }
110
+ };
111
+ await page.route(pattern, handler);
112
+ st.routes.push({ pattern, handler });
113
+ st.installed.add(cfg.name);
114
+ if (!st.counts.has(cfg.name)) st.counts.set(cfg.name, 0);
115
+ }
116
+
117
+ /** Handled-request count for a mock installed in THIS scenario — throws for a name
118
+ * that was never installed here (an exact 0 on an uninstalled mock must be a loud
119
+ * error, not a passing assertion). */
120
+ callCount(page: Page, name: string): number {
121
+ const st = this.state.get(page);
122
+ if (!st || !st.installed.has(name)) {
123
+ throw new Error(`mock "${name}" was never installed in this scenario — add \`Given [${name}] mock is active\``);
124
+ }
125
+ return st.counts.get(name) ?? 0;
126
+ }
127
+
128
+ /** Fail-on-unmocked verdict — emitted by the compiler as the LAST statement of every
129
+ * @mock scenario. Throws with the full list of aborted unmatched requests; a scenario
130
+ * whose app swallowed those failed fetches must not stay green for the wrong reason. */
131
+ async assertNoUnmocked(page: Page): Promise<void> {
132
+ const st = this.state.get(page);
133
+ if (!st || st.violations.length === 0) return;
134
+ const list = st.violations.slice();
135
+ st.violations.length = 0;
136
+ throw new Error(
137
+ `${list.length} unmocked request(s) hit the fail-on-unmocked guard:\n - ${list.join('\n - ')}\n` +
138
+ 'Add entries to mock/mocks.yaml, or opt out with @mock:passthrough.');
139
+ }
140
+
141
+ /** @serial only, emitted by the compiler at the END of the Background block (beforeAll):
142
+ * everything installed so far — Background mocks and the guard — persists across the
143
+ * feature's scenarios; reset() removes only what each scenario added on top. Without
144
+ * this, scenario 1's reset would kill the Background mocks and scenarios 2+ would run
145
+ * against the REAL backend with no guard — the silent-green class this driver exists
146
+ * to prevent. */
147
+ snapshotBackground(page: Page): void {
148
+ const st = this.stateFor(page);
149
+ st.backgroundRouteCount = st.routes.length;
150
+ st.backgroundNames = new Set(st.installed);
151
+ }
152
+
153
+ /** Scoped teardown for the @serial shared page: unroute exactly what THIS scenario
154
+ * installed (Background routes captured by snapshotBackground persist), zero every
155
+ * counter, clear flags/violations. Emitted at the end of each @mock scenario on the
156
+ * serial path; the default per-test path never needs it (page dies with the test). */
157
+ async reset(page: Page): Promise<void> {
158
+ const st = this.state.get(page);
159
+ if (!st) return;
160
+ if (st.backgroundRouteCount < 0) {
161
+ // No Background snapshot → drop everything (the pre-snapshot / default behavior).
162
+ for (const r of st.routes) {
163
+ try { await page.unroute(r.pattern, r.handler); } catch { /* page may be closing */ }
164
+ }
165
+ this.state.delete(page);
166
+ return;
167
+ }
168
+ for (const r of st.routes.slice(st.backgroundRouteCount)) {
169
+ try { await page.unroute(r.pattern, r.handler); } catch { /* page may be closing */ }
170
+ }
171
+ st.routes.length = st.backgroundRouteCount;
172
+ st.installed = new Set(st.backgroundNames);
173
+ st.counts = new Map();
174
+ st.violations.length = 0;
175
+ st.passthrough = false;
176
+ }
177
+ }
178
+
179
+ export const mock = new MockRouter();
@@ -1 +1 @@
1
- {"version":3,"file":"test-data-guide.d.ts","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":"AAaA,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgBvD;AAuGD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAoB1D"}
1
+ {"version":3,"file":"test-data-guide.d.ts","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":"AAaA,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgBvD;AA2GD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAoB1D"}
@@ -163,6 +163,10 @@ const ENV_QA_EXAMPLE = `# Secret test data referenced as \${QA_*} in qa/**/test-
163
163
  # Per-environment overrides: .env.qa.<env> + run with SUNGEN_ENV=<env>.
164
164
  #
165
165
  # QA_ADMIN_PASSWORD=
166
+ #
167
+ # Web baseURL for this environment — overrides the static value in
168
+ # playwright.config.ts (declare per env: .env.qa.dev, .env.qa.uat, ...).
169
+ # SUNGEN_BASE_URL=
166
170
  `;
167
171
  /**
168
172
  * Idempotently create qa/TEST-DATA-GUIDE.md and .env.qa.example, and make sure
@@ -1 +1 @@
1
- {"version":3,"file":"test-data-guide.js","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,wCAgBC;AA2GD,4DAoBC;AA7JD;;;;;;;;;GASG;AACH,uCAAyB;AACzB,2CAA6B;AAE7B,oEAAoE;AACpE,SAAgB,cAAc,CAAC,QAAgB;IAC7C,OAAO;QACL,KAAK,QAAQ,+DAA+D;QAC5E,+EAA+E;QAC/E,sBAAsB;QACtB,2EAA2E;QAC3E,4EAA4E;QAC5E,wEAAwE;QACxE,iCAAiC;QACjC,8EAA8E;QAC9E,yEAAyE;QACzE,8EAA8E;QAC9E,0EAA0E;QAC1E,+EAA+E;QAC/E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4FhB,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;CAKtB,CAAC;AAEF;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,GAAW;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC7D,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrE,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACtD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACnD,IAAI,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvF,6EAA6E;QAC7E,oEAAoE;QACpE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1F,EAAE,CAAC,cAAc,CAAC,aAAa,EAAE,iFAAiF,CAAC,CAAC;QACtH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,+EAA+E,EAAE,OAAO,CAAC,CAAC;IAC5H,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"test-data-guide.js","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,wCAgBC;AA+GD,4DAoBC;AAjKD;;;;;;;;;GASG;AACH,uCAAyB;AACzB,2CAA6B;AAE7B,oEAAoE;AACpE,SAAgB,cAAc,CAAC,QAAgB;IAC7C,OAAO;QACL,KAAK,QAAQ,+DAA+D;QAC5E,+EAA+E;QAC/E,sBAAsB;QACtB,2EAA2E;QAC3E,4EAA4E;QAC5E,wEAAwE;QACxE,iCAAiC;QACjC,8EAA8E;QAC9E,yEAAyE;QACzE,8EAA8E;QAC9E,0EAA0E;QAC1E,+EAA+E;QAC/E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4FhB,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;CAStB,CAAC;AAEF;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,GAAW;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC7D,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrE,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACtD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACnD,IAAI,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvF,6EAA6E;QAC7E,oEAAoE;QACpE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1F,EAAE,CAAC,cAAc,CAAC,aAAa,EAAE,iFAAiF,CAAC,CAAC;QACtH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,+EAA+E,EAAE,OAAO,CAAC,CAAC;IAC5H,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.18",
3
+ "version": "3.2.19",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -39,8 +39,8 @@
39
39
  "@babel/types": "^7.28.5",
40
40
  "@cucumber/gherkin": "^37.0.0",
41
41
  "@cucumber/messages": "^31.0.0",
42
- "@sungen/driver-data-factory": "3.2.18",
43
- "@sungen/driver-ui": "3.2.18",
42
+ "@sungen/driver-data-factory": "3.2.19",
43
+ "@sungen/driver-ui": "3.2.19",
44
44
  "chalk": "^5.6.2",
45
45
  "commander": "^14.0.2",
46
46
  "dotenv": "^17.2.3",
@@ -27,7 +27,10 @@ import { LOCAL_DRIVERS, registerCoreCapability } from './builtins';
27
27
  * move to their packages (R5.5/R5.6) they join this list and leave `LOCAL_DRIVERS`; eventually this
28
28
  * becomes a scan of installed `@sungen/driver-*` packages (R5.7).
29
29
  */
30
- const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api', '@sungen/driver-mobile', '@sungen/driver-data-factory'];
30
+ // driver-mock appended at the END: ordering = registration = composition (tie-breaks
31
+ // between equal-priority patterns follow registration order) — inserting mid-list
32
+ // shifts them and shows up as golden diffs.
33
+ const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api', '@sungen/driver-mobile', '@sungen/driver-data-factory', '@sungen/driver-mock'];
31
34
 
32
35
  /**
33
36
  * Bundled (non-opt-in) drivers: shipped as a core dependency (`drivers.yaml` `bundled: true`), so the
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
+ import { featureFilesFor } from '../../harness/unit-paths';
4
5
 
5
6
  export function registerAddCommand(program: Command): void {
6
7
  program
@@ -27,8 +28,9 @@ export function registerAddCommand(program: Command): void {
27
28
  // When --figma is set on an existing screen, skip scaffolding and
28
29
  // just refresh Figma-derived files (spec_figma.md, ui/*.png).
29
30
  const screenDir = path.join(process.cwd(), 'qa', 'screens', options.screen);
30
- const featureFile = path.join(screenDir, 'features', `${options.screen}.feature`);
31
- const screenAlreadyExists = fs.existsSync(featureFile);
31
+ // ANY `.feature` means the screen exists — its file may be named after the --path
32
+ // segment or be a `--feature` sub-feature, not `<screen>.feature`.
33
+ const screenAlreadyExists = featureFilesFor(screenDir, options.screen).some((f) => fs.existsSync(f));
32
34
 
33
35
  if (!(options.figma && screenAlreadyExists)) {
34
36
  const { ScreenManager } = require('../../orchestrator/screen-manager');
@@ -12,6 +12,7 @@ import * as path from 'path';
12
12
  import * as fs from 'fs';
13
13
  import { execSync } from 'child_process';
14
14
  import { EnvironmentInfo } from '../../exporters/types';
15
+ import { resolveWebBaseUrl } from '../../exporters/env-qa';
15
16
  import {
16
17
  buildDashboardSnapshot,
17
18
  DashboardTarget,
@@ -42,11 +43,14 @@ function getEnvironment(cwd: string): EnvironmentInfo {
42
43
  const configPath = path.join(cwd, 'playwright.config.ts');
43
44
  if (fs.existsSync(configPath)) {
44
45
  const c = fs.readFileSync(configPath, 'utf-8');
45
- const baseMatch = c.match(/baseURL:\s*['"]([^'"]+)['"]/);
46
+ // Both config shapes: legacy literal and env-aware resolveBaseUrl('x') wrapper.
47
+ const baseMatch = c.match(/baseURL:\s*(?:resolveBaseUrl\(\s*)?['"]([^'"]+)['"]/);
46
48
  if (baseMatch) baseURL = baseMatch[1];
47
49
  const projMatch = c.match(/name:\s*['"]([^'"]+)['"]/);
48
50
  if (projMatch) projectName = projMatch[1];
49
51
  }
52
+ // Same SUNGEN_BASE_URL override as delivery — the two surfaces must agree.
53
+ baseURL = resolveWebBaseUrl(cwd, baseURL);
50
54
 
51
55
  let executor = process.env.CI_USER || process.env.USER || '';
52
56
  try {
@@ -61,6 +65,7 @@ interface DashboardOptions {
61
65
  noHistory?: boolean;
62
66
  open?: boolean;
63
67
  maxHistory?: string;
68
+ env?: string;
64
69
  }
65
70
 
66
71
  export function registerDashboardCommand(program: Command): void {
@@ -71,9 +76,13 @@ export function registerDashboardCommand(program: Command): void {
71
76
  .option('--no-history', 'Do not persist this run under qa/dashboard/history/')
72
77
  .option('--max-history <n>', `Limit snapshots included in dashboard stats (default: ${DEFAULT_MAX_HISTORY}). Older files are kept on disk.`)
73
78
  .option('--open', 'Open the rendered HTML in the default browser when done')
79
+ .option('--env <env>', 'Build the dashboard for one environment/locale (sets SUNGEN_ENV): picks that env\'s result files, and Trends/Compare only see history snapshots of the same env')
74
80
  .action(async (names: string[], options: DashboardOptions) => {
75
81
  try {
76
82
  const cwd = process.cwd();
83
+ // Same convention as run-test/delivery: SUNGEN_ENV drives result-file
84
+ // resolution, locale pairing, and (new) history scoping downstream.
85
+ if (options.env) process.env.SUNGEN_ENV = options.env;
77
86
 
78
87
  // 1. Scope detection
79
88
  let targets: DashboardTarget[];
@@ -24,7 +24,9 @@ import {
24
24
  import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet } from '../../exporters/xlsx-exporter';
25
25
  import { loadApiCatalog } from '../../exporters/api-catalog-loader';
26
26
  import { resolveApiEnvironment } from '../../exporters/api-environment';
27
+ import { resolveWebBaseUrl } from '../../exporters/env-qa';
27
28
  import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
29
+ import { scanTestDataSecrets } from '../../harness/secret-scan';
28
30
  import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
29
31
  import { mapFilePath } from '../../exporters/matrix/map-loader';
30
32
  import {
@@ -343,11 +345,16 @@ function getEnvironment(cwd: string): EnvironmentInfo {
343
345
  const configPath = path.join(cwd, 'playwright.config.ts');
344
346
  if (fs.existsSync(configPath)) {
345
347
  const c = fs.readFileSync(configPath, 'utf-8');
346
- const baseMatch = c.match(/baseURL:\s*['"]([^'"]+)['"]/);
348
+ // Both config shapes: the legacy literal `baseURL: 'x'` and the env-aware
349
+ // `baseURL: resolveBaseUrl('x')` (the quoted value is the static fallback).
350
+ const baseMatch = c.match(/baseURL:\s*(?:resolveBaseUrl\(\s*)?['"]([^'"]+)['"]/);
347
351
  if (baseMatch) baseURL = baseMatch[1];
348
352
  const projMatch = c.match(/name:\s*['"]([^'"]+)['"]/);
349
353
  if (projMatch) projectName = projMatch[1];
350
354
  }
355
+ // SUNGEN_BASE_URL (shell > .env.qa.<SUNGEN_ENV> > .env.qa) overrides the static
356
+ // value — the Test Environment column must show the host the run actually hit.
357
+ baseURL = resolveWebBaseUrl(cwd, baseURL);
351
358
 
352
359
  let executor = process.env.CI_USER || process.env.USER || '';
353
360
  try {
@@ -894,6 +901,17 @@ export function registerDeliveryCommand(program: Command): void {
894
901
  }
895
902
  }
896
903
 
904
+ // Security S0 — warn (never block) if test-data looks to hold a real secret.
905
+ // Deliverables are COMMITTED files; delivery is the last stop before a
906
+ // hardcoded credential lands in one. Same scan `generate` runs.
907
+ const scanDirs = [...new Set(toExport.map((t) => qaDir(cwd, t)))];
908
+ const secretHits = scanDirs.flatMap((d) => scanTestDataSecrets(d));
909
+ if (secretHits.length) {
910
+ log(`\n⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):`);
911
+ for (const h of secretHits.slice(0, 10)) log(` ${h.file}:${h.line} — ${h.reason}`);
912
+ log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.\n`);
913
+ }
914
+
897
915
  // 3. Export
898
916
  const legacy = options.legacy || options.full;
899
917
  if (!legacy) {
@@ -212,7 +212,10 @@ export function registerGenerateCommand(program: Command): void {
212
212
  if (screenName) scanDirs.push(path.join(process.cwd(), 'qa', 'screens', screenName));
213
213
  else if (flowName) scanDirs.push(path.join(process.cwd(), 'qa', 'flows', flowName));
214
214
  else {
215
- for (const base of ['screens', 'flows']) {
215
+ // qa/api units are yaml sinks too (apis.yaml headers, mock bodies once shared);
216
+ // api/flows nests one level deeper. A file entry in the list is harmless —
217
+ // scanTestDataSecrets only reads known subdirs of each entry.
218
+ for (const base of ['screens', 'flows', 'api', path.join('api', 'flows')]) {
216
219
  const d = path.join(process.cwd(), 'qa', base);
217
220
  if (fs.existsSync(d)) for (const n of fs.readdirSync(d)) scanDirs.push(path.join(d, n));
218
221
  }
@@ -48,8 +48,14 @@ export function buildPayload(
48
48
  historyLimit?: number,
49
49
  ): DashboardPayload {
50
50
  const all = readHistory(cwd, historyLimit);
51
- // Exclude `current` from history if it's been written there already.
52
- const history = all.filter((s) => s.runId !== current.runId);
51
+ // Exclude `current` from history if it's been written there already, and keep
52
+ // ONLY snapshots of the SAME environment: qa/dashboard/history/ accumulates
53
+ // dev/uat/stg runs side by side, and blending them gave Trends one mixed
54
+ // pass-rate line and let Compare diff across environments. Legacy snapshots
55
+ // without the field count as base runs ('').
56
+ const currentEnv = current.environment?.sungenEnv || '';
57
+ const history = all.filter((s) =>
58
+ s.runId !== current.runId && (s.environment?.sungenEnv || '') === currentEnv);
53
59
  return { current, history };
54
60
  }
55
61
 
@@ -541,6 +541,9 @@ function collectEnvironment(cwd: string, env: EnvironmentInfo) {
541
541
  sungenVersion: getPackageVersion(),
542
542
  gitBranch,
543
543
  gitSha,
544
+ // Which environment this snapshot belongs to — dev/uat/stg runs must never
545
+ // blend into one trend line ('' = base run).
546
+ sungenEnv: process.env.SUNGEN_ENV || '',
544
547
  };
545
548
  }
546
549
 
@@ -23,6 +23,10 @@ export interface SnapshotEnvironment {
23
23
  sungenVersion: string;
24
24
  gitBranch?: string;
25
25
  gitSha?: string;
26
+ /** SUNGEN_ENV the snapshot was built under ('' = base run, absent on legacy
27
+ * snapshots). Trends/Compare must never mix runs of different environments,
28
+ * so the payload builder filters history by this field. */
29
+ sungenEnv?: string;
26
30
  }
27
31
 
28
32
  export interface AggregateSummary {
@@ -19,6 +19,9 @@ import * as fs from 'fs';
19
19
  import * as path from 'path';
20
20
  import { parse as parseYaml } from 'yaml';
21
21
  import { ApiCatalogEntry } from './types';
22
+ // .env.qa reading (runtime-precedence, local map) moved to ./env-qa so the web
23
+ // baseURL resolver shares the exact same rules.
24
+ import { buildEnvMap } from './env-qa';
22
25
 
23
26
  interface ApiDatasourceConf {
24
27
  kind?: string;
@@ -26,26 +29,6 @@ interface ApiDatasourceConf {
26
29
  baseUrl?: string;
27
30
  }
28
31
 
29
- /** Read KEY=value lines from one dotenv-style file (quotes stripped, comments ignored). */
30
- function readEnvFile(file: string, into: Record<string, string>): void {
31
- if (!fs.existsSync(file)) return;
32
- for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
33
- const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
34
- if (m && into[m[1]] === undefined) into[m[1]] = m[2].replace(/^["']|["']$/g, '');
35
- }
36
- }
37
-
38
- /** Shell env > .env.qa.<SUNGEN_ENV> > .env.qa — same precedence as the runtime's
39
- * loadEnvQa, but built as a LOCAL map (the exporter must not mutate process.env). */
40
- function buildEnvMap(cwd: string): Record<string, string> {
41
- const map: Record<string, string> = {};
42
- for (const name of [`.env.qa.${process.env.SUNGEN_ENV || ''}`, '.env.qa']) {
43
- if (!name.endsWith('.')) readEnvFile(path.join(cwd, name), map);
44
- }
45
- for (const [k, v] of Object.entries(process.env)) if (v !== undefined) map[k] = v;
46
- return map;
47
- }
48
-
49
32
  function loadDatasources(cwd: string): Record<string, ApiDatasourceConf> {
50
33
  const file = [path.join(cwd, 'datasources.yaml'), path.join(cwd, 'qa', 'datasources.yaml')]
51
34
  .find((f) => fs.existsSync(f));
@@ -295,7 +295,7 @@ function formatBodyFile(bodyFile: unknown, args: Record<string, string>, localiz
295
295
  * test-data keys — catches variants like confirmPassword / new_password /
296
296
  * password_confirmation / access_token / client_secret / otp / passcode.
297
297
  */
298
- const SENSITIVE_NAME = /pass(word|code)?|passwd|pwd|secret|token|otp|api[-_]?key|apikey|credential|authorization/i;
298
+ export const SENSITIVE_NAME = /pass(word|code)?|passwd|pwd|secret|token|otp|api[-_]?key|apikey|credential|authorization/i;
299
299
  function isSensitiveField(key: string): boolean {
300
300
  return SENSITIVE_NAME.test(key);
301
301
  }
@@ -319,7 +319,13 @@ export function collectSecretValues(testData: Record<string, string>): string[]
319
319
  export function redactSecrets(text: string, secrets: string[]): string {
320
320
  if (secrets.length === 0 || !text) return text;
321
321
  let out = text;
322
- for (const s of secrets) out = out.split(s).join('***');
322
+ for (const s of secrets) {
323
+ // Floor lives HERE, not only in the collectors: an empty string would split('')
324
+ // and interleave `***` between every character; a 1-2 char value masks
325
+ // coincidental text. Callers building their own denylist must not bypass it.
326
+ if (!s || s.length < 3) continue;
327
+ out = out.split(s).join('***');
328
+ }
323
329
  return out;
324
330
  }
325
331