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