swipium 1.4.0 → 2.0.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 (541) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +175 -219
  3. package/THREAT_MODEL.md +223 -47
  4. package/dist/appMap/automationLink.js +39 -26
  5. package/dist/appMap/automationLink.js.map +1 -1
  6. package/dist/appMap/build.js +68 -20
  7. package/dist/appMap/build.js.map +1 -1
  8. package/dist/appMap/codeIndex.js +31 -5
  9. package/dist/appMap/codeIndex.js.map +1 -1
  10. package/dist/appMap/featureIndex.js +23 -5
  11. package/dist/appMap/featureIndex.js.map +1 -1
  12. package/dist/appMap/featureModel.js +44 -8
  13. package/dist/appMap/featureModel.js.map +1 -1
  14. package/dist/appMap/firstRunApply.js +24 -6
  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 +10 -30
  19. package/dist/appMap/issues.js.map +1 -1
  20. package/dist/appMap/migrations.js +6 -6
  21. package/dist/appMap/migrations.js.map +1 -1
  22. package/dist/appMap/prelaunch.js +2 -2
  23. package/dist/appMap/prelaunch.js.map +1 -1
  24. package/dist/appMap/projectRegistry.js +83 -11
  25. package/dist/appMap/projectRegistry.js.map +1 -1
  26. package/dist/appMap/provenance.js +2 -2
  27. package/dist/appMap/provenance.js.map +1 -1
  28. package/dist/appMap/query.js +21 -5
  29. package/dist/appMap/query.js.map +1 -1
  30. package/dist/appMap/runtimeMerge.js +12 -5
  31. package/dist/appMap/runtimeMerge.js.map +1 -1
  32. package/dist/appMap/schema.js +1 -1
  33. package/dist/appMap/schema.js.map +1 -1
  34. package/dist/appMap/screenMatch.js +6 -2
  35. package/dist/appMap/screenMatch.js.map +1 -1
  36. package/dist/appMap/staticScan.js +151 -39
  37. package/dist/appMap/staticScan.js.map +1 -1
  38. package/dist/appMap/store.js +58 -85
  39. package/dist/appMap/store.js.map +1 -1
  40. package/dist/appMap/tsAstScan.js +34 -12
  41. package/dist/appMap/tsAstScan.js.map +1 -1
  42. package/dist/artifacts/bundletool.js +49 -12
  43. package/dist/artifacts/bundletool.js.map +1 -1
  44. package/dist/artifacts/resolve.js +41 -14
  45. package/dist/artifacts/resolve.js.map +1 -1
  46. package/dist/automation/capabilities.js +112 -30
  47. package/dist/automation/capabilities.js.map +1 -1
  48. package/dist/automation/gestures.js +6 -2
  49. package/dist/automation/gestures.js.map +1 -1
  50. package/dist/automation/plan.js +71 -23
  51. package/dist/automation/plan.js.map +1 -1
  52. package/dist/automation/report.js +15 -5
  53. package/dist/automation/report.js.map +1 -1
  54. package/dist/automation/selectors.js +22 -28
  55. package/dist/automation/selectors.js.map +1 -1
  56. package/dist/automation/types.js +1 -1
  57. package/dist/automation/types.js.map +1 -1
  58. package/dist/automation/waits.js +14 -8
  59. package/dist/automation/waits.js.map +1 -1
  60. package/dist/automation/webview.js +9 -3
  61. package/dist/automation/webview.js.map +1 -1
  62. package/dist/automationGen/appiumModel.js +16 -8
  63. package/dist/automationGen/appiumModel.js.map +1 -1
  64. package/dist/automationGen/ciEmitter.js +6 -6
  65. package/dist/automationGen/ciEmitter.js.map +1 -1
  66. package/dist/automationGen/identifiers.js +224 -0
  67. package/dist/automationGen/identifiers.js.map +1 -0
  68. package/dist/automationGen/jsEmitter.js +288 -121
  69. package/dist/automationGen/jsEmitter.js.map +1 -1
  70. package/dist/automationGen/packagePatch.js +2 -2
  71. package/dist/automationGen/packagePatch.js.map +1 -1
  72. package/dist/automationGen/platformResolve.js +36 -0
  73. package/dist/automationGen/platformResolve.js.map +1 -0
  74. package/dist/automationGen/projectProfile.js +76 -27
  75. package/dist/automationGen/projectProfile.js.map +1 -1
  76. package/dist/automationGen/pythonEmitter.js +278 -107
  77. package/dist/automationGen/pythonEmitter.js.map +1 -1
  78. package/dist/automationGen/readmeEmitter.js +7 -6
  79. package/dist/automationGen/readmeEmitter.js.map +1 -1
  80. package/dist/automationGen/run.js +388 -0
  81. package/dist/automationGen/run.js.map +1 -0
  82. package/dist/automationGen/suitePlan.js +17 -10
  83. package/dist/automationGen/suitePlan.js.map +1 -1
  84. package/dist/automationGen/validation.js +52 -12
  85. package/dist/automationGen/validation.js.map +1 -1
  86. package/dist/build/parseBuildLog.js +235 -31
  87. package/dist/build/parseBuildLog.js.map +1 -1
  88. package/dist/build/plan.js +39 -18
  89. package/dist/build/plan.js.map +1 -1
  90. package/dist/ci/preflight.js +13 -4
  91. package/dist/ci/preflight.js.map +1 -1
  92. package/dist/cli/gc.js +74 -0
  93. package/dist/cli/gc.js.map +1 -0
  94. package/dist/cli/init.js +217 -26
  95. package/dist/cli/init.js.map +1 -1
  96. package/dist/cli/main.js +50 -0
  97. package/dist/cli/main.js.map +1 -0
  98. package/dist/cli/report.js +218 -0
  99. package/dist/cli/report.js.map +1 -0
  100. package/dist/cli/scan.js +36 -15
  101. package/dist/cli/scan.js.map +1 -1
  102. package/dist/cli/suite.js +7 -7
  103. package/dist/cli/suite.js.map +1 -1
  104. package/dist/cli/verify.js +7 -7
  105. package/dist/cli/verify.js.map +1 -1
  106. package/dist/consent/consent.js +162 -5
  107. package/dist/consent/consent.js.map +1 -1
  108. package/dist/context/detect.js +55 -18
  109. package/dist/context/detect.js.map +1 -1
  110. package/dist/context/findApps.js +22 -8
  111. package/dist/context/findApps.js.map +1 -1
  112. package/dist/context/projectRoot.js +174 -19
  113. package/dist/context/projectRoot.js.map +1 -1
  114. package/dist/context/scan.js +1 -1
  115. package/dist/context/scan.js.map +1 -1
  116. package/dist/core/capabilityGroups.js +90 -0
  117. package/dist/core/capabilityGroups.js.map +1 -0
  118. package/dist/core/target.js +10 -8
  119. package/dist/core/target.js.map +1 -1
  120. package/dist/core/targetPlan.js +160 -39
  121. package/dist/core/targetPlan.js.map +1 -1
  122. package/dist/drivers/DirectDriver.js +317 -43
  123. package/dist/drivers/DirectDriver.js.map +1 -1
  124. package/dist/drivers/SimctlDriver.js +124 -8
  125. package/dist/drivers/SimctlDriver.js.map +1 -1
  126. package/dist/drivers/WdaDriver.js +365 -55
  127. package/dist/drivers/WdaDriver.js.map +1 -1
  128. package/dist/explore/candidates.js +7 -3
  129. package/dist/explore/candidates.js.map +1 -1
  130. package/dist/explore/graph.js +15 -6
  131. package/dist/explore/graph.js.map +1 -1
  132. package/dist/explore/memory.js +4 -1
  133. package/dist/explore/memory.js.map +1 -1
  134. package/dist/explore/planner.js +54 -9
  135. package/dist/explore/planner.js.map +1 -1
  136. package/dist/explore/policy.js +11 -6
  137. package/dist/explore/policy.js.map +1 -1
  138. package/dist/explore/runner.js +363 -190
  139. package/dist/explore/runner.js.map +1 -1
  140. package/dist/explore/signatures.js +4 -2
  141. package/dist/explore/signatures.js.map +1 -1
  142. package/dist/explore/suite.js +3 -1
  143. package/dist/explore/suite.js.map +1 -1
  144. package/dist/featureTesting/executionBootstrap.js +137 -29
  145. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  146. package/dist/featureTesting/featureMap.js +6 -3
  147. package/dist/featureTesting/featureMap.js.map +1 -1
  148. package/dist/featureTesting/featureScope.js +101 -20
  149. package/dist/featureTesting/featureScope.js.map +1 -1
  150. package/dist/featureTesting/mapFeatureScope.js +44 -15
  151. package/dist/featureTesting/mapFeatureScope.js.map +1 -1
  152. package/dist/featureTesting/objectiveModel.js +15 -11
  153. package/dist/featureTesting/objectiveModel.js.map +1 -1
  154. package/dist/featureTesting/resultMerge.js +16 -8
  155. package/dist/featureTesting/resultMerge.js.map +1 -1
  156. package/dist/featureTesting/sources.js +8 -5
  157. package/dist/featureTesting/sources.js.map +1 -1
  158. package/dist/featureTesting/suiteBridge.js +8 -2
  159. package/dist/featureTesting/suiteBridge.js.map +1 -1
  160. package/dist/featureTesting/synonyms.js +32 -4
  161. package/dist/featureTesting/synonyms.js.map +1 -1
  162. package/dist/featureTesting/testCaseFactory.js +27 -15
  163. package/dist/featureTesting/testCaseFactory.js.map +1 -1
  164. package/dist/featureTesting/testPlan.js +34 -12
  165. package/dist/featureTesting/testPlan.js.map +1 -1
  166. package/dist/firstRun/authStateMachine.js +1 -1
  167. package/dist/firstRun/authStateMachine.js.map +1 -1
  168. package/dist/firstRun/classifyScreen.js +9 -6
  169. package/dist/firstRun/classifyScreen.js.map +1 -1
  170. package/dist/firstRun/firstRunPlanner.js +16 -13
  171. package/dist/firstRun/firstRunPlanner.js.map +1 -1
  172. package/dist/firstRun/firstRunRunner.js +92 -31
  173. package/dist/firstRun/firstRunRunner.js.map +1 -1
  174. package/dist/firstRun/generatedDataPolicy.js +25 -15
  175. package/dist/firstRun/generatedDataPolicy.js.map +1 -1
  176. package/dist/firstRun/inputPlanner.js +45 -11
  177. package/dist/firstRun/inputPlanner.js.map +1 -1
  178. package/dist/firstRun/onboardingStateMachine.js +1 -1
  179. package/dist/firstRun/onboardingStateMachine.js.map +1 -1
  180. package/dist/firstRun/paywallPolicy.js +3 -3
  181. package/dist/firstRun/paywallPolicy.js.map +1 -1
  182. package/dist/firstRun/types.js +1 -1
  183. package/dist/firstRun/types.js.map +1 -1
  184. package/dist/fixtures/catalog.js +72 -9
  185. package/dist/fixtures/catalog.js.map +1 -1
  186. package/dist/fixtures/load.js +50 -0
  187. package/dist/fixtures/load.js.map +1 -0
  188. package/dist/flows/discover.js +2 -2
  189. package/dist/flows/discover.js.map +1 -1
  190. package/dist/flows/generate.js +68 -17
  191. package/dist/flows/generate.js.map +1 -1
  192. package/dist/flows/lint.js +88 -14
  193. package/dist/flows/lint.js.map +1 -1
  194. package/dist/flows/pack.js +21 -5
  195. package/dist/flows/pack.js.map +1 -1
  196. package/dist/flows/paths.js +57 -0
  197. package/dist/flows/paths.js.map +1 -0
  198. package/dist/flows/repair.js +176 -37
  199. package/dist/flows/repair.js.map +1 -1
  200. package/dist/flows/run.js +308 -99
  201. package/dist/flows/run.js.map +1 -1
  202. package/dist/flows/schema.js +107 -25
  203. package/dist/flows/schema.js.map +1 -1
  204. package/dist/flows/seedExec.js +8 -4
  205. package/dist/flows/seedExec.js.map +1 -1
  206. package/dist/flows/templates.js +5 -5
  207. package/dist/index.js +50 -8
  208. package/dist/index.js.map +1 -1
  209. package/dist/ios/signing.js +32 -3
  210. package/dist/ios/signing.js.map +1 -1
  211. package/dist/issues/classify.js +28 -20
  212. package/dist/issues/classify.js.map +1 -1
  213. package/dist/issues/fingerprint.js +35 -9
  214. package/dist/issues/fingerprint.js.map +1 -1
  215. package/dist/issues/index.js +93 -46
  216. package/dist/issues/index.js.map +1 -1
  217. package/dist/issues/metrics.js +6 -5
  218. package/dist/issues/metrics.js.map +1 -1
  219. package/dist/issues/recurrence.js +35 -8
  220. package/dist/issues/recurrence.js.map +1 -1
  221. package/dist/issues/report.js +12 -8
  222. package/dist/issues/report.js.map +1 -1
  223. package/dist/issues/reportBridge.js +40 -9
  224. package/dist/issues/reportBridge.js.map +1 -1
  225. package/dist/issues/schema.js +2 -2
  226. package/dist/issues/schema.js.map +1 -1
  227. package/dist/issues/sourceRevision.js +2 -2
  228. package/dist/issues/sourceRevision.js.map +1 -1
  229. package/dist/issues/store.js +110 -32
  230. package/dist/issues/store.js.map +1 -1
  231. package/dist/lib/abortScope.js +48 -0
  232. package/dist/lib/abortScope.js.map +1 -0
  233. package/dist/lib/android.js +84 -8
  234. package/dist/lib/android.js.map +1 -1
  235. package/dist/lib/coordSpace.js +1 -1
  236. package/dist/lib/coordSpace.js.map +1 -1
  237. package/dist/lib/device.js +53 -12
  238. package/dist/lib/device.js.map +1 -1
  239. package/dist/lib/gestures.js +141 -0
  240. package/dist/lib/gestures.js.map +1 -0
  241. package/dist/lib/gitignore.js +4 -3
  242. package/dist/lib/gitignore.js.map +1 -1
  243. package/dist/lib/image.js +12 -3
  244. package/dist/lib/image.js.map +1 -1
  245. package/dist/lib/lockfile.js +399 -0
  246. package/dist/lib/lockfile.js.map +1 -0
  247. package/dist/lib/needsInput.js +9 -6
  248. package/dist/lib/needsInput.js.map +1 -1
  249. package/dist/lib/png.js +4 -3
  250. package/dist/lib/png.js.map +1 -1
  251. package/dist/lib/redact.js +170 -9
  252. package/dist/lib/redact.js.map +1 -1
  253. package/dist/lib/result.js +94 -10
  254. package/dist/lib/result.js.map +1 -1
  255. package/dist/lib/schemaHash.js +7 -5
  256. package/dist/lib/schemaHash.js.map +1 -1
  257. package/dist/lib/sensitive.js +6 -3
  258. package/dist/lib/sensitive.js.map +1 -1
  259. package/dist/lib/simctl.js +3 -3
  260. package/dist/lib/simctl.js.map +1 -1
  261. package/dist/lib/spawn.js +78 -17
  262. package/dist/lib/spawn.js.map +1 -1
  263. package/dist/lib/toolAnnotations.js +114 -0
  264. package/dist/lib/toolAnnotations.js.map +1 -0
  265. package/dist/lib/wda.js +321 -25
  266. package/dist/lib/wda.js.map +1 -1
  267. package/dist/lib/wdaConfig.js +12 -5
  268. package/dist/lib/wdaConfig.js.map +1 -1
  269. package/dist/lib/wdaTune.js +52 -23
  270. package/dist/lib/wdaTune.js.map +1 -1
  271. package/dist/mobileAudit/checks.js +150 -28
  272. package/dist/mobileAudit/checks.js.map +1 -1
  273. package/dist/mobileAudit/evidence.js +4 -4
  274. package/dist/mobileAudit/evidence.js.map +1 -1
  275. package/dist/mobileAudit/profiles.js +121 -20
  276. package/dist/mobileAudit/profiles.js.map +1 -1
  277. package/dist/mobileAudit/results.js +9 -4
  278. package/dist/mobileAudit/results.js.map +1 -1
  279. package/dist/mobileAudit/runner.js +214 -35
  280. package/dist/mobileAudit/runner.js.map +1 -1
  281. package/dist/oracle/auth.js +1 -1
  282. package/dist/oracle/auth.js.map +1 -1
  283. package/dist/oracle/failures.js +1085 -145
  284. package/dist/oracle/failures.js.map +1 -1
  285. package/dist/oracle/health.js +148 -26
  286. package/dist/oracle/health.js.map +1 -1
  287. package/dist/oracle/locator.js +148 -19
  288. package/dist/oracle/locator.js.map +1 -1
  289. package/dist/oracle/record.js +3 -3
  290. package/dist/oracle/record.js.map +1 -1
  291. package/dist/orchestration/envelope.js +2 -2
  292. package/dist/orchestration/envelope.js.map +1 -1
  293. package/dist/orchestration/goal.js +7 -13
  294. package/dist/orchestration/goal.js.map +1 -1
  295. package/dist/orchestration/testThis/execute.js +167 -0
  296. package/dist/orchestration/testThis/execute.js.map +1 -0
  297. package/dist/orchestration/testThis/pipeline.js +413 -0
  298. package/dist/orchestration/testThis/pipeline.js.map +1 -0
  299. package/dist/orchestration/testThis/plan.js +519 -0
  300. package/dist/orchestration/testThis/plan.js.map +1 -0
  301. package/dist/orchestration/testThis/sessionIntent.js +72 -0
  302. package/dist/orchestration/testThis/sessionIntent.js.map +1 -0
  303. package/dist/orchestration/testThis/terminal.js +136 -0
  304. package/dist/orchestration/testThis/terminal.js.map +1 -0
  305. package/dist/orchestration/testThis/types.js +12 -0
  306. package/dist/orchestration/testThis/types.js.map +1 -0
  307. package/dist/plan/plan.js +9 -9
  308. package/dist/plan/plan.js.map +1 -1
  309. package/dist/prompts/index.js +24 -24
  310. package/dist/prompts/index.js.map +1 -1
  311. package/dist/report/coverage.js +0 -1
  312. package/dist/report/coverage.js.map +1 -1
  313. package/dist/report/evidence.js.map +1 -1
  314. package/dist/report/export.js +449 -72
  315. package/dist/report/export.js.map +1 -1
  316. package/dist/report/findingsDedupe.js +41 -0
  317. package/dist/report/findingsDedupe.js.map +1 -0
  318. package/dist/report/flake.js +13 -14
  319. package/dist/report/flake.js.map +1 -1
  320. package/dist/report/history.js +72 -38
  321. package/dist/report/history.js.map +1 -1
  322. package/dist/report/policy.js +18 -7
  323. package/dist/report/policy.js.map +1 -1
  324. package/dist/report/qaLevel.js +10 -16
  325. package/dist/report/qaLevel.js.map +1 -1
  326. package/dist/report/readiness.js +42 -12
  327. package/dist/report/readiness.js.map +1 -1
  328. package/dist/report/sarifSources.js +160 -0
  329. package/dist/report/sarifSources.js.map +1 -0
  330. package/dist/report/summary.js +2 -2
  331. package/dist/report/summary.js.map +1 -1
  332. package/dist/report/testCatalog.js +4 -4
  333. package/dist/report/testCatalog.js.map +1 -1
  334. package/dist/report/timing.js +23 -23
  335. package/dist/report/timing.js.map +1 -1
  336. package/dist/report/toolHealth.js +124 -0
  337. package/dist/report/toolHealth.js.map +1 -0
  338. package/dist/server.js +501 -69
  339. package/dist/server.js.map +1 -1
  340. package/dist/services/automationGenerate.js +8 -2
  341. package/dist/services/automationGenerate.js.map +1 -1
  342. package/dist/services/build.js +10 -4
  343. package/dist/services/build.js.map +1 -1
  344. package/dist/services/flowGenerate.js +75 -0
  345. package/dist/services/flowGenerate.js.map +1 -0
  346. package/dist/services/preflight.js +40 -8
  347. package/dist/services/preflight.js.map +1 -1
  348. package/dist/services/prepareAndroid.js +92 -18
  349. package/dist/services/prepareAndroid.js.map +1 -1
  350. package/dist/services/prepareIos.js +94 -17
  351. package/dist/services/prepareIos.js.map +1 -1
  352. package/dist/services/report.js +271 -80
  353. package/dist/services/report.js.map +1 -1
  354. package/dist/services/smoke.js +68 -22
  355. package/dist/services/smoke.js.map +1 -1
  356. package/dist/services/suiteGenerate.js +43 -6
  357. package/dist/services/suiteGenerate.js.map +1 -1
  358. package/dist/services/testSuiteKnowledge.js +78 -33
  359. package/dist/services/testSuiteKnowledge.js.map +1 -1
  360. package/dist/session/attach.js +284 -4
  361. package/dist/session/attach.js.map +1 -1
  362. package/dist/session/processRegistry.js +433 -0
  363. package/dist/session/processRegistry.js.map +1 -0
  364. package/dist/session/progress.js +1 -1
  365. package/dist/session/progress.js.map +1 -1
  366. package/dist/session/retention.js +201 -0
  367. package/dist/session/retention.js.map +1 -0
  368. package/dist/session/store.js +459 -59
  369. package/dist/session/store.js.map +1 -1
  370. package/dist/snapshot/overlays.js +60 -10
  371. package/dist/snapshot/overlays.js.map +1 -1
  372. package/dist/snapshot/parse.js +30 -12
  373. package/dist/snapshot/parse.js.map +1 -1
  374. package/dist/snapshot/present.js +36 -6
  375. package/dist/snapshot/present.js.map +1 -1
  376. package/dist/snapshot/settle.js +39 -9
  377. package/dist/snapshot/settle.js.map +1 -1
  378. package/dist/state/consent.js +99 -0
  379. package/dist/state/consent.js.map +1 -0
  380. package/dist/state/profile.js +82 -11
  381. package/dist/state/profile.js.map +1 -1
  382. package/dist/suite/compile.js +15 -6
  383. package/dist/suite/compile.js.map +1 -1
  384. package/dist/suite/lint.js +22 -4
  385. package/dist/suite/lint.js.map +1 -1
  386. package/dist/suite/pom.js +75 -44
  387. package/dist/suite/pom.js.map +1 -1
  388. package/dist/suite/secretGuard.js +266 -0
  389. package/dist/suite/secretGuard.js.map +1 -0
  390. package/dist/suite/testcase.js +15 -11
  391. package/dist/suite/testcase.js.map +1 -1
  392. package/dist/testSuite/exporter.js +7 -3
  393. package/dist/testSuite/exporter.js.map +1 -1
  394. package/dist/testSuite/generator.js +43 -14
  395. package/dist/testSuite/generator.js.map +1 -1
  396. package/dist/testSuite/history.js +1 -1
  397. package/dist/testSuite/history.js.map +1 -1
  398. package/dist/testSuite/issueLinks.js +3 -3
  399. package/dist/testSuite/issueLinks.js.map +1 -1
  400. package/dist/testSuite/lint.js +4 -4
  401. package/dist/testSuite/lint.js.map +1 -1
  402. package/dist/testSuite/merge.js +13 -7
  403. package/dist/testSuite/merge.js.map +1 -1
  404. package/dist/testSuite/schema.js +21 -16
  405. package/dist/testSuite/schema.js.map +1 -1
  406. package/dist/testSuite/store.js +12 -4
  407. package/dist/testSuite/store.js.map +1 -1
  408. package/dist/testSuite/traceability.js +2 -2
  409. package/dist/testSuite/traceability.js.map +1 -1
  410. package/dist/tools/act.js +868 -174
  411. package/dist/tools/act.js.map +1 -1
  412. package/dist/tools/agent.js +521 -112
  413. package/dist/tools/agent.js.map +1 -1
  414. package/dist/tools/appControl.js +82 -19
  415. package/dist/tools/appControl.js.map +1 -1
  416. package/dist/tools/appMap.js +393 -189
  417. package/dist/tools/appMap.js.map +1 -1
  418. package/dist/tools/build.js +92 -51
  419. package/dist/tools/build.js.map +1 -1
  420. package/dist/tools/bundletool.js +141 -40
  421. package/dist/tools/bundletool.js.map +1 -1
  422. package/dist/tools/clearOverlay.js +98 -18
  423. package/dist/tools/clearOverlay.js.map +1 -1
  424. package/dist/tools/device.js +108 -19
  425. package/dist/tools/device.js.map +1 -1
  426. package/dist/tools/doctor.js +98 -35
  427. package/dist/tools/doctor.js.map +1 -1
  428. package/dist/tools/explore.js +110 -42
  429. package/dist/tools/explore.js.map +1 -1
  430. package/dist/tools/featureTesting.js +152 -130
  431. package/dist/tools/featureTesting.js.map +1 -1
  432. package/dist/tools/firstRun.js +94 -66
  433. package/dist/tools/firstRun.js.map +1 -1
  434. package/dist/tools/flow.js +347 -132
  435. package/dist/tools/flow.js.map +1 -1
  436. package/dist/tools/flowRepair.js +42 -10
  437. package/dist/tools/flowRepair.js.map +1 -1
  438. package/dist/tools/generate.js +199 -0
  439. package/dist/tools/generate.js.map +1 -0
  440. package/dist/tools/getArtifact.js +37 -8
  441. package/dist/tools/getArtifact.js.map +1 -1
  442. package/dist/tools/health.js +17 -12
  443. package/dist/tools/health.js.map +1 -1
  444. package/dist/tools/ios.js +148 -194
  445. package/dist/tools/ios.js.map +1 -1
  446. package/dist/tools/issues.js +256 -171
  447. package/dist/tools/issues.js.map +1 -1
  448. package/dist/tools/jobs.js +55 -11
  449. package/dist/tools/jobs.js.map +1 -1
  450. package/dist/tools/metro.js +84 -38
  451. package/dist/tools/metro.js.map +1 -1
  452. package/dist/tools/mobileAudit.js +83 -26
  453. package/dist/tools/mobileAudit.js.map +1 -1
  454. package/dist/tools/network.js +36 -14
  455. package/dist/tools/network.js.map +1 -1
  456. package/dist/tools/note.js +31 -17
  457. package/dist/tools/note.js.map +1 -1
  458. package/dist/tools/prepareIosTarget.js +31 -14
  459. package/dist/tools/prepareIosTarget.js.map +1 -1
  460. package/dist/tools/prepareTarget.js +193 -47
  461. package/dist/tools/prepareTarget.js.map +1 -1
  462. package/dist/tools/report.js +16 -8
  463. package/dist/tools/report.js.map +1 -1
  464. package/dist/tools/resolveArtifact.js +12 -12
  465. package/dist/tools/resolveArtifact.js.map +1 -1
  466. package/dist/tools/resolveTarget.js +81 -18
  467. package/dist/tools/resolveTarget.js.map +1 -1
  468. package/dist/tools/screenRecord.js +85 -20
  469. package/dist/tools/screenRecord.js.map +1 -1
  470. package/dist/tools/screenshot.js +42 -18
  471. package/dist/tools/screenshot.js.map +1 -1
  472. package/dist/tools/smoke.js +29 -12
  473. package/dist/tools/smoke.js.map +1 -1
  474. package/dist/tools/snapshot.js +157 -107
  475. package/dist/tools/snapshot.js.map +1 -1
  476. package/dist/tools/startSession.js +117 -84
  477. package/dist/tools/startSession.js.map +1 -1
  478. package/dist/tools/suite.js +391 -310
  479. package/dist/tools/suite.js.map +1 -1
  480. package/dist/tools/testSuite.js +112 -36
  481. package/dist/tools/testSuite.js.map +1 -1
  482. package/dist/tools/testThis.js +35 -802
  483. package/dist/tools/testThis.js.map +1 -1
  484. package/dist/tools/visual.js +698 -116
  485. package/dist/tools/visual.js.map +1 -1
  486. package/dist/tools/wait.js +9 -23
  487. package/dist/tools/wait.js.map +1 -1
  488. package/dist/tools/wda.js +312 -54
  489. package/dist/tools/wda.js.map +1 -1
  490. package/dist/version.js +37 -64
  491. package/dist/version.js.map +1 -1
  492. package/dist/visual/ocr.js +65 -8
  493. package/dist/visual/ocr.js.map +1 -1
  494. package/dist/visual/provider.js +87 -15
  495. package/dist/visual/provider.js.map +1 -1
  496. package/docs/README.md +24 -10
  497. package/docs/ci-reports.md +328 -0
  498. package/docs/concepts.md +227 -0
  499. package/docs/flows.md +153 -0
  500. package/docs/mcp-server.md +210 -104
  501. package/docs/physical-devices.md +84 -0
  502. package/docs/tools.md +808 -217
  503. package/package.json +16 -6
  504. package/dist/appMap/diff.js +0 -60
  505. package/dist/appMap/diff.js.map +0 -1
  506. package/dist/automation/maestroIr.js +0 -282
  507. package/dist/automation/maestroIr.js.map +0 -1
  508. package/dist/interop/maestro.js +0 -151
  509. package/dist/interop/maestro.js.map +0 -1
  510. package/dist/tools/assertVisual.js +0 -74
  511. package/dist/tools/assertVisual.js.map +0 -1
  512. package/dist/tools/automationGenerate.js +0 -394
  513. package/dist/tools/automationGenerate.js.map +0 -1
  514. package/dist/tools/capabilities.js +0 -209
  515. package/dist/tools/capabilities.js.map +0 -1
  516. package/dist/tools/detectContext.js +0 -41
  517. package/dist/tools/detectContext.js.map +0 -1
  518. package/dist/tools/flowGenerate.js +0 -55
  519. package/dist/tools/flowGenerate.js.map +0 -1
  520. package/dist/tools/history.js +0 -56
  521. package/dist/tools/history.js.map +0 -1
  522. package/dist/tools/idling.js +0 -104
  523. package/dist/tools/idling.js.map +0 -1
  524. package/dist/tools/inputCapabilities.js +0 -30
  525. package/dist/tools/inputCapabilities.js.map +0 -1
  526. package/dist/tools/locator.js +0 -57
  527. package/dist/tools/locator.js.map +0 -1
  528. package/dist/tools/maestro.js +0 -78
  529. package/dist/tools/maestro.js.map +0 -1
  530. package/dist/tools/permissions.js +0 -149
  531. package/dist/tools/permissions.js.map +0 -1
  532. package/dist/tools/plan.js +0 -49
  533. package/dist/tools/plan.js.map +0 -1
  534. package/dist/tools/screenInfo.js +0 -70
  535. package/dist/tools/screenInfo.js.map +0 -1
  536. package/dist/tools/seed.js +0 -101
  537. package/dist/tools/seed.js.map +0 -1
  538. package/dist/tools/state.js +0 -170
  539. package/dist/tools/state.js.map +0 -1
  540. package/dist/tools/visualText.js +0 -61
  541. package/dist/tools/visualText.js.map +0 -1
@@ -1,36 +1,43 @@
1
- // qa_pom_generate / qa_suite_generate / qa_testcase_generate / qa_suite_lint (roadmap §6 / §7 / §12).
1
+ // Per-run suite generation cores + qa_flow_compile.
2
2
  //
3
3
  // These turn the actions recorded during a session (qa_act) into a MAINTAINABLE POM suite under
4
- // .swipium/ — page objects (selectors), tests (reference elements by name), a suite, a test-case
5
- // catalog, and a locator audit — and lint the result for brittle/coordinate-only locators.
4
+ // .swipium/: page objects (selectors), tests (reference elements by name), a suite, a test-case
5
+ // catalog, and a locator audit. The generation entry points (page objects, full suite, test-case
6
+ // catalog) are exposed through qa_generate (src/tools/generate.ts); this file registers only
7
+ // qa_flow_compile, which compiles a generated POM suite into runnable Flow V2 for qa_flow_run.
6
8
  import { z } from 'zod';
7
9
  import { createHash } from 'node:crypto';
8
10
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
11
  import { join, dirname } from 'node:path';
10
- import { qaOk, qaError } from '../lib/result.js';
12
+ import { qaOk, qaError, unknownSessionError } from '../lib/result.js';
13
+ import { resolveProjectRoot, unresolvedProjectRootError } from '../context/projectRoot.js';
11
14
  import { requireConsent, consumeConsent } from '../consent/consent.js';
12
15
  import { generatePom } from '../suite/pom.js';
13
16
  import { generateTestCases } from '../suite/testcase.js';
14
- import { lintSuitePages } from '../suite/lint.js';
15
17
  import { compileSuite } from '../suite/compile.js';
16
18
  import { parseFlow } from '../flows/schema.js';
17
19
  import { runFlow } from '../flows/run.js';
18
20
  import { loadProjectConfig } from '../cli/scan.js';
19
21
  import { generateAndCompileSuite } from '../services/suiteGenerate.js';
22
+ import { assertNoSecretLeaks, findSecretLeaks, inputBindings, secretSafeNotes, structuralLiterals } from '../suite/secretGuard.js';
20
23
  import { getDriver } from '../session/attach.js';
21
24
  import { readinessForSession } from '../report/readiness.js';
22
25
  import { loadStateProfile, prepareStateProfile, teardownStateProfile, verifyStateProfile } from '../state/profile.js';
23
- import { hasStateProfileMutation, stateProfileAffects, stateRisk } from './state.js';
26
+ import { hasStateProfileMutation, stateProfileAffects, stateRisk } from '../state/consent.js';
24
27
  function stable(value) {
25
28
  if (Array.isArray(value))
26
29
  return value.map(stable);
27
30
  if (value && typeof value === 'object') {
28
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, stable(v)]));
31
+ return Object.fromEntries(Object.entries(value)
32
+ .sort(([a], [b]) => a.localeCompare(b))
33
+ .map(([k, v]) => [k, stable(v)]));
29
34
  }
30
35
  return value;
31
36
  }
32
37
  function hash(value) {
33
- return createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
38
+ return createHash('sha256')
39
+ .update(JSON.stringify(stable(value)))
40
+ .digest('hex');
34
41
  }
35
42
  function hashFile(path) {
36
43
  if (!path)
@@ -54,9 +61,9 @@ function compiledFlowIdentity(c) {
54
61
  };
55
62
  }
56
63
  function appIdOf(session) {
57
- return session.appId ?? (loadProjectConfig(session.root)?.appId ?? undefined);
64
+ return session.appId ?? loadProjectConfig(session.root)?.appId ?? undefined;
58
65
  }
59
- /** Map the replay outcome to the test-case catalog's replay status (Deliverable 4 — honest plumbing). */
66
+ /** Map the replay outcome to the test-case catalog's replay status (Deliverable 4, honest plumbing). */
60
67
  function catalogReplayStatus(mode, results, passed) {
61
68
  if (mode === 'none' || results.length === 0)
62
69
  return 'not_replayed';
@@ -79,6 +86,7 @@ function suiteDir(session) {
79
86
  }
80
87
  /** Write generated files under .swipium/, returning absolute paths written. */
81
88
  function writeFiles(session, files) {
89
+ assertNoSecretLeaks(files, session.secrets, 'suite generation', { structural: structuralLiterals(session.recordedActions) }); // backstop: never write a secret
82
90
  const base = suiteDir(session);
83
91
  const written = [];
84
92
  for (const f of files) {
@@ -89,12 +97,29 @@ function writeFiles(session, files) {
89
97
  }
90
98
  return written;
91
99
  }
100
+ /** Loud refusal when generated files would carry a registered secret value (nothing is written). */
101
+ function secretLeakError(session, files) {
102
+ const leaks = findSecretLeaks(files, session.secrets, { structural: structuralLiterals(session.recordedActions) });
103
+ if (!leaks.length)
104
+ return null;
105
+ return qaError({
106
+ what: `Not generated: a registered secret value would be written in plaintext (${leaks
107
+ .slice(0, 5)
108
+ .map((l) => `${l.path}:${l.line}`)
109
+ .join(', ')})`,
110
+ changedState: false,
111
+ retrySafe: false,
112
+ failureCode: 'SECRET_IN_GENERATED_OUTPUT',
113
+ nextSteps: ['Nothing was written. Re-record the credential step so it is captured as a ${VAR}, then regenerate.'],
114
+ });
115
+ }
92
116
  function requireActions(session) {
93
117
  if (!session.recordedActions.length) {
94
118
  return qaError({
95
119
  what: 'No actions recorded in this session yet',
96
120
  changedState: false,
97
121
  retrySafe: true,
122
+ failureCode: 'NO_RECORDED_ACTIONS',
98
123
  nextSteps: ['Drive the app with qa_act first (each action is recorded), then regenerate the suite.'],
99
124
  });
100
125
  }
@@ -103,312 +128,389 @@ function requireActions(session) {
103
128
  function pomFor(session, name) {
104
129
  const appId = appIdOf(session);
105
130
  const flowName = (name ?? `${(appId ?? 'app').split('.').pop()}-smoke`).replace(/[^\w.-]+/g, '-');
106
- const pom = generatePom(session.recordedActions, { name: flowName, appId, budgetProfile: session.budgetProfile });
131
+ const pom = generatePom(session.recordedActions, {
132
+ name: flowName,
133
+ appId,
134
+ budgetProfile: session.budgetProfile,
135
+ secrets: session.secrets,
136
+ inputs: inputBindings(session), // typed text equal to a stored input maps to its ${SWIPIUM_TEST_*} placeholder
137
+ });
107
138
  return { pom, flowName };
108
139
  }
109
- export function registerSuite(server, sessions) {
110
- // ---- qa_pom_generate ----
111
- server.registerTool('qa_pom_generate', {
112
- title: 'Generate page objects',
113
- description: 'Generate Screen/Page Object Model files from the actions recorded this session: one page object per screen with named elements (selectors hoisted out of tests), plus a locator audit (durable vs brittle, with app-code remediation). Pass save:true to write under .swipium/pages + .swipium/locators. Use qa_suite_generate for the full suite (pages + tests + suite + test cases).',
114
- inputSchema: {
115
- sessionId: z.string(),
116
- name: z.string().optional(),
117
- save: z.boolean().optional(),
118
- },
119
- }, async ({ sessionId, name, save }) => {
120
- const session = sessions.get(sessionId);
121
- if (!session)
122
- return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first.'] });
123
- const noActions = requireActions(session);
124
- if (noActions)
125
- return noActions;
126
- const { pom } = pomFor(session, name);
127
- const pageFiles = pom.files.filter((f) => f.path.startsWith('pages/') || f.path.startsWith('locators/'));
128
- const written = save ? writeFiles(session, pageFiles) : [];
129
- const summary = `Generated ${pom.pages.length} page object(s): ${pom.pages.map((p) => p.name).join(', ')}\n` +
130
- `locator audit: ${pom.audit.durable} durable / ${pom.audit.semi} semi / ${pom.audit.brittle} brittle (${pom.audit.brittlePct}% brittle)` +
131
- (save ? `\nsaved ${written.length} files under .swipium/` : `\n(not saved — pass save:true)`);
132
- return qaOk({ pages: pom.pages, audit: pom.audit, files: pageFiles, written }, summary);
133
- });
134
- // ---- qa_suite_generate ----
135
- server.registerTool('qa_suite_generate', {
136
- title: 'Generate an automation suite',
137
- description: 'Generate a full, committed-ready automation suite under .swipium/ from this session\'s recorded actions: page objects (pages/), a POM test (tests/), a suite (suites/), a test case catalog (testcases/), a locator audit (locators/), AND — unless compile:false — runnable Flow V2 compiled into flows/ + compiled/. Reports durability, brittle locators, and whether the suite is runnable. Pass save:false to preview without writing.',
138
- inputSchema: {
139
- sessionId: z.string(),
140
- name: z.string().optional(),
141
- save: z.boolean().optional().describe('Write files under .swipium/ (default true).'),
142
- compile: z.boolean().optional().describe('Also compile to runnable Flow V2 (default true).'),
143
- replay: z.enum(['none', 'dry_run', 'same_session', 'fresh_state']).optional().describe('Replay gate: dry_run validates compiled flows; same_session executes compiled flows now; fresh_state requires a state profile and is reported as blocked when absent. Default dry_run.'),
144
- stateProfile: z.string().optional().describe('Required for replay:"fresh_state" so CI-ready means replay from declared state.'),
145
- consentId: z.string().optional().describe('Required when replay:"fresh_state" applies a mutating state profile.'),
146
- approve: z.boolean().optional().describe('Approve the exact fresh-state replay consent request.'),
147
- },
148
- }, async ({ sessionId, name, save, compile, replay, stateProfile, consentId, approve }) => {
149
- const session = sessions.get(sessionId);
150
- if (!session)
151
- return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first.'] });
152
- const noActions = requireActions(session);
153
- if (noActions)
154
- return noActions;
155
- const res = generateAndCompileSuite(sessions, session, { name, save: save !== false, compile: compile !== false });
156
- const audit = res.audit;
157
- const ranOk = res.compiledFlows.filter((c) => c.ok).length;
158
- const replayMode = replay ?? 'dry_run';
159
- const replayResults = [];
160
- const replayEvidence = {
161
- stateLedgerUris: [],
162
- consentRequired: false,
163
- consentApproved: false,
164
- };
165
- if (replayMode === 'dry_run') {
166
- for (const c of res.compiledFlows)
167
- replayResults.push({ name: c.name, mode: replayMode, status: c.ok ? 'validated' : 'failed', reason: c.errors.join('; ') || undefined });
168
- }
169
- else if (replayMode === 'same_session') {
170
- const { driver } = await getDriver(session);
171
- if (!driver)
172
- replayResults.push({ name: res.name ?? 'suite', mode: replayMode, status: 'blocked', reason: 'no driver attached for same-session replay' });
173
- else {
174
- for (const c of res.compiledFlows.filter((f) => f.ok && f.flowPath)) {
175
- const parsed = parseFlow(readFileSync(c.flowPath, 'utf8'));
176
- if (!parsed.flow) {
177
- replayResults.push({ name: c.name, mode: replayMode, status: 'failed', reason: parsed.errors.join('; ') });
178
- continue;
179
- }
180
- const r = await runFlow(sessions, session, driver, parsed.flow, { variables: sessions.inputVariables(session) });
181
- replayResults.push({ name: c.name, mode: replayMode, status: r.passed ? 'passed' : 'failed', reason: r.reason });
140
+ /** Core handler for qa_generate target:"pom": page objects + locator audit from recorded actions. */
141
+ export async function runPomGenerate(sessions, { sessionId, name, save }) {
142
+ const session = sessions.get(sessionId);
143
+ if (!session)
144
+ return unknownSessionError(sessionId);
145
+ const noActions = requireActions(session);
146
+ if (noActions)
147
+ return noActions;
148
+ const { pom } = pomFor(session, name);
149
+ const pageFiles = pom.files.filter((f) => f.path.startsWith('pages/') || f.path.startsWith('locators/'));
150
+ const pomLeak = secretLeakError(session, pageFiles);
151
+ if (pomLeak)
152
+ return pomLeak;
153
+ const written = save ? writeFiles(session, pageFiles) : [];
154
+ const summary = `Generated ${pom.pages.length} page object(s): ${pom.pages.map((p) => p.name).join(', ')}\n` +
155
+ `locator audit: ${pom.audit.durable} durable / ${pom.audit.semi} semi / ${pom.audit.brittle} brittle (${pom.audit.brittlePct}% brittle)` +
156
+ (save ? `\nsaved ${written.length} files under .swipium/` : `\n(not saved; pass save:true)`);
157
+ return qaOk({ pages: pom.pages, audit: pom.audit, files: pageFiles, written }, summary);
158
+ }
159
+ /**
160
+ * Core handler for qa_generate target:"suite": the full per-run .swipium/ suite from recorded
161
+ * actions: pages + tests + suite + test cases + locator audit, plus compile and replay gates.
162
+ */
163
+ export async function runSuiteGenerate(sessions, { sessionId, name, save, compile, replay, stateProfile, consentId, approve }) {
164
+ const session = sessions.get(sessionId);
165
+ if (!session)
166
+ return unknownSessionError(sessionId);
167
+ const noActions = requireActions(session);
168
+ if (noActions)
169
+ return noActions;
170
+ const res = generateAndCompileSuite(sessions, session, { name, save: save !== false, compile: compile !== false });
171
+ if (res.failureCode)
172
+ return qaError({
173
+ what: `Suite not generated: ${res.skippedReason}`,
174
+ changedState: false,
175
+ retrySafe: false,
176
+ failureCode: 'SECRET_IN_GENERATED_OUTPUT',
177
+ nextSteps: [res.recommendation ?? 'Re-record the credential step so it is captured as a ${VAR}, then regenerate.'],
178
+ });
179
+ const audit = res.audit;
180
+ const ranOk = res.compiledFlows.filter((c) => c.ok).length;
181
+ const replayMode = replay ?? 'dry_run';
182
+ const replayResults = [];
183
+ const replayEvidence = {
184
+ stateLedgerUris: [],
185
+ consentRequired: false,
186
+ consentApproved: false,
187
+ };
188
+ if (replayMode === 'dry_run') {
189
+ for (const c of res.compiledFlows)
190
+ replayResults.push({
191
+ name: c.name,
192
+ mode: replayMode,
193
+ status: c.ok ? 'validated' : 'failed',
194
+ reason: c.errors.join('; ') || undefined,
195
+ });
196
+ }
197
+ else if (replayMode === 'same_session') {
198
+ const { driver } = await getDriver(session);
199
+ if (!driver)
200
+ replayResults.push({
201
+ name: res.name ?? 'suite',
202
+ mode: replayMode,
203
+ status: 'blocked',
204
+ reason: 'no driver attached for same-session replay',
205
+ });
206
+ else {
207
+ for (const c of res.compiledFlows.filter((f) => f.ok && f.flowPath)) {
208
+ const parsed = parseFlow(readFileSync(c.flowPath, 'utf8'));
209
+ if (!parsed.flow) {
210
+ replayResults.push({ name: c.name, mode: replayMode, status: 'failed', reason: parsed.errors.join('; ') });
211
+ continue;
182
212
  }
213
+ const r = await runFlow(sessions, session, driver, parsed.flow, { variables: sessions.inputVariables(session) });
214
+ replayResults.push({ name: c.name, mode: replayMode, status: r.passed ? 'passed' : 'failed', reason: r.reason });
183
215
  }
184
216
  }
185
- else if (replayMode === 'fresh_state') {
186
- const { driver } = await getDriver(session);
187
- if (!stateProfile)
188
- replayResults.push({ name: res.name ?? 'suite', mode: replayMode, status: 'blocked', reason: 'stateProfile is required for fresh_state replay' });
189
- else if (!driver)
190
- replayResults.push({ name: res.name ?? 'suite', mode: replayMode, status: 'blocked', reason: 'no driver attached for fresh-state replay' });
217
+ }
218
+ else if (replayMode === 'fresh_state') {
219
+ const { driver } = await getDriver(session);
220
+ if (!stateProfile)
221
+ replayResults.push({
222
+ name: res.name ?? 'suite',
223
+ mode: replayMode,
224
+ status: 'blocked',
225
+ reason: 'stateProfile is required for fresh_state replay',
226
+ });
227
+ else if (!driver)
228
+ replayResults.push({
229
+ name: res.name ?? 'suite',
230
+ mode: replayMode,
231
+ status: 'blocked',
232
+ reason: 'no driver attached for fresh-state replay',
233
+ });
234
+ else {
235
+ const loaded = loadStateProfile(session.root, stateProfile);
236
+ if (!loaded.profile)
237
+ replayResults.push({
238
+ name: res.name ?? 'suite',
239
+ mode: replayMode,
240
+ status: 'blocked',
241
+ reason: loaded.error ?? 'could not load state profile',
242
+ });
191
243
  else {
192
- const loaded = loadStateProfile(session.root, stateProfile);
193
- if (!loaded.profile)
194
- replayResults.push({ name: res.name ?? 'suite', mode: replayMode, status: 'blocked', reason: loaded.error ?? 'could not load state profile' });
195
- else {
196
- replayEvidence.stateProfile = loaded.profile.name;
197
- replayEvidence.stateProfilePath = loaded.path ?? null;
198
- const mutatingProfile = hasStateProfileMutation('prepare', loaded.profile, session) || hasStateProfileMutation('teardown', loaded.profile, session);
199
- replayEvidence.consentRequired = mutatingProfile;
200
- let mutationPayloadHash;
201
- if (mutatingProfile) {
202
- const prepareAffects = stateProfileAffects(session, 'prepare', loaded.profile, loaded.path);
203
- const teardownAffects = stateProfileAffects(session, 'teardown', loaded.profile, loaded.path);
204
- const affects = {
205
- suite: res.name ?? 'suite',
206
- replayMode,
207
- compiledFlows: res.compiledFlows.map(compiledFlowIdentity),
208
- suiteManifestPath: res.manifestPath ?? null,
209
- suiteManifestHash: hashFile(res.manifestPath),
210
- statePrepare: prepareAffects,
211
- stateTeardown: teardownAffects,
212
- };
213
- mutationPayloadHash = hash(affects);
214
- const risk = stateRisk('prepare', loaded.profile, session) === 'high' || stateRisk('teardown', loaded.profile, session) === 'high'
215
- ? 'high'
216
- : stateRisk('prepare', loaded.profile, session) === 'medium' || stateRisk('teardown', loaded.profile, session) === 'medium'
217
- ? 'medium'
218
- : 'low';
219
- const gate = consumeConsent(consentId, approve, { action: 'suite_fresh_state_replay', affects });
220
- if (!gate.approved) {
221
- sessions.recordMutation(session, {
222
- tool: 'qa_suite_generate',
223
- action: 'suite_fresh_state_replay',
224
- risk,
225
- target: affects,
226
- consent: { required: true, approved: false, payloadHash: mutationPayloadHash },
227
- status: 'requested',
228
- });
229
- return requireConsent({
230
- action: 'suite_fresh_state_replay',
231
- risk,
232
- exactCommand: `qa_suite_generate replay=fresh_state stateProfile=${loaded.profile.name}`,
233
- affects,
234
- explain: `Replay generated suite "${res.name ?? 'suite'}" from state profile "${loaded.profile.name}"? This can mutate app/device state through prepare/teardown before proving CI readiness.`,
235
- });
236
- }
237
- replayEvidence.consentApproved = true;
244
+ replayEvidence.stateProfile = loaded.profile.name;
245
+ replayEvidence.stateProfilePath = loaded.path ?? null;
246
+ const mutatingProfile = hasStateProfileMutation('prepare', loaded.profile, session) || hasStateProfileMutation('teardown', loaded.profile, session);
247
+ replayEvidence.consentRequired = mutatingProfile;
248
+ let mutationPayloadHash;
249
+ if (mutatingProfile) {
250
+ const prepareAffects = stateProfileAffects(session, 'prepare', loaded.profile, loaded.path);
251
+ const teardownAffects = stateProfileAffects(session, 'teardown', loaded.profile, loaded.path);
252
+ const affects = {
253
+ suite: res.name ?? 'suite',
254
+ replayMode,
255
+ compiledFlows: res.compiledFlows.map(compiledFlowIdentity),
256
+ suiteManifestPath: res.manifestPath ?? null,
257
+ suiteManifestHash: hashFile(res.manifestPath),
258
+ statePrepare: prepareAffects,
259
+ stateTeardown: teardownAffects,
260
+ };
261
+ mutationPayloadHash = hash(affects);
262
+ const risk = stateRisk('prepare', loaded.profile, session) === 'high' || stateRisk('teardown', loaded.profile, session) === 'high'
263
+ ? 'high'
264
+ : stateRisk('prepare', loaded.profile, session) === 'medium' || stateRisk('teardown', loaded.profile, session) === 'medium'
265
+ ? 'medium'
266
+ : 'low';
267
+ const gate = consumeConsent(consentId, approve, { action: 'suite_fresh_state_replay', affects });
268
+ if (!gate.approved) {
238
269
  sessions.recordMutation(session, {
239
- tool: 'qa_suite_generate',
270
+ tool: 'qa_generate',
240
271
  action: 'suite_fresh_state_replay',
241
272
  risk,
242
273
  target: affects,
243
- consent: { required: true, consentId, approved: true, payloadHash: mutationPayloadHash },
244
- status: 'approved',
274
+ consent: { required: true, approved: false, payloadHash: mutationPayloadHash },
275
+ status: 'requested',
276
+ });
277
+ return requireConsent({
278
+ action: 'suite_fresh_state_replay',
279
+ risk,
280
+ exactCommand: `qa_generate target=suite replay=fresh_state stateProfile=${loaded.profile.name}`,
281
+ affects,
282
+ explain: `Replay generated suite "${res.name ?? 'suite'}" from state profile "${loaded.profile.name}"? This can mutate app/device state through prepare/teardown before proving CI readiness.`,
245
283
  });
246
284
  }
247
- const ledgers = [];
248
- const saveLedger = (action, ledger) => {
249
- const uri = sessions.saveArtifact(session, 'state', `suite-${action}-${loaded.profile.name}-${Date.now()}.json`, JSON.stringify(ledger, null, 2), 'application/json', `suite fresh-state ${action} ledger`);
250
- ledgers.push(uri);
251
- replayEvidence.stateLedgerUris.push(uri);
252
- };
253
- const prepared = await prepareStateProfile(sessions, session, driver, loaded.profile);
254
- saveLedger('prepare', prepared);
255
- if (prepared.status === 'state_refused' || prepared.status === 'state_blocked') {
256
- replayResults.push({ name: res.name ?? 'suite', mode: replayMode, status: 'blocked', reason: `state prepare ${prepared.status}; ledgers=${ledgers.join(',')}` });
285
+ replayEvidence.consentApproved = true;
286
+ sessions.recordMutation(session, {
287
+ tool: 'qa_generate',
288
+ action: 'suite_fresh_state_replay',
289
+ risk,
290
+ target: affects,
291
+ consent: { required: true, consentId, approved: true, payloadHash: mutationPayloadHash },
292
+ status: 'approved',
293
+ });
294
+ }
295
+ const ledgers = [];
296
+ const saveLedger = (action, ledger) => {
297
+ const uri = sessions.saveArtifact(session, 'state', `suite-${action}-${loaded.profile.name}-${Date.now()}.json`, JSON.stringify(ledger, null, 2), 'application/json', `suite fresh-state ${action} ledger`);
298
+ ledgers.push(uri);
299
+ replayEvidence.stateLedgerUris.push(uri);
300
+ };
301
+ const prepared = await prepareStateProfile(sessions, session, driver, loaded.profile);
302
+ saveLedger('prepare', prepared);
303
+ if (prepared.status === 'state_refused' || prepared.status === 'state_blocked') {
304
+ replayResults.push({
305
+ name: res.name ?? 'suite',
306
+ mode: replayMode,
307
+ status: 'blocked',
308
+ reason: `state prepare ${prepared.status}; ledgers=${ledgers.join(',')}`,
309
+ });
310
+ }
311
+ else {
312
+ const verified = await verifyStateProfile(driver, loaded.profile);
313
+ saveLedger('verify', verified);
314
+ if (verified.status === 'state_blocked') {
315
+ replayResults.push({
316
+ name: res.name ?? 'suite',
317
+ mode: replayMode,
318
+ status: 'blocked',
319
+ reason: `state verify blocked; ledgers=${ledgers.join(',')}`,
320
+ });
257
321
  }
258
322
  else {
259
- const verified = await verifyStateProfile(driver, loaded.profile);
260
- saveLedger('verify', verified);
261
- if (verified.status === 'state_blocked') {
262
- replayResults.push({ name: res.name ?? 'suite', mode: replayMode, status: 'blocked', reason: `state verify blocked; ledgers=${ledgers.join(',')}` });
263
- }
264
- else {
265
- for (const c of res.compiledFlows.filter((f) => f.ok && f.flowPath)) {
266
- const parsed = parseFlow(readFileSync(c.flowPath, 'utf8'));
267
- if (!parsed.flow) {
268
- replayResults.push({ name: c.name, mode: replayMode, status: 'failed', reason: parsed.errors.join('; ') });
269
- continue;
270
- }
271
- const r = await runFlow(sessions, session, driver, parsed.flow, { variables: sessions.inputVariables(session) });
272
- replayResults.push({ name: c.name, mode: replayMode, status: r.passed ? 'passed' : 'failed', reason: r.reason });
323
+ for (const c of res.compiledFlows.filter((f) => f.ok && f.flowPath)) {
324
+ const parsed = parseFlow(readFileSync(c.flowPath, 'utf8'));
325
+ if (!parsed.flow) {
326
+ replayResults.push({ name: c.name, mode: replayMode, status: 'failed', reason: parsed.errors.join('; ') });
327
+ continue;
273
328
  }
329
+ const r = await runFlow(sessions, session, driver, parsed.flow, { variables: sessions.inputVariables(session) });
330
+ replayResults.push({ name: c.name, mode: replayMode, status: r.passed ? 'passed' : 'failed', reason: r.reason });
274
331
  }
275
332
  }
276
- const tornDown = await teardownStateProfile(sessions, session, driver, loaded.profile);
277
- saveLedger('teardown', tornDown);
278
- if (mutatingProfile) {
279
- const ok = replayResults.length > 0 && replayResults.every((r) => r.status === 'passed');
280
- sessions.recordMutation(session, {
281
- tool: 'qa_suite_generate',
282
- action: 'suite_fresh_state_replay',
283
- risk: stateRisk('prepare', loaded.profile, session),
284
- target: { stateProfile: loaded.profile.name, suite: res.name ?? 'suite', ledgerUris: ledgers },
285
- consent: { required: true, consentId, approved: true, payloadHash: mutationPayloadHash },
286
- status: ok ? 'executed' : 'blocked',
287
- ledgerUri: ledgers[0],
288
- detail: ok ? 'fresh_state replay passed' : 'fresh_state replay did not pass',
289
- });
290
- }
333
+ }
334
+ const tornDown = await teardownStateProfile(sessions, session, driver, loaded.profile);
335
+ saveLedger('teardown', tornDown);
336
+ if (mutatingProfile) {
337
+ const ok = replayResults.length > 0 && replayResults.every((r) => r.status === 'passed');
338
+ sessions.recordMutation(session, {
339
+ tool: 'qa_generate',
340
+ action: 'suite_fresh_state_replay',
341
+ risk: stateRisk('prepare', loaded.profile, session),
342
+ target: { stateProfile: loaded.profile.name, suite: res.name ?? 'suite', ledgerUris: ledgers },
343
+ consent: { required: true, consentId, approved: true, payloadHash: mutationPayloadHash },
344
+ status: ok ? 'executed' : 'blocked',
345
+ ledgerUri: ledgers[0],
346
+ detail: ok ? 'fresh_state replay passed' : 'fresh_state replay did not pass',
347
+ });
291
348
  }
292
349
  }
293
350
  }
294
- const replayPassed = replayResults.length > 0 && replayResults.every((r) => r.status === 'passed' || r.status === 'validated');
295
- const ciReadyGate = {
296
- compiled: res.suiteRunnable,
297
- freshStateReplay: replayMode === 'fresh_state' && replayResults.length > 0 && replayResults.every((r) => r.status === 'passed'),
298
- declaredStateProfile: replayMode === 'fresh_state' && !!stateProfile,
299
- stateEvidence: replayMode === 'fresh_state' && replayEvidence.stateLedgerUris.length >= 2,
300
- stableEvidence: replayMode === 'fresh_state' && replayResults.length > 0 && replayResults.every((r) => r.status === 'passed') && replayEvidence.stateLedgerUris.length >= 2,
301
- };
302
- const ciReady = Object.values(ciReadyGate).every(Boolean);
303
- const suiteReplayed = (replayMode === 'same_session' || replayMode === 'fresh_state') && replayPassed;
304
- const readinessLabels = readinessForSession(session, { suiteRunnable: res.suiteRunnable, suiteReplayed, ciReady });
305
- if (!readinessLabels.includes('generated'))
306
- readinessLabels.push('generated');
307
- if (res.suiteRunnable && !readinessLabels.includes('compiled'))
308
- readinessLabels.push('compiled');
309
- if (suiteReplayed && !readinessLabels.includes('replayed'))
310
- readinessLabels.push('replayed');
311
- if (ciReady && !readinessLabels.includes('ci_ready'))
312
- readinessLabels.push('ci_ready');
313
- const readiness = readinessLabels.at(-1) ?? 'generated';
314
- const written = res.manifestPath ? [...res.written] : res.written;
315
- if (res.manifestPath) {
316
- writeFileSync(res.manifestPath, JSON.stringify({
317
- schema: 'swipium.suite.manifest.v1',
318
- generatedAt: new Date(session.createdAt).toISOString(),
319
- name: res.name,
320
- suiteFile: 'suites/smoke.yaml',
321
- readinessLabels,
322
- readiness,
323
- suiteRunnable: res.suiteRunnable,
324
- replay: { mode: replayMode, results: replayResults, passed: replayPassed, evidence: replayEvidence },
325
- ciReady,
326
- ciReadyGate,
327
- compiledFlows: res.compiledFlows.map((f) => ({ name: f.name, slug: f.slug, ok: f.ok, errors: f.errors, flowPath: f.flowPath ?? null, compiledPath: f.compiledPath ?? null })),
328
- audit,
329
- variables: res.variables ?? [],
330
- }, null, 2));
331
- }
332
- // Plumb the ACTUAL replay outcome back into the written catalog (review item #5): the suite
333
- // service writes the catalog before replay (honest `not_replayed`); once we have a real replay
334
- // result, rewrite the YAML/MD so the committed test cases reflect proven replay status.
335
- let finalTestCases = res.testCases;
336
- if (res.manifestPath) {
337
- const replayStatus = catalogReplayStatus(replayMode, replayResults, replayPassed);
338
- if (replayStatus !== 'not_replayed') {
339
- const { pom, flowName } = pomFor(session, name);
340
- const tc = generateTestCases(pom, {
341
- appId: appIdOf(session),
342
- fixtures: session.fixtures,
343
- notes: session.notes,
344
- budgetProfile: session.budgetProfile,
345
- replayStatus,
346
- });
347
- writeFiles(session, [
348
- { path: `testcases/${flowName}.cases.yaml`, content: tc.yaml },
349
- { path: `testcases/${flowName}.cases.md`, content: tc.markdown },
350
- ]);
351
- finalTestCases = tc.cases;
352
- }
351
+ }
352
+ const replayPassed = replayResults.length > 0 && replayResults.every((r) => r.status === 'passed' || r.status === 'validated');
353
+ const ciReadyGate = {
354
+ compiled: res.suiteRunnable,
355
+ freshStateReplay: replayMode === 'fresh_state' && replayResults.length > 0 && replayResults.every((r) => r.status === 'passed'),
356
+ declaredStateProfile: replayMode === 'fresh_state' && !!stateProfile,
357
+ stateEvidence: replayMode === 'fresh_state' && replayEvidence.stateLedgerUris.length >= 2,
358
+ stableEvidence: replayMode === 'fresh_state' &&
359
+ replayResults.length > 0 &&
360
+ replayResults.every((r) => r.status === 'passed') &&
361
+ replayEvidence.stateLedgerUris.length >= 2,
362
+ };
363
+ const ciReady = Object.values(ciReadyGate).every(Boolean);
364
+ const suiteReplayed = (replayMode === 'same_session' || replayMode === 'fresh_state') && replayPassed;
365
+ const readinessLabels = readinessForSession(session, { suiteRunnable: res.suiteRunnable, suiteReplayed, ciReady });
366
+ if (!readinessLabels.includes('generated'))
367
+ readinessLabels.push('generated');
368
+ if (res.suiteRunnable && !readinessLabels.includes('compiled'))
369
+ readinessLabels.push('compiled');
370
+ if (suiteReplayed && !readinessLabels.includes('replayed'))
371
+ readinessLabels.push('replayed');
372
+ if (ciReady && !readinessLabels.includes('ci_ready'))
373
+ readinessLabels.push('ci_ready');
374
+ const readiness = readinessLabels.at(-1) ?? 'generated';
375
+ const written = res.manifestPath ? [...res.written] : res.written;
376
+ if (res.manifestPath) {
377
+ writeFileSync(res.manifestPath, JSON.stringify({
378
+ schema: 'swipium.suite.manifest.v1',
379
+ generatedAt: new Date(session.createdAt).toISOString(),
380
+ name: res.name,
381
+ suiteFile: 'suites/smoke.yaml',
382
+ readinessLabels,
383
+ readiness,
384
+ suiteRunnable: res.suiteRunnable,
385
+ replay: { mode: replayMode, results: replayResults, passed: replayPassed, evidence: replayEvidence },
386
+ ciReady,
387
+ ciReadyGate,
388
+ compiledFlows: res.compiledFlows.map((f) => ({
389
+ name: f.name,
390
+ slug: f.slug,
391
+ ok: f.ok,
392
+ errors: f.errors,
393
+ flowPath: f.flowPath ?? null,
394
+ compiledPath: f.compiledPath ?? null,
395
+ })),
396
+ audit,
397
+ variables: res.variables ?? [],
398
+ }, null, 2));
399
+ }
400
+ // Plumb the ACTUAL replay outcome back into the written catalog (review item #5): the suite
401
+ // service writes the catalog before replay (honest `not_replayed`); once we have a real replay
402
+ // result, rewrite the YAML/MD so the committed test cases reflect proven replay status.
403
+ let finalTestCases = res.testCases;
404
+ if (res.manifestPath) {
405
+ const replayStatus = catalogReplayStatus(replayMode, replayResults, replayPassed);
406
+ if (replayStatus !== 'not_replayed') {
407
+ const { pom, flowName } = pomFor(session, name);
408
+ const tc = generateTestCases(pom, {
409
+ appId: appIdOf(session),
410
+ fixtures: session.fixtures,
411
+ notes: secretSafeNotes(session.notes, session.secrets),
412
+ budgetProfile: session.budgetProfile,
413
+ replayStatus,
414
+ });
415
+ writeFiles(session, [
416
+ { path: `testcases/${flowName}.cases.yaml`, content: tc.yaml },
417
+ { path: `testcases/${flowName}.cases.md`, content: tc.markdown },
418
+ ]);
419
+ finalTestCases = tc.cases;
353
420
  }
354
- const summary = `✅ Suite "${res.name}" generated (${res.pages?.length ?? 0} pages, ${res.testCases?.length ?? 0} test case, suite + audit).\n` +
355
- `durability: ${audit.durable} durable / ${audit.semi} semi / ${audit.brittle} brittle (${audit.brittlePct}% brittle)` +
356
- (res.variables?.length ? `\nvariables: ${res.variables.join(', ')}` : '') +
357
- (audit.brittle ? `\n⚠ ${audit.brittle} brittle locator(s) — see locators/locator-audit.json for app-code fixes.` : '') +
358
- (written.length ? `\nwrote ${written.length} files under ${suiteDir(session)}` : `\n(preview — pass save:true to write)`) +
359
- (res.compiledFlows.length ? `\ncompiled ${ranOk}/${res.compiledFlows.length} runnable flow(s) → run: swipium suite run (or qa_flow_run).` : `\nNext: qa_suite_compile to produce runnable Flow V2.`) +
360
- `\nreplay gate (${replayMode}): ${replayPassed ? 'passed' : replayResults.length ? 'not proven' : 'not run'}; readiness=${readiness}; labels=${readinessLabels.join(' → ')}` +
361
- (ciReady ? '\nci_ready: compiled + fresh-state replay + state evidence proved' : replayMode === 'fresh_state' ? `\nci_ready missing: ${Object.entries(ciReadyGate).filter(([, ok]) => !ok).map(([k]) => k).join(', ') || 'none'}` : '');
362
- return qaOk({ name: res.name, pages: res.pages, audit, variables: res.variables, testCases: finalTestCases, written, manifestPath: res.manifestPath ?? null, compiledFlows: res.compiledFlows, suiteRunnable: res.suiteRunnable, replay: { mode: replayMode, results: replayResults, passed: replayPassed, evidence: replayEvidence }, readiness, readinessLabels, ciReady, ciReadyGate }, summary);
363
- });
364
- // ---- qa_testcase_generate ----
365
- server.registerTool('qa_testcase_generate', {
366
- title: 'Generate test case docs',
367
- description: 'Generate an industry-style test case catalog (TC-xxx: purpose, priority, preconditions, steps, expected, automation status, known blockers, evidence) from this session\'s recorded actions + observed outcomes. Returns YAML + Markdown. Pass save:true to write under .swipium/testcases.',
368
- inputSchema: {
369
- sessionId: z.string(),
370
- name: z.string().optional(),
371
- format: z.enum(['yaml', 'markdown', 'both']).optional(),
372
- save: z.boolean().optional(),
373
- },
374
- }, async ({ sessionId, name, format, save }) => {
375
- const session = sessions.get(sessionId);
376
- if (!session)
377
- return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first.'] });
378
- const noActions = requireActions(session);
379
- if (noActions)
380
- return noActions;
381
- const { pom, flowName } = pomFor(session, name);
382
- const tc = generateTestCases(pom, { appId: appIdOf(session), fixtures: session.fixtures, notes: session.notes, budgetProfile: session.budgetProfile });
383
- const fmt = format ?? 'both';
384
- const files = [];
385
- if (fmt !== 'markdown')
386
- files.push({ path: `testcases/${flowName}.cases.yaml`, content: tc.yaml });
387
- if (fmt !== 'yaml')
388
- files.push({ path: `testcases/${flowName}.cases.md`, content: tc.markdown });
389
- const written = save ? writeFiles(session, files) : [];
390
- const summary = `Generated ${tc.cases.length} test case(s): ${tc.cases.map((c) => `${c.id} ${c.title}`).join('; ')}` + (written.length ? `\nsaved under .swipium/testcases` : '');
391
- return qaOk({ cases: tc.cases, yaml: fmt !== 'markdown' ? tc.yaml : undefined, markdown: fmt !== 'yaml' ? tc.markdown : undefined, written }, summary);
421
+ }
422
+ const summary = `✅ Suite "${res.name}" generated (${res.pages?.length ?? 0} pages, ${res.testCases?.length ?? 0} test case, suite + audit).\n` +
423
+ `durability: ${audit.durable} durable / ${audit.semi} semi / ${audit.brittle} brittle (${audit.brittlePct}% brittle)` +
424
+ (res.variables?.length ? `\nvariables: ${res.variables.join(', ')}` : '') +
425
+ (audit.brittle ? `\n⚠ ${audit.brittle} brittle locator(s), see locators/locator-audit.json for app-code fixes.` : '') +
426
+ (written.length ? `\nwrote ${written.length} files under ${suiteDir(session)}` : `\n(preview; pass save:true to write)`) +
427
+ (res.compiledFlows.length
428
+ ? `\ncompiled ${ranOk}/${res.compiledFlows.length} runnable flow(s) > run: swipium suite run (or qa_flow_run).`
429
+ : `\nNext: qa_flow_compile to produce runnable Flow V2.`) +
430
+ `\nreplay gate (${replayMode}): ${replayPassed ? 'passed' : replayResults.length ? 'not proven' : 'not run'}; readiness=${readiness}; labels=${readinessLabels.join(' > ')}` +
431
+ (ciReady
432
+ ? '\nci_ready: compiled + fresh-state replay + state evidence proved'
433
+ : replayMode === 'fresh_state'
434
+ ? `\nci_ready missing: ${Object.entries(ciReadyGate)
435
+ .filter(([, ok]) => !ok)
436
+ .map(([k]) => k)
437
+ .join(', ') || 'none'}`
438
+ : '');
439
+ return qaOk({
440
+ name: res.name,
441
+ pages: res.pages,
442
+ audit,
443
+ variables: res.variables,
444
+ testCases: finalTestCases,
445
+ written,
446
+ manifestPath: res.manifestPath ?? null,
447
+ compiledFlows: res.compiledFlows,
448
+ suiteRunnable: res.suiteRunnable,
449
+ replay: { mode: replayMode, results: replayResults, passed: replayPassed, evidence: replayEvidence },
450
+ readiness,
451
+ readinessLabels,
452
+ ciReady,
453
+ ciReadyGate,
454
+ }, summary);
455
+ }
456
+ /** Core handler for qa_generate target:"testcases": industry-style test case catalog from recorded actions. */
457
+ export async function runTestcaseGenerate(sessions, { sessionId, name, format, save }) {
458
+ const session = sessions.get(sessionId);
459
+ if (!session)
460
+ return unknownSessionError(sessionId);
461
+ const noActions = requireActions(session);
462
+ if (noActions)
463
+ return noActions;
464
+ const { pom, flowName } = pomFor(session, name);
465
+ const tc = generateTestCases(pom, {
466
+ appId: appIdOf(session),
467
+ fixtures: session.fixtures,
468
+ notes: secretSafeNotes(session.notes, session.secrets),
469
+ budgetProfile: session.budgetProfile,
392
470
  });
393
- // ---- qa_suite_compile ----
394
- server.registerTool('qa_suite_compile', {
471
+ const fmt = format ?? 'both';
472
+ const files = [];
473
+ if (fmt !== 'markdown')
474
+ files.push({ path: `testcases/${flowName}.cases.yaml`, content: tc.yaml });
475
+ if (fmt !== 'yaml')
476
+ files.push({ path: `testcases/${flowName}.cases.md`, content: tc.markdown });
477
+ const tcLeak = secretLeakError(session, files);
478
+ if (tcLeak)
479
+ return tcLeak;
480
+ const written = save ? writeFiles(session, files) : [];
481
+ const summary = `Generated ${tc.cases.length} test case(s): ${tc.cases.map((c) => `${c.id} ${c.title}`).join('; ')}` +
482
+ (written.length ? `\nsaved under .swipium/testcases` : '');
483
+ return qaOk({ cases: tc.cases, yaml: fmt !== 'markdown' ? tc.yaml : undefined, markdown: fmt !== 'yaml' ? tc.markdown : undefined, written }, summary);
484
+ }
485
+ export function registerSuite(server, sessions) {
486
+ // ---- qa_flow_compile ----
487
+ server.registerTool('qa_flow_compile', {
395
488
  title: 'Compile a POM suite to runnable flows',
396
- description: 'Compile the generated POM suite (.swipium/suites/<suite>.yaml → its POM tests) into Flow V2 YAML that qa_flow_run and `swipium ci` can execute. Resolves page-object element refs to selectors, carries variables/secrets, and writes runnable flows to .swipium/flows/ (+ a readable copy under .swipium/compiled/). Validates each compiled flow through parseFlow and reports any errors. This is the step that makes a generated suite runnable, not just documentation.',
489
+ description: 'Compile an existing POM suite on disk (.swipium/suites/<suite>.yaml, e.g. committed or hand-edited) into runnable Flow V2 ' +
490
+ 'for qa_flow_run: resolves page-object refs to selectors, carries variables, writes .swipium/flows/<slug>.yaml (+ a copy ' +
491
+ 'under .swipium/compiled/), and validates each flow. Needs no session or recorded actions; qa_generate target:"suite" ' +
492
+ 'already compiles the suite it generates from a run.',
397
493
  inputSchema: {
398
494
  sessionId: z.string().optional(),
399
495
  projectRoot: z.string().optional(),
400
496
  suite: z.string().optional().describe('Suite file relative to .swipium/ (default suites/smoke.yaml).'),
401
497
  },
402
498
  }, async ({ sessionId, projectRoot, suite }) => {
403
- let root;
404
- if (sessionId)
405
- root = sessions.get(sessionId)?.root;
406
- root = root ?? projectRoot;
407
- if (!root)
408
- return qaError({ what: 'No project root', changedState: false, retrySafe: true, nextSteps: ['Pass sessionId or projectRoot.'] });
499
+ let root = sessionId ? sessions.get(sessionId)?.root : undefined;
500
+ if (!root) {
501
+ const resolved = await resolveProjectRoot(server, projectRoot);
502
+ if (!resolved.root)
503
+ return unresolvedProjectRootError(resolved);
504
+ root = resolved.root;
505
+ }
409
506
  const result = compileSuite(root, suite ?? 'suites/smoke.yaml');
410
507
  if (result.errors.length && result.flows.length === 0) {
411
- return qaError({ what: `Could not compile suite: ${result.errors.join('; ')}`, changedState: false, retrySafe: true, nextSteps: ['Generate a suite first: qa_suite_generate, then qa_suite_compile.'] });
508
+ return qaError({
509
+ what: `Could not compile suite: ${result.errors.join('; ')}`,
510
+ changedState: false,
511
+ retrySafe: true,
512
+ nextSteps: ['Generate a suite first: qa_generate target:"suite", then qa_flow_compile.'],
513
+ });
412
514
  }
413
515
  const flowsDir = join(root, '.swipium', 'flows');
414
516
  const compiledDir = join(root, '.swipium', 'compiled');
@@ -421,7 +523,7 @@ export function registerSuite(server, sessions) {
421
523
  let flowPath;
422
524
  let compiledPath;
423
525
  if (ok) {
424
- flowPath = join(flowsDir, `${slug}.yaml`); // discoverable by `swipium ci --flow <slug>`
526
+ flowPath = join(flowsDir, `${slug}.yaml`); // discoverable by qa_flow_run flow:"<slug>"
425
527
  compiledPath = join(compiledDir, `${slug}.flow.yaml`);
426
528
  writeFileSync(flowPath, f.yaml);
427
529
  writeFileSync(compiledPath, f.yaml);
@@ -430,31 +532,10 @@ export function registerSuite(server, sessions) {
430
532
  });
431
533
  const okCount = compiled.filter((c) => c.ok).length;
432
534
  const summary = `Compiled ${okCount}/${compiled.length} flow(s) from ${result.suite}.\n` +
433
- compiled.map((c) => ` ${c.ok ? '✓' : '✗'} ${c.name}${c.ok ? ` → flows/${c.slug}.yaml (run: swipium ci --flow ${c.slug})` : `: ${c.errors.join('; ')}`}`).join('\n');
535
+ compiled
536
+ .map((c) => ` ${c.ok ? '✓' : '✗'} ${c.name}${c.ok ? ` > flows/${c.slug}.yaml (run: qa_flow_run flow:"${c.slug}")` : `: ${c.errors.join('; ')}`}`)
537
+ .join('\n');
434
538
  return qaOk({ suite: result.suite, flows: compiled, okCount }, summary);
435
539
  });
436
- // ---- qa_suite_lint ----
437
- server.registerTool('qa_suite_lint', {
438
- title: 'Lint the generated suite',
439
- description: 'Lint the page objects under .swipium/pages for durability problems: missing durable locators (coordinate-only), copy/locale-fragile text selectors, and dynamic-looking selectors. Reports each with the page/element and the app-code fix. Use after qa_suite_generate, before committing or wiring CI.',
440
- inputSchema: { sessionId: z.string().optional(), projectRoot: z.string().optional() },
441
- }, async ({ sessionId, projectRoot }) => {
442
- let root;
443
- if (sessionId)
444
- root = sessions.get(sessionId)?.root;
445
- root = root ?? projectRoot;
446
- if (!root)
447
- return qaError({ what: 'No project root', changedState: false, retrySafe: true, nextSteps: ['Pass sessionId or projectRoot.'] });
448
- const findings = lintSuitePages(root);
449
- if (!findings.exists) {
450
- return qaError({ what: 'No .swipium/pages to lint', changedState: false, retrySafe: true, nextSteps: ['Generate a suite first: qa_suite_generate.'] });
451
- }
452
- const errors = findings.items.filter((f) => f.severity === 'error');
453
- const summary = `Linted ${findings.pageCount} page object(s): ${findings.items.length} issue(s) (${errors.length} brittle).\n` +
454
- (findings.items.length
455
- ? findings.items.slice(0, 20).map((i) => ` ${i.severity === 'error' ? '✗' : '⚠'} ${i.page}.${i.element}: ${i.message}`).join('\n')
456
- : ' ✓ all locators are durable.');
457
- return qaOk({ pageCount: findings.pageCount, items: findings.items }, summary);
458
- });
459
540
  }
460
541
  //# sourceMappingURL=suite.js.map