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
package/dist/flows/run.js CHANGED
@@ -1,78 +1,83 @@
1
- // Flow runner v2 (NEXT-PLAN: Flow System V2). Drives a parsed Flow step-by-step using core
1
+ // Flow runner v2 (Flow System V2). Drives a parsed Flow step-by-step using core
2
2
  // primitives + the driver directly, WITHOUT re-entering through the model. Fail-fast, never
3
- // auto-retries (DESIGN §6). v2 adds: setup/teardown phases (teardown always runs), selector-bound
3
+ // auto-retries. v2 adds: setup/teardown phases (teardown always runs), selector-bound
4
4
  // inputText, visual steps (image match / diff / visual evidence), device-relative gestures,
5
5
  // overlay/network/lifecycle/seed/note steps, and typed per-step failure codes. On failure it
6
6
  // captures a screenshot + health so the result stands alone.
7
7
  import { readFileSync, existsSync } from 'node:fs';
8
- import { isAbsolute, join } from 'node:path';
8
+ import { join } from 'node:path';
9
9
  import { createHash } from 'node:crypto';
10
10
  import { parseSnapshot, signature } from '../snapshot/parse.js';
11
11
  import { settle } from '../snapshot/settle.js';
12
12
  import { checkHealth } from '../oracle/health.js';
13
13
  import { recordHealthFindings } from '../oracle/record.js';
14
+ import { isAbortError } from '../lib/abortScope.js';
14
15
  import { boundsBucket, resolveTarget, resourceIdMatches } from '../core/target.js';
15
16
  import { imageDiff, findTemplate } from '../lib/image.js';
16
17
  import { captureCoordinateSpace, toDevicePoint } from '../lib/coordSpace.js';
17
18
  import { executeSeed } from './seedExec.js';
18
- import { resolveVars } from './schema.js';
19
+ import { withinRootOrNull } from './paths.js';
20
+ import { lookupFlowVar, missingVarMessage, resolveVars, SECRET_VAR_NAME, } from './schema.js';
21
+ // Shared with qa_act: flow replay keeps the default inset 0 so recorded flows
22
+ // reproduce their historical vectors byte-for-byte.
23
+ import { swipeVector } from '../lib/gestures.js';
19
24
  import { configuredOcrCommand, findOcrRegion, runOcr } from '../visual/ocr.js';
20
25
  import { makeRedactor } from '../lib/redact.js';
21
- const FALLBACK_SWIPE = {
22
- up: [540, 1500, 540, 600],
23
- down: [540, 600, 540, 1500],
24
- left: [800, 1100, 200, 1100],
25
- right: [200, 1100, 800, 1100],
26
- };
27
- const AREA_ANCHOR = { center: [0.5, 0.5], top: [0.5, 0.3], bottom: [0.5, 0.7], left: [0.3, 0.5], right: [0.7, 0.5] };
28
- const SECRET_VAR_NAME = /pass|secret|token|otp|pin|cvv|key/i;
29
- /** Device-relative swipe vector (fractions of the screen), falling back to fixed coords if no size. */
30
- function swipeVector(size, dir, area = 'center', distance = 0.6) {
31
- if (!size)
32
- return FALLBACK_SWIPE[dir];
33
- const [ax, ay] = AREA_ANCHOR[area];
34
- const cx = size.width * ax;
35
- const cy = size.height * ay;
36
- const vd = (size.height * distance) / 2;
37
- const hd = (size.width * distance) / 2;
38
- const clampX = (x) => Math.max(1, Math.min(size.width - 1, Math.round(x)));
39
- const clampY = (y) => Math.max(1, Math.min(size.height - 1, Math.round(y)));
40
- const v = {
41
- up: [cx, cy + vd, cx, cy - vd],
42
- down: [cx, cy - vd, cx, cy + vd],
43
- left: [cx + hd, cy, cx - hd, cy],
44
- right: [cx - hd, cy, cx + hd, cy],
45
- }[dir];
46
- return [clampX(v[0]), clampY(v[1]), clampX(v[2]), clampY(v[3])];
47
- }
26
+ import { classifyForeground, detectTreeOverlays } from '../snapshot/overlays.js';
48
27
  function describe(step) {
49
28
  switch (step.kind) {
50
- case 'tap': return `tap ${step.selector}`;
51
- case 'tapAt': return `tapAt ${step.x},${step.y}`;
52
- case 'tapImage': return `tapImage ${step.template}`;
53
- case 'tapOcrText': return `tapOcrText ${JSON.stringify(step.query)}`;
54
- case 'inputText': return `inputText${step.into ? ` into "${step.into}"` : ''} ${step.secret ? '«secret»' : JSON.stringify(step.value)}`;
55
- case 'assertVisible': return `assertVisible ${JSON.stringify(step.query)}`;
56
- case 'assertNotVisible': return `assertNotVisible ${JSON.stringify(step.query)}`;
57
- case 'assertImage': return `assertImage ${step.template}`;
58
- case 'assertOcrText': return `assertOcrText ${JSON.stringify(step.query)}`;
59
- case 'assertVisual': return `assertVisual ${JSON.stringify(step.description)}`;
60
- case 'assertDiff': return `assertDiff ${step.baseline}`;
61
- case 'swipe': return `swipe ${step.direction}${step.area ? `@${step.area}` : ''}`;
62
- case 'scrollTo': return `scrollTo ${JSON.stringify(step.query)}`;
63
- case 'press': return `press ${step.key}`;
64
- case 'openUrl': return `openUrl ${step.url}`;
65
- case 'wait': return step.ms != null ? `wait ${step.ms}ms` : `wait for ${JSON.stringify(step.query)}`;
66
- case 'waitForIdle': return 'waitForIdle';
67
- case 'waitForVisible': return `waitForVisible ${JSON.stringify(step.query)}`;
68
- case 'clearOverlay': return 'clearOverlay';
69
- case 'networkOffline': return 'networkOffline';
70
- case 'networkOnline': return 'networkOnline';
71
- case 'restartApp': return 'restartApp';
72
- case 'seed': return `seed ${step.fixture}`;
73
- case 'note': return `note(${step.outcome}) ${JSON.stringify(step.reason)}`;
74
- case 'screenshot': return 'screenshot';
75
- case 'prepareTarget': return 'prepareTarget';
29
+ case 'tap':
30
+ return `tap ${step.selector}`;
31
+ case 'tapAt':
32
+ return `tapAt ${step.x},${step.y}`;
33
+ case 'tapImage':
34
+ return `tapImage ${step.template}`;
35
+ case 'tapOcrText':
36
+ return `tapOcrText ${JSON.stringify(step.query)}`;
37
+ case 'inputText':
38
+ return `inputText${step.into ? ` into "${step.into}"` : ''} ${step.secret ? '«secret»' : JSON.stringify(step.value)}`;
39
+ case 'assertVisible':
40
+ return `assertVisible ${JSON.stringify(step.query)}`;
41
+ case 'assertNotVisible':
42
+ return `assertNotVisible ${JSON.stringify(step.query)}`;
43
+ case 'assertImage':
44
+ return `assertImage ${step.template}`;
45
+ case 'assertOcrText':
46
+ return `assertOcrText ${JSON.stringify(step.query)}`;
47
+ case 'assertVisual':
48
+ return `assertVisual ${JSON.stringify(step.description)}`;
49
+ case 'assertDiff':
50
+ return `assertDiff ${step.baseline}`;
51
+ case 'swipe':
52
+ return `swipe ${step.direction}${step.area ? `@${step.area}` : ''}`;
53
+ case 'scrollTo':
54
+ return `scrollTo ${JSON.stringify(step.query)}`;
55
+ case 'press':
56
+ return `press ${step.key}`;
57
+ case 'openUrl':
58
+ return `openUrl ${step.url}`;
59
+ case 'wait':
60
+ return step.ms != null ? `wait ${step.ms}ms` : `wait for ${JSON.stringify(step.query)}`;
61
+ case 'waitForIdle':
62
+ return 'waitForIdle';
63
+ case 'waitForVisible':
64
+ return `waitForVisible ${JSON.stringify(step.query)}`;
65
+ case 'clearOverlay':
66
+ return 'clearOverlay';
67
+ case 'networkOffline':
68
+ return 'networkOffline';
69
+ case 'networkOnline':
70
+ return 'networkOnline';
71
+ case 'restartApp':
72
+ return 'restartApp';
73
+ case 'seed':
74
+ return `seed ${step.fixture}`;
75
+ case 'note':
76
+ return `note(${step.outcome}) ${JSON.stringify(step.reason)}`;
77
+ case 'screenshot':
78
+ return 'screenshot';
79
+ case 'prepareTarget':
80
+ return 'prepareTarget';
76
81
  }
77
82
  }
78
83
  async function snapshot(session, d) {
@@ -95,14 +100,16 @@ function resourceIdSelector(selector) {
95
100
  }
96
101
  function provenanceForStep(flow, stepIndex, kind) {
97
102
  const oneBased = stepIndex + 1;
98
- return (flow.provenance ?? []).find((entry) => (entry.generatedStepIndex === oneBased &&
99
- (!entry.generatedKind || entry.generatedKind === kind || (kind === 'wait' && entry.generatedKind === 'waitForVisible'))));
103
+ return (flow.provenance ?? []).find((entry) => entry.generatedStepIndex === oneBased &&
104
+ (!entry.generatedKind || entry.generatedKind === kind || (kind === 'wait' && entry.generatedKind === 'waitForVisible')));
100
105
  }
101
106
  function targetHintsFromProvenance(provenance) {
102
107
  if (!provenance)
103
108
  return {};
104
109
  return {
105
- packageName: provenance.resourceId?.includes(':id/') ? provenance.resourceId.slice(0, provenance.resourceId.indexOf(':id/')) : undefined,
110
+ packageName: provenance.resourceId?.includes(':id/')
111
+ ? provenance.resourceId.slice(0, provenance.resourceId.indexOf(':id/'))
112
+ : undefined,
106
113
  className: provenance.className ?? provenance.elementRole,
107
114
  textHint: provenance.text ?? provenance.accessibilityLabel,
108
115
  boundsHint: provenance.boundsBucket,
@@ -178,17 +185,57 @@ async function resourceIdVisibility(session, d, id, provenance) {
178
185
  const matches = parsed.allNodes.filter((node) => resourceIdMatches(undefined, node.id, id));
179
186
  const screenMismatch = provenance?.originalScreenSignature && flowScreenSignature(parsed) !== provenance.originalScreenSignature;
180
187
  if (!matches.length)
181
- return { visible: false, detail: `resource-id "${id}" not visible${screenMismatch ? ' (recorded screen signature differs from current screen)' : ''}` };
188
+ return {
189
+ visible: false,
190
+ detail: `resource-id "${id}" not visible${screenMismatch ? ' (recorded screen signature differs from current screen)' : ''}`,
191
+ };
182
192
  const narrowed = matches.length > 1 ? narrowRawResourceMatches(matches, provenance) : matches;
183
193
  if (narrowed.length > 1) {
184
194
  const labels = narrowed.map((node, index) => `${index + 1}:${node.cls}:${node.text || node.desc || node.id || 'unlabeled'}`).join(', ');
185
- return { visible: true, ambiguous: true, detail: `AMBIGUOUS_SELECTOR: resource-id ${id} matched ${narrowed.length} visible nodes (${labels}); add text/class/bounds hints or a unique testID${screenMismatch ? ' (recorded screen signature differs from current screen)' : ''}.` };
195
+ return {
196
+ visible: true,
197
+ ambiguous: true,
198
+ detail: `AMBIGUOUS_SELECTOR: resource-id ${id} matched ${narrowed.length} visible nodes (${labels}); add text/class/bounds hints or a unique testID${screenMismatch ? ' (recorded screen signature differs from current screen)' : ''}.`,
199
+ };
186
200
  }
187
201
  const node = narrowed[0];
188
202
  return { visible: node.enabled !== false && node.bounds[2] > node.bounds[0] && node.bounds[3] > node.bounds[1] };
189
203
  }
204
+ /** In-tree modal surfaces that BACK dismisses (Android dialogs / bottom sheets / popups, iOS
205
+ * alerts and action sheets, system permission dialogs rendered in-tree). */
206
+ const MODAL_CLASS_RE = /BottomSheet|AlertDialog|DialogTitle|PopupWindow|XCUIElementTypeAlert|XCUIElementTypeSheet/;
207
+ const MODAL_ID_RE = /design_bottom_sheet|bottom_sheet|touch_outside|android:id\/(?:alertTitle|button1|button2|parentPanel|select_dialog_listview)|permissioncontroller:id\/|packageinstaller:id\/permission|:id\/(?:modal|dialog)[_a-z]*/i;
208
+ /** Classify what (if anything) the clearOverlay flow step may safely dismiss with BACK.
209
+ * Snackbars / banners are NOT BACK-dismissible (BACK would navigate the app instead). */
210
+ export function detectBackDismissibleOverlay(nodes, screen, appId, foregroundOwner) {
211
+ const fg = classifyForeground(appId, foregroundOwner);
212
+ if (fg && (fg.type === 'permission_dialog' || fg.type === 'account_picker'))
213
+ return fg.type;
214
+ const tree = detectTreeOverlays(nodes, screen).find((o) => o.type === 'native_dialog' || o.type === 'rn_logbox' || o.type === 'rn_redbox');
215
+ if (tree)
216
+ return tree.type;
217
+ const modal = nodes.find((n) => MODAL_CLASS_RE.test(n.cls) || MODAL_ID_RE.test(n.id));
218
+ if (modal)
219
+ return /permission/i.test(modal.id) ? 'permission_dialog' : /sheet/i.test(`${modal.cls} ${modal.id}`) ? 'bottom_sheet' : 'modal';
220
+ return undefined;
221
+ }
190
222
  export function classifyFlowDriverError(error, fallback = 'UNKNOWN') {
223
+ // Cancelled work (the call/job was aborted) is CANCELLED, never WDA_UNREACHABLE / SNAPSHOT_FAILED
224
+ // because the aborted request's message says "aborted" or "dump failed".
225
+ if (isAbortError(error))
226
+ return 'CANCELLED';
191
227
  const msg = String(error?.message ?? error);
228
+ // A crashing OCR/mask provider (tapOcrText/assertOcrText) is typed, not UNKNOWN.
229
+ if (error?.code === 'OCR_PROVIDER_FAILED')
230
+ return 'OCR_PROVIDER_FAILED';
231
+ // Driver-side argument validation (e.g. a malformed app id from assertAndroidAppId) carries
232
+ // code INVALID_ARGUMENT and/or an `INVALID_ARGUMENT:` message prefix. That's a caller error, not UNKNOWN.
233
+ if (error?.code === 'INVALID_ARGUMENT' || /^(?:Error:\s*)?INVALID_ARGUMENT\b/.test(msg))
234
+ return 'INVALID_ARGUMENT';
235
+ // Drivers self-classify undeliverable text (DirectDriver/WdaDriver prefix the code), so match
236
+ // first so the free-form detail after the prefix can't hit a broader pattern below.
237
+ if (/TEXT_INPUT_UNSUPPORTED/.test(msg))
238
+ return 'TEXT_INPUT_UNSUPPORTED';
192
239
  if (/not hittable|not hit.?point|not visible.*hittable|is not enabled|element.*obscured|other element.*would receive/i.test(msg))
193
240
  return 'ELEMENT_NOT_HITTABLE';
194
241
  if (/stale element|stale.*reference|element.*no longer|invalid element/i.test(msg))
@@ -231,22 +278,34 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
231
278
  const steps = [];
232
279
  const appId = flow.appId ?? session.appId;
233
280
  const timeoutMs = 8000;
234
- // Ensure session.driver is the driver we're running with — core helpers (resolveTarget) read it.
281
+ // Ensure session.driver is the driver we're running with. Core helpers (resolveTarget) read it.
235
282
  session.driver = d;
236
- const screenSize = await d.screenSize().catch(() => null);
283
+ // Re-read per gesture: a rotation mid-flow swaps the axes (the WDA driver caches per
284
+ // orientation, so this is cheap there). Keeps the first read as the fallback.
285
+ const initialScreenSize = await d.screenSize().catch(() => null);
286
+ const currentScreenSize = async () => (await d.screenSize().catch(() => null)) ?? initialScreenSize;
237
287
  const mutationConsent = opts.mutationConsent ?? { required: false, approved: true };
238
288
  const resolveFlowText = (value) => {
239
289
  const resolved = resolveVars(value, vars);
240
290
  for (const m of value.matchAll(/\$\{([^}]+)\}/g)) {
241
291
  const name = m[1];
242
- const v = vars[name] ?? process.env[name];
243
- if (v != null && SECRET_VAR_NAME.test(name))
292
+ const v = lookupFlowVar(name, vars);
293
+ if (v != null && v !== '' && SECRET_VAR_NAME.test(name))
244
294
  session.secrets.add(v);
245
295
  }
246
296
  return resolved;
247
297
  };
248
- const unresolved = (missing) => ({ ok: false, detail: `unresolved variable(s): ${missing.join(', ')}`, failureCode: 'MISSING_FIXTURE' });
298
+ const unresolved = (missing) => ({
299
+ ok: false,
300
+ detail: missingVarMessage(missing),
301
+ failureCode: 'MISSING_FIXTURE',
302
+ });
249
303
  const shown = (value) => makeRedactor(session.secrets)(value) ?? value;
304
+ const outsideRoot = (p) => ({
305
+ ok: false,
306
+ detail: `path "${p}" resolves outside the project root. Image templates and baselines must live under the project root`,
307
+ failureCode: 'UNSAFE_ACTION_REFUSED',
308
+ });
250
309
  const recordFlowMutation = (action, risk, target, status, detail) => {
251
310
  sessions.recordMutation(session, {
252
311
  tool: 'qa_flow_run',
@@ -263,8 +322,8 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
263
322
  let reason;
264
323
  let failureCode;
265
324
  const tapImageHit = async (template, minScore) => {
266
- const tplPath = isAbsolute(template) ? template : join(session.root, template);
267
- if (!existsSync(tplPath))
325
+ const tplPath = withinRootOrNull(session.root, template);
326
+ if (!tplPath || !existsSync(tplPath))
268
327
  return null;
269
328
  const png = await d.screenshot();
270
329
  const m = findTemplate(png, readFileSync(tplPath), minScore ?? 0.85);
@@ -315,7 +374,11 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
315
374
  return { ok: true };
316
375
  }
317
376
  if (native && native.using !== 'accessibility id' && native.using !== 'name') {
318
- return { ok: false, detail: `${native.using} selectors require backend-native selector support`, failureCode: 'BACKEND_UNSUPPORTED' };
377
+ return {
378
+ ok: false,
379
+ detail: `${native.using} selectors require backend-native selector support`,
380
+ failureCode: 'BACKEND_UNSUPPORTED',
381
+ };
319
382
  }
320
383
  const t = await resolveTarget(session, targetForTapSelector(selector, provenance));
321
384
  if ('error' in t)
@@ -331,6 +394,8 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
331
394
  await settle(d, { timeoutMs });
332
395
  return { ok: true };
333
396
  case 'tapImage': {
397
+ if (!withinRootOrNull(session.root, step.template))
398
+ return outsideRoot(step.template);
334
399
  const pt = await tapImageHit(step.template, step.minScore);
335
400
  if (!pt)
336
401
  return { ok: false, detail: `image "${step.template}" not found on screen`, failureCode: 'ELEMENT_NOT_FOUND' };
@@ -345,7 +410,11 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
345
410
  return unresolved(resolved.missing);
346
411
  const query = resolved.out;
347
412
  if (session.sensitive)
348
- return { ok: false, detail: 'Sensitive mode refuses OCR visual text search because it would pass a screenshot to an external provider', failureCode: 'UNSAFE_ACTION_REFUSED' };
413
+ return {
414
+ ok: false,
415
+ detail: 'Sensitive mode refuses OCR visual text search because it would pass a screenshot to an external provider',
416
+ failureCode: 'UNSAFE_ACTION_REFUSED',
417
+ };
349
418
  const command = configuredOcrCommand(session.root);
350
419
  if (!command)
351
420
  return { ok: false, detail: 'OCR command is not configured', failureCode: 'VISUAL_ONLY_SCREEN' };
@@ -360,9 +429,17 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
360
429
  case 'inputText': {
361
430
  const { out, missing } = resolveFlowText(step.value);
362
431
  if (missing.length)
363
- return { ok: false, detail: `unresolved variable(s): ${missing.join(', ')}`, failureCode: 'MISSING_FIXTURE' };
432
+ return unresolved(missing);
433
+ // Register a secret BEFORE typing so a failing driver call's error is scrubbed too (H2).
434
+ if (step.secret && out)
435
+ session.secrets.add(out);
436
+ // eslint-disable-next-line no-control-regex -- intentional: detect non-ASCII (outside \x00-\x7F) before adb text input
364
437
  if (d.kind === 'direct' && /[^\x00-\x7F]/.test(out)) {
365
- return { ok: false, detail: 'Android DirectDriver text entry is ASCII-safe only for generated replay values', failureCode: 'TEXT_INPUT_UNSUPPORTED' };
438
+ return {
439
+ ok: false,
440
+ detail: 'Android DirectDriver text entry is ASCII-safe only for generated replay values',
441
+ failureCode: 'TEXT_INPUT_UNSUPPORTED',
442
+ };
366
443
  }
367
444
  if (step.into) {
368
445
  const into = resolveFlowText(step.into);
@@ -384,12 +461,20 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
384
461
  return { ok: true };
385
462
  }
386
463
  if (native && native.using !== 'accessibility id' && native.using !== 'name') {
387
- return { ok: false, detail: `${native.using} selectors require backend-native selector support`, failureCode: 'BACKEND_UNSUPPORTED' };
464
+ return {
465
+ ok: false,
466
+ detail: `${native.using} selectors require backend-native selector support`,
467
+ failureCode: 'BACKEND_UNSUPPORTED',
468
+ };
388
469
  }
389
470
  // selector-bound: focus the named field first (no reliance on existing focus)
390
471
  const t = await resolveTarget(session, targetForTapSelector(into.out, provenance));
391
472
  if ('error' in t)
392
- return { ok: false, detail: `field "${shown(into.out)}": ${t.error}`, failureCode: classifyFlowDriverError(t.error, 'ELEMENT_NOT_FOUND') };
473
+ return {
474
+ ok: false,
475
+ detail: `field "${shown(into.out)}": ${t.error}`,
476
+ failureCode: classifyFlowDriverError(t.error, 'ELEMENT_NOT_FOUND'),
477
+ };
393
478
  await d.tapXY(t.x, t.y);
394
479
  await new Promise((r) => setTimeout(r, 600));
395
480
  await d.clearFocusedText(t.textLen);
@@ -418,7 +503,9 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
418
503
  const visibleById = await resourceIdVisibility(session, d, id, provenance);
419
504
  if (visibleById.ambiguous)
420
505
  return { ok: false, detail: visibleById.detail, failureCode: 'AMBIGUOUS_SELECTOR' };
421
- return visibleById.visible ? { ok: true } : { ok: false, detail: visibleById.detail ?? `"${shown(query)}" not visible`, failureCode: 'ASSERTION_FAILED' };
506
+ return visibleById.visible
507
+ ? { ok: true }
508
+ : { ok: false, detail: visibleById.detail ?? `"${shown(query)}" not visible`, failureCode: 'ASSERTION_FAILED' };
422
509
  }
423
510
  const ok = visible(await snapshot(session, d), fallbackVisibleQuery(query));
424
511
  return ok ? { ok } : { ok, detail: `"${shown(query)}" not visible`, failureCode: 'ASSERTION_FAILED' };
@@ -431,7 +518,9 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
431
518
  try {
432
519
  const native = await nativeVisible(d, query);
433
520
  if (native != null)
434
- return !native ? { ok: true } : { ok: false, detail: `"${shown(query)}" unexpectedly visible`, failureCode: 'ASSERTION_FAILED' };
521
+ return !native
522
+ ? { ok: true }
523
+ : { ok: false, detail: `"${shown(query)}" unexpectedly visible`, failureCode: 'ASSERTION_FAILED' };
435
524
  }
436
525
  catch (e) {
437
526
  return { ok: false, detail: String(e.message ?? e), failureCode: 'BACKEND_UNSUPPORTED' };
@@ -441,17 +530,23 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
441
530
  const visibleById = await resourceIdVisibility(session, d, id, provenance);
442
531
  if (visibleById.ambiguous)
443
532
  return { ok: false, detail: visibleById.detail, failureCode: 'AMBIGUOUS_SELECTOR' };
444
- return !visibleById.visible ? { ok: true } : { ok: false, detail: `"${shown(query)}" unexpectedly visible`, failureCode: 'ASSERTION_FAILED' };
533
+ return !visibleById.visible
534
+ ? { ok: true }
535
+ : { ok: false, detail: `"${shown(query)}" unexpectedly visible`, failureCode: 'ASSERTION_FAILED' };
445
536
  }
446
537
  const ok = !visible(await snapshot(session, d), fallbackVisibleQuery(query));
447
538
  return ok ? { ok } : { ok, detail: `"${shown(query)}" unexpectedly visible`, failureCode: 'ASSERTION_FAILED' };
448
539
  }
449
540
  case 'assertImage': {
450
- const tplPath = isAbsolute(step.template) ? step.template : join(session.root, step.template);
541
+ const tplPath = withinRootOrNull(session.root, step.template);
542
+ if (!tplPath)
543
+ return outsideRoot(step.template);
451
544
  if (!existsSync(tplPath))
452
545
  return { ok: false, detail: `template not found: ${tplPath}`, failureCode: 'ASSERTION_FAILED' };
453
546
  const m = findTemplate(await d.screenshot(), readFileSync(tplPath), step.minScore ?? 0.85);
454
- return m.found ? { ok: true } : { ok: false, detail: `image "${step.template}" not visible (best score ${m.score})`, failureCode: 'ASSERTION_FAILED' };
547
+ return m.found
548
+ ? { ok: true }
549
+ : { ok: false, detail: `image "${step.template}" not visible (best score ${m.score})`, failureCode: 'ASSERTION_FAILED' };
455
550
  }
456
551
  case 'assertOcrText': {
457
552
  const resolved = resolveFlowText(step.query);
@@ -459,7 +554,11 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
459
554
  return unresolved(resolved.missing);
460
555
  const query = resolved.out;
461
556
  if (session.sensitive)
462
- return { ok: false, detail: 'Sensitive mode refuses OCR visual text assertion because it would pass a screenshot to an external provider', failureCode: 'UNSAFE_ACTION_REFUSED' };
557
+ return {
558
+ ok: false,
559
+ detail: 'Sensitive mode refuses OCR visual text assertion because it would pass a screenshot to an external provider',
560
+ failureCode: 'UNSAFE_ACTION_REFUSED',
561
+ };
463
562
  const command = configuredOcrCommand(session.root);
464
563
  if (!command)
465
564
  return { ok: false, detail: 'OCR command is not configured', failureCode: 'VISUAL_ONLY_SCREEN' };
@@ -468,22 +567,51 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
468
567
  }
469
568
  case 'assertVisual': {
470
569
  // human-readable visual checkpoint: capture evidence + record a visual note; passes.
570
+ // Sensitive mode never persists screenshots, so record an honest skipped checkpoint instead.
571
+ if (session.sensitive) {
572
+ sessions.addNote(session, {
573
+ at: Date.now(),
574
+ workflow: `${flow.name}:visual`,
575
+ outcome: 'skipped',
576
+ reason: `${step.description}: not captured, sensitive mode disables screenshots`,
577
+ method: 'visual',
578
+ });
579
+ return { ok: true, detail: 'sensitive mode: visual checkpoint screenshot not captured' };
580
+ }
471
581
  const png = await d.screenshot();
472
582
  const uri = sessions.saveArtifact(session, 'screenshot', `flow-visual-${Date.now()}.png`, png, 'image/png', `visual checkpoint: ${step.description}`);
473
583
  sessions.bump(session, 'screenshots');
474
- sessions.addNote(session, { at: Date.now(), workflow: `${flow.name}:visual`, outcome: 'pass', reason: step.description, method: 'visual', verifiedVisually: true, artifactUris: [uri] });
584
+ sessions.addNote(session, {
585
+ at: Date.now(),
586
+ workflow: `${flow.name}:visual`,
587
+ outcome: 'pass',
588
+ reason: step.description,
589
+ method: 'visual',
590
+ verifiedVisually: true,
591
+ artifactUris: [uri],
592
+ });
475
593
  return { ok: true };
476
594
  }
477
595
  case 'assertDiff': {
478
- const basePath = join(session.root, '.swipium', 'baselines', `${step.baseline}.png`);
596
+ const basePath = withinRootOrNull(join(session.root, '.swipium', 'baselines'), `${step.baseline}.png`);
597
+ if (!basePath)
598
+ return outsideRoot(step.baseline);
479
599
  if (!existsSync(basePath))
480
600
  return { ok: false, detail: `no baseline "${step.baseline}" (create a visual baseline first)`, failureCode: 'MISSING_FIXTURE' };
481
601
  const res = imageDiff(readFileSync(basePath), await d.screenshot());
482
602
  const tol = step.threshold ?? 0.02;
483
- return res.comparable && res.ratio <= tol ? { ok: true } : { ok: false, detail: res.comparable ? `${(res.ratio * 100).toFixed(2)}% changed > ${(tol * 100).toFixed(1)}%` : `not comparable: ${res.reason}`, failureCode: 'ASSERTION_FAILED' };
603
+ return res.comparable && res.ratio <= tol
604
+ ? { ok: true }
605
+ : {
606
+ ok: false,
607
+ detail: res.comparable
608
+ ? `${(res.ratio * 100).toFixed(2)}% changed > ${(tol * 100).toFixed(1)}%`
609
+ : `not comparable: ${res.reason}`,
610
+ failureCode: 'ASSERTION_FAILED',
611
+ };
484
612
  }
485
613
  case 'swipe': {
486
- const v = swipeVector(screenSize, step.direction, step.area, step.distance);
614
+ const v = swipeVector(await currentScreenSize(), step.direction, step.area, step.distance);
487
615
  await d.swipe(v[0], v[1], v[2], v[3], 300);
488
616
  sessions.bump(session, 'actions');
489
617
  await settle(d, { timeoutMs });
@@ -494,6 +622,7 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
494
622
  if (resolved.missing.length)
495
623
  return unresolved(resolved.missing);
496
624
  const query = resolved.out;
625
+ const screenSize = await currentScreenSize();
497
626
  for (let n = 0; n < 8; n++) {
498
627
  try {
499
628
  const native = await nativeVisible(d, query);
@@ -534,7 +663,7 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
534
663
  case 'openUrl': {
535
664
  const { out, missing } = resolveFlowText(step.url);
536
665
  if (missing.length)
537
- return { ok: false, detail: `unresolved variable(s): ${missing.join(', ')}`, failureCode: 'MISSING_FIXTURE' };
666
+ return unresolved(missing);
538
667
  await d.openUrl(out);
539
668
  await settle(d, { timeoutMs });
540
669
  return { ok: true };
@@ -553,7 +682,7 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
553
682
  if (resolved.missing.length)
554
683
  return unresolved(resolved.missing);
555
684
  const query = resolved.out;
556
- const deadline = Date.now() + (step.kind === 'waitForVisible' ? step.timeoutMs ?? timeoutMs : timeoutMs);
685
+ const deadline = Date.now() + (step.kind === 'waitForVisible' ? (step.timeoutMs ?? timeoutMs) : timeoutMs);
557
686
  const waitStarted = Date.now();
558
687
  while (Date.now() < deadline) {
559
688
  try {
@@ -602,13 +731,62 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
602
731
  return { ok: true };
603
732
  }
604
733
  case 'clearOverlay': {
605
- // best-effort: hide the keyboard / dismiss a dialog with BACK (a flow cleanup, always ok)
606
- if (await d.imeShown().catch(() => false))
607
- await d.pressKey('back');
608
- else
609
- await d.pressKey('back');
734
+ // Best-effort cleanup (always ok), but BACK only when something is there to dismiss:
735
+ // with nothing open, BACK navigates away / exits the app (H7).
736
+ if (await d.imeShown().catch(() => false)) {
737
+ if (d.hideKeyboard)
738
+ await d.hideKeyboard().catch(() => false);
739
+ else
740
+ await d.pressKey('back');
741
+ await settle(d, { timeoutMs });
742
+ return { ok: true, detail: 'keyboard hidden' };
743
+ }
744
+ const parsed = await snapshot(session, d).catch(() => null);
745
+ const fg = await d.foregroundOwner().catch(() => 'unknown');
746
+ const dismissible = detectBackDismissibleOverlay(parsed?.allNodes ?? [], parsed?.screen, session.appId, fg);
747
+ if (!dismissible) {
748
+ return {
749
+ ok: true,
750
+ nothingCleared: true,
751
+ detail: 'nothing to clear (no keyboard, dialog, sheet or permission prompt detected), BACK not pressed',
752
+ };
753
+ }
754
+ // iOS has no BACK for alerts/sheets: WDA "back" taps the nav bar / edge-swipes, which
755
+ // navigates the app instead of dismissing the alert. Use the native alert API, and never
756
+ // claim success when it is unavailable or the overlay is still there.
757
+ if (d.kind === 'wda' || d.kind === 'simulator') {
758
+ if (!d.dismissAlert) {
759
+ return {
760
+ ok: true,
761
+ nothingCleared: false,
762
+ detail: `${dismissible} detected but NOT dismissed. This iOS backend has no native alert API (BACK is not used on iOS); dismiss it with an explicit tap step`,
763
+ };
764
+ }
765
+ try {
766
+ await d.dismissAlert();
767
+ }
768
+ catch (e) {
769
+ return {
770
+ ok: true,
771
+ nothingCleared: false,
772
+ detail: `${dismissible} detected but NOT dismissed: native alert dismiss failed: ${String(e?.message ?? e)}`,
773
+ };
774
+ }
775
+ await settle(d, { timeoutMs });
776
+ const after = await snapshot(session, d).catch(() => null);
777
+ const fgAfter = await d.foregroundOwner().catch(() => 'unknown');
778
+ const still = after ? detectBackDismissibleOverlay(after.allNodes, after.screen, session.appId, fgAfter) : undefined;
779
+ if (still)
780
+ return {
781
+ ok: true,
782
+ nothingCleared: false,
783
+ detail: `${dismissible} still present after native alert dismiss. Dismiss it with an explicit tap step`,
784
+ };
785
+ return { ok: true, detail: `dismissed ${dismissible} with the native alert API` };
786
+ }
787
+ await d.pressKey('back');
610
788
  await settle(d, { timeoutMs });
611
- return { ok: true };
789
+ return { ok: true, detail: `dismissed ${dismissible} with BACK` };
612
790
  }
613
791
  case 'networkOffline':
614
792
  case 'networkOnline': {
@@ -637,7 +815,15 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
637
815
  const r = await executeSeed(sessions, session, d, step.fixture, fixture.seed);
638
816
  const risk = fixture.seed.type === 'script' ? 'high' : fixture.seed.type === 'api' ? 'medium' : 'low';
639
817
  if (!r.ok) {
640
- sessions.addNote(session, { at: Date.now(), workflow: `seed:${step.fixture}`, outcome: 'blocked', category: 'missing_test_data', reason: `seed failed: ${r.detail}`, requiredState: fixture.requiredState, recommendedSetup: fixture.recommendedSetup });
818
+ sessions.addNote(session, {
819
+ at: Date.now(),
820
+ workflow: `seed:${step.fixture}`,
821
+ outcome: 'blocked',
822
+ category: 'missing_test_data',
823
+ reason: `seed failed: ${r.detail}`,
824
+ requiredState: fixture.requiredState,
825
+ recommendedSetup: fixture.recommendedSetup,
826
+ });
641
827
  recordFlowMutation('flow_seed_state', risk, { stepKind: step.kind, fixture: step.fixture, seedType: fixture.seed.type }, 'blocked', r.detail);
642
828
  return { ok: false, detail: `seed "${step.fixture}" failed: ${r.detail}`, failureCode: 'SEED_FAILED' };
643
829
  }
@@ -648,6 +834,8 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
648
834
  sessions.addNote(session, { at: Date.now(), workflow: `${flow.name}:note`, outcome: step.outcome, reason: step.reason });
649
835
  return { ok: true };
650
836
  case 'screenshot': {
837
+ if (session.sensitive)
838
+ return { ok: true, detail: 'sensitive mode: screenshot not captured' };
651
839
  const png = await d.screenshot();
652
840
  sessions.saveArtifact(session, 'screenshot', `flow-shot-${Date.now()}.png`, png, 'image/png', step.reason ?? 'flow screenshot');
653
841
  sessions.bump(session, 'screenshots');
@@ -670,7 +858,7 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
670
858
  reason,
671
859
  failureCode,
672
860
  steps,
673
- durationSec: Math.round(elapsedMs / 100) / 10,
861
+ durationMs: Math.round(elapsedMs),
674
862
  counters: session.counters,
675
863
  };
676
864
  };
@@ -682,7 +870,7 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
682
870
  const summary = describe(step);
683
871
  const budget = sessions.budgetStop(session);
684
872
  if (budget && phase !== 'teardown') {
685
- steps.push({ index, phase, kind: step.kind, summary, ok: false, durationSec: 0, detail: budget });
873
+ steps.push({ index, phase, kind: step.kind, summary, ok: false, durationMs: 0, detail: budget });
686
874
  passed = false;
687
875
  failedAtStep = index;
688
876
  reason = `budget: ${budget}`;
@@ -696,24 +884,45 @@ export async function runFlow(sessions, session, d, flow, opts = {}) {
696
884
  catch (e) {
697
885
  outcome = { ok: false, detail: String(e), failureCode: classifyFlowDriverError(e) };
698
886
  }
699
- const rec = { index, phase, kind: step.kind, summary, ok: outcome.ok, durationSec: Math.round((Date.now() - stepStarted) / 100) / 10, detail: outcome.detail, failureCode: outcome.failureCode };
887
+ // H2: step detail can carry driver errors (argv/stderr), so scrub session secrets, which
888
+ // include every value typed into a secret step/variable before it ran.
889
+ if (outcome.detail)
890
+ outcome = { ...outcome, detail: shown(outcome.detail) };
891
+ const rec = {
892
+ index,
893
+ phase,
894
+ kind: step.kind,
895
+ summary: shown(summary),
896
+ ok: outcome.ok,
897
+ durationMs: Math.round(Date.now() - stepStarted),
898
+ detail: outcome.detail,
899
+ failureCode: outcome.failureCode,
900
+ ...(outcome.nothingCleared !== undefined ? { nothingCleared: outcome.nothingCleared } : {}),
901
+ };
700
902
  steps.push(rec);
701
903
  if (!outcome.ok && failFast) {
702
904
  passed = false;
703
905
  failedAtStep = index;
704
906
  reason = outcome.detail ?? 'step failed';
705
907
  failureCode = outcome.failureCode;
908
+ // Sensitive mode never persists screenshots, including failure evidence.
909
+ if (session.sensitive) {
910
+ rec.detail = `${rec.detail ?? 'step failed'} (sensitive mode: failure screenshot not captured)`;
911
+ return index;
912
+ }
706
913
  try {
707
914
  const png = await d.screenshot();
708
915
  rec.screenshotUri = sessions.saveArtifact(session, 'screenshot', `flow-fail-${index}-${Date.now()}.png`, png, 'image/png', `flow ${flow.name} failed at step ${index}: ${summary}`);
709
916
  }
710
- catch { /* best-effort */ }
917
+ catch {
918
+ /* best-effort */
919
+ }
711
920
  return index;
712
921
  }
713
922
  }
714
923
  return -1;
715
924
  };
716
- // setup → main → teardown (teardown ALWAYS runs, even after a failure).
925
+ // setup > main > teardown (teardown ALWAYS runs, even after a failure).
717
926
  let nextBase = 0;
718
927
  const setupFail = await runPhase(flow.setup, 'setup', nextBase, true);
719
928
  nextBase += flow.setup.length;