mobster-cli 0.4.3__py3-none-any.whl

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 (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,976 @@
1
+ """One `mobster verify` run: the verdict comes from the assertions, never a model; mobile_agent.sim loads lazily."""
2
+
3
+ import json
4
+ import re
5
+ import threading
6
+ import time
7
+ import traceback
8
+ from pathlib import Path
9
+
10
+ from .. import project_env
11
+ from ..transport import request_once
12
+ from ..wda import timeouts as wda_timeouts
13
+ from . import device_audit, snapshots, wire
14
+ from .a11y import NAME as A11Y
15
+ from .a11y import Collector as A11yCollector
16
+ from .actions import ActionError, ActionJournal, ActionNotDispatched
17
+ from .assertions import evaluate_on
18
+ from .business_run import BusinessRun
19
+ from .checks import project_root
20
+ from .flow import Flow
21
+ from .l10n import Collector as L10nCollector
22
+ from .replay_admission import ReplayAdmission
23
+ from .run_cleanup import RunCleanup
24
+ from .run_links import RunLinks
25
+ from .run_perf import PerfMeasure
26
+ from .run_secrets import ProjectSecrets
27
+ from .runs import make_run_dir
28
+ from .screen_coverage import ScreenCoverage
29
+ from .smart import is_real_device, no_smart_key
30
+ from .sound import Signal as SoundSignal
31
+ from .tree import GLANCE_SOURCE_PATH, TreeError
32
+ from .tree import read_tree as read_tree_from
33
+ from .verdict import CRASH_EXPLAINS, CouldntRun, Verdict, mobster_version
34
+
35
+ MODES = ("smart", "keyless", "launch")
36
+ FOREGROUND_WAIT_SECONDS = 15
37
+ BASELINE_SETTLE_SECONDS = 3
38
+ SETTLE_FIRST_PAUSE, SETTLE_PAUSE = .3, .5
39
+ FRAME_QUALITY = 75
40
+ CRASH_FIX = "The report's Logs and Crash pane shows the stack and the app's log around the crash."
41
+
42
+
43
+ def _now():
44
+ return time.monotonic()
45
+
46
+
47
+ def _pause(seconds):
48
+ time.sleep(seconds)
49
+
50
+
51
+ def wda_session(wda_url):
52
+ # Plain capabilities, the benchmark's way (bench/iosworld.wda_session): Smart's simulator numbers were measured so.
53
+ status = request_once(wda_url, "GET", "/status", timeout=10)
54
+ if status.get("sessionId"):
55
+ return status["sessionId"]
56
+ created = request_once(wda_url, "POST", "/session", {"capabilities": {"alwaysMatch": {}}}, timeout=30)
57
+ return created["sessionId"]
58
+
59
+
60
+ def front_app(wda_url, timeout=5):
61
+ # An app with a big tree can hold this read for 17-50 s, so it never waits past the probe bound.
62
+ timeout = min(timeout, wda_timeouts.WDA_CALL_TIMEOUTS["probe"])
63
+ value = request_once(wda_url, "GET", "/wda/activeAppInfo", timeout=timeout).get("value")
64
+ value = value if isinstance(value, dict) else {}
65
+ bundle, pid = value.get("bundleId"), value.get("pid")
66
+ return (bundle if isinstance(bundle, str) and bundle else None,
67
+ pid if isinstance(pid, int) and not isinstance(pid, bool) and pid > 0 else None)
68
+
69
+
70
+ def build_driver(target, manager, run_dir, mode):
71
+ own = getattr(manager, "build_driver", None) # a manager that is not the simulator's (Android) builds its own
72
+ if own is not None:
73
+ return own(target, run_dir, mode)
74
+ from ..compose import build_target_driver
75
+ from ..wda.frame_clock import attach_frame_clock
76
+ url = target.wda_url
77
+ session = wda_session(url)
78
+ driver = build_target_driver(wda_url=url, session=session)
79
+ try:
80
+ driver.restarter = manager.restarter(target)
81
+ except Exception:
82
+ driver.restarter = None # recovery then waits for the runner instead of restarting it
83
+ driver.new_session = lambda: wda_session(url)
84
+ if mode != "launch":
85
+ attach_frame_clock(driver, wda_url=url, session=session, mjpeg_url=target.mjpeg_url,
86
+ log_path=str(Path(run_dir) / "frameclock.jsonl"))
87
+ if mode != "smart":
88
+ driver.tune(rich=True, glide=True)
89
+ return driver
90
+
91
+
92
+ def _sim_error(error):
93
+ """A simulator manager error (duck-typed by ``kind``) as CouldntRun."""
94
+ kind = getattr(error, "kind", None)
95
+ if isinstance(kind, str):
96
+ return CouldntRun(kind, str(error), getattr(error, "fix", "") or "")
97
+ return None
98
+
99
+
100
+ class VerifyRun(ScreenCoverage, Flow, BusinessRun, ReplayAdmission, RunCleanup, RunLinks, ProjectSecrets,
101
+ PerfMeasure, Verdict):
102
+ """One run. run_id and run_dir exist from __init__, so the MCP server hands out the id before prepare() ends."""
103
+
104
+ def __init__(self, check, *, mode, runs_dir, manager=None, progress=None, key=None, assert_timeout=5,
105
+ replay="off", simctl=None, business_binding=None):
106
+ if mode not in MODES:
107
+ raise ValueError(f"mode must be one of {', '.join(MODES)}")
108
+ self.check, self.mode, self.manager, self.key = check, mode, manager, key
109
+ self.assert_timeout = assert_timeout
110
+ self._progress = progress
111
+ self.runs_dir = Path(runs_dir).expanduser().resolve()
112
+ self.run_id, self.run_dir = make_run_dir(self.runs_dir)
113
+ self.actions = ActionJournal(self.run_dir)
114
+ self.state = "new"
115
+ self.secrets = {} # {NAME: value} of the project's ${NAME}s this check uses (project_env.py)
116
+ self._live = check # the check with those expanded: only launches and Smart's request read it
117
+ self.steps = []
118
+ self.error = None
119
+ self.lock = threading.RLock()
120
+ self.driver = self.target = self.app = self.lease = None
121
+ self.frames = [] # every frame, relative to run_dir, in order
122
+ self.step_frames = [] # the step frames among them (the 60-frame cap applies to these)
123
+ self.timing = {}
124
+ self.baseline = {"taken": False, "held": []}
125
+ self.business_binding = business_binding
126
+ self.business_fixture = None
127
+ self.business_result = ({"adapter": check.business.adapter, "fixture": check.business.fixture,
128
+ "status": "unavailable", "schemaVersion": "mobster.business/1",
129
+ "assertions": [], "setup": "not_started", "cleanup": "not_needed"}
130
+ if check.business is not None else None)
131
+ self._business_baseline = None
132
+ self._business_cleanup_done = False
133
+ self.agent = None # Smart's {status, note, model, turns}
134
+ self.cost_usd = 0.0
135
+ self.result = None
136
+ self._t0 = time.monotonic()
137
+ self._t0_wall = time.time() # the same moment on the wall clock: log lines and crash reports carry it
138
+ self.evidence = None # the app's log and crash reports (evidence.py), from just before the launch
139
+ self._failed_ms = self._verdict_ms = None
140
+ self._ready_at = None
141
+ self._launch = {} # perf: interactiveMs and foregroundMs, from the launch call (perf.py)
142
+ self._sampler = None
143
+ self._app_pid = None
144
+ self.perf = None # perf.build, once the verdict read starts
145
+ self._frame_n = 0
146
+ self._frame_width = None
147
+ self._finish_lock = threading.Lock()
148
+ self._stop = threading.Event()
149
+ self._smart_status = None
150
+ self._smart_reason = ""
151
+ self._left_app = None # on a real device: the app Smart opened outside the check's (StayInApp)
152
+ self._model_failed = False
153
+ self._write_lock = threading.Lock()
154
+ self._driver_closed = False
155
+ self._preparing = False # prepare() is in flight on some thread
156
+ self._release_lock = threading.Lock() # a leaf lock: nothing is taken while it is held
157
+ self._awaiting_frame = [] # Smart steps whose after-frame hasn't been taken yet
158
+ self._held_somewhere = set()
159
+ self._unframed_findings = {}
160
+ self._screen_coverage_lock = threading.RLock()
161
+ self._screen_frames_pending = 0
162
+ self._screen_frames_sealed = False
163
+ self._screen_result_sealed = False
164
+ self._screen_coverage_final = None
165
+ self._verdict_thread = None
166
+ self._screen_verdict_audited = False
167
+ self._screen_coverage_issue = None
168
+ self.source_observation = None
169
+ level = "strict" if A11Y in check.audit else check.a11y
170
+ self._a11y = A11yCollector((check.conditions or {}).get("content_size"), level=level,
171
+ baseline=check.a11y_baseline, scope=check.a11y_scope) if level else None
172
+ self._ax_device = device_audit.for_run(self, check.audit)
173
+ self._sound = SoundSignal(check.sound, self) if check.sound else None
174
+ locale = (check.conditions or {}).get("locale")
175
+ self._l10n = L10nCollector(locale) if check.l10n and locale else None
176
+ self._simctl = simctl # conditions.simctl, or a fake in tests
177
+ self._wire = None # wire.Judge when the check has request, no_request or storage expectations
178
+ self._conditions = None # conditions.Applier, once the device is held and the app is in place
179
+ self.dialogs = None # dialogs.Policy, once the device is held: how this run answers iOS's own dialogs
180
+ self.snapshot = None # snapshot_run.take's result: the screen against its baseline
181
+ self._status_bar_set = False # the status bar is overridden (9:41, full bars), so a snapshot needn't mask it
182
+ self._conditions_restored = False
183
+ self.conditions_cleanup = "not_needed"
184
+ self.findings = [] # audit findings and screens that broke `throughout` (see _findings)
185
+ self._replay_drift = None # a replay that stopped at a changed step: the failure's sentence
186
+ self.replay = None
187
+ if replay != "off" and mode == "smart":
188
+ from ..replay_core.checks import CheckReplay
189
+ self.replay = CheckReplay(self, replay)
190
+ self.log(f"Mobster {mobster_version()} verify, run {self.run_id}, mode {mode}"
191
+ + (f", replay {replay}" if self.replay is not None else ""))
192
+
193
+ def _scrub(self, text):
194
+ if self.secrets:
195
+ text = project_env.mask_text(text, self.secrets)
196
+ return text.replace(self.key, "[key]") if self.key and isinstance(text, str) else text
197
+
198
+ def log(self, line):
199
+ with self._write_lock:
200
+ try:
201
+ with open(self.run_dir / "run.log", "a", encoding="utf-8") as handle:
202
+ handle.write(f"{time.strftime('%H:%M:%S')} {self._scrub(line)}\n")
203
+ except OSError:
204
+ pass
205
+
206
+ def progress(self, line):
207
+ """A progress line: stderr (through the caller's ``progress``) and run.log."""
208
+ self.log(line)
209
+ if self._progress is not None:
210
+ try:
211
+ self._progress(self._scrub(line))
212
+ except Exception:
213
+ pass
214
+
215
+ def event(self, event):
216
+ """One line of events.jsonl: Smart's frontier events (no prompts, no images) or key-less actions."""
217
+ with self._write_lock:
218
+ try:
219
+ line = self._scrub(json.dumps(project_env.mask_deep(event, self.secrets), ensure_ascii=False,
220
+ default=str))
221
+ with open(self.run_dir / "events.jsonl", "a", encoding="utf-8") as handle:
222
+ handle.write(line + "\n")
223
+ except (OSError, TypeError, ValueError):
224
+ pass
225
+
226
+ def _sleep(self, seconds):
227
+ if seconds > 0:
228
+ _pause(seconds)
229
+
230
+ def _ms(self):
231
+ return round((time.monotonic() - self._t0) * 1000)
232
+
233
+ def _check_stop(self):
234
+ if self._stop.is_set():
235
+ raise CouldntRun("stopped", "The run was stopped")
236
+
237
+ def _fail(self, error):
238
+ self.error = {"klass": error.klass, "message": error.message, "fix": error.fix}
239
+ if self._failed_ms is None:
240
+ self._failed_ms = self._ms()
241
+ self.log(f"couldn't run ({error.klass}): {error.message}")
242
+ return error
243
+
244
+ def _manager(self):
245
+ if self.manager is None:
246
+ try:
247
+ from ..sim import SimulatorManager
248
+ except ImportError as error:
249
+ if not str(getattr(error, "name", "") or "").startswith(__package__.rsplit(".", 1)[0] + ".sim"):
250
+ raise
251
+ raise CouldntRun("environment", "This build of Mobster has no simulator manager.",
252
+ "Install the full Mobster CLI (docs/verify.md).") from None
253
+ self.manager = SimulatorManager(progress=self.progress)
254
+ return self.manager
255
+
256
+ def _call(self, what, *args, **kwargs):
257
+ """A manager call; its SimError becomes CouldntRun."""
258
+ try:
259
+ return what(*args, **kwargs)
260
+ except ActionNotDispatched:
261
+ raise
262
+ except CouldntRun:
263
+ raise
264
+ except Exception as error:
265
+ converted = _sim_error(error)
266
+ if converted is not None:
267
+ raise converted from None
268
+ raise
269
+
270
+ def prepare(self):
271
+ # abort() decides under _finish_lock who releases the simulator, so each stop check and the state change it
272
+ # guards are one step under that lock: an abort between them once left the lease held and the driver open.
273
+ with self._finish_lock:
274
+ if self._stop.is_set() or self.result is not None: # the abort that stopped it writes the result
275
+ raise CouldntRun("stopped", "The run was stopped")
276
+ self.state = "preparing"
277
+ self._preparing = True
278
+ try:
279
+ try:
280
+ self._prepare()
281
+ except CouldntRun as error:
282
+ error = self._cleanup_after_run() or error
283
+ if self.result is None: # an abort's result already carries its own reason
284
+ self._fail(error)
285
+ self._release()
286
+ raise error from None
287
+ except ActionError as action_error:
288
+ error = self._cleanup_after_run() or CouldntRun(action_error.klass, str(action_error))
289
+ if self.result is None:
290
+ self._fail(error)
291
+ self._release()
292
+ raise error from None
293
+ except Exception as unexpected:
294
+ self.log("".join(traceback.format_exception(type(unexpected), unexpected,
295
+ unexpected.__traceback__)))
296
+ failure = self._cleanup_after_run()
297
+ if failure is not None and self.result is None:
298
+ self._fail(failure)
299
+ self._release()
300
+ if failure is not None:
301
+ raise failure from None
302
+ raise
303
+ except BaseException:
304
+ self._cleanup_after_run()
305
+ self._release()
306
+ raise
307
+ with self._finish_lock:
308
+ if self._stop.is_set() or self.result is not None:
309
+ self._cleanup_after_run()
310
+ self._release()
311
+ stopped = CouldntRun("stopped", "The run was stopped")
312
+ raise self._fail(stopped) if self.result is None else stopped # an abort's result stands
313
+ self.state = "ready"
314
+ self._ready_at = time.monotonic()
315
+ finally:
316
+ self._preparing = False
317
+
318
+ def _prepare(self):
319
+ from . import conditions
320
+ if conditions.is_matrix(self.check.conditions): # before any simulator is touched
321
+ raise CouldntRun("usage", "This check lists several appearances, text sizes or locales, and one run has "
322
+ "one device state.", "Run it with `mobster test`, which runs each "
323
+ "combination, or write one value.")
324
+ self._apply_fixtures()
325
+ self._load_secrets()
326
+ check = self._live
327
+ self._load_business()
328
+ self._pin_assets()
329
+ manager = self._manager()
330
+ if is_real_device(manager):
331
+ self._refuse_fixtures_on_phone()
332
+ if self.business_binding is not None:
333
+ raise CouldntRun("business", "App-state fixtures require a managed simulator.")
334
+ started = time.monotonic()
335
+ try:
336
+ with self.actions.action("ACQUIRE"):
337
+ self.lease = self._call(manager.acquire, check.device, check.runtime,
338
+ cancelled=self._stop.is_set)
339
+ except ActionNotDispatched as rejected:
340
+ raise _sim_error(rejected.error) or rejected.error from None
341
+ self.target = self.lease.target
342
+ from .dialogs import for_run
343
+ self.dialogs = for_run(self.check, manager, self.target, actions=self.actions)
344
+ self.timing["simulator"] = round(time.monotonic() - started, 2)
345
+ self.log(f"simulator {self.target.name} {self.target.udid} {self.target.wda_url}")
346
+ self._check_stop()
347
+
348
+ started = time.monotonic()
349
+ if check.app_path:
350
+ self.app = self._call(manager.app_info, check.app_path)
351
+ if check.bundle_id and check.bundle_id != self.app.bundle_id:
352
+ raise CouldntRun("install", f"{check.app_path} is {self.app.bundle_id}, not {check.bundle_id}.",
353
+ "Pass the .app of the app the check names, or drop the bundle ID.")
354
+ else:
355
+ self.app = self._call(manager.installed_app, self.target, check.bundle_id)
356
+ bundle = self.app.bundle_id
357
+ if self.business_binding is not None and bundle != self.business_binding.bundle:
358
+ raise CouldntRun("business", "The installed app does not match the enrolled app-state adapter.",
359
+ "Enroll a fixture for the app's actual bundle ID.")
360
+ self._pin_replay_before_effects()
361
+ # The log starts while the app installs, so its process is up before the launch (and never competes with it).
362
+ self._start_evidence()
363
+ system = getattr(manager, "system_prefixes", ("com.apple.",)) # apps that ship with the OS keep their data
364
+ level = check.reset_level() if not bundle.startswith(system) else "none"
365
+ if check.app_path:
366
+ self.progress(f"Installing {self.app.name} {self.app.version}")
367
+ if level == "reinstall":
368
+ with self.actions.action("RESET"):
369
+ self._call(manager.reset, self.target, bundle, "reinstall", app_path=check.app_path)
370
+ else:
371
+ with self.actions.action("INSTALL"):
372
+ self.app = self._call(manager.install, self.target, check.app_path)
373
+ if level == "data":
374
+ with self.actions.action("RESET"):
375
+ self._call(manager.reset, self.target, bundle, "data")
376
+ elif level != "none":
377
+ with self.actions.action("RESET"):
378
+ self._call(manager.reset, self.target, bundle, level)
379
+ self.log(f"app {bundle} {self.app.version}, reset {level}")
380
+ self._seed(manager, bundle)
381
+ self._setup_business(manager)
382
+ if self.app.icon:
383
+ (self.run_dir / "app-icon.png").write_bytes(self.app.icon)
384
+ self.timing["install"] = round(time.monotonic() - started, 2)
385
+ self._check_stop()
386
+
387
+ launch_args = self._set_conditions(manager, bundle)
388
+ started, launched = time.monotonic(), _now()
389
+ stale = self._stale_process()
390
+ # The driver's session and settings are made while simctl launches the app (28 Sep: 0.3 s and 0.5-0.8 s).
391
+ built = {}
392
+
393
+ def build():
394
+ try:
395
+ with self.actions.action("NEW_SESSION"):
396
+ built["driver"] = build_driver(self.target, manager, self.run_dir, self.mode)
397
+ except BaseException as error: # handed to the preparing thread below
398
+ built["error"] = error
399
+ builder = threading.Thread(target=build, name="mobster-verify-driver", daemon=True)
400
+ builder.start()
401
+ try:
402
+ if self.business_fixture is not None:
403
+ launch_args = (*launch_args, "-MobsterBusinessRun", self.business_fixture.token)
404
+ with self.actions.action("LAUNCH"):
405
+ self._call(manager.launch, self.target, bundle, launch_args, self._launch_env())
406
+ finally:
407
+ builder.join()
408
+ self.driver = built.get("driver")
409
+ if self.driver is not None:
410
+ self.actions.guard_driver(self.driver)
411
+ if check.throughout or check.somewhere or self._a11y or self._l10n:
412
+ from .source_observation import SourceObservation
413
+ self.source_observation = SourceObservation(self)
414
+ self.source_observation.attach(self.driver)
415
+ error = built.get("error")
416
+ if isinstance(error, CouldntRun):
417
+ raise error
418
+ if error is not None:
419
+ raise CouldntRun("wda", f"WebDriverAgent didn't answer on the simulator ({type(error).__name__}).",
420
+ "Run `mobster sim doctor --fix`, then try again.") from None
421
+ in_front = self._await_foreground(stale)
422
+ self._measure_launch(launched, in_front)
423
+ self._send_push()
424
+ if check.open_url:
425
+ self._open_link(check.open_url)
426
+ self.timing["launch"] = round(time.monotonic() - started, 2)
427
+ from ..intents import harvest # the link atlas fills as the simulator is ready (dark; never raises)
428
+ harvest.on_device_ready(self.target.udid, self.log)
429
+ self._check_stop()
430
+ self.frame("launch")
431
+ if self._wire is not None:
432
+ self._wire.baseline()
433
+ if check.steps:
434
+ self._take_baseline()
435
+ if self._sound is not None:
436
+ self._sound.start()
437
+
438
+ def _apply_fixtures(self):
439
+ """Flatten the check's fixtures (.mobster/fixtures/) into it, or refuse before any simulator is touched."""
440
+ from . import fixtures
441
+ try:
442
+ self.check = self._live = fixtures.for_run(self.check, self.runs_dir)
443
+ except fixtures.FixtureError as error:
444
+ raise CouldntRun("usage", str(error), "`mobster fixtures list` shows the project's fixtures.") from None
445
+ if self.check.fixture:
446
+ self.log(f"fixtures: {', '.join(self.check.fixture)}")
447
+
448
+ def _refuse_fixtures_on_phone(self):
449
+ from . import fixtures
450
+ try:
451
+ fixtures.refuse_on_phone(self.check)
452
+ except fixtures.FixtureError as error:
453
+ raise CouldntRun("environment", str(error), fixtures.PHONE_FIX) from None
454
+
455
+ def _seed(self, manager, bundle):
456
+ """Put the fixtures' keychain reset, files and pasteboard on the simulator,
457
+ after the reset and before the launch."""
458
+ if not self.check.seed:
459
+ return
460
+ if getattr(manager, "platform", "ios") != "ios":
461
+ raise CouldntRun("environment", "Fixtures seed iOS simulators only.",
462
+ "Remove seed from the check, or run it on an iOS simulator with --sim.")
463
+ from . import conditions, fixtures
464
+ try:
465
+ did = fixtures.seed_simulator(self.check.seed, udid=self.target.udid, bundle=bundle,
466
+ run=self._simctl or conditions.simctl, actions=self.actions)
467
+ except fixtures.FixtureError as error:
468
+ raise CouldntRun("environment", str(error), "Fix the fixture, then run the check again.") from None
469
+ self.log(f"fixtures seeded: {', '.join(did) or 'nothing'}")
470
+
471
+ def _set_conditions(self, manager, bundle):
472
+ # A condition that can't be set is couldn't run: the check never runs in the wrong state.
473
+ from . import conditions
474
+ args = tuple(self._live.launch_args)
475
+ spec = dict(self.check.conditions or {})
476
+ own = getattr(manager, "conditions", None) # a manager that is not the simulator's sets its own
477
+ device = manager if getattr(manager, "real_device", False) else None
478
+ if self.check.snapshot and device is None and own is None and "status_bar" not in spec:
479
+ spec["status_bar"] = snapshots.SNAPSHOT_STATUS_BAR # the same bar in every baseline
480
+ self._status_bar_set = device is None and own is None and "status_bar" in spec
481
+ if not (spec or self.check.stubs or self.check.wire):
482
+ return args
483
+ if own is not None:
484
+ if self.check.stubs or self.check.wire: # the proxy, shim and container reads are an iOS simulator's
485
+ raise CouldntRun("environment", "Request stubs and wire assertions run on an iOS simulator only, so "
486
+ "this check can't run here.", "Run it on a simulator, or remove them from the check.")
487
+ self._conditions = own(spec, self.target, bundle, actions=self.actions)
488
+ else:
489
+ self._conditions = conditions.Applier(spec, udid=self.target.udid, bundle=bundle,
490
+ device=device, run=self._simctl or conditions.simctl,
491
+ actions=self.actions, stubs=self.check.stubs,
492
+ watch=wire.watch_hosts(self.check.wire))
493
+ self._wire = wire.Judge(self.check.wire, run=self._simctl or conditions.simctl, udid=self.target.udid,
494
+ bundle=bundle) if self.check.wire else None
495
+ self.conditions_cleanup = "pending"
496
+ try:
497
+ self._conditions.before_launch()
498
+ except conditions.ConditionError as error:
499
+ raise CouldntRun("environment", error.message, error.fix) from None
500
+ for item in self._conditions.skipped:
501
+ self.log(f"condition {item['key']} skipped: {item['why']}")
502
+ self.log(f"conditions set: {', '.join(self._conditions.applied) or 'none'}")
503
+ return args + self._conditions.launch_args()
504
+
505
+ def _launch_env(self):
506
+ # The shim's variables win over the check's own launch_env: a check can't turn its network conditions off.
507
+ return {**self._live.launch_env, **(self._conditions.launch_env() if self._conditions is not None else {})}
508
+
509
+ def _send_push(self):
510
+ if self._conditions is None:
511
+ return
512
+ from . import conditions
513
+ try:
514
+ self._conditions.after_launch()
515
+ except conditions.ConditionError as error:
516
+ raise CouldntRun("environment", error.message, error.fix) from None
517
+
518
+ def _stale_process(self):
519
+ """The app's process ID when it is WDA's foreground app now, before a launch replaces it; else None."""
520
+ bundle, pid = self._front()
521
+ return pid if bundle == self.app.bundle_id else None
522
+
523
+ def _await_foreground(self, stale=None):
524
+ # Just after a launch WDA can name the replaced process, or the app with no process (28 Sep, Settings reruns).
525
+ bundle = self.app.bundle_id
526
+ started = _now()
527
+ deadline = started + FOREGROUND_WAIT_SECONDS
528
+ while True:
529
+ remaining = deadline - _now()
530
+ front, pid = self._front(timeout=max(1.0, remaining))
531
+ if front == bundle and (stale is None or pid not in (None, stale)):
532
+ if stale is not None:
533
+ self.log(f"{bundle} in front in process {pid} (was {stale}) after "
534
+ f"{_now() - started:.2f} s")
535
+ return True
536
+ if _now() >= deadline:
537
+ self.log(f"{bundle} was not in front {FOREGROUND_WAIT_SECONDS} s after launch"
538
+ + (f" (still process {stale})" if front == bundle else ""))
539
+ return False
540
+ self._sleep(.2)
541
+
542
+ def _front(self, timeout=2):
543
+ """WDA's foreground app as (bundle ID, process ID); (None, None) when WDA doesn't answer."""
544
+ try:
545
+ own = getattr(self.manager, "front_app", None)
546
+ bundle, pid = own(self.target) if own is not None else front_app(self.target.wda_url, timeout=timeout)
547
+ except Exception:
548
+ return None, None
549
+ if self.app is not None and bundle == self.app.bundle_id:
550
+ self._app_pid = pid
551
+ if self.evidence is not None:
552
+ self.evidence.saw(pid)
553
+ return bundle, pid
554
+
555
+ def _start_evidence(self):
556
+ # Evidence never costs a run: a failure here is logged and the run goes on without it.
557
+ from . import evidence
558
+ try:
559
+ root = project_root(self.run_dir) if self.runs_dir.parent.name == ".mobster" else None
560
+ self.evidence = evidence.for_run(self.manager, self.target, self.app, self.run_dir,
561
+ t0_wall=self._t0_wall, project_root=root, log=self.log)
562
+ if self.evidence is not None:
563
+ self.evidence.start()
564
+ except Exception as error:
565
+ self.evidence = None
566
+ self.log(f"evidence: not started ({type(error).__name__}: {error})")
567
+
568
+ def _stop_evidence(self):
569
+ if self.evidence is not None:
570
+ try:
571
+ self.evidence.stop()
572
+ except Exception as error:
573
+ self.log(f"evidence: stop failed ({type(error).__name__})")
574
+
575
+ def _crash(self, *, suspect):
576
+ """The app's crash in this run (evidence.Evidence.crash), or None."""
577
+ if self.evidence is None:
578
+ return None
579
+ try:
580
+ return self.evidence.crash(suspect=suspect)
581
+ except Exception as error:
582
+ self.log(f"evidence: crash lookup failed ({type(error).__name__}: {error})")
583
+ return None
584
+
585
+ def _crashed(self, crash, decided=None):
586
+ """The verdict for a run whose app crashed: failed, class app_crashed, the crash in one sentence."""
587
+ from .crashes import headline
588
+ decided = decided or {"results": [], "tree": None, "stable": None, "frame": None, "alert": None}
589
+ return {**decided, "verdict": "failed", "klass": "app_crashed",
590
+ "message": headline(crash, self.app.name if self.app is not None else "The app"), "fix": CRASH_FIX}
591
+
592
+ def _foreground(self, timeout=5):
593
+ try:
594
+ return self.driver.active_app(timeout=timeout)
595
+ except Exception:
596
+ return None
597
+
598
+ def _take_baseline(self):
599
+ # The first glance that agrees with the one before it (at most 3 s), so a launch screen isn't the flow's start.
600
+ deadline = _now() + BASELINE_SETTLE_SECONDS
601
+ tree = self.read_tree(glance=True)
602
+ while _now() < deadline:
603
+ self._sleep(SETTLE_FIRST_PAUSE)
604
+ again = self.read_tree(glance=True)
605
+ same = again.layout() == tree.layout()
606
+ tree = again
607
+ if same:
608
+ break
609
+ tree = self.read_tree()
610
+ held = [result.ok for result in evaluate_on(self.check.expect, tree)]
611
+ self.baseline = {"taken": True, "held": held}
612
+ self._take_business_baseline()
613
+ self.log(f"baseline: {sum(held)} of {len(held)} expectations held before the flow")
614
+
615
+ def read_tree(self, glance=False):
616
+ """One evidence read of the app's tree; a glance (no ``visible``) never decides and falls back to one."""
617
+ if self.driver is None:
618
+ raise CouldntRun("wda", "The run has no WebDriverAgent connection yet")
619
+ try:
620
+ if glance:
621
+ try:
622
+ tree = read_tree_from(self.driver, pause=self._sleep, path=GLANCE_SOURCE_PATH)
623
+ tree.glance = True
624
+ return tree
625
+ except Exception:
626
+ pass # fail closed: the evidence read below decides, or fails the run
627
+ return read_tree_from(self.driver, pause=self._sleep)
628
+ except TreeError as error:
629
+ raise CouldntRun("wda", str(error)) from None
630
+ except CouldntRun:
631
+ raise
632
+ except Exception as error:
633
+ raise CouldntRun("wda", f"WebDriverAgent stopped answering ({type(error).__name__}).",
634
+ "Run `mobster sim doctor --fix`, then try again.") from None
635
+
636
+ def _frame_path(self, label):
637
+ self._frame_n += 1
638
+ label = re.sub(r"[^a-z0-9-]+", "-", str(label).lower()).strip("-") or "frame"
639
+ return self.run_dir / "frames" / f"{self._frame_n:02d}-{label}.jpg"
640
+
641
+ def _register_frame(self, path, label):
642
+ if self._late_screen_frame(label, path):
643
+ return None
644
+ relative = path.relative_to(self.run_dir).as_posix()
645
+ self.frames.append(relative)
646
+ if label == "step":
647
+ self.step_frames.append(relative)
648
+ if label in ("launch", "step"):
649
+ self._observe_screen(relative)
650
+ return path
651
+
652
+ def frame(self, label):
653
+ """A screenshot as frames/NN-label.jpg (JPEG, quality 75, half the simulator's pixel width)."""
654
+ with self._screen_frame(label) as admitted:
655
+ if not admitted:
656
+ return None
657
+ path = self._frame_path(label)
658
+ try:
659
+ self._call(self.manager.screenshot, self.target, path, max_width=self._frame_width,
660
+ quality=FRAME_QUALITY)
661
+ if self._late_screen_frame(label, path):
662
+ return None
663
+ if self._frame_width is None:
664
+ self._frame_width = _halve(path)
665
+ except CouldntRun as error:
666
+ self.log(f"no {label} frame: {error.message}")
667
+ if label in ("launch", "step") and not self._late_screen_frame(label, path):
668
+ self._observe_screen(None)
669
+ return None
670
+ except Exception as error:
671
+ self.log(f"no {label} frame: {type(error).__name__}")
672
+ if label in ("launch", "step") and not self._late_screen_frame(label, path):
673
+ self._observe_screen(None)
674
+ return None
675
+ return self._register_frame(path, label)
676
+
677
+ def frame_from_bytes(self, data, label):
678
+ """A frame from JPEG or PNG bytes (the image Smart saw), at the run's frame width."""
679
+ with self._screen_frame(label) as admitted:
680
+ if not admitted:
681
+ return None
682
+ path = self._frame_path(label)
683
+ try:
684
+ from io import BytesIO
685
+
686
+ from PIL import Image
687
+ image = Image.open(BytesIO(data)).convert("RGB")
688
+ if self._frame_width and image.width > self._frame_width:
689
+ image = image.resize((self._frame_width, round(image.height * self._frame_width / image.width)))
690
+ image.save(path, "JPEG", quality=FRAME_QUALITY)
691
+ if self._late_screen_frame(label, path):
692
+ return None
693
+ except Exception as error:
694
+ self.log(f"no {label} frame: {type(error).__name__}")
695
+ if label in ("launch", "step") and not self._late_screen_frame(label, path):
696
+ self._observe_screen(None)
697
+ return None
698
+ return self._register_frame(path, label)
699
+
700
+ def record_step(self, *, op, text, target=None, typed=None, changed=None, frame=None):
701
+ """One step (§5.3) for the result and `wait`'s steps_so_far; key-less actions also go to events.jsonl."""
702
+ text, typed = self._scrub(text), self._scrub(typed)
703
+ step = {"index": len(self.steps) + 1, "op": op, "text": text,
704
+ "target": ({"id": target.get("id"), "label": target.get("label"), "role": target.get("role")}
705
+ if target else None),
706
+ "typed": typed, "changed": changed, "frame": self._relative(frame), "at_ms": self._ms()}
707
+ step = project_env.mask_deep(step, self.secrets)
708
+ self.steps.append(step)
709
+ if self.mode != "smart":
710
+ self.event({"event": "step", **step})
711
+ self.log(f"step {step['index']}: {text}")
712
+ return step
713
+
714
+ def _relative(self, frame):
715
+ """A frame path as the result writes it: relative to run_dir."""
716
+ if not frame:
717
+ return None
718
+ path = Path(frame)
719
+ try:
720
+ return (path.relative_to(self.run_dir) if path.is_absolute() else path).as_posix()
721
+ except ValueError:
722
+ return str(path)
723
+
724
+ def relaunch(self):
725
+ """Terminate the app and launch it again with the same arguments and environment, keeping its data."""
726
+ with self.lock:
727
+ bundle = self.app.bundle_id
728
+ stale = self._stale_process()
729
+ with self.actions.action("TERMINATE"):
730
+ self._call(self.manager.terminate, self.target, bundle)
731
+ args = tuple(self._live.launch_args)
732
+ if self._conditions is not None:
733
+ args += self._conditions.launch_args()
734
+ if self.business_fixture is not None:
735
+ args += ("-MobsterBusinessRun", self.business_fixture.token)
736
+ with self.actions.action("LAUNCH"):
737
+ self._call(self.manager.launch, self.target, bundle, args, self._launch_env())
738
+ self._await_foreground(stale)
739
+
740
+ def open_url(self, url):
741
+ """A deep link into the running app."""
742
+ from .checks import URL_SCHEME
743
+ if not isinstance(url, str) or not URL_SCHEME.match(url) or len(url) > 2000:
744
+ from .checks import CheckError
745
+ raise CheckError("the link needs a scheme, such as yourapp://lesson")
746
+ with self.lock:
747
+ self._open_link(url)
748
+
749
+ def evaluate(self, assertions, *, timeout=5):
750
+ with self.lock:
751
+ results, _, _ = self._settle(assertions, timeout)
752
+ return results
753
+
754
+ def _settle(self, assertions, timeout):
755
+ timeout = max(0.0, float(timeout))
756
+ deadline = _now() + timeout
757
+ previous = self._timed_read(0, glance=True)
758
+ self._sleep(SETTLE_FIRST_PAUSE)
759
+ reads = 1
760
+ while True:
761
+ tree = self._timed_read(reads)
762
+ reads += 1
763
+ # A glance has no ``visible``, so the first pair compares layouts; evidence pairs compare exactly.
764
+ stable = (tree.layout() == previous.layout() if previous.glance
765
+ else tree.fingerprint() == previous.fingerprint())
766
+ results = evaluate_on(assertions, tree)
767
+ held = sum(result.ok for result in results)
768
+ self.log(f" {'same screen' if stable else 'screen changed'}, {held} of {len(results)} held")
769
+ if stable and held == len(results):
770
+ return results, tree, True
771
+ now = _now()
772
+ if now >= deadline:
773
+ return results, tree, stable
774
+ self._sleep(min(SETTLE_PAUSE, deadline - now))
775
+ previous = tree
776
+
777
+ def _timed_read(self, index, glance=False):
778
+ started = time.monotonic()
779
+ tree = self.read_tree(glance=glance)
780
+ self.log(f"read {index + 1}: {round((time.monotonic() - started) * 1000)} ms, {len(tree.shown())} shown"
781
+ f"{' (glance)' if tree.glance else ''}")
782
+ return tree
783
+
784
+ def _audit_findings(self):
785
+ """The findings of the check's static audits, run on the built app."""
786
+ names = [name for name in self.check.audit if name not in (A11Y, device_audit.NAME)]
787
+ if not names:
788
+ return []
789
+ from . import audit
790
+ if not self.check.app_path:
791
+ return [{**audit.Finding("audit-needs-app", "2.1", "review",
792
+ "The audit reads a built app, and the check names none. Add app.path.",
793
+ "the check").to_dict(), "frame": None}]
794
+ out = []
795
+ for name in names:
796
+ out.extend({**finding.to_dict(), "frame": None} for finding in audit.run_audit(name, self.check.app_path))
797
+ return out
798
+
799
+ def _give_after_frame(self, path):
800
+ """``path`` becomes the frame of every Smart step still waiting for the screen after it."""
801
+ # A turn's actions share one frame: Smart sees no image between them.
802
+ for step in self._awaiting_frame:
803
+ step["frame"] = self._relative(path)
804
+ self._awaiting_frame.clear()
805
+
806
+ def abort(self, klass, message, fix=""):
807
+ """End the run as couldnt_run with ``klass``, release the simulator; a preparing run stops at its next step."""
808
+ self._stop.set()
809
+ dispatching = self.actions.request_stop()
810
+ action_failure = self._action_failure()
811
+ if action_failure is not None:
812
+ klass, message, fix = action_failure.klass, action_failure.message, action_failure.fix
813
+ elif dispatching:
814
+ klass, message, fix = ("unknown_outcome", "An action may have reached the device before the run stopped.",
815
+ "Inspect the run's actions.jsonl and device before another run.")
816
+ with self._finish_lock: # prepare() changes the state under this lock, so the check below holds
817
+ if self.state == "preparing":
818
+ try:
819
+ if self.result is None:
820
+ if (self.business_fixture is not None and self.business_result is not None
821
+ and (self.business_fixture.seeded or self.business_result["setup"] == "pending")):
822
+ self.business_result["cleanup"] = "needs_reconciliation"
823
+ if self._conditions is not None and self.conditions_cleanup == "pending":
824
+ self.conditions_cleanup = "needs_reconciliation"
825
+ self.error = {"klass": klass, "message": message, "fix": fix or ""}
826
+ self.log(f"couldn't run ({klass}): {message}")
827
+ result = self._compose(None)
828
+ self._write(result)
829
+ self.result = result
830
+ self.state = "finished" # the preparing thread releases the simulator at its next phase
831
+ return self.result
832
+ finally:
833
+ self._cancel_build()
834
+ with self.lock:
835
+ if self.result is None and self.error is None:
836
+ self._fail(CouldntRun(klass, message, fix))
837
+ return self.finish()
838
+
839
+ def _cancel_build(self):
840
+ # The build (up to 20 minutes the first time) runs in its own process group, so nothing else would stop it.
841
+ cancel = getattr(getattr(self.manager, "wda", None), "cancel", None)
842
+ if callable(cancel):
843
+ try:
844
+ cancel()
845
+ except Exception as error:
846
+ self.log(f"stopping the WebDriverAgent build failed: {type(error).__name__}")
847
+
848
+ def fail_internal(self, error):
849
+ """An unexpected exception: couldnt_run, class internal; the traceback goes to run.log only."""
850
+ self.log("".join(traceback.format_exception(type(error), error, error.__traceback__)))
851
+ self.error = {"klass": "internal", "message": f"Mobster hit an unexpected error ({type(error).__name__}).",
852
+ "fix": "run.log in the run folder has the details; please report it."}
853
+
854
+ def finish(self):
855
+ """The verdict (§4) and every artifact: result.json, report.html, check.yaml, the verdict frames."""
856
+ # Always lock, then _finish_lock: abort() is called with lock held while another thread may call finish().
857
+ with self.lock, self._finish_lock:
858
+ if self.result is not None:
859
+ if not self._preparing: # else the preparing thread releases it, at its next phase
860
+ self._release() # idempotent: a result written while preparing still frees the simulator
861
+ return self.result
862
+ self._seal_screen_frames()
863
+ interrupted = None
864
+ try:
865
+ decided = self._decide()
866
+ except CouldntRun as error:
867
+ # WebDriverAgent losing an app that crashed during the verdict read is the crash.
868
+ crash = self._crash(suspect=True) if error.klass in CRASH_EXPLAINS else None
869
+ decided = self._crashed(crash) if crash else None
870
+ if decided is None:
871
+ self._fail(error)
872
+ except KeyboardInterrupt as error:
873
+ # ctrl+c during the verdict read: the run still ends as stopped with every artifact, then it goes on.
874
+ if self.error is None:
875
+ self._fail(CouldntRun("stopped", "Stopped with ctrl+c."))
876
+ decided, interrupted = None, error
877
+ except Exception as error:
878
+ self.fail_internal(error)
879
+ decided = None
880
+ self._stop_evidence() # the log ends with the verdict read
881
+ primary = (
882
+ {"verdict": decided["verdict"], "class": decided["klass"]} if decided is not None
883
+ else {"verdict": "couldnt_run", "class": self.error["klass"]} if self.error is not None
884
+ else None
885
+ )
886
+ cleanup = self._cleanup_after_run()
887
+ failure = self._action_failure() or cleanup
888
+ if self.error is not None and self.error["klass"] == "unknown_outcome":
889
+ failure = None # a later cleanup failure cannot replace a prior unknown effect
890
+ decided = None
891
+ elif failure is not None or cleanup is not None:
892
+ if self.business_result is not None and primary is not None:
893
+ self.business_result["primary"] = primary
894
+ failed = self._fail(failure or cleanup)
895
+ if decided is not None:
896
+ decided = {**decided, "verdict": "couldnt_run", "klass": failed.klass,
897
+ "message": failed.message, "fix": failed.fix}
898
+ result = self._compose(decided)
899
+ if self.replay is not None:
900
+ try:
901
+ self.replay.after(result)
902
+ except Exception as error: # the cache never costs a run its verdict
903
+ self.log("".join(traceback.format_exception(type(error), error, error.__traceback__)))
904
+ try:
905
+ self._write(result)
906
+ finally:
907
+ self._release()
908
+ self.result = result
909
+ self.state = "finished"
910
+ if interrupted is not None:
911
+ raise interrupted
912
+ return result
913
+
914
+ def _halve(path):
915
+ """Resize the first frame to half its pixel width (603 px from an iPhone 17 Pro's 1206); returns that width."""
916
+ from PIL import Image
917
+ with Image.open(path) as image:
918
+ image = image.convert("RGB")
919
+ width = max(1, image.width // 2)
920
+ image.resize((width, max(1, round(image.height * width / image.width)))).save(path, "JPEG",
921
+ quality=FRAME_QUALITY)
922
+ return width
923
+
924
+
925
+ def resolve_mode(check, mode):
926
+ """The mode a run uses: auto means Smart with steps, launch-only without."""
927
+ if mode == "auto":
928
+ return "smart" if check.steps else "launch"
929
+ if mode not in MODES:
930
+ raise ValueError(f"mode must be auto or one of {', '.join(MODES)}")
931
+ return mode
932
+
933
+
934
+ def verify(check, *, mode="auto", runs_dir, manager=None, progress=None, key=None, cancelled=None,
935
+ assert_timeout=5):
936
+ """The whole run -> the result dict (§5.3)."""
937
+ from . import fixtures
938
+ from .system_surfaces.play import drive as drive_surfaces
939
+ try:
940
+ check = fixtures.for_run(check, runs_dir)
941
+ except fixtures.FixtureError as error:
942
+ run = VerifyRun(check, mode=resolve_mode(check, mode), runs_dir=runs_dir, manager=manager,
943
+ progress=progress, key=key, assert_timeout=assert_timeout)
944
+ return run.abort("usage", str(error), "`mobster fixtures list` shows the project's fixtures.")
945
+ mode = resolve_mode(check, mode)
946
+ run = VerifyRun(check, mode=mode, runs_dir=runs_dir, manager=manager, progress=progress, key=key,
947
+ assert_timeout=assert_timeout)
948
+ if check.steps and mode == "launch":
949
+ return run.abort("usage", "--keyless never calls a model, so it can't run steps.",
950
+ "Drop the steps for a launch-only check, or let your coding agent drive through "
951
+ "`mobster mcp`.")
952
+ if check.steps and mode == "keyless":
953
+ return run.abort("usage", "A key-less run with steps needs a coding agent to drive it.",
954
+ "Run it through `mobster mcp`, or drop the steps for a launch-only check.")
955
+ if mode == "smart":
956
+ from ..engines import smart_key
957
+ run.key = key or smart_key()
958
+ if not run.key:
959
+ why, fix = no_smart_key()
960
+ return run.abort("usage", f"These steps need Smart, and {why}.", fix)
961
+ try:
962
+ run.prepare()
963
+ if mode == "smart":
964
+ run.run_smart(cancelled)
965
+ drive_surfaces(run)
966
+ except CouldntRun as error:
967
+ if run.error is None:
968
+ run._fail(error)
969
+ except KeyboardInterrupt as stop:
970
+ # devtools.Terminated (SIGTERM, SIGHUP) is a KeyboardInterrupt too; the result rides on it for the CLI's JSON.
971
+ name = getattr(stop, "signum", None)
972
+ stop.result = run.abort("stopped", "Stopped with ctrl+c." if name is None else f"Stopped by {stop}.")
973
+ raise
974
+ except Exception as error:
975
+ run.fail_internal(error)
976
+ return run.finish()