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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/dist/capabilities/context-router.d.ts.map +1 -1
  2. package/dist/capabilities/context-router.js +15 -7
  3. package/dist/capabilities/context-router.js.map +1 -1
  4. package/dist/cli/commands/capability.d.ts.map +1 -1
  5. package/dist/cli/commands/capability.js +6 -48
  6. package/dist/cli/commands/capability.js.map +1 -1
  7. package/dist/cli/commands/changed.d.ts +14 -0
  8. package/dist/cli/commands/changed.d.ts.map +1 -0
  9. package/dist/cli/commands/changed.js +128 -0
  10. package/dist/cli/commands/changed.js.map +1 -0
  11. package/dist/cli/commands/delivery.d.ts.map +1 -1
  12. package/dist/cli/commands/delivery.js +25 -1
  13. package/dist/cli/commands/delivery.js.map +1 -1
  14. package/dist/cli/commands/generate.js +1 -1
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/cli/commands/results-merge.d.ts +19 -0
  17. package/dist/cli/commands/results-merge.d.ts.map +1 -0
  18. package/dist/cli/commands/results-merge.js +191 -0
  19. package/dist/cli/commands/results-merge.js.map +1 -0
  20. package/dist/cli/commands/update.d.ts.map +1 -1
  21. package/dist/cli/commands/update.js +100 -12
  22. package/dist/cli/commands/update.js.map +1 -1
  23. package/dist/cli/index.js +4 -0
  24. package/dist/cli/index.js.map +1 -1
  25. package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
  26. package/dist/exporters/api-catalog-loader.js +10 -3
  27. package/dist/exporters/api-catalog-loader.js.map +1 -1
  28. package/dist/exporters/api-testcase-formatter.d.ts +9 -0
  29. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  30. package/dist/exporters/api-testcase-formatter.js +55 -3
  31. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  32. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  33. package/dist/exporters/csv-exporter.js +10 -1
  34. package/dist/exporters/csv-exporter.js.map +1 -1
  35. package/dist/exporters/json-exporter.d.ts.map +1 -1
  36. package/dist/exporters/json-exporter.js +6 -1
  37. package/dist/exporters/json-exporter.js.map +1 -1
  38. package/dist/exporters/matrix/build.d.ts.map +1 -1
  39. package/dist/exporters/matrix/build.js +10 -0
  40. package/dist/exporters/matrix/build.js.map +1 -1
  41. package/dist/exporters/matrix/export.js +1 -1
  42. package/dist/exporters/matrix/export.js.map +1 -1
  43. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  44. package/dist/exporters/matrix/render-csv.js +5 -1
  45. package/dist/exporters/matrix/render-csv.js.map +1 -1
  46. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  47. package/dist/exporters/matrix/render-xlsx.js +4 -1
  48. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  49. package/dist/exporters/matrix/types.d.ts +7 -0
  50. package/dist/exporters/matrix/types.d.ts.map +1 -1
  51. package/dist/exporters/matrix/types.js.map +1 -1
  52. package/dist/exporters/playwright-report-parser.d.ts +17 -2
  53. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  54. package/dist/exporters/playwright-report-parser.js +68 -8
  55. package/dist/exporters/playwright-report-parser.js.map +1 -1
  56. package/dist/exporters/results-merge.d.ts +46 -0
  57. package/dist/exporters/results-merge.d.ts.map +1 -0
  58. package/dist/exporters/results-merge.js +148 -0
  59. package/dist/exporters/results-merge.js.map +1 -0
  60. package/dist/exporters/selector-key-resolver.d.ts +4 -0
  61. package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
  62. package/dist/exporters/selector-key-resolver.js +101 -1
  63. package/dist/exporters/selector-key-resolver.js.map +1 -1
  64. package/dist/exporters/types.d.ts +8 -1
  65. package/dist/exporters/types.d.ts.map +1 -1
  66. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  67. package/dist/exporters/xlsx-report-builder.js +5 -4
  68. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  69. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  70. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  71. package/dist/generators/gherkin-parser/index.js +52 -1
  72. package/dist/generators/gherkin-parser/index.js.map +1 -1
  73. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
  74. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  75. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
  76. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  77. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  78. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  79. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/biometric-action.hbs +9 -0
  80. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
  81. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  82. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/deep-link-action.hbs +8 -0
  83. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  84. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/location-services-action.hbs +11 -0
  85. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
  86. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
  87. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
  88. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
  89. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
  90. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
  91. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.hbs +5 -0
  92. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  93. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +6 -4
  94. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
  95. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -0
  96. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  97. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +3 -0
  98. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
  99. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
  100. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +302 -29
  101. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
  102. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  103. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  104. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  105. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  106. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  107. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  108. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  109. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  110. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  113. package/dist/generators/test-generator/code-generator.d.ts +6 -0
  114. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  115. package/dist/generators/test-generator/code-generator.js +80 -7
  116. package/dist/generators/test-generator/code-generator.js.map +1 -1
  117. package/dist/generators/test-generator/diagnostics.d.ts +60 -6
  118. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  119. package/dist/generators/test-generator/diagnostics.js +59 -5
  120. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  121. package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
  122. package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
  123. package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
  124. package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
  125. package/dist/generators/test-generator/patterns/index.d.ts +19 -12
  126. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  127. package/dist/generators/test-generator/patterns/index.js +58 -38
  128. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  129. package/dist/generators/test-generator/patterns/types.d.ts +23 -0
  130. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  131. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
  132. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
  133. package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
  134. package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
  135. package/dist/generators/test-generator/recipe-expander.js +9 -1
  136. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  137. package/dist/generators/test-generator/reset-tier.d.ts +68 -0
  138. package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
  139. package/dist/generators/test-generator/reset-tier.js +94 -0
  140. package/dist/generators/test-generator/reset-tier.js.map +1 -0
  141. package/dist/generators/test-generator/step-mapper.d.ts +39 -0
  142. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  143. package/dist/generators/test-generator/step-mapper.js +144 -5
  144. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  145. package/dist/generators/test-generator/template-engine.d.ts +2 -10
  146. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  147. package/dist/generators/test-generator/template-engine.js +19 -2
  148. package/dist/generators/test-generator/template-engine.js.map +1 -1
  149. package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
  150. package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
  151. package/dist/generators/test-generator/utils/format-pattern.js +82 -0
  152. package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
  153. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
  154. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
  155. package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
  156. package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
  157. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
  158. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
  159. package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
  160. package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
  161. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
  162. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  163. package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
  164. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  165. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  166. package/dist/generators/test-generator/utils/selector-resolver.js +24 -1
  167. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  168. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
  169. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
  170. package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
  171. package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
  172. package/dist/harness/annotation-overrides.d.ts +0 -12
  173. package/dist/harness/annotation-overrides.d.ts.map +1 -1
  174. package/dist/harness/annotation-overrides.js +9 -5
  175. package/dist/harness/annotation-overrides.js.map +1 -1
  176. package/dist/harness/audit.d.ts.map +1 -1
  177. package/dist/harness/audit.js +7 -5
  178. package/dist/harness/audit.js.map +1 -1
  179. package/dist/harness/capability-plan.d.ts +2 -1
  180. package/dist/harness/capability-plan.d.ts.map +1 -1
  181. package/dist/harness/capability-plan.js +14 -4
  182. package/dist/harness/capability-plan.js.map +1 -1
  183. package/dist/harness/capability.d.ts +11 -0
  184. package/dist/harness/capability.d.ts.map +1 -1
  185. package/dist/harness/capability.js +8 -0
  186. package/dist/harness/capability.js.map +1 -1
  187. package/dist/harness/catalog/drivers.yaml +5 -0
  188. package/dist/harness/parse.d.ts +1 -0
  189. package/dist/harness/parse.d.ts.map +1 -1
  190. package/dist/harness/parse.js +6 -1
  191. package/dist/harness/parse.js.map +1 -1
  192. package/dist/harness/run-manifest.d.ts +58 -0
  193. package/dist/harness/run-manifest.d.ts.map +1 -0
  194. package/dist/harness/run-manifest.js +264 -0
  195. package/dist/harness/run-manifest.js.map +1 -0
  196. package/dist/harness/script-check.d.ts.map +1 -1
  197. package/dist/harness/script-check.js +6 -2
  198. package/dist/harness/script-check.js.map +1 -1
  199. package/dist/harness/sensors.d.ts.map +1 -1
  200. package/dist/harness/sensors.js +5 -1
  201. package/dist/harness/sensors.js.map +1 -1
  202. package/dist/harness/viewpoint-ledger.d.ts +1 -1
  203. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  204. package/dist/harness/viewpoint-ledger.js +25 -4
  205. package/dist/harness/viewpoint-ledger.js.map +1 -1
  206. package/dist/index.d.ts +1 -0
  207. package/dist/index.d.ts.map +1 -1
  208. package/dist/index.js +5 -1
  209. package/dist/index.js.map +1 -1
  210. package/dist/orchestrator/lockstep-version.d.ts +22 -0
  211. package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
  212. package/dist/orchestrator/lockstep-version.js +58 -0
  213. package/dist/orchestrator/lockstep-version.js.map +1 -0
  214. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
  215. package/dist/orchestrator/mobile-runtime-scaffolder.js +5 -1
  216. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
  217. package/dist/orchestrator/project-deps-sync.d.ts +39 -0
  218. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
  219. package/dist/orchestrator/project-deps-sync.js +175 -0
  220. package/dist/orchestrator/project-deps-sync.js.map +1 -0
  221. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  222. package/dist/orchestrator/project-initializer.js +5 -2
  223. package/dist/orchestrator/project-initializer.js.map +1 -1
  224. package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  225. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  226. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  227. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  228. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  229. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +120 -10
  230. package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  231. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +31 -0
  232. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  233. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
  234. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  235. package/dist/orchestrator/templates/env.appium.example +21 -0
  236. package/dist/orchestrator/templates/specs-api.d.ts +4 -2
  237. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  238. package/dist/orchestrator/templates/specs-api.js +233 -10
  239. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  240. package/dist/orchestrator/templates/specs-api.ts +214 -11
  241. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  242. package/dist/orchestrator/templates/specs-db.js +79 -6
  243. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  244. package/dist/orchestrator/templates/specs-db.ts +70 -7
  245. package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
  246. package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
  247. package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
  248. package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
  249. package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
  250. package/dist/orchestrator/templates/specs-test-data.ts +53 -6
  251. package/dist/orchestrator/templates/wdio.conf.ts +126 -10
  252. package/dist/utils/selector-types.d.ts +1 -1
  253. package/dist/utils/selector-types.d.ts.map +1 -1
  254. package/dist/utils/selector-types.js +1 -0
  255. package/dist/utils/selector-types.js.map +1 -1
  256. package/package.json +8 -6
  257. package/src/capabilities/context-router.ts +13 -5
  258. package/src/cli/commands/capability.ts +4 -38
  259. package/src/cli/commands/changed.ts +104 -0
  260. package/src/cli/commands/delivery.ts +27 -2
  261. package/src/cli/commands/generate.ts +1 -1
  262. package/src/cli/commands/results-merge.ts +153 -0
  263. package/src/cli/commands/update.ts +72 -13
  264. package/src/cli/index.ts +4 -0
  265. package/src/exporters/api-catalog-loader.ts +10 -2
  266. package/src/exporters/api-testcase-formatter.ts +59 -3
  267. package/src/exporters/csv-exporter.ts +11 -2
  268. package/src/exporters/json-exporter.ts +7 -2
  269. package/src/exporters/matrix/build.ts +9 -0
  270. package/src/exporters/matrix/export.ts +1 -1
  271. package/src/exporters/matrix/render-csv.ts +5 -1
  272. package/src/exporters/matrix/render-xlsx.ts +4 -1
  273. package/src/exporters/matrix/types.ts +7 -0
  274. package/src/exporters/playwright-report-parser.ts +68 -9
  275. package/src/exporters/results-merge.ts +150 -0
  276. package/src/exporters/selector-key-resolver.ts +81 -1
  277. package/src/exporters/types.ts +8 -1
  278. package/src/exporters/xlsx-report-builder.ts +5 -4
  279. package/src/generators/gherkin-parser/index.ts +55 -1
  280. package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
  281. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
  282. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  283. package/src/generators/test-generator/adapters/appium/templates/steps/actions/biometric-action.hbs +9 -0
  284. package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
  285. package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  286. package/src/generators/test-generator/adapters/appium/templates/steps/actions/deep-link-action.hbs +8 -0
  287. package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  288. package/src/generators/test-generator/adapters/appium/templates/steps/actions/location-services-action.hbs +11 -0
  289. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
  290. package/src/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
  291. package/src/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
  292. package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
  293. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
  294. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
  295. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.hbs +5 -0
  296. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  297. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +6 -4
  298. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
  299. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -0
  300. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  301. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +3 -0
  302. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
  303. package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
  304. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +302 -29
  305. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
  306. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  307. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  308. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  309. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  310. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  311. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  312. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  313. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  314. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  315. package/src/generators/test-generator/code-generator.ts +86 -7
  316. package/src/generators/test-generator/diagnostics.ts +60 -6
  317. package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
  318. package/src/generators/test-generator/patterns/index.ts +58 -33
  319. package/src/generators/test-generator/patterns/types.ts +36 -0
  320. package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
  321. package/src/generators/test-generator/recipe-expander.ts +9 -1
  322. package/src/generators/test-generator/reset-tier.ts +93 -0
  323. package/src/generators/test-generator/step-mapper.ts +149 -6
  324. package/src/generators/test-generator/template-engine.ts +22 -3
  325. package/src/generators/test-generator/utils/format-pattern.ts +79 -0
  326. package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
  327. package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
  328. package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
  329. package/src/generators/test-generator/utils/selector-resolver.ts +26 -1
  330. package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
  331. package/src/harness/annotation-overrides.ts +10 -5
  332. package/src/harness/audit.ts +7 -5
  333. package/src/harness/capability-plan.ts +13 -6
  334. package/src/harness/capability.ts +16 -0
  335. package/src/harness/catalog/drivers.yaml +5 -0
  336. package/src/harness/parse.ts +7 -1
  337. package/src/harness/run-manifest.ts +254 -0
  338. package/src/harness/script-check.ts +6 -2
  339. package/src/harness/sensors.ts +5 -1
  340. package/src/harness/viewpoint-ledger.ts +26 -5
  341. package/src/index.ts +1 -0
  342. package/src/orchestrator/lockstep-version.ts +46 -0
  343. package/src/orchestrator/mobile-runtime-scaffolder.ts +5 -1
  344. package/src/orchestrator/project-deps-sync.ts +175 -0
  345. package/src/orchestrator/project-initializer.ts +5 -2
  346. package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  347. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  348. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  349. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  350. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  351. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +120 -10
  352. package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  353. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +31 -0
  354. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  355. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
  356. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  357. package/src/orchestrator/templates/env.appium.example +21 -0
  358. package/src/orchestrator/templates/specs-api.ts +214 -11
  359. package/src/orchestrator/templates/specs-db.ts +70 -7
  360. package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
  361. package/src/orchestrator/templates/specs-test-data.ts +53 -6
  362. package/src/orchestrator/templates/wdio.conf.ts +126 -10
  363. package/src/utils/selector-types.ts +1 -0
@@ -27,7 +27,7 @@ const ident = (s: string): string => {
27
27
  // SQL engines: verified via the direct guarded-SELECT path and needing the `$n`→`?` placeholder
28
28
  // rewrite. This literal is the single source of truth for that rewrite condition and is quoted, by
29
29
  // name, in the sungen-gherkin-syntax skill's "direct SQL verification" note — keep them in sync.
30
- const SUPPORTED_ENGINES = ['postgres', 'sqlite', 'mysql'] as const;
30
+ const SUPPORTED_ENGINES = ['postgres', 'sqlite', 'device-sqlite', 'mysql'] as const;
31
31
  // NoSQL engines: implement the Engine contract natively (no SQL rewrite). Tracked separately so the
32
32
  // SQL-rewrite guard and its skill-parity test stay pinned to the three SQL engines above.
33
33
  const NOSQL_ENGINES = ['cosmos', 'mongodb', 'dynamodb'] as const;
@@ -43,9 +43,15 @@ interface SshConfig {
43
43
  }
44
44
 
45
45
  interface DataSourceConfig {
46
- engine: 'postgres' | 'mysql' | 'sqlite' | 'cosmos' | 'mongodb' | 'dynamodb';
46
+ engine: 'postgres' | 'mysql' | 'sqlite' | 'device-sqlite' | 'cosmos' | 'mongodb' | 'dynamodb';
47
47
  url: string;
48
48
  readonly?: boolean;
49
+ // device-sqlite (mobile): an ON-DEVICE app database, pulled FRESH per query through the active
50
+ // Appium session (pullFile's `@<package>/<path>` app-container syntax — run-as under the hood,
51
+ // so it needs a debuggable build or an emulator). `package` = the app id, `db_path` = the
52
+ // path inside the app's data dir (e.g. databases/app.db). No `url`. Same SQLite dialect.
53
+ package?: string;
54
+ db_path?: string;
49
55
  statement_timeout_ms?: number;
50
56
  max_rows?: number;
51
57
  // Fallback path: tunnel the DB SOCKET through an SSH bastion. DB-only — the browser/E2E
@@ -160,7 +166,7 @@ function loadConfig(): Record<string, DataSourceConfig> {
160
166
  * reorder is essential there too (an author-written PartiQL `WHERE sk = $2 AND pk = $1`).
161
167
  */
162
168
  export function rewritePlaceholders(engine: string, sql: string, params: any[]): { sql: string; params: any[] } {
163
- if (engine !== 'sqlite' && engine !== 'mysql' && engine !== 'dynamodb') return { sql, params };
169
+ if (engine !== 'sqlite' && engine !== 'device-sqlite' && engine !== 'mysql' && engine !== 'dynamodb') return { sql, params };
164
170
  let out = '';
165
171
  let inStr = false;
166
172
  const newParams: any[] = [];
@@ -517,10 +523,11 @@ class DataSource {
517
523
  const { key, conf } = this.cfg(name);
518
524
  if (this.engines.has(key)) return { engine: this.engines.get(key)!, conf };
519
525
  let engine: Engine;
520
- if (conf.engine === 'postgres' || conf.engine === 'mysql' || conf.engine === 'sqlite') {
521
- // url is required for the SQL engines (host-based, or a sqlite file path). NoSQL engines that
522
- // have no connection URL (e.g. DynamoDB region+creds) supply their own config and skip this.
523
- if (!conf.url) throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
526
+ if (conf.engine === 'postgres' || conf.engine === 'mysql' || conf.engine === 'sqlite' || conf.engine === 'device-sqlite') {
527
+ // url is required for the HOST-based SQL engines (or a sqlite file path). device-sqlite has
528
+ // no url the file comes off the device (package + db_path, validated in its exec builder);
529
+ // NoSQL engines without a URL (e.g. DynamoDB region+creds) also supply their own config.
530
+ if (!conf.url && conf.engine !== 'device-sqlite') throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
524
531
  engine = new SqlEngine(conf.engine, await this.sqlExec(key, conf));
525
532
  } else if (conf.engine === 'cosmos') {
526
533
  engine = this.cosmosEngine(key, conf);
@@ -586,6 +593,62 @@ class DataSource {
586
593
  }
587
594
  };
588
595
  }
596
+ // device-sqlite — an ON-DEVICE app database. Every query pulls a FRESH copy through the
597
+ // active Appium session so the oracle always reads CURRENT device state (persistence checks
598
+ // assert AFTER actions). pullFile's `@<package>/<path>` app-container syntax uses run-as
599
+ // under the hood → needs a debuggable build or an emulator. SQLite WAL sidecars (-wal/-shm)
600
+ // are pulled best-effort alongside: Room defaults to WAL, where recent commits live in the
601
+ // -wal file — without it the main file silently misses the newest writes.
602
+ if (conf.engine === 'device-sqlite') {
603
+ if (!conf.package || !conf.db_path) {
604
+ throw new Error(`Data Driver: datasource "${key}" (device-sqlite) needs package: and db_path: (path inside the app data dir, e.g. databases/app.db).`);
605
+ }
606
+ const pkg = conf.package;
607
+ const rel = conf.db_path.replace(/^\/+/, '');
608
+ return async (sql, params) => {
609
+ const wd = (globalThis as any).driver;
610
+ if (!wd || typeof wd.pullFile !== 'function') {
611
+ throw new Error(`Data Driver: datasource "${key}" (device-sqlite) needs an active Appium session — device @query steps only run inside mobile specs.`);
612
+ }
613
+ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path');
614
+ const base = path.join(os.tmpdir(), `sungen-devdb-${process.pid}-${Date.now()}`);
615
+ const files: string[] = [];
616
+ const pull = async (suffix: string, required: boolean): Promise<void> => {
617
+ let b64: string | null = null;
618
+ let pullErr: unknown;
619
+ try {
620
+ b64 = await wd.pullFile(`@${pkg}/${rel}${suffix}`);
621
+ } catch (e) {
622
+ pullErr = e;
623
+ // pullFile's container route does `run-as cp → /data/local/tmp`, which newer Android
624
+ // blocks for the app UID. Fallback: stream base64 over run-as via `mobile: shell`
625
+ // (opt-in — APPIUM_ALLOW_ADB_SHELL=1, see the wdio.conf template).
626
+ try {
627
+ const out = await wd.execute('mobile: shell', { command: 'run-as', args: [pkg, 'base64', `${rel}${suffix}`] });
628
+ b64 = String(out).replace(/\s+/g, '');
629
+ } catch (e2) {
630
+ if (required) {
631
+ throw new Error(`Data Driver: could not pull @${pkg}/${rel} from the device. pullFile: ${pullErr} · shell fallback: ${e2} — needs a DEBUGGABLE build; if pullFile's container route fails on this Android version, set APPIUM_ALLOW_ADB_SHELL=1 (.env.appium).`);
632
+ }
633
+ return;
634
+ }
635
+ }
636
+ if (b64 == null) return;
637
+ fs.writeFileSync(base + suffix, Buffer.from(b64, 'base64'));
638
+ files.push(base + suffix);
639
+ };
640
+ await pull('', true);
641
+ await pull('-wal', false);
642
+ await pull('-shm', false);
643
+ try {
644
+ const Database = require('better-sqlite3');
645
+ const db = new Database(base, { readonly: false }); // WAL recovery needs write access to the TEMP copy
646
+ try { return db.prepare(sql).all(...params); } finally { db.close(); }
647
+ } finally {
648
+ for (const f of files) { try { fs.unlinkSync(f); } catch { /* best-effort */ } }
649
+ }
650
+ };
651
+ }
589
652
  // sqlite
590
653
  if (conf.ssh) console.warn(`Data Driver: datasource "${key}" sets ssh: but engine is sqlite (file-based) — ssh ignored.`);
591
654
  const Database = require('better-sqlite3');
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sungen regex-escape helper — auto-generated into specs/regex-escape.ts. DO NOT EDIT.
3
+ *
4
+ * Test-data values are LITERAL TEXT. When a generated assertion needs an anchored
5
+ * match (`filter({ hasText: /^…$/ })`), the runtime value is interpolated into a
6
+ * `new RegExp(...)` — so a value containing `( [ $ . * +` would either throw a
7
+ * SyntaxError or silently match the wrong element unless it is escaped HERE, at
8
+ * runtime, where the real value is finally known. Template-time escaping cannot do
9
+ * this: at generate time the value is still a placeholder marker.
10
+ */
11
+ /** Escape a runtime string so RegExp treats every character literally. */
12
+ export declare function escapeRegExp(text: string): string;
13
+ //# sourceMappingURL=specs-regex-escape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specs-regex-escape.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-regex-escape.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AAEH,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /* eslint-disable */
3
+ /**
4
+ * Sungen regex-escape helper — auto-generated into specs/regex-escape.ts. DO NOT EDIT.
5
+ *
6
+ * Test-data values are LITERAL TEXT. When a generated assertion needs an anchored
7
+ * match (`filter({ hasText: /^…$/ })`), the runtime value is interpolated into a
8
+ * `new RegExp(...)` — so a value containing `( [ $ . * +` would either throw a
9
+ * SyntaxError or silently match the wrong element unless it is escaped HERE, at
10
+ * runtime, where the real value is finally known. Template-time escaping cannot do
11
+ * this: at generate time the value is still a placeholder marker.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.escapeRegExp = escapeRegExp;
15
+ /** Escape a runtime string so RegExp treats every character literally. */
16
+ function escapeRegExp(text) {
17
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ }
19
+ //# sourceMappingURL=specs-regex-escape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specs-regex-escape.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-regex-escape.ts"],"names":[],"mappings":";AAAA,oBAAoB;AACpB;;;;;;;;;GASG;;AAGH,oCAEC;AAHD,0EAA0E;AAC1E,SAAgB,YAAY,CAAC,IAAY;IACvC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,16 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Sungen regex-escape helper — auto-generated into specs/regex-escape.ts. DO NOT EDIT.
4
+ *
5
+ * Test-data values are LITERAL TEXT. When a generated assertion needs an anchored
6
+ * match (`filter({ hasText: /^…$/ })`), the runtime value is interpolated into a
7
+ * `new RegExp(...)` — so a value containing `( [ $ . * +` would either throw a
8
+ * SyntaxError or silently match the wrong element unless it is escaped HERE, at
9
+ * runtime, where the real value is finally known. Template-time escaping cannot do
10
+ * this: at generate time the value is still a placeholder marker.
11
+ */
12
+
13
+ /** Escape a runtime string so RegExp treats every character literally. */
14
+ export function escapeRegExp(text: string): string {
15
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+ }
@@ -50,16 +50,42 @@ export class TestDataLoader {
50
50
  if (value === undefined || value === null) {
51
51
  throw new Error(`Test data key not found: ${key}`);
52
52
  }
53
- // A projected array (e.g. `{{q.rows[*].name}}`) serializes as a JSON list so the full
54
- // ordered set is preserved for the assert-compare — String([...]) would flatten it to a
55
- // lossy comma-join that can't be distinguished from a scalar containing commas.
56
- const result = Array.isArray(value)
57
- ? this.interpolate(JSON.stringify(value))
53
+ // A projected array (e.g. `{{q.rows[*].name}}`) and a PLAIN object (an API response
54
+ // body) serialize as JSON text — String([...]) flattens to a lossy comma-join and
55
+ // String({}) is "[object Object]", both un-assertable. Embedded `{{ref}}`s interpolate
56
+ // INSIDE the value before stringify, so a ref carrying quotes/backslashes can never
57
+ // corrupt the serialized JSON (the old order substituted raw text into JSON text).
58
+ // Only PLAIN objects take this path: Date/Buffer/ObjectId values bound by @query from
59
+ // a real driver keep their String()/toString() output — JSON.stringify would wrap a
60
+ // Date in quotes and flip every assertion that already compares its string form.
61
+ const result = isJsonContainer(value)
62
+ ? JSON.stringify(this.interpolateDeep(value))
58
63
  : this.interpolate(String(value));
59
64
  assertNoUnresolvedEnvRef(key, result);
60
65
  return result;
61
66
  }
62
67
 
68
+ /**
69
+ * Interpolate `{{ref}}`s in every string leaf AND key of a JSON container,
70
+ * recursively (matching the old whole-JSON-text pass). Non-plain nested
71
+ * objects are passed through untouched — JSON.stringify serializes them via
72
+ * toJSON. A truly cyclic object throws a RangeError; every current bind
73
+ * source (JSON.parse output, DB rows, YAML after load-time resolution) is
74
+ * acyclic. Built with Object.fromEntries, which DEFINES data properties — a
75
+ * body field literally named "__proto__" survives instead of silently
76
+ * rewiring the result's prototype to external input.
77
+ */
78
+ private interpolateDeep(value: any): any {
79
+ if (typeof value === 'string') return this.interpolate(value);
80
+ if (Array.isArray(value)) return value.map((v) => this.interpolateDeep(v));
81
+ if (isPlainObject(value)) {
82
+ return Object.fromEntries(
83
+ Object.entries(value).map(([k, v]) => [this.interpolate(k), this.interpolateDeep(v)])
84
+ );
85
+ }
86
+ return value;
87
+ }
88
+
63
89
  /**
64
90
  * Resolve a key to its RAW, uncoerced value (array/object/number kept as-is) — for binding
65
91
  * DB query / API request params, where type-strict sinks (MySQL numeric columns, MongoDB,
@@ -68,7 +94,8 @@ export class TestDataLoader {
68
94
  * as get()), so an override like `token={{login.body.token}}` or `id={{prefix}}-001` still
69
95
  * interpolates — only the final scalar/array/object type is preserved uncoerced. Arrays and
70
96
  * objects are returned as-is; refs embedded inside their elements are not interpolated (unlike
71
- * get()'s JSON-text pass), since raw() callers bind structured values directly, not text.
97
+ * get(), which interpolates every string leaf of a plain container before serializing), since
98
+ * raw() callers bind structured values directly, not text.
72
99
  * Throws on missing, mirroring get()'s guard.
73
100
  */
74
101
  raw(key: string): any {
@@ -202,6 +229,26 @@ export class TestDataLoader {
202
229
  }
203
230
  }
204
231
 
232
+ /**
233
+ * A value get() must serialize as JSON: an array, or a PLAIN object (literal or
234
+ * null-prototype). Class instances at the TOP level (Date, Buffer, ObjectId…,
235
+ * and driver row wrappers like mysql2's RowDataPacket) are NOT containers —
236
+ * they keep the String()/toString() path their existing assertions rely on;
237
+ * nested inside a plain container they serialize through JSON.stringify/toJSON.
238
+ */
239
+ function isJsonContainer(value: any): boolean {
240
+ return Array.isArray(value) || isPlainObject(value);
241
+ }
242
+
243
+ // Prototype check only — a `value.constructor === Object` probe is defeated by
244
+ // a body FIELD literally named "constructor" (JSON.parse gives it as an own
245
+ // property), which would silently fall back to "[object Object]" again.
246
+ function isPlainObject(value: any): boolean {
247
+ if (value === null || typeof value !== 'object') return false;
248
+ const proto = Object.getPrototypeOf(value);
249
+ return proto === Object.prototype || proto === null;
250
+ }
251
+
205
252
  function loadYamlSync(filePath: string): Record<string, any> | null {
206
253
  if (!fs.existsSync(filePath)) return null;
207
254
  const content = fs.readFileSync(filePath, 'utf-8');
@@ -178,6 +178,80 @@ function resolveLocaleCaps(): Record<string, unknown> {
178
178
  }
179
179
  const localeCaps = resolveLocaleCaps();
180
180
 
181
+ // ── Connectivity self-heal (#615) ───────────────────────────────────────────
182
+ // The connectivity/location steps mutate DEVICE state (airplane, wifi, data, GPS). A run that
183
+ // aborts mid-toggle — e.g. an Appium session timeout during an airplane round-trip — leaves the
184
+ // device offline as PERSISTENT state, so every later launch fails with the app's network error
185
+ // and the next run dies at cold start. Restore the baseline around the run via host-side adb:
186
+ // it works even when no Appium session survives (a spec-level after() cannot — the session is
187
+ // exactly what died). Android-only (the mutating steps are Android-only), idempotent (on/on/on
188
+ // is the baseline every suite assumes; offline scenarios set their own state mid-test), and
189
+ // best-effort per command (a device lacking one command just skips that line).
190
+ function restoreConnectivity(hook: string): void {
191
+ if (!RUN_ANDROID) return;
192
+ // Appium finds adb via ANDROID_HOME — PATH may not carry it (fresh CI runner), which would
193
+ // make every heal command fail silently. Resolve the same way Appium does, fall back to PATH.
194
+ const adbBin = process.env.ANDROID_HOME ? path.join(process.env.ANDROID_HOME, 'platform-tools', 'adb') : 'adb';
195
+ const adb = `"${adbBin}" -s ${process.env.ANDROID_UDID || '__ANDROID_UDID__'} shell`;
196
+ // stderr ignored: with a detached device the reads would otherwise spray raw adb errors on every run.
197
+ const sh = (cmd: string) => execSync(`${adb} ${cmd}`, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
198
+ // Detect-first: read each state and only touch what is actually wedged, so a healthy run
199
+ // costs four quick reads, mutates nothing, and logs nothing. Each fix lists every command
200
+ // variant (emulator `svc`, real-device `cmd`, API<30 fallbacks) — all best-effort, same
201
+ // direction, idempotent, so running the inapplicable ones is harmless.
202
+ let touched = false;
203
+ const heal = (read: string, wedged: string, fixes: string[]) => {
204
+ try {
205
+ if (sh(read) !== wedged) return;
206
+ for (const fix of fixes) { try { sh(fix); } catch { /* variant not on this API — fine */ } }
207
+ touched = true;
208
+ } catch { /* unreadable state (device gone / key absent) — leave untouched */ }
209
+ };
210
+ heal('settings get global airplane_mode_on', '1', [
211
+ 'cmd connectivity airplane-mode disable', // API 30+
212
+ 'settings put global airplane_mode_on 0', // API<30 wedge (settings+broadcast)
213
+ 'am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false',
214
+ ]);
215
+ heal('settings get global wifi_on', '0', [
216
+ 'svc wifi enable', // emulator flavor
217
+ 'cmd -w wifi set-wifi-enabled enabled', // real-device flavor (svc can no-op silently)
218
+ ]);
219
+ heal('settings get global mobile_data', '0', [
220
+ 'svc data enable',
221
+ 'cmd phone data enable',
222
+ ]);
223
+ heal('cmd location is-location-enabled', 'false', ['cmd location set-location-enabled true']);
224
+ if (!touched) return;
225
+ console.log(`[sungen] connectivity self-heal (${hook}): device was offline — a previous run likely aborted mid-toggle. Baseline restored (airplane off, wifi/data/location on).`);
226
+ // Radios re-associate asynchronously (emulator wifi can take 10-30s) — wait for REAL IP
227
+ // connectivity before the session starts, or the app's first cold start still lands on its
228
+ // network-error dialog and the auth/first assert fails despite the heal.
229
+ for (let i = 0; i < 10; i++) {
230
+ try { execSync(`${adb} ping -c 1 -W 2 8.8.8.8`, { stdio: 'ignore', timeout: 8000 }); return; } catch { /* not yet */ }
231
+ try { execSync(`${adb} sleep 2`, { stdio: 'ignore', timeout: 8000 }); } catch { /* device gone — stop waiting */ return; }
232
+ }
233
+ console.log('[sungen] connectivity self-heal: no ICMP reply within ~30s — continuing (fine if this network blocks ping; otherwise the device may need a cold boot).');
234
+ }
235
+
236
+ // Evidence readability: render a dot at every injected tap so screen recordings show WHERE the
237
+ // test touched. Android-only (iOS has no supported equivalent). The pre-run value is remembered
238
+ // and restored on onComplete, so a developer's own device setting is not permanently mutated.
239
+ let showTouchesBefore: string | null = null;
240
+ function setShowTouches(hook: 'onPrepare' | 'onComplete'): void {
241
+ if (!RUN_ANDROID) return;
242
+ const adbBin = process.env.ANDROID_HOME ? path.join(process.env.ANDROID_HOME, 'platform-tools', 'adb') : 'adb';
243
+ const adb = `"${adbBin}" -s ${process.env.ANDROID_UDID || '__ANDROID_UDID__'} shell`;
244
+ const sh = (cmd: string) => execSync(`${adb} ${cmd}`, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
245
+ try {
246
+ if (hook === 'onPrepare') {
247
+ showTouchesBefore = sh('settings get system show_touches');
248
+ if (showTouchesBefore !== '1') sh('settings put system show_touches 1');
249
+ } else if (showTouchesBefore !== null && showTouchesBefore !== '1') {
250
+ sh(`settings put system show_touches ${showTouchesBefore === 'null' ? '0' : showTouchesBefore}`);
251
+ }
252
+ } catch { /* device gone / key absent — cosmetic feature, never block the run */ }
253
+ }
254
+
181
255
  export const config: WebdriverIO.Config = {
182
256
  runner: 'local',
183
257
  tsConfigPath: './tsconfig.json',
@@ -192,7 +266,10 @@ export const config: WebdriverIO.Config = {
192
266
  // Global auto-wait for $/$$ matchers (toBeDisplayed, click, …). Default is 3s, too short for a
193
267
  // heavy app's cold (re)launch — each scenario terminates+activates its app, so the first
194
268
  // find/click must wait out the splash/first-frame. 20s keeps slow native/Flutter apps green.
195
- waitforTimeout: 20000,
269
+ // 30s (not the classic 20s): on a Compose app with looping in-app animations the accessibility
270
+ // manager can starve for 10s+ per query ("Timed out waiting for the root AccessibilityNodeInfo",
271
+ // observed live) — a 20s budget turns that latency into flaky not-found failures.
272
+ waitforTimeout: 30000,
196
273
 
197
274
  // Capability set(s) selected by MOBILE_PLATFORM (android | ios | both). Each cap carries its OWN
198
275
  // `specs` (per-OS routing): the Android cap runs android + mobile specs, the iOS cap runs ios +
@@ -207,16 +284,24 @@ export const config: WebdriverIO.Config = {
207
284
  'appium:udid': process.env.ANDROID_UDID || '__ANDROID_UDID__',
208
285
  // APP_APK installs from a file (Appium reads pkg/activity from the manifest);
209
286
  // otherwise target an already-installed app by package + activity.
210
- ...(APP_APK
211
- ? { 'appium:app': APP_APK }
212
- : {
213
- 'appium:appPackage': APP_PACKAGE,
214
- // Omit when unset UiAutomator2 launches the package's default launcher activity.
215
- ...(HAS_REAL_APP_ACTIVITY ? { 'appium:appActivity': APP_ACTIVITY } : {}),
216
- }),
287
+ ...(APP_APK ? { 'appium:app': APP_APK } : {}),
288
+ // Explicit APP_PACKAGE/APP_ACTIVITY are passed even alongside APP_APK: the driver's
289
+ // APK-manifest parse can flake (empty activity → bare MAIN/LAUNCHER intent, which
290
+ // Android rejects as "matches multiple activities"), and explicit caps override it.
291
+ ...(process.env.APP_PACKAGE || !APP_APK ? { 'appium:appPackage': APP_PACKAGE } : {}),
292
+ // Omit when unset → UiAutomator2 launches the package's default launcher activity.
293
+ ...(HAS_REAL_APP_ACTIVITY ? { 'appium:appActivity': APP_ACTIVITY } : {}),
217
294
  'appium:noReset': true,
218
295
  'appium:autoGrantPermissions': true,
219
296
  'appium:newCommandTimeout': 300,
297
+ // Compose/animated UIs (a looping promo animation is enough) never report accessibility-idle,
298
+ // so every find first waits out UiAutomator2's full idle timeout — 10000ms by default. 2s keeps
299
+ // interactions responsive while still absorbing screen transitions. NOTE: milliseconds — the
300
+ // same-named XCUITest cap below is in seconds; the two drivers define the knob differently.
301
+ 'appium:settings[waitForIdleTimeout]': 2000,
302
+ // Disable SYSTEM animation scales for the session — trims accessibility-tree churn so
303
+ // queries stall less on animation-heavy apps (in-app Compose animations still run).
304
+ 'appium:disableWindowAnimation': true,
220
305
  // i18n: when SUNGEN_ENV/SUNGEN_LOCALE is a locale code, this overrides noReset→false and
221
306
  // adds appium:language/appium:locale so the app boots in that locale. Empty otherwise.
222
307
  ...localeCaps,
@@ -275,7 +360,23 @@ export const config: WebdriverIO.Config = {
275
360
  reporters: ['spec', [PwShapeReporter, {}]],
276
361
 
277
362
  // Auto-start a local Appium server on :4723 (base path '/')
278
- services: ['appium'],
363
+ // Appium server options — security-gated features, each opt-in via .env.appium:
364
+ // APPIUM_ALLOW_ADB_SHELL=1 → --allow-insecure uiautomator2:adb_shell
365
+ // (raw shell access — device-level oracles like device-sqlite's run-as fallback)
366
+ // APPIUM_CHROMEDRIVER_AUTODOWNLOAD=1 → --allow-insecure uiautomator2:chromedriver_autodownload
367
+ // (hybrid WebView: downloads the chromedriver matching the device's WebView at session start —
368
+ // without it, `switch to [X] frame` fails with "No Chromedriver found that can automate Chrome N")
369
+ // Both stay OFF by default (see docs/spec/security-and-telemetry-roadmap.md).
370
+ services: [
371
+ ['appium', (() => {
372
+ const feats: string[] = [];
373
+ if (process.env.APPIUM_ALLOW_ADB_SHELL) feats.push('uiautomator2:adb_shell');
374
+ if (process.env.APPIUM_CHROMEDRIVER_AUTODOWNLOAD) feats.push('uiautomator2:chromedriver_autodownload');
375
+ // NOTE: pass a comma-joined STRING — @wdio/appium-service JSON.stringifies arrays, which the
376
+ // appium server then reads as a JSON literal and the features silently never register.
377
+ return feats.length ? { args: { allowInsecure: feats.join(',') } } : {};
378
+ })()],
379
+ ],
279
380
  port: 4723,
280
381
  path: '/',
281
382
 
@@ -291,12 +392,27 @@ export const config: WebdriverIO.Config = {
291
392
  retries: 1,
292
393
  },
293
394
 
395
+ // expect-webdriverio's default assertion budget (~2s wait / 100ms interval) is tuned for
396
+ // sub-second finds. On an accessibility tree churned by looping in-app animations (a rotating
397
+ // promo carousel is enough) a single server-side find can take 5-15s, so every toBeDisplayed()
398
+ // would expire before its first query even returns — align the budget with waitforTimeout.
399
+ before: async () => {
400
+ const { setOptions } = await import('expect-webdriverio');
401
+ setOptions({ wait: 20000, interval: 500 });
402
+ },
403
+
404
+ // Connectivity self-heal (#615): onPrepare heals a device wedged by a previous KILLED run
405
+ // (its onComplete never fired) before the session starts; onComplete restores after this run,
406
+ // pass or fail. Both run in the launcher process, so they fire even when the worker/session died.
407
+ onPrepare: () => { restoreConnectivity('onPrepare'); setShowTouches('onPrepare'); },
408
+ onComplete: () => { restoreConnectivity('onComplete'); setShowTouches('onComplete'); },
409
+
294
410
  // iOS + locale: XCUITest's `noReset:false` does NOT actually clear the app's data container
295
411
  // (verified live 2026-06-11) — a Flutter app that persisted its language on a previous launch keeps
296
412
  // it and ignores the new device locale, so the i18n run asserts the wrong language. Wipe the app
297
413
  // data via simctl BEFORE the session so first-launch locale detection re-runs. Android needs no
298
414
  // hook (its noReset:false reset works natively).
299
- beforeSession: async (_config, capabilities) => {
415
+ beforeSession: async (_config, capabilities, specs) => {
300
416
  if (Object.keys(localeCaps).length === 0) return; // not a locale run
301
417
  const caps = capabilities as Record<string, unknown>;
302
418
  if (String(caps['platformName'] || '').toLowerCase() !== 'ios') return;
@@ -2,6 +2,6 @@
2
2
  * Valid selector types used across scaffold generator, selector resolver, and validator.
3
3
  * Single source of truth — import this instead of hardcoding.
4
4
  */
5
- export declare const VALID_SELECTOR_TYPES: readonly ["placeholder", "role", "testid", "label", "text", "page", "column", "locator", "id", "upload", "frame", "table", "accessibility-id", "xpath", "android-uiautomator", "ios-predicate"];
5
+ export declare const VALID_SELECTOR_TYPES: readonly ["placeholder", "role", "testid", "label", "text", "page", "column", "locator", "id", "upload", "frame", "table", "accessibility-id", "xpath", "android-uiautomator", "ios-predicate", "css"];
6
6
  export type SelectorType = (typeof VALID_SELECTOR_TYPES)[number];
7
7
  //# sourceMappingURL=selector-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"selector-types.d.ts","sourceRoot":"","sources":["../../src/utils/selector-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,iMAkBvB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"selector-types.d.ts","sourceRoot":"","sources":["../../src/utils/selector-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,wMAmBvB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC"}
@@ -23,5 +23,6 @@ exports.VALID_SELECTOR_TYPES = [
23
23
  'xpath', // cross-platform, last resort
24
24
  'android-uiautomator', // Android only — UiSelector expressions
25
25
  'ios-predicate', // iOS only — NSPredicate strings
26
+ 'css', // hybrid WebView only — CSS inside a WEBVIEW context (after `switch to [X] frame`)
26
27
  ];
27
28
  //# sourceMappingURL=selector-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"selector-types.js","sourceRoot":"","sources":["../../src/utils/selector-types.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACU,QAAA,oBAAoB,GAAG;IAClC,aAAa;IACb,MAAM;IACN,QAAQ;IACR,OAAO;IACP,MAAM;IACN,MAAM;IACN,QAAQ;IACR,SAAS;IACT,IAAI;IACJ,QAAQ;IACR,OAAO;IACP,OAAO;IACP,8EAA8E;IAC9E,kBAAkB,EAAK,sEAAsE;IAC7F,OAAO,EAAgB,8BAA8B;IACrD,qBAAqB,EAAE,wCAAwC;IAC/D,eAAe,EAAQ,iCAAiC;CAChD,CAAC"}
1
+ {"version":3,"file":"selector-types.js","sourceRoot":"","sources":["../../src/utils/selector-types.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACU,QAAA,oBAAoB,GAAG;IAClC,aAAa;IACb,MAAM;IACN,QAAQ;IACR,OAAO;IACP,MAAM;IACN,MAAM;IACN,QAAQ;IACR,SAAS;IACT,IAAI;IACJ,QAAQ;IACR,OAAO;IACP,OAAO;IACP,8EAA8E;IAC9E,kBAAkB,EAAK,sEAAsE;IAC7F,OAAO,EAAgB,8BAA8B;IACrD,qBAAqB,EAAE,wCAAwC;IAC/D,eAAe,EAAQ,iCAAiC;IACxD,KAAK,EAAkB,mFAAmF;CAClG,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.24-beta.6",
3
+ "version": "3.2.25",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -12,6 +12,7 @@
12
12
  "copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && mkdir -p dist/generators/test-generator/adapters/appium/templates/steps && cp -r src/generators/test-generator/adapters/appium/templates/*.hbs dist/generators/test-generator/adapters/appium/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/appium/templates/steps dist/generators/test-generator/adapters/appium/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
13
13
  "build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
14
14
  "dev": "tsx src/cli/index.ts",
15
+ "gen:step-matrix": "tsx tools/step-support-matrix/index.ts",
15
16
  "test": "tsx tests/run-all.ts",
16
17
  "test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
17
18
  "prepublishOnly": "npm run build:dashboard && npm run build"
@@ -39,8 +40,8 @@
39
40
  "@babel/types": "^7.28.5",
40
41
  "@cucumber/gherkin": "^37.0.0",
41
42
  "@cucumber/messages": "^31.0.0",
42
- "@sungen/driver-data-factory": "3.2.24-beta.6",
43
- "@sungen/driver-ui": "3.2.24-beta.6",
43
+ "@sungen/driver-data-factory": "3.2.25",
44
+ "@sungen/driver-ui": "3.2.25",
44
45
  "chalk": "^5.6.2",
45
46
  "commander": "^14.0.2",
46
47
  "dotenv": "^17.2.3",
@@ -66,9 +67,10 @@
66
67
  "LICENSE"
67
68
  ],
68
69
  "devDependencies": {
69
- "mysql2": "^3",
70
- "mongodb": "^7",
71
70
  "@aws-sdk/client-dynamodb": "^3",
72
- "@aws-sdk/lib-dynamodb": "^3"
71
+ "@aws-sdk/lib-dynamodb": "^3",
72
+ "fast-xml-parser": "^5.3.3",
73
+ "mongodb": "^7",
74
+ "mysql2": "^3"
73
75
  }
74
76
  }
@@ -46,11 +46,19 @@ class ContextRouter {
46
46
  */
47
47
  capabilitiesFor(tags: string[] = [], platform?: string): string[] {
48
48
  const ids = new Set<string>();
49
- const def = capabilityRegistry.defaultCapabilityId();
50
- if (def) ids.add(def);
51
- // A platform capability (mobile/native/…) is activated by the project's platform, not by a tag.
52
- // `web` has no capability of its own falls through to the default `ui` (byte-identical).
53
- if (platform && capabilityRegistry.get(platform)) ids.add(platform);
49
+ // A platform capability (mobile/native/…) is activated by the project's platform, not by a tag,
50
+ // and it REPLACES the default rather than stacking on it. The default `ui` exists because `web`
51
+ // has no capability of its own; when a real platform capability is active, its gate is the one
52
+ // that applies. Adding both meant a mobile project ran `ui-viewpoint` AND `mobile-viewpoint`
53
+ // two sensors emitting the same UNIVERSAL finding, so every mobile audit printed it twice while
54
+ // web printed it once (#626). Scoring already resolved this the same way
55
+ // (`scoringCapabilityFor` prefers the platform over the default).
56
+ const platformCap = platform && capabilityRegistry.get(platform) ? platform : undefined;
57
+ if (platformCap) ids.add(platformCap);
58
+ else {
59
+ const def = capabilityRegistry.defaultCapabilityId();
60
+ if (def) ids.add(def);
61
+ }
54
62
  for (const cap of capabilityRegistry.all()) {
55
63
  if ((cap.annotations ?? []).some((a) => tags.includes(a))) ids.add(cap.id);
56
64
  }
@@ -9,43 +9,9 @@ import {
9
9
  import { buildPlan, MANUAL_REASONS } from '../../harness/capability-plan';
10
10
  import { adapterRegistry } from '../../generators/test-generator/adapters';
11
11
  import { emitMobileRuntime, installMobileDriver } from '../../orchestrator/mobile-runtime-scaffolder';
12
+ import { coreVersion, resolveDriverVersion } from '../../orchestrator/lockstep-version';
12
13
  import { findUnitDir, resolveUnit } from '../resolve-unit';
13
14
 
14
- /** The running CLI's (core) version — opt-in drivers install in lockstep with it (not `@latest`). */
15
- function coreVersion(): string {
16
- try { return require('../../../package.json').version || 'latest'; } catch { return 'latest'; }
17
- }
18
-
19
- /**
20
- * Resolve the driver version to install for the running core `core`.
21
- *
22
- * Lockstep wants the EXACT `core` version. But the beta channel publishes `<core>-beta.N` before a
23
- * stable `<core>` ever exists — so a released core (e.g. 3.2.10) asking for `@3.2.10` hits ETARGET
24
- * out-of-the-box when npm only has `3.2.10-beta.2` (`sungen capability add` then fails for a fresh
25
- * user). Fall back on the SAME version line: prefer exact `core`, else the highest `core-beta.N`,
26
- * else the `beta` dist-tag as a last resort. Keeps the driver's pinned core-dep on the same line.
27
- */
28
- function resolveDriverVersion(pkg: string, core: string): string {
29
- if (core === 'latest') return 'beta';
30
- try {
31
- const out = spawnSync('npm', ['view', pkg, 'versions', '--json'], { encoding: 'utf-8', shell: true });
32
- if (out.status !== 0 || !out.stdout) return core; // can't query → let the exact install try/fail
33
- let versions: string[] = [];
34
- const parsed = JSON.parse(out.stdout);
35
- versions = Array.isArray(parsed) ? parsed : [parsed];
36
- if (versions.includes(core)) return core; // exact exists → lockstep
37
- const prefix = `${core}-beta.`;
38
- const betas = versions
39
- .filter((v) => typeof v === 'string' && v.startsWith(prefix))
40
- .map((v) => parseInt(v.slice(prefix.length), 10))
41
- .filter((n) => Number.isInteger(n));
42
- if (betas.length) return `${core}-beta.${Math.max(...betas)}`; // highest same-line prerelease
43
- return 'beta'; // nothing on this line → newest beta dist-tag
44
- } catch {
45
- return core;
46
- }
47
- }
48
-
49
15
  /**
50
16
  * Add `test:mobile` to the project's package.json scripts (if absent).
51
17
  * Idempotent — skips if the script is already set.
@@ -140,7 +106,7 @@ export function registerCapabilityCommand(program: Command): void {
140
106
  const resolved = resolveUnit(o.screen);
141
107
  if (!resolved) throw new Error(`Screen/flow not found: qa/screens/${o.screen} or qa/flows/${o.screen}`);
142
108
  // The catalog unit id carries the unit KIND, which gates which drivers can serve it.
143
- const plan = buildPlan(resolved.dir, o.screen, resolved.unitId);
109
+ const plan = buildPlan(resolved.dir, o.screen, resolved.unitId, readCapabilities(process.cwd()).platform);
144
110
 
145
111
  const outDir = path.join(process.cwd(), '.sungen', 'reports');
146
112
  fs.mkdirSync(outDir, { recursive: true });
@@ -166,9 +132,9 @@ export function registerCapabilityCommand(program: Command): void {
166
132
  }
167
133
  }
168
134
  if (plan.unavailable.length) {
169
- L(' Cannot help here (driver does not support this unit kind — do NOT install):');
135
+ L(' Cannot help here (driver cannot serve this unit kind or platform — do NOT install):');
170
136
  for (const r of plan.unavailable) {
171
- L(` • ${r.count} @manual:${r.reason} → \`${r.driver}\` has no ${r.unitKind} support; automate the behaviour on the owning SCREEN unit instead`);
137
+ L(` • ${r.count} @manual:${r.reason} → \`${r.driver}\` ${r.why}`);
172
138
  }
173
139
  }
174
140
  if (plan.keep.length) {