@sun-asterisk/sungen 3.2.18 → 3.2.20

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 (264) 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 +10 -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 +15 -6
  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/test-data-resolver.d.ts +15 -0
  67. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  68. package/dist/exporters/test-data-resolver.js +34 -1
  69. package/dist/exporters/test-data-resolver.js.map +1 -1
  70. package/dist/exporters/types.d.ts +9 -8
  71. package/dist/exporters/types.d.ts.map +1 -1
  72. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +2 -0
  73. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  74. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -0
  75. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  76. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  77. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
  78. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -0
  79. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  80. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  81. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +6 -0
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
  83. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
  84. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
  85. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
  86. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +4 -0
  87. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
  88. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
  89. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +15 -0
  90. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  91. package/dist/generators/test-generator/code-generator.js +78 -7
  92. package/dist/generators/test-generator/code-generator.js.map +1 -1
  93. package/dist/generators/test-generator/diagnostics.d.ts +16 -1
  94. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  95. package/dist/generators/test-generator/diagnostics.js +15 -0
  96. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  97. package/dist/generators/test-generator/recipe-expander.d.ts +1 -1
  98. package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -1
  99. package/dist/generators/test-generator/recipe-expander.js +1 -1
  100. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  101. package/dist/generators/test-generator/step-mapper.d.ts +41 -0
  102. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  103. package/dist/generators/test-generator/step-mapper.js +161 -17
  104. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  105. package/dist/generators/test-generator/template-engine.d.ts +2 -0
  106. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  107. package/dist/generators/test-generator/template-engine.js +1 -1
  108. package/dist/generators/test-generator/template-engine.js.map +1 -1
  109. package/dist/generators/test-generator/utils/data-resolver.d.ts +18 -1
  110. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  111. package/dist/generators/test-generator/utils/data-resolver.js +28 -4
  112. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  113. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +2 -2
  114. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  115. package/dist/generators/test-generator/utils/runtime-data-transformer.js +56 -11
  116. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  117. package/dist/generators/test-generator/utils/selector-resolver.d.ts +18 -2
  118. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  119. package/dist/generators/test-generator/utils/selector-resolver.js +29 -6
  120. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  121. package/dist/harness/audit.d.ts.map +1 -1
  122. package/dist/harness/audit.js +8 -7
  123. package/dist/harness/audit.js.map +1 -1
  124. package/dist/harness/capability-plan.d.ts.map +1 -1
  125. package/dist/harness/capability-plan.js +2 -3
  126. package/dist/harness/capability-plan.js.map +1 -1
  127. package/dist/harness/catalog/drivers.yaml +1 -1
  128. package/dist/harness/challenge.d.ts.map +1 -1
  129. package/dist/harness/challenge.js +1 -55
  130. package/dist/harness/challenge.js.map +1 -1
  131. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  132. package/dist/harness/data-driven-lint.js +14 -9
  133. package/dist/harness/data-driven-lint.js.map +1 -1
  134. package/dist/harness/depth-lint.d.ts.map +1 -1
  135. package/dist/harness/depth-lint.js +1 -3
  136. package/dist/harness/depth-lint.js.map +1 -1
  137. package/dist/harness/flow-check.d.ts.map +1 -1
  138. package/dist/harness/flow-check.js +9 -7
  139. package/dist/harness/flow-check.js.map +1 -1
  140. package/dist/harness/flow-plan.d.ts.map +1 -1
  141. package/dist/harness/flow-plan.js +4 -3
  142. package/dist/harness/flow-plan.js.map +1 -1
  143. package/dist/harness/manifest.js +2 -2
  144. package/dist/harness/manifest.js.map +1 -1
  145. package/dist/harness/parse.d.ts +12 -0
  146. package/dist/harness/parse.d.ts.map +1 -1
  147. package/dist/harness/parse.js +28 -1
  148. package/dist/harness/parse.js.map +1 -1
  149. package/dist/harness/script-check.d.ts.map +1 -1
  150. package/dist/harness/script-check.js +61 -31
  151. package/dist/harness/script-check.js.map +1 -1
  152. package/dist/harness/secret-scan.d.ts +3 -1
  153. package/dist/harness/secret-scan.d.ts.map +1 -1
  154. package/dist/harness/secret-scan.js +27 -11
  155. package/dist/harness/secret-scan.js.map +1 -1
  156. package/dist/harness/sensors.d.ts.map +1 -1
  157. package/dist/harness/sensors.js +15 -4
  158. package/dist/harness/sensors.js.map +1 -1
  159. package/dist/harness/trace.js +1 -1
  160. package/dist/harness/trace.js.map +1 -1
  161. package/dist/harness/unit-paths.d.ts +16 -0
  162. package/dist/harness/unit-paths.d.ts.map +1 -1
  163. package/dist/harness/unit-paths.js +39 -2
  164. package/dist/harness/unit-paths.js.map +1 -1
  165. package/dist/index.d.ts +1 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +3 -1
  168. package/dist/index.js.map +1 -1
  169. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  170. package/dist/orchestrator/screen-manager.js +8 -2
  171. package/dist/orchestrator/screen-manager.js.map +1 -1
  172. package/dist/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
  173. package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  174. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +8 -0
  175. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  176. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +79 -2
  177. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +7 -1
  178. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +12 -3
  179. package/dist/orchestrator/templates/playwright.config.ts +33 -2
  180. package/dist/orchestrator/templates/specs-mock.d.ts +66 -0
  181. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -0
  182. package/dist/orchestrator/templates/specs-mock.js +137 -0
  183. package/dist/orchestrator/templates/specs-mock.js.map +1 -0
  184. package/dist/orchestrator/templates/specs-mock.ts +179 -0
  185. package/dist/orchestrator/templates/specs-url-assert.d.ts +67 -0
  186. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -0
  187. package/dist/orchestrator/templates/specs-url-assert.js +121 -0
  188. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -0
  189. package/dist/orchestrator/templates/specs-url-assert.ts +134 -0
  190. package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
  191. package/dist/orchestrator/test-data-guide.js +4 -0
  192. package/dist/orchestrator/test-data-guide.js.map +1 -1
  193. package/package.json +3 -3
  194. package/src/capabilities/discover.ts +4 -1
  195. package/src/cli/commands/add.ts +4 -2
  196. package/src/cli/commands/dashboard.ts +10 -1
  197. package/src/cli/commands/delivery.ts +19 -1
  198. package/src/cli/commands/generate.ts +9 -1
  199. package/src/dashboard/html-renderer.ts +8 -2
  200. package/src/dashboard/snapshot-builder.ts +3 -0
  201. package/src/dashboard/types.ts +4 -0
  202. package/src/exporters/api-environment.ts +3 -20
  203. package/src/exporters/api-testcase-formatter.ts +8 -2
  204. package/src/exporters/csv-exporter.ts +47 -8
  205. package/src/exporters/env-qa.ts +41 -0
  206. package/src/exporters/feature-parser.ts +17 -38
  207. package/src/exporters/json-exporter.ts +71 -24
  208. package/src/exporters/matrix/build.ts +15 -6
  209. package/src/exporters/matrix/gates.ts +3 -2
  210. package/src/exporters/matrix/step-buckets.ts +15 -0
  211. package/src/exporters/qa-env-secret-denylist.ts +118 -0
  212. package/src/exporters/scenario-merger.ts +80 -42
  213. package/src/exporters/step-formatter.ts +67 -66
  214. package/src/exporters/test-data-resolver.ts +34 -1
  215. package/src/exporters/types.ts +9 -8
  216. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  217. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
  218. package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
  219. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  220. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +6 -0
  221. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
  222. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
  223. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
  224. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
  225. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +4 -0
  226. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
  227. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
  228. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +15 -0
  229. package/src/generators/test-generator/code-generator.ts +91 -8
  230. package/src/generators/test-generator/diagnostics.ts +16 -1
  231. package/src/generators/test-generator/recipe-expander.ts +2 -2
  232. package/src/generators/test-generator/step-mapper.ts +160 -15
  233. package/src/generators/test-generator/template-engine.ts +2 -2
  234. package/src/generators/test-generator/utils/data-resolver.ts +27 -5
  235. package/src/generators/test-generator/utils/runtime-data-transformer.ts +57 -11
  236. package/src/generators/test-generator/utils/selector-resolver.ts +29 -5
  237. package/src/harness/audit.ts +10 -9
  238. package/src/harness/capability-plan.ts +2 -2
  239. package/src/harness/catalog/drivers.yaml +1 -1
  240. package/src/harness/challenge.ts +2 -3
  241. package/src/harness/data-driven-lint.ts +13 -9
  242. package/src/harness/depth-lint.ts +2 -4
  243. package/src/harness/flow-check.ts +9 -7
  244. package/src/harness/flow-plan.ts +4 -3
  245. package/src/harness/manifest.ts +3 -3
  246. package/src/harness/parse.ts +28 -1
  247. package/src/harness/script-check.ts +61 -33
  248. package/src/harness/secret-scan.ts +23 -8
  249. package/src/harness/sensors.ts +15 -4
  250. package/src/harness/trace.ts +2 -2
  251. package/src/harness/unit-paths.ts +39 -2
  252. package/src/index.ts +1 -0
  253. package/src/orchestrator/screen-manager.ts +7 -2
  254. package/src/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
  255. package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  256. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +8 -0
  257. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  258. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +79 -2
  259. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +7 -1
  260. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +12 -3
  261. package/src/orchestrator/templates/playwright.config.ts +33 -2
  262. package/src/orchestrator/templates/specs-mock.ts +179 -0
  263. package/src/orchestrator/templates/specs-url-assert.ts +134 -0
  264. package/src/orchestrator/test-data-guide.ts +4 -0
@@ -45,18 +45,11 @@ function extractReferencedVars(scenario: ParsedScenario, includeComments = false
45
45
  }
46
46
 
47
47
  /**
48
- * Classify each step into Given / When / Then bucket based on its preceding
49
- * explicit keyword. "And" inherits from the previous explicit keyword.
48
+ * Classify each step into its Given / When / Then bucket based on the
49
+ * preceding explicit keyword. "And" / "But" inherit the previous bucket.
50
+ * Chronological order is preserved.
50
51
  */
51
- function classifySteps(scenario: ParsedScenario): {
52
- given: string[];
53
- when: string[];
54
- then: string[];
55
- ordered: OrderedStep[];
56
- } {
57
- const given: string[] = [];
58
- const when: string[] = [];
59
- const then: string[] = [];
52
+ function classifySteps(scenario: ParsedScenario): OrderedStep[] {
60
53
  const ordered: OrderedStep[] = [];
61
54
  let currentBucket: 'given' | 'when' | 'then' = 'given';
62
55
 
@@ -67,14 +60,10 @@ function classifySteps(scenario: ParsedScenario): {
67
60
  else if (keyword === 'Then') currentBucket = 'then';
68
61
  // And / But → inherit currentBucket
69
62
 
70
- if (currentBucket === 'given') given.push(step.text);
71
- else if (currentBucket === 'when') when.push(step.text);
72
- else then.push(step.text);
73
-
74
63
  ordered.push({ text: step.text, bucket: currentBucket });
75
64
  }
76
65
 
77
- return { given, when, then, ordered };
66
+ return ordered;
78
67
  }
79
68
 
80
69
  /**
@@ -86,35 +75,25 @@ export function parseFeatureMetadata(featureFilePath: string): FeatureMetadata {
86
75
 
87
76
  // Background block — classify once and surface on FeatureMetadata so the
88
77
  // merger can prepend it to each runnable scenario without re-parsing.
89
- const bg = parsed.background
78
+ const backgroundOrderedSteps = parsed.background
90
79
  ? classifySteps(parsed.background)
91
- : { given: [], when: [], then: [], ordered: [] as OrderedStep[] };
92
-
93
- const scenarios: ScenarioMetadata[] = parsed.scenarios.map((sc) => {
94
- const { given, when, then, ordered } = classifySteps(sc);
80
+ : [];
95
81
 
96
- return {
97
- name: sc.name,
98
- tags: [...parsed.tags, ...sc.tags],
99
- stepsName: sc.stepsName,
100
- extendsName: sc.extendsName,
101
- referencedVars: extractReferencedVars(sc, extractTestcaseType([...parsed.tags, ...sc.tags]) === 'Manual'),
102
- rawGivenSteps: given,
103
- rawWhenSteps: when,
104
- rawThenSteps: then,
105
- orderedSteps: ordered,
106
- comments: sc.comments,
107
- };
108
- });
82
+ const scenarios: ScenarioMetadata[] = parsed.scenarios.map((sc) => ({
83
+ name: sc.name,
84
+ tags: [...parsed.tags, ...sc.tags],
85
+ stepsName: sc.stepsName,
86
+ extendsName: sc.extendsName,
87
+ referencedVars: extractReferencedVars(sc, extractTestcaseType([...parsed.tags, ...sc.tags]) === 'Manual'),
88
+ orderedSteps: classifySteps(sc),
89
+ comments: sc.comments,
90
+ }));
109
91
 
110
92
  return {
111
93
  featureName: parsed.name,
112
94
  featurePath: parsed.path,
113
95
  featureTags: parsed.tags,
114
- backgroundGivenSteps: bg.given,
115
- backgroundWhenSteps: bg.when,
116
- backgroundThenSteps: bg.then,
117
- backgroundOrderedSteps: bg.ordered,
96
+ backgroundOrderedSteps,
118
97
  scenarios,
119
98
  };
120
99
  }
@@ -14,7 +14,7 @@ import {
14
14
  mapVpToCategory2,
15
15
  splitVpAndName,
16
16
  } from './feature-parser';
17
- import { formatPrecondition, keepActor, stripActor } from './step-formatter';
17
+ import { formatPrecondition, groupExpectedByAnchor, keepActor, stripActor } from './step-formatter';
18
18
  import { formatTestData, substituteTestDataVars } from './test-data-resolver';
19
19
  import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
20
20
  import {
@@ -24,6 +24,7 @@ import {
24
24
  import { ApiCatalogEntry, EnvironmentInfo, PlaywrightResult } from './types';
25
25
  import { SelectorKeyMap, substituteSelectorKeysInStep } from './selector-key-resolver';
26
26
  import { formatVariantRow, resolveResultVariants } from './result-variants';
27
+ import { collectQaEnvSecretDenylist, warnIfRedactionIncomplete } from './qa-env-secret-denylist';
27
28
  import {
28
29
  ScenarioSnapshot,
29
30
  ScreenSnapshot,
@@ -71,24 +72,56 @@ export interface BuildScreenSnapshotInput {
71
72
  export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnapshot {
72
73
  const scenarios: ScenarioSnapshot[] = [];
73
74
  let fallbackIndex = 1;
75
+ // Runtime failure messages embed RESOLVED ${QA_*} credentials, and `errorMessage`
76
+ // feeds the dashboard snapshot + its committed history (never deleted by design) —
77
+ // build the value denylist once per unit so every errorMessage gets scrubbed.
78
+ const qaEnvSecrets = collectQaEnvSecretDenylist(input.testData);
74
79
 
75
80
  for (const m of input.merged) {
76
81
  const priority = extractPriority(m.feature.tags) as ScenarioSnapshot['priority'];
77
82
  const baseType = extractTestcaseType(m.feature.tags);
78
83
  const authRole = extractAuthRole(m.feature.tags);
79
84
 
80
- // Pre-condition is auth-only. Steps section = Background + @extend base
81
- // (all of it, including its Then) + own pre-Then. Expected = own's
82
- // Then-onwards only. Given keepActor (state), When/Then stripActor.
83
- const precondition = formatPrecondition(authRole);
84
- const localize = (text: string): string =>
85
- input.selectorKeyMap
85
+ // Pre-condition = auth line + feature Background steps (kept out of Steps
86
+ // so numbering matches the .feature scenario parity with csv-exporter).
87
+ // Steps section = @extend base (all of it, including its Then) + own
88
+ // pre-Then. Expected = own's Then-onwards only. Given → keepActor (state),
89
+ // When/Then stripActor.
90
+ const localize = (text: string): string => {
91
+ // Same two-step resolution as csv-exporter's localize: 1. resolve [Reference]
92
+ // selector keys → their label/value; 2. resolve {{var}} test-data placeholders →
93
+ // concrete values, so the dashboard shows the identical Steps / Expected text as
94
+ // the delivery sheet. An unknown {{var}} is left literal.
95
+ const withKeys = input.selectorKeyMap
86
96
  ? substituteSelectorKeysInStep(text, input.selectorKeyMap, input.testData)
87
97
  : text;
98
+ return substituteTestDataVars(withKeys, input.testData);
99
+ };
88
100
  const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
89
101
  localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
102
+ const precondition = formatPrecondition(
103
+ authRole,
104
+ (m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
105
+ );
90
106
  let steps = m.resolvedSteps.map(formatStep).filter(Boolean);
91
- let expectedResults = m.resolvedExpected.map(formatStep).filter(Boolean);
107
+ // Same anchor rule as csv-exporter (shared groupExpectedByAnchor) so the
108
+ // dashboard shows the identical Expected text as the delivery sheet.
109
+ const anchorResult = groupExpectedByAnchor(
110
+ m.resolvedExpected
111
+ .map((s) => ({ text: formatStep(s), anchorStep: s.anchorStep }))
112
+ .filter((e) => e.text),
113
+ m.resolvedAnchorActionCount ?? 0
114
+ );
115
+ // Grouped cells carry their own headers + per-group numbering: store as
116
+ // ONE entry so the dashboard XLSX adapter (formatList: 1 entry → raw)
117
+ // reproduces the delivery cell verbatim instead of re-numbering lines.
118
+ let expectedResults = anchorResult.grouped
119
+ ? [anchorResult.lines.join('\n')]
120
+ : anchorResult.lines;
121
+ // Group headers reference Steps numbering; the adapter leaves a
122
+ // single-entry list unnumbered, so bake the "1. " in when a lone action
123
+ // is referenced by headers.
124
+ if (anchorResult.grouped && steps.length === 1) steps = [`1. ${steps[0]}`];
92
125
 
93
126
  // API test cases: mirror csv-exporter's request/response rendering so the
94
127
  // dashboard (UI + XLSX export) shows the same Steps / Expected results as
@@ -97,20 +130,15 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
97
130
  // adapter's formatList() (1 entry → raw) reproduces the delivery cell
98
131
  // byte-for-byte, and the UI renders the block with pre-wrap.
99
132
  if (input.kind === 'api' && input.apiCatalog && hasApiCalls(m.feature.tags)) {
100
- // Delivery cells substitute {{var}} test-data refs (csv-exporter's localize);
101
- // scope that to the API branch so non-API snapshot behavior is untouched.
102
- const localizeApi = (text: string): string => substituteTestDataVars(localize(text), input.testData);
103
- const formatStepApi = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
104
- localizeApi(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
105
133
  const joinCell = (lines: string[]): string =>
106
134
  lines.length === 0 ? '' :
107
135
  lines.length === 1 ? lines[0] :
108
136
  lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
109
- let stepsCell = joinCell(m.resolvedSteps.map(formatStepApi).filter(Boolean));
110
- let expectedCell = joinCell(m.resolvedExpected.map(formatStepApi).filter(Boolean));
111
- const apiRequest = formatApiRequest(m.feature.tags, input.apiCatalog, localizeApi);
137
+ let stepsCell = joinCell(m.resolvedSteps.map(formatStep).filter(Boolean));
138
+ let expectedCell = joinCell(m.resolvedExpected.map(formatStep).filter(Boolean));
139
+ const apiRequest = formatApiRequest(m.feature.tags, input.apiCatalog, localize);
112
140
  if (apiRequest) stepsCell = stepsCell ? `${stepsCell}\n\n${apiRequest}` : apiRequest;
113
- const apiExpected = formatApiExpected(m.resolvedExpected, m.feature.tags, localizeApi);
141
+ const apiExpected = formatApiExpected(m.resolvedExpected, m.feature.tags, localize);
114
142
  if (apiExpected) expectedCell = apiExpected;
115
143
  // Same value-based redaction backstop as the CSV path.
116
144
  const secrets = collectSecretValues(input.testData);
@@ -121,9 +149,19 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
121
149
  steps = stepsCell ? [stepsCell] : [];
122
150
  expectedResults = expectedCell ? [expectedCell] : [];
123
151
  }
152
+ // Runtime-secret pass over EVERY kind's content cells (parity with csv-exporter):
153
+ // value-based only, so demo values / error-message strings / element names stay
154
+ // readable — only a resolved sensitive ${QA_*} value that reached cell content is erased.
155
+ if (qaEnvSecrets.values.length > 0) {
156
+ steps = steps.map((s) => redactSecrets(s, qaEnvSecrets.values));
157
+ expectedResults = expectedResults.map((s) => redactSecrets(s, qaEnvSecrets.values));
158
+ }
124
159
  // Dashboard modal uses whitespace-pre-wrap — pass '\n' to render one
125
160
  // key/value per line instead of the CSV-friendly "; " join.
126
- const baseTestDataStr = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
161
+ const baseTestDataStr = redactSecrets(
162
+ formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n'),
163
+ qaEnvSecrets.values,
164
+ );
127
165
 
128
166
  // @cases scenarios expand to one snapshot entry per executed dataset row — and manual
129
167
  // @cases split per DATASET row (shared with the CSV exporter via resolveResultVariants),
@@ -132,7 +170,8 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
132
170
  for (const variant of variants) {
133
171
  // Per-line Test Data: shared vars + THIS variant's dataset row (parity with csv-exporter).
134
172
  let testDataStr = baseTestDataStr;
135
- const variantRow = formatVariantRow(variant, '\n');
173
+ // A @cases dataset row can carry the same values — redact it like the shared part.
174
+ const variantRow = redactSecrets(formatVariantRow(variant, '\n'), qaEnvSecrets.values);
136
175
  if (variantRow) testDataStr = testDataStr ? `${testDataStr}\n${variantRow}` : variantRow;
137
176
  const displayName = `${m.feature.name}${variant.nameSuffix}`;
138
177
  const { vpId, category1 } = splitVpAndName(displayName);
@@ -164,11 +203,14 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
164
203
  // mid-selector. Drop the stack trace (frames start with "at "), collapse the remaining
165
204
  // newlines (incl. the Expected/Received lines) to spaces, and strip a leading "Error:"
166
205
  // so consumers (dashboard / CSV `Note`) can prepend their own label.
167
- errorMessage = String(result.error)
168
- .split(/\n\s*at\s/)[0]
169
- .replace(/\s*\n\s*/g, ' ')
170
- .trim()
171
- .replace(/^Error:\s*/i, '');
206
+ errorMessage = redactSecrets(
207
+ String(result.error)
208
+ .split(/\n\s*at\s/)[0]
209
+ .replace(/\s*\n\s*/g, ' ')
210
+ .trim()
211
+ .replace(/^Error:\s*/i, ''),
212
+ qaEnvSecrets.values,
213
+ );
172
214
  }
173
215
  if (result.tracePath) tracePath = result.tracePath;
174
216
  testEnvironment = formatTestEnvironment(input.env.baseURL, input.featurePath, result.projectName || input.env.projectName);
@@ -208,6 +250,11 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
208
250
  }
209
251
  }
210
252
 
253
+ // A sensitive ${QA_*} ref that resolved to nothing here can still appear RESOLVED in
254
+ // failure text (the run may have happened in another shell, e.g. CI artifacts exported
255
+ // locally) — never export silently unredacted.
256
+ warnIfRedactionIncomplete(qaEnvSecrets, scenarios.some((s) => s.status === 'Failed'), input.screen);
257
+
211
258
  return {
212
259
  name: input.screen,
213
260
  isFlow: input.isFlow,
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { FeatureMetadata, PlaywrightResult } from '../types';
9
+ import { bucketTexts } from './step-buckets';
9
10
  import { MergedScenario } from '../scenario-merger';
10
11
  import {
11
12
  extractAuthRole,
@@ -14,6 +15,7 @@ import {
14
15
  splitVpAndName,
15
16
  } from '../feature-parser';
16
17
  import { getCasesDatasetRows, resolveResultVariants } from '../result-variants';
18
+ import { renderDynamicTokens } from '../test-data-resolver';
17
19
  import { classifyManualComments, renderAction, renderExpected, renderPrecondition, renderSetupInstruction, runtimeVarsOf } from './wording';
18
20
  import { scenarioFingerprint, combinedFingerprint, mapContentFingerprint } from './fingerprint';
19
21
  import {
@@ -126,9 +128,9 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
126
128
  : null;
127
129
 
128
130
  // Raw (pre-wording) step texts — these feed shapes + Gate C, never the cells.
129
- const rawTrigger = manual ? manual.actions : m.feature.rawWhenSteps;
131
+ const rawTrigger = manual ? manual.actions : bucketTexts(m.feature.orderedSteps, 'when');
130
132
  const rawOracle = manual
131
- ? (manual.expected.length > 0 ? manual.expected : m.feature.rawThenSteps)
133
+ ? (manual.expected.length > 0 ? manual.expected : bucketTexts(m.feature.orderedSteps, 'then'))
132
134
  : m.resolvedExpected.filter((s) => s.bucket === 'then').map((s) => s.text);
133
135
 
134
136
  // Values the scenario itself produces during the run — captured from the page
@@ -142,7 +144,7 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
142
144
  const preconditionProfile = [
143
145
  authRole ?? '-',
144
146
  m.feature.extendsName ?? '-',
145
- ...m.feature.rawGivenSteps.map(normalizeShape),
147
+ ...bucketTexts(m.feature.orderedSteps, 'given').map(normalizeShape),
146
148
  ...(manual?.preconditions ?? []).map(normalizeShape),
147
149
  ].join(' | ');
148
150
  // Display precondition = auth state + Background Given (shared start state) +
@@ -150,8 +152,8 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
150
152
  // scenario often repeats the Background navigation as its own Given).
151
153
  const precondition = Array.from(new Set([
152
154
  ...(authRole ? [authRole === 'no-auth' ? 'The user is signed out.' : `The user is signed in as ${authRole}.`] : []),
153
- ...inputs.feature.backgroundGivenSteps.map(renderPrecondition),
154
- ...m.feature.rawGivenSteps.map(renderPrecondition),
155
+ ...bucketTexts(inputs.feature.backgroundOrderedSteps, 'given').map(renderPrecondition),
156
+ ...bucketTexts(m.feature.orderedSteps, 'given').map(renderPrecondition),
155
157
  ...(manual?.preconditions ?? []).map((t) => renderSetupInstruction(t)),
156
158
  ]));
157
159
 
@@ -264,7 +266,7 @@ function substituteDisplayVars(
264
266
  // `remember … as {{v}}` DECLARES v — leave that token for renderAction, which
265
267
  // turns it into a quoted name ("note it as …"), not a value to look up.
266
268
  const isCapture = /\bremember\b/i.test(text);
267
- return text.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (m, key: string, offset: number) => {
269
+ const out = text.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (m, key: string, offset: number) => {
268
270
  if (isCapture && /\bas\s+$/i.test(text.slice(0, offset))) return m;
269
271
  // A value the test produces at run time has no static form — name it so the
270
272
  // tester knows to compare against what the earlier step captured.
@@ -272,6 +274,9 @@ function substituteDisplayVars(
272
274
  if (!(key in vars)) return m; // unknown stays literal → Gate D flags it
273
275
  return displayValue(vars[key]);
274
276
  });
277
+ // A dynamic token written straight into a step (`{{$uuid}}`) never reaches
278
+ // displayValue — name it here so the same wording covers both routes.
279
+ return renderDynamicTokens(out);
275
280
  }
276
281
 
277
282
  /**
@@ -280,6 +285,10 @@ function substituteDisplayVars(
280
285
  * The canonical value is never changed — only its presentation.
281
286
  */
282
287
  export function displayValue(v: string): string {
288
+ // Runtime-generated tokens nested in a value ("Summary intro {{$timestamp}}")
289
+ // are presentation, not canon — name them before the fidelity checks below,
290
+ // which judge the value's real whitespace.
291
+ v = renderDynamicTokens(v);
283
292
  if (v === '') return '(empty)';
284
293
  if (/^\s+$/.test(v)) return `(${v.length} space${v.length > 1 ? 's' : ''})`;
285
294
  if (v !== v.trim()) return `"${v}"`;
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { splitVpAndName, extractTestcaseType } from '../feature-parser';
15
15
  import { mapContentFingerprint } from './fingerprint';
16
+ import { bucketTexts } from './step-buckets';
16
17
  import { CoverageVariant, MatrixFinding, MAX_VARIANTS_PER_ITEM } from './types';
17
18
  import { expandMapRef, BuildInputs } from './build';
18
19
 
@@ -73,7 +74,7 @@ function gateKCategory(ctx: GateContext, findings: MatrixFinding[]): void {
73
74
  function gateASource(ctx: GateContext, findings: MatrixFinding[]): void {
74
75
  const { feature, merged } = ctx.inputs;
75
76
 
76
- if (feature.backgroundWhenSteps.length > 0 || feature.backgroundThenSteps.length > 0) {
77
+ if (bucketTexts(feature.backgroundOrderedSteps, 'when').length > 0 || bucketTexts(feature.backgroundOrderedSteps, 'then').length > 0) {
77
78
  findings.push({
78
79
  gate: 'A', severity: 'error',
79
80
  message: 'Background contains When/Then steps — setup only; move the action/oracle into scenarios',
@@ -247,7 +248,7 @@ function gateDExecutability(ctx: GateContext, findings: MatrixFinding[]): void {
247
248
  for (const g of map.groups) {
248
249
  const vs = g.variants.flatMap((ref) => expandMapRef(ref, variantsByVp));
249
250
  for (const v of vs) {
250
- const hasPrecondition = v.precondition.length > 0 || feature.backgroundGivenSteps.length > 0;
251
+ const hasPrecondition = v.precondition.length > 0 || bucketTexts(feature.backgroundOrderedSteps, 'given').length > 0;
251
252
  if (!hasPrecondition) {
252
253
  findings.push({
253
254
  gate: 'D', severity: 'error', ref: v.ref,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Per-bucket raw step texts, derived from `orderedSteps`.
3
+ *
4
+ * The parser's `rawGiven/When/ThenSteps` and `backgroundGiven/When/ThenSteps`
5
+ * arrays were removed as dead code (0c3f4ca7 — they duplicated what
6
+ * `orderedSteps` already carries) while the matrix module, merged in parallel,
7
+ * still consumed them: main stopped compiling. classifySteps used to build
8
+ * those arrays as exactly this filter+map over the same buckets, so this view
9
+ * is 1:1 with the removed fields.
10
+ */
11
+ import { OrderedStep } from '../types';
12
+
13
+ export function bucketTexts(steps: OrderedStep[], bucket: OrderedStep['bucket']): string[] {
14
+ return steps.filter((s) => s.bucket === bucket).map((s) => s.text);
15
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Denylist of RESOLVED `${QA_*}` secret values, for redacting runtime failure text
3
+ * (the CSV/XLSX `Note` column and the dashboard snapshot's `errorMessage`).
4
+ *
5
+ * A failed assertion's message carries the credential the runtime already substituted
6
+ * (`Expected: "<real password>"`), and delivery lifts that message into files that get
7
+ * committed permanently (qa/deliverables/*.csv, qa/dashboard/history/*.json). Key-based
8
+ * masking can't help there — the secret appears as a free-standing VALUE — so we build a
9
+ * value denylist and erase every occurrence.
10
+ *
11
+ * Selection = env-var names referenced as `${QA_*}` in the unit's test data, INTERSECTED
12
+ * with a sensitive-name signal on the ENV VAR NAME: QA_ADMIN_PASSWORD / QA_ACCESS_TOKEN
13
+ * are denylisted, QA_BASE_URL / QA_ADMIN_EMAIL are not — non-secret values must stay
14
+ * readable so the deliverable keeps its reproduction value.
15
+ *
16
+ * Values resolve like the runtime does (specs-test-data loadEnvQa precedence):
17
+ * shell/CI env highest > .env.qa.<SUNGEN_ENV> > .env.qa. Reading process.env is
18
+ * deliberate — on CI (where deliverables are actually committed) the .env.qa file is
19
+ * gitignored and usually absent; secrets arrive via env, and a parse-file-only denylist
20
+ * would be empty exactly where it matters. `.env.qa.example` (committed placeholders) is
21
+ * never read. This module only BUILDS the denylist — values are erased from output,
22
+ * never rendered into it.
23
+ */
24
+
25
+ import * as fs from 'fs';
26
+ import * as path from 'path';
27
+ // Sensitive-name signal shared with the API exporter's key/body-field classifier —
28
+ // here it is applied to the ENV VAR NAME (QA_ACCESS_TOKEN), not the yaml key.
29
+ import { SENSITIVE_NAME } from './api-testcase-formatter';
30
+
31
+ /** `${QA_*}` env reference — same shape the runtime resolver allowlists (uppercase, QA_ prefix). */
32
+ const QA_ENV_REF = /\$\{(QA_[A-Z0-9_]+)\}/g;
33
+
34
+ export interface QaEnvSecretDenylist {
35
+ /** Resolved secret values to erase, deduped, longest-first (so a longer secret wins over its substring). */
36
+ values: string[];
37
+ /** Sensitive `${QA_*}` names referenced by the unit that resolved to no usable value. */
38
+ unresolved: string[];
39
+ }
40
+
41
+ /**
42
+ * Build the denylist for one unit from its LOADED test-data map (values still carry the
43
+ * literal `${QA_*}` refs — delivery never resolves them into cells). `cwd` is the project
44
+ * root the delivery CLI runs from, where .env.qa files live.
45
+ */
46
+ export function collectQaEnvSecretDenylist(
47
+ testData: Record<string, string>,
48
+ cwd: string = process.cwd(),
49
+ ): QaEnvSecretDenylist {
50
+ const referenced = new Set<string>();
51
+ for (const v of Object.values(testData)) {
52
+ if (typeof v !== 'string') continue;
53
+ for (const m of v.matchAll(QA_ENV_REF)) referenced.add(m[1]);
54
+ }
55
+ const sensitive = [...referenced].filter((n) => SENSITIVE_NAME.test(n));
56
+ if (sensitive.length === 0) return { values: [], unresolved: [] };
57
+
58
+ const fileEnv = readEnvQaFiles(cwd);
59
+ const values: string[] = [];
60
+ const unresolved: string[] = [];
61
+ for (const name of sensitive) {
62
+ const v = process.env[name] !== undefined ? process.env[name] : fileEnv[name];
63
+ // Values shorter than 3 chars are unusable as a denylist entry (they'd mask
64
+ // coincidental text); treat them as unresolved so coverage gaps stay visible.
65
+ if (typeof v === 'string' && v.length >= 3) values.push(v);
66
+ else unresolved.push(name);
67
+ }
68
+ return {
69
+ values: [...new Set(values)].sort((a, b) => b.length - a.length),
70
+ unresolved,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Read .env.qa files into a local map (never mutating process.env), with the runtime's
76
+ * precedence: first file that sets a key wins, and `.env.qa.<SUNGEN_ENV>` is read before
77
+ * the base `.env.qa`. Callers give process.env priority over everything here.
78
+ */
79
+ function readEnvQaFiles(cwd: string): Record<string, string> {
80
+ const out: Record<string, string> = {};
81
+ const env = process.env.SUNGEN_ENV;
82
+ for (const name of [env ? `.env.qa.${env}` : '', '.env.qa']) {
83
+ // .env.qa.example is a committed placeholder file, never a source of real values.
84
+ if (!name || name === '.env.qa.example') continue;
85
+ const p = path.join(cwd, name);
86
+ if (!fs.existsSync(p)) continue;
87
+ for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
88
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
89
+ if (m && out[m[1]] === undefined) out[m[1]] = m[2].replace(/^["']|["']$/g, '');
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /** Keys already warned about, so per-variant exporter loops don't repeat themselves. */
96
+ const warnedUnits = new Set<string>();
97
+
98
+ /**
99
+ * Fail-loud seam: a unit references sensitive `${QA_*}` vars whose values can't be
100
+ * resolved here, AND the run has failures whose messages may embed those very values
101
+ * (resolved by the runtime in a different shell, e.g. CI artifacts exported locally).
102
+ * Silence would mean exporting unredacted secrets without a trace — warn instead.
103
+ */
104
+ export function warnIfRedactionIncomplete(
105
+ denylist: QaEnvSecretDenylist,
106
+ hasFailures: boolean,
107
+ unitLabel: string,
108
+ ): void {
109
+ if (!hasFailures || denylist.unresolved.length === 0) return;
110
+ const key = `${unitLabel}:${denylist.unresolved.join(',')}`;
111
+ if (warnedUnits.has(key)) return;
112
+ warnedUnits.add(key);
113
+ console.warn(
114
+ `⚠ [${unitLabel}] redaction coverage incomplete: ${denylist.unresolved.join(', ')} ` +
115
+ 'referenced in test-data but not resolvable here — failure notes in the deliverable ' +
116
+ 'may contain unredacted secret values. Set the variable(s) in the shell/CI env or .env.qa before exporting.',
117
+ );
118
+ }