@sun-asterisk/sungen 3.2.24-beta.6 → 3.2.25

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 (363) hide show
  1. package/dist/capabilities/context-router.d.ts.map +1 -1
  2. package/dist/capabilities/context-router.js +15 -7
  3. package/dist/capabilities/context-router.js.map +1 -1
  4. package/dist/cli/commands/capability.d.ts.map +1 -1
  5. package/dist/cli/commands/capability.js +6 -48
  6. package/dist/cli/commands/capability.js.map +1 -1
  7. package/dist/cli/commands/changed.d.ts +14 -0
  8. package/dist/cli/commands/changed.d.ts.map +1 -0
  9. package/dist/cli/commands/changed.js +128 -0
  10. package/dist/cli/commands/changed.js.map +1 -0
  11. package/dist/cli/commands/delivery.d.ts.map +1 -1
  12. package/dist/cli/commands/delivery.js +25 -1
  13. package/dist/cli/commands/delivery.js.map +1 -1
  14. package/dist/cli/commands/generate.js +1 -1
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/cli/commands/results-merge.d.ts +19 -0
  17. package/dist/cli/commands/results-merge.d.ts.map +1 -0
  18. package/dist/cli/commands/results-merge.js +191 -0
  19. package/dist/cli/commands/results-merge.js.map +1 -0
  20. package/dist/cli/commands/update.d.ts.map +1 -1
  21. package/dist/cli/commands/update.js +100 -12
  22. package/dist/cli/commands/update.js.map +1 -1
  23. package/dist/cli/index.js +4 -0
  24. package/dist/cli/index.js.map +1 -1
  25. package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
  26. package/dist/exporters/api-catalog-loader.js +10 -3
  27. package/dist/exporters/api-catalog-loader.js.map +1 -1
  28. package/dist/exporters/api-testcase-formatter.d.ts +9 -0
  29. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  30. package/dist/exporters/api-testcase-formatter.js +55 -3
  31. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  32. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  33. package/dist/exporters/csv-exporter.js +10 -1
  34. package/dist/exporters/csv-exporter.js.map +1 -1
  35. package/dist/exporters/json-exporter.d.ts.map +1 -1
  36. package/dist/exporters/json-exporter.js +6 -1
  37. package/dist/exporters/json-exporter.js.map +1 -1
  38. package/dist/exporters/matrix/build.d.ts.map +1 -1
  39. package/dist/exporters/matrix/build.js +10 -0
  40. package/dist/exporters/matrix/build.js.map +1 -1
  41. package/dist/exporters/matrix/export.js +1 -1
  42. package/dist/exporters/matrix/export.js.map +1 -1
  43. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  44. package/dist/exporters/matrix/render-csv.js +5 -1
  45. package/dist/exporters/matrix/render-csv.js.map +1 -1
  46. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  47. package/dist/exporters/matrix/render-xlsx.js +4 -1
  48. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  49. package/dist/exporters/matrix/types.d.ts +7 -0
  50. package/dist/exporters/matrix/types.d.ts.map +1 -1
  51. package/dist/exporters/matrix/types.js.map +1 -1
  52. package/dist/exporters/playwright-report-parser.d.ts +17 -2
  53. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  54. package/dist/exporters/playwright-report-parser.js +68 -8
  55. package/dist/exporters/playwright-report-parser.js.map +1 -1
  56. package/dist/exporters/results-merge.d.ts +46 -0
  57. package/dist/exporters/results-merge.d.ts.map +1 -0
  58. package/dist/exporters/results-merge.js +148 -0
  59. package/dist/exporters/results-merge.js.map +1 -0
  60. package/dist/exporters/selector-key-resolver.d.ts +4 -0
  61. package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
  62. package/dist/exporters/selector-key-resolver.js +101 -1
  63. package/dist/exporters/selector-key-resolver.js.map +1 -1
  64. package/dist/exporters/types.d.ts +8 -1
  65. package/dist/exporters/types.d.ts.map +1 -1
  66. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  67. package/dist/exporters/xlsx-report-builder.js +5 -4
  68. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  69. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  70. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  71. package/dist/generators/gherkin-parser/index.js +52 -1
  72. package/dist/generators/gherkin-parser/index.js.map +1 -1
  73. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
  74. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  75. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
  76. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  77. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  78. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  79. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/biometric-action.hbs +9 -0
  80. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
  81. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  82. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/deep-link-action.hbs +8 -0
  83. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  84. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/location-services-action.hbs +11 -0
  85. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
  86. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
  87. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
  88. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
  89. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
  90. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
  91. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.hbs +5 -0
  92. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  93. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +6 -4
  94. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
  95. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -0
  96. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  97. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +3 -0
  98. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
  99. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
  100. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +302 -29
  101. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
  102. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  103. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  104. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  105. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  106. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  107. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  108. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  109. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  110. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  113. package/dist/generators/test-generator/code-generator.d.ts +6 -0
  114. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  115. package/dist/generators/test-generator/code-generator.js +80 -7
  116. package/dist/generators/test-generator/code-generator.js.map +1 -1
  117. package/dist/generators/test-generator/diagnostics.d.ts +60 -6
  118. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  119. package/dist/generators/test-generator/diagnostics.js +59 -5
  120. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  121. package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
  122. package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
  123. package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
  124. package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
  125. package/dist/generators/test-generator/patterns/index.d.ts +19 -12
  126. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  127. package/dist/generators/test-generator/patterns/index.js +58 -38
  128. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  129. package/dist/generators/test-generator/patterns/types.d.ts +23 -0
  130. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  131. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
  132. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
  133. package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
  134. package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
  135. package/dist/generators/test-generator/recipe-expander.js +9 -1
  136. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  137. package/dist/generators/test-generator/reset-tier.d.ts +68 -0
  138. package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
  139. package/dist/generators/test-generator/reset-tier.js +94 -0
  140. package/dist/generators/test-generator/reset-tier.js.map +1 -0
  141. package/dist/generators/test-generator/step-mapper.d.ts +39 -0
  142. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  143. package/dist/generators/test-generator/step-mapper.js +144 -5
  144. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  145. package/dist/generators/test-generator/template-engine.d.ts +2 -10
  146. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  147. package/dist/generators/test-generator/template-engine.js +19 -2
  148. package/dist/generators/test-generator/template-engine.js.map +1 -1
  149. package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
  150. package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
  151. package/dist/generators/test-generator/utils/format-pattern.js +82 -0
  152. package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
  153. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
  154. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
  155. package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
  156. package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
  157. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
  158. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
  159. package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
  160. package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
  161. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
  162. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  163. package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
  164. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  165. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  166. package/dist/generators/test-generator/utils/selector-resolver.js +24 -1
  167. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  168. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
  169. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
  170. package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
  171. package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
  172. package/dist/harness/annotation-overrides.d.ts +0 -12
  173. package/dist/harness/annotation-overrides.d.ts.map +1 -1
  174. package/dist/harness/annotation-overrides.js +9 -5
  175. package/dist/harness/annotation-overrides.js.map +1 -1
  176. package/dist/harness/audit.d.ts.map +1 -1
  177. package/dist/harness/audit.js +7 -5
  178. package/dist/harness/audit.js.map +1 -1
  179. package/dist/harness/capability-plan.d.ts +2 -1
  180. package/dist/harness/capability-plan.d.ts.map +1 -1
  181. package/dist/harness/capability-plan.js +14 -4
  182. package/dist/harness/capability-plan.js.map +1 -1
  183. package/dist/harness/capability.d.ts +11 -0
  184. package/dist/harness/capability.d.ts.map +1 -1
  185. package/dist/harness/capability.js +8 -0
  186. package/dist/harness/capability.js.map +1 -1
  187. package/dist/harness/catalog/drivers.yaml +5 -0
  188. package/dist/harness/parse.d.ts +1 -0
  189. package/dist/harness/parse.d.ts.map +1 -1
  190. package/dist/harness/parse.js +6 -1
  191. package/dist/harness/parse.js.map +1 -1
  192. package/dist/harness/run-manifest.d.ts +58 -0
  193. package/dist/harness/run-manifest.d.ts.map +1 -0
  194. package/dist/harness/run-manifest.js +264 -0
  195. package/dist/harness/run-manifest.js.map +1 -0
  196. package/dist/harness/script-check.d.ts.map +1 -1
  197. package/dist/harness/script-check.js +6 -2
  198. package/dist/harness/script-check.js.map +1 -1
  199. package/dist/harness/sensors.d.ts.map +1 -1
  200. package/dist/harness/sensors.js +5 -1
  201. package/dist/harness/sensors.js.map +1 -1
  202. package/dist/harness/viewpoint-ledger.d.ts +1 -1
  203. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  204. package/dist/harness/viewpoint-ledger.js +25 -4
  205. package/dist/harness/viewpoint-ledger.js.map +1 -1
  206. package/dist/index.d.ts +1 -0
  207. package/dist/index.d.ts.map +1 -1
  208. package/dist/index.js +5 -1
  209. package/dist/index.js.map +1 -1
  210. package/dist/orchestrator/lockstep-version.d.ts +22 -0
  211. package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
  212. package/dist/orchestrator/lockstep-version.js +58 -0
  213. package/dist/orchestrator/lockstep-version.js.map +1 -0
  214. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
  215. package/dist/orchestrator/mobile-runtime-scaffolder.js +5 -1
  216. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
  217. package/dist/orchestrator/project-deps-sync.d.ts +39 -0
  218. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
  219. package/dist/orchestrator/project-deps-sync.js +175 -0
  220. package/dist/orchestrator/project-deps-sync.js.map +1 -0
  221. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  222. package/dist/orchestrator/project-initializer.js +5 -2
  223. package/dist/orchestrator/project-initializer.js.map +1 -1
  224. package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  225. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  226. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  227. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  228. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  229. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +120 -10
  230. package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  231. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +31 -0
  232. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  233. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
  234. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  235. package/dist/orchestrator/templates/env.appium.example +21 -0
  236. package/dist/orchestrator/templates/specs-api.d.ts +4 -2
  237. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  238. package/dist/orchestrator/templates/specs-api.js +233 -10
  239. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  240. package/dist/orchestrator/templates/specs-api.ts +214 -11
  241. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  242. package/dist/orchestrator/templates/specs-db.js +79 -6
  243. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  244. package/dist/orchestrator/templates/specs-db.ts +70 -7
  245. package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
  246. package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
  247. package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
  248. package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
  249. package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
  250. package/dist/orchestrator/templates/specs-test-data.ts +53 -6
  251. package/dist/orchestrator/templates/wdio.conf.ts +126 -10
  252. package/dist/utils/selector-types.d.ts +1 -1
  253. package/dist/utils/selector-types.d.ts.map +1 -1
  254. package/dist/utils/selector-types.js +1 -0
  255. package/dist/utils/selector-types.js.map +1 -1
  256. package/package.json +8 -6
  257. package/src/capabilities/context-router.ts +13 -5
  258. package/src/cli/commands/capability.ts +4 -38
  259. package/src/cli/commands/changed.ts +104 -0
  260. package/src/cli/commands/delivery.ts +27 -2
  261. package/src/cli/commands/generate.ts +1 -1
  262. package/src/cli/commands/results-merge.ts +153 -0
  263. package/src/cli/commands/update.ts +72 -13
  264. package/src/cli/index.ts +4 -0
  265. package/src/exporters/api-catalog-loader.ts +10 -2
  266. package/src/exporters/api-testcase-formatter.ts +59 -3
  267. package/src/exporters/csv-exporter.ts +11 -2
  268. package/src/exporters/json-exporter.ts +7 -2
  269. package/src/exporters/matrix/build.ts +9 -0
  270. package/src/exporters/matrix/export.ts +1 -1
  271. package/src/exporters/matrix/render-csv.ts +5 -1
  272. package/src/exporters/matrix/render-xlsx.ts +4 -1
  273. package/src/exporters/matrix/types.ts +7 -0
  274. package/src/exporters/playwright-report-parser.ts +68 -9
  275. package/src/exporters/results-merge.ts +150 -0
  276. package/src/exporters/selector-key-resolver.ts +81 -1
  277. package/src/exporters/types.ts +8 -1
  278. package/src/exporters/xlsx-report-builder.ts +5 -4
  279. package/src/generators/gherkin-parser/index.ts +55 -1
  280. package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
  281. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
  282. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  283. package/src/generators/test-generator/adapters/appium/templates/steps/actions/biometric-action.hbs +9 -0
  284. package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
  285. package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  286. package/src/generators/test-generator/adapters/appium/templates/steps/actions/deep-link-action.hbs +8 -0
  287. package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  288. package/src/generators/test-generator/adapters/appium/templates/steps/actions/location-services-action.hbs +11 -0
  289. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
  290. package/src/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
  291. package/src/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
  292. package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
  293. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
  294. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
  295. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.hbs +5 -0
  296. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  297. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +6 -4
  298. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
  299. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -0
  300. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  301. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +3 -0
  302. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
  303. package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
  304. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +302 -29
  305. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
  306. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  307. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  308. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  309. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  310. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  311. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  312. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  313. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  314. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  315. package/src/generators/test-generator/code-generator.ts +86 -7
  316. package/src/generators/test-generator/diagnostics.ts +60 -6
  317. package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
  318. package/src/generators/test-generator/patterns/index.ts +58 -33
  319. package/src/generators/test-generator/patterns/types.ts +36 -0
  320. package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
  321. package/src/generators/test-generator/recipe-expander.ts +9 -1
  322. package/src/generators/test-generator/reset-tier.ts +93 -0
  323. package/src/generators/test-generator/step-mapper.ts +149 -6
  324. package/src/generators/test-generator/template-engine.ts +22 -3
  325. package/src/generators/test-generator/utils/format-pattern.ts +79 -0
  326. package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
  327. package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
  328. package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
  329. package/src/generators/test-generator/utils/selector-resolver.ts +26 -1
  330. package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
  331. package/src/harness/annotation-overrides.ts +10 -5
  332. package/src/harness/audit.ts +7 -5
  333. package/src/harness/capability-plan.ts +13 -6
  334. package/src/harness/capability.ts +16 -0
  335. package/src/harness/catalog/drivers.yaml +5 -0
  336. package/src/harness/parse.ts +7 -1
  337. package/src/harness/run-manifest.ts +254 -0
  338. package/src/harness/script-check.ts +6 -2
  339. package/src/harness/sensors.ts +5 -1
  340. package/src/harness/viewpoint-ledger.ts +26 -5
  341. package/src/index.ts +1 -0
  342. package/src/orchestrator/lockstep-version.ts +46 -0
  343. package/src/orchestrator/mobile-runtime-scaffolder.ts +5 -1
  344. package/src/orchestrator/project-deps-sync.ts +175 -0
  345. package/src/orchestrator/project-initializer.ts +5 -2
  346. package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  347. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  348. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  349. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  350. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  351. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +120 -10
  352. package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  353. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +31 -0
  354. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  355. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
  356. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  357. package/src/orchestrator/templates/env.appium.example +21 -0
  358. package/src/orchestrator/templates/specs-api.ts +214 -11
  359. package/src/orchestrator/templates/specs-db.ts +70 -7
  360. package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
  361. package/src/orchestrator/templates/specs-test-data.ts +53 -6
  362. package/src/orchestrator/templates/wdio.conf.ts +126 -10
  363. package/src/utils/selector-types.ts +1 -0
@@ -16,7 +16,7 @@ import {
16
16
  } from './feature-parser';
17
17
  import { formatPrecondition, groupExpectedByAnchor, keepActor, stripActor } from './step-formatter';
18
18
  import { formatTestData, substituteTestDataVars } from './test-data-resolver';
19
- import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
19
+ import { collectSecretValues, formatApiCallSummaryLines, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
20
20
  import {
21
21
  formatExecutedDate,
22
22
  statusToTestResult,
@@ -99,9 +99,14 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
99
99
  };
100
100
  const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
101
101
  localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
102
+ // Same `@api:` precondition lines as csv-exporter's buildTestCaseRows — the
103
+ // dashboard and the delivery CSV must not disagree about what a row does.
104
+ const apiCallLines = (input.kind === 'screen' || input.kind === 'flow') && input.apiCatalog && hasApiCalls(m.feature.tags)
105
+ ? formatApiCallSummaryLines(m.feature.tags, input.apiCatalog)
106
+ : [];
102
107
  const precondition = formatPrecondition(
103
108
  authRole,
104
- (m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
109
+ [...(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean), ...apiCallLines]
105
110
  );
106
111
  let steps = m.resolvedSteps.map(formatStep).filter(Boolean);
107
112
  // Same anchor rule as csv-exporter (shared groupExpectedByAnchor) so the
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { ApiCatalogEntry, FeatureMetadata, PlaywrightResult } from '../types';
9
9
  import { formatApiCallLines, parseApiCalls } from '../api-testcase-formatter';
10
+ import { collectQaEnvSecretDenylist } from '../qa-env-secret-denylist';
10
11
  import { bucketTexts } from './step-buckets';
11
12
  import { MergedScenario } from '../scenario-merger';
12
13
  import {
@@ -140,6 +141,10 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
140
141
  const { vpId, category1 } = splitVpAndName(m.feature.name);
141
142
  if (!vpId) continue; // Gate A reports it — no variant can be addressed without a VP-id
142
143
  const tags = m.feature.tags;
144
+ // @exploration probes live outside the official suite (the same contract the audit's
145
+ // source-tracing exemption follows) — they never become deliverable coverage variants,
146
+ // so the gates and the map never ask for them.
147
+ if (tags.some((t) => /^@exploration\b/i.test(t))) continue;
143
148
  const mode = extractTestcaseType(tags) === 'Manual' ? 'manual' : 'auto';
144
149
  const authRole = extractAuthRole(tags);
145
150
  // Category segment of the id, whatever the project's scheme:
@@ -513,6 +518,9 @@ export function expandMapRef(ref: string, variantsByVp: Map<string, CoverageVari
513
518
  export function buildMatrix(inputs: BuildInputs): MatrixModel {
514
519
  const { unit, map, transformerVersion } = inputs;
515
520
  const variants = deriveVariants(inputs);
521
+ // Same denylist the testcases CSV applies to its Note column — the matrix error
522
+ // cells render the same failure messages and go into the same client hand-off.
523
+ const secretValues = inputs.testData ? collectQaEnvSecretDenylist(inputs.testData).values : [];
516
524
 
517
525
  const variantsByVp = new Map<string, CoverageVariant[]>();
518
526
  for (const v of variants) {
@@ -612,6 +620,7 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
612
620
  dispositions,
613
621
  requirements,
614
622
  findings,
623
+ secretValues,
615
624
  manifest: {
616
625
  unit,
617
626
  transformerVersion,
@@ -53,7 +53,7 @@ function readSpecText(paths: MatrixTargetPaths): string {
53
53
  : '';
54
54
  }
55
55
 
56
- /** apis.yaml endpoint catalog for an api unit ({} for screen/flow loader no-ops). */
56
+ /** apis.yaml endpoint catalog — resolved for EVERY kind (screen/flow read their unit-scoped catalog + the shared one), so hybrid @api preconditions render their real request contract. */
57
57
  function catalogFor(paths: MatrixTargetPaths): Record<string, ApiCatalogEntry> {
58
58
  return loadApiCatalog(paths.cwd, paths.kind ?? 'screen', paths.area ?? paths.unit);
59
59
  }
@@ -5,6 +5,8 @@
5
5
  * and result), so downstream pipelines can re-derive both views from one file.
6
6
  */
7
7
 
8
+ import { redactSecrets } from '../api-testcase-formatter';
9
+ import { clipNoteField } from '../playwright-report-parser';
8
10
  import { variantState } from './build';
9
11
  import { coverageSummary, itemModeLabel, itemResultLabel } from './render-xlsx';
10
12
  import { MatrixModel } from './types';
@@ -75,7 +77,9 @@ export function renderMatrixCsv(model: MatrixModel): string {
75
77
  v.traces.join(' '),
76
78
  variantState(v),
77
79
  isoDate(v.result?.startTime),
78
- v.result?.error ? String(v.result.error).slice(0, 200) : '',
80
+ // Redact BEFORE clipping a clip boundary must never split a secret in
81
+ // a way that lets its prefix through. Same denylist as the Note column.
82
+ v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
79
83
  '',
80
84
  ].map(esc).join(','));
81
85
  }
@@ -27,6 +27,8 @@ import {
27
27
  BLACK,
28
28
  LAVENDER,
29
29
  } from '../xlsx-report-builder';
30
+ import { redactSecrets } from '../api-testcase-formatter';
31
+ import { clipNoteField } from '../playwright-report-parser';
30
32
  import { variantState } from './build';
31
33
  import { DeliveryItem, MatrixModel, MapCategory } from './types';
32
34
 
@@ -229,7 +231,8 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
229
231
  variantState(v),
230
232
  isoDate(v.result?.startTime),
231
233
  '',
232
- v.result?.error ? String(v.result.error).slice(0, 200) : '',
234
+ // Redact BEFORE clipping same denylist as the testcases CSV Note column.
235
+ v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
233
236
  '',
234
237
  ];
235
238
  vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
@@ -235,6 +235,13 @@ export interface MatrixModel {
235
235
  manifest: MatrixManifest;
236
236
  /** Perf verdict (#569) — present when qa/perf.yaml sets a budget AND the run produced durations. */
237
237
  perf?: import('../../harness/perf').PerfVerdict | null;
238
+ /**
239
+ * Resolved ${QA_*} secret values referenced by this unit's test data. Failure
240
+ * messages quote runtime values (a Received: line can echo a typed password),
241
+ * so every error cell the renderers emit must be redacted against this list —
242
+ * the deliverable ships to the client.
243
+ */
244
+ secretValues: string[];
238
245
  }
239
246
 
240
247
  /** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
@@ -16,14 +16,22 @@ import { PlaywrightResult } from './types';
16
16
  * Candidate order (first existing wins):
17
17
  * 1. <genDir>/<featureBaseName>-test-result.<SUNGEN_ENV>.json (env runs)
18
18
  * 2. <genDir>/<featureBaseName>-test-result.json (per-feature reporter)
19
- * 3. <cwd>/test-results/results.<SUNGEN_ENV>.json
20
- * 4. <cwd>/test-results/results.json (playwright default)
19
+ * 3. <genDir>/<featureBaseName>-test-result.<os>[.<SUNGEN_ENV>].json — mobile per-OS
20
+ * reporter files (wdio writes one per capability; a mobile project has NO base file).
21
+ * MOBILE_PLATFORM picks the OS explicitly; otherwise android, then ios.
22
+ * 4. <cwd>/test-results/results.<SUNGEN_ENV>.json
23
+ * 5. <cwd>/test-results/results.json (playwright default)
21
24
  */
22
25
  export function resolveResultsPath(cwd: string, genDir: string, featureBaseName: string): string | null {
23
26
  const env = process.env.SUNGEN_ENV;
24
27
  const candidates: string[] = [];
25
28
  if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${env}.json`));
26
29
  candidates.push(path.join(genDir, `${featureBaseName}-test-result.json`));
30
+ const mobileOs = (process.env.MOBILE_PLATFORM || '').toLowerCase();
31
+ for (const os of mobileOs === 'android' || mobileOs === 'ios' ? [mobileOs] : ['android', 'ios']) {
32
+ if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${os}.${env}.json`));
33
+ candidates.push(path.join(genDir, `${featureBaseName}-test-result.${os}.json`));
34
+ }
27
35
  if (env) candidates.push(path.join(cwd, 'test-results', `results.${env}.json`));
28
36
  candidates.push(path.join(cwd, 'test-results', 'results.json'));
29
37
  for (const c of candidates) {
@@ -57,6 +65,9 @@ interface PlaywrightSpec {
57
65
  duration?: number; // ms — Playwright emits this on every result (#569 perf budgets)
58
66
  startTime?: string;
59
67
  error?: { message?: string; stack?: string };
68
+ // ALL errors of the attempt. `error` above is only the first one — when a
69
+ // timeout interrupts an assertion, the assertion detail is errors[1+].
70
+ errors?: Array<{ message?: string; stack?: string }>;
60
71
  attachments?: Array<{ name?: string; path?: string; contentType?: string }>;
61
72
  }>;
62
73
  }>;
@@ -110,7 +121,30 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
110
121
  const results = test?.results || [];
111
122
  const res = results[results.length - 1];
112
123
  const status = normalizeStatus(res?.status);
113
- const errorMsg = stripAnsi(res?.error?.message || '');
124
+ // Collect EVERY failure message of the attempt, not just the first: a test
125
+ // timeout wraps the real assertion failure, so `error` alone holds only
126
+ // "Test timeout of 15000ms exceeded." while the Locator/Expected/Received
127
+ // detail QA needs sits in errors[1+]. Stack frames are stripped PER message
128
+ // before joining — otherwise the first message's stack swallows the rest
129
+ // when downstream consumers cut at the first "at " frame.
130
+ const rawMessages = (res?.errors?.length ? res.errors.map((e) => e.message) : [res?.error?.message])
131
+ .map((m) => stripAnsi(m || ''))
132
+ .filter(Boolean);
133
+ const distinct: string[] = [];
134
+ const seenKeys = new Set<string>();
135
+ for (const m of rawMessages) {
136
+ const stripped = m.split(/\n\s*at\s/)[0].trim();
137
+ // Key ignores a leading `Error:` — "Error: page closed" and "page closed"
138
+ // are the same failure and must not render as two Note bullets.
139
+ const key = stripped.replace(/^Error:\s*/i, '');
140
+ if (!key || seenKeys.has(key)) continue;
141
+ seenKeys.add(key);
142
+ distinct.push(stripped);
143
+ }
144
+ // Single message: keep the legacy shape (stack intact) so every existing
145
+ // consumer renders byte-identical output. Multiple: join the stack-stripped
146
+ // distinct messages so none can hide behind another's stack.
147
+ const errorMsg = distinct.length > 1 ? distinct.join('\n') : stripAnsi(res?.error?.message || rawMessages[0] || '');
114
148
  const trace = res?.attachments?.find((a) => a.name === 'trace' || a.contentType === 'application/zip')?.path;
115
149
 
116
150
  // Strip the leading file-path suite title if present.
@@ -124,6 +158,7 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
124
158
  duration: typeof res?.duration === 'number' ? res.duration : undefined,
125
159
  startTime: res?.startTime,
126
160
  error: errorMsg,
161
+ errorMessages: distinct.length > 0 ? distinct : undefined,
127
162
  tracePath: trace,
128
163
  projectName: test?.projectName,
129
164
  });
@@ -178,7 +213,7 @@ function normalizeStatus(s: string | undefined): PlaywrightResult['status'] {
178
213
  * "specs/generated/kudos/kudos.spec.ts > Feature Name > ...",
179
214
  * strip that leading segment so matching works against "Feature Name > ...".
180
215
  */
181
- function stripLeadingFilePathSuite(fullTitle: string): string {
216
+ export function stripLeadingFilePathSuite(fullTitle: string): string {
182
217
  const parts = fullTitle.split(' > ');
183
218
  if (parts.length > 1 && /\.spec\.ts$/i.test(parts[0])) {
184
219
  return parts.slice(1).join(' > ');
@@ -256,11 +291,23 @@ export function parseExpectedReceived(raw: string): { expected?: string; receive
256
291
  export function formatNote(result: PlaywrightResult | undefined, expectedFallback = ''): string {
257
292
  if (!result) return '';
258
293
  if (result.status !== 'failed' && result.status !== 'timedOut') return '';
259
- // Full assertion message with stack frames dropped (frames start with "at ") + a leading `Error:`
260
- // stripped. Keep newlines here so parseExpectedReceived can delimit the Expected/Received lines.
261
- const raw = (result.error || '').split(/\n\s*at\s/)[0].replace(/^Error:\s*/i, '').trim();
262
- const { expected, received } = parseExpectedReceived(raw);
263
- const actual = received || clip(raw); // received value, else the raw error
294
+ // Per-message: stack frames dropped (frames start with "at ") + a leading `Error:` stripped.
295
+ // Keep newlines inside each message so parseExpectedReceived can delimit Expected/Received.
296
+ const messages = (result.errorMessages?.length ? result.errorMessages : [result.error || ''])
297
+ .map((m) => m.split(/\n\s*at\s/)[0].replace(/^Error:\s*/i, '').trim())
298
+ .filter(Boolean);
299
+ // The message that carries the assertion detail drives Expected/Actual; the other
300
+ // messages (e.g. the "Test timeout of 15000ms exceeded." wrapper) must still be
301
+ // shown — a real assertion failure rendered as a bare timeout reads as flaky.
302
+ // Prefer a message carrying BOTH halves: a half-match (Expected in one message,
303
+ // Received in another) would otherwise put the wrong text in Actual. With
304
+ // expect.soft the first assertion drives the split and the later assertion
305
+ // messages render as `• Error:` bullets — content preserved, columns describe #1.
306
+ const halves = (m: string): number => { const er = parseExpectedReceived(m); return (er.expected ? 1 : 0) + (er.received ? 1 : 0); };
307
+ const assertion = messages.find((m) => halves(m) === 2) ?? messages.find((m) => halves(m) >= 1) ?? messages[0] ?? '';
308
+ const wrappers = messages.filter((m) => m !== assertion);
309
+ const { expected, received } = parseExpectedReceived(assertion);
310
+ const actual = received || clip(assertion); // received value, else the raw error
264
311
 
265
312
  // Newline-separated bullets so each field lands on its own row when the cell wraps — matches the
266
313
  // Test Data column's `• key: value` formatting. Excel/CSV honour `\n` inside quoted cells; the
@@ -276,6 +323,18 @@ export function formatNote(result: PlaywrightResult | undefined, expectedFallbac
276
323
  else if (lines.length > 1) parts.push(`• Expected result:\n${lines.map((l) => ` ${l}`).join('\n')}`);
277
324
  }
278
325
  if (actual) parts.push(`• Actual result: ${actual}`);
326
+ // Non-assertion companions of an assertion failure (timeout wrapper etc.), one
327
+ // clipped bullet each so no cell field can blow past the XLSX-safe cap.
328
+ for (const w of wrappers) parts.push(`• Error: ${clip(w)}`);
279
329
  if (result.tracePath) parts.push(`• Trace: ${result.tracePath}`);
280
330
  return parts.join('\n');
281
331
  }
332
+
333
+ /**
334
+ * One-cell error rendering for the matrix deliverable: newlines collapsed and
335
+ * clipped to the same cap as Note fields. Replaces ad-hoc `slice(0, 200)` calls
336
+ * that could cut a joined multi-error message mid-assertion.
337
+ */
338
+ export function clipNoteField(s: string): string {
339
+ return clip(s);
340
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Merge two Playwright JSON reports by FULL TEST TITLE — the fix for subset
3
+ * re-runs: Playwright's JSON reporter overwrites the per-target result file,
4
+ * so re-running the 3 failed cases of a 100-case screen used to throw away
5
+ * the 97 passing results (QA then re-ran everything, or asked an AI to merge
6
+ * by hand — non-deterministic).
7
+ *
8
+ * Semantics (deterministic):
9
+ * - same title in both → the NEW spec wins wholesale (its results, its
10
+ * attempts, its timing);
11
+ * - title only in base → kept as-is;
12
+ * - title only in new → appended (its suite structure preserved).
13
+ * Titles compare AFTER stripping the leading file-path suite segment, so a
14
+ * base and a new report generated from different cwd/suite paths still match.
15
+ * `@cases` rows carry a ` — <label>` title suffix and merge as independent
16
+ * entries — re-running one dataset row keeps the other rows.
17
+ *
18
+ * The merge never deletes: a base title missing from the current spec (a
19
+ * scenario renamed by a spec edit) is a GHOST the caller may WARN about, but
20
+ * pruning is not this function's job — the delivery builds rows from the
21
+ * feature, so ghosts never render; only the dead `stats` block would drift,
22
+ * and it is recomputed below.
23
+ *
24
+ * Everything outside suites/specs/tests is passed through untouched from the
25
+ * BASE report (config, metadata, unknown future fields) — tolerance over
26
+ * strictness, the shape belongs to Playwright.
27
+ */
28
+
29
+ import { stripLeadingFilePathSuite } from './playwright-report-parser';
30
+
31
+ interface SpecRef {
32
+ title: string; // stripped full title
33
+ parent: any[]; // the specs array holding the node
34
+ index: number;
35
+ node: any;
36
+ }
37
+
38
+ export interface MergeOutcome {
39
+ merged: any;
40
+ replaced: string[]; // titles taken from the new report over the base
41
+ added: string[]; // titles only in the new report (appended)
42
+ kept: string[]; // titles only in the base (untouched)
43
+ }
44
+
45
+ /** Collect every spec node with its stripped full title and parent array. */
46
+ function collectSpecs(report: any): SpecRef[] {
47
+ const out: SpecRef[] = [];
48
+ const walk = (suites: any[] | undefined, stack: string[]): void => {
49
+ if (!Array.isArray(suites)) return;
50
+ for (const suite of suites) {
51
+ if (!suite || typeof suite !== 'object') continue;
52
+ const nextStack = typeof suite.title === 'string' && suite.title ? [...stack, suite.title] : stack;
53
+ if (Array.isArray(suite.specs)) {
54
+ suite.specs.forEach((spec: any, index: number) => {
55
+ if (!spec || typeof spec.title !== 'string') return;
56
+ const fullTitle = stripLeadingFilePathSuite([...nextStack, spec.title].join(' > '));
57
+ out.push({ title: fullTitle, parent: suite.specs, index, node: spec });
58
+ });
59
+ }
60
+ walk(suite.suites, nextStack);
61
+ }
62
+ };
63
+ walk(report?.suites, []);
64
+ return out;
65
+ }
66
+
67
+ /** Drop consumed specs from a suite tree; prune suites left with nothing. */
68
+ function pruneConsumed(suites: any[], consumed: Set<any>): any[] {
69
+ const kept: any[] = [];
70
+ for (const suite of suites) {
71
+ if (!suite || typeof suite !== 'object') continue;
72
+ if (Array.isArray(suite.specs)) suite.specs = suite.specs.filter((s: any) => !consumed.has(s));
73
+ if (Array.isArray(suite.suites)) suite.suites = pruneConsumed(suite.suites, consumed);
74
+ const hasSpecs = Array.isArray(suite.specs) && suite.specs.length > 0;
75
+ const hasSuites = Array.isArray(suite.suites) && suite.suites.length > 0;
76
+ if (hasSpecs || hasSuites) kept.push(suite);
77
+ }
78
+ return kept;
79
+ }
80
+
81
+ /** Recompute the report-level stats counters from the merged tree's final attempts. */
82
+ function recomputeStats(merged: any): void {
83
+ if (!merged.stats || typeof merged.stats !== 'object') return;
84
+ let expected = 0, unexpected = 0, skipped = 0, flaky = 0;
85
+ for (const ref of collectSpecs(merged)) {
86
+ for (const test of ref.node.tests ?? []) {
87
+ const results = test?.results ?? [];
88
+ const last = results[results.length - 1];
89
+ const status = last?.status;
90
+ if (status === 'passed') { if (results.some((r: any) => r?.status === 'failed' || r?.status === 'timedOut')) flaky++; else expected++; }
91
+ else if (status === 'skipped') skipped++;
92
+ else if (status) unexpected++;
93
+ }
94
+ }
95
+ merged.stats = { ...merged.stats, expected, unexpected, skipped, flaky };
96
+ }
97
+
98
+ /**
99
+ * Merge `next` into a DEEP COPY of `base` (neither input is mutated).
100
+ * Throws on inputs that are not report-shaped objects.
101
+ */
102
+ export function mergeResultsReports(base: any, next: any): MergeOutcome {
103
+ for (const [label, r] of [['base', base], ['new', next]] as const) {
104
+ if (!r || typeof r !== 'object' || Array.isArray(r)) {
105
+ throw new Error(`results merge: the ${label} report is not a JSON object.`);
106
+ }
107
+ }
108
+ const merged = JSON.parse(JSON.stringify(base));
109
+ const nextCopy = JSON.parse(JSON.stringify(next));
110
+
111
+ // Last occurrence wins within one report — the same tie-break walkSuites uses.
112
+ const newByTitle = new Map<string, SpecRef>();
113
+ for (const ref of collectSpecs(nextCopy)) newByTitle.set(ref.title, ref);
114
+
115
+ const replaced: string[] = [];
116
+ const kept: string[] = [];
117
+ const consumed = new Set<any>();
118
+ for (const ref of collectSpecs(merged)) {
119
+ const incoming = newByTitle.get(ref.title);
120
+ if (incoming) {
121
+ ref.parent[ref.index] = incoming.node; // the new spec wins wholesale
122
+ consumed.add(incoming.node);
123
+ replaced.push(ref.title);
124
+ } else {
125
+ kept.push(ref.title);
126
+ }
127
+ }
128
+
129
+ const added: string[] = [];
130
+ for (const [title, ref] of newByTitle) {
131
+ if (!consumed.has(ref.node)) added.push(title);
132
+ }
133
+ if (added.length > 0 && Array.isArray(nextCopy.suites)) {
134
+ merged.suites = [...(Array.isArray(merged.suites) ? merged.suites : []), ...pruneConsumed(nextCopy.suites, consumed)];
135
+ }
136
+
137
+ recomputeStats(merged);
138
+ return { merged, replaced, added, kept };
139
+ }
140
+
141
+ /**
142
+ * Ghost detection for the WARN (never a prune): base-kept titles that no
143
+ * longer correspond to a CURRENT spec title. A `@cases` report title carries
144
+ * a ` — <label>` suffix while the spec title is the base form, so a ghost is
145
+ * a title that neither equals a spec title nor extends one with that suffix.
146
+ */
147
+ export function findGhostTitles(keptTitles: string[], specTitles: string[]): string[] {
148
+ return keptTitles.filter((title) =>
149
+ !specTitles.some((spec) => title === spec || title.startsWith(`${spec} — `)));
150
+ }
@@ -18,6 +18,10 @@
18
18
  * • `value` for `type: 'role'` (role name like 'button', not display)
19
19
  * • Entries with no `name` and no usable `value`
20
20
  * • Composite templates with 2+ vars (ambiguous which to use)
21
+ * • Locators whose single var is SCOPE text — inside `:has(...)`/`:not(...)`,
22
+ * in a non-final segment, or in an XPath nested predicate / pre-step
23
+ * position (see locatorVarIsScopeText) — substituting those renames the
24
+ * ref to text of a DIFFERENT element (a column header naming a table)
21
25
  */
22
26
 
23
27
  import * as fs from 'fs';
@@ -90,13 +94,89 @@ function resolveField(value: string, isLocator: boolean): SelectorResolution | n
90
94
  }
91
95
  if (vars.length === 1) {
92
96
  // Sole `{{var}}` or composite like `has-text("{{var}}")`. Both resolve
93
- // to the same var's locale-specific display text.
97
+ // to the same var's locale-specific display text — but ONLY when the var
98
+ // is the target element's OWN text. A var inside a `:has(...)` argument
99
+ // or in an ancestor segment names a DIFFERENT element (a column header
100
+ // scoping a table), and substituting it renames the ref to unrelated text.
101
+ if (isLocator && locatorVarIsScopeText(value)) return null;
94
102
  return { varName: vars[0] };
95
103
  }
96
104
  // 2+ vars in one template — ambiguous which to use; skip rather than guess.
97
105
  return null;
98
106
  }
99
107
 
108
+ /**
109
+ * True when a locator's single `{{var}}` (FIRST occurrence — repeats of the
110
+ * same var judge by that one, erring toward fidelity) belongs to a SCOPE
111
+ * rather than the target element's own display text.
112
+ *
113
+ * CSS / Playwright engines:
114
+ * - inside a `:has(...)` or `:not(...)` argument at any depth — that text
115
+ * describes ANOTHER element (or the text this one must NOT contain);
116
+ * `:has-text(...)` / `:text(...)` ARE the element's own text and pass;
117
+ * - in a non-final segment — a combinator (space, `>`, `>>`, `+`, `~`) at
118
+ * top level (outside parens, brackets and quotes) follows the var with a
119
+ * real selector after it. A bare trailing match flag (`"{{x}}" i`) or an
120
+ * attribute-internal space (`[name="{{x}}" i]`) is NOT a combinator.
121
+ *
122
+ * XPath (`xpath=` / `//…`):
123
+ * - the var sits in a NESTED predicate (bracket depth ≥ 2, e.g.
124
+ * `//table[.//th[text()="{{col}}"]]` — the th labels the table); or
125
+ * - a location step (`/`) follows the var at top level
126
+ * (`//th[text()="{{col}}"]/ancestor::table`).
127
+ *
128
+ * One left-to-right scan tracking quote state (with backslash-run counting so
129
+ * a literal trailing `\\` can't wedge the scanner in "quoted"), paren frames,
130
+ * and bracket depth.
131
+ */
132
+ function locatorVarIsScopeText(locator: string): boolean {
133
+ const m = varPattern().exec(locator);
134
+ if (!m) return false;
135
+ const varIndex = m.index;
136
+ const isXpath = /^\s*(?:xpath=|\(*\/\/)/i.test(locator);
137
+ let quote: string | null = null;
138
+ const scopeFrames: boolean[] = []; // one entry per open paren: is it a :has(/:not( frame?
139
+ let bracketDepth = 0;
140
+ let passedVar = false;
141
+ const escaped = (i: number): boolean => {
142
+ let n = 0;
143
+ for (let j = i - 1; j >= 0 && locator[j] === '\\'; j--) n++;
144
+ return n % 2 === 1;
145
+ };
146
+ for (let i = 0; i < locator.length; i++) {
147
+ if (i === varIndex) {
148
+ passedVar = true;
149
+ if (!isXpath && scopeFrames.some(Boolean)) return true;
150
+ if (isXpath && bracketDepth >= 2) return true;
151
+ }
152
+ const c = locator[i];
153
+ if (quote) {
154
+ if (c === quote && !escaped(i)) quote = null;
155
+ continue;
156
+ }
157
+ if (c === '"' || c === "'") { quote = c; continue; }
158
+ if (c === '(') {
159
+ const pseudo = locator.slice(Math.max(0, i - 4), i).toLowerCase();
160
+ scopeFrames.push(!isXpath && (pseudo === ':has' || pseudo === ':not'));
161
+ continue;
162
+ }
163
+ if (c === ')') { scopeFrames.pop(); continue; }
164
+ if (c === '[') { bracketDepth++; continue; }
165
+ if (c === ']') { bracketDepth = Math.max(0, bracketDepth - 1); continue; }
166
+ if (!passedVar || scopeFrames.length > 0 || bracketDepth > 0) continue;
167
+ if (isXpath) {
168
+ if (c === '/') return true; // a location step after the var → the target comes later
169
+ continue;
170
+ }
171
+ if (c === ' ' || c === '>' || c === '+' || c === '~') {
172
+ const rest = locator.slice(i).replace(/^[\s>+~]+/, '');
173
+ // Bare Playwright match flags after a quoted value are not a new segment.
174
+ if (rest.length > 0 && !/^[is](?:\s|$)/.test(rest)) return true;
175
+ }
176
+ }
177
+ return false;
178
+ }
179
+
100
180
  /**
101
181
  * Build the SelectorKey → resolution map from a selectors YAML file.
102
182
  * Returns an empty map when the file is missing or malformed.
@@ -125,6 +125,13 @@ export interface PlaywrightResult {
125
125
  duration?: number;
126
126
  startTime?: string;
127
127
  error?: string;
128
+ /**
129
+ * Every distinct failure message of the final attempt (ANSI/stack stripped).
130
+ * Playwright's top-level `error` is only the FIRST error — when a timeout
131
+ * wraps an assertion, the assertion detail (Locator/Expected/Received) lives
132
+ * in `results[].errors[1+]` and would otherwise never reach the deliverable.
133
+ */
134
+ errorMessages?: string[];
128
135
  tracePath?: string;
129
136
  projectName?: string;
130
137
  }
@@ -179,7 +186,7 @@ export interface ApiCatalogEntry {
179
186
  /** Request-body wire format — decides the Content-Type the request sends.
180
187
  * Absent/`json` → application/json, `form` → application/x-www-form-urlencoded,
181
188
  * `multipart` → multipart/form-data. */
182
- encoding?: 'json' | 'form' | 'multipart';
189
+ encoding?: 'json' | 'form' | 'multipart' | 'xml';
183
190
  /** Multipart file parts (multipart/form-data). A field value may be a single file spec, its string
184
191
  * shorthand, or an array of specs (multiple files under one field name). Typed loosely — guard before use. */
185
192
  files?: Record<string, unknown>;
@@ -640,10 +640,11 @@ export function buildApiDetailRows(
640
640
  const uniqueAuth = [...new Set(authTags.map((t) => t.slice('@auth:'.length)))];
641
641
  const authDatasource = [datasource, ...uniqueAuth].filter(Boolean).join('; ');
642
642
 
643
- // Request shape: compose from body + params + encoding.
644
- const bodyStr = entry.body
645
- ? `body: ${typeof entry.body === 'string' ? entry.body : JSON.stringify(entry.body)}`
646
- : '';
643
+ // Request shape: compose from body + params + encoding. A raw XML template can
644
+ // run long — cap the cell so one entry can't blow up the sheet's row height.
645
+ const rawBody = entry.body ? (typeof entry.body === 'string' ? entry.body : JSON.stringify(entry.body)) : '';
646
+ const cappedBody = rawBody.length > 500 ? `${rawBody.slice(0, 499)}…` : rawBody;
647
+ const bodyStr = cappedBody ? `body: ${cappedBody}` : '';
647
648
  const paramsArr = Array.isArray(entry.params) ? entry.params as string[] : [];
648
649
  const paramsStr = paramsArr.length > 0 ? `params: [${paramsArr.join(', ')}]` : '';
649
650
  const requestShape = [bodyStr, paramsStr].filter(Boolean).join('; ') || '—';
@@ -4,6 +4,51 @@ import * as Messages from '@cucumber/messages';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { SelectorResolver } from '../test-generator/utils/selector-resolver';
7
+ import { maskRefsForMatching } from '../test-generator/utils/mask-refs-for-matching';
8
+ import type { StateClause } from '../test-generator/patterns/types';
9
+
10
+ /**
11
+ * Canonical state clauses, longest-first. Order is load-bearing twice over: `sorted ascending` must
12
+ * be tried before any bare word so it is not split, and `unchecked` before `checked` so the
13
+ * substring never misclassifies — the same hazard, and the same fix, as `qualifiedState()` in
14
+ * driver-ui's assertion-patterns.
15
+ */
16
+ const STATE_CLAUSES: ReadonlyArray<StateClause> = [
17
+ 'sorted ascending', 'sorted descending',
18
+ 'unchecked', 'checked',
19
+ 'hidden', 'visible', 'disabled', 'enabled', 'focused', 'empty', 'loading', 'selected',
20
+ ];
21
+
22
+ /**
23
+ * Lift a trailing state clause out of a step sentence so the pattern layer can tell whether the
24
+ * pattern that won actually asserts it (SG-E021). Before this the clause lived ONLY inside
25
+ * `step.text`, every pattern re-regexed it privately, and a clause swallowed by a higher-priority
26
+ * pattern was undetectable by construction.
27
+ *
28
+ * Narrow on purpose — everything this rejects is a step that must keep compiling exactly as it does
29
+ * today:
30
+ * - requires the literal `is` — `wait for [X] to be hidden` is a wait, not a state assertion;
31
+ * - requires TRAILING position — `… is hidden after the modal closes` is prose, not a clause;
32
+ * - rejects `is not <state>` — `in local storage is not empty` is a real storage grammar (D5);
33
+ * - masks `[Ref]` and `{{data}}` CONTENT first, so `[Sorted Ascending]` (a button) and
34
+ * `{{is_empty}}` (a variable) are not read as states.
35
+ */
36
+ function extractStateClause(text: string): StateClause | undefined {
37
+ // Collapse runs of whitespace and drop a trailing period, so `… is hidden .` reads the same as
38
+ // `… is hidden`. Compared as a plain string tail rather than a built regex: the clause list holds
39
+ // a two-word member (`sorted ascending`), and interpolating it into a RegExp means escaping the
40
+ // separator, which is exactly the kind of quiet breakage `\b`-in-a-template-literal already is.
41
+ const tail = maskRefsForMatching(text)
42
+ .replace(/\s+/g, ' ')
43
+ .replace(/ ?\.$/, '')
44
+ .toLowerCase();
45
+ // ` is ` must sit IMMEDIATELY before the clause. That single space is what excludes `is not empty`
46
+ // — the storage grammar — without a negative lookbehind.
47
+ for (const state of STATE_CLAUSES) {
48
+ if (tail.endsWith(` is ${state}`)) return state;
49
+ }
50
+ return undefined;
51
+ }
7
52
 
8
53
  export interface DataTableRow {
9
54
  cells: string[]; // Cell values in order
@@ -28,6 +73,7 @@ export interface ParsedStep {
28
73
  parentRef?: string; // Parent scope reference: "in [Parent Name] parentType" → parentRef = "Parent Name"
29
74
  parentType?: string; // Parent scope type: table, list, section, dialog, form
30
75
  dataTable?: ParsedDataTable; // Inline data table (Cucumber DataTable syntax)
76
+ state?: StateClause; // Trailing canonical state clause, e.g. "hidden" from "… is hidden" (drives SG-E021)
31
77
  }
32
78
 
33
79
  export interface ParsedScenario {
@@ -204,7 +250,14 @@ export class GherkinParser {
204
250
  const selectorRef = selectorMatch ? selectorMatch[1] : undefined;
205
251
 
206
252
  // Extract data reference: {{login.email.valid}} or <valid_user.email> (legacy)
207
- const dataMatch = text.match(/\{\{([a-z0-9\-\.\_]+)\}\}|<([^>]+)>/i);
253
+ // A `{{var}}` inside a [Reference] is NEVER the step's data ref: a bracket carries a
254
+ // selectors.yaml key or a browser-storage key, both matched verbatim. Blank the brackets
255
+ // out first — the match is un-anchored and takes the FIRST hit, so a step with braces on
256
+ // both sides (`see [{{code}}_TOKEN] in session storage has length {{n}}`, or
257
+ // `remember [X] text as {{v}}` written with a bracket var) would otherwise bind the
258
+ // bracket's variable and silently lose the real ref.
259
+ const textOutsideRefs = text.replace(/\[[^\]]*\]/g, '');
260
+ const dataMatch = textOutsideRefs.match(/\{\{([a-z0-9\-\.\_]+)\}\}|<([^>]+)>/i);
208
261
  const dataRef = dataMatch ? (dataMatch[1] || dataMatch[2]) : undefined;
209
262
 
210
263
  // Extract quoted value: "Hello, how are you?"
@@ -269,6 +322,7 @@ export class GherkinParser {
269
322
  parentRef,
270
323
  parentType,
271
324
  dataTable,
325
+ state: extractStateClause(step.text),
272
326
  };
273
327
  }
274
328