@sun-asterisk/sungen 3.2.31 → 3.2.33

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/cli/commands/audit.d.ts +1 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +13 -2
  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 +46 -17
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/delivery.d.ts.map +1 -1
  9. package/dist/cli/commands/delivery.js +39 -18
  10. package/dist/cli/commands/delivery.js.map +1 -1
  11. package/dist/cli/commands/generate.d.ts.map +1 -1
  12. package/dist/cli/commands/generate.js +14 -7
  13. package/dist/cli/commands/generate.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +2 -0
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  18. package/dist/cli/commands/results-merge.js +2 -12
  19. package/dist/cli/commands/results-merge.js.map +1 -1
  20. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  21. package/dist/cli/commands/results-sanitize.js +2 -13
  22. package/dist/cli/commands/results-sanitize.js.map +1 -1
  23. package/dist/cli/commands/setup-mobile-env.d.ts +15 -0
  24. package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
  25. package/dist/cli/commands/setup-mobile-env.js +65 -0
  26. package/dist/cli/commands/setup-mobile-env.js.map +1 -0
  27. package/dist/cli/index.js +2 -0
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  30. package/dist/dashboard/snapshot-builder.js +6 -10
  31. package/dist/dashboard/snapshot-builder.js.map +1 -1
  32. package/dist/exporters/feature-parser.d.ts +21 -0
  33. package/dist/exporters/feature-parser.d.ts.map +1 -1
  34. package/dist/exporters/feature-parser.js +32 -1
  35. package/dist/exporters/feature-parser.js.map +1 -1
  36. package/dist/exporters/locale-variants.d.ts +7 -1
  37. package/dist/exporters/locale-variants.d.ts.map +1 -1
  38. package/dist/exporters/locale-variants.js +23 -16
  39. package/dist/exporters/locale-variants.js.map +1 -1
  40. package/dist/exporters/matrix/build.d.ts.map +1 -1
  41. package/dist/exporters/matrix/build.js +13 -7
  42. package/dist/exporters/matrix/build.js.map +1 -1
  43. package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
  44. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  45. package/dist/exporters/matrix/carry-manual-results.js +232 -0
  46. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  47. package/dist/exporters/matrix/export.d.ts +2 -0
  48. package/dist/exporters/matrix/export.d.ts.map +1 -1
  49. package/dist/exporters/matrix/export.js +7 -1
  50. package/dist/exporters/matrix/export.js.map +1 -1
  51. package/dist/exporters/matrix/wording.d.ts +7 -0
  52. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  53. package/dist/exporters/matrix/wording.js +27 -1
  54. package/dist/exporters/matrix/wording.js.map +1 -1
  55. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  56. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  57. package/dist/exporters/xlsx-exporter.js +7 -2
  58. package/dist/exporters/xlsx-exporter.js.map +1 -1
  59. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  60. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  61. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  62. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  63. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  64. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  65. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  66. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  67. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  68. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  69. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  70. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  71. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  72. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  73. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
  74. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  75. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  76. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  77. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  78. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  79. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  80. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  81. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
  83. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  84. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  85. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  86. package/dist/generators/test-generator/code-generator.js +122 -15
  87. package/dist/generators/test-generator/code-generator.js.map +1 -1
  88. package/dist/generators/test-generator/diagnostics.d.ts +70 -4
  89. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  90. package/dist/generators/test-generator/diagnostics.js +69 -3
  91. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  92. package/dist/generators/test-generator/patterns/index.d.ts +13 -3
  93. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  94. package/dist/generators/test-generator/patterns/index.js +16 -10
  95. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  96. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  97. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  98. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  99. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  100. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  101. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  102. package/dist/generators/test-generator/step-mapper.d.ts +81 -0
  103. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  104. package/dist/generators/test-generator/step-mapper.js +187 -13
  105. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  106. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  107. package/dist/generators/test-generator/template-engine.js +18 -0
  108. package/dist/generators/test-generator/template-engine.js.map +1 -1
  109. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  110. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  111. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  112. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  113. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  114. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  115. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  116. package/dist/generators/test-generator/utils/selector-resolver.d.ts +84 -4
  117. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  118. package/dist/generators/test-generator/utils/selector-resolver.js +229 -82
  119. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  120. package/dist/harness/audit.d.ts.map +1 -1
  121. package/dist/harness/audit.js +35 -8
  122. package/dist/harness/audit.js.map +1 -1
  123. package/dist/harness/capability.d.ts +7 -1
  124. package/dist/harness/capability.d.ts.map +1 -1
  125. package/dist/harness/capability.js +15 -12
  126. package/dist/harness/capability.js.map +1 -1
  127. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  128. package/dist/harness/config-file.d.ts +17 -0
  129. package/dist/harness/config-file.d.ts.map +1 -0
  130. package/dist/harness/config-file.js +100 -0
  131. package/dist/harness/config-file.js.map +1 -0
  132. package/dist/harness/depth-lint.d.ts.map +1 -1
  133. package/dist/harness/depth-lint.js +2 -1
  134. package/dist/harness/depth-lint.js.map +1 -1
  135. package/dist/harness/flow-check.d.ts.map +1 -1
  136. package/dist/harness/flow-check.js +6 -2
  137. package/dist/harness/flow-check.js.map +1 -1
  138. package/dist/harness/keyword-match.d.ts +59 -0
  139. package/dist/harness/keyword-match.d.ts.map +1 -0
  140. package/dist/harness/keyword-match.js +86 -0
  141. package/dist/harness/keyword-match.js.map +1 -0
  142. package/dist/harness/next-step.d.ts.map +1 -1
  143. package/dist/harness/next-step.js +2 -1
  144. package/dist/harness/next-step.js.map +1 -1
  145. package/dist/harness/parse.d.ts.map +1 -1
  146. package/dist/harness/parse.js +42 -7
  147. package/dist/harness/parse.js.map +1 -1
  148. package/dist/harness/perf.d.ts +7 -1
  149. package/dist/harness/perf.d.ts.map +1 -1
  150. package/dist/harness/perf.js +13 -10
  151. package/dist/harness/perf.js.map +1 -1
  152. package/dist/harness/quality-gates.d.ts +9 -2
  153. package/dist/harness/quality-gates.d.ts.map +1 -1
  154. package/dist/harness/quality-gates.js +16 -4
  155. package/dist/harness/quality-gates.js.map +1 -1
  156. package/dist/harness/query-catalog.js +0 -0
  157. package/dist/harness/query-catalog.js.map +1 -1
  158. package/dist/harness/read-text.d.ts +6 -0
  159. package/dist/harness/read-text.d.ts.map +1 -1
  160. package/dist/harness/read-text.js +7 -1
  161. package/dist/harness/read-text.js.map +1 -1
  162. package/dist/harness/secret-scan.d.ts +27 -1
  163. package/dist/harness/secret-scan.d.ts.map +1 -1
  164. package/dist/harness/secret-scan.js +57 -4
  165. package/dist/harness/secret-scan.js.map +1 -1
  166. package/dist/harness/sensors.d.ts +12 -0
  167. package/dist/harness/sensors.d.ts.map +1 -1
  168. package/dist/harness/sensors.js +41 -18
  169. package/dist/harness/sensors.js.map +1 -1
  170. package/dist/harness/spec-branches.d.ts +7 -20
  171. package/dist/harness/spec-branches.d.ts.map +1 -1
  172. package/dist/harness/spec-branches.js +85 -13
  173. package/dist/harness/spec-branches.js.map +1 -1
  174. package/dist/harness/spec-coverage.d.ts +16 -0
  175. package/dist/harness/spec-coverage.d.ts.map +1 -1
  176. package/dist/harness/spec-coverage.js +131 -12
  177. package/dist/harness/spec-coverage.js.map +1 -1
  178. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  179. package/dist/harness/viewpoint-ledger.js +2 -1
  180. package/dist/harness/viewpoint-ledger.js.map +1 -1
  181. package/dist/harness/write-atomic.d.ts +2 -0
  182. package/dist/harness/write-atomic.d.ts.map +1 -0
  183. package/dist/harness/write-atomic.js +62 -0
  184. package/dist/harness/write-atomic.js.map +1 -0
  185. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  186. package/dist/orchestrator/ai-skills/writer.js +15 -7
  187. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  188. package/dist/orchestrator/appium-environment-setup.d.ts +24 -0
  189. package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
  190. package/dist/orchestrator/appium-environment-setup.js +157 -0
  191. package/dist/orchestrator/appium-environment-setup.js.map +1 -0
  192. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  193. package/dist/orchestrator/assets-drift.js +11 -6
  194. package/dist/orchestrator/assets-drift.js.map +1 -1
  195. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +2 -0
  196. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
  197. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
  198. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  199. package/dist/orchestrator/project-deps-sync.js +22 -1
  200. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  201. package/dist/orchestrator/project-initializer.d.ts +9 -0
  202. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  203. package/dist/orchestrator/project-initializer.js +63 -7
  204. package/dist/orchestrator/project-initializer.js.map +1 -1
  205. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  206. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  207. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  208. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  209. package/dist/orchestrator/security-overrides.d.ts +51 -0
  210. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  211. package/dist/orchestrator/security-overrides.js +165 -0
  212. package/dist/orchestrator/security-overrides.js.map +1 -0
  213. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  214. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
  215. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
  216. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  217. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  218. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  219. package/dist/orchestrator/templates/appium-environment/README.md +83 -0
  220. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  221. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  222. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  223. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  224. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  225. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  226. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  227. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  228. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  229. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  230. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  231. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  232. package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  233. package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  234. package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  235. package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
  236. package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
  237. package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
  238. package/dist/orchestrator/templates/specs-checked-state.js +26 -3
  239. package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
  240. package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
  241. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  242. package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
  243. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
  244. package/dist/orchestrator/templates/specs-mock.js +10 -3
  245. package/dist/orchestrator/templates/specs-mock.js.map +1 -1
  246. package/dist/orchestrator/templates/specs-mock.ts +12 -4
  247. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  248. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  249. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  250. package/dist/orchestrator/templates/specs-url-assert.js +14 -2
  251. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  252. package/dist/orchestrator/templates/specs-url-assert.ts +24 -2
  253. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  254. package/package.json +3 -3
  255. package/src/cli/commands/audit.ts +12 -3
  256. package/src/cli/commands/capability.ts +63 -19
  257. package/src/cli/commands/delivery.ts +38 -16
  258. package/src/cli/commands/generate.ts +15 -7
  259. package/src/cli/commands/init.ts +3 -0
  260. package/src/cli/commands/results-merge.ts +2 -8
  261. package/src/cli/commands/results-sanitize.ts +2 -9
  262. package/src/cli/commands/setup-mobile-env.ts +64 -0
  263. package/src/cli/index.ts +2 -0
  264. package/src/dashboard/snapshot-builder.ts +6 -8
  265. package/src/exporters/feature-parser.ts +32 -2
  266. package/src/exporters/locale-variants.ts +23 -17
  267. package/src/exporters/matrix/build.ts +14 -8
  268. package/src/exporters/matrix/carry-manual-results.ts +229 -0
  269. package/src/exporters/matrix/export.ts +10 -4
  270. package/src/exporters/matrix/wording.ts +26 -1
  271. package/src/exporters/xlsx-exporter.ts +7 -2
  272. package/src/generators/test-generator/adapters/adapter-interface.ts +2 -2
  273. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  274. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  275. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  276. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  277. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  278. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  279. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  280. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  281. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  282. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  283. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  284. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  285. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  286. package/src/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  287. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
  288. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  289. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  290. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  291. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  292. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  293. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  294. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  295. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  296. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
  297. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  298. package/src/generators/test-generator/code-generator.ts +119 -19
  299. package/src/generators/test-generator/diagnostics.ts +70 -4
  300. package/src/generators/test-generator/patterns/index.ts +16 -9
  301. package/src/generators/test-generator/patterns/types.ts +15 -0
  302. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  303. package/src/generators/test-generator/step-mapper.ts +196 -13
  304. package/src/generators/test-generator/template-engine.ts +18 -0
  305. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  306. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  307. package/src/generators/test-generator/utils/selector-resolver.ts +256 -85
  308. package/src/harness/audit.ts +41 -12
  309. package/src/harness/capability.ts +23 -12
  310. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  311. package/src/harness/config-file.ts +66 -0
  312. package/src/harness/depth-lint.ts +2 -1
  313. package/src/harness/flow-check.ts +6 -2
  314. package/src/harness/keyword-match.ts +85 -0
  315. package/src/harness/next-step.ts +2 -1
  316. package/src/harness/parse.ts +42 -7
  317. package/src/harness/perf.ts +15 -7
  318. package/src/harness/quality-gates.ts +16 -4
  319. package/src/harness/query-catalog.ts +0 -0
  320. package/src/harness/read-text.ts +7 -1
  321. package/src/harness/secret-scan.ts +60 -4
  322. package/src/harness/sensors.ts +50 -19
  323. package/src/harness/spec-branches.ts +72 -14
  324. package/src/harness/spec-coverage.ts +133 -13
  325. package/src/harness/viewpoint-ledger.ts +2 -1
  326. package/src/harness/write-atomic.ts +22 -0
  327. package/src/orchestrator/ai-skills/writer.ts +18 -3
  328. package/src/orchestrator/appium-environment-setup.ts +127 -0
  329. package/src/orchestrator/assets-drift.ts +10 -5
  330. package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
  331. package/src/orchestrator/project-deps-sync.ts +23 -1
  332. package/src/orchestrator/project-initializer.ts +69 -7
  333. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  334. package/src/orchestrator/security-overrides.ts +149 -0
  335. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  336. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
  337. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
  338. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  339. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  340. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  341. package/src/orchestrator/templates/appium-environment/README.md +83 -0
  342. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  343. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  344. package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  345. package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  346. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  347. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  348. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  349. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  350. package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  351. package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  352. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  353. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  354. package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  355. package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  356. package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  357. package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
  358. package/src/orchestrator/templates/specs-checked-state.ts +31 -6
  359. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  360. package/src/orchestrator/templates/specs-mock.ts +12 -4
  361. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  362. package/src/orchestrator/templates/specs-url-assert.ts +24 -2
  363. package/src/orchestrator/templates/wdio.conf.ts +24 -1
@@ -46,6 +46,16 @@ export interface UrlExpectation {
46
46
  declared?: string;
47
47
  /** The test-data key behind `declared`, so a failure can name what an author must go fix. */
48
48
  dataRef?: string;
49
+ /**
50
+ * The page selector declared `query: any` — the query string is not part of this page's identity,
51
+ * so SILENCE from the step stops meaning "there must be no query" (#748).
52
+ *
53
+ * Emitted only for a page whose entry says so. The default stays strict: a bare page means a bare
54
+ * page, which is the #464 assertion and the reason a filter regression cannot read as green.
55
+ * It is the no-`dataRef` branch alone that this relaxes — a step that declares `with {{v}}` still
56
+ * asserts every param it names, on this page as on any other.
57
+ */
58
+ ignoreQuery?: boolean;
49
59
  }
50
60
 
51
61
  /**
@@ -113,8 +123,14 @@ function locationMatches(pathname: RegExp, hostname: RegExp | undefined, u: URL)
113
123
  * tracking/UTM param the test never cared about cannot turn it red.
114
124
  * A declared fragment is asserted too; a declared value carrying no query
115
125
  * at all constrains the path only (the compiler warns: SG-W014).
126
+ *
127
+ * `query: any` on the page's selector entry overrides the FIRST rule only (#748): silence then
128
+ * means "the query is not mine to judge" instead of "there must be none". It is the escape hatch
129
+ * for a page whose query is generated at run time — `/admins/detail?id=<the row just clicked>` —
130
+ * which the strict rule makes unassertable and `with {{v}}` cannot express, since that asserts a
131
+ * literal. The second rule is untouched: declare params and they are still all checked.
116
132
  */
117
- export function urlMatches({ pathname, hostname, declared, dataRef }: UrlExpectation): (u: URL) => boolean {
133
+ export function urlMatches({ pathname, hostname, declared, dataRef, ignoreQuery }: UrlExpectation): (u: URL) => boolean {
118
134
  return (u: URL): boolean => {
119
135
  if (!locationMatches(pathname, hostname, u)) return false;
120
136
 
@@ -123,7 +139,13 @@ export function urlMatches({ pathname, hostname, declared, dataRef }: UrlExpecta
123
139
  // undefined, and they mean opposite things: the first is a real assertion (a bare page must be
124
140
  // bare), the second is broken test-data. Only the generator knows which, and it says so by
125
141
  // emitting `dataRef` exactly when the step carried a `with {{v}}`.
126
- if (!dataRef) return u.search === '';
142
+ // `query: any` on the page entry (#748): the query is not part of this page's identity, so
143
+ // silence from the step is not an assertion about it. Without this there is NO way to express
144
+ // "this path, whatever the query" — `with {{v}}` asserts a LITERAL value, and a detail page
145
+ // keyed by `?id=<the row just clicked>` has no literal to declare. Checked before the bare-page
146
+ // rule, never instead of the declared-value rule below: a step that names params still asserts
147
+ // them here exactly as anywhere else.
148
+ if (!dataRef) return ignoreQuery ? true : u.search === '';
127
149
 
128
150
  // Silence is the whole bug class this assertion exists to kill, so an unusable declared value
129
151
  // is an ERROR, not a quiet verdict. Returning false would read as "the app is wrong"; these
@@ -48,6 +48,14 @@ function evidenceSlug(s: string): string {
48
48
  .slice(0, 80) || 'unnamed';
49
49
  }
50
50
 
51
+ /**
52
+ * True only in the launcher process. `@wdio/runner` re-parses this very config inside EVERY worker
53
+ * it spawns, so anything logged at module scope prints once per spec unless it is gated. WDIO's own
54
+ * "am I a worker" test is this env var (`@wdio/local-runner` sets it when spawning; `@wdio/utils`
55
+ * reads it) — reuse it rather than inventing a second signal.
56
+ */
57
+ const IS_LAUNCHER = !process.env.WDIO_WORKER_ID;
58
+
51
59
  /**
52
60
  * Auto-select MOBILE specs and classify each by its target OS (dual-target routing).
53
61
  *
@@ -82,7 +90,12 @@ function classifyMobileSpecs(root: string): Array<{ file: string; platform: stri
82
90
  .filter((full) => {
83
91
  try {
84
92
  return fs.readFileSync(full, 'utf-8').includes('@wdio/globals');
85
- } catch {
93
+ } catch (e) {
94
+ // A spec listed by readdir that cannot be read cannot be classified, so it is excluded.
95
+ // Excluding it SILENTLY is the fault: WDIO only fails the run when the spec list is
96
+ // EMPTY ("No specs found to run"), so dropping one spec out of ten leaves a green suite
97
+ // containing a test that never executed. Name it, and let the count below show the shortfall.
98
+ if (IS_LAUNCHER) console.warn(`[sungen] could not read ${full} — EXCLUDED from this run: ${(e as Error).message}`);
86
99
  return false;
87
100
  }
88
101
  })
@@ -98,6 +111,16 @@ const androidSpecs = specEntries.filter((e) => e.platform !== 'ios').map((e) =>
98
111
  const iosSpecs = specEntries.filter((e) => e.platform !== 'android').map((e) => e.file);
99
112
  const mobileSpecs = specEntries.map((e) => e.file); // union — top-level fallback
100
113
 
114
+ // What DISCOVERY found — a mobile suite is only trustworthy if you can see how many specs it
115
+ // resolved: WDIO fails loudly on an EMPTY list but says nothing when the list is merely short,
116
+ // which is exactly what a dropped spec looks like.
117
+ //
118
+ // Two things this number is not. It is not an error on 0: WDIO already exits 1 there
119
+ // ("No specs found to run"), and a `--shard` that legitimately receives none must still return 0.
120
+ // And under `--shard` it is the PRE-SHARD total, because ConfigParser slices `specs` down to the
121
+ // shard's portion only after this file has finished evaluating.
122
+ if (IS_LAUNCHER) console.log(`[sungen] specs: ${specEntries.length} mobile spec(s) — android ${androidSpecs.length}, ios ${iosSpecs.length}`);
123
+
101
124
  // App under test — env overrides the init-configured defaults.
102
125
  const APP_PACKAGE = process.env.APP_PACKAGE || '__APP_PACKAGE__';
103
126
  const APP_ACTIVITY = process.env.APP_ACTIVITY || '__APP_ACTIVITY__';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.31",
3
+ "version": "3.2.33",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -40,8 +40,8 @@
40
40
  "@babel/types": "^7.28.5",
41
41
  "@cucumber/gherkin": "^37.0.0",
42
42
  "@cucumber/messages": "^31.0.0",
43
- "@sungen/driver-data-factory": "3.2.31",
44
- "@sungen/driver-ui": "3.2.31",
43
+ "@sungen/driver-data-factory": "3.2.33",
44
+ "@sungen/driver-ui": "3.2.33",
45
45
  "chalk": "^5.6.2",
46
46
  "commander": "^14.0.2",
47
47
  "dotenv": "^17.2.3",
@@ -7,8 +7,11 @@ import { acceptViewpointBaseline } from '../../harness/viewpoint-baseline';
7
7
  import { findUnitDir } from '../resolve-unit';
8
8
 
9
9
 
10
- function bar(ratio: number, width = 20): string {
11
- const n = Math.round(ratio * width);
10
+ export function bar(ratio: number, width = 20): string {
11
+ // A display helper must not be able to take the CLI down whatever the model hands it: an
12
+ // out-of-range ratio made `String.repeat` throw `Invalid count value: -5`, which killed the
13
+ // run mid-scorecard so the gate, the findings and the next-step hint never printed (#762).
14
+ const n = Math.max(0, Math.min(width, Math.round((Number.isFinite(ratio) ? ratio : 0) * width)));
12
15
  return '█'.repeat(n) + '░'.repeat(width - n);
13
16
  }
14
17
 
@@ -47,6 +50,11 @@ function render(r: AuditReport): void {
47
50
  L(` ① Viewpoint Gate — page-type: ${r.gate.pageType ?? 'unknown'} — themes ${r.gate.themesCovered}/${r.gate.themesTotal} covered`);
48
51
  if (r.gate.gaps.length) for (const g of r.gate.gaps) L(` ✗ ${g.status === 'shallow' ? 'SHALLOW' : 'MISSING'}: ${g.theme}`);
49
52
  else L(' ✓ all critical themes covered (with data assertions)');
53
+ // Name the scenario behind each covered theme (#776). A bare count gives a reviewer nothing to
54
+ // check, so a keyword matching something absurd stays invisible; the evidence line makes it
55
+ // obvious. Second line of defence — it holds even if the matcher is wrong again.
56
+ for (const c of r.gate.coveredBy ?? [])
57
+ L(` ✓ ${c.theme} ← [${c.keyword}] ${c.by.length > 88 ? `${c.by.slice(0, 88)}…` : c.by}`);
50
58
  L('');
51
59
  if (r.flowQuality?.hasContract) {
52
60
  const fq = r.flowQuality;
@@ -109,7 +117,8 @@ function render(r: AuditReport): void {
109
117
  L(` ⑦ Spec coverage — FR ${r.spec.frCovered}/${r.spec.frTotal} covered [${r.spec.verdict.toUpperCase()}]`);
110
118
  for (const g of r.spec.triggerGaps) L(` ✗ TRIGGER-UNCOVERED: "${g.constraint}"${g.code ? ` (${g.code})` : ''} mandated on [${g.required.join(', ')}], only tested on [${g.found.join(', ') || 'none'}] → missing ${g.missing.join(', ')}`);
111
119
  for (const u of r.spec.uncoveredMust.slice(0, 6)) L(` ✗ SPEC-UNCOVERED: ${u.id} (MUST) — "${u.text}"`);
112
- if (!r.spec.triggerGaps.length && !r.spec.uncoveredMust.length) L(' ✓ every MUST FR + per-constraint trigger covered');
120
+ for (const c of r.spec.clauseGaps.slice(0, 6)) L(` ✗ SPEC-CLAUSE-UNCOVERED: ${c.id} (${c.polarity === 'negate' ? 'MUST NOT' : 'MUST'}) — "${c.text}"`);
121
+ if (!r.spec.triggerGaps.length && !r.spec.uncoveredMust.length && !r.spec.clauseGaps.length) L(' ✓ every MUST FR + per-constraint trigger covered');
113
122
  L('');
114
123
  }
115
124
  if (r.ledger.hasViewpoint && r.ledger.total > 0) {
@@ -4,33 +4,46 @@ import { createRequire } from 'module';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import {
7
- readCapabilities, writeCapabilities, loadDriverCatalog, driverMeta,
7
+ readCapabilities, writeCapabilities, loadDriverCatalog, driverMeta, capabilitiesPath,
8
+ type CapabilityProfile,
8
9
  } from '../../harness/capability';
9
10
  import { buildPlan, MANUAL_REASONS } from '../../harness/capability-plan';
10
11
  import { adapterRegistry } from '../../generators/test-generator/adapters';
11
12
  import { emitMobileRuntime, installMobileDriver } from '../../orchestrator/mobile-runtime-scaffolder';
13
+ import { emitAppiumEnvironment, runAppiumEnvironmentSetup } from '../../orchestrator/appium-environment-setup';
12
14
  import { coreVersion, resolveDriverVersion } from '../../orchestrator/lockstep-version';
13
15
  import { findUnitDir, resolveUnit } from '../resolve-unit';
16
+ import { readTextFile } from '../../harness/read-text';
14
17
 
15
18
  /**
16
- * Add `test:mobile` to the project's package.json scripts (if absent).
17
- * Idempotent — skips if the script is already set.
18
- * Returns true if the script was added, false if already present or no package.json.
19
+ * Add `test:mobile` to the project's package.json scripts (if absent). Idempotent.
20
+ *
21
+ * Four outcomes, not two. The old boolean collapsed "already present", "no package.json" and
22
+ * "could not parse it" into a single `false`, and the caller rendered that as
23
+ * `– skipped package.json scripts[test:mobile] (already exists)` — which, on a malformed
24
+ * package.json, states the exact opposite of the truth: the script is absent, the command promised
25
+ * it, and the next thing the user sees is `npm error Missing script: "test:mobile"`.
19
26
  */
20
- function addMobileScript(cwd: string): boolean {
27
+ type MobileScriptOutcome =
28
+ | { state: 'added' }
29
+ | { state: 'already-present' }
30
+ | { state: 'no-package-json' }
31
+ | { state: 'unreadable'; error: string };
32
+
33
+ function addMobileScript(cwd: string): MobileScriptOutcome {
21
34
  const pkgPath = path.join(cwd, 'package.json');
22
- if (!fs.existsSync(pkgPath)) return false;
35
+ if (!fs.existsSync(pkgPath)) return { state: 'no-package-json' };
23
36
  let pkg: Record<string, any>;
24
37
  try {
25
- pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
26
- } catch {
27
- return false;
38
+ pkg = JSON.parse(readTextFile(pkgPath)); // BOM-tolerant: npm reads such a file fine, so must we
39
+ } catch (e) {
40
+ return { state: 'unreadable', error: (e as Error).message.split('\n')[0] };
28
41
  }
29
42
  if (!pkg.scripts || typeof pkg.scripts !== 'object') pkg.scripts = {};
30
- if (pkg.scripts['test:mobile']) return false;
43
+ if (pkg.scripts['test:mobile']) return { state: 'already-present' };
31
44
  pkg.scripts['test:mobile'] = 'wdio run wdio.conf.ts';
32
45
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
33
- return true;
46
+ return { state: 'added' };
34
47
  }
35
48
 
36
49
  /**
@@ -157,7 +170,8 @@ export function registerCapabilityCommand(program: Command): void {
157
170
  .description('Enable a driver: install its package (if external) + record it in qa/capabilities.yaml')
158
171
  .option('--skip-install', 'Only record in the profile; do not run npm install')
159
172
  .option('--force', 'Switch the project platform even if a different one is already set (sungen is one platform per project)')
160
- .action((driver: string, o: { skipInstall?: boolean; force?: boolean }) => {
173
+ .option('--skip-env-setup', 'Mobile: emit appium-environment/ but do not run it now — re-run later: ./appium-environment/setup.sh (Windows: setup.ps1)')
174
+ .action((driver: string, o: { skipInstall?: boolean; force?: boolean; skipEnvSetup?: boolean }) => {
161
175
  try {
162
176
  const cwd = process.cwd();
163
177
  const meta = driverMeta(driver);
@@ -219,9 +233,13 @@ export function registerCapabilityCommand(program: Command): void {
219
233
  }
220
234
 
221
235
  const profile = readCapabilities(cwd);
236
+ assertProfileWritable(profile, cwd);
222
237
  const enabled = Array.from(new Set([...profile.enabled, driver]));
223
238
  const platform = meta.kind === 'platform' ? driver : profile.platform;
224
- writeCapabilities(cwd, { platform, enabled });
239
+ // Carry `verification` through. It is the recorded TEST TYPE, not a derived value: dropping
240
+ // it here silently re-derives the scope from `enabled`, handing an E2E-only project the
241
+ // @api/@query verification it had explicitly narrowed away.
242
+ writeCapabilities(cwd, { platform, enabled, verification: profile.verification });
225
243
  console.log(`✓ capability "${driver}" enabled${meta.kind === 'platform' ? ` (platform=${driver})` : ''}.`);
226
244
 
227
245
  // Emit the mobile runtime harness when the mobile platform driver is added.
@@ -234,14 +252,23 @@ export function registerCapabilityCommand(program: Command): void {
234
252
  for (const f of result.skipped) console.log(` – skipped ${f} (already exists)`);
235
253
 
236
254
  // Add test:mobile script to package.json (idempotent)
237
- if (addMobileScript(cwd)) {
238
- console.log(' ✓ created package.json scripts[test:mobile]');
239
- } else {
240
- console.log(' – skipped package.json scripts[test:mobile] (already exists)');
241
- }
255
+ const script = addMobileScript(cwd);
256
+ if (script.state === 'added') console.log(' ✓ created package.json scripts[test:mobile]');
257
+ else if (script.state === 'already-present') console.log(' – skipped package.json scripts[test:mobile] (already exists)');
258
+ else if (script.state === 'no-package-json') console.log(' ✗ NOT added package.json scripts[test:mobile] — no package.json here; run `npm init -y` first, then add: "test:mobile": "wdio run wdio.conf.ts"');
259
+ else console.log(` ✗ NOT added package.json scripts[test:mobile] — package.json is not valid JSON (${script.error}); fix it, then add: "test:mobile": "wdio run wdio.conf.ts"`);
260
+
261
+ // appium-environment/ provisioning bundle (idempotent per file) — emitted before the driver
262
+ // install, run after it so its Appium step finds the project-local CLI.
263
+ const env = emitAppiumEnvironment(cwd);
264
+ if (env.created.length > 0) console.log(` ✓ created appium-environment/ (${env.created.length} files)`);
265
+ else console.log(' – skipped appium-environment/ (already exists)');
242
266
 
243
267
  // Best-effort driver install — non-fatal so capability add still succeeds offline.
244
268
  installMobileDriver(cwd);
269
+
270
+ // --skip-install means "record only" — then provisioning the machine is not wanted either.
271
+ runAppiumEnvironmentSetup(cwd, { skip: !!(o.skipEnvSetup || o.skipInstall) });
245
272
  }
246
273
 
247
274
  console.log('');
@@ -251,6 +278,22 @@ export function registerCapabilityCommand(program: Command): void {
251
278
  }
252
279
  });
253
280
 
281
+ /**
282
+ * Every path that WRITES qa/capabilities.yaml first reads it, edits one field, and writes the whole
283
+ * file back. That round-trip is only safe when the read succeeded: an unreadable profile reads as
284
+ * "no platform, no drivers", so the write-back would replace the user's declaration with just the
285
+ * one driver being added — and `remove` would replace it with nothing at all. Refuse instead.
286
+ */
287
+ function assertProfileWritable(profile: CapabilityProfile, cwd: string): void {
288
+ if (profile.source !== 'unreadable') return;
289
+ console.error(
290
+ `✗ ${path.relative(cwd, capabilitiesPath(cwd))} exists but could not be parsed.\n` +
291
+ ` Refusing to rewrite it: this command replaces the whole file, so it would discard the\n` +
292
+ ` platform, drivers and verification scope you declared. Fix the YAML and re-run.`,
293
+ );
294
+ process.exit(1);
295
+ }
296
+
254
297
  // ── remove ─────────────────────────────────────────────────────────────
255
298
  cap
256
299
  .command('remove <driver>')
@@ -260,9 +303,10 @@ export function registerCapabilityCommand(program: Command): void {
260
303
  try {
261
304
  const cwd = process.cwd();
262
305
  const profile = readCapabilities(cwd);
306
+ assertProfileWritable(profile, cwd);
263
307
  const enabled = profile.enabled.filter((d) => d !== driver);
264
308
  const platform = profile.platform === driver ? undefined : profile.platform;
265
- writeCapabilities(cwd, { platform, enabled });
309
+ writeCapabilities(cwd, { platform, enabled, verification: profile.verification });
266
310
  const meta = driverMeta(driver);
267
311
  if (o.uninstall && meta && !adapterRegistry.hasAdapter(meta.adapter || meta.id)) {
268
312
  spawnSync('npm', ['uninstall', meta.package], { stdio: 'inherit', shell: true });
@@ -26,7 +26,7 @@ import { loadApiCatalog } from '../../exporters/api-catalog-loader';
26
26
  import { resolveApiEnvironment } from '../../exporters/api-environment';
27
27
  import { resolveWebBaseUrl } from '../../exporters/env-qa';
28
28
  import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
29
- import { scanTestDataSecrets } from '../../harness/secret-scan';
29
+ import { scanTestDataSecrets, formatSecretScan, type SecretScanResult } from '../../harness/secret-scan';
30
30
  import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
31
31
  import { mapFilePath } from '../../exporters/matrix/map-loader';
32
32
  import {
@@ -39,7 +39,9 @@ import {
39
39
  hasReview,
40
40
  } from '../../exporters/matrix/export';
41
41
  import { MatrixFinding } from '../../exporters/matrix/types';
42
+ import { ManualCarryReport } from '../../exporters/matrix/carry-manual-results';
42
43
  import { formatTestOutcome, resultTotals } from '../../exporters/matrix/result-totals';
44
+ import { readCapabilities } from '../../harness/capability';
43
45
 
44
46
  const COLOR = {
45
47
  reset: '\x1b[0m',
@@ -375,14 +377,11 @@ function getEnvironment(cwd: string): EnvironmentInfo {
375
377
  * emits one csv+xlsx per OS and skips the (empty/misleading) base aggregate.
376
378
  */
377
379
  function isMobileProject(cwd: string): boolean {
378
- try {
379
- const p = path.join(cwd, 'qa', 'capabilities.yaml');
380
- if (!fs.existsSync(p)) return false;
381
- const parsed = parseYaml(fs.readFileSync(p, 'utf-8')) as { platform?: string } | null;
382
- return parsed?.platform === 'mobile';
383
- } catch {
384
- return false;
385
- }
380
+ // Reads through `readCapabilities` rather than re-parsing the file: the third copy of this parse
381
+ // was the third place a malformed capabilities.yaml quietly read as "web". The shared reader
382
+ // reports the breakage once per process; here it still degrades to web, which is the safe
383
+ // direction (per-OS delivery is skipped, nothing is deleted).
384
+ return readCapabilities(cwd).platform === 'mobile';
386
385
  }
387
386
 
388
387
  /**
@@ -736,11 +735,20 @@ async function exportMatrixTarget(
736
735
  return null;
737
736
  }
738
737
 
739
- const { csvPath, xlsxPath } = await writeMatrixDeliverables(paths, model, opts.format ?? 'xlsx');
738
+ let deliverables: Awaited<ReturnType<typeof writeMatrixDeliverables>>;
739
+ try {
740
+ deliverables = await writeMatrixDeliverables(paths, model, opts.format ?? 'xlsx');
741
+ } catch (e) {
742
+ // The existing workbook could not be read, so writing would destroy its Manual results.
743
+ log(` ${COLOR.red}not written:${COLOR.reset} ${(e as Error).message}`);
744
+ return null;
745
+ }
746
+ const { csvPath, xlsxPath, manualCarry } = deliverables;
740
747
  const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
741
748
  const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
742
749
  const written = [xlsxPath, csvPath].filter((p): p is string => !!p).map((p) => path.relative(cwd, p));
743
750
  for (const rel of written) log(` ${COLOR.green}→ ${rel}${COLOR.reset}`);
751
+ if (manualCarry) printManualCarry(manualCarry);
744
752
  return {
745
753
  label: paths.label,
746
754
  items: model.manifest.itemCount,
@@ -751,6 +759,23 @@ async function exportMatrixTarget(
751
759
  };
752
760
  }
753
761
 
762
+ /** What happened to the tester's hand-entered Manual results on this write (#756). */
763
+ function printManualCarry(r: ManualCarryReport): void {
764
+ const kept = r.carried + r.unchecked;
765
+ if (kept + r.reset.length + r.notPlaced.length + r.duplicates.length === 0) return;
766
+ const list = (refs: string[]) => `${refs.slice(0, 4).join(', ')}${refs.length > 4 ? ', …' : ''}`;
767
+ log(` Manual results: ${kept} kept${r.unchecked > 0 ? ` (${r.unchecked} without a drift check — the file predates the fingerprint stamp)` : ''}`);
768
+ if (r.reset.length > 0) {
769
+ log(` ${COLOR.yellow}reset to Pending${COLOR.reset} — procedure changed since the result was recorded (${r.reset.length}): ${list(r.reset)}; the previous result is kept in Note`);
770
+ }
771
+ if (r.notPlaced.length > 0) {
772
+ log(` ${COLOR.yellow}not carried${COLOR.reset} — no matching manual case in the feature any more (${r.notPlaced.length}): ${list(r.notPlaced.map((n) => `${n.ref} was ${n.result}`))}`);
773
+ }
774
+ if (r.duplicates.length > 0) {
775
+ log(` ${COLOR.yellow}not carried${COLOR.reset} — a second row for the same case; only the first was kept (${r.duplicates.length}): ${list(r.duplicates.map((n) => `${n.ref} was ${n.result}`))}`);
776
+ }
777
+ }
778
+
754
779
  /**
755
780
  * One place that accounts for every target the run did NOT write, so a sweep is
756
781
  * never silently partial: scaffolds skipped by preflight and units whose gates /
@@ -934,12 +959,9 @@ export function registerDeliveryCommand(program: Command): void {
934
959
  // Deliverables are COMMITTED files; delivery is the last stop before a
935
960
  // hardcoded credential lands in one. Same scan `generate` runs.
936
961
  const scanDirs = [...new Set(toExport.map((t) => qaDir(cwd, t)))];
937
- const secretHits = scanDirs.flatMap((d) => scanTestDataSecrets(d));
938
- if (secretHits.length) {
939
- log(`\n⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):`);
940
- for (const h of secretHits.slice(0, 10)) log(` ${h.file}:${h.line} — ${h.reason}`);
941
- log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.\n`);
942
- }
962
+ const secretScan = scanDirs.map((d) => scanTestDataSecrets(d))
963
+ .reduce<SecretScanResult>((a, r) => ({ hits: [...a.hits, ...r.hits], unscanned: [...a.unscanned, ...r.unscanned] }), { hits: [], unscanned: [] });
964
+ for (const line of formatSecretScan(secretScan)) log(line);
943
965
 
944
966
  // 3. Export
945
967
  const legacy = options.legacy || options.full;
@@ -3,7 +3,7 @@ import * as path from 'path';
3
3
  import * as fs from 'fs';
4
4
  import { CodeGenerator } from '../../generators/test-generator/code-generator';
5
5
  import { adapterRegistry } from '../../generators/test-generator/adapters';
6
- import { scanTestDataSecrets } from '../../harness/secret-scan';
6
+ import { scanTestDataSecrets, formatSecretScan, type SecretScanResult } from '../../harness/secret-scan';
7
7
  import { capabilityRegistry } from '../../capabilities/registry';
8
8
  import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
9
9
  import { readCapabilities, writeCapabilities, driverMeta, loadDriverCatalog } from '../../harness/capability';
@@ -160,6 +160,17 @@ export function registerGenerateCommand(program: Command): void {
160
160
  const cap = readCapabilities(cwd);
161
161
  if (cap.platform) {
162
162
  platform = cap.platform;
163
+ } else if (cap.source === 'unreadable') {
164
+ // The file EXISTS and we failed to parse it. Scaffolding the web default here would
165
+ // overwrite whatever the user declared — a mobile project's platform, drivers and
166
+ // verification scope, gone, while the notice claimed the file was missing. Refuse:
167
+ // the one thing we know is that we do not know what this project is.
168
+ throw new Error(
169
+ `qa/capabilities.yaml exists but could not be parsed — fix the YAML and re-run.\n` +
170
+ ` Refusing to scaffold a default over it: that would discard the platform, drivers ` +
171
+ `and verification scope it declares.\n` +
172
+ ` To compile without reading it, pass --framework <web|mobile|…>.`,
173
+ );
163
174
  } else {
164
175
  platform = 'web';
165
176
  writeCapabilities(cwd, { platform: 'web', enabled: ['web'] });
@@ -225,12 +236,9 @@ export function registerGenerateCommand(program: Command): void {
225
236
  if (fs.existsSync(d)) for (const n of fs.readdirSync(d)) scanDirs.push(path.join(d, n));
226
237
  }
227
238
  }
228
- const secretHits = scanDirs.flatMap((d) => scanTestDataSecrets(d));
229
- if (secretHits.length) {
230
- console.log(`\n⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):`);
231
- for (const h of secretHits.slice(0, 10)) console.log(` ${h.file}:${h.line} — ${h.reason}`);
232
- console.log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.`);
233
- }
239
+ const secretScan = scanDirs.map((d) => scanTestDataSecrets(d))
240
+ .reduce<SecretScanResult>((a, r) => ({ hits: [...a.hits, ...r.hits], unscanned: [...a.unscanned, ...r.unscanned] }), { hits: [], unscanned: [] });
241
+ for (const line of formatSecretScan(secretScan)) console.log(line);
234
242
 
235
243
  // Advisory harness sensors (e.g. @cases/@query lint) — run via the Capability SPI,
236
244
  // never block generation. Each capability registers its own; the kernel just runs them.
@@ -13,6 +13,7 @@ export function registerInitCommand(program: Command): void {
13
13
  .option('--ios-bundle-id <id>', 'iOS bundle id for mobile (e.g. com.example.app.dev) — default app when MOBILE_PLATFORM=ios|both. Used with --mobile')
14
14
  .option('--ios-device <name>', "iOS Simulator device name for mobile (default: 'iPhone 16'). Used with --mobile")
15
15
  .option('--ios-version <ver>', "iOS Simulator platform version for mobile (default: '18.0'). Used with --mobile")
16
+ .option('--skip-env-setup', 'Mobile: emit appium-environment/ (JDK, Android SDK, Xcode CLT, env vars, Appium drivers) but do not run it now — re-run later: ./appium-environment/setup.sh (Windows: setup.ps1). Used with --mobile')
16
17
  .option(
17
18
  '--assistant <name>',
18
19
  'AI assistant to install for: claude | copilot | codex | all (repeatable). Default: claude',
@@ -30,6 +31,7 @@ export function registerInitCommand(program: Command): void {
30
31
  iosBundleId?: string;
31
32
  iosDevice?: string;
32
33
  iosVersion?: string;
34
+ skipEnvSetup?: boolean;
33
35
  assistant?: string[];
34
36
  force?: boolean;
35
37
  }) => {
@@ -55,6 +57,7 @@ export function registerInitCommand(program: Command): void {
55
57
  iosBundleId: options.iosBundleId,
56
58
  iosDevice: options.iosDevice,
57
59
  iosVersion: options.iosVersion,
60
+ skipEnvSetup: options.skipEnvSetup,
58
61
  });
59
62
  await initializer.initialize(projectName);
60
63
  } catch (error) {
@@ -13,6 +13,7 @@ import { detectReportRoot, sanitizeReportPaths } from '../../exporters/report-pa
13
13
  import { registerResultsSanitizeCommand } from './results-sanitize';
14
14
  import { resolveResultsPath } from '../../exporters/playwright-report-parser';
15
15
  import { parseSpecFile } from '../../exporters/spec-parser';
16
+ import { writeFileAtomic } from '../../harness/write-atomic';
16
17
 
17
18
  interface MergeCliOptions {
18
19
  out?: string;
@@ -125,14 +126,7 @@ export function runResultsMerge(baseArg: string | undefined, newArg: string | un
125
126
  // that file is the single source of execution truth for delivery/dashboard.
126
127
  // (Same-dir rename = same device; concurrent merges are last-rename-wins.)
127
128
  const outFile = opts.out ?? baseFile;
128
- const tmp = `${outFile}.tmp-${process.pid}`;
129
- try {
130
- fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
131
- fs.renameSync(tmp, outFile);
132
- } catch (err) {
133
- try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup */ }
134
- throw err;
135
- }
129
+ writeFileAtomic(outFile, JSON.stringify(merged, null, 2));
136
130
 
137
131
  console.log(`✓ merged ${path.basename(newFile)} into ${path.relative(cwd, outFile)} — ${replaced.length} updated, ${added.length} added, ${kept.length} kept`);
138
132
  return outFile;
@@ -16,6 +16,7 @@ import * as fs from 'fs';
16
16
  import * as path from 'path';
17
17
  import fg from 'fast-glob';
18
18
  import { detectReportRoot, sanitizeReportPaths } from '../../exporters/report-path-sanitizer';
19
+ import { writeFileAtomic } from '../../harness/write-atomic';
19
20
 
20
21
  /** Where a project keeps the JSON files that carry paths. */
21
22
  export const DEFAULT_REPORT_GLOBS = [
@@ -50,15 +51,7 @@ export function runResultsSanitize(files: string[] | undefined, cwd: string = pr
50
51
  const root = detectReportRoot(doc) ?? nearestProjectRoot(path.dirname(file)) ?? cwd;
51
52
  const { doc: clean, rewritten } = sanitizeReportPaths(doc, { root });
52
53
  if (rewritten > 0) {
53
- // Atomic write, same as `results merge`: a crash mid-write must never leave a truncated report.
54
- const tmp = `${file}.tmp-${process.pid}`;
55
- try {
56
- fs.writeFileSync(tmp, JSON.stringify(clean, null, 2) + (raw.endsWith('\n') ? '\n' : ''));
57
- fs.renameSync(tmp, file);
58
- } catch (err) {
59
- try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup */ }
60
- throw err;
61
- }
54
+ writeFileAtomic(file, JSON.stringify(clean, null, 2) + (raw.endsWith('\n') ? '\n' : ''));
62
55
  }
63
56
  out.push({ file: path.relative(cwd, file), rewritten });
64
57
  }
@@ -0,0 +1,64 @@
1
+ import { Command } from 'commander';
2
+
3
+ /**
4
+ * `sungen setup-mobile-env` — provision this machine for Appium, in a project that already exists.
5
+ *
6
+ * `sungen init --mobile` runs the same bundle, but only the person who created the project ever runs
7
+ * init. The second QA clones a ready-made repo and had no command to reach for: `capability add
8
+ * mobile` re-records a capability that is already recorded, and knowing to run
9
+ * `./appium-environment/setup.sh` by hand assumes they know the folder exists. This is that command.
10
+ *
11
+ * Emits `appium-environment/` when it is missing — a project initialised before the bundle shipped,
12
+ * or one where the folder was deleted — then runs it. Both halves are idempotent, so running it on a
13
+ * fully provisioned machine reports a wall of skips and changes nothing.
14
+ */
15
+ export function registerSetupMobileEnvCommand(program: Command): void {
16
+ program
17
+ .command('setup-mobile-env')
18
+ .description('Install the Appium environment on THIS machine (JDK, Android SDK, env vars, Xcode CLT, Appium drivers) for an existing mobile project — idempotent, safe to re-run')
19
+ .option('--check', 'Report what is missing and install nothing')
20
+ .option('--skip-android', 'Skip the JDK / Android SDK / uiautomator2 steps')
21
+ .option('--skip-ios', 'Skip the Xcode CLT / applesimutils / xcuitest steps (macOS only)')
22
+ .option('--no-profile', 'Do not write ANDROID_HOME / JAVA_HOME / PATH to your shell profile')
23
+ .option('--emit-only', 'Write appium-environment/ into the project but do not run it')
24
+ .action((o: { check?: boolean; skipAndroid?: boolean; skipIos?: boolean; profile?: boolean; emitOnly?: boolean }) => {
25
+ try {
26
+ const cwd = process.cwd();
27
+ const {
28
+ emitAppiumEnvironment,
29
+ runAppiumEnvironmentSetup,
30
+ appiumEnvironmentRerunCommand,
31
+ } = require('../../orchestrator/appium-environment-setup');
32
+
33
+ // Idempotent per file, so an edited script (a bumped cmdline-tools build, a local hint)
34
+ // survives. A project that already has the folder just sees the skip line.
35
+ const emitted = emitAppiumEnvironment(cwd);
36
+ if (emitted.created.length > 0) {
37
+ console.log(`✓ created appium-environment/ (${emitted.created.length} files)`);
38
+ } else {
39
+ console.log('– skipped appium-environment/ (already present)');
40
+ }
41
+
42
+ if (o.emitOnly) {
43
+ console.log(`\nRun it when you are ready: ${appiumEnvironmentRerunCommand()}`);
44
+ return;
45
+ }
46
+
47
+ // commander maps `--no-profile` to `profile: false`, so the flag is absent when profile is
48
+ // anything but an explicit false.
49
+ const flags: string[] = [];
50
+ if (o.check) flags.push('--check');
51
+ if (o.skipAndroid) flags.push('--skip-android');
52
+ if (o.skipIos) flags.push('--skip-ios');
53
+ if (o.profile === false) flags.push('--no-profile');
54
+
55
+ const outcome = runAppiumEnvironmentSetup(cwd, { flags });
56
+ // The bundle already printed its own per-step report and summary; exit non-zero so CI and a
57
+ // `&&` chain can act on a machine that is not ready.
58
+ if (outcome === 'failed') process.exitCode = 1;
59
+ } catch (error) {
60
+ console.error('❌ setup-mobile-env failed:', error);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ }
package/src/cli/index.ts CHANGED
@@ -9,6 +9,7 @@ import { registerInitCommand } from './commands/init';
9
9
  import { registerAddCommand } from './commands/add';
10
10
  import { registerGenerateCommand } from './commands/generate';
11
11
  import { registerMakeauthCommand } from './commands/makeauth';
12
+ import { registerSetupMobileEnvCommand } from './commands/setup-mobile-env';
12
13
  import { registerUpdateCommand } from './commands/update';
13
14
  import { registerDeliveryCommand } from './commands/delivery';
14
15
  import { registerFigmaCommand } from './commands/figma';
@@ -61,6 +62,7 @@ async function main() {
61
62
  registerAddCommand(program);
62
63
  registerGenerateCommand(program);
63
64
  registerMakeauthCommand(program);
65
+ registerSetupMobileEnvCommand(program);
64
66
  registerUpdateCommand(program);
65
67
  registerDeliveryCommand(program);
66
68
  registerFigmaCommand(program);
@@ -29,6 +29,7 @@ import { resolvePlatformAppId } from '../exporters/mobile-app-id';
29
29
  import { discoverLocaleVariants, LocaleVariant } from '../exporters/locale-variants';
30
30
  import { getPackageVersion } from '../exporters/package-info';
31
31
  import { ApiCatalogEntry, EnvironmentInfo } from '../exporters/types';
32
+ import { readCapabilities } from '../harness/capability';
32
33
  import {
33
34
  AggregateSummary,
34
35
  DashboardSnapshot,
@@ -98,14 +99,11 @@ export function buildDashboardSnapshot(options: BuildSnapshotOptions): Dashboard
98
99
  * Mirrors `isMobileProject` in src/cli/commands/delivery.ts.
99
100
  */
100
101
  function isMobileProject(cwd: string): boolean {
101
- try {
102
- const p = path.join(cwd, 'qa', 'capabilities.yaml');
103
- if (!fs.existsSync(p)) return false;
104
- const parsed = parseYaml(fs.readFileSync(p, 'utf-8')) as { platform?: string } | null;
105
- return parsed?.platform === 'mobile';
106
- } catch {
107
- return false;
108
- }
102
+ // Reads through `readCapabilities` rather than re-parsing the file: the third copy of this parse
103
+ // was the third place a malformed capabilities.yaml quietly read as "web". The shared reader
104
+ // reports the breakage once per process; here it still degrades to web, which is the safe
105
+ // direction (per-OS delivery is skipped, nothing is deleted).
106
+ return readCapabilities(cwd).platform === 'mobile';
109
107
  }
110
108
 
111
109
  function buildOneScreen(