swipium 1.3.0 → 1.5.0

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 (438) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +89 -25
  3. package/THREAT_MODEL.md +1 -1
  4. package/dist/appMap/automationLink.js +13 -3
  5. package/dist/appMap/automationLink.js.map +1 -1
  6. package/dist/appMap/build.js +28 -6
  7. package/dist/appMap/build.js.map +1 -1
  8. package/dist/appMap/codeIndex.js +29 -3
  9. package/dist/appMap/codeIndex.js.map +1 -1
  10. package/dist/appMap/featureIndex.js +20 -2
  11. package/dist/appMap/featureIndex.js.map +1 -1
  12. package/dist/appMap/featureModel.js +42 -6
  13. package/dist/appMap/featureModel.js.map +1 -1
  14. package/dist/appMap/firstRunApply.js +22 -4
  15. package/dist/appMap/firstRunApply.js.map +1 -1
  16. package/dist/appMap/fsWalk.js +19 -2
  17. package/dist/appMap/fsWalk.js.map +1 -1
  18. package/dist/appMap/issues.js +0 -20
  19. package/dist/appMap/issues.js.map +1 -1
  20. package/dist/appMap/migrations.js +1 -1
  21. package/dist/appMap/migrations.js.map +1 -1
  22. package/dist/appMap/prelaunch.js.map +1 -1
  23. package/dist/appMap/projectRegistry.js +8 -2
  24. package/dist/appMap/projectRegistry.js.map +1 -1
  25. package/dist/appMap/query.js +20 -4
  26. package/dist/appMap/query.js.map +1 -1
  27. package/dist/appMap/runtimeMerge.js +9 -2
  28. package/dist/appMap/runtimeMerge.js.map +1 -1
  29. package/dist/appMap/schema.js.map +1 -1
  30. package/dist/appMap/screenMatch.js +5 -1
  31. package/dist/appMap/screenMatch.js.map +1 -1
  32. package/dist/appMap/staticScan.js +134 -22
  33. package/dist/appMap/staticScan.js.map +1 -1
  34. package/dist/appMap/store.js +4 -79
  35. package/dist/appMap/store.js.map +1 -1
  36. package/dist/appMap/tsAstScan.js +31 -9
  37. package/dist/appMap/tsAstScan.js.map +1 -1
  38. package/dist/artifacts/bundletool.js +44 -7
  39. package/dist/artifacts/bundletool.js.map +1 -1
  40. package/dist/artifacts/resolve.js +33 -6
  41. package/dist/artifacts/resolve.js.map +1 -1
  42. package/dist/automation/capabilities.js +109 -27
  43. package/dist/automation/capabilities.js.map +1 -1
  44. package/dist/automation/gestures.js +5 -1
  45. package/dist/automation/gestures.js.map +1 -1
  46. package/dist/automation/plan.js +65 -17
  47. package/dist/automation/plan.js.map +1 -1
  48. package/dist/automation/report.js +12 -2
  49. package/dist/automation/report.js.map +1 -1
  50. package/dist/automation/selectors.js +8 -3
  51. package/dist/automation/selectors.js.map +1 -1
  52. package/dist/automation/types.js.map +1 -1
  53. package/dist/automation/waits.js +9 -3
  54. package/dist/automation/waits.js.map +1 -1
  55. package/dist/automation/webview.js +7 -1
  56. package/dist/automation/webview.js.map +1 -1
  57. package/dist/automationGen/appiumModel.js +3 -1
  58. package/dist/automationGen/appiumModel.js.map +1 -1
  59. package/dist/automationGen/ciEmitter.js +1 -1
  60. package/dist/automationGen/ciEmitter.js.map +1 -1
  61. package/dist/automationGen/jsEmitter.js +29 -21
  62. package/dist/automationGen/jsEmitter.js.map +1 -1
  63. package/dist/automationGen/projectProfile.js +5 -1
  64. package/dist/automationGen/projectProfile.js.map +1 -1
  65. package/dist/automationGen/pythonEmitter.js +3 -3
  66. package/dist/automationGen/pythonEmitter.js.map +1 -1
  67. package/dist/automationGen/readmeEmitter.js.map +1 -1
  68. package/dist/automationGen/suitePlan.js +11 -6
  69. package/dist/automationGen/suitePlan.js.map +1 -1
  70. package/dist/automationGen/validation.js +33 -5
  71. package/dist/automationGen/validation.js.map +1 -1
  72. package/dist/build/parseBuildLog.js +234 -30
  73. package/dist/build/parseBuildLog.js.map +1 -1
  74. package/dist/build/plan.js +33 -12
  75. package/dist/build/plan.js.map +1 -1
  76. package/dist/ci/preflight.js.map +1 -1
  77. package/dist/cli/init.js.map +1 -1
  78. package/dist/cli/scan.js +1 -1
  79. package/dist/cli/scan.js.map +1 -1
  80. package/dist/cli/suite.js +2 -2
  81. package/dist/cli/suite.js.map +1 -1
  82. package/dist/cli/verify.js.map +1 -1
  83. package/dist/context/detect.js +10 -4
  84. package/dist/context/detect.js.map +1 -1
  85. package/dist/context/findApps.js +16 -2
  86. package/dist/context/findApps.js.map +1 -1
  87. package/dist/context/scan.js.map +1 -1
  88. package/dist/core/target.js +7 -5
  89. package/dist/core/target.js.map +1 -1
  90. package/dist/core/targetPlan.js +135 -17
  91. package/dist/core/targetPlan.js.map +1 -1
  92. package/dist/drivers/DirectDriver.js +17 -7
  93. package/dist/drivers/DirectDriver.js.map +1 -1
  94. package/dist/drivers/SimctlDriver.js +4 -1
  95. package/dist/drivers/SimctlDriver.js.map +1 -1
  96. package/dist/drivers/WdaDriver.js +15 -5
  97. package/dist/drivers/WdaDriver.js.map +1 -1
  98. package/dist/explore/candidates.js +5 -1
  99. package/dist/explore/candidates.js.map +1 -1
  100. package/dist/explore/graph.js +11 -2
  101. package/dist/explore/graph.js.map +1 -1
  102. package/dist/explore/memory.js +4 -1
  103. package/dist/explore/memory.js.map +1 -1
  104. package/dist/explore/planner.js +54 -9
  105. package/dist/explore/planner.js.map +1 -1
  106. package/dist/explore/policy.js +6 -1
  107. package/dist/explore/policy.js.map +1 -1
  108. package/dist/explore/runner.js +179 -23
  109. package/dist/explore/runner.js.map +1 -1
  110. package/dist/explore/signatures.js +3 -1
  111. package/dist/explore/signatures.js.map +1 -1
  112. package/dist/explore/suite.js +3 -1
  113. package/dist/explore/suite.js.map +1 -1
  114. package/dist/featureTesting/executionBootstrap.js +128 -19
  115. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  116. package/dist/featureTesting/featureMap.js +4 -1
  117. package/dist/featureTesting/featureMap.js.map +1 -1
  118. package/dist/featureTesting/featureScope.js +94 -13
  119. package/dist/featureTesting/featureScope.js.map +1 -1
  120. package/dist/featureTesting/mapFeatureScope.js +37 -8
  121. package/dist/featureTesting/mapFeatureScope.js.map +1 -1
  122. package/dist/featureTesting/objectiveModel.js +5 -1
  123. package/dist/featureTesting/objectiveModel.js.map +1 -1
  124. package/dist/featureTesting/resultMerge.js +10 -2
  125. package/dist/featureTesting/resultMerge.js.map +1 -1
  126. package/dist/featureTesting/sources.js +4 -1
  127. package/dist/featureTesting/sources.js.map +1 -1
  128. package/dist/featureTesting/suiteBridge.js +7 -1
  129. package/dist/featureTesting/suiteBridge.js.map +1 -1
  130. package/dist/featureTesting/synonyms.js +31 -3
  131. package/dist/featureTesting/synonyms.js.map +1 -1
  132. package/dist/featureTesting/testCaseFactory.js +16 -4
  133. package/dist/featureTesting/testCaseFactory.js.map +1 -1
  134. package/dist/featureTesting/testPlan.js +32 -10
  135. package/dist/featureTesting/testPlan.js.map +1 -1
  136. package/dist/firstRun/classifyScreen.js +4 -1
  137. package/dist/firstRun/classifyScreen.js.map +1 -1
  138. package/dist/firstRun/firstRunPlanner.js +5 -2
  139. package/dist/firstRun/firstRunPlanner.js.map +1 -1
  140. package/dist/firstRun/firstRunRunner.js +84 -23
  141. package/dist/firstRun/firstRunRunner.js.map +1 -1
  142. package/dist/firstRun/generatedDataPolicy.js +16 -6
  143. package/dist/firstRun/generatedDataPolicy.js.map +1 -1
  144. package/dist/firstRun/inputPlanner.js +40 -6
  145. package/dist/firstRun/inputPlanner.js.map +1 -1
  146. package/dist/firstRun/paywallPolicy.js.map +1 -1
  147. package/dist/fixtures/catalog.js +39 -6
  148. package/dist/fixtures/catalog.js.map +1 -1
  149. package/dist/flows/generate.js +53 -12
  150. package/dist/flows/generate.js.map +1 -1
  151. package/dist/flows/lint.js +88 -14
  152. package/dist/flows/lint.js.map +1 -1
  153. package/dist/flows/pack.js +18 -2
  154. package/dist/flows/pack.js.map +1 -1
  155. package/dist/flows/repair.js +71 -20
  156. package/dist/flows/repair.js.map +1 -1
  157. package/dist/flows/run.js +164 -51
  158. package/dist/flows/run.js.map +1 -1
  159. package/dist/flows/schema.js +57 -20
  160. package/dist/flows/schema.js.map +1 -1
  161. package/dist/flows/seedExec.js +5 -1
  162. package/dist/flows/seedExec.js.map +1 -1
  163. package/dist/ios/signing.js +32 -3
  164. package/dist/ios/signing.js.map +1 -1
  165. package/dist/issues/classify.js +12 -4
  166. package/dist/issues/classify.js.map +1 -1
  167. package/dist/issues/fingerprint.js +11 -5
  168. package/dist/issues/fingerprint.js.map +1 -1
  169. package/dist/issues/index.js +4 -20
  170. package/dist/issues/index.js.map +1 -1
  171. package/dist/issues/metrics.js +2 -1
  172. package/dist/issues/metrics.js.map +1 -1
  173. package/dist/issues/recurrence.js.map +1 -1
  174. package/dist/issues/report.js +8 -4
  175. package/dist/issues/report.js.map +1 -1
  176. package/dist/issues/reportBridge.js +36 -5
  177. package/dist/issues/reportBridge.js.map +1 -1
  178. package/dist/issues/schema.js.map +1 -1
  179. package/dist/issues/store.js.map +1 -1
  180. package/dist/lib/android.js +9 -1
  181. package/dist/lib/android.js.map +1 -1
  182. package/dist/lib/device.js +21 -5
  183. package/dist/lib/device.js.map +1 -1
  184. package/dist/lib/image.js +11 -2
  185. package/dist/lib/image.js.map +1 -1
  186. package/dist/lib/lockfile.js +60 -0
  187. package/dist/lib/lockfile.js.map +1 -0
  188. package/dist/lib/needsInput.js +6 -3
  189. package/dist/lib/needsInput.js.map +1 -1
  190. package/dist/lib/png.js +2 -1
  191. package/dist/lib/png.js.map +1 -1
  192. package/dist/lib/result.js +16 -1
  193. package/dist/lib/result.js.map +1 -1
  194. package/dist/lib/schemaHash.js +4 -2
  195. package/dist/lib/schemaHash.js.map +1 -1
  196. package/dist/lib/sensitive.js +3 -1
  197. package/dist/lib/sensitive.js.map +1 -1
  198. package/dist/lib/simctl.js.map +1 -1
  199. package/dist/lib/spawn.js +29 -10
  200. package/dist/lib/spawn.js.map +1 -1
  201. package/dist/lib/wda.js +27 -10
  202. package/dist/lib/wda.js.map +1 -1
  203. package/dist/lib/wdaConfig.js +12 -5
  204. package/dist/lib/wdaConfig.js.map +1 -1
  205. package/dist/lib/wdaTune.js +52 -23
  206. package/dist/lib/wdaTune.js.map +1 -1
  207. package/dist/mobileAudit/checks.js +141 -19
  208. package/dist/mobileAudit/checks.js.map +1 -1
  209. package/dist/mobileAudit/profiles.js +120 -19
  210. package/dist/mobileAudit/profiles.js.map +1 -1
  211. package/dist/mobileAudit/results.js +7 -2
  212. package/dist/mobileAudit/results.js.map +1 -1
  213. package/dist/mobileAudit/runner.js +144 -19
  214. package/dist/mobileAudit/runner.js.map +1 -1
  215. package/dist/oracle/auth.js.map +1 -1
  216. package/dist/oracle/failures.js +888 -131
  217. package/dist/oracle/failures.js.map +1 -1
  218. package/dist/oracle/health.js +90 -14
  219. package/dist/oracle/health.js.map +1 -1
  220. package/dist/oracle/locator.js +147 -18
  221. package/dist/oracle/locator.js.map +1 -1
  222. package/dist/oracle/record.js.map +1 -1
  223. package/dist/orchestration/goal.js +3 -9
  224. package/dist/orchestration/goal.js.map +1 -1
  225. package/dist/orchestration/testThis/execute.js +126 -0
  226. package/dist/orchestration/testThis/execute.js.map +1 -0
  227. package/dist/orchestration/testThis/pipeline.js +324 -0
  228. package/dist/orchestration/testThis/pipeline.js.map +1 -0
  229. package/dist/orchestration/testThis/plan.js +430 -0
  230. package/dist/orchestration/testThis/plan.js.map +1 -0
  231. package/dist/orchestration/testThis/terminal.js +118 -0
  232. package/dist/orchestration/testThis/terminal.js.map +1 -0
  233. package/dist/orchestration/testThis/types.js +12 -0
  234. package/dist/orchestration/testThis/types.js.map +1 -0
  235. package/dist/plan/plan.js.map +1 -1
  236. package/dist/prompts/index.js +3 -3
  237. package/dist/prompts/index.js.map +1 -1
  238. package/dist/report/coverage.js +0 -1
  239. package/dist/report/coverage.js.map +1 -1
  240. package/dist/report/evidence.js.map +1 -1
  241. package/dist/report/export.js +59 -37
  242. package/dist/report/export.js.map +1 -1
  243. package/dist/report/flake.js +13 -14
  244. package/dist/report/flake.js.map +1 -1
  245. package/dist/report/history.js +72 -38
  246. package/dist/report/history.js.map +1 -1
  247. package/dist/report/policy.js +11 -3
  248. package/dist/report/policy.js.map +1 -1
  249. package/dist/report/qaLevel.js +2 -8
  250. package/dist/report/qaLevel.js.map +1 -1
  251. package/dist/report/readiness.js +42 -12
  252. package/dist/report/readiness.js.map +1 -1
  253. package/dist/report/summary.js.map +1 -1
  254. package/dist/report/timing.js +23 -23
  255. package/dist/report/timing.js.map +1 -1
  256. package/dist/server.js +42 -37
  257. package/dist/server.js.map +1 -1
  258. package/dist/services/automationGenerate.js.map +1 -1
  259. package/dist/services/build.js +8 -2
  260. package/dist/services/build.js.map +1 -1
  261. package/dist/services/preflight.js +38 -6
  262. package/dist/services/preflight.js.map +1 -1
  263. package/dist/services/prepareAndroid.js +46 -10
  264. package/dist/services/prepareAndroid.js.map +1 -1
  265. package/dist/services/prepareIos.js +72 -10
  266. package/dist/services/prepareIos.js.map +1 -1
  267. package/dist/services/report.js +173 -37
  268. package/dist/services/report.js.map +1 -1
  269. package/dist/services/smoke.js +36 -6
  270. package/dist/services/smoke.js.map +1 -1
  271. package/dist/services/suiteGenerate.js +16 -4
  272. package/dist/services/suiteGenerate.js.map +1 -1
  273. package/dist/services/testSuiteKnowledge.js +38 -6
  274. package/dist/services/testSuiteKnowledge.js.map +1 -1
  275. package/dist/session/attach.js +16 -0
  276. package/dist/session/attach.js.map +1 -1
  277. package/dist/session/processRegistry.js +164 -0
  278. package/dist/session/processRegistry.js.map +1 -0
  279. package/dist/session/progress.js +1 -1
  280. package/dist/session/progress.js.map +1 -1
  281. package/dist/session/store.js +216 -43
  282. package/dist/session/store.js.map +1 -1
  283. package/dist/snapshot/overlays.js +7 -2
  284. package/dist/snapshot/overlays.js.map +1 -1
  285. package/dist/snapshot/parse.js +2 -8
  286. package/dist/snapshot/parse.js.map +1 -1
  287. package/dist/snapshot/present.js +27 -3
  288. package/dist/snapshot/present.js.map +1 -1
  289. package/dist/state/profile.js +36 -9
  290. package/dist/state/profile.js.map +1 -1
  291. package/dist/suite/compile.js +7 -2
  292. package/dist/suite/compile.js.map +1 -1
  293. package/dist/suite/lint.js +21 -3
  294. package/dist/suite/lint.js.map +1 -1
  295. package/dist/suite/pom.js +26 -4
  296. package/dist/suite/pom.js.map +1 -1
  297. package/dist/suite/testcase.js +6 -4
  298. package/dist/suite/testcase.js.map +1 -1
  299. package/dist/testSuite/exporter.js +6 -2
  300. package/dist/testSuite/exporter.js.map +1 -1
  301. package/dist/testSuite/generator.js +31 -6
  302. package/dist/testSuite/generator.js.map +1 -1
  303. package/dist/testSuite/issueLinks.js.map +1 -1
  304. package/dist/testSuite/lint.js +1 -1
  305. package/dist/testSuite/lint.js.map +1 -1
  306. package/dist/testSuite/merge.js +8 -2
  307. package/dist/testSuite/merge.js.map +1 -1
  308. package/dist/testSuite/schema.js +16 -11
  309. package/dist/testSuite/schema.js.map +1 -1
  310. package/dist/testSuite/store.js +9 -1
  311. package/dist/testSuite/store.js.map +1 -1
  312. package/dist/tools/act.js +107 -43
  313. package/dist/tools/act.js.map +1 -1
  314. package/dist/tools/agent.js +123 -25
  315. package/dist/tools/agent.js.map +1 -1
  316. package/dist/tools/appControl.js +16 -3
  317. package/dist/tools/appControl.js.map +1 -1
  318. package/dist/tools/appMap.js +331 -63
  319. package/dist/tools/appMap.js.map +1 -1
  320. package/dist/tools/assertVisual.js +12 -2
  321. package/dist/tools/assertVisual.js.map +1 -1
  322. package/dist/tools/automationGenerate.js +276 -241
  323. package/dist/tools/automationGenerate.js.map +1 -1
  324. package/dist/tools/build.js +92 -41
  325. package/dist/tools/build.js.map +1 -1
  326. package/dist/tools/bundletool.js +139 -28
  327. package/dist/tools/bundletool.js.map +1 -1
  328. package/dist/tools/capabilities.js +45 -71
  329. package/dist/tools/capabilities.js.map +1 -1
  330. package/dist/tools/clearOverlay.js +45 -8
  331. package/dist/tools/clearOverlay.js.map +1 -1
  332. package/dist/tools/detectContext.js +1 -1
  333. package/dist/tools/detectContext.js.map +1 -1
  334. package/dist/tools/device.js +44 -7
  335. package/dist/tools/device.js.map +1 -1
  336. package/dist/tools/doctor.js +38 -15
  337. package/dist/tools/doctor.js.map +1 -1
  338. package/dist/tools/explore.js +96 -22
  339. package/dist/tools/explore.js.map +1 -1
  340. package/dist/tools/featureTesting.js +153 -114
  341. package/dist/tools/featureTesting.js.map +1 -1
  342. package/dist/tools/firstRun.js +95 -60
  343. package/dist/tools/firstRun.js.map +1 -1
  344. package/dist/tools/flow.js +236 -101
  345. package/dist/tools/flow.js.map +1 -1
  346. package/dist/tools/flowGenerate.js +51 -47
  347. package/dist/tools/flowGenerate.js.map +1 -1
  348. package/dist/tools/flowRepair.js +24 -4
  349. package/dist/tools/flowRepair.js.map +1 -1
  350. package/dist/tools/generate.js +236 -0
  351. package/dist/tools/generate.js.map +1 -0
  352. package/dist/tools/getArtifact.js +21 -3
  353. package/dist/tools/getArtifact.js.map +1 -1
  354. package/dist/tools/health.js +3 -1
  355. package/dist/tools/health.js.map +1 -1
  356. package/dist/tools/history.js +40 -10
  357. package/dist/tools/history.js.map +1 -1
  358. package/dist/tools/ios.js +157 -25
  359. package/dist/tools/ios.js.map +1 -1
  360. package/dist/tools/issues.js +19 -150
  361. package/dist/tools/issues.js.map +1 -1
  362. package/dist/tools/jobs.js +23 -4
  363. package/dist/tools/jobs.js.map +1 -1
  364. package/dist/tools/locator.js +36 -8
  365. package/dist/tools/locator.js.map +1 -1
  366. package/dist/tools/metro.js +61 -16
  367. package/dist/tools/metro.js.map +1 -1
  368. package/dist/tools/mobileAudit.js +67 -15
  369. package/dist/tools/mobileAudit.js.map +1 -1
  370. package/dist/tools/network.js +6 -1
  371. package/dist/tools/network.js.map +1 -1
  372. package/dist/tools/note.js +20 -6
  373. package/dist/tools/note.js.map +1 -1
  374. package/dist/tools/permissions.js +31 -6
  375. package/dist/tools/permissions.js.map +1 -1
  376. package/dist/tools/plan.js +5 -2
  377. package/dist/tools/plan.js.map +1 -1
  378. package/dist/tools/prepareIosTarget.js +31 -8
  379. package/dist/tools/prepareIosTarget.js.map +1 -1
  380. package/dist/tools/prepareTarget.js +97 -18
  381. package/dist/tools/prepareTarget.js.map +1 -1
  382. package/dist/tools/report.js +24 -5
  383. package/dist/tools/report.js.map +1 -1
  384. package/dist/tools/resolveArtifact.js +9 -5
  385. package/dist/tools/resolveArtifact.js.map +1 -1
  386. package/dist/tools/resolveTarget.js +11 -2
  387. package/dist/tools/resolveTarget.js.map +1 -1
  388. package/dist/tools/screenInfo.js +8 -2
  389. package/dist/tools/screenInfo.js.map +1 -1
  390. package/dist/tools/screenRecord.js +74 -11
  391. package/dist/tools/screenRecord.js.map +1 -1
  392. package/dist/tools/screenshot.js +23 -6
  393. package/dist/tools/screenshot.js.map +1 -1
  394. package/dist/tools/seed.js +38 -6
  395. package/dist/tools/seed.js.map +1 -1
  396. package/dist/tools/smoke.js +16 -3
  397. package/dist/tools/smoke.js.map +1 -1
  398. package/dist/tools/snapshot.js +59 -6
  399. package/dist/tools/snapshot.js.map +1 -1
  400. package/dist/tools/startSession.js +54 -9
  401. package/dist/tools/startSession.js.map +1 -1
  402. package/dist/tools/state.js +62 -15
  403. package/dist/tools/state.js.map +1 -1
  404. package/dist/tools/suite.js +350 -298
  405. package/dist/tools/suite.js.map +1 -1
  406. package/dist/tools/testSuite.js +94 -28
  407. package/dist/tools/testSuite.js.map +1 -1
  408. package/dist/tools/testThis.js +30 -793
  409. package/dist/tools/testThis.js.map +1 -1
  410. package/dist/tools/visual.js +124 -18
  411. package/dist/tools/visual.js.map +1 -1
  412. package/dist/tools/visualText.js +51 -6
  413. package/dist/tools/visualText.js.map +1 -1
  414. package/dist/tools/wait.js +18 -3
  415. package/dist/tools/wait.js.map +1 -1
  416. package/dist/tools/wda.js +157 -29
  417. package/dist/tools/wda.js.map +1 -1
  418. package/dist/version.js +17 -49
  419. package/dist/version.js.map +1 -1
  420. package/dist/visual/ocr.js +12 -2
  421. package/dist/visual/ocr.js.map +1 -1
  422. package/dist/visual/provider.js +7 -3
  423. package/dist/visual/provider.js.map +1 -1
  424. package/docs/mcp-server.md +44 -4
  425. package/docs/tools.md +42 -100
  426. package/package.json +14 -5
  427. package/dist/appMap/diff.js +0 -60
  428. package/dist/appMap/diff.js.map +0 -1
  429. package/dist/automation/maestroIr.js +0 -282
  430. package/dist/automation/maestroIr.js.map +0 -1
  431. package/dist/interop/maestro.js +0 -151
  432. package/dist/interop/maestro.js.map +0 -1
  433. package/dist/tools/idling.js +0 -104
  434. package/dist/tools/idling.js.map +0 -1
  435. package/dist/tools/inputCapabilities.js +0 -30
  436. package/dist/tools/inputCapabilities.js.map +0 -1
  437. package/dist/tools/maestro.js +0 -78
  438. package/dist/tools/maestro.js.map +0 -1
@@ -1,8 +1,10 @@
1
- // SWIPIUM-REQ-04 — "Automate my app" MCP tools. Generates a runnable JS/TS or Python Appium POM
2
- // suite from a session's recorded actions, adapting to the project's language/platform/test stack.
1
+ // SWIPIUM-REQ-04 — "Automate my app". Generates a runnable JS/TS or Python Appium POM suite from a
2
+ // session's recorded actions, adapting to the project's language/platform/test stack.
3
3
  //
4
- // qa_automation_plan — read-only: project profile + generation plan + blockers.
5
- // qa_automation_generate — emit + write the JS/Python suite (+ README, optional CI), validate.
4
+ // runAutomationPlan — read-only core for qa_generate target:"appium" mode:"plan":
5
+ // project profile + generation plan + blockers.
6
+ // runAutomationGenerate — core for qa_generate target:"appium": emit + write the JS/Python
7
+ // suite (+ README, optional CI), validate.
6
8
  // qa_automation_validate — validate generated code without a device (secrets, durability, syntax).
7
9
  //
8
10
  // The canonical Swipium YAML/POM remains the intermediate model (non-goal: don't replace it). JS/Python
@@ -12,7 +14,7 @@ import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSy
12
14
  import { join, relative, dirname } from 'node:path';
13
15
  import { qaOk, qaError } from '../lib/result.js';
14
16
  import { requireConsent, consumeConsent } from '../consent/consent.js';
15
- import { assembleAutomationSuite, buildAutomationProfile, writeAutomationFiles, mapPresent, } from '../services/automationGenerate.js';
17
+ import { assembleAutomationSuite, buildAutomationProfile, writeAutomationFiles, mapPresent } from '../services/automationGenerate.js';
16
18
  import { buildSuitePlan } from '../automationGen/suitePlan.js';
17
19
  import { buildProjectProfile } from '../automationGen/projectProfile.js';
18
20
  import { validateGeneratedSuite } from '../automationGen/validation.js';
@@ -27,7 +29,6 @@ import { bootstrapFeatureExecution } from '../featureTesting/executionBootstrap.
27
29
  import { log } from '../lib/logger.js';
28
30
  const languageEnum = z.enum(['auto', 'javascript', 'typescript', 'python']);
29
31
  const platformEnum = z.enum(['auto', 'android', 'ios', 'both']);
30
- const backendEnum = z.enum(['auto', 'appium', 'swipium_flow']);
31
32
  /** Merge a prebuilt appMapUri into a tool result's structured content (Fix 6 — a bootstrap blocker
32
33
  * still built the static map, so the URI must survive on the returned blocker/consent result). */
33
34
  function withAppMapUri(result, appMapUri) {
@@ -73,256 +74,276 @@ function readGeneratedFromDisk(root, outputDir) {
73
74
  walk(base);
74
75
  return files;
75
76
  }
76
- export function registerAutomationGenerate(server, sessions) {
77
- // ---- qa_automation_plan ----
78
- server.registerTool('qa_automation_plan', {
79
- title: 'Plan an Appium automation suite',
80
- description: 'Read-only. Inspect the project (language, platform support, existing test stack) and build a plan for an "Automate my app" Appium POM suite: selected automation language (JS/TS/Python), Android-first/iOS backends, app-map/recorded-action coverage, the files that would be generated, locator readiness, and blockers. Pass sessionId to use recorded actions, or projectRoot to plan from scratch.',
81
- inputSchema: {
82
- sessionId: z.string().optional(),
83
- projectRoot: z.string().optional(),
84
- feature: z.string().optional().describe('Optional feature label to focus the suite (recorded for the plan).'),
85
- language: languageEnum.optional(),
86
- platform: platformEnum.optional(),
87
- backend: backendEnum.optional(),
88
- includeCi: z.boolean().optional(),
89
- },
90
- }, async ({ sessionId, projectRoot, feature, language, platform, backend, includeCi }) => {
91
- const { session, root } = resolveRoot(sessions, sessionId, projectRoot);
92
- if (!root)
93
- return qaError({ what: 'No project root', changedState: false, retrySafe: true, nextSteps: ['Pass sessionId or projectRoot.'] });
94
- const inputs = profileInputs(language, platform);
95
- const profile = session ? buildAutomationProfile(session, inputs) : buildAutomationProfileFromRoot(root, inputs);
96
- let plan;
97
- if (session && session.recordedActions.length) {
98
- const assembled = assembleAutomationSuite(session, { ...inputs, includeCi });
99
- plan = assembled.plan;
100
- }
101
- else {
102
- plan = buildSuitePlan(profile, { actionCount: session?.recordedActions.length ?? 0, mapPresent: mapPresent(root), includeCi });
77
+ /**
78
+ * Core handler for qa_generate target:"appium" mode:"plan". Read-only: inspect the project
79
+ * (language, platform support, existing test stack) and build a plan for an "Automate my app"
80
+ * Appium POM suite — selected automation language, backends, coverage, files planned, blockers.
81
+ */
82
+ export async function runAutomationPlan(sessions, { sessionId, projectRoot, feature, language, platform, backend, includeCi }) {
83
+ const { session, root } = resolveRoot(sessions, sessionId, projectRoot);
84
+ if (!root)
85
+ return qaError({ what: 'No project root', changedState: false, retrySafe: true, nextSteps: ['Pass sessionId or projectRoot.'] });
86
+ const inputs = profileInputs(language, platform);
87
+ const profile = session ? buildAutomationProfile(session, inputs) : buildAutomationProfileFromRoot(root, inputs);
88
+ let plan;
89
+ if (session && session.recordedActions.length) {
90
+ const assembled = assembleAutomationSuite(session, { ...inputs, includeCi });
91
+ plan = assembled.plan;
92
+ }
93
+ else {
94
+ plan = buildSuitePlan(profile, { actionCount: session?.recordedActions.length ?? 0, mapPresent: mapPresent(root), includeCi });
95
+ }
96
+ const backendNote = backend && backend !== 'auto' && backend !== 'appium'
97
+ ? `\nNote: backend=${backend} requested — Appium code is generated as an ADDITIONAL layer; existing ${backend} flows are kept.`
98
+ : '';
99
+ const featureNote = feature ? ` (feature focus: ${feature})` : '';
100
+ const summary = `Automation plan${featureNote}: ${profile.automationLanguage} / ${profile.testFramework}, ` +
101
+ `default backend ${profile.defaultBackend}${profile.secondaryBackend ? ` (+${profile.secondaryBackend})` : ''}.\n` +
102
+ `platforms: android=${profile.platforms.android.level}, ios=${profile.platforms.ios.level}\n` +
103
+ (plan.mapCoverage
104
+ ? `coverage: ${plan.mapCoverage.actionCount} step(s), ${plan.mapCoverage.screenCount} screen(s); locators ${plan.locatorReadiness} (${plan.mapCoverage.brittlePct}% brittle)`
105
+ : 'coverage: no recorded actions yet') +
106
+ (plan.blockers.length ? `\nblockers: ${plan.blockers.map((b) => b.code).join(', ')}` : '') +
107
+ `\nfiles planned: ${plan.filesPlanned.length}\nnext: ${plan.nextAction}` +
108
+ backendNote;
109
+ return qaOk({ profile, plan, feature: feature ?? null, backend: backend ?? 'auto' }, summary);
110
+ }
111
+ /**
112
+ * Core handler for qa_generate target:"appium". Generates a runnable Appium POM suite from the
113
+ * session's recorded actions (WebdriverIO TS/JS or Python), bootstrapping a device/session/actions
114
+ * from projectRoot when none exist. Never inlines secrets; integrateIntoProject is consent-gated.
115
+ */
116
+ export async function runAutomationGenerate(server, sessions, { sessionId, projectRoot, bootstrap, feature, device, name, language, platform, save, integrateIntoProject, includeCi, candidateOnly, brittleThreshold, consentId, approve, }) {
117
+ let session = sessionId ? sessions.get(sessionId) : undefined;
118
+ if (sessionId && !session)
119
+ return qaError({
120
+ what: `Unknown sessionId ${sessionId}`,
121
+ changedState: false,
122
+ retrySafe: true,
123
+ nextSteps: ['Call qa_start_session first, or omit sessionId to bootstrap from projectRoot.'],
124
+ });
125
+ // Fix 6 — one-call "Automate my app": when no session/actions exist, build the static app map
126
+ // (so it always exists) and bootstrap a device (consent-gated) + record actions via exploration.
127
+ const wantBootstrap = bootstrap !== false && (bootstrap === true || bootstrap === 'auto' || !sessionId);
128
+ let appMapUri;
129
+ const rootForMap = session?.root ?? (projectRoot && projectRoot.trim() ? projectRoot.trim() : undefined);
130
+ if (rootForMap && existsSync(rootForMap)) {
131
+ try {
132
+ appMapUri = ensurePrelaunchAppMap(rootForMap, { at: new Date().toISOString() }).appMapUri;
103
133
  }
104
- const backendNote = backend && backend !== 'auto' && backend !== 'appium'
105
- ? `\nNote: backend=${backend} requested — Appium code is generated as an ADDITIONAL layer; existing ${backend} flows are kept.`
106
- : '';
107
- const featureNote = feature ? ` (feature focus: ${feature})` : '';
108
- const summary = `Automation plan${featureNote}: ${profile.automationLanguage} / ${profile.testFramework}, ` +
109
- `default backend ${profile.defaultBackend}${profile.secondaryBackend ? ` (+${profile.secondaryBackend})` : ''}.\n` +
110
- `platforms: android=${profile.platforms.android.level}, ios=${profile.platforms.ios.level}\n` +
111
- (plan.mapCoverage
112
- ? `coverage: ${plan.mapCoverage.actionCount} step(s), ${plan.mapCoverage.screenCount} screen(s); locators ${plan.locatorReadiness} (${plan.mapCoverage.brittlePct}% brittle)`
113
- : 'coverage: no recorded actions yet') +
114
- (plan.blockers.length ? `\nblockers: ${plan.blockers.map((b) => b.code).join(', ')}` : '') +
115
- `\nfiles planned: ${plan.filesPlanned.length}\nnext: ${plan.nextAction}` +
116
- backendNote;
117
- return qaOk({ profile, plan, feature: feature ?? null, backend: backend ?? 'auto' }, summary);
118
- });
119
- // ---- qa_automation_generate ----
120
- server.registerTool('qa_automation_generate', {
121
- title: 'Generate an Appium automation suite',
122
- description: 'Generate a runnable Appium POM suite from this session\'s recorded actions, adapting to the project language: WebdriverIO TS/JS or Python (Appium-Python-Client). Writes page/screen objects (centralized selectors), env-driven capabilities, structured waits, a smoke test, and a README under .swipium/automation/<lang>/ by default. Never inlines secrets; marks brittle/coordinate locators non-release-grade; never emits XPath. integrateIntoProject (consent-gated) writes into the project e2e dir without overwriting existing files.',
123
- inputSchema: {
124
- sessionId: z.string().optional().describe('Prepared session with recorded actions. Omit to bootstrap from projectRoot.'),
125
- projectRoot: z.string().optional().describe('Project root — used to bootstrap a device/session when no sessionId is given (Fix 6 one-call workflow).'),
126
- bootstrap: z.union([z.boolean(), z.literal('auto')]).optional().describe('Bootstrap the missing map/session/actions safely (smoke+explore) when none exist. Default "auto" when no sessionId is provided.'),
127
- feature: z.string().optional().describe('Optional focus for the bootstrap exploration.'),
128
- device: z.string().optional().describe('Specific device/simulator to prepare when bootstrapping.'),
129
- name: z.string().optional(),
130
- language: languageEnum.optional(),
131
- platform: platformEnum.optional(),
132
- save: z.boolean().optional().describe('Write files (default true).'),
133
- integrateIntoProject: z.boolean().optional().describe('Write into the project test dir instead of .swipium (consent-gated, never overwrites).'),
134
- includeCi: z.boolean().optional().describe('Also emit ci.example.yml.'),
135
- candidateOnly: z.boolean().optional().describe('Label the suite candidate-only so brittle locators do not fail validation.'),
136
- brittleThreshold: z.number().optional().describe('Max brittle-locator percent before validation fails (default 40).'),
137
- consentId: z.string().optional(),
138
- approve: z.boolean().optional(),
139
- },
140
- }, async ({ sessionId, projectRoot, bootstrap, feature, device, name, language, platform, save, integrateIntoProject, includeCi, candidateOnly, brittleThreshold, consentId, approve }) => {
141
- let session = sessionId ? sessions.get(sessionId) : undefined;
142
- if (sessionId && !session)
143
- return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first, or omit sessionId to bootstrap from projectRoot.'] });
144
- // Fix 6 — one-call "Automate my app": when no session/actions exist, build the static app map
145
- // (so it always exists) and bootstrap a device (consent-gated) + record actions via exploration.
146
- const wantBootstrap = bootstrap !== false && (bootstrap === true || bootstrap === 'auto' || !sessionId);
147
- let appMapUri;
148
- const rootForMap = session?.root ?? (projectRoot && projectRoot.trim() ? projectRoot.trim() : undefined);
149
- if (rootForMap && existsSync(rootForMap)) {
150
- try {
151
- appMapUri = ensurePrelaunchAppMap(rootForMap, { at: new Date().toISOString() }).appMapUri;
152
- }
153
- catch (e) {
154
- log('warn', 'automation prelaunch app map failed', { err: String(e) });
155
- }
134
+ catch (e) {
135
+ log('warn', 'automation prelaunch app map failed', { err: String(e) });
156
136
  }
157
- if ((!session || !session.recordedActions.length) && wantBootstrap) {
158
- let driver = session ? (await getDriver(session)).driver : undefined;
159
- if (!session || !driver) {
160
- if (!projectRoot?.trim() && !session) {
161
- return qaError({ what: 'Need a sessionId or projectRoot to bootstrap an automation suite', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot="/abs/path" (bootstrap defaults on), or a prepared sessionId.'] }, appMapUri ? { appMapUri } : {});
162
- }
163
- const boot = await bootstrapFeatureExecution({ server, sessions, projectRoot: projectRoot?.trim() ?? session.root, feature: feature ?? 'create automation suite', platform: platform === 'android' || platform === 'ios' ? platform : undefined, device, consentId, approve });
164
- // Honest target blocker / consent request — but the static app map WAS still built, so the
165
- // caller can read it even though no device/artifact is available (Fix 6 acceptance).
166
- if (!boot.ok)
167
- return withAppMapUri(boot.result, appMapUri);
168
- session = boot.session;
169
- driver = boot.driver;
170
- if (!appMapUri && existsSync(session.root)) {
171
- try {
172
- appMapUri = ensurePrelaunchAppMap(session.root, { at: new Date().toISOString() }).appMapUri;
173
- }
174
- catch { /* best-effort */ }
175
- }
137
+ }
138
+ if ((!session || !session.recordedActions.length) && wantBootstrap) {
139
+ let driver = session ? (await getDriver(session)).driver : undefined;
140
+ if (!session || !driver) {
141
+ if (!projectRoot?.trim() && !session) {
142
+ return qaError({
143
+ what: 'Need a sessionId or projectRoot to bootstrap an automation suite',
144
+ changedState: false,
145
+ retrySafe: true,
146
+ nextSteps: ['Pass projectRoot="/abs/path" (bootstrap defaults on), or a prepared sessionId.'],
147
+ }, appMapUri ? { appMapUri } : {});
176
148
  }
177
- // Record durable actions via a bounded, safe exploration so generation has something to emit.
178
- if (driver && !session.recordedActions.length) {
149
+ const boot = await bootstrapFeatureExecution({
150
+ server,
151
+ sessions,
152
+ projectRoot: projectRoot?.trim() ?? session.root,
153
+ feature: feature ?? 'create automation suite',
154
+ platform: platform === 'android' || platform === 'ios' ? platform : undefined,
155
+ device,
156
+ consentId,
157
+ approve,
158
+ });
159
+ // Honest target blocker / consent request — but the static app map WAS still built, so the
160
+ // caller can read it even though no device/artifact is available (Fix 6 acceptance).
161
+ if (!boot.ok)
162
+ return withAppMapUri(boot.result, appMapUri);
163
+ session = boot.session;
164
+ driver = boot.driver;
165
+ if (!appMapUri && existsSync(session.root)) {
179
166
  try {
180
- await runExplore(sessions, session, driver, { goal: feature, maxScreens: 6, maxActions: 16, stopOnAuth: true });
167
+ appMapUri = ensurePrelaunchAppMap(session.root, { at: new Date().toISOString() }).appMapUri;
181
168
  }
182
- catch (e) {
183
- log('warn', 'automation bootstrap exploration failed', { err: String(e) });
169
+ catch {
170
+ /* best-effort */
184
171
  }
185
172
  }
186
173
  }
187
- if (!session)
188
- return qaError({ what: 'Need a sessionId or projectRoot', changedState: false, retrySafe: true, nextSteps: ['Pass a prepared sessionId, or projectRoot to bootstrap.'] }, appMapUri ? { appMapUri } : {});
189
- if (!session.recordedActions.length) {
190
- return qaError({
191
- what: 'No recorded actions to turn into an automation suite (bootstrap could not record any)',
192
- changedState: false,
193
- retrySafe: true,
194
- failureCode: 'NO_RECORDED_ACTIONS',
195
- nextSteps: [
196
- 'Run qa_test_this { goal: "create_automation_suite" } (smoke + explore records actions + builds the app map), then re-run qa_automation_generate.',
197
- 'Or drive the app with qa_act/qa_smoke/qa_explore first.',
198
- ],
199
- }, appMapUri ? { appMapUri } : {});
200
- }
201
- const inputs = profileInputs(language, platform, integrateIntoProject);
202
- const assembled = assembleAutomationSuite(session, { ...inputs, name, includeCi });
203
- const validation = validateGeneratedSuite(assembled.files, {
204
- brittlePct: assembled.model.audit.brittlePct,
205
- brittleThreshold,
206
- candidateOnly,
207
- secrets: assembled.model.secrets,
208
- });
209
- const doSave = save !== false;
210
- let written = [];
211
- let targetDir = assembled.outputDir;
212
- let integrated = false;
213
- let skippedExisting = [];
214
- if (doSave && integrateIntoProject) {
215
- // Consent-gated project mutation; never overwrite existing files.
216
- const projectDir = projectTestDir(session.root, assembled.profile.automationLanguage);
217
- const affects = { projectDir, files: assembled.files.map((f) => f.path) };
218
- const gate = consumeConsent(consentId, approve, { action: 'automation_project_write', affects });
219
- if (!gate.approved) {
220
- return requireConsent({
221
- action: 'automation_project_write',
222
- risk: 'medium',
223
- exactCommand: `write ${assembled.files.length} files into ${projectDir} (no overwrite)`,
224
- affects,
225
- explain: `Write the generated Appium suite into your project at ${projectDir}? Existing files are never overwritten.`,
226
- });
174
+ // Record durable actions via a bounded, safe exploration so generation has something to emit.
175
+ if (driver && !session.recordedActions.length) {
176
+ try {
177
+ await runExplore(sessions, session, driver, { goal: feature, maxScreens: 6, maxActions: 16, stopOnAuth: true });
178
+ }
179
+ catch (e) {
180
+ log('warn', 'automation bootstrap exploration failed', { err: String(e) });
227
181
  }
228
- const res = writeProjectFilesNoOverwrite(session.root, projectDir, assembled.files);
229
- written = res.written;
230
- skippedExisting = res.skipped;
231
- targetDir = projectDir;
232
- integrated = true;
233
- sessions.recordMutation(session, {
234
- tool: 'qa_automation_generate',
182
+ }
183
+ }
184
+ if (!session)
185
+ return qaError({
186
+ what: 'Need a sessionId or projectRoot',
187
+ changedState: false,
188
+ retrySafe: true,
189
+ nextSteps: ['Pass a prepared sessionId, or projectRoot to bootstrap.'],
190
+ }, appMapUri ? { appMapUri } : {});
191
+ if (!session.recordedActions.length) {
192
+ return qaError({
193
+ what: 'No recorded actions to turn into an automation suite (bootstrap could not record any)',
194
+ changedState: false,
195
+ retrySafe: true,
196
+ failureCode: 'NO_RECORDED_ACTIONS',
197
+ nextSteps: [
198
+ 'Run qa_test_this { goal: "create_automation_suite" } (smoke + explore records actions + builds the app map), then re-run qa_generate target:"appium".',
199
+ 'Or drive the app with qa_act/qa_smoke/qa_explore first.',
200
+ ],
201
+ }, appMapUri ? { appMapUri } : {});
202
+ }
203
+ const inputs = profileInputs(language, platform, integrateIntoProject);
204
+ const assembled = assembleAutomationSuite(session, { ...inputs, name, includeCi });
205
+ const validation = validateGeneratedSuite(assembled.files, {
206
+ brittlePct: assembled.model.audit.brittlePct,
207
+ brittleThreshold,
208
+ candidateOnly,
209
+ secrets: assembled.model.secrets,
210
+ });
211
+ const doSave = save !== false;
212
+ let written = [];
213
+ let targetDir = assembled.outputDir;
214
+ let integrated = false;
215
+ let skippedExisting = [];
216
+ if (doSave && integrateIntoProject) {
217
+ // Consent-gated project mutation; never overwrite existing files.
218
+ const projectDir = projectTestDir(session.root, assembled.profile.automationLanguage);
219
+ const affects = { projectDir, files: assembled.files.map((f) => f.path) };
220
+ const gate = consumeConsent(consentId, approve, { action: 'automation_project_write', affects });
221
+ if (!gate.approved) {
222
+ return requireConsent({
235
223
  action: 'automation_project_write',
236
224
  risk: 'medium',
237
- target: affects,
238
- consent: { required: true, consentId, approved: true },
239
- status: 'executed',
225
+ exactCommand: `write ${assembled.files.length} files into ${projectDir} (no overwrite)`,
226
+ affects,
227
+ explain: `Write the generated Appium suite into your project at ${projectDir}? Existing files are never overwritten.`,
240
228
  });
241
229
  }
242
- else if (doSave) {
243
- written = writeAutomationFiles(session.root, assembled.outputDir, assembled.files);
230
+ const res = writeProjectFilesNoOverwrite(session.root, projectDir, assembled.files);
231
+ written = res.written;
232
+ skippedExisting = res.skipped;
233
+ targetDir = projectDir;
234
+ integrated = true;
235
+ sessions.recordMutation(session, {
236
+ tool: 'qa_generate',
237
+ action: 'automation_project_write',
238
+ risk: 'medium',
239
+ target: affects,
240
+ consent: { required: true, consentId, approved: true },
241
+ status: 'executed',
242
+ });
243
+ }
244
+ else if (doSave) {
245
+ written = writeAutomationFiles(session.root, assembled.outputDir, assembled.files);
246
+ }
247
+ // Fix 7 — link the generated suite back into the durable app map + persistent suite, so future
248
+ // feature/ticket decisions know which flows are automated. Best-effort (warnings, never fatal).
249
+ let suiteDelta;
250
+ let suiteUri;
251
+ const automationWarnings = [];
252
+ if (doSave) {
253
+ const now = new Date().toISOString();
254
+ const screenNames = assembled.model.screens.map((s) => s.pageName || s.className);
255
+ try {
256
+ const fw = detectFramework(session.root);
257
+ const fallback = {
258
+ root: session.root,
259
+ gitRemote: null,
260
+ packageName: null,
261
+ workspaceTarget: null,
262
+ framework: fw,
263
+ platforms: fw === 'native-android' ? ['android'] : fw === 'native-ios' ? ['ios'] : ['android', 'ios'],
264
+ };
265
+ const existingMap = loadAppMap(session.root, fallback, now).map;
266
+ const links = existingMap ? deriveAutomationLinks(existingMap, screenNames) : { screenIds: [], featureIds: [] };
267
+ const suiteRef = {
268
+ name: name ?? `swipium-appium-${assembled.profile.automationLanguage}`,
269
+ path: targetDir,
270
+ framework: assembled.profile.automationLanguage === 'python' ? 'appium-python' : 'wdio',
271
+ linkedFeatureIds: links.featureIds,
272
+ linkedScreenIds: links.screenIds,
273
+ };
274
+ const linkRes = linkAutomationSuite(session.root, suiteRef, now);
275
+ if (linkRes.ok)
276
+ appMapUri = linkRes.appMapUri ?? appMapUri;
277
+ else
278
+ automationWarnings.push('app map not found — automation suite not linked into the map');
244
279
  }
245
- // Fix 7 — link the generated suite back into the durable app map + persistent suite, so future
246
- // feature/ticket decisions know which flows are automated. Best-effort (warnings, never fatal).
247
- let suiteDelta;
248
- let suiteUri;
249
- const automationWarnings = [];
250
- if (doSave) {
251
- const now = new Date().toISOString();
252
- const screenNames = assembled.model.screens.map((s) => s.pageName || s.className);
253
- try {
254
- const fw = detectFramework(session.root);
255
- const fallback = { root: session.root, gitRemote: null, packageName: null, workspaceTarget: null, framework: fw, platforms: fw === 'native-android' ? ['android'] : fw === 'native-ios' ? ['ios'] : ['android', 'ios'] };
256
- const existingMap = loadAppMap(session.root, fallback, now).map;
257
- const links = existingMap ? deriveAutomationLinks(existingMap, screenNames) : { screenIds: [], featureIds: [] };
258
- const suiteRef = {
259
- name: name ?? `swipium-appium-${assembled.profile.automationLanguage}`,
260
- path: targetDir,
261
- framework: assembled.profile.automationLanguage === 'python' ? 'appium-python' : 'wdio',
262
- linkedFeatureIds: links.featureIds,
263
- linkedScreenIds: links.screenIds,
264
- };
265
- const linkRes = linkAutomationSuite(session.root, suiteRef, now);
266
- if (linkRes.ok)
267
- appMapUri = linkRes.appMapUri ?? appMapUri;
268
- else
269
- automationWarnings.push('app map not found — automation suite not linked into the map');
270
- }
271
- catch (e) {
272
- automationWarnings.push(`app-map automation link failed: ${String(e)}`);
273
- }
274
- try {
275
- const fw2 = detectFramework(session.root);
276
- const fb2 = { root: session.root, gitRemote: null, packageName: null, workspaceTarget: null, framework: fw2, platforms: fw2 === 'native-android' ? ['android'] : fw2 === 'native-ios' ? ['ios'] : ['android', 'ios'] };
277
- const mapForLinks = loadAppMap(session.root, fb2, now).map;
278
- const links = mapForLinks ? deriveAutomationLinks(mapForLinks, assembled.model.screens.map((s) => s.pageName || s.className)) : { screenIds: [], featureIds: [] };
279
- const merge = mergeFromAutomation(session.root, session, { assembled, validationOk: validation.ok, links }, { source: 'generate', now, runId: `automation-${Date.now()}`, sourceUri: appMapUri, appId: session.appId, sessionId: session.id });
280
- suiteDelta = merge.delta;
281
- suiteUri = merge.suiteUri;
282
- if (merge.warning)
283
- automationWarnings.push(merge.warning);
284
- }
285
- catch (e) {
286
- automationWarnings.push(`suite automation merge failed: ${String(e)}`);
287
- }
280
+ catch (e) {
281
+ automationWarnings.push(`app-map automation link failed: ${String(e)}`);
288
282
  }
289
- const a = assembled.model.audit;
290
- const recommendation = integrated
291
- ? undefined
292
- : assembled.profile.outputMode === 'project_native'
293
- ? 'Copy the suite into your project test dir, or re-run with integrateIntoProject:true (consent-gated).'
294
- : `Generated under ${assembled.outputDir}. Install deps (${assembled.dependencyPatch.notes[0]}) and run per the README.`;
295
- const summary = `✅ Generated ${assembled.profile.automationLanguage} Appium POM suite (${assembled.model.screens.length} screen(s), ${assembled.files.length} files).\n` +
296
- `runner ${assembled.profile.testFramework}; backend ${assembled.profile.defaultBackend}${assembled.profile.secondaryBackend ? ` (+${assembled.profile.secondaryBackend})` : ''}\n` +
297
- `locators: ${a.durable} durable / ${a.semi} semi / ${a.brittle} brittle (${a.brittlePct}% brittle)\n` +
298
- `validation: ${validation.ok ? 'passed' : 'FAILED'} (${validation.findings.filter((f) => f.severity === 'error').length} error, ${validation.findings.filter((f) => f.severity === 'warning').length} warn); secrets ${validation.secretsClean ? 'clean' : 'LEAK'}\n` +
299
- (written.length ? `wrote ${written.length} files under ${join(session.root, targetDir)}` : '(preview — pass save:true)') +
300
- (skippedExisting.length ? `\nskipped ${skippedExisting.length} existing file(s) (no overwrite): ${skippedExisting.slice(0, 5).join(', ')}` : '') +
301
- (recommendation ? `\n${recommendation}` : '');
302
- return qaOk({
303
- profile: assembled.profile,
304
- outputDir: targetDir,
305
- integrated,
306
- files: assembled.files.map((f) => f.path),
307
- written,
308
- skippedExisting,
309
- screens: assembled.model.screens.map((s) => s.className),
310
- variables: assembled.model.variables,
311
- secrets: assembled.model.secrets,
312
- audit: a,
313
- validation,
314
- dependencyPatch: assembled.dependencyPatch,
315
- plan: assembled.plan,
316
- appMapUri: appMapUri ?? null,
317
- suiteDelta: suiteDelta ?? null,
318
- suiteUri: suiteUri ?? null,
319
- automationWarnings,
320
- }, summary);
321
- });
283
+ try {
284
+ const fw2 = detectFramework(session.root);
285
+ const fb2 = {
286
+ root: session.root,
287
+ gitRemote: null,
288
+ packageName: null,
289
+ workspaceTarget: null,
290
+ framework: fw2,
291
+ platforms: fw2 === 'native-android' ? ['android'] : fw2 === 'native-ios' ? ['ios'] : ['android', 'ios'],
292
+ };
293
+ const mapForLinks = loadAppMap(session.root, fb2, now).map;
294
+ const links = mapForLinks
295
+ ? deriveAutomationLinks(mapForLinks, assembled.model.screens.map((s) => s.pageName || s.className))
296
+ : { screenIds: [], featureIds: [] };
297
+ const merge = mergeFromAutomation(session.root, session, { assembled, validationOk: validation.ok, links }, { source: 'generate', now, runId: `automation-${Date.now()}`, sourceUri: appMapUri, appId: session.appId, sessionId: session.id });
298
+ suiteDelta = merge.delta;
299
+ suiteUri = merge.suiteUri;
300
+ if (merge.warning)
301
+ automationWarnings.push(merge.warning);
302
+ }
303
+ catch (e) {
304
+ automationWarnings.push(`suite automation merge failed: ${String(e)}`);
305
+ }
306
+ }
307
+ const a = assembled.model.audit;
308
+ const recommendation = integrated
309
+ ? undefined
310
+ : assembled.profile.outputMode === 'project_native'
311
+ ? 'Copy the suite into your project test dir, or re-run with integrateIntoProject:true (consent-gated).'
312
+ : `Generated under ${assembled.outputDir}. Install deps (${assembled.dependencyPatch.notes[0]}) and run per the README.`;
313
+ const summary = `✅ Generated ${assembled.profile.automationLanguage} Appium POM suite (${assembled.model.screens.length} screen(s), ${assembled.files.length} files).\n` +
314
+ `runner ${assembled.profile.testFramework}; backend ${assembled.profile.defaultBackend}${assembled.profile.secondaryBackend ? ` (+${assembled.profile.secondaryBackend})` : ''}\n` +
315
+ `locators: ${a.durable} durable / ${a.semi} semi / ${a.brittle} brittle (${a.brittlePct}% brittle)\n` +
316
+ `validation: ${validation.ok ? 'passed' : 'FAILED'} (${validation.findings.filter((f) => f.severity === 'error').length} error, ${validation.findings.filter((f) => f.severity === 'warning').length} warn); secrets ${validation.secretsClean ? 'clean' : 'LEAK'}\n` +
317
+ (written.length ? `wrote ${written.length} files under ${join(session.root, targetDir)}` : '(preview — pass save:true)') +
318
+ (skippedExisting.length
319
+ ? `\nskipped ${skippedExisting.length} existing file(s) (no overwrite): ${skippedExisting.slice(0, 5).join(', ')}`
320
+ : '') +
321
+ (recommendation ? `\n${recommendation}` : '');
322
+ return qaOk({
323
+ profile: assembled.profile,
324
+ outputDir: targetDir,
325
+ integrated,
326
+ files: assembled.files.map((f) => f.path),
327
+ written,
328
+ skippedExisting,
329
+ screens: assembled.model.screens.map((s) => s.className),
330
+ variables: assembled.model.variables,
331
+ secrets: assembled.model.secrets,
332
+ audit: a,
333
+ validation,
334
+ dependencyPatch: assembled.dependencyPatch,
335
+ plan: assembled.plan,
336
+ appMapUri: appMapUri ?? null,
337
+ suiteDelta: suiteDelta ?? null,
338
+ suiteUri: suiteUri ?? null,
339
+ automationWarnings,
340
+ }, summary);
341
+ }
342
+ export function registerAutomationValidate(server, sessions) {
322
343
  // ---- qa_automation_validate ----
323
344
  server.registerTool('qa_automation_validate', {
324
345
  title: 'Validate a generated automation suite',
325
- description: 'Validate generated Appium code without a device: no inlined secrets, locator durability within threshold (unless candidate-only), capability config present, balanced JS/TS syntax, no empty files. Pass sessionId to validate the suite from recorded actions, or projectRoot to validate already-written files under .swipium/automation/<lang>/.',
346
+ description: 'Validate generated Appium code without a device: no inlined secrets, locator durability within threshold (unless candidate-only), capability config present, balanced JS/TS syntax, no empty files. Pass sessionId to validate the suite from recorded actions (as qa_generate target:"appium" would emit it), or projectRoot to validate already-written files under .swipium/automation/<lang>/.',
326
347
  inputSchema: {
327
348
  sessionId: z.string().optional(),
328
349
  projectRoot: z.string().optional(),
@@ -354,7 +375,7 @@ export function registerAutomationGenerate(server, sessions) {
354
375
  what: `No generated suite found under ${outputDir}`,
355
376
  changedState: false,
356
377
  retrySafe: true,
357
- nextSteps: ['Run qa_automation_generate first, or pass a sessionId with recorded actions.'],
378
+ nextSteps: ['Run qa_generate target:"appium" first, or pass a sessionId with recorded actions.'],
358
379
  });
359
380
  }
360
381
  }
@@ -362,9 +383,23 @@ export function registerAutomationGenerate(server, sessions) {
362
383
  const errors = validation.findings.filter((f) => f.severity === 'error');
363
384
  const summary = `Validation ${validation.ok ? 'passed' : 'FAILED'}: ${errors.length} error(s), ${validation.findings.length - errors.length} warning(s) over ${validation.filesChecked} files.\n` +
364
385
  `secrets ${validation.secretsClean ? 'clean' : 'LEAK DETECTED'}; brittle ${validation.brittlePct}% (threshold ${validation.brittleThreshold}%${validation.candidateOnly ? ', candidate-only' : ''})` +
365
- (validation.findings.length ? `\n` + validation.findings.slice(0, 12).map((f) => ` ${f.severity === 'error' ? '✗' : '⚠'} ${f.code}${f.file ? ` (${f.file})` : ''}: ${f.message}`).join('\n') : '');
386
+ (validation.findings.length
387
+ ? `\n` +
388
+ validation.findings
389
+ .slice(0, 12)
390
+ .map((f) => ` ${f.severity === 'error' ? '✗' : '⚠'} ${f.code}${f.file ? ` (${f.file})` : ''}: ${f.message}`)
391
+ .join('\n')
392
+ : '');
366
393
  const payload = { validation };
367
- return validation.ok ? qaOk(payload, summary) : qaError({ what: 'Generated suite failed validation', changedState: false, retrySafe: true, failureCode: 'VALIDATION_FAILED', nextSteps: errors.map((e) => e.message) }, payload);
394
+ return validation.ok
395
+ ? qaOk(payload, summary)
396
+ : qaError({
397
+ what: 'Generated suite failed validation',
398
+ changedState: false,
399
+ retrySafe: true,
400
+ failureCode: 'VALIDATION_FAILED',
401
+ nextSteps: errors.map((e) => e.message),
402
+ }, payload);
368
403
  });
369
404
  }
370
405
  // ---- helpers that need root-only (no session) profile ----