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,378 @@
1
+ """`mobster test --xctest`: the project's own XCTest and Swift Testing targets, run beside the checks and read from the
2
+ .xcresult into the suite's one results model (doc["xctest"]).
3
+
4
+ This is not a general XCTest runner: one simulator, no retries of a test, no device runs, and the project's schemes
5
+ and test plans are read, never changed."""
6
+
7
+ import json
8
+ import plistlib
9
+ import re
10
+ from pathlib import Path
11
+
12
+ from ..sim.models import SimError
13
+ from .prbuild import BuildError, find_project, pick_scheme, project_arg, run_command
14
+ from .prbuild import build as build_app
15
+
16
+ BUNDLE_DIR = "xctest"
17
+ BUNDLE_NAME = "tests.xcresult"
18
+ ATTACHMENTS = "attachments"
19
+ RUN_LOG = "xctest.log"
20
+ RUN_TIMEOUT = 30 * 60
21
+ READ_TIMEOUT = 120
22
+ FAILURE_EXIT = 65 # xcodebuild's code for "the tests ran and some failed"
23
+ MAX_FAILURES = 5 # failure messages kept per test
24
+ MAX_MESSAGE = 600
25
+ ONLY_TESTING = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_ .+()-]*(/[A-Za-z0-9_ .+()-]+){0,2}$")
26
+ LOCATION = re.compile(r"^(?P<file>[^\n:]+\.(?:swift|m|mm|h|c|cpp)):(?P<line>\d+):?\s*(?P<text>.*)$", re.S)
27
+ EXPORT_SUFFIX = re.compile(r"_\d+_[0-9A-Fa-f-]{36}(?=\.[^.]*$|$)") # xcresulttool appends _<n>_<UUID> to a name
28
+ RESULTS = {"passed": "passed", "failed": "failed", "skipped": "skipped", "expected failure": "passed"}
29
+
30
+
31
+ class XctestError(Exception):
32
+ """The XCTest half of the run couldn't be done: what happened, and what to do (the checks still ran)."""
33
+
34
+ def __init__(self, message, fix=""):
35
+ super().__init__(message)
36
+ self.message, self.fix = message, fix
37
+
38
+
39
+ def only_testing_ok(items):
40
+ """``--only-testing`` values: Target, Target/Class or Target/Class/test. XCTestError otherwise."""
41
+ for item in items:
42
+ if not ONLY_TESTING.match(item):
43
+ raise XctestError(f"--only-testing {item!r} isn't a test identifier.",
44
+ "Write Target, Target/Class or Target/Class/testName.")
45
+ return tuple(items)
46
+
47
+
48
+ def find_xctestrun(derived):
49
+ """The .xctestrun file build-for-testing wrote under ``derived``, or None."""
50
+ found = sorted((Path(derived) / "Build" / "Products").glob("*.xctestrun"))
51
+ return found[-1] if found else None
52
+
53
+
54
+ def targets_of(xctestrun):
55
+ """The test targets an .xctestrun names, in the order it lists them."""
56
+ try:
57
+ with open(xctestrun, "rb") as handle:
58
+ data = plistlib.load(handle)
59
+ except (OSError, plistlib.InvalidFileException, ValueError):
60
+ return []
61
+ blocks = []
62
+ for config in data.get("TestConfigurations") or ():
63
+ blocks += [block for block in config.get("TestTargets") or () if isinstance(block, dict)]
64
+ blocks += [block for key, block in data.items() if key != "__xctestrun_metadata__" and isinstance(block, dict)
65
+ and "TestBundlePath" in block]
66
+ names = [str(block.get("BlueprintName") or block.get("ProductModuleName") or "") for block in blocks]
67
+ return list(dict.fromkeys(name for name in names if name))
68
+
69
+
70
+ def select_targets(targets, changed, only=()):
71
+ """(targets to run, {target: why}). ``only`` (--only-testing) wins; else the targets whose folder holds a changed
72
+ file; else every target, because a change that touches no test file can still break one."""
73
+ if only:
74
+ named = {item.split("/", 1)[0] for item in only}
75
+ return [name for name in targets if name in named], {name: "named with --only-testing" for name in named
76
+ if name in targets}
77
+ if changed is None:
78
+ return list(targets), {}
79
+ hit = {}
80
+ for target in targets:
81
+ files = [path for path in changed if target in Path(path).parts]
82
+ if files:
83
+ hit[target] = f"{files[0]} changed" + (f" and {len(files) - 1} more" if len(files) > 1 else "")
84
+ if hit:
85
+ return [name for name in targets if name in hit], hit
86
+ return list(targets), {name: "no test file changed, so every target runs" for name in targets}
87
+
88
+
89
+ def build(root, *, scheme, project, test_plan=None, runner=None, progress=None, bundle=None, configuration="Debug"):
90
+ """build-for-testing into .mobster/build: the app the checks drive, the test bundles and the .xctestrun. The
91
+ result is prbuild.build's, plus "xctestrun" and "targets". BuildError."""
92
+ built = build_app(root, scheme=scheme, project=project, platform="sim", configuration=configuration,
93
+ runner=runner, progress=progress, bundle=bundle, for_testing=True, test_plan=test_plan)
94
+ derived = Path(root) / ".mobster" / "build"
95
+ run_file = find_xctestrun(derived)
96
+ if run_file is None:
97
+ raise BuildError(f"{scheme} built, but it has no test target to run.",
98
+ "Add a unit test target to the scheme's Test action, or leave --xctest out.")
99
+ return {**built, "scheme": scheme, "xctestrun": str(run_file), "targets": targets_of(run_file)}
100
+
101
+
102
+ def prepare(planned, progress, *, runner=None):
103
+ """The build for a run that didn't build for testing first (`mobster test --xctest`): the project's own scheme.
104
+ BuildError."""
105
+ options, root = planned.options, planned.root
106
+ found = project_arg(Path(root) / options.xctest_project) if options.xctest_project else find_project(root)
107
+ if found is None:
108
+ raise BuildError(f"{root} has no Xcode project to build the tests from.",
109
+ "Pass --project YourApp.xcodeproj, or leave --xctest out.")
110
+ scheme = options.xctest_scheme or pick_scheme(found, runner=runner, cwd=root)
111
+ return build(root, scheme=scheme, project=found, test_plan=options.xctest_plan, runner=runner, progress=progress)
112
+
113
+
114
+ def run_argv(xctestrun, udid, bundle_path, only):
115
+ argv = ["xcodebuild", "test-without-building", "-xctestrun", str(xctestrun), "-destination", f"id={udid}",
116
+ "-resultBundlePath", str(bundle_path)]
117
+ for item in only:
118
+ argv.append(f"-only-testing:{item}")
119
+ return argv
120
+
121
+
122
+ def run_tests(xctestrun, udid, folder, only, *, runner=None, progress=None):
123
+ """Run the built tests on simulator ``udid``; the result bundle goes under ``folder``/xctest/. The bundle's path,
124
+ or XctestError when xcodebuild stopped before running tests."""
125
+ progress = progress or (lambda line: None)
126
+ bundle = Path(folder) / BUNDLE_DIR / BUNDLE_NAME
127
+ bundle.parent.mkdir(parents=True, exist_ok=True)
128
+ progress("Running your XCTest targets")
129
+ code, output = (runner or run_command)(run_argv(xctestrun, udid, bundle, only), Path(folder),
130
+ bundle.parent / RUN_LOG, RUN_TIMEOUT)
131
+ if code not in (0, FAILURE_EXIT) or not (bundle / "Info.plist").is_file():
132
+ errors = [line.strip() for line in output.splitlines() if "error:" in line or "Testing failed" in line][:3]
133
+ detail = "; ".join(errors)[:500] if errors else f"xcodebuild exited {code}"
134
+ raise XctestError(f"The tests didn't run: {detail}.", f"Read {bundle.parent / RUN_LOG}, then run it again.")
135
+ return bundle
136
+
137
+
138
+ def _json(argv, runner):
139
+ code, output = (runner or run_command)(argv, Path.cwd(), None, READ_TIMEOUT)
140
+ start = output.find("{")
141
+ try:
142
+ data = json.JSONDecoder().raw_decode(output[start:])[0] if code == 0 and start >= 0 else None
143
+ except ValueError:
144
+ data = None
145
+ if not isinstance(data, dict):
146
+ raise XctestError("Mobster couldn't read the test results.", "Run `xcrun xcresulttool get test-results "
147
+ "summary --path <the .xcresult>` to see why; it needs Xcode 16 or later.")
148
+ return data
149
+
150
+
151
+ def read_bundle(bundle, *, runner=None):
152
+ """(tests, summary): xcresulttool's JSON for ``bundle``. XctestError when it can't be read."""
153
+ base = ["xcrun", "xcresulttool", "get", "test-results"]
154
+ tail = ["--path", str(bundle), "--format", "json"]
155
+ return _json(base + ["tests"] + tail, runner), _json(base + ["summary"] + tail, runner)
156
+
157
+
158
+ def _seconds(node):
159
+ value = node.get("durationInSeconds")
160
+ if isinstance(value, (int, float)):
161
+ return value
162
+ match = re.match(r"^\s*([0-9.]+)\s*(ms|s|m)\b", str(node.get("duration") or ""))
163
+ if not match:
164
+ return 0
165
+ return float(match.group(1)) * {"ms": 0.001, "s": 1, "m": 60}[match.group(2)]
166
+
167
+
168
+ def _failures(node):
169
+ """The case's failure messages, each as {"message", "file", "line"}."""
170
+ out = []
171
+ for child in node.get("children") or ():
172
+ if child.get("nodeType") == "Failure Message":
173
+ text = str(child.get("name") or "").strip()
174
+ found = LOCATION.match(text)
175
+ entry = {"message": (found.group("text") if found else text)[:MAX_MESSAGE], "file": None, "line": None}
176
+ if found:
177
+ entry["file"], entry["line"] = Path(found.group("file")).name, int(found.group("line"))
178
+ out.append(entry)
179
+ elif isinstance(child.get("children"), list):
180
+ out += _failures(child)
181
+ return out[:MAX_FAILURES]
182
+
183
+
184
+ def _cases(nodes, bundle=None, suites=()):
185
+ """Every Test Case under ``nodes`` as (bundle, suite names, node)."""
186
+ for node in nodes:
187
+ kind = str(node.get("nodeType") or "")
188
+ if kind == "Test Case":
189
+ yield bundle, suites, node
190
+ elif isinstance(node.get("children"), list):
191
+ name = str(node.get("name") or "")
192
+ yield from _cases(node["children"], name if "test bundle" in kind.lower() else bundle,
193
+ suites + (name,) if kind == "Test Suite" else suites)
194
+
195
+
196
+ def parse(tests, summary=None, device=None):
197
+ """The targets of an xcresulttool ``tests`` document as the suite's results model:
198
+ [{"name", "status", "tests", "passed", "failed", "skipped", "durationMs", "cases": [...]}]. A case has name,
199
+ classname, status (passed, failed or skipped), durationMs, failures [{message, file, line}] and attachments []."""
200
+ targets = {}
201
+ where = {} if device is None else {"device": device["name"], "deviceKey": device["key"]}
202
+ for bundle, suites, node in _cases(tests.get("testNodes") or ()):
203
+ target = targets.setdefault(bundle or "Tests", {"name": bundle or "Tests", "cases": []})
204
+ status = RESULTS.get(str(node.get("result") or "").lower(), "failed")
205
+ name = re.sub(r"\(\)$", "", str(node.get("name") or ""))
206
+ failures = _failures(node)
207
+ if status == "failed" and not failures:
208
+ failures = [{"message": "The test failed without a message.", "file": None, "line": None}]
209
+ target["cases"].append({
210
+ "id": str(node.get("nodeIdentifier") or "/".join(filter(None, [suites[-1] if suites else None, name]))),
211
+ "name": name, "classname": suites[-1] if suites else target["name"], "status": status,
212
+ "durationMs": round(_seconds(node) * 1000), "failures": failures, "attachments": [], **where})
213
+ out = []
214
+ for target in targets.values():
215
+ cases = sorted(target["cases"], key=lambda case: (case["status"] != "failed", case["classname"], case["name"]))
216
+ counts = {key: sum(case["status"] == key for case in cases) for key in ("passed", "failed", "skipped")}
217
+ out.append({"name": target["name"], "status": "failed" if counts["failed"] else "passed", "tests": len(cases),
218
+ **counts, "durationMs": sum(case["durationMs"] for case in cases), "cases": cases})
219
+ return out
220
+
221
+
222
+ def export_attachments(bundle, folder, targets, *, runner=None):
223
+ """Export the screenshots and files the tests attached into ``folder``/xctest/attachments/ and name them on their
224
+ cases (paths relative to ``folder``). Best effort: a test's verdict never depends on it."""
225
+ folder = Path(folder)
226
+ out_dir = folder / BUNDLE_DIR / ATTACHMENTS
227
+ (runner or run_command)(["xcrun", "xcresulttool", "export", "attachments", "--path", str(bundle),
228
+ "--output-path", str(out_dir)], folder, None, READ_TIMEOUT)
229
+ try:
230
+ manifest = json.loads((out_dir / "manifest.json").read_text(encoding="utf-8"))
231
+ except (OSError, ValueError):
232
+ return 0
233
+ wanted = {case["id"]: case for target in targets for case in target["cases"]}
234
+ count = 0
235
+ for entry in manifest if isinstance(manifest, list) else ():
236
+ case = wanted.get(str(entry.get("testIdentifier") or ""))
237
+ for item in entry.get("attachments") or ():
238
+ name = str(item.get("exportedFileName") or "")
239
+ if case is None or not name or not (out_dir / name).is_file() or Path(name).name != name:
240
+ continue
241
+ label = EXPORT_SUFFIX.sub("", str(item.get("suggestedHumanReadableName") or name))[:120]
242
+ case["attachments"].append({"name": label, "path": f"{BUNDLE_DIR}/{ATTACHMENTS}/{name}"})
243
+ count += 1
244
+ return count
245
+
246
+
247
+ def section(targets, *, scheme, test_plan, device, bundle=None, why=None):
248
+ """doc["xctest"]: the targets plus their totals, the device they ran on and why each target ran."""
249
+ totals = {key: sum(target[key] for target in targets) for key in ("tests", "passed", "failed", "skipped")}
250
+ status = "failed" if totals["failed"] else "passed"
251
+ return {"status": status, "scheme": scheme, "testPlan": test_plan, "device": device["name"],
252
+ "deviceKey": device["key"], "resultBundle": bundle, "why": dict(why or {}), "summary": totals,
253
+ "durationMs": sum(target["durationMs"] for target in targets), "targets": targets}
254
+
255
+
256
+ def milliseconds(ms):
257
+ """"38 ms", "1.2 s": a test's time."""
258
+ return f"{ms} ms" if ms < 1000 else f"{ms / 1000:.1f} s"
259
+
260
+
261
+ def couldnt_run(error, *, scheme=None, test_plan=None, why=None):
262
+ return {"status": "couldnt_run", "scheme": scheme, "testPlan": test_plan, "reason": {
263
+ "message": error.message, "fix": error.fix}, "why": dict(why or {}), "summary": {
264
+ "tests": 0, "passed": 0, "failed": 0, "skipped": 0}, "durationMs": 0, "targets": []}
265
+
266
+
267
+ def words(found):
268
+ """"XCTest 3 of 4 passed", "XCTest 1 failed of 4", "XCTest couldn't run"."""
269
+ totals = found.get("summary") or {}
270
+ if found.get("status") == "couldnt_run":
271
+ return "XCTest couldn't run"
272
+ if totals.get("failed"):
273
+ return f"XCTest {totals['failed']} failed of {totals['tests']}"
274
+ return f"XCTest {totals.get('passed', 0)} of {totals.get('tests', 0)} passed"
275
+
276
+
277
+ def failed_cases(doc):
278
+ """(target name, case) for every failed XCTest case in a suite's results document."""
279
+ return [(target["name"], case) for target in (doc.get("xctest") or {}).get("targets") or ()
280
+ for case in target.get("cases") or () if case.get("status") == "failed"]
281
+
282
+
283
+ def failure_line(case):
284
+ """"PlanTests.swift:14: XCTAssertEqual failed ..." for a failed case."""
285
+ first = (case.get("failures") or [{}])[0]
286
+ where = f"{first['file']}:{first['line']}: " if first.get("file") else ""
287
+ return (where + str(first.get("message") or "failed")).strip()
288
+
289
+
290
+ # The worst outcome decides the exit code: failed, couldn't run, review, passed.
291
+ EXIT_RANK = {1: 0, 3: 1, 2: 2, 0: 3}
292
+
293
+
294
+ def merge_summary(doc):
295
+ """Fold the XCTest result into the suite's summary and exit code (130, a stopped run, stays)."""
296
+ found = doc.get("xctest")
297
+ if not found:
298
+ return doc
299
+ summary = doc.setdefault("summary", {})
300
+ summary["xctest"] = dict(found["summary"], status=found["status"])
301
+ own = {"failed": 1, "couldnt_run": 3}.get(found["status"], 0)
302
+ current = doc.get("exitCode")
303
+ if current in EXIT_RANK and EXIT_RANK[own] < EXIT_RANK[current]:
304
+ doc["exitCode"] = own
305
+ elif current is None:
306
+ doc["exitCode"] = own
307
+ return doc
308
+
309
+
310
+ def run_suite(planned, attempts, folder, progress, *, runner=None):
311
+ """Build for testing (unless `test pr` did), run the targets on a simulator leased from ``attempts`` and return
312
+ doc["xctest"].
313
+
314
+ The simulator is the first simulator target of the run (the lease the checks just used is free again, so a booted
315
+ one is reused); a run of iPhones only has none, and says so."""
316
+ options = planned.options
317
+ labels = {"scheme": options.xctest_scheme, "test_plan": options.xctest_plan, "why": {}}
318
+ built = planned.xctest_build
319
+ if built is None:
320
+ try:
321
+ built = prepare(planned, progress, runner=runner)
322
+ except BuildError as error:
323
+ return couldnt_run(XctestError(error.message, error.fix), **labels)
324
+ chosen, why = select_targets(built.get("targets") or [], planned.changed, tuple(options.xctest_only))
325
+ labels = {"scheme": built.get("scheme"), "test_plan": options.xctest_plan, "why": why}
326
+ target = next((t for t in planned.targets if t.kind in ("default", "simulator") and t.record is None), None)
327
+ if target is None:
328
+ return couldnt_run(XctestError("--xctest runs on a simulator, and this run names only iPhones.",
329
+ "Add --sim, or run the XCTest targets in another run."), **labels)
330
+ if not chosen:
331
+ return couldnt_run(XctestError("None of the scheme's test targets matches --only-testing.",
332
+ f"The scheme's targets are {', '.join(built.get('targets') or []) or 'none'}."),
333
+ **labels)
334
+ only = [item for item in options.xctest_only if item.split("/", 1)[0] in chosen]
335
+ if not only and set(chosen) != set(built.get("targets") or []):
336
+ only = list(chosen) # the targets a changed file picked
337
+ try:
338
+ lease = attempts.simulators().acquire(target.device_type, target.runtime)
339
+ except SimError as error:
340
+ return couldnt_run(XctestError(f"No simulator was free for the tests: {error}",
341
+ error.fix or "Run `mobster sim doctor`."), **labels)
342
+ try:
343
+ sim = lease.target
344
+ device = {"name": sim.name, "key": f"sim:{sim.device_type}@{sim.runtime}"}
345
+ try:
346
+ bundle = run_tests(built["xctestrun"], sim.udid, folder, only, runner=runner, progress=progress)
347
+ tests, summary = read_bundle(bundle, runner=runner)
348
+ except XctestError as error:
349
+ return couldnt_run(error, **labels)
350
+ targets = parse(tests, summary, device)
351
+ if not targets:
352
+ return couldnt_run(XctestError("The scheme ran no tests.", "Check --only-testing and the scheme's Test "
353
+ "action."), **labels)
354
+ export_attachments(bundle, folder, targets, runner=runner)
355
+ return section(targets, scheme=built.get("scheme"), test_plan=options.xctest_plan, device=device,
356
+ bundle=Path(bundle).relative_to(folder).as_posix(), why=why)
357
+ finally:
358
+ lease.release()
359
+
360
+
361
+ def text_lines(doc, paint):
362
+ """One line per failed XCTest case, with its file and line, and the XCTest totals."""
363
+ found = doc.get("xctest")
364
+ if not found:
365
+ return []
366
+ lines = [f"{paint('✗ failed', 'failed')} {target}: {case['classname']}.{case['name']}\n "
367
+ f"{failure_line(case)}" for target, case in failed_cases(doc)]
368
+ totals = found["summary"]
369
+ if found["status"] == "couldnt_run":
370
+ reason = found.get("reason") or {}
371
+ lines.append(f"{paint('✗ couldn\'t run', 'couldnt_run')} your XCTest targets\n "
372
+ f"{reason.get('message')} {reason.get('fix')}".rstrip())
373
+ else:
374
+ lines.append(f"XCTest: {totals['passed']} of {totals['tests']} passed"
375
+ + (f", {totals['failed']} failed" if totals["failed"] else "")
376
+ + (f", {totals['skipped']} skipped" if totals["skipped"] else "")
377
+ + f" in {len(found['targets'])} target{'' if len(found['targets']) == 1 else 's'}")
378
+ return lines
@@ -0,0 +1 @@
1
+ """Track `conversations`."""
@@ -0,0 +1,264 @@
1
+ """`mobster chat "message"` and `mobster chat --workflow`: the scripted path."""
2
+
3
+ import json
4
+ import shlex
5
+ import time
6
+
7
+ from ..narrate import approval_title, completion_message, status_label
8
+ from .client import NoService, ServiceError
9
+ from .recent import remember_thread
10
+
11
+ EXIT_DONE, EXIT_NOT_DONE, EXIT_NEEDS_YOU, EXIT_COULDNT, EXIT_RUNNING = 0, 1, 2, 3, 4
12
+ DONE = frozenset({"completed", "completed_unverified", "user_condition_met"})
13
+
14
+
15
+ def outcome(run, thread_id):
16
+ summary = run.get("summary") or {}
17
+ data = summary.get("data")
18
+ out = {"threadId": thread_id, "runId": run.get("id"), "status": run.get("status"),
19
+ "outcome": summary.get("outcome"), "answer": summary.get("answer"),
20
+ "costUsd": run.get("costUsd"), "elapsedMs": summary.get("elapsed_ms")}
21
+ if data is not None:
22
+ out["data"] = data
23
+ if run.get("status") not in DONE and summary.get("reason"):
24
+ out["reason"] = summary["reason"]
25
+ return out
26
+
27
+
28
+ def waiting(run, thread_id):
29
+ approval = run.get("approval") or {}
30
+ if approval.get("kind") == "clarify":
31
+ return {"threadId": thread_id, "runId": run.get("id"), "status": "waiting_for_answer",
32
+ "question": approval.get("question") or approval.get("label"),
33
+ "choices": [c.get("label") for c in approval.get("choices") or ()]}
34
+ out = {"threadId": thread_id, "runId": run.get("id"), "status": "waiting_for_approval",
35
+ "approval": {"title": approval_title(approval), "text": approval.get("text"), "app": approval.get("app")}}
36
+ return out
37
+
38
+
39
+ def once(client, chat, text, *, as_json, wait, out, keep=False):
40
+ """`mobster chat "message"`."""
41
+ tty = chat.interactive
42
+ if tty:
43
+ response = chat.send(text) # prints the task's first line and follows it
44
+ if response is None:
45
+ return EXIT_COULDNT
46
+ else:
47
+ body = {"text": text, **({"device": chat.device} if chat.device else {})}
48
+ try:
49
+ chat.thread_id, response = client.send(chat.thread_id, body)
50
+ except ServiceError as error:
51
+ if as_json:
52
+ print(json.dumps({"status": "refused", "error": str(error), "code": error.code}), file=out,
53
+ flush=True)
54
+ else:
55
+ chat.say(str(error))
56
+ return EXIT_COULDNT
57
+ remember_thread(client.url, chat.thread_id)
58
+ routed = routed_as(response)
59
+ if routed == "remember":
60
+ return offered(chat, response, tty=tty, as_json=as_json, wait=wait, out=out)
61
+ run_id = (response.get("run") or {}).get("id") or (response.get("item") or {}).get("runId")
62
+ if routed in ("steer", "answer"):
63
+ if as_json:
64
+ print(json.dumps({"threadId": chat.thread_id, "runId": run_id, "routed": routed}), file=out, flush=True)
65
+ else:
66
+ chat.say(" Mobster has your message; the task goes on. Follow it with: mobster chat --thread "
67
+ f"{chat.thread_id}", "faint")
68
+ return EXIT_DONE
69
+ if not tty:
70
+ # --json, or no terminal: never answers an approval; waits until the task ends or needs someone (with
71
+ # --keep-waiting, until it ends: someone answers in Mobster).
72
+ return finish(client.wait_run(run_id, wait, through=keep), chat.thread_id, as_json, out, chat)
73
+ return follow_here(chat, run_id, wait, out)
74
+
75
+
76
+ def follow_here(chat, run_id, wait, out):
77
+ """On a terminal: the steps as they happen; an approval waits for a typed y, n or x. Returns the exit code."""
78
+ deadline = time.monotonic() + wait
79
+ term = chat.term
80
+ try:
81
+ while time.monotonic() < deadline:
82
+ chat.pump(0.1)
83
+ if bool(chat.pending) != term.prompting:
84
+ term.clear()
85
+ term.prompting = bool(chat.pending)
86
+ term.show()
87
+ done = chat.finished.get(run_id)
88
+ if done is not None:
89
+ return finish(done, chat.thread_id, False, out, chat, printed=True)
90
+ if chat.pending and chat.pending.get("kind") == "clarify":
91
+ chat.say(f" Answer with: mobster chat --thread {chat.thread_id} \"your answer\"", "faint")
92
+ return EXIT_NEEDS_YOU
93
+ if chat.pending:
94
+ chat.feed(chat.term.keys(0.1) or "")
95
+ except KeyboardInterrupt:
96
+ chat.stop()
97
+ return 130
98
+ chat.say(f" Still working. Follow it with: mobster chat --thread {chat.thread_id}", "faint")
99
+ return EXIT_RUNNING
100
+
101
+
102
+ def routed_as(response):
103
+ """How Mobster took a message: new_run, steer, answer or remember."""
104
+ return response.get("routed") or (response.get("item") or {}).get("routed") or "new_run"
105
+
106
+
107
+ def proposals_of(client, thread_id, response):
108
+ """The suggestions to remember a message made, [{id, text, pin}]. Where the answer leaves them out (that older
109
+ Mobster), the conversation's pending ones."""
110
+ if "proposals" in response:
111
+ return response.get("proposals") or []
112
+ from urllib.parse import quote
113
+ try:
114
+ listed = client.call("GET", f"/api/memory/proposals?thread={quote(str(thread_id), safe='')}")["proposals"]
115
+ except (ServiceError, NoService, KeyError, TypeError):
116
+ return []
117
+ return [{"id": p.get("id"), "text": p.get("text"), "scope": p.get("scope") or "global",
118
+ "pin": bool(p.get("pin"))} for p in listed
119
+ if isinstance(p, dict)]
120
+
121
+
122
+ def where_to_remember(proposal):
123
+ scope = proposal.get("scope") or "global"
124
+ command = ["mobster", "memory", "add", str(proposal.get("text") or "")]
125
+ if scope != "global":
126
+ command.extend(("--scope", scope))
127
+ return "Answer it in Mobster for Mac, or save it with: " + shlex.join(command)
128
+
129
+
130
+ def offered(chat, response, *, tty, as_json, wait, out):
131
+ """A message that only asked Mobster to remember something: no task started, so there's no run to follow."""
132
+ if not tty:
133
+ proposals = proposals_of(chat.client, chat.thread_id, response)
134
+ if as_json:
135
+ print(json.dumps({"threadId": chat.thread_id, "status": "remember", "proposals": proposals},
136
+ ensure_ascii=False), file=out, flush=True)
137
+ else:
138
+ chat.offer(proposals)
139
+ return EXIT_DONE
140
+ # chat.send showed the first suggestion already.
141
+ term, deadline = chat.term, time.monotonic() + wait
142
+ try:
143
+ while chat.offers and not chat.quit and time.monotonic() < deadline:
144
+ if not term.prompting:
145
+ term.prompting = True
146
+ term.show()
147
+ data = term.keys(0.1)
148
+ if data is None:
149
+ break
150
+ chat.feed(data)
151
+ except KeyboardInterrupt:
152
+ return 130
153
+ for proposal in chat.offers:
154
+ chat.say(" " + where_to_remember(proposal), "faint")
155
+ chat.offers = []
156
+ return EXIT_DONE
157
+
158
+
159
+ def finish(run, thread_id, as_json, out, chat, printed=False):
160
+ if run.get("finishedAt") is None:
161
+ if run.get("approval"):
162
+ state = waiting(run, thread_id)
163
+ if as_json:
164
+ print(json.dumps(state, ensure_ascii=False), file=out, flush=True)
165
+ else:
166
+ if state["status"] == "waiting_for_answer":
167
+ chat.note(chat.paint(str(state["question"]), "bold"), glyph="?", role="accent", indent="")
168
+ chat.say(f" Answer with: mobster chat --thread {thread_id} \"your answer\"", "faint")
169
+ else:
170
+ chat.show_approval(run["approval"])
171
+ chat.note("Waiting for your approval in Mobster (or in `mobster chat` on a terminal).")
172
+ return EXIT_NEEDS_YOU
173
+ if as_json:
174
+ print(json.dumps({"threadId": thread_id, "runId": run.get("id"), "status": "running"}), file=out,
175
+ flush=True)
176
+ else:
177
+ chat.say(f"Still working. Follow it with: mobster chat --thread {thread_id}", "faint")
178
+ return EXIT_RUNNING
179
+ result = outcome(run, thread_id)
180
+ if as_json:
181
+ print(json.dumps(result, ensure_ascii=False, allow_nan=False), file=out, flush=True)
182
+ elif not printed:
183
+ chat.say(status_label(result["status"]), "bold")
184
+ message = result.get("data") if isinstance(result.get("data"), str) else result.get("answer")
185
+ if message:
186
+ chat.say(str(message))
187
+ elif result.get("reason"):
188
+ chat.say(completion_message(result["status"], result["reason"], summary=result))
189
+ return EXIT_DONE if result["status"] in DONE else EXIT_NOT_DONE
190
+
191
+
192
+ def blank_values(pairs):
193
+ """{"topic": "basketball"} from ["topic=basketball", ...]. ValueError for a pair without "="."""
194
+ values = {}
195
+ for pair in pairs or ():
196
+ name, sep, value = pair.partition("=")
197
+ if not sep or not name.strip():
198
+ raise ValueError(f"Give each blank as NAME=VALUE, like --set topic=basketball (not {pair!r})")
199
+ values[name.strip()] = value.strip()
200
+ return values
201
+
202
+
203
+ def follow_graph(client, workflow, batch, thread_id, *, as_json, wait, out):
204
+ """A workflow run as a graph: each step's state as it changes, then how the run ended (with --json, one object
205
+ with every step). 0 when it completed, 1 when it didn't, 4 while it still runs."""
206
+ def changed(node):
207
+ if as_json or str(node.get("type", "")).startswith("trigger."):
208
+ return
209
+ line = f"{node.get('instance')}: {node.get('state')}"
210
+ if node.get("reason") and node.get("state") in ("failed", "blocked", "skipped", "unknown", "cancelled"):
211
+ line += f" ({node['reason']})"
212
+ out.write(line + "\n")
213
+ ended = client.wait_batch(workflow["id"], batch["id"], wait, on_node=changed) or batch
214
+ status = ended.get("status")
215
+ if as_json:
216
+ nodes = {key: {k: node[k] for k in ("state", "reason", "output", "runId") if k in node}
217
+ for key, node in (ended.get("nodes") or {}).items()}
218
+ out.write(json.dumps({"workflow": workflow["id"], "threadId": thread_id, "batch": ended.get("id"),
219
+ "status": status, "reason": ended.get("reason"), "result": ended.get("result"),
220
+ "nodes": nodes}) + "\n")
221
+ else:
222
+ out.write(f"“{workflow['name']}” {str(status).replace('_', ' ')}." +
223
+ (f" {ended['reason']}" if ended.get("reason") else "") + "\n")
224
+ return 0 if status == "completed" else 4 if status == "running" else 1
225
+
226
+
227
+ def workflow_once(client, chat, args, *, as_json, wait, out, fail):
228
+ """`mobster chat --workflow NAME [--set BLANK=VALUE]...`: start it, then follow it as one message."""
229
+ try:
230
+ values = blank_values(args.set)
231
+ workflow = client.find_workflow(args.workflow)
232
+ except ValueError as error:
233
+ return fail(str(error), "usage")
234
+ except LookupError as error:
235
+ return fail(str(error), "not_found")
236
+ try:
237
+ thread_id, answer = client.run_workflow(workflow["id"], values=values, thread_id=chat.thread_id,
238
+ new_conversation=bool(args.new))
239
+ except ServiceError as error:
240
+ return fail(str(error), error.code or "refused")
241
+ run = answer.get("run") or {}
242
+ chat.thread_id = thread_id
243
+ if thread_id:
244
+ remember_thread(client.url, thread_id)
245
+ batch = answer.get("batch") or {}
246
+ if batch.get("graph") == 2 and not chat.interactive:
247
+ return follow_graph(client, workflow, batch, thread_id, as_json=as_json, wait=wait, out=out)
248
+ if not run.get("id"):
249
+ # A workflow of steps that waits before its first task: nothing to follow yet. Its conversation has each step.
250
+ batch = answer.get("batch") or {}
251
+ if as_json:
252
+ out.write(json.dumps({"workflow": workflow["id"], "threadId": thread_id, "batch": batch.get("id"),
253
+ "status": batch.get("status")}) + "\n")
254
+ else:
255
+ out.write(f"Started “{workflow['name']}”. It waits before its first task; its conversation shows each "
256
+ "step.\n")
257
+ return 0
258
+ if not chat.interactive:
259
+ keep = bool(getattr(args, "keep_waiting", False))
260
+ return finish(client.wait_run(run["id"], wait, through=keep), thread_id, as_json, out, chat)
261
+ where = run.get("deviceName")
262
+ chat.renderer(run["id"]).start(run.get("goal") or workflow["name"], f"on {where}" if where else "")
263
+ chat.follow(run["id"])
264
+ return follow_here(chat, run["id"], wait, out)