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,670 @@
1
+ import hashlib
2
+ import json
3
+ import re
4
+ import threading
5
+ import time
6
+ from collections import deque
7
+ from dataclasses import dataclass, field
8
+ from typing import Optional
9
+
10
+ from ..verify.report import dots, has_node
11
+ from .targets import Refused, Target, TargetError, for_target
12
+
13
+ EXIT_CODES = {"passed": 0, "failed": 1, "needs_review": 2, "couldnt_run": 3}
14
+ # A cell's status when its repetitions disagree, and the suite's exit code: the first of these present wins.
15
+ WORST_FIRST = ("failed", "couldnt_run", "needs_review", "passed")
16
+ # couldnt_run classes a second attempt can fix.
17
+ RETRY_CLASSES = frozenset({"wda", "simulator", "launch", "busy", "internal", "replay"})
18
+ SAFETY_STOPS = ("unknown_outcome", "journal")
19
+ SAFETY_EXIT_STOPS = (*SAFETY_STOPS, "refused")
20
+ STOP_RANK = {"stopped": 0, "refused": 1, "journal": 2, "unknown_outcome": 3}
21
+ MAX_RETRIES, MAX_REPEAT, MAX_PARALLEL = 5, 20, 8
22
+ NOT_RUN = "The suite was stopped before this ran."
23
+ MARKS = {"passed": "✓", "failed": "✗", "needs_review": "!", "couldnt_run": "✗"}
24
+ # A real iPhone is asked this long whether it still answers after a check couldn't run on it.
25
+ ALIVE_TIMEOUT = 3.0
26
+ LOST = ("{name} stopped answering while this check was running. Mobster didn't run it again, because it can't tell "
27
+ "how far it got.")
28
+ LOST_FIX = "Reconnect and unlock the iPhone, then run the check again."
29
+ LOST_BEFORE = "{name} stopped answering earlier in this run, so this check didn't run on it."
30
+ BUSY_BEFORE = "{name} is in use by another Mobster app or command, so this check didn't run on it."
31
+ NO_DEVICE_LEFT = "Every device in this run stopped answering or is in use elsewhere, so this check didn't run."
32
+ # Where a spread check that never started is reported: on no device.
33
+ NOT_RUN_TARGET = Target("none", "device", record={"id": "none", "name": "Not run", "kind": "none"})
34
+
35
+
36
+ @dataclass
37
+ class Unit:
38
+ """One repetition of one check on one target, and its attempts."""
39
+ index: int
40
+ entry: object
41
+ target: object
42
+ repetition: int
43
+ check: object = None
44
+ record: Optional[dict] = None
45
+ refused: Optional[dict] = None
46
+ notes: list = field(default_factory=list)
47
+ attempts: list = field(default_factory=list)
48
+ started: bool = False
49
+
50
+
51
+ def worst(verdicts):
52
+ for verdict in WORST_FIRST:
53
+ if verdict in verdicts:
54
+ return verdict
55
+ return "couldnt_run"
56
+
57
+
58
+ def attempt_record(result, attempt):
59
+ """What a suite keeps of one verify result."""
60
+ reason = result.get("reason") or {}
61
+ frames = result.get("frames") or []
62
+ run_dir = result.get("run_dir")
63
+ frame = next((f for f in reversed(frames) if f.endswith("-verdict-ax.jpg")), None) or \
64
+ next((f for f in reversed(frames) if f.endswith("-verdict.jpg")), None)
65
+ return {"attempt": attempt, "runId": result.get("run_id"), "verdict": result.get("verdict") or "couldnt_run",
66
+ "class": reason.get("class"), "message": reason.get("message") or result.get("summary"),
67
+ "fix": reason.get("fix"), "summary": result.get("summary"), "seconds": result.get("seconds") or 0,
68
+ "runDir": run_dir, "report": result.get("report"),
69
+ "frame": f"{run_dir}/{frame}" if run_dir and frame else None, "video": result.get("video"),
70
+ "videoOffsetMs": result.get("video_offset_ms"), "device": dict(result.get("device") or {}),
71
+ "costUsd": result.get("cost_usd") or 0,
72
+ "replay": dict(result.get("replay") or {}) or None,
73
+ "linkFirst": dict((result.get("agent") or {}).get("linkFirst") or {}) or None,
74
+ "conditions": result.get("conditions"),
75
+ "business": _business_proof(result), "visual": dict(result.get("visual") or {}) or None,
76
+ "findings": [dict(item) for item in result.get("findings") or () if isinstance(item, dict)],
77
+ "screen": dict(result.get("screen") or {}) or None,
78
+ "snapshot": snapshot_of(result),
79
+ "evidence": dict(result.get("evidence") or {}) or None, "stepProof": result.get("step_proof"),
80
+ "perf": result.get("perf") or None, "screenCoverage": dict(result.get("screenCoverage") or {}) or None,
81
+ "healed": [dict(item) for item in result.get("healed") or () if isinstance(item, dict)],
82
+ "assertions": [{"index": item.get("index"), "text": item.get("text"), "ok": bool(item.get("ok")),
83
+ "observed": item.get("observed"), "outlined": has_node(item)}
84
+ for item in result.get("assertions") or () if isinstance(item, dict)]}
85
+
86
+
87
+ def _business_proof(result):
88
+ """Keep lifecycle proof and registered field IDs, without app-produced values."""
89
+ business = result.get("business") or {}
90
+ if not isinstance(business, dict) or not business:
91
+ return None
92
+ declared = ((result.get("check") or {}).get("business") or {}).get("expect")
93
+ observed = business.get("assertions")
94
+ fields = ([item.get("field") for item in declared] if isinstance(declared, list) and
95
+ all(isinstance(item, dict) and isinstance(item.get("field"), str) for item in declared) else None)
96
+ complete = (fields is not None and len(fields) == len(set(fields)) and isinstance(observed, list)
97
+ and len(observed) == len(fields) and
98
+ all(isinstance(item, dict) and type(item.get("ok")) is bool for item in observed) and
99
+ [item.get("field") for item in observed] == fields)
100
+ generation = business.get("generation")
101
+ source_digest = business.get("sourceSha256")
102
+ return {"bindingSha256": business.get("bindingSha256"), "setup": business.get("setup"),
103
+ "status": business.get("status"), "cleanup": business.get("cleanup"),
104
+ "generation": generation if type(generation) is int and generation >= 1 else None,
105
+ "sourceSha256": source_digest if isinstance(source_digest, str) and
106
+ re.fullmatch(r"[0-9a-f]{64}", source_digest) else None,
107
+ "assertionFields": fields if complete else None,
108
+ "passedFields": [item["field"] for item in observed if item["ok"]] if complete else None,
109
+ "failedFields": [item["field"] for item in observed if not item["ok"]] if complete else None}
110
+
111
+
112
+ def snapshot_of(result):
113
+ """The run's snapshot (verify/snapshot_run.py) with its image paths made absolute, or None."""
114
+ snapshot = result.get("snapshot")
115
+ run_dir = result.get("run_dir")
116
+ if not isinstance(snapshot, dict):
117
+ return None
118
+ out = dict(snapshot)
119
+ for key in ("baseline", "current", "diff"):
120
+ if out.get(key) and run_dir:
121
+ out[key] = f"{run_dir}/{out[key]}"
122
+ return out
123
+
124
+
125
+ def retryable(record):
126
+ if record["class"] in SAFETY_STOPS:
127
+ return False
128
+ if record["verdict"] == "failed":
129
+ return True
130
+ return record["verdict"] == "couldnt_run" and record["class"] in RETRY_CLASSES
131
+
132
+
133
+ def device_label(target, attempts, check=None, resolved=None):
134
+ # Any idle emulator's record names no device, so a default-target Android run is named by what it reported.
135
+ named = resolved if resolved is not None and resolved.get("kind") != "android" else target.record
136
+ if named is not None:
137
+ device = (attempts[-1].get("device") or {}) if attempts else {}
138
+ runtime = device.get("runtime")
139
+ label = (named.get("name") or named.get("id") or "device") + (
140
+ f" · {runtime}" if runtime and named.get("kind") != "simulator" else "")
141
+ return label, target.key if named is target.record else (
142
+ f"device:{named.get('id') or named.get('udid') or named.get('name') or 'unknown'}")
143
+ for record in reversed(attempts):
144
+ device = record.get("device") or {}
145
+ if device.get("type") and device.get("runtime") and device.get("udid"):
146
+ return f"{device['type']} · {device['runtime']}", f"sim:{device['type']}@{device['runtime']}"
147
+ if target.kind == "default":
148
+ named = getattr(check, "device", None)
149
+ label = " · ".join(filter(None, [named or "Default simulator", getattr(check, "runtime", None)]))
150
+ # Until a run reports its simulator, the label says each check picks its own; the key stays stable.
151
+ return (label if named else target.label), f"default:{label}"
152
+ return target.label, target.key
153
+
154
+
155
+ GB_PER_SIMULATOR = 4 # the memory budget for a simulator, WebDriverAgent and a run
156
+
157
+
158
+ def machine_parallel_cap(cpus=None, memory=None, environ=None):
159
+ """How many simulators this Mac runs at once: one per two cores and per 4 GB of memory, 1 to MAX_PARALLEL."""
160
+ import os
161
+ environ = os.environ if environ is None else environ
162
+ try:
163
+ fixed = int(environ.get("MOBSTER_TEST_MAX_PARALLEL", ""))
164
+ except ValueError:
165
+ fixed = 0
166
+ if fixed > 0:
167
+ return min(fixed, MAX_PARALLEL)
168
+ cpus = cpus if cpus is not None else (os.cpu_count() or 2)
169
+ if memory is None:
170
+ try:
171
+ memory = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
172
+ except (ValueError, OSError, AttributeError):
173
+ memory = 16 * 1024 ** 3
174
+ return max(1, min(MAX_PARALLEL, cpus // 2, int(memory // (GB_PER_SIMULATOR * 1024 ** 3))))
175
+
176
+
177
+ class Suite:
178
+ """``attempt(check, record, *, label, video_name)`` runs one attempt and returns verify's result (execute.Attempts,
179
+ or a fake)."""
180
+
181
+ def __init__(self, entries, targets, *, attempt, retries=1, repeat=1, parallel=1, strict=False, gate=None,
182
+ named_device=None, progress=None, stop=None, clock=time.time, on_cancel=None, spread=False,
183
+ alive=None, cap=None, before_attempt=None):
184
+ self.entries, self.targets = list(entries), list(targets)
185
+ self.attempt = attempt
186
+ self.retries = max(0, min(int(retries), MAX_RETRIES))
187
+ self.repeat = max(1, min(int(repeat), MAX_REPEAT))
188
+ self.parallel = max(1, min(int(parallel), MAX_PARALLEL))
189
+ self.progress = progress or (lambda line: None)
190
+ cap = machine_parallel_cap() if cap is None else cap
191
+ if self.parallel > cap:
192
+ self.progress(f"This Mac’s configured simulator limit is {cap}, so --parallel {self.parallel} runs {cap}. "
193
+ "MOBSTER_TEST_MAX_PARALLEL=N changes the limit.")
194
+ self.parallel = cap
195
+ self.strict = bool(strict)
196
+ self.gate, self.named_device = gate, named_device
197
+ self.stop = stop or threading.Event()
198
+ self.clock = clock
199
+ self.on_cancel = on_cancel
200
+ self.before_attempt = before_attempt
201
+ self.spread = bool(spread)
202
+ self.alive = alive or phone_answers # alive(target) -> whether a real iPhone still answers
203
+ self.out = {} # target key -> "lost" | "busy": devices that take no more checks in this run
204
+ self._inflight = 0
205
+ self.interrupted = None # the KeyboardInterrupt (ctrl+c, or devtools.Terminated) that stopped the run
206
+ self.units = []
207
+ self.started_at = self.finished_at = None
208
+ self._lock = threading.Lock()
209
+ self._abort_called = False
210
+ self._sealed = False
211
+ self.stop_reason = None
212
+ bind_gate = getattr(attempt, "bind_gate", None)
213
+ self._leased_real = callable(bind_gate)
214
+ if self._leased_real:
215
+ bind_gate(gate)
216
+ self._plan()
217
+
218
+ def _plan(self):
219
+ index = 0
220
+ for entry in self.entries:
221
+ if self.spread:
222
+ for repetition in range(1, self.repeat + 1):
223
+ index += 1
224
+ refused = None if entry.check is not None else self._resolve(entry, None)[2]
225
+ self.units.append(Unit(index, entry, NOT_RUN_TARGET if refused else None, repetition,
226
+ refused=refused))
227
+ continue
228
+ for target in self.targets:
229
+ check, record, refused, notes = self._resolve(entry, target)
230
+ if self.repeat > 1 and check is not None:
231
+ bind_repeat = getattr(self.attempt, "bind_repeat", None)
232
+ if callable(bind_repeat):
233
+ bind_repeat(check, record)
234
+ for repetition in range(1, self.repeat + 1):
235
+ index += 1
236
+ self.units.append(Unit(index, entry, target, repetition, check=check, record=record,
237
+ refused=refused, notes=list(notes)))
238
+
239
+ def _resolve(self, entry, target):
240
+ if entry.check is None:
241
+ return None, None, {"class": "usage", "message": entry.error or "The check can't be read.", "fix": None}, []
242
+ if target is not None and target.real and not self._leased_real:
243
+ return None, None, {"class": "device", "message": "Real-device checks need a runner that holds the "
244
+ "device lease during app admission.", "fix": None}, []
245
+ try:
246
+ check, notes, record = for_target(entry.check, target, named_device=self.named_device, gate=self.gate)
247
+ except Refused as error:
248
+ return None, None, {"class": error.klass, "message": error.message, "fix": error.fix or None}, []
249
+ except TargetError as error:
250
+ return None, None, {"class": "usage", "message": str(error), "fix": None}, []
251
+ return check, record, None, notes
252
+
253
+ @property
254
+ def total(self):
255
+ return len(self.units)
256
+
257
+ @property
258
+ def done(self):
259
+ with self._lock:
260
+ return sum(1 for unit in self.units if unit.refused is not None or (
261
+ unit.attempts and (self.stop.is_set() or not self._needs_more(unit))))
262
+
263
+ def cancel(self, reason="stopped"):
264
+ """Seal dispatch and abort in-flight work once, including an already-set external stop."""
265
+ with self._lock:
266
+ if self._sealed:
267
+ return False
268
+ self.stop.set()
269
+ if self.stop_reason is None or STOP_RANK[reason] > STOP_RANK[self.stop_reason]:
270
+ self.stop_reason = reason
271
+ abort = not self._abort_called
272
+ self._abort_called = True
273
+ if abort and self.on_cancel is not None:
274
+ self.on_cancel()
275
+ return True
276
+
277
+ def seal(self):
278
+ """Commit the terminal stop decision before reports are serialized."""
279
+ with self._lock:
280
+ if self.stop.is_set() and self.stop_reason is None:
281
+ self.stop_reason = "stopped"
282
+ self._sealed = True
283
+
284
+ def run(self):
285
+ """Run every unit; returns ``results()``. Stops early, between attempts, once ``stop`` is set."""
286
+ self.started_at = self.clock()
287
+ if self.stop.is_set():
288
+ self.cancel()
289
+ workers = []
290
+ if self.spread:
291
+ queue = deque(unit for unit in self.units if unit.refused is None)
292
+ sims = [target for target in self.targets if target.record is None]
293
+ lanes = [(queue, target) for target in self.targets if target.record is not None]
294
+ lanes += [(queue,
295
+ sims[number % len(sims)]) for number in range(max(self.parallel, len(sims)))] if sims else []
296
+ else:
297
+ queues = {}
298
+ for unit in self.units:
299
+ if unit.refused is None:
300
+ record = unit.record
301
+ lane = (f"device:{record.get('udid') or record.get('id') or 'unknown'}"
302
+ if record is not None else "sims")
303
+ queues.setdefault(lane, deque()).append(unit)
304
+ lanes = [(queue, None) for lane, queue in queues.items()
305
+ for _ in range(self.parallel if lane == "sims" else 1)]
306
+ try:
307
+ for queue, target in lanes:
308
+ worker = threading.Thread(target=self._work, args=(queue, target), daemon=True,
309
+ name=f"mobster-test-{len(workers) + 1}")
310
+ workers.append(worker)
311
+ worker.start()
312
+ for worker in workers:
313
+ while worker.is_alive():
314
+ if self.stop.is_set():
315
+ self.cancel()
316
+ worker.join(.2)
317
+ except BaseException as error:
318
+ if isinstance(error, KeyboardInterrupt):
319
+ self.interrupted = error
320
+ self.cancel()
321
+ self.progress("Stopping: the runs in flight end and write their reports.")
322
+ # A terminal report cannot outrun a worker that still owns a device: its final action may be uncertain.
323
+ while any(worker.is_alive() for worker in workers):
324
+ for worker in workers:
325
+ if worker.is_alive():
326
+ try:
327
+ worker.join(.2)
328
+ except KeyboardInterrupt as second:
329
+ self.interrupted = self.interrupted or second
330
+ if not isinstance(error, KeyboardInterrupt):
331
+ raise
332
+ if self.stop.is_set():
333
+ self.cancel()
334
+ self.finished_at = self.clock()
335
+ return self.results()
336
+
337
+ def _take(self, queue, target):
338
+ """The next unit for a worker. Spread: it goes to ``target``; a unit that target refuses is finished here."""
339
+ while True:
340
+ with self._lock:
341
+ if self.stop.is_set():
342
+ return None
343
+ unit = queue.popleft() if queue else None
344
+ if unit is not None:
345
+ unit.started = True
346
+ self._inflight += 1
347
+ elif not self._inflight:
348
+ return None
349
+ if unit is None:
350
+ time.sleep(.02)
351
+ continue
352
+ if self.spread:
353
+ unit.target = target
354
+ unit.check, unit.record, unit.refused, unit.notes = self._resolve(unit.entry, target)
355
+ if self.repeat > 1 and unit.check is not None:
356
+ bind_repeat = getattr(self.attempt, "bind_repeat", None)
357
+ if callable(bind_repeat):
358
+ bind_repeat(unit.check, unit.record)
359
+ if unit.refused is not None:
360
+ with self._lock:
361
+ self._inflight -= 1
362
+ continue
363
+ return unit
364
+
365
+ def _work(self, queue, target):
366
+ while True:
367
+ if target is not None and target.key in self.out:
368
+ return
369
+ unit = self._take(queue, target)
370
+ if unit is None:
371
+ return
372
+ try:
373
+ self._run_unit(unit, queue)
374
+ finally:
375
+ with self._lock:
376
+ self._inflight -= 1
377
+
378
+ def _needs_more(self, unit):
379
+ last = unit.attempts[-1] if unit.attempts else None
380
+ if last is None:
381
+ return True
382
+ return last["verdict"] != "passed" and retryable(last) and len(unit.attempts) <= self.retries
383
+
384
+ def _run_unit(self, unit, queue):
385
+ if unit.target.key in self.out and not self.spread:
386
+ return # its device is gone for this run; the cell says so
387
+ while not self.stop.is_set():
388
+ number = len(unit.attempts) + 1
389
+ label = self._label(unit, number)
390
+ self.progress(f"▸ {label}")
391
+ validation_refused = False
392
+ try:
393
+ if self.before_attempt is not None:
394
+ self.before_attempt(unit.entry, unit.check, unit.record)
395
+ except Refused as error:
396
+ validation_refused = True
397
+ self.cancel("refused")
398
+ result = {"verdict": "couldnt_run", "reason": {"class": error.klass, "message": error.message,
399
+ "fix": error.fix or None}}
400
+ except Exception as error:
401
+ validation_refused = True
402
+ self.cancel("refused")
403
+ result = {"verdict": "couldnt_run", "reason": {"class": "internal", "message":
404
+ f"Mobster couldn't check whether this run is still allowed ({type(error).__name__})."}}
405
+ else:
406
+ if self.stop.is_set():
407
+ self.cancel()
408
+ result = {"verdict": "couldnt_run", "reason": {"class": "stopped", "message": NOT_RUN}}
409
+ else:
410
+ try:
411
+ result = self.attempt(unit.check, unit.record, label=label,
412
+ video_name=f"{unit.index:03d}-{_slug(unit.entry.name)}-{number}")
413
+ except Exception as error: # an attempt runner that raises: couldn't run, class internal
414
+ result = {"verdict": "couldnt_run", "reason": {"class": "internal", "message":
415
+ f"Mobster hit an unexpected error ({type(error).__name__})."}}
416
+ record = self._lost(unit.target, attempt_record(result, number))
417
+ if validation_refused:
418
+ record["_validation_refused"] = True
419
+ if self.spread and not unit.attempts and record["class"] == "busy" and unit.target.real and \
420
+ result.get("not_dispatched"):
421
+ # Held by another Mobster process before anything was tapped: another device takes the check.
422
+ with self._lock:
423
+ self.out[unit.target.key] = "busy"
424
+ unit.started, unit.target = False, None
425
+ queue.appendleft(unit)
426
+ self.progress(f"! {unit.entry.name}: {record['message']} Another device takes it.")
427
+ return
428
+ with self._lock:
429
+ unit.notes.extend(result.get("notes") or ())
430
+ unit.attempts.append(record)
431
+ safety_stop = record["class"] in SAFETY_STOPS
432
+ if safety_stop:
433
+ self.stop.set()
434
+ more = self._needs_more(unit) and not self.stop.is_set()
435
+ if safety_stop:
436
+ self.cancel(record["class"])
437
+ self.progress(self._ended(unit, record, more))
438
+ if not more:
439
+ return
440
+
441
+ def _lost(self, target, record):
442
+ """``record``, or device_lost when a real iPhone's check couldn't run because the phone stopped answering."""
443
+ # Never run again: Mobster can't tell how far it got, and a step may have sent something.
444
+ if record["verdict"] != "couldnt_run" or record["class"] not in ("wda", "device") or not target.real \
445
+ or self.alive(target):
446
+ return record
447
+ with self._lock:
448
+ self.out[target.key] = "lost"
449
+ return {**record, "class": "device_lost", "message": LOST.format(name=f"“{dots(target.label)}”"),
450
+ "fix": LOST_FIX}
451
+
452
+ def _label(self, unit, number, attempts=()):
453
+ """The line's name: the simulator a default-target run reported once it has, else the target's own label."""
454
+ where = device_label(unit.target, attempts, unit.entry.check)[0] if unit.target.kind == "default" \
455
+ else unit.target.label
456
+ parts = [f"{unit.entry.name} on {dots(where)}"]
457
+ if self.repeat > 1:
458
+ parts.append(f"run {unit.repetition} of {self.repeat}")
459
+ if number > 1:
460
+ parts.append(f"attempt {number} of {self.retries + 1}")
461
+ return parts[0] + (f" ({', '.join(parts[1:])})" if len(parts) > 1 else "")
462
+
463
+ def _ended(self, unit, record, more):
464
+ mark = MARKS.get(record["verdict"], "?")
465
+ word = {"passed": "passed", "failed": "failed", "needs_review": "needs review",
466
+ "couldnt_run": "couldn't run"}.get(record["verdict"], record["verdict"])
467
+ line = f"{mark} {word} {self._label(unit, record['attempt'], [record])} ({record['seconds']} s)"
468
+ if record["verdict"] != "passed" and record.get("message"):
469
+ line += f"\n {record['message']}"
470
+ if more:
471
+ line += "\n Trying again."
472
+ return line
473
+
474
+ def _units(self, entry, target):
475
+ return [unit for unit in self.units if unit.entry is entry and (unit.target or NOT_RUN_TARGET) is target]
476
+
477
+ def _targets_of(self, entry):
478
+ """The targets ``entry`` has a result on: every target, or, spread, the ones its units ran on (or none)."""
479
+ if not self.spread:
480
+ return self.targets
481
+ found = {}
482
+ for unit in self.units:
483
+ if unit.entry is entry:
484
+ target = unit.target or NOT_RUN_TARGET
485
+ found[target.key] = target
486
+ return list(found.values())
487
+
488
+ def _unstarted(self, target):
489
+ """Why a unit that never ran has no attempt: its device was lost or busy, none was left, or the suite ended."""
490
+ state = self.out.get(target.key)
491
+ if state == "lost":
492
+ return {"class": "device_lost", "message": LOST_BEFORE.format(name=f"“{dots(target.label)}”"),
493
+ "fix": LOST_FIX}
494
+ if state == "busy":
495
+ return {"class": "busy", "message": BUSY_BEFORE.format(name=f"“{dots(target.label)}”"), "fix": None}
496
+ if self.spread and self.out and not self.stop.is_set():
497
+ return {"class": "device_lost", "message": NO_DEVICE_LEFT, "fix": LOST_FIX}
498
+ return {"class": "stopped", "message": NOT_RUN, "fix": None}
499
+
500
+ def cell(self, entry, target):
501
+ """One check on one target, across its repetitions: results.json's per-device result."""
502
+ units = self._units(entry, target)
503
+ finals, attempts = [], []
504
+ for unit in units:
505
+ if unit.refused is not None:
506
+ finals.append(("couldnt_run", {}, dict(unit.refused)))
507
+ continue
508
+ if not unit.attempts:
509
+ finals.append(("couldnt_run", {}, self._unstarted(unit.target or target)))
510
+ continue
511
+ last = unit.attempts[-1]
512
+ finals.append((last["verdict"], last, {"class": last["class"], "message": last["message"],
513
+ "fix": last["fix"]}))
514
+ attempts.extend({**record, "repetition": unit.repetition} for record in unit.attempts)
515
+ passed = sum(verdict == "passed" for verdict, _, _ in finals)
516
+ status = worst({verdict for verdict, _, _ in finals})
517
+ selected, reason = next(((record, why) for verdict, record, why in reversed(finals)
518
+ if verdict == status), ({}, None))
519
+ safety_record = next((record for klass in SAFETY_STOPS for record in reversed(attempts)
520
+ if record["class"] == klass), None)
521
+ safety_record = safety_record or next((record for record in reversed(attempts)
522
+ if record.get("_validation_refused")), None)
523
+ safety_stop = None
524
+ if safety_record is not None:
525
+ status, selected = "couldnt_run", safety_record
526
+ reason = {"class": safety_record["class"], "message": safety_record["message"],
527
+ "fix": safety_record["fix"]}
528
+ safety_stop = safety_record["class"] if safety_record["class"] in SAFETY_STOPS else "refused"
529
+ verdicts = {record["verdict"] for record in attempts}
530
+ flaky = "passed" in verdicts and len(verdicts) > 1
531
+ label, key = device_label(target, attempts, entry.check, units[0].record if units else None)
532
+ notes = list(dict.fromkeys(note for unit in units for note in unit.notes))
533
+ skipped = (selected.get("conditions") or {}).get("skipped") or ()
534
+ if skipped:
535
+ notes.append("Skipped on this device: " + " ".join(f"{item['key']}: {item['why']}" for item in skipped))
536
+ out = {"device": label, "deviceKey": key, "status": status, "attempts": len(attempts), "flaky": flaky,
537
+ "runId": selected.get("runId"), "runDir": selected.get("runDir"),
538
+ "report": selected.get("report"), "visual": selected.get("visual"),
539
+ "business": selected.get("business"),
540
+ "screenCoverage": selected.get("screenCoverage"),
541
+ "durationMs": round(sum(r["seconds"] for r in attempts) * 1000),
542
+ "healed": list(selected.get("healed") or ()), "reason": None if status == "passed" else reason,
543
+ "quarantined": entry.quarantined is not None,
544
+ "frame": selected.get("frame"), "video": selected.get("video"),
545
+ "videoOffsetMs": selected.get("videoOffsetMs"),
546
+ "assertions": list(selected.get("assertions") or ()), "stepProof": selected.get("stepProof"),
547
+ "costUsd": round(sum(r.get("costUsd") or 0 for r in attempts), 5),
548
+ "replayed": (selected.get("replay") or {}).get("mode") in ("replayed", "healed"),
549
+ "runs": [{key_: record.get(key_) for key_ in ("repetition", "attempt", "runId", "verdict", "class",
550
+ "message", "seconds", "report", "runDir", "frame",
551
+ "video", "business", "visual", "screenCoverage")}
552
+ for record in attempts]}
553
+ device = selected.get("device") or {}
554
+ out["environment"] = {"kind": "iphone" if target.real else "simulator",
555
+ "model": device.get("type"), "runtime": device.get("runtime")}
556
+ if self.repeat > 1:
557
+ out["passRate"] = round(passed / len(finals), 4) if finals else 0.0
558
+ if flaky:
559
+ out["flakyReason"] = next(({"class": r["class"], "message": r["message"],
560
+ "assertions": [item for item in r.get("assertions") or () if not item["ok"]]}
561
+ for r in attempts if r["verdict"] != "passed"), None)
562
+ if selected.get("conditions"):
563
+ out["conditions"] = selected["conditions"]
564
+ if selected.get("findings"):
565
+ out["findings"] = selected["findings"]
566
+ if selected.get("screen"):
567
+ out["screen"] = selected["screen"]
568
+ if selected.get("snapshot"):
569
+ out["snapshot"] = selected["snapshot"]
570
+ if selected.get("perf"):
571
+ out["perf"] = selected["perf"]
572
+ if notes:
573
+ out["notes"] = notes
574
+ if safety_stop:
575
+ out["safetyStop"] = safety_stop
576
+ # A pass after retry still retains the earlier crash/log evidence for diagnosing flakiness.
577
+ evidence = (_evidence([selected]) if selected else None) or _evidence(attempts)
578
+ if evidence is not None:
579
+ out["evidence"] = evidence
580
+ if selected.get("linkFirst"):
581
+ out["linkFirst"] = selected["linkFirst"]
582
+ if selected.get("replay"):
583
+ replay = selected["replay"]
584
+ out["replay"] = {key_: replay.get(key_) for key_ in ("mode", "reason", "steps", "replayed", "healed",
585
+ "live", "seconds", "costUsd", "pendingHeals",
586
+ "status", "approvedRevision", "fixed", "saved")}
587
+ return out
588
+
589
+ def results(self):
590
+ checks, devices = [], {}
591
+ for entry in self.entries:
592
+ results = []
593
+ for target in self._targets_of(entry):
594
+ result = self.cell(entry, target)
595
+ results.append(result)
596
+ known = devices.setdefault(result["deviceKey"], {**target.public(), "key": result["deviceKey"],
597
+ "name": result["device"]})
598
+ if len(result["device"]) > len(known["name"]):
599
+ known["name"] = result["device"] # "Sam's iPhone · iOS 26.4" once a run there reported its iOS
600
+ item = {"name": entry.name, "file": entry.file, "classname": entry.classname, "tags": list(entry.tags),
601
+ "quarantined": entry.quarantined is not None, "results": results}
602
+ if entry.locale:
603
+ item["locale"] = entry.locale
604
+ identity = json.dumps((entry.file, entry.locale), ensure_ascii=False, separators=(",", ":"))
605
+ item["caseId"] = "locale-" + hashlib.sha256(identity.encode("utf-8")).hexdigest()
606
+ if entry.quarantined:
607
+ item["quarantineReason"] = entry.quarantined
608
+ if entry.error:
609
+ item["error"] = entry.error
610
+ checks.append(item)
611
+ cells = [result for check in checks for result in check["results"]]
612
+ counted = [result for result in cells if not result["quarantined"]]
613
+ summary = {"passed": sum(r["status"] == "passed" for r in counted),
614
+ "failed": sum(r["status"] == "failed" for r in counted),
615
+ "needsReview": sum(r["status"] == "needs_review" for r in counted),
616
+ "couldntRun": sum(r["status"] == "couldnt_run" for r in counted),
617
+ "flaky": sum(bool(r["flaky"]) for r in counted),
618
+ "quarantined": len(cells) - len(counted), "total": len(cells),
619
+ "costUsd": round(sum(r["costUsd"] for r in cells), 5)}
620
+ smart = [r for r in cells if r.get("replay")]
621
+ if smart:
622
+ summary["replayed"] = sum(bool(r.get("replayed")) for r in smart)
623
+ summary["healed"] = sum(len(r.get("healed") or ()) for r in smart)
624
+ summary["cacheHitRate"] = round(summary["replayed"] / len(smart), 4)
625
+ linked = [r["linkFirst"] for r in cells if r.get("linkFirst")]
626
+ if linked:
627
+ summary["linkFirst"] = {"steps": sum(r["taken"] for r in linked),
628
+ "modelCallsSaved": sum(r["modelCallsSaved"] for r in linked)}
629
+ return {"checks": checks, "devices": list(devices.values()), "summary": summary,
630
+ "stopReason": self.stop_reason,
631
+ "exitCode": self.exit_code(counted),
632
+ "startedAt": _ms(self.started_at), "finishedAt": _ms(self.finished_at),
633
+ "durationMs": round(((self.finished_at or self.clock()) - (self.started_at or self.clock())) * 1000)}
634
+
635
+ def exit_code(self, counted):
636
+ if self.stop_reason in SAFETY_EXIT_STOPS:
637
+ return 3
638
+ if self.stop_reason == "stopped":
639
+ signum = getattr(self.interrupted, "signum", None)
640
+ return 130 if signum is None else 128 + int(signum)
641
+ statuses = {result["status"] for result in counted}
642
+ if "failed" in statuses or (self.strict and any(result["flaky"] for result in counted)):
643
+ return 1
644
+ if "couldnt_run" in statuses:
645
+ return 3
646
+ if "needs_review" in statuses:
647
+ return 2
648
+ return 0
649
+
650
+
651
+ def _evidence(attempts):
652
+ for record in reversed(attempts):
653
+ if record["verdict"] != "passed" and record.get("evidence"):
654
+ return {**record["evidence"], "runId": record.get("runId"), "runDir": record.get("runDir")}
655
+ return None
656
+
657
+
658
+ def phone_answers(target):
659
+ """Whether a real iPhone's WebDriverAgent still answers. An iPhone with no address doesn't."""
660
+ from ..devices import wda_answers
661
+ url = (target.record or {}).get("wdaUrl") or ""
662
+ return bool(url) and wda_answers(url, timeout=ALIVE_TIMEOUT)
663
+
664
+
665
+ def _ms(seconds):
666
+ return round(seconds * 1000) if seconds is not None else None
667
+
668
+
669
+ def _slug(text):
670
+ return re.sub(r"[^a-z0-9]+", "-", str(text).lower()).strip("-")[:40] or "check"