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,823 +1,56 @@
1
- // qa_test_this (roadmap §3.1) — the "just test this" entry point. A DETERMINISTIC orchestration
1
+ // qa_test_this: the "just test this" entry point. A DETERMINISTIC orchestration
2
2
  // state machine so a first run does not depend on the agent's skill or token budget. It resolves
3
3
  // the project, finds (or plans a build for) an artifact, picks a target, and returns an ordered
4
- // plan with the EXACT next tool call to make — or a typed blocker / one concise NeedsInput
4
+ // plan with the EXACT next tool call to make, or a typed blocker / one concise NeedsInput
5
5
  // question. It performs the cheap, side-effect-free resolution itself; the heavy device steps
6
6
  // (build, boot/install, smoke) are dispatched to the existing one-shot tools via `nextAction`,
7
7
  // so an agent reaches real work in one or two calls instead of ten.
8
8
  //
9
9
  // Honesty (§2.2): the result state is exactly one of ready | needs_input | blocked | unsafe, and
10
10
  // every safe fallback Swipium chose is recorded in `workaroundsAttempted` (§11).
11
+ //
12
+ // This file is the MCP registration + schema only; the state machine lives in
13
+ // src/orchestration/testThis/ (plan resolution, execute gate, pipeline, terminal assembly).
11
14
  import { z } from 'zod';
12
- import { qaOk, qaError } from '../lib/result.js';
13
- import { qaFail } from '../oracle/failures.js';
14
- import { qaNeedsInput, NeedsInput } from '../lib/needsInput.js';
15
- import { scanProject } from '../context/scan.js';
16
- import { findMobileApps, classifyDiscovery } from '../context/findApps.js';
17
- import { resolveGoalFlags } from '../orchestration/goal.js';
18
- import { buildTerminalEnvelope, typedBlockerFromCode } from '../orchestration/envelope.js';
19
- import { resolveArtifact } from '../artifacts/resolve.js';
20
- import { buildPlan } from '../build/plan.js';
21
- import { adbDevices, listAvds, which, apkPackageId } from '../lib/android.js';
22
- import { simctlAvailable, listSimulators } from '../lib/simctl.js';
23
- import { checkWda } from '../lib/wda.js';
24
- import { loadWdaConfig } from '../lib/wdaConfig.js';
25
- import { planTarget } from '../core/targetPlan.js';
26
- import { requireConsent, consumeConsent } from '../consent/consent.js';
27
- import { executeBuild } from '../services/build.js';
28
- import { convertAabToApk, universalApkCachePath } from '../artifacts/bundletool.js';
29
- import { prepareAndroid } from '../services/prepareAndroid.js';
30
- import { prepareIos } from '../services/prepareIos.js';
31
- import { runSmoke } from '../services/smoke.js';
32
- import { runExplore } from '../explore/runner.js';
33
- import { runFirstRun, resolveFirstRunPolicy, observeScreen } from '../firstRun/firstRunRunner.js';
34
- import { planFirstRun } from '../firstRun/firstRunPlanner.js';
35
- import { generateSessionReport } from '../services/report.js';
36
- import { generateAndCompileSuite } from '../services/suiteGenerate.js';
37
- import { buildTestThisPreflight } from '../services/preflight.js';
38
- import { startProgress } from '../session/progress.js';
39
- import { ensurePrelaunchAppMap } from '../appMap/prelaunch.js';
40
- import { staticCandidatesForObservation } from '../appMap/screenMatch.js';
41
- import { DirectDriver } from '../drivers/DirectDriver.js';
42
- import { log } from '../lib/logger.js';
43
- import { existsSync, readFileSync } from 'node:fs';
44
- import { sep } from 'node:path';
45
- import { createHash } from 'node:crypto';
46
- // Runtime screen purposes that should trigger first-run autonomy even when static auth detection
47
- // missed them (SWIPIUM-REQ-02 Fix Group 5).
48
- const FIRST_RUN_TRIGGER_PURPOSES = new Set([
49
- 'login', 'create_account', 'login_or_create_account', 'otp_or_email_verification', 'onboarding', 'permissions_prompt', 'paywall',
50
- ]);
51
- const selToPlatform = {
52
- 'android-emulator': 'android',
53
- 'android-real': 'android',
54
- 'ios-simulator': 'ios',
55
- 'ios-real': 'ios',
56
- };
57
- function isAndroidEmulatorSerial(serial) {
58
- return /^emulator-\d+/.test(serial);
59
- }
15
+ import { handleTestThis } from '../orchestration/testThis/plan.js';
60
16
  export function registerTestThis(server, sessions) {
61
17
  server.registerTool('qa_test_this', {
62
18
  title: 'Test this app (autopilot)',
63
- description: 'One-shot QA orchestration for "test this". Resolves the project, finds the best installable artifact (or builds one), picks the best device/simulator, then — depending on mode — returns a plan or EXECUTES the whole path. mode: "plan" (default) returns an ordered plan + exact nextAction; "interactive" runs until the first question (login/consent) and returns it; "execute" runs build/convert→prepare→smoke→report→(suite) as a background JOB. IMPORTANT: the execute call returns immediately with state:"running" + a jobId — the TERMINAL state (completed/blocked/unsafe) and the reportUri are in the JOB RESULT, retrieved via qa_job_status (or pass waitForCompletion:true to block for short paths). Execute requests one combined consent for any privileged steps (boot, external-APK install, iOS .app install, build) BEFORE starting. Honest: typed blockers (NO_BUILD_ARTIFACT, NO_DEVICE, AAB_NEEDS_BUNDLETOOL, …), one NeedsInput at a time, a recorded workaroundsAttempted trail, and an auto-generated report in every terminal state. Creates a session if none is given.',
19
+ description: 'Autopilot for "test this app": finds or builds an artifact, picks a device/simulator, then plans ' +
20
+ '(mode:"plan", default, no side effects) or executes prepare > smoke > (explore) > report > (suite). execute returns ' +
21
+ 'state:"running" + jobId; the terminal state (completed/blocked/unsafe/needs_input) and reportUri are in the ' +
22
+ 'qa_job_status result. One combined consent covers boot/install/build. A report in every terminal state. iOS without WDA ' +
23
+ 'falls back to a visual-only smoke.',
64
24
  inputSchema: {
65
- sessionId: z.string().optional().describe('Reuse an existing session; otherwise one is created.'),
25
+ sessionId: z.string().optional().describe('Reuse a session; otherwise one is created.'),
66
26
  projectRoot: z.string().optional(),
67
- mode: z.enum(['plan', 'execute', 'interactive']).optional().describe('plan (default) | execute (run the full safe path as a job) | interactive (run until the first question).'),
68
- goal: z.enum(['smoke', 'explore', 'create_automation_suite', 'release_gate', 'test_login', 'reproduce_bug']).optional().describe('Autopilot intent (default smoke). Adjusts orchestration flags + required outputs only: smoke=fast launch; explore=guided exploration; create_automation_suite=explore+generate POM suite; release_gate=stricter report+readiness; test_login=drive login (stop for credentials if none); reproduce_bug=focused exploration from goalText. Explicit explore/generateSuite/stopOnNeedsInput flags override the goal default.'),
69
- goalText: z.string().optional().describe('For goal="reproduce_bug": a short description of the bug/flow to focus on.'),
70
- fastSmoke: z.boolean().optional().describe('Opt out of the default "leave behind automation" behavior: just launch + smoke, skip POM suite generation. Ignored when an explicit goal/generateSuite is given.'),
71
- platform: z.enum(['android', 'ios']).optional().describe('Force a platform; otherwise inferred from the artifact/devices.'),
27
+ mode: z
28
+ .enum(['plan', 'execute', 'interactive'])
29
+ .optional()
30
+ .describe('plan (default) | execute (background job) | interactive (run until the first question).'),
31
+ goal: z
32
+ .enum(['smoke', 'explore', 'create_automation_suite', 'release_gate', 'test_login', 'reproduce_bug'])
33
+ .optional()
34
+ .describe('Intent. Omitted: smoke, then tries a suite; "smoke" is fastest. Sets explore/generateSuite/stopOnNeedsInput; flags win.'),
35
+ goalText: z.string().optional().describe('reproduce_bug: the bug/flow to focus on.'),
36
+ fastSmoke: z.boolean().optional().describe('Just launch + smoke; skip suite generation (ignored with goal/generateSuite).'),
37
+ platform: z.enum(['android', 'ios']).optional().describe('Force a platform (default inferred).'),
72
38
  device: z.string().optional(),
73
- preferRealDevice: z.boolean().optional(),
39
+ preferRealDevice: z.boolean().optional().describe('Out of scope: returns PHYSICAL_DEVICE_UNSUPPORTED.'),
74
40
  allowOutsideRoot: z.boolean().optional(),
75
- buildIfNeeded: z.boolean().optional().describe('If no artifact exists but the project is buildable, build it (default true).'),
76
- generateSuite: z.boolean().optional().describe('In execute mode, also generate a POM suite from the run (default false).'),
77
- explore: z.boolean().optional().describe('In execute mode, run guided exploration (qa_explore) after the launch smoke to discover reachable workflows + build a screen graph (default false → fast smoke only).'),
78
- stopOnNeedsInput: z.boolean().optional().describe('In execute mode, stop and ask when login/test-data is needed instead of testing pre-login (default false → effective pre-login coverage).'),
79
- waitForCompletion: z.boolean().optional().describe('In execute mode, block until the job finishes (or timeoutMs) and return the terminal result instead of a running jobId. Use for short paths.'),
80
- timeoutMs: z.number().optional().describe('With waitForCompletion: max ms to wait (default 120000).'),
41
+ buildIfNeeded: z.boolean().optional().describe('Build from source when no artifact exists (default true).'),
42
+ generateSuite: z.boolean().optional().describe('execute: also generate a POM suite from the run.'),
43
+ explore: z.boolean().optional().describe('execute: run guided exploration after the smoke.'),
44
+ stopOnNeedsInput: z.boolean().optional().describe('execute: ask for login/test data instead of testing pre-login only.'),
45
+ waitForCompletion: z.boolean().optional().describe('execute: block until done (or timeoutMs) and return the terminal result.'),
46
+ timeoutMs: z.number().optional().describe('waitForCompletion cap (default 120000).'),
81
47
  consentId: z.string().optional(),
82
48
  approve: z.boolean().optional(),
49
+ responseMode: z
50
+ .enum(['compact', 'normal', 'verbose'])
51
+ .optional()
52
+ .describe('Text channel for this session: compact = summary + URIs (structuredContent stays full).'),
83
53
  },
84
- }, async ({ sessionId, projectRoot, mode, goal, goalText, fastSmoke, platform, device, preferRealDevice, allowOutsideRoot, buildIfNeeded, generateSuite, explore, stopOnNeedsInput, waitForCompletion, timeoutMs, consentId, approve }) => {
85
- // Goal routing (Milestone C): the goal sets orchestration FLAGS only; explicit booleans win.
86
- // Default policy = "leave behind automation when possible" (fastSmoke opts out).
87
- const goalFlags = resolveGoalFlags(goal, { explore, generateSuite, stopOnNeedsInput }, { fastSmoke });
88
- // ---- resolve / create session ----
89
- let session = sessionId ? sessions.get(sessionId) : undefined;
90
- if (sessionId && !session) {
91
- return qaError({ what: `Unknown sessionId "${sessionId}"`, changedState: false, retrySafe: true, nextSteps: ['Omit sessionId to create one, or pass a valid session.'] });
92
- }
93
- if (!session) {
94
- const { resolveProjectRoot } = await import('../context/projectRoot.js');
95
- const resolved = await resolveProjectRoot(server, projectRoot);
96
- if (!resolved.root)
97
- return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot="/abs/path".'], clientHint: resolved.hint });
98
- session = sessions.create(resolved.root, undefined, {});
99
- }
100
- let root = session.root;
101
- const wa = (note) => sessions.addWorkaround(session, note);
102
- // ---- 1. project scan ----
103
- let scan = await scanProject(root);
104
- // What Swipium has actually tried before any pre-execute question/blocker (review item 5):
105
- // concrete steps, not just the workaround log. Grows as resolution proceeds.
106
- const preAttempted = [`scanned project root (framework=${scan.framework})`];
107
- // ---- 1b. low-context app discovery (§3.1/§3.7): the root may be a monorepo or a parent
108
- // directory rather than the app itself. Discover candidates and either adopt the one
109
- // strong candidate (recording a workaround) or ask ONE disambiguation question. ----
110
- if (scan.framework === 'unknown') {
111
- const disc = findMobileApps(root);
112
- preAttempted.push(`ran low-context app discovery (searched ${disc.searchedLocations.length} location(s), found ${disc.candidates.length} candidate(s))`);
113
- const cls = classifyDiscovery(disc);
114
- if (cls.decision === 'adopt' && cls.chosen) {
115
- wa(`root is not itself a mobile app — discovered one candidate (${cls.chosen.framework}) at ${cls.chosen.rel}; proceeding with it`);
116
- root = cls.chosen.path;
117
- session.root = root; // adopt the app subdir as the effective project root
118
- sessions.persist(session);
119
- scan = await scanProject(root);
120
- }
121
- else if (cls.decision === 'disambiguate') {
122
- return qaNeedsInput(NeedsInput.monorepoTarget(cls.choices.map((c) => c.path)), {
123
- sessionId: session.id,
124
- state: 'needs_input',
125
- attempted: [...preAttempted, `identified ${cls.choices.length} plausible app targets — cannot pick safely`],
126
- candidates: cls.choices.slice(0, 6).map((c) => ({ path: c.path, rel: c.rel, framework: c.framework, score: c.score, reasons: c.reasons })),
127
- });
128
- }
129
- else {
130
- const empty = scan.readiness === 'blocked' && scan.missing.some((m) => /empty/i.test(m));
131
- return qaFail(empty ? 'PROJECT_ROOT_EMPTY' : 'NOT_MOBILE_PROJECT', {
132
- what: `No supported mobile project at ${root}`,
133
- nextSteps: [`Searched: ${disc.searchedLocations.slice(0, 8).join('; ') || '(root only)'}`, 'Pass projectRoot="/abs/path/to/app" pointing at the app directory.'],
134
- extra: { sessionId: session.id, scan, searchedLocations: disc.searchedLocations, state: 'blocked' },
135
- });
136
- }
137
- }
138
- else if (scan.monorepo) {
139
- // The root is itself an app but also a monorepo: surface other sibling apps for disambiguation
140
- // only when there are genuinely multiple recognized targets to choose between.
141
- const disc = findMobileApps(root);
142
- preAttempted.push(`detected a monorepo at the root; ran app discovery (${disc.candidates.length} candidate(s))`);
143
- const others = disc.candidates.filter((c) => c.path !== root);
144
- if (others.length >= 1) {
145
- return qaNeedsInput(NeedsInput.monorepoTarget([root, ...others.map((c) => c.path)]), {
146
- sessionId: session.id,
147
- state: 'needs_input',
148
- attempted: [...preAttempted, `the root app plus ${others.length} sibling app(s) are all plausible targets`],
149
- candidates: disc.candidates.slice(0, 6).map((c) => ({ path: c.path, rel: c.rel, framework: c.framework, score: c.score, reasons: c.reasons })),
150
- });
151
- }
152
- wa('monorepo detected — proceeding with the resolved root; pass projectRoot/target to disambiguate if the wrong app is chosen');
153
- }
154
- // ---- 1c. pre-launch app map (Vision Gap Fix 1): load or build the durable STATIC app map
155
- // BEFORE prepare/smoke so first-run decisions, exploration seeding, automation readiness,
156
- // and the report are informed by the map at the time they are made — and so the map still
157
- // exists when no device/artifact is available (every blocker below carries appMapUri). ----
158
- let prelaunchMap;
159
- let appMapUri;
160
- try {
161
- const at = new Date().toISOString();
162
- const pre = ensurePrelaunchAppMap(root, {
163
- at,
164
- appIdentityHints: {
165
- androidPackage: scan.framework === 'native-android' || scan.framework === 'expo' || scan.framework === 'bare-react-native' ? scan.appId : null,
166
- iosBundleId: scan.framework === 'native-ios' ? scan.appId : null,
167
- },
168
- });
169
- prelaunchMap = pre.map;
170
- appMapUri = pre.appMapUri;
171
- preAttempted.push(`${pre.rescanned ? 'built' : 'loaded'} static app map (${pre.reason}; ${pre.map.staticTopology.screens.length} static screen(s)) → ${pre.appMapUri}`);
172
- if (pre.rescanned)
173
- wa(`built static app map before launch (${pre.map.staticTopology.screens.length} screen(s)) — durable map drives first-run/exploration/report`);
174
- }
175
- catch (e) {
176
- log('warn', 'test_this pre-launch app map failed', { err: String(e) });
177
- }
178
- const buildPref = buildIfNeeded !== false;
179
- const steps = [];
180
- // ---- 2. artifact ----
181
- const art = await resolveArtifact({ projectRoot: root, platform: platform ?? 'any', allowOutsideRoot });
182
- let artifactPlatform = art.best?.platform;
183
- let needBuild = false;
184
- if (art.failureCode === 'ARTIFACT_OUTSIDE_ROOT_REQUIRES_APPROVAL') {
185
- const outside = art.candidates.find((c) => c.outsideRoot);
186
- return qaNeedsInput(NeedsInput.artifactOutsideRoot(outside?.path ?? '(outside root)'), {
187
- sessionId: session.id,
188
- state: 'needs_input',
189
- attempted: [...preAttempted, 'resolved candidate artifacts — the best one is outside the project root'],
190
- candidates: art.candidates.slice(0, 3),
191
- });
192
- }
193
- const isAab = art.best?.type === 'aab';
194
- if (art.best) {
195
- steps.push({ tool: '(resolved)', why: `Using ${art.best.type.toUpperCase()} ${art.best.path}`, status: 'satisfied' });
196
- if (isAab) {
197
- // Honest (P0.2): an .aab is not installable — it must be converted first.
198
- wa('only a .aab is present; qa_test_this will convert it to an installable universal APK with bundletool');
199
- steps.push({ tool: 'qa_test_this', why: 'Convert the .aab to an installable universal APK before install', args: { sessionId: session.id }, status: 'pending' });
200
- }
201
- }
202
- else {
203
- // No artifact. Build from source if possible (effective-before-exhaustive, §2.1).
204
- const targetPlatform = (platform ?? artifactPlatform ?? (scan.framework === 'native-ios' ? 'ios' : 'android'));
205
- if (buildPref) {
206
- const bp = await buildPlan({ projectRoot: root, platform: targetPlatform });
207
- if (!bp.failureCode && bp.build && bp.toolchainOk) {
208
- needBuild = true;
209
- artifactPlatform = targetPlatform;
210
- const expoAndroidLocalRun = scan.framework === 'expo' && targetPlatform === 'android';
211
- wa(expoAndroidLocalRun
212
- ? `no prebuilt APK - Expo Android local run selected (${bp.build.command}); first run can take several minutes, later JS/TS-only work should reuse Metro/dev-build caches`
213
- : `no prebuilt artifact - will build ${targetPlatform} from source (${bp.build.command})`);
214
- steps.push({
215
- tool: 'qa_test_this',
216
- why: expoAndroidLocalRun ? 'No reusable APK/dev build found; run Expo Android local build/install/Metro path' : `No artifact found; build ${targetPlatform} from source before install`,
217
- args: { sessionId: session.id },
218
- status: 'pending',
219
- produces: ['artifact.path', 'artifact.appId'],
220
- });
221
- }
222
- else {
223
- return qaFail(bp.failureCode ?? 'NO_BUILD_ARTIFACT', {
224
- what: `No artifact found under ${root}, and a build is not currently possible`,
225
- nextSteps: [...(bp.missingToolchain.length ? [`Install: ${bp.missingToolchain.join(', ')}`] : []), ...bp.notes, `Searched: ${art.searchedLocations.slice(0, 6).join('; ')}`, ...(appMapUri ? [`Static app map is still available: qa_app_map_read { projectRoot:"${root}" }`] : [])],
226
- extra: { sessionId: session.id, buildPlan: bp, searchedLocations: art.searchedLocations, appMapUri, state: 'blocked' },
227
- });
228
- }
229
- }
230
- else {
231
- return qaFail('NO_BUILD_ARTIFACT', { what: `No artifact found under ${root}`, nextSteps: ['Build the app with your normal build command, or pass an explicit artifact path.', `Searched: ${art.searchedLocations.slice(0, 6).join('; ')}`, ...(appMapUri ? [`Static app map is still available: qa_app_map_read { projectRoot:"${root}" }`] : [])], extra: { sessionId: session.id, searchedLocations: art.searchedLocations, appMapUri, state: 'blocked' } });
232
- }
233
- }
234
- // ---- 3. target ----
235
- // Public v1 is simulator-only. Real-device-only artifacts and explicit real-device requests
236
- // are blocked instead of silently routing to non-v1 behavior.
237
- const iosArtifactRealOnly = !!art.best && art.best.platform === 'ios' && Array.isArray(art.best.installableOn) &&
238
- art.best.installableOn.length > 0 && art.best.installableOn.every((t) => t === 'ios-real');
239
- if (preferRealDevice || iosArtifactRealOnly) {
240
- return qaFail('BACKEND_UNSUPPORTED', {
241
- what: 'Swipium 1.0.0 supports Android Emulator and iOS Simulator targets only.',
242
- nextSteps: ['Use an Android emulator build or an iOS Simulator .app for v1.', 'Real-device workflows are outside the public v1 scope.'],
243
- extra: { sessionId: session.id, appMapUri, state: 'blocked' },
244
- });
245
- }
246
- const [adbPresent, simPresent] = await Promise.all([which('adb'), simctlAvailable()]);
247
- const [onlineAll, avds, sims] = await Promise.all([
248
- adbPresent ? adbDevices() : Promise.resolve([]),
249
- adbPresent ? listAvds() : Promise.resolve([]),
250
- simPresent ? listSimulators() : Promise.resolve([]),
251
- ]);
252
- const online = onlineAll.filter(isAndroidEmulatorSerial);
253
- if (device && onlineAll.includes(device) && !isAndroidEmulatorSerial(device)) {
254
- return qaFail('BACKEND_UNSUPPORTED', {
255
- what: `Android target "${device}" appears to be a real device. Swipium 1.0.0 supports Android Emulator only.`,
256
- nextSteps: ['Start or create an Android Emulator, then retry without a real-device serial.'],
257
- extra: { sessionId: session.id, appMapUri, state: 'blocked' },
258
- });
259
- }
260
- const tInputs = {
261
- requestedPlatform: platform,
262
- requestedDevice: device,
263
- preferRealDevice: false,
264
- artifactPlatform,
265
- artifactInstallTargets: art.best?.installableOn,
266
- android: { online, avds },
267
- ios: {
268
- bootedSimulators: sims.filter((s) => s.state === 'Booted').map((s) => ({ udid: s.udid, name: s.name })),
269
- availableSimulators: sims.filter((s) => s.state !== 'Booted').map((s) => ({ udid: s.udid, name: s.name })),
270
- realDevices: [],
271
- },
272
- };
273
- const target = planTarget(tInputs);
274
- if (target.blocked) {
275
- return qaFail(target.blocked.failureCode, { what: target.blocked.detail, extra: { sessionId: session.id, targetPlan: target, workaroundsAttempted: session.workarounds, appMapUri, state: 'blocked' } });
276
- }
277
- if (target.willBoot)
278
- wa(`no online ${selToPlatform[target.selected]} target — will boot ${target.bootTarget}`);
279
- const isAndroid = selToPlatform[target.selected] === 'android';
280
- const isIosReal = false;
281
- const requiresStructuredIos = !isAndroid &&
282
- !isIosReal &&
283
- (goalFlags.explore || goalFlags.generateSuite || goalFlags.goal === 'test_login' || goalFlags.goal === 'create_automation_suite');
284
- if (requiresStructuredIos) {
285
- const wdaConfig = loadWdaConfig(root);
286
- const wda = await checkWda(wdaConfig.url, 1500);
287
- if (!wda.reachable || !wda.ready) {
288
- return qaFail('WDA_UNREACHABLE', {
289
- what: `iOS ${goalFlags.goal} requires a structured WebDriverAgent backend, but WDA is not ready at ${wdaConfig.url}`,
290
- nextSteps: [
291
- 'Run qa_wda { action:"doctor" } to inspect WDA setup.',
292
- 'Run qa_wda { action:"build" } and qa_wda { action:"start" }, or attach an external WDA URL.',
293
- ...(appMapUri ? [`Static app map is still available: qa_app_map_read { projectRoot:"${root}" }`] : []),
294
- ],
295
- extra: {
296
- sessionId: session.id,
297
- state: 'blocked',
298
- goal: goalFlags.goal,
299
- target,
300
- wda: { config: { url: wdaConfig.url, mode: wdaConfig.mode, derivedDataPath: wdaConfig.derivedDataPath }, status: wda },
301
- appMapUri,
302
- },
303
- });
304
- }
305
- }
306
- // ---- 4. auth (be effective: proceed pre-login; surface the question as optional) ----
307
- let optionalQuestion;
308
- if (scan.likelyAuth) {
309
- wa('app likely has a login — will test pre-login coverage; authenticated flows are blocked until credentials are provided');
310
- optionalQuestion = NeedsInput.credentials('Authenticated workflows need a test account.');
311
- }
312
- // ---- 5. assemble the ordered plan ----
313
- // Effective installable artifact path: a converted universal APK for an .aab, else the artifact.
314
- const effectiveApk = art.best ? (isAab ? universalApkCachePath(root, art.best.path) : art.best.path) : undefined;
315
- // Milestone E: when a build is pending, the artifact does not exist yet — use SYMBOLIC refs
316
- // (${artifact.path}/${artifact.appId}) and mark the dependency, never `undefined` args.
317
- const prepArgs = { sessionId: session.id };
318
- if (isAndroid) {
319
- prepArgs.apk = needBuild ? '${artifact.path}' : effectiveApk;
320
- if (target.device)
321
- prepArgs.device = target.device;
322
- if (target.willBoot && target.bootTarget)
323
- prepArgs.avd = target.bootTarget;
324
- }
325
- else {
326
- prepArgs.app = needBuild ? '${artifact.path}' : art.best?.path;
327
- prepArgs.bundleId = needBuild ? '${artifact.appId}' : art.best?.appId ?? undefined;
328
- prepArgs.simulator = target.device;
329
- prepArgs.attachWda = 'auto';
330
- if (target.device)
331
- prepArgs.device = target.device;
332
- }
333
- if (isAndroid && scan.metroNeed === 'likely')
334
- wa('debug RN/Expo build likely needs Metro/dev server serving before launch');
335
- steps.push({
336
- tool: isAndroid ? 'qa_prepare_target' : 'qa_prepare_ios_target',
337
- why: `Install + launch on ${target.selected}`,
338
- args: prepArgs,
339
- status: 'pending',
340
- ...(needBuild ? { requires: ['artifact.path', 'artifact.appId'] } : {}),
341
- });
342
- steps.push({ tool: 'qa_smoke', why: 'Run a guardrail/smoke pass and capture evidence', args: { sessionId: session.id, profile: scan.recommendedProfile }, status: 'pending' });
343
- steps.push({ tool: 'qa_report', why: 'Summarize what was tested/blocked with evidence', args: { sessionId: session.id }, status: 'pending' });
344
- steps.push({ tool: 'qa_suite_generate', why: 'Optional: turn this run into a durable POM suite', args: { sessionId: session.id }, status: 'pending' });
345
- const nextAction = steps.find((s) => s.status === 'pending') ?? null;
346
- const state = 'ready';
347
- // Choice explanations for the uniform envelope (Dev 2 owns the source data; Dev 1 the shape).
348
- const artifactChoice = art.best
349
- ? { path: art.best.path, why: `${art.best.type.toUpperCase()} selected${art.best.buildType ? ` (${art.best.buildType})` : ''}`, alternatives: art.candidates.filter((c) => c.path !== art.best.path).slice(0, 4).map((c) => c.path) }
350
- : needBuild
351
- ? { why: 'No prebuilt artifact — building from source', alternatives: [] }
352
- : null;
353
- const targetChoice = target.selected
354
- ? { target: `${target.selected}${target.device ? ` (${target.device})` : ''}`, why: target.reason, alternatives: [] }
355
- : null;
356
- // ---- EXECUTE / INTERACTIVE modes (P0.1): actually run the path as a job ----
357
- if (mode === 'execute' || mode === 'interactive') {
358
- return runExecuteMode(server, sessions, session, {
359
- mode, scan, art, target, isAndroid, isIosReal, isAab, needBuild, effectiveApk,
360
- platform, goal: goalFlags.goal, goalText, releaseGate: goalFlags.releaseGate, requiredOutputs: goalFlags.requiredOutputs,
361
- generateSuite: goalFlags.generateSuite, explore: goalFlags.explore, stopOnNeedsInput: goalFlags.stopOnNeedsInput,
362
- artifactChoice, targetChoice,
363
- waitForCompletion: !!waitForCompletion, timeoutMs,
364
- consentId, approve, optionalQuestion, workaroundLog: () => session.workarounds,
365
- prelaunchMap, appMapUri,
366
- });
367
- }
368
- const summary = `🚀 test-this plan for ${root} (framework=${scan.framework}, session ${session.id})\n` +
369
- `target: ${target.selected}${target.device ? ` (${target.device})` : ''} — ${target.reason}\n` +
370
- (art.best ? `artifact: ${art.best.type.toUpperCase()} ${art.best.path}` : `artifact: none — ${needBuild ? 'will build from source' : 'blocked'}`) + '\n' +
371
- `plan:\n${steps.map((s, i) => ` ${i + 1}. [${s.status === 'satisfied' ? '✓' : ' '}] ${s.tool} — ${s.why}`).join('\n')}\n` +
372
- (nextAction ? `→ next: call ${nextAction.tool} ${JSON.stringify(nextAction.args ?? {})}\n` : '') +
373
- (session.workarounds.length ? `workarounds: ${session.workarounds.length} (see workaroundsAttempted)\n` : '') +
374
- (optionalQuestion ? `note: ${optionalQuestion.question}` : '');
375
- return qaOk({
376
- sessionId: session.id,
377
- state,
378
- goal: goalFlags.goal,
379
- framework: scan.framework,
380
- appMapUri,
381
- appIdentity: prelaunchMap?.appIdentity,
382
- target,
383
- targetChoice,
384
- artifact: art.best ?? null,
385
- artifactChoice,
386
- needBuild,
387
- recommendedProfile: scan.recommendedProfile,
388
- plan: steps,
389
- nextAction,
390
- workaroundsAttempted: session.workarounds,
391
- optionalQuestion: optionalQuestion ?? null,
392
- searchedLocations: art.best ? undefined : art.searchedLocations,
393
- }, summary);
394
- });
395
- }
396
- /** Whether the secure store already has login credentials (so we don't re-ask). */
397
- function hasCredentials(session) {
398
- return session.inputs.some((i) => /EMAIL|PASSWORD/.test(i.varName));
399
- }
400
- /** Execute / interactive orchestration: handle questions + consent synchronously, then run the
401
- * build/convert → prepare → smoke → report pipeline as a job. */
402
- async function runExecuteMode(server, sessions, session, a) {
403
- // 1. Auth question — interactive (or stopOnNeedsInput / goal:test_login) asks; execute proceeds pre-login.
404
- if (a.scan.likelyAuth && !hasCredentials(session) && (a.mode === 'interactive' || a.stopOnNeedsInput)) {
405
- const attempted = [
406
- `scanned project (framework=${a.scan.framework}; detected likely auth: ${a.scan.authSignals.slice(0, 3).join(', ') || 'login UI'})`,
407
- `resolved artifact + target (${a.target.selected ?? 'unknown'})`,
408
- ...session.workarounds,
409
- ];
410
- return qaNeedsInput(NeedsInput.credentials('Authenticated workflows need a test account.'), { sessionId: session.id, state: 'needs_input', attempted, appMapUri: a.appMapUri, resumeWith: 'qa_continue_from_blocker' });
411
- }
412
- // 2. Unified execution preflight (Milestone A): execute mode must request the SAME consent the
413
- // lower-level tools would for boot / external-APK / iOS .app install / build-from-source.
414
- const buildPlatform = (a.platform ?? (a.scan.framework === 'native-ios' ? 'ios' : 'android'));
415
- // Resolve the EXACT build command so the consent shows what will run (not just "build_from_source").
416
- let buildCommand;
417
- if (a.needBuild) {
418
- const bp = await buildPlan({ projectRoot: session.root, platform: buildPlatform });
419
- buildCommand = bp.build?.command;
420
- }
421
- // Hash an external (outside-root) APK so the consent shows what is being installed.
422
- let externalApk;
423
- if (a.isAndroid && !a.needBuild && a.effectiveApk && existsSync(a.effectiveApk) && !a.effectiveApk.startsWith(session.root + sep)) {
424
- try {
425
- externalApk = { path: a.effectiveApk, sha256: createHash('sha256').update(readFileSync(a.effectiveApk)).digest('hex') };
426
- }
427
- catch {
428
- /* unreadable — treated as in-root install */
429
- }
430
- }
431
- const iosApp = !a.isAndroid && !a.isIosReal ? a.art.best?.path : undefined;
432
- const preflight = buildTestThisPreflight({
433
- isAndroid: a.isAndroid,
434
- needBuild: a.needBuild,
435
- buildPlatform,
436
- buildCommand,
437
- willBoot: a.target.willBoot,
438
- bootTarget: a.target.bootTarget,
439
- isAab: a.isAab,
440
- apkPath: a.isAndroid ? a.effectiveApk : undefined,
441
- externalApk,
442
- iosApp,
443
- iosAppOutsideRoot: iosApp ? !iosApp.startsWith(session.root + sep) : undefined,
444
- iosReal: a.isIosReal,
445
- iosRealUdid: a.isIosReal ? a.target.device : undefined,
446
- iosRealApp: a.isIosReal ? a.art.best?.path : undefined,
447
- });
448
- let mutationConsent;
449
- let testThisPlanMutation;
450
- if (preflight.consentRequired) {
451
- const gate = consumeConsent(a.consentId, a.approve, { action: 'test_this_plan', affects: preflight.consentAffects });
452
- if (!gate.approved) {
453
- sessions.recordMutation(session, {
454
- tool: 'qa_test_this',
455
- action: 'test_this_plan',
456
- risk: preflight.risk,
457
- target: preflight.consentAffects,
458
- consent: { required: true, approved: false },
459
- status: 'requested',
460
- });
461
- const consentResult = requireConsent({
462
- action: 'test_this_plan',
463
- risk: preflight.risk,
464
- exactCommand: preflight.exactCommand,
465
- affects: preflight.consentAffects,
466
- explain: `Running "test this" needs these privileged steps (approved together so they don't re-prompt):\n${preflight.exactCommand}\nApprove to start; Swipium then installs, smokes, reports${a.generateSuite ? ', and generates a suite' : ''}.`,
467
- });
468
- // The static app map was already built pre-launch — keep its URI on the consent result (Fix 1).
469
- if (a.appMapUri && consentResult.structuredContent) {
470
- consentResult.structuredContent = { ...consentResult.structuredContent, appMapUri: a.appMapUri };
471
- }
472
- return consentResult;
473
- }
474
- mutationConsent = { required: true, consentId: a.consentId, approved: true };
475
- testThisPlanMutation = { affects: preflight.consentAffects, risk: preflight.risk };
476
- sessions.recordMutation(session, {
477
- tool: 'qa_test_this',
478
- action: 'test_this_plan',
479
- risk: preflight.risk,
480
- target: preflight.consentAffects,
481
- consent: mutationConsent,
482
- status: 'approved',
483
- });
484
- }
485
- const job = sessions.createJob(session, `test_this:${a.mode}`);
486
- const execArgs = { ...a, mutationConsent, testThisPlanMutation };
487
- const run = runExecutePipeline(sessions, session, job, execArgs);
488
- // Optional blocking mode for short paths (Milestone D). Default = return the running job.
489
- if (a.waitForCompletion) {
490
- const deadline = Date.now() + (a.timeoutMs ?? 120_000);
491
- await Promise.race([run, new Promise((r) => setTimeout(r, Math.max(0, deadline - Date.now())))]);
492
- const cur = session.jobs.get(job.jobId);
493
- if (cur && cur.status !== 'running') {
494
- const res = (cur.result ?? {});
495
- return qaOk({ sessionId: session.id, mode: a.mode, jobId: job.jobId, appMapUri: a.appMapUri, ...res }, cur.resultText ?? `test-this ${a.mode} ${res.state ?? cur.status}.`);
496
- }
497
- // Timed out — leave the job running and tell the agent to poll.
498
- return qaOk({ sessionId: session.id, state: 'running', mode: a.mode, jobId: job.jobId, appMapUri: a.appMapUri, timedOutWaiting: true }, `⏳ test-this ${a.mode} still running after the wait window — poll qa_job_status { sessionId:"${session.id}", jobId:"${job.jobId}" }.`);
499
- }
500
- void run;
501
- return qaOk({ sessionId: session.id, state: 'running', mode: a.mode, jobId: job.jobId, kind: job.kind, appMapUri: a.appMapUri, target: a.target }, `🚀 test-this ${a.mode} started as job ${job.jobId} (${a.isAndroid ? 'Android' : 'iOS'} · ${a.target.selected}). Poll qa_job_status { sessionId:"${session.id}", jobId:"${job.jobId}" } for the terminal result (state: completed | blocked | unsafe).`);
502
- }
503
- async function runExecutePipeline(sessions, session, job, a) {
504
- const signal = sessions.abortSignal(session, job.jobId);
505
- const upd = (patch) => sessions.updateJobIfRunning(session, job, patch);
506
- const attempted = [];
507
- const artifacts = [];
508
- const firstRunPatches = []; // first-run classifications folded into the durable map (§D)
509
- // Suite outputs (set in the terminal section) are embedded in the report, not just the job result.
510
- let suiteForReport;
511
- // Every terminal state — completed OR blocked — generates a report artifact (Milestone B).
512
- const finish = async (state, failureCode, summary, extra = {}) => {
513
- if (a.testThisPlanMutation) {
514
- sessions.recordMutation(session, {
515
- tool: 'qa_test_this',
516
- action: 'test_this_plan',
517
- risk: a.testThisPlanMutation.risk,
518
- target: a.testThisPlanMutation.affects,
519
- consent: a.mutationConsent,
520
- status: state === 'completed' ? 'executed' : 'blocked',
521
- detail: failureCode,
522
- });
523
- }
524
- const reportProg = startProgress(sessions, session, job, 'reporting', { statusText: 'Generating the session report.' });
525
- let reportUri;
526
- let manifestUri;
527
- let appVerdict;
528
- let coverageVerdict;
529
- let toolVerdict;
530
- const suiteOpt = suiteForReport
531
- ? { generated: !suiteForReport.skipped, skippedReason: suiteForReport.skippedReason, name: suiteForReport.name, written: suiteForReport.written, compiledFlows: suiteForReport.compiledFlows, suiteRunnable: suiteForReport.suiteRunnable, readinessLabels: suiteForReport.readinessLabels }
532
- : undefined;
533
- try {
534
- const r = await generateSessionReport(sessions, session, {
535
- format: 'summary', includeCurrentDump: true, suite: suiteOpt,
536
- });
537
- reportUri = r.reportUri;
538
- manifestUri = r.manifestUri;
539
- if (!artifacts.includes(reportUri))
540
- artifacts.push(reportUri);
541
- appVerdict = r.report.appVerdict;
542
- coverageVerdict = r.report.coverageVerdict;
543
- toolVerdict = r.report.toolVerdict;
544
- }
545
- catch (e) {
546
- log('warn', 'test_this report generation failed', { err: String(e) });
547
- }
548
- reportProg.done('Report ready.');
549
- const native = session.findings.some((f) => f.layer === 'native' && f.severity === 'high') ? 'error' : 'OK';
550
- const app = session.findings.some((f) => f.layer === 'app' && f.severity === 'high') ? 'error' : session.findings.some((f) => f.layer === 'app' && f.severity === 'medium') ? 'degraded' : 'OK';
551
- // The report already exists — point the agent AT it (fetch/open), not back at qa_report.
552
- const nextRecommendedAction = state === 'completed'
553
- ? reportUri
554
- ? {
555
- tool: 'qa_get_artifact',
556
- args: { uri: reportUri },
557
- why: `Open the generated report (${reportUri})`,
558
- }
559
- : { tool: 'qa_report', args: { sessionId: session.id }, why: 'Generate the report' }
560
- : { tool: 'qa_explain_blocker', args: { failureCode: failureCode ?? 'UNKNOWN' }, why: 'Understand the blocker and how to fix it' };
561
- // Uniform terminal envelope (Milestone B): summarizable from structured output, no log parsing.
562
- const envelope = buildTerminalEnvelope({
563
- state,
564
- sessionId: session.id,
565
- jobId: job.jobId,
566
- summary,
567
- attempted,
568
- workaroundsAttempted: session.workarounds,
569
- artifactChoice: a.artifactChoice,
570
- targetChoice: a.targetChoice,
571
- verdicts: { ...(appVerdict ? { app: appVerdict } : {}), ...(coverageVerdict ? { coverage: coverageVerdict } : {}), ...(toolVerdict ? { tool: toolVerdict } : {}) },
572
- blockers: state === 'completed' || !failureCode ? [] : [typedBlockerFromCode(failureCode)],
573
- reportUri: reportUri ?? null,
574
- nextRecommendedAction,
575
- });
576
- upd({
577
- status: state === 'completed' ? 'done' : 'failed',
578
- progress: state,
579
- result: {
580
- ...envelope,
581
- // Pre-launch static app map URI is present in EVERY terminal state — including blocked (Fix 1).
582
- appMapUri: a.appMapUri,
583
- goal: a.goal, requiredOutputs: a.requiredOutputs, releaseGateRequested: a.releaseGate,
584
- failureCode, artifacts, manifestUri,
585
- health: { native, app },
586
- inputsProvided: session.inputs.map((i) => i.varName),
587
- notes: session.notes.length, findings: session.findings.length, ...extra,
588
- },
589
- resultText: summary + (reportUri ? `\nreport: ${reportUri}` : ''),
590
- endedAt: Date.now(),
591
- });
592
- };
593
- try {
594
- // ---- A. obtain an installable artifact ----
595
- let apkPath = a.isAndroid ? a.effectiveApk : undefined;
596
- let appPath = !a.isAndroid ? a.art.best?.path : undefined;
597
- let appId = a.art.best?.appId ?? a.scan.appId ?? undefined;
598
- if (a.needBuild) {
599
- const prog = startProgress(sessions, session, job, a.isAndroid ? 'building_android' : 'building_ios', { statusText: 'Building the app from source.', nextExpected: 'Resolve the produced artifact, then install.' });
600
- attempted.push('build from source');
601
- const targetPlatform = (a.platform ?? (a.scan.framework === 'native-ios' ? 'ios' : 'android'));
602
- const plan = await buildPlan({ projectRoot: session.root, platform: targetPlatform });
603
- const res = await executeBuild(sessions, session, plan, { signal, onProgress: (p) => prog.event(p, { statusText: `Building: ${p}` }) });
604
- if (res.logUri)
605
- artifacts.push(res.logUri);
606
- if (signal?.aborted)
607
- return;
608
- if (!res.ok) {
609
- const code = res.failureCode ?? 'BUILD_FAILED';
610
- return await finish('blocked', code, `Build failed (${code}). A build failure is not a test failure. Log: ${res.logUri ?? 'n/a'}`, { buildLog: res.logUri });
611
- }
612
- if (!res.artifact) {
613
- const code = res.failureCode ?? 'BUILD_ARTIFACT_UNRESOLVED_AFTER_SUCCESS';
614
- const searched = res.searchedLocations?.length ? ` Searched: ${res.searchedLocations.slice(0, 6).join(', ')}` : '';
615
- return await finish('blocked', code, `Build succeeded, but Swipium could not resolve the produced ${a.isAndroid ? 'APK' : 'simulator app'}. This is not an app build failure. Log: ${res.logUri ?? 'n/a'}.${searched}`, { buildLog: res.logUri });
616
- }
617
- prog.done('Build complete.');
618
- if (a.isAndroid)
619
- apkPath = res.artifact.path;
620
- else
621
- appPath = res.artifact.path;
622
- appId = res.artifact.appId ?? appId;
623
- }
624
- else if (a.isAab && a.art.best) {
625
- const prog = startProgress(sessions, session, job, 'converting_aab', { statusText: 'Converting .aab → universal APK (bundletool).' });
626
- attempted.push('aab → apk conversion');
627
- const conv = await convertAabToApk(a.art.best.path, session.root, { signal });
628
- if (signal?.aborted)
629
- return;
630
- if (!conv.ok) {
631
- return await finish('blocked', conv.failureCode ?? 'AAB_NEEDS_BUNDLETOOL', `❌ Could not convert the .aab (${conv.failureCode}): ${conv.error}. Install bundletool or build an APK directly.`);
632
- }
633
- prog.done('Universal APK ready.');
634
- apkPath = conv.apkPath;
635
- sessions.addWorkaround(session, 'converted .aab → universal .apk for install');
636
- }
637
- // ---- B. prepare (boot/install/launch) ----
638
- if (a.isAndroid) {
639
- if (!apkPath)
640
- return await finish('blocked', 'NO_BUILD_ARTIFACT', '❌ No installable APK to prepare.');
641
- if (!appId)
642
- appId = (await apkPackageId(apkPath)) ?? undefined;
643
- if (!appId)
644
- return await finish('blocked', 'BUNDLE_ID_NOT_FOUND', '❌ Could not determine the app id from the APK.');
645
- const rnDebug = a.scan.metroNeed === 'likely';
646
- const prog = startProgress(sessions, session, job, 'preparing_android', { statusText: `Installing + launching ${appId}.`, nextExpected: 'Run smoke.' });
647
- attempted.push('prepare android (boot/install/launch)');
648
- const driver = session.driver ?? new DirectDriver();
649
- driver.setSignal?.(signal);
650
- const res = await prepareAndroid(sessions, session, driver, {
651
- needBoot: a.target.willBoot, bootTarget: a.target.bootTarget, serial: a.target.device,
652
- resolvedAppId: appId, apkPath, rnDebug, allowLaunchWithoutMetro: false, mutationConsent: a.mutationConsent,
653
- }, { signal, onProgress: (p) => prog.event(p) });
654
- if (res.aborted)
655
- return;
656
- if (!res.ok) {
657
- if (res.failureCode === 'METRO_REQUIRED')
658
- prog.needsUser('Metro must be serving for this debug build.');
659
- return await finish('blocked', res.failureCode ?? 'APP_LAUNCH_FAILED', `❌ Prepare failed: ${res.error}`);
660
- }
661
- prog.done(res.resultText ?? 'App launched.');
662
- }
663
- else {
664
- const prog = startProgress(sessions, session, job, 'preparing_ios', { statusText: 'Booting simulator + installing/launching .app.', nextExpected: 'Run smoke.' });
665
- attempted.push('prepare ios (boot/install/launch)');
666
- const res = await prepareIos(sessions, session, { app: appPath, bundleId: appId, simulator: a.target.device, attachWda: 'auto', mutationConsent: a.mutationConsent }, { onProgress: (p) => prog.event(p) });
667
- if (!res.ok)
668
- return await finish('blocked', res.failureCode ?? 'APP_LAUNCH_FAILED', `❌ iOS prepare failed: ${res.error}`);
669
- prog.done(res.resultText ?? 'iOS app launched.');
670
- }
671
- if (signal?.aborted)
672
- return;
673
- // ---- C. smoke ----
674
- const smokeProg = startProgress(sessions, session, job, 'smoke', { statusText: 'Running the smoke pass + saved flows.' });
675
- attempted.push('smoke');
676
- const driver = session.driver;
677
- if (!driver)
678
- return await finish('blocked', 'NO_DEVICE', '❌ No driver bound after prepare.');
679
- const smoke = await runSmoke(sessions, session, driver, { variables: sessions.inputVariables(session) });
680
- for (const art of session.artifacts)
681
- if (!artifacts.includes(art.uri))
682
- artifacts.push(art.uri);
683
- smokeProg.done(`smoke done — flows ${smoke.flowsPassed}/${smoke.flowsTotal}.`);
684
- // ---- C1.5 first-run autonomy (SWIPIUM-REQ-02): when the app appears gated AND the environment
685
- // is a disposable test/staging one where generated accounts are policy-safe, progress
686
- // through auth/onboarding before exploring. Safe-by-default: in an unknown/production-like
687
- // environment the decision is "not allowed" and this block is skipped (pre-login coverage
688
- // stands, and the optional credentials question is still surfaced). ----
689
- if (smoke.baseline.launch?.outcome !== 'fail') {
690
- try {
691
- // Fix Group 5: trigger first-run from the ACTUAL first runtime screen, not only the static
692
- // auth signal. Classify the current screen read-only; if it is auth/onboarding/paywall/
693
- // permission/verification, route through first-run logic (which itself refuses generated
694
- // account creation in unknown/production-like environments and surfaces NeedsInput instead).
695
- const { policy, decision } = resolveFirstRunPolicy(session);
696
- let runtimePurpose;
697
- try {
698
- const obs = await observeScreen(sessions, session, driver);
699
- // Fix 2: corroborate the observed first screen with static app-map candidates.
700
- const staticCandidates = a.prelaunchMap ? staticCandidatesForObservation(a.prelaunchMap, { foreground: obs.foreground, visibleText: obs.visibleText }) : undefined;
701
- runtimePurpose = planFirstRun({ ...obs, screenSignature: obs.screenSignature, appError: obs.appError, staticCandidates }, session, { policy, decision }).classification.purpose;
702
- }
703
- catch {
704
- /* classification best-effort; fall back to the static signal */
705
- }
706
- const shouldFirstRun = a.scan.likelyAuth || (runtimePurpose != null && FIRST_RUN_TRIGGER_PURPOSES.has(runtimePurpose));
707
- if (shouldFirstRun) {
708
- const frProg = startProgress(sessions, session, job, 'first_run', { statusText: 'First-run autonomy: auth/onboarding with safe test data.' });
709
- attempted.push(`first-run autonomy (trigger: ${a.scan.likelyAuth ? 'static_auth' : `runtime_${runtimePurpose}`})`);
710
- const fr = await runFirstRun(sessions, session, driver, { mode: 'until_home', appMap: a.prelaunchMap }, { signal, onProgress: (p) => frProg.event(p) });
711
- for (const u of fr.evidenceUris)
712
- if (!artifacts.includes(u))
713
- artifacts.push(u);
714
- firstRunPatches.push(...fr.mapUpdates); // folded into the durable app map in §D
715
- sessions.addWorkaround(session, `first-run: ${fr.pathTaken} path → ${fr.accountOutcome} (${fr.environment.environment} env)`);
716
- frProg.done(`first-run ${fr.state} — account ${fr.accountOutcome}`);
717
- }
718
- }
719
- catch (e) {
720
- log('warn', 'test_this first-run autonomy failed', { jobId: job.jobId, err: String(e) });
721
- }
722
- }
723
- // ---- C2. optional guided exploration (§9.1) ----
724
- if (a.explore && smoke.baseline.launch?.outcome !== 'fail') {
725
- const exProg = startProgress(sessions, session, job, 'exploring', { statusText: 'Guided exploration of reachable screens.' });
726
- attempted.push('guided exploration');
727
- try {
728
- const ex = await runExplore(sessions, session, driver, { stopOnAuth: a.stopOnNeedsInput, goal: a.goalText }, { signal, onProgress: (p) => exProg.event(p) });
729
- const at = Date.now();
730
- const graphUri = sessions.saveArtifact(session, 'explore', `graph-${at}.json`, JSON.stringify(ex.graph.serialize(new Date(at).toISOString()), null, 2), 'application/json', 'exploration screen graph');
731
- const graphMdUri = sessions.saveArtifact(session, 'explore', `graph-${at}.md`, ex.graph.toMarkdown(new Date(at).toISOString()), 'text/markdown', 'exploration screen graph (readable)');
732
- sessions.setExploration(session, { at, graphUri, graphMdUri, state: ex.state, stoppedReason: ex.stoppedReason, summary: ex.summary });
733
- if (!artifacts.includes(graphUri))
734
- artifacts.push(graphUri);
735
- exProg.done(`explored ${ex.summary.screensVisited} screens`);
736
- }
737
- catch (e) {
738
- exProg.done(`exploration error: ${String(e)}`);
739
- }
740
- }
741
- // ---- D. optional suite generation (Milestone C — really writes + compiles, or honest skip) ----
742
- let suite;
743
- if (a.generateSuite) {
744
- const suiteProg = startProgress(sessions, session, job, 'suite_generating', { statusText: 'Generating + compiling a POM suite from the run.' });
745
- attempted.push('generate suite');
746
- suite = generateAndCompileSuite(sessions, session, { save: true, compile: true });
747
- if (suite.skipped) {
748
- suiteProg.done(`suite skipped — ${suite.skippedReason}`);
749
- }
750
- else {
751
- for (const w of suite.written) {
752
- const uri = `file://${w}`;
753
- if (!artifacts.includes(uri))
754
- artifacts.push(uri);
755
- }
756
- sessions.addWorkaround(session, `generated a POM suite (${suite.compiledFlows.filter((c) => c.ok).length} runnable flow(s))`);
757
- suiteProg.done(`suite ready — runnable=${suite.suiteRunnable}`);
758
- }
759
- suiteForReport = suite; // embed suite outputs in the report artifact (item 3)
760
- }
761
- // ---- D2. App Knowledge Map (SWIPIUM-REQ-01) — update the durable map after runtime execution.
762
- // Best-effort: a map failure must never turn a passing run into a blocked one.
763
- let appMap;
764
- try {
765
- const { buildAppMap, summarizeMap, quickCoverage } = await import('../appMap/build.js');
766
- const at = new Date().toISOString();
767
- const before = quickCoverage(session.root, at);
768
- // If guided exploration ran, merge its screen graph too (links runtime → static screens).
769
- let exploreGraph = null;
770
- if (session.exploration?.graphUri) {
771
- const found = sessions.findArtifact(session.exploration.graphUri);
772
- if (found) {
773
- try {
774
- exploreGraph = JSON.parse(readFileSync(found.rec.path, 'utf8'));
775
- }
776
- catch {
777
- /* ignore unreadable graph */
778
- }
779
- }
780
- }
781
- // Static topology was already built/refreshed pre-launch (Fix 1) — merge runtime on top. With no
782
- // existing map (pre-launch failed) runtime_merge still rescans static (doRescan when !existed).
783
- const built = buildAppMap(session.root, { mode: 'runtime_merge', at, sessionId: session.id, exploreGraph, firstRunPatches, persist: true });
784
- const after = built.map.coverage;
785
- appMap = {
786
- appMapUri: built.save?.resourceUri,
787
- appMapSummary: summarizeMap(built.map),
788
- mapCoverageDelta: {
789
- coveragePercent: after.overallPercent - before.overallPercent,
790
- runtimeScreens: after.runtimeScreens - before.runtimeScreens,
791
- staticScreens: after.staticScreens - before.staticScreens,
792
- },
793
- };
794
- if (built.save?.resourceUri && !artifacts.includes(built.save.resourceUri))
795
- artifacts.push(built.save.resourceUri);
796
- }
797
- catch (e) {
798
- log('warn', 'test_this app map update failed', { jobId: job.jobId, err: String(e) });
799
- }
800
- // ---- E. terminal state (report is generated inside finish) ----
801
- const launchOutcome = smoke.baseline.launch?.outcome;
802
- const hardFail = session.findings.some((f) => f.severity === 'high') || launchOutcome === 'fail';
803
- const suiteResult = suite
804
- ? { generated: !suite.skipped, skippedReason: suite.skippedReason, written: suite.written, compiledFlows: suite.compiledFlows, suiteRunnable: suite.suiteRunnable, readinessLabels: suite.readinessLabels, manifestPath: suite.manifestPath ?? null }
805
- : undefined;
806
- const summary = `${hardFail ? '⚠️' : '✅'} test-this ${a.mode} ${hardFail ? 'finished with findings' : 'completed'} on ${a.target.selected}.\n` +
807
- `smoke: launch=${launchOutcome ?? 'unknown'}, flows ${smoke.flowsPassed}/${smoke.flowsTotal}; findings=${session.findings.length}.` +
808
- (suite ? `\nsuite: ${suite.skipped ? `skipped (${suite.skippedReason})` : `${suite.compiledFlows.filter((c) => c.ok).length}/${suite.compiledFlows.length} runnable flow(s)`}` : '');
809
- await finish('completed', undefined, summary + (appMap?.appMapUri ? `\nappMap: ${appMap.appMapUri}` : ''), {
810
- smoke: { launch: launchOutcome ?? 'unknown', flowsPassed: smoke.flowsPassed, flowsTotal: smoke.flowsTotal },
811
- highFindings: session.findings.filter((f) => f.severity === 'high').length,
812
- ...(suiteResult ? { suite: suiteResult } : {}),
813
- ...(appMap ? { appMapUri: appMap.appMapUri, appMapSummary: appMap.appMapSummary, mapCoverageDelta: appMap.mapCoverageDelta } : {}),
814
- });
815
- }
816
- catch (e) {
817
- if (signal?.aborted)
818
- return;
819
- log('error', 'test_this execute pipeline failed', { jobId: job.jobId, err: String(e) });
820
- await finish('blocked', 'UNKNOWN', `❌ Execution error: ${String(e)}`);
821
- }
54
+ }, async (input) => handleTestThis(server, sessions, input));
822
55
  }
823
56
  //# sourceMappingURL=testThis.js.map