@sun-asterisk/sungen 3.2.32 → 3.2.34

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 (381) hide show
  1. package/dist/cli/commands/audit.d.ts +5 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +26 -4
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/capability.d.ts.map +1 -1
  6. package/dist/cli/commands/capability.js +35 -17
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/changed.d.ts.map +1 -1
  9. package/dist/cli/commands/changed.js +2 -3
  10. package/dist/cli/commands/changed.js.map +1 -1
  11. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  12. package/dist/cli/commands/dashboard.js +2 -28
  13. package/dist/cli/commands/dashboard.js.map +1 -1
  14. package/dist/cli/commands/delivery.d.ts.map +1 -1
  15. package/dist/cli/commands/delivery.js +79 -53
  16. package/dist/cli/commands/delivery.js.map +1 -1
  17. package/dist/cli/commands/generate.d.ts.map +1 -1
  18. package/dist/cli/commands/generate.js +14 -7
  19. package/dist/cli/commands/generate.js.map +1 -1
  20. package/dist/cli/commands/results-merge.d.ts +9 -0
  21. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  22. package/dist/cli/commands/results-merge.js +9 -12
  23. package/dist/cli/commands/results-merge.js.map +1 -1
  24. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  25. package/dist/cli/commands/results-sanitize.js +2 -13
  26. package/dist/cli/commands/results-sanitize.js.map +1 -1
  27. package/dist/cli/commands/select.d.ts +12 -0
  28. package/dist/cli/commands/select.d.ts.map +1 -0
  29. package/dist/cli/commands/select.js +122 -0
  30. package/dist/cli/commands/select.js.map +1 -0
  31. package/dist/cli/commands/update.d.ts.map +1 -1
  32. package/dist/cli/commands/update.js +3 -0
  33. package/dist/cli/commands/update.js.map +1 -1
  34. package/dist/cli/index.js +7 -2
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  37. package/dist/dashboard/snapshot-builder.js +6 -10
  38. package/dist/dashboard/snapshot-builder.js.map +1 -1
  39. package/dist/dashboard/templates/index.html +1 -1
  40. package/dist/dashboard/types.d.ts +1 -0
  41. package/dist/dashboard/types.d.ts.map +1 -1
  42. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  43. package/dist/exporters/csv-exporter.js +11 -10
  44. package/dist/exporters/csv-exporter.js.map +1 -1
  45. package/dist/exporters/feature-parser.d.ts +21 -0
  46. package/dist/exporters/feature-parser.d.ts.map +1 -1
  47. package/dist/exporters/feature-parser.js +32 -1
  48. package/dist/exporters/feature-parser.js.map +1 -1
  49. package/dist/exporters/json-exporter.d.ts.map +1 -1
  50. package/dist/exporters/json-exporter.js +7 -0
  51. package/dist/exporters/json-exporter.js.map +1 -1
  52. package/dist/exporters/locale-variants.d.ts +7 -1
  53. package/dist/exporters/locale-variants.d.ts.map +1 -1
  54. package/dist/exporters/locale-variants.js +23 -16
  55. package/dist/exporters/locale-variants.js.map +1 -1
  56. package/dist/exporters/manual-hybrid-oracle.d.ts +54 -0
  57. package/dist/exporters/manual-hybrid-oracle.d.ts.map +1 -0
  58. package/dist/exporters/manual-hybrid-oracle.js +114 -0
  59. package/dist/exporters/manual-hybrid-oracle.js.map +1 -0
  60. package/dist/exporters/matrix/build.d.ts.map +1 -1
  61. package/dist/exporters/matrix/build.js +19 -9
  62. package/dist/exporters/matrix/build.js.map +1 -1
  63. package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
  64. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  65. package/dist/exporters/matrix/carry-manual-results.js +232 -0
  66. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  67. package/dist/exporters/matrix/export.d.ts +5 -0
  68. package/dist/exporters/matrix/export.d.ts.map +1 -1
  69. package/dist/exporters/matrix/export.js +8 -1
  70. package/dist/exporters/matrix/export.js.map +1 -1
  71. package/dist/exporters/matrix/gates.js +6 -0
  72. package/dist/exporters/matrix/gates.js.map +1 -1
  73. package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
  74. package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
  75. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
  76. package/dist/exporters/matrix/types.d.ts +8 -1
  77. package/dist/exporters/matrix/types.d.ts.map +1 -1
  78. package/dist/exporters/matrix/types.js.map +1 -1
  79. package/dist/exporters/matrix/wording.d.ts +8 -1
  80. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  81. package/dist/exporters/matrix/wording.js +37 -2
  82. package/dist/exporters/matrix/wording.js.map +1 -1
  83. package/dist/exporters/run-environment.d.ts +32 -0
  84. package/dist/exporters/run-environment.d.ts.map +1 -0
  85. package/dist/exporters/run-environment.js +125 -0
  86. package/dist/exporters/run-environment.js.map +1 -0
  87. package/dist/exporters/scenario-merger.d.ts +18 -0
  88. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  89. package/dist/exporters/scenario-merger.js +47 -3
  90. package/dist/exporters/scenario-merger.js.map +1 -1
  91. package/dist/exporters/test-data-resolver.d.ts +0 -4
  92. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  93. package/dist/exporters/test-data-resolver.js +0 -7
  94. package/dist/exporters/test-data-resolver.js.map +1 -1
  95. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  96. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  97. package/dist/exporters/xlsx-exporter.js +7 -2
  98. package/dist/exporters/xlsx-exporter.js.map +1 -1
  99. package/dist/generators/gherkin-parser/index.d.ts +11 -4
  100. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  101. package/dist/generators/gherkin-parser/index.js +49 -19
  102. package/dist/generators/gherkin-parser/index.js.map +1 -1
  103. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
  104. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  105. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  106. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  107. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  108. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  109. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  110. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  113. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  114. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  115. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  116. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  117. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  118. package/dist/generators/test-generator/code-generator.js +90 -5
  119. package/dist/generators/test-generator/code-generator.js.map +1 -1
  120. package/dist/generators/test-generator/diagnostics.d.ts +36 -5
  121. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  122. package/dist/generators/test-generator/diagnostics.js +35 -4
  123. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  124. package/dist/generators/test-generator/patterns/index.d.ts +12 -2
  125. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  126. package/dist/generators/test-generator/patterns/index.js +15 -9
  127. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  128. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  129. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  130. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  131. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  132. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  133. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  134. package/dist/generators/test-generator/step-mapper.d.ts +46 -0
  135. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  136. package/dist/generators/test-generator/step-mapper.js +84 -9
  137. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  138. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  139. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  140. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  141. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  142. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  143. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  144. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  145. package/dist/generators/test-generator/utils/selector-resolver.d.ts +37 -4
  146. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  147. package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
  148. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  149. package/dist/harness/audit.d.ts +3 -0
  150. package/dist/harness/audit.d.ts.map +1 -1
  151. package/dist/harness/audit.js +29 -12
  152. package/dist/harness/audit.js.map +1 -1
  153. package/dist/harness/capability.d.ts +7 -1
  154. package/dist/harness/capability.d.ts.map +1 -1
  155. package/dist/harness/capability.js +15 -12
  156. package/dist/harness/capability.js.map +1 -1
  157. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  158. package/dist/harness/config-file.d.ts +17 -0
  159. package/dist/harness/config-file.d.ts.map +1 -0
  160. package/dist/harness/config-file.js +100 -0
  161. package/dist/harness/config-file.js.map +1 -0
  162. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  163. package/dist/harness/data-driven-lint.js +69 -27
  164. package/dist/harness/data-driven-lint.js.map +1 -1
  165. package/dist/harness/depth-lint.d.ts.map +1 -1
  166. package/dist/harness/depth-lint.js +2 -1
  167. package/dist/harness/depth-lint.js.map +1 -1
  168. package/dist/harness/flow-check.d.ts.map +1 -1
  169. package/dist/harness/flow-check.js +6 -2
  170. package/dist/harness/flow-check.js.map +1 -1
  171. package/dist/harness/keyword-match.d.ts +59 -0
  172. package/dist/harness/keyword-match.d.ts.map +1 -0
  173. package/dist/harness/keyword-match.js +86 -0
  174. package/dist/harness/keyword-match.js.map +1 -0
  175. package/dist/harness/next-step.d.ts.map +1 -1
  176. package/dist/harness/next-step.js +1 -0
  177. package/dist/harness/next-step.js.map +1 -1
  178. package/dist/harness/parse.d.ts +24 -0
  179. package/dist/harness/parse.d.ts.map +1 -1
  180. package/dist/harness/parse.js +117 -13
  181. package/dist/harness/parse.js.map +1 -1
  182. package/dist/harness/perf.d.ts +7 -1
  183. package/dist/harness/perf.d.ts.map +1 -1
  184. package/dist/harness/perf.js +13 -10
  185. package/dist/harness/perf.js.map +1 -1
  186. package/dist/harness/quality-gates.d.ts +9 -2
  187. package/dist/harness/quality-gates.d.ts.map +1 -1
  188. package/dist/harness/quality-gates.js +22 -8
  189. package/dist/harness/quality-gates.js.map +1 -1
  190. package/dist/harness/query-catalog.js +0 -0
  191. package/dist/harness/query-catalog.js.map +1 -1
  192. package/dist/harness/read-text.d.ts +21 -0
  193. package/dist/harness/read-text.d.ts.map +1 -1
  194. package/dist/harness/read-text.js +32 -1
  195. package/dist/harness/read-text.js.map +1 -1
  196. package/dist/harness/secret-scan.d.ts +27 -1
  197. package/dist/harness/secret-scan.d.ts.map +1 -1
  198. package/dist/harness/secret-scan.js +57 -4
  199. package/dist/harness/secret-scan.js.map +1 -1
  200. package/dist/harness/select-by-ids.d.ts +31 -0
  201. package/dist/harness/select-by-ids.d.ts.map +1 -0
  202. package/dist/harness/select-by-ids.js +106 -0
  203. package/dist/harness/select-by-ids.js.map +1 -0
  204. package/dist/harness/sensors.d.ts +20 -1
  205. package/dist/harness/sensors.d.ts.map +1 -1
  206. package/dist/harness/sensors.js +102 -27
  207. package/dist/harness/sensors.js.map +1 -1
  208. package/dist/harness/spec-branches.d.ts +0 -18
  209. package/dist/harness/spec-branches.d.ts.map +1 -1
  210. package/dist/harness/spec-branches.js +24 -3
  211. package/dist/harness/spec-branches.js.map +1 -1
  212. package/dist/harness/spec-coverage.d.ts.map +1 -1
  213. package/dist/harness/spec-coverage.js +14 -4
  214. package/dist/harness/spec-coverage.js.map +1 -1
  215. package/dist/harness/ui-inventory.d.ts +40 -0
  216. package/dist/harness/ui-inventory.d.ts.map +1 -0
  217. package/dist/harness/ui-inventory.js +191 -0
  218. package/dist/harness/ui-inventory.js.map +1 -0
  219. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  220. package/dist/harness/viewpoint-ledger.js +66 -10
  221. package/dist/harness/viewpoint-ledger.js.map +1 -1
  222. package/dist/harness/write-atomic.d.ts +2 -0
  223. package/dist/harness/write-atomic.d.ts.map +1 -0
  224. package/dist/harness/write-atomic.js +62 -0
  225. package/dist/harness/write-atomic.js.map +1 -0
  226. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  227. package/dist/orchestrator/ai-skills/writer.js +15 -7
  228. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  229. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  230. package/dist/orchestrator/assets-drift.js +11 -6
  231. package/dist/orchestrator/assets-drift.js.map +1 -1
  232. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  233. package/dist/orchestrator/project-deps-sync.js +22 -1
  234. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  235. package/dist/orchestrator/project-initializer.d.ts +8 -0
  236. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  237. package/dist/orchestrator/project-initializer.js +49 -7
  238. package/dist/orchestrator/project-initializer.js.map +1 -1
  239. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  240. package/dist/orchestrator/screen-manager.js +3 -11
  241. package/dist/orchestrator/screen-manager.js.map +1 -1
  242. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  243. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  244. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  245. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  246. package/dist/orchestrator/security-overrides.d.ts +51 -0
  247. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  248. package/dist/orchestrator/security-overrides.js +165 -0
  249. package/dist/orchestrator/security-overrides.js.map +1 -0
  250. package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  251. package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  252. package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  253. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
  254. package/dist/orchestrator/templates/ai-src/commands/run-test.md +70 -13
  255. package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  256. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  257. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
  258. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  259. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  260. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  261. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
  262. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  263. package/dist/orchestrator/templates/playwright.config.ts +4 -2
  264. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  265. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  266. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  267. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  268. package/dist/orchestrator/templates/specs-url-assert.js +38 -4
  269. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  270. package/dist/orchestrator/templates/specs-url-assert.ts +47 -4
  271. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  272. package/dist/orchestrator/trace-mode-install.d.ts +14 -0
  273. package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
  274. package/dist/orchestrator/trace-mode-install.js +132 -0
  275. package/dist/orchestrator/trace-mode-install.js.map +1 -0
  276. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +20 -0
  277. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
  278. package/dist/orchestrator/viewpoint-pattern-catalog.js +45 -0
  279. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
  280. package/package.json +3 -3
  281. package/src/cli/commands/audit.ts +24 -5
  282. package/src/cli/commands/capability.ts +51 -18
  283. package/src/cli/commands/changed.ts +2 -4
  284. package/src/cli/commands/dashboard.ts +1 -26
  285. package/src/cli/commands/delivery.ts +75 -48
  286. package/src/cli/commands/generate.ts +15 -7
  287. package/src/cli/commands/results-merge.ts +9 -9
  288. package/src/cli/commands/results-sanitize.ts +2 -9
  289. package/src/cli/commands/select.ts +88 -0
  290. package/src/cli/commands/update.ts +3 -0
  291. package/src/cli/index.ts +7 -2
  292. package/src/dashboard/snapshot-builder.ts +6 -8
  293. package/src/dashboard/templates/index.html +1 -1
  294. package/src/dashboard/types.ts +1 -0
  295. package/src/exporters/csv-exporter.ts +7 -10
  296. package/src/exporters/feature-parser.ts +32 -2
  297. package/src/exporters/json-exporter.ts +6 -1
  298. package/src/exporters/locale-variants.ts +23 -17
  299. package/src/exporters/manual-hybrid-oracle.ts +112 -0
  300. package/src/exporters/matrix/build.ts +20 -10
  301. package/src/exporters/matrix/carry-manual-results.ts +229 -0
  302. package/src/exporters/matrix/export.ts +14 -4
  303. package/src/exporters/matrix/gates.ts +6 -0
  304. package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
  305. package/src/exporters/matrix/types.ts +8 -1
  306. package/src/exporters/matrix/wording.ts +36 -2
  307. package/src/exporters/run-environment.ts +89 -0
  308. package/src/exporters/scenario-merger.ts +49 -4
  309. package/src/exporters/test-data-resolver.ts +0 -6
  310. package/src/exporters/xlsx-exporter.ts +7 -2
  311. package/src/generators/gherkin-parser/index.ts +50 -18
  312. package/src/generators/test-generator/adapters/adapter-interface.ts +6 -2
  313. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  314. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  315. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  316. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  317. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  318. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  319. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  320. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  321. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  322. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  323. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  324. package/src/generators/test-generator/code-generator.ts +77 -5
  325. package/src/generators/test-generator/diagnostics.ts +36 -5
  326. package/src/generators/test-generator/patterns/index.ts +15 -8
  327. package/src/generators/test-generator/patterns/types.ts +15 -0
  328. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  329. package/src/generators/test-generator/step-mapper.ts +90 -8
  330. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  331. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  332. package/src/generators/test-generator/utils/selector-resolver.ts +238 -82
  333. package/src/harness/audit.ts +37 -16
  334. package/src/harness/capability.ts +23 -12
  335. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  336. package/src/harness/config-file.ts +66 -0
  337. package/src/harness/data-driven-lint.ts +82 -24
  338. package/src/harness/depth-lint.ts +2 -1
  339. package/src/harness/flow-check.ts +6 -2
  340. package/src/harness/keyword-match.ts +85 -0
  341. package/src/harness/next-step.ts +1 -0
  342. package/src/harness/parse.ts +123 -16
  343. package/src/harness/perf.ts +15 -7
  344. package/src/harness/quality-gates.ts +22 -8
  345. package/src/harness/query-catalog.ts +0 -0
  346. package/src/harness/read-text.ts +32 -1
  347. package/src/harness/secret-scan.ts +60 -4
  348. package/src/harness/select-by-ids.ts +83 -0
  349. package/src/harness/sensors.ts +109 -28
  350. package/src/harness/spec-branches.ts +6 -3
  351. package/src/harness/spec-coverage.ts +14 -4
  352. package/src/harness/ui-inventory.ts +164 -0
  353. package/src/harness/viewpoint-ledger.ts +59 -12
  354. package/src/harness/write-atomic.ts +22 -0
  355. package/src/orchestrator/ai-skills/writer.ts +18 -3
  356. package/src/orchestrator/assets-drift.ts +10 -5
  357. package/src/orchestrator/project-deps-sync.ts +23 -1
  358. package/src/orchestrator/project-initializer.ts +54 -6
  359. package/src/orchestrator/screen-manager.ts +3 -11
  360. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  361. package/src/orchestrator/security-overrides.ts +149 -0
  362. package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  363. package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  364. package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  365. package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
  366. package/src/orchestrator/templates/ai-src/commands/run-test.md +70 -13
  367. package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  368. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  369. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
  370. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  371. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  372. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  373. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
  374. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  375. package/src/orchestrator/templates/playwright.config.ts +4 -2
  376. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  377. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  378. package/src/orchestrator/templates/specs-url-assert.ts +47 -4
  379. package/src/orchestrator/templates/wdio.conf.ts +24 -1
  380. package/src/orchestrator/trace-mode-install.ts +94 -0
  381. package/src/orchestrator/viewpoint-pattern-catalog.ts +58 -0
@@ -154,11 +154,18 @@ export class GherkinParser {
154
154
  }
155
155
 
156
156
  /**
157
- * Range-attach top-level Gherkin comments to the scenario whose body contains them.
158
- * A comment belongs to scenario i when its line is after scenario i's `Scenario:` line
159
- * and before scenario i+1's header (its first tag line, or its `Scenario:` line). The
160
- * leading `# ` is stripped. Used to surface a @manual scenario's tester-procedure comments
157
+ * Attach top-level Gherkin comments to the scenario they describe. The leading `# ` is
158
+ * stripped. Used to surface a @manual scenario's tester procedure and `# MANUAL:` reason
161
159
  * (its only "steps") in the delivery report.
160
+ *
161
+ * A scenario owns:
162
+ * • its LEADING block — the comment lines directly above its header with no blank line in
163
+ * between (and any comment between its tag lines and `Scenario:`), as long as they sit
164
+ * after the previous scenario's last step. `# MANUAL (…): reason` written above the tags
165
+ * belongs to the scenario below it; it used to range onto the scenario ABOVE, where the
166
+ * manual parser appended it to that scenario's last oracle line (#810);
167
+ * • its BODY — every other comment after its `Scenario:` line and before the next
168
+ * scenario's leading block / header.
162
169
  */
163
170
  private attachScenarioComments(
164
171
  doc: Messages.GherkinDocument,
@@ -167,22 +174,32 @@ export class GherkinParser {
167
174
  ): void {
168
175
  const comments = doc.comments || [];
169
176
  if (!comments.length) return;
170
- const scenChildren = feature.children
171
- .filter((c) => c.scenario)
172
- .map((c) => c.scenario!);
177
+ const lineOf = new Map<number, string>(
178
+ comments.map((cm) => [cm.location.line, cm.text.replace(/^\s*#\s?/, '').replace(/\s+$/, '')]),
179
+ );
173
180
  // Same filter/order as parseScenarios → indices line up with `scenarios`.
181
+ const scenChildren = feature.children.filter((c) => c.scenario).map((c) => c.scenario!);
182
+ const background = feature.children.find((c) => c.background)?.background;
183
+ const headerLine = (sc: Messages.Scenario): number =>
184
+ Math.min(sc.location.line, ...(sc.tags || []).map((t) => t.location.line));
185
+
186
+ // Leading block of scenario i: [firstLeading, sc.location.line).
187
+ const leadingStart = scenChildren.map((sc, i) => {
188
+ const prevEnd = i > 0 ? bodyEndLine(scenChildren[i - 1]) : background ? bodyEndLine(background) : feature.location.line;
189
+ let first = headerLine(sc);
190
+ while (lineOf.has(first - 1) && first - 1 > prevEnd) first--;
191
+ return first;
192
+ });
193
+
174
194
  for (let i = 0; i < scenChildren.length && i < scenarios.length; i++) {
175
- const startLine = scenChildren[i].location.line;
176
- const next = scenChildren[i + 1];
177
- let endLine = Number.POSITIVE_INFINITY;
178
- if (next) {
179
- const tagLines = (next.tags || []).map((t) => t.location.line);
180
- endLine = Math.min(next.location.line, ...(tagLines.length ? tagLines : [next.location.line]));
181
- }
182
- const body = comments
183
- .filter((cm) => cm.location.line > startLine && cm.location.line < endLine)
184
- .map((cm) => cm.text.replace(/^\s*#\s?/, '').replace(/\s+$/, ''));
185
- if (body.length) scenarios[i].comments = body;
195
+ const from = leadingStart[i];
196
+ const to = i + 1 < scenChildren.length ? leadingStart[i + 1] : Number.POSITIVE_INFINITY;
197
+ const own = comments
198
+ .map((cm) => cm.location.line)
199
+ .filter((line) => line >= from && line < to)
200
+ .sort((a, b) => a - b)
201
+ .map((line) => lineOf.get(line)!);
202
+ if (own.length) scenarios[i].comments = own;
186
203
  }
187
204
  }
188
205
 
@@ -360,3 +377,18 @@ export class GherkinParser {
360
377
  return files;
361
378
  }
362
379
  }
380
+
381
+ /** Last source line a scenario/background occupies: its steps, their data tables/doc strings, examples. */
382
+ function bodyEndLine(block: Messages.Scenario | Messages.Background): number {
383
+ let end = block.location.line;
384
+ for (const step of block.steps) {
385
+ end = Math.max(end, step.location.line);
386
+ for (const row of step.dataTable?.rows ?? []) end = Math.max(end, row.location.line);
387
+ if (step.docString) end = Math.max(end, step.docString.location.line + step.docString.content.split('\n').length + 1);
388
+ }
389
+ for (const ex of ('examples' in block ? block.examples : [])) {
390
+ end = Math.max(end, ex.location.line);
391
+ for (const row of [...(ex.tableHeader ? [ex.tableHeader] : []), ...ex.tableBody]) end = Math.max(end, row.location.line);
392
+ }
393
+ return end;
394
+ }
@@ -5,7 +5,11 @@
5
5
 
6
6
  export interface AuthGroup {
7
7
  authRole?: string;
8
+ noAuth?: boolean;
8
9
  scenarios: string[];
10
+ /** The feature's `@afterEach`, scoped to this group — set when the feature mixes `@no-auth` with
11
+ * other groups, so the hook never runs on an anonymous test's empty session (#815). */
12
+ afterEach?: string;
9
13
  }
10
14
 
11
15
  /**
@@ -42,7 +46,7 @@ export interface TestFileData {
42
46
  runtimeData?: boolean; // --runtime-data flag: testData.get() instead of hardcoded values
43
47
  screenName?: string; // Screen name for TestDataLoader.load()
44
48
  featureFileName?: string; // Feature file name for TestDataLoader.load()
45
- isParallel?: boolean; // @parallel tag: fresh page per test (opt-out from serial default)
49
+ isParallel?: boolean; // !@serial: fresh page per test. Parallel is the default; @parallel only names it
46
50
  flowMode?: boolean; // flow: cross-screen → longer per-test timeout (bug #243)
47
51
  platform?: string; // mobile @platform:* value (mobile|android|ios) → `// sungen:platform=` marker for wdio per-OS routing
48
52
  cleanup?: { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean };
@@ -65,7 +69,7 @@ export interface ScenarioData {
65
69
  scenarioName: string;
66
70
  steps: Array<{ comment?: string; code: string }>;
67
71
  authRole?: string; // Auth role for storage state
68
- isParallel?: boolean; // @parallel: use fresh page from fixture
72
+ isParallel?: boolean; // !@serial: use fresh page from fixture
69
73
  tags?: string; // Pass-through tags for Playwright { tag: [...] }, e.g. "'@smoke', '@high'"
70
74
  casesDataset?: string; // @cases:<dataset> — emit one test() per row of the runtime dataset
71
75
  resetTier?: string; // Mobile (appium): scenario-level `@reset:` override; raises (never lowers) the feature tier for this scenario only
@@ -2,8 +2,8 @@
2
2
  // Persistence oracles (dismissals, pins) hinge on what survives a reinstall.
3
3
  await (async () => {
4
4
  const __pkg = __resolveAppId();
5
- const __caps = (driver.requestedCapabilities || {}) as Record<string, string>;
6
- const __appPath = __caps['appium:app'] || __caps['app'];
5
+ // Same reader the reset tiers use — one place decides what "the session has a build file" means.
6
+ const __appPath = __appBuildPath();
7
7
  if (!__appPath) {
8
8
  throw new Error('reinstall the app: the session must install from a build file — set APP_APK (Android) / IOS_APP (iOS) so the appium:app capability is present.');
9
9
  }
@@ -9,12 +9,18 @@
9
9
  Flutter screen; UiSelector has no OR, hence the xpath. iOS scatters it across
10
10
  label/value, which the predicate already ORs.
11
11
 
12
+ SCOPE (#747): an `in [Parent] <type>` clause chains this under the parent element. The android
13
+ xpath must go RELATIVE (`.//`) to do so — a child xpath beginning with `//` is ABSOLUTE in
14
+ Appium even when chained, so keeping it would emit code that LOOKS scoped and searches the whole
15
+ screen, which is the same silent widening the scope clause exists to prevent. The iOS predicate
16
+ carries no such trap and chains as written.
17
+
12
18
  The expected value must stay a standalone quoted literal so the runtime-data transform
13
19
  can swap it for testData.get(...). --}}
14
20
  {
15
21
  const __expected = String('{{escapeQuotes dataValue}}').trim();
16
22
  const __sel = driver.isIOS
17
23
  ? `-ios predicate string:label == "${__expected}" OR value == "${__expected}"`
18
- : `//*[@text="${__expected}" or @content-desc="${__expected}"]`;
19
- await expect($(__sel)).toBeDisplayed();
24
+ : `{{#if parentLocator}}.{{/if}}//*[@text="${__expected}" or @content-desc="${__expected}"]`;
25
+ await expect({{#if parentLocator}}{{parentLocator}}.$(__sel){{else}}$(__sel){{/if}}).toBeDisplayed();
20
26
  }
@@ -1,13 +1,17 @@
1
1
  {{!-- Role variable, visible (mobile best-effort): ARIA role has no native-mobile analog — reuse
2
2
  the role→accessibility-id mapping in appium-selector.hbs (`~name`) when a static name
3
3
  exists; the resolver already folds dataValue into name when there is none, so this is only
4
- the fallback for a caller that omits both. --}}
4
+ the fallback for a caller that omits both.
5
+
6
+ SCOPE (#747): an `in [Parent] <type>` clause chains this under the parent element. Both
7
+ branches locate by accessibility id / predicate, neither of which carries the absolute-xpath
8
+ trap the label-value twin has to work around. --}}
5
9
  {{#if name}}
6
- await expect($('~{{escapeQuotes name}}')).toBeDisplayed();
10
+ await expect({{#if parentLocator}}{{parentLocator}}.$('~{{escapeQuotes name}}'){{else}}$('~{{escapeQuotes name}}'){{/if}}).toBeDisplayed();
7
11
  {{else}}
8
12
  {
9
13
  const __expected = String('{{escapeQuotes dataValue}}').trim();
10
14
  const __sel = driver.isIOS ? `-ios predicate string:label == "${__expected}" OR value == "${__expected}"` : `android=new UiSelector().text("${__expected}")`;
11
- await expect($(__sel)).toBeDisplayed();
15
+ await expect({{#if parentLocator}}{{parentLocator}}.$(__sel){{else}}$(__sel){{/if}}).toBeDisplayed();
12
16
  }
13
17
  {{/if}}
@@ -502,9 +502,36 @@ describe('{{featureName}}', () => {
502
502
  // lose the escalation between them — SG-W024 already reports that shape when tiers are involved.)
503
503
  let __escalateAfter: string | null = null;
504
504
 
505
+ // `@known-defect` marks a scenario that is EXPECTED to be red — it asserts a live defect. The
506
+ // scaffolded /sungen:run-test command tells authors exactly that ("expected-red scenarios stay red;
507
+ // never 'fix' them"), so the ladder honours it here: such a failure is the suite working as
508
+ // designed, not evidence of leftover state. Arming escalation off it would make the NEXT scenario
509
+ // pay a harder reset for nothing — on every run, forever, because the defect is known.
510
+ //
511
+ // Read off the TITLE because `@known-defect` is a pass-through tag: appium appends it there as the
512
+ // `[@a @b]` suffix (which is what keeps it greppable via `--grep`), and that is how the field
513
+ // report shows it. Anchored to that trailing group, so a scenario merely NAMED `… known-defect …`
514
+ // cannot exempt itself — the same reason __tierFor refuses to strip a trailing `[Selector]` ref.
515
+ //
516
+ // Matched as a whole TAG TOKEN, never as a substring. `@known-defect-list` is a DIFFERENT tag, and
517
+ // reading it as this one would silently stop the next scenario's real dirty-state failure from
518
+ // getting the escalated reset it needs — with no log line at all, because "never armed" is not the
519
+ // "capped" path and prints nothing. The `:<ref>` parameter form is accepted because that is what
520
+ // the rest of the vocabulary does (`@manual:M10`, `@reset:data`).
521
+ //
522
+ // Caveat, inherited rather than introduced: a @cases scenario builds its title at runtime as
523
+ // `name — <row label>`, so a row LABEL that itself ends in `[@…]` would be read as the tag group.
524
+ // Same trailing-bracket ambiguity __tierFor accepts for a `[Selector]` ref.
525
+ function __isExpectedFail(title: string): boolean {
526
+ const tagGroup = title.match(/ \[@[^\]]*\]$/);
527
+ if (!tagGroup) return false;
528
+ return tagGroup[0].slice(2, -1).split(/\s+/)
529
+ .some((t) => t === '@known-defect' || t.startsWith('@known-defect:'));
530
+ }
531
+
505
532
  // Registered BEFORE any user @afterEach block so a throwing user hook cannot skip the bookkeeping.
506
533
  afterEach(async function () {
507
- __escalateAfter = this.currentTest?.state === 'failed' ? (this.currentTest?.title ?? '') : null;
534
+ __escalateAfter = this.currentTest?.state === 'failed' && !__isExpectedFail(this.currentTest?.title ?? '') ? (this.currentTest?.title ?? '') : null;
508
535
  });
509
536
 
510
537
  {{#if scenarioResetTiers}}
@@ -583,9 +610,31 @@ describe('{{featureName}}', () => {
583
610
  await driver.execute('mobile: clearApp', { appId: pkg });
584
611
  }
585
612
 
586
- async function __reinstallApp(pkg: string): Promise<void> {
613
+ /** The app's build file, when the session was given one. Absent when the app was attached BY PACKAGE. */
614
+ function __appBuildPath(): string | undefined {
587
615
  const caps = (driver.requestedCapabilities || {}) as Record<string, string>;
588
- const appPath = caps['appium:app'] || caps['app'];
616
+ return caps['appium:app'] || caps['app'];
617
+ }
618
+
619
+ /**
620
+ * The strongest tier THIS SESSION can actually carry out.
621
+ *
622
+ * Reinstalling needs the app's build file, and `data` on iOS IS a reinstall (no `mobile: clearApp`
623
+ * outside UiAutomator2). Attaching the app by package — the common local setup — leaves no build
624
+ * file, so those tiers cannot run at all.
625
+ *
626
+ * This exists so ESCALATION can cap itself. Escalation is a recovery the runner CHOSE; nothing
627
+ * asked for it. Escalating into a tier that throws turns one failed scenario into a dead spec file,
628
+ * because the throw lands in `beforeEach` and mocha then skips every remaining test in the file.
629
+ * A DECLARED tier is never capped — that is an unmet contract and must still throw by name.
630
+ */
631
+ function __performableTier(): string {
632
+ if (__appBuildPath()) return 'full';
633
+ return driver.isIOS ? 'soft' : 'data';
634
+ }
635
+
636
+ async function __reinstallApp(pkg: string): Promise<void> {
637
+ const appPath = __appBuildPath();
589
638
  if (!appPath) {
590
639
  throw new Error(
591
640
  `reset tier needs the app's build file to reinstall — set APP_APK (Android) / IOS_APP (iOS) so the appium:app capability is present, or drop to @reset:soft.`,
@@ -637,9 +686,24 @@ describe('{{featureName}}', () => {
637
686
  // Same title ⟹ this is a retry of the scenario that just failed: keep its declared tier so a real
638
687
  // failure fails both attempts (see __escalateAfter above).
639
688
  const __escalate = __escalateAfter !== null && __escalateAfter !== (this.currentTest?.title ?? '');
640
- const __tier = __escalate ? __ESCALATE[__base] : __base;
689
+ // Escalation caps at what this session can perform, with the DECLARED tier as the FLOOR.
690
+ // The cap stops a tier NOBODY ASKED FOR from throwing in this hook and taking the rest of the
691
+ // spec file with it. The floor stops the cap from silently WEAKENING what the feature declared:
692
+ // a declared tier this session cannot perform still reaches __resetApp and still throws by name,
693
+ // because that one IS an unmet contract.
694
+ const __want = __escalate ? __ESCALATE[__base] : __base;
695
+ const __tier = __escalate
696
+ ? __TIER_ORDER[Math.max(
697
+ __TIER_ORDER.indexOf(__base),
698
+ Math.min(__TIER_ORDER.indexOf(__want), __TIER_ORDER.indexOf(__performableTier())),
699
+ )]
700
+ : __want;
641
701
  if (__tier !== __base) {
642
702
  console.log(` \u2191 reset escalated ${__base} \u2192 ${__tier} (previous scenario failed: "${__escalateAfter}")`);
703
+ } else if (__want !== __base) {
704
+ // Said per occurrence, not once per run: this names the scenario that did NOT get the stronger
705
+ // reset it was owed, which is a fact about THIS scenario, not just about the session.
706
+ console.warn(` ! reset could not escalate ${__base} \u2192 ${__want} — this session has no app build file to reinstall from (the app was attached by package). Staying at ${__base}; set APP_APK (Android) / IOS_APP (iOS) to enable the stronger reset.`);
643
707
  }
644
708
  await __resetApp(__tier);
645
709
  {{#if backgroundSteps}}
@@ -1,2 +1,2 @@
1
1
  await {{> locator}}.click();
2
- await page.getByText('{{selectValue}}', { exact: true }).first().click();
2
+ {{> pick-option}}
@@ -9,7 +9,7 @@ if ((await el.evaluate((n) => n.tagName.toLowerCase())) === 'select') {
9
9
  await el.selectOption('{{selectValue}}');
10
10
  } else {
11
11
  await el.click();
12
- await page.getByText('{{selectValue}}', { exact: true }).first().click();
12
+ {{> pick-option}}
13
13
  }
14
14
  }
15
15
  {{~else~}}
@@ -1 +1,7 @@
1
- await expect(page.getByText(/{{#if label}}{{escapeRegex label}}\s*{{else}}(?:^|\s){{/if}}{{escapeRegex dataValue}}$/)).toBeVisible();
1
+ {{!-- Caption+value read: the ref names on-screen TEXT ("学科 -"), not an element, so the locator is
2
+ built inline from the caption rather than through the `locator` partial. That is why the scope
3
+ clause needs its own slot here — the partial's `parentLocator` handling never runs (#747).
4
+ Unscoped, `… in [Education Card 6] card` compiled away to nothing: on a screen repeating ten
5
+ 学歴・職歴 cards the assertion strict-mode-failed where the caption repeats, and where it
6
+ happened to be unique elsewhere it passed having read a DIFFERENT card than the sentence names. --}}
7
+ await expect({{#if parentLocator}}{{parentLocator}}{{else}}page{{/if}}.getByText(/{{#if label}}{{escapeRegex label}}\s*{{else}}(?:^|\s){{/if}}{{escapeRegex dataValue}}$/)).toBeVisible();
@@ -1,9 +1,13 @@
1
+ {{!-- `getByRole` is built inline from role/name rather than through the `locator` partial, so the
2
+ scope clause needs its own slot here exactly as in label-value-assertion — the label branch's
3
+ twin (#747). `see [Logo] image with {{alt}} in [Product Card 3] card` searched every card on
4
+ the page before this. --}}
1
5
  {{#if name}}
2
6
  {{#if dataValue}}
3
- await expect(page.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
7
+ await expect({{#if parentLocator}}{{parentLocator}}{{else}}page{{/if}}.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
4
8
  {{else}}
5
- await expect(page.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{> locator-nth}}).toBeVisible();
9
+ await expect({{#if parentLocator}}{{parentLocator}}{{else}}page{{/if}}.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{> locator-nth}}).toBeVisible();
6
10
  {{/if}}
7
11
  {{else}}
8
- await expect(page.getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
9
- {{/if}}
12
+ await expect({{#if parentLocator}}{{parentLocator}}{{else}}page{{/if}}.getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
13
+ {{/if}}
@@ -0,0 +1,13 @@
1
+ {{!-- #847 — pick the value in an OPENED custom listbox (Radix/shadcn, MUI, Headless UI).
2
+ The popup portals to the end of <body>, so a page-wide getByText(…).first() resolved to
3
+ any EARLIER element with the same text (a badge showing the current choice, a result
4
+ card) — and the open popup blocks clicks outside it, so the step timed out every time,
5
+ looking like a race. The ARIA option lives only in the open listbox: wait briefly for it
6
+ (a count() right after opening would race the popup animation), and fall back to the old
7
+ text match only for a widget that exposes no listbox roles. --}}
8
+ { const __opt = page.getByRole('option', { name: '{{selectValue}}', exact: true }).first();
9
+ if (await __opt.waitFor({ state: 'visible', timeout: 3000 }).then(() => true, () => false)) {
10
+ await __opt.click();
11
+ } else {
12
+ await page.getByText('{{selectValue}}', { exact: true }).first().click();
13
+ } }
@@ -23,8 +23,12 @@
23
23
  `https://admin.example.com/jobs` asserted the PATH on any host, and the public site's /jobs
24
24
  satisfied it.
25
25
 
26
+ `ignoreQuery` comes from the page ENTRY's `query: any`, not from the step, because it is a fact
27
+ about the page rather than about one assertion (#748). It relaxes only the bare-page rule; the
28
+ declared-value half below is untouched, so the two compose rather than override each other.
29
+
26
30
  The declared-value half is gated on `dataRef`, not on `query`. The step wrote `with {{v}}`
27
31
  either way; gating on the resolved VALUE meant an empty test-data entry silently dropped the
28
32
  whole check and fell back to the bare-page rule, which is the one case specs/url-assert.ts
29
33
  exists to throw on. --}}
30
- await expectPage(page, { pathname: /^{{pathRegex}}\/?$/{{#if hostRegex}}, hostname: /^{{hostRegex}}$/{{/if}}{{#if dataRef}}, dataRef: '{{escapeQuotes dataRef}}', declared: '{{escapeQuotes query}}'{{/if}} });
34
+ await expectPage(page, { pathname: /^{{pathRegex}}\/?$/{{#if hostRegex}}, hostname: /^{{hostRegex}}$/{{/if}}{{#if ignoreQuery}}, ignoreQuery: true{{/if}}{{#if dataRef}}, dataRef: '{{escapeQuotes dataRef}}', declared: '{{escapeQuotes query}}'{{/if}} });
@@ -46,6 +46,10 @@ test.describe('{{featureName}}', () => {
46
46
  test.describe('{{authRole}}', () => {
47
47
  test.use({ storageState: 'specs/.auth/{{authRole}}.json' });
48
48
 
49
+ {{#if afterEach}}
50
+ {{indent afterEach 2}}
51
+
52
+ {{/if}}
49
53
  {{#each scenarios}}
50
54
  {{indent this 2}}
51
55
 
@@ -63,6 +67,18 @@ test.describe('{{featureName}}', () => {
63
67
  {{#each scenarios}}
64
68
  {{indent this 2}}
65
69
 
70
+ {{/each}}
71
+ });
72
+
73
+ {{else if afterEach}}
74
+ {{!-- The un-tagged group next to a `@no-auth` one (#815): an ANONYMOUS describe scopes the hook to
75
+ these tests without adding a title segment, so test titles (and delivery fingerprints) stay put. --}}
76
+ test.describe(() => {
77
+ {{indent afterEach 2}}
78
+
79
+ {{#each scenarios}}
80
+ {{indent this 2}}
81
+
66
82
  {{/each}}
67
83
  });
68
84
 
@@ -10,6 +10,7 @@ import { capabilityRegistry } from '../../capabilities/registry';
10
10
  import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
11
11
  import { readCapabilities } from '../../harness/capability';
12
12
  import { parseManualComments } from '../../exporters/scenario-merger';
13
+ import { isPresencePlaceholderHybrid } from '../../exporters/manual-hybrid-oracle';
13
14
  import { findSyntaxError } from './utils/spec-syntax-check';
14
15
 
15
16
  /** One capability-injected precondition statement (`@api:`/`@query:` → `testData.bind(name, …)`). */
@@ -210,7 +211,7 @@ function getAuthMode(
210
211
  * Grouping key for the anonymous bucket. A literal that no `@auth:<role>` can produce, so an
211
212
  * imaginative role name can never land in the same group as `@no-auth`.
212
213
  */
213
- const NO_AUTH_GROUP_KEY = 'no-auth';
214
+ const NO_AUTH_GROUP_KEY = '\0no-auth';
214
215
 
215
216
 
216
217
  /**
@@ -266,6 +267,56 @@ function warnMultipleAuthTags(tags: string[], context: string): void {
266
267
  }
267
268
  }
268
269
 
270
+ /**
271
+ * Drop the YAML comment from ONE line: a `#` at line start or preceded by whitespace,
272
+ * and not inside a quoted scalar. Line-level only — see `yamlWithoutComments` for the
273
+ * block-scalar case, which this function cannot see from a single line.
274
+ */
275
+ function stripYamlComment(line: string): string {
276
+ let quote: string | null = null;
277
+ for (let i = 0; i < line.length; i++) {
278
+ const c = line[i];
279
+ if (quote) {
280
+ if (quote === '"' && c === '\\') { i++; continue; }
281
+ if (c === quote) quote = null;
282
+ continue;
283
+ }
284
+ if (c === '"' || c === "'") { quote = c; continue; }
285
+ if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i);
286
+ }
287
+ return line;
288
+ }
289
+
290
+ /**
291
+ * The YAML text a `${QA_*}` scan may look at: comments removed, block scalars kept whole.
292
+ *
293
+ * SG-W009's reference scan used to run over the whole raw file, so a `${QA_*}` that only ever
294
+ * appeared in a `#` comment counted as a live reference — including the one sungen's own
295
+ * scaffolded "HOW TO FILL VALUES" block writes into every test-data file. The declared side has
296
+ * always been line-wise; this brings the reference side to the same footing.
297
+ *
298
+ * Inside a `|`/`>` block scalar every character is literal content, `#` included, so those lines
299
+ * pass through untouched. Stripping them would send the check the other way — a real, undeclared
300
+ * `${QA_*}` written after a `#` in a block scalar would go unreported, and the run would only fail
301
+ * later with "environment variable not set". A missed warning is worse than a noisy one.
302
+ */
303
+ function yamlWithoutComments(text: string): string {
304
+ const out: string[] = [];
305
+ let blockIndent: number | null = null;
306
+ for (const line of text.split('\n')) {
307
+ if (blockIndent !== null) {
308
+ const indent = line.search(/\S/);
309
+ // blank lines and anything indented deeper than the key stay inside the scalar
310
+ if (indent === -1 || indent > blockIndent) { out.push(line); continue; }
311
+ blockIndent = null;
312
+ }
313
+ const block = /^(\s*)[^#\s][^:]*:\s*[|>][+-]?\d*[+-]?\s*$/.exec(line);
314
+ if (block) { blockIndent = block[1].length; out.push(line); continue; }
315
+ out.push(stripYamlComment(line));
316
+ }
317
+ return out.join('\n');
318
+ }
319
+
269
320
  /**
270
321
  * Result of test generation for a single feature file
271
322
  */
@@ -740,11 +791,18 @@ export class CodeGenerator {
740
791
  // labeled `Observable:`/`Oracle:`/`Expected:` line (parseManualComments — the
741
792
  // SAME parser delivery uses). Field finding (student-site, 87 scenarios): prose
742
793
  // paragraphs and rationale-only comments exported EMPTY Steps + Expected cells.
794
+ // • HAS a Then, but every assertion is a bare presence placeholder while the real oracle
795
+ // sits in the comments (#810) — the same banned hybrid wearing a Then: the deliverable
796
+ // reads "See [X] table" and a tester ticking it Passed records a check nobody made.
743
797
  const isStructured = scenario.steps.length > 0;
744
798
  if (isStructured && !hasThenStep(scenario)) {
745
799
  this.diagnostics.add('SG-W008',
746
800
  `@manual scenario "${scenario.name}" has Gherkin steps but no Then — a When without Then is invalid Gherkin. Add the Then step, or remove the steps and write the full procedure (action steps, then "Observable:"/"Oracle:" lines) in its \`# Tester verifies:\` block`,
747
801
  { feature: feature.name, step: scenario.name });
802
+ } else if (isStructured && isPresencePlaceholderHybrid(scenario.steps, scenario.comments ?? [])) {
803
+ this.diagnostics.add('SG-W008',
804
+ `@manual scenario "${scenario.name}" asserts only presence (e.g. "User see [X] table") while its real oracle sits in the \`# Tester verifies:\` comment — the deliverable's Expected would not say what the case checks. Write the oracle as the Then step (e.g. \`table match data:\`), or remove the steps and write the full numbered procedure ("1. <action>" … "N. Observable: <what>")`,
805
+ { feature: feature.name, step: scenario.name });
748
806
  } else if (!isStructured) {
749
807
  const parsed = parseManualComments(scenario.comments ?? []);
750
808
  if (!parsed.steps.length || !parsed.expected.length) {
@@ -830,7 +888,7 @@ export class CodeGenerator {
830
888
  authGroupMap.get(key)!.push(code);
831
889
  }
832
890
 
833
- const authGroups = groupOrder.map(key => ({
891
+ const authGroups = groupOrder.map((key): { authRole?: string; noAuth: boolean; scenarios: string[]; afterEach?: string } => ({
834
892
  authRole: key === NO_AUTH_GROUP_KEY ? undefined : (key || undefined),
835
893
  noAuth: key === NO_AUTH_GROUP_KEY,
836
894
  scenarios: authGroupMap.get(key)!,
@@ -873,6 +931,19 @@ export class CodeGenerator {
873
931
  }
874
932
  const scenarios = renderedScenarios.map(s => s.code);
875
933
 
934
+ // #815 — a feature-level `@afterEach` used to sit in the outermost describe, so it also ran after
935
+ // every `@no-auth` test, on an EMPTY session: a hook that needs the signed-in page (`Given User is
936
+ // on [probe] page` → redirected to /login) failed an anonymous test whose own steps passed. When
937
+ // the feature mixes `@no-auth` with any other group, the hook moves INTO each other group and the
938
+ // no-auth group runs without it. A feature that is anonymous throughout keeps the hook — it was
939
+ // written for those tests.
940
+ // (A no-auth group beside another one means ≥2 groups, so `renderAuthBlocks` is already true.)
941
+ const afterEachInGroups = !!afterEach
942
+ && authGroups.some(g => g.noAuth) && authGroups.some(g => !g.noAuth);
943
+ if (afterEachInGroups) {
944
+ for (const g of authGroups) if (!g.noAuth) g.afterEach = afterEach;
945
+ }
946
+
876
947
  // For single group, extract the auth role to put test.use at describe level. Keyed on
877
948
  // `renderAuthBlocks`, not `needsGrouping`: a lone `@no-auth` group now renders its own block, so
878
949
  // hoisting anything to the feature describe would double up. Its authRole is undefined anyway.
@@ -969,7 +1040,7 @@ export class CodeGenerator {
969
1040
  featureDescription: feature.description,
970
1041
  background,
971
1042
  beforeAll,
972
- afterEach,
1043
+ afterEach: afterEachInGroups ? undefined : afterEach,
973
1044
  afterAll,
974
1045
  cleanupConfig,
975
1046
  screenshotOnFailure,
@@ -1314,7 +1385,8 @@ export class CodeGenerator {
1314
1385
  * a name in the committed .env.qa.example. An undeclared ref means the test throws
1315
1386
  * "environment variable not set" the moment the key is used — surface it at compile
1316
1387
  * time instead. Only the `${QA_*}` shape is checked: any other `${...}` text is
1317
- * ordinary literal data by design (never resolved, never warned about).
1388
+ * ordinary literal data by design (never resolved, never warned about). A `${QA_*}`
1389
+ * that appears only inside a `#` comment is prose, not a reference, and is ignored.
1318
1390
  */
1319
1391
  private reportedEnvRefs = new Set<string>();
1320
1392
  private checkEnvSecretRefs(featureFile: string): void {
@@ -1322,7 +1394,7 @@ export class CodeGenerator {
1322
1394
  if (!fs.existsSync(testDataDir)) return;
1323
1395
  const referenced = new Set<string>();
1324
1396
  for (const f of fs.readdirSync(testDataDir).filter((x) => x.endsWith('.yaml') || x.endsWith('.yml'))) {
1325
- const text = fs.readFileSync(path.join(testDataDir, f), 'utf-8');
1397
+ const text = yamlWithoutComments(fs.readFileSync(path.join(testDataDir, f), 'utf-8'));
1326
1398
  for (const m of text.matchAll(/\$\{(QA_[A-Z0-9_]+)\}/g)) referenced.add(m[1]);
1327
1399
  }
1328
1400
  if (!referenced.size) return;
@@ -24,7 +24,9 @@
24
24
  * SG-W008 — @manual scenario is incomplete in either direction: Gherkin steps without a
25
25
  * Then (invalid Gherkin; comments cannot substitute), or a documentation-style
26
26
  * manual (no steps) whose comments lack the extractable procedure — ≥1 numbered
27
- * action line + ≥1 labeled Observable:/Oracle:/Expected: line
27
+ * action line + ≥1 labeled Observable:/Oracle:/Expected: line — or a Then that
28
+ * asserts only presence while the real oracle sits in a `# Tester verifies:`
29
+ * comment (#810)
28
30
  * SG-W009 — test-data references a `${QA_*}` secret that is declared in neither .env.qa
29
31
  * nor .env.qa.example — the test will throw "not set" the moment the key is used
30
32
  * SG-W010 — a row step's [Ref] looks POSITIONAL (First/Last/Top/…) but has no selectors.yaml
@@ -186,12 +188,29 @@
186
188
  * build names itself at generate time instead of one step further downstream
187
189
  *
188
190
  * SG-W028 — a step's trailing scope clause was DROPPED, so the step runs against more of the page
189
- * than its sentence names. Two causes, one code: the `<word>` after `in [X]` is not a
191
+ * than its sentence names. Three causes, one code: the `<word>` after `in [X]` is not a
190
192
  * container type (a typo, or a leaf element used as a parent), or the by-content form
191
193
  * `in [X] <type> with {{v}}` was written for the mobile adapter, which chains a parent
192
194
  * element and has no content filter. Before this, both compiled clean and silently
193
195
  * widened the assertion to the whole screen — the failure mode that only a diff of the
194
- * generated spec against the Gherkin could reveal (#667)
196
+ * generated spec against the Gherkin could reveal (#667).
197
+ * The third is the RENDER-time half #667 could not reach: the clause parsed and the
198
+ * container resolved, but the step form that won the sentence emits a locator built
199
+ * inline and never applies it. Judged on the emitted code — every path that honours a
200
+ * scope emits the resolved container locator verbatim, so its absence IS the drop —
201
+ * which makes the check total rather than a list of templates to keep in sync. Found
202
+ * via `see [学科] label with {{v}} in [Education Card 6] card` compiling to a page-wide
203
+ * `getByText` on a screen repeating ten identical cards (#747)
204
+ * SG-W037 — a `type: page` entry says something about the QUERY that the compiler cannot use.
205
+ * Two causes, one code, and the same root confusion behind both: a bare `see [X] page`
206
+ * asserts the URL carries NO query (#464), which is unsatisfiable for a page keyed by
207
+ * one (`/admins/detail?id=5`, where the id comes from the row just clicked). Either the
208
+ * entry's `value` carries a `?`/`#` — dropped, because a page value is the path only and
209
+ * the pattern is matched against `u.pathname`, which never holds a query — or `query:`
210
+ * declares something other than `any`, which is ignored rather than read as permissive,
211
+ * since a typo must never quietly switch off the #464 assertion. Both used to be silent,
212
+ * and the first is how #748 was misdiagnosed in the field as a regex-escaping bug: the
213
+ * obvious workaround produced byte-identical output and no hint why
195
214
  * SG-W029 — a mobile (appium) feature compiled with NO usable app-package `Path:` — either the line
196
215
  * is missing outright, or it holds a web-style route (`Path: /applied-jobs`) that the
197
216
  * package guard rejects. Both used to be silent, and both cost the same two things: the
@@ -255,9 +274,21 @@
255
274
  * than the one the scenario means, and nothing else would ever say so. Not reported for
256
275
  * testid/label/placeholder/role+name/text entries, which name a particular thing —
257
276
  * warning there would fire on nearly every capture in a suite
277
+ * SG-E038 — a `{{var}}` the compiler cannot resolve: no test-data file for the feature, no such
278
+ * key, or a key holding a list/map where a value was needed. Twenty-four step patterns
279
+ * used to catch this and emit the PLACEHOLDER TEXT as the value — `fill [ID] with
280
+ * {{valid_partner_id}}` compiled to `fill('${valid_partner_id}')`, generate exited 0,
281
+ * and the literal 20 characters were typed into the app. The field under test applied
282
+ * no format validation, so the string was accepted, the server saw an unknown user, and
283
+ * the scenario failed at the assertion it was written to fail at, for the reason it was
284
+ * written to fail: a suite that looked exactly right while feeding the system garbage.
285
+ * Worse than one wrong value — every run shared the SAME literal id, so a scenario
286
+ * needing a fresh user silently reused one and its state leaked between runs (#781).
287
+ * Note a feature resolves `{{var}}` against a data file named after ITSELF, so the
288
+ * second `.feature` in a screen directory starts life with no data file at all
258
289
  * Diagnostics never change the EXIT CODE by themselves; `sungen generate --strict` turns any
259
290
  * collected diagnostic into a non-zero exit. They are not all inert, though: SG-W001, SG-W011,
260
- * SG-E020, SG-E021, SG-W020, SG-E023, SG-E024, SG-E025, SG-E026, SG-E027 and SG-E035 additionally compile
291
+ * SG-E020, SG-E021, SG-W020, SG-E023, SG-E024, SG-E025, SG-E026, SG-E027, SG-E035 and SG-E038 additionally compile
261
292
  * the offending step to a runtime `throw` — a step that reads as a check and silently checks
262
293
  * nothing is worse than one that fails loudly — and SG-E031 refuses the file outright. (SG-E022 is
263
294
  * the one exception to the "blame the Gherkin" rule and throws nothing: the file is still written
@@ -265,7 +296,7 @@
265
296
  * Generation always COMPLETES — the rest of the scenario and the rest of the file still compile.
266
297
  */
267
298
 
268
- export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W004' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015' | 'SG-W016' | 'SG-W017' | 'SG-W018' | 'SG-E020' | 'SG-E021' | 'SG-W020' | 'SG-W021' | 'SG-W022' | 'SG-W023' | 'SG-W024' | 'SG-W026' | 'SG-E025' | 'SG-E026' | 'SG-E022' | 'SG-E023' | 'SG-E024' | 'SG-E027' | 'SG-W028' | 'SG-W029' | 'SG-W030' | 'SG-E031' | 'SG-W032' | 'SG-W034' | 'SG-E035' | 'SG-W036';
299
+ export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W004' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015' | 'SG-W016' | 'SG-W017' | 'SG-W018' | 'SG-E020' | 'SG-E021' | 'SG-W020' | 'SG-W021' | 'SG-W022' | 'SG-W023' | 'SG-W024' | 'SG-W026' | 'SG-E025' | 'SG-E026' | 'SG-E022' | 'SG-E023' | 'SG-E024' | 'SG-E027' | 'SG-W028' | 'SG-W029' | 'SG-W030' | 'SG-E031' | 'SG-W032' | 'SG-W034' | 'SG-E035' | 'SG-W036' | 'SG-W037' | 'SG-E038';
269
300
 
270
301
  export interface Diagnostic {
271
302
  code: DiagnosticCode;