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,948 @@
1
+ """`mobster test`: run a project's checks on simulators and iPhones, with retries, flake detection and reports."""
2
+
3
+ import json
4
+ import os
5
+ import shlex
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from ..paths import write_text_atomic
10
+ from ..verify.checks import LIMITS
11
+ from ..verify.report import dots
12
+ from . import repro, snapshots_cli, xctest
13
+ from .cli_arguments import USAGE_EXIT, add_arguments
14
+
15
+ RECORD = "record"
16
+ IMPORT = "import"
17
+ IMPORT_SHOWN = 8
18
+ ADD = "add"
19
+ PR = "pr"
20
+ PR_FLAGS = (("base", "--base"), ("no_build", "--no-build"), ("scheme", "--scheme"), ("project", "--project"),
21
+ ("drafts", "--drafts"), ("no_gifs", "--no-gifs"), ("tested_with", "--tested-with"),
22
+ ("comment", "--comment"), ("github", "--github"))
23
+ HEAL = "heal"
24
+ NIGHTLY = "nightly"
25
+ SCHEDULE = "schedule"
26
+ CAPSULE = "capsule"
27
+ COMPARE = "compare"
28
+ SCENARIO = "scenario"
29
+ MINIMIZE = "minimize"
30
+ LOCAL_RESULTS = {
31
+ "sim": None, "device": None, "testflight": None, "matrix": None, "locale": None,
32
+ "spread": False, "parallel": 1, "retries": None, "repeat": 1, "strict": False,
33
+ "quarantine": None, "auto_quarantine": False, "tag": None, "audit": None,
34
+ "a11y": None, "update_a11y_baseline": False, "shard": None, "keyless": False,
35
+ "replay": False, "fresh": False, "no_heal": False, "junit": None, "html": None,
36
+ "video": False, "history": False, "share": False, "share_days": 30,
37
+ "assert_timeout": 5, "record_run": None, "record_name": None,
38
+ "bundle": None, "app": None, "skip_unsupported": False, "dry_run": False,
39
+ "force": False, "add_list": False, "add_set": None, "base": None,
40
+ "no_build": False, "scheme": None, "project": None, "drafts": "run",
41
+ "no_gifs": False, "tested_with": False, "comment": None, "github": "check",
42
+ "at": None, "accept": False, "reject": False,
43
+ }
44
+ REPRO = "repro"
45
+ TONES = {"passed": "green", "failed": "coral", "needs_review": "accent", "couldnt_run": "tertiary", "flaky": "amber",
46
+ "draft": "accent"}
47
+
48
+
49
+ def _paint(color):
50
+ from ..style import Palette
51
+ return Palette(color)
52
+
53
+
54
+ def _pr_flags(args):
55
+ """The first `mobster test pr` flag given without pr, or None."""
56
+ for name, flag in PR_FLAGS:
57
+ if name in ("scheme", "project") and getattr(args, "xctest", False):
58
+ continue
59
+ value = getattr(args, name, None)
60
+ if value and not (name == "drafts" and value == "run") and not (name == "github" and value == "check"):
61
+ return flag
62
+ return None
63
+
64
+
65
+ def _isatty(stream):
66
+ try:
67
+ return stream.isatty()
68
+ except (AttributeError, ValueError):
69
+ return False
70
+
71
+
72
+ def _only_testing(args, options):
73
+ """Set options.xctest_only from --only-testing; True (after saying why) when one isn't a test identifier."""
74
+ try:
75
+ options.xctest_only = xctest.only_testing_ok(getattr(args, "only_testing", None) or ())
76
+ except xctest.XctestError as error:
77
+ _usage(f"{error.message} {error.fix}")
78
+ return True
79
+ return False
80
+
81
+
82
+ def _usage(message):
83
+ print(f"mobster test: {message}", file=sys.stderr, flush=True)
84
+ return USAGE_EXIT
85
+
86
+
87
+ def run(args):
88
+ """`mobster test` for parsed ``args``; returns the exit code. Never raises into __main__."""
89
+ from ..devtools import signals_as_interrupt
90
+ paths = list(getattr(args, "paths", None) or ())
91
+ if (getattr(args, "minimize_bad_app", None) or getattr(args, "minimize_control_app", None) or
92
+ getattr(args, "minimize_isolated", False) or getattr(args, "minimize_max_attempts", None) is not None) \
93
+ and (not paths or paths[0] != MINIMIZE):
94
+ return _usage("--bad-app, --control-app, --isolated and --max-attempts go with mobster test minimize")
95
+ if getattr(args, "capsule_include", None) and (not paths or paths[0] != CAPSULE):
96
+ return _usage("--include goes with capsule export: mobster test capsule export SUITE_ID --include KIND")
97
+ if paths and paths[0] in (CAPSULE, COMPARE, SCENARIO, MINIMIZE):
98
+ command = paths[0]
99
+ for name, default in LOCAL_RESULTS.items():
100
+ if getattr(args, name, default) != default:
101
+ return _usage(f"--{name.replace('_', '-')} doesn't go with mobster test {command}")
102
+ if command != CAPSULE and getattr(args, "record_output", None):
103
+ return _usage(f"--output doesn't go with mobster test {command}")
104
+ if command == SCENARIO:
105
+ from . import scenario_cli
106
+ with signals_as_interrupt():
107
+ try:
108
+ return scenario_cli.run(args, paths[1:])
109
+ except KeyboardInterrupt:
110
+ print("mobster test scenario: stopped", file=sys.stderr, flush=True)
111
+ return 130
112
+ except Exception as error:
113
+ print(f"mobster test scenario: unexpected error ({type(error).__name__}: {error}). "
114
+ "Report the command you ran.", file=sys.stderr, flush=True)
115
+ return USAGE_EXIT
116
+ if command == MINIMIZE:
117
+ from . import minimizer_cli
118
+ with signals_as_interrupt():
119
+ try:
120
+ return minimizer_cli.run(args, paths[1:])
121
+ except KeyboardInterrupt:
122
+ print("mobster test minimize: stopped; review the saved trial before another attempt",
123
+ file=sys.stderr, flush=True)
124
+ return 130
125
+ except Exception as error:
126
+ print(f"mobster test minimize: unexpected error ({type(error).__name__}). "
127
+ "Review the saved trial before another attempt.", file=sys.stderr, flush=True)
128
+ return USAGE_EXIT
129
+ from . import capsule_cli, comparison_cli
130
+ return (capsule_cli if command == CAPSULE else comparison_cli).run(args, paths[1:])
131
+ simple = {RECORD: record, IMPORT: import_flows, ADD: add, snapshots_cli.SNAPSHOTS: snapshots_cli.snapshots,
132
+ REPRO: repro.main, NIGHTLY: nightly, SCHEDULE: schedule} # each takes (args, the rest of the paths)
133
+ if paths and paths[0] in simple:
134
+ return simple[paths[0]](args, paths[1:])
135
+ if paths and paths[0] == PR:
136
+ with signals_as_interrupt():
137
+ try:
138
+ return pr(args, paths[1:])
139
+ except KeyboardInterrupt:
140
+ print("mobster test: stopped", file=sys.stderr, flush=True)
141
+ return 130
142
+ except Exception as error: # a last resort, as for the suite
143
+ print(f"mobster test pr: unexpected error ({type(error).__name__}: {error}). Report it at "
144
+ "github.com/RadishSoftware/mobster/issues with the command you ran.", file=sys.stderr, flush=True)
145
+ return USAGE_EXIT
146
+ if getattr(args, "history", False):
147
+ return history(args, paths)
148
+ if (getattr(args, "test_plan", None) or getattr(args, "only_testing", None)) and not getattr(args, "xctest", False):
149
+ return _usage("--test-plan and --only-testing go with --xctest: mobster test --xctest --test-plan NAME")
150
+ if getattr(args, "at", None):
151
+ return _usage("--at goes with nightly: mobster test nightly on --at 02:00")
152
+ stray = _pr_flags(args)
153
+ if stray:
154
+ return _usage(f"{stray} goes with pr: mobster test pr {stray} …")
155
+ if paths and paths[0] == HEAL:
156
+ return heal(args, paths[1:])
157
+ if getattr(args, "accept", False) or getattr(args, "reject", False):
158
+ return _usage("--accept and --reject go with heal: mobster test heal --accept CHECK")
159
+ if getattr(args, "fresh", False) and (getattr(args, "no_heal", False) or getattr(args, "replay", False)):
160
+ return _usage("--fresh records the path again, so it doesn't go with --replay or --no-heal")
161
+ if getattr(args, "keyless", False) and (getattr(args, "fresh", False) or getattr(args, "no_heal", False)):
162
+ return _usage("--keyless never runs Smart, so --fresh and --no-heal don't apply")
163
+ if getattr(args, "record_run", None) or getattr(args, "record_name", None) or getattr(args, "record_output", None):
164
+ return _usage("--run, --name and -o go with record: mobster test record --run RUN_ID")
165
+ if getattr(args, "repro_arg", None) or getattr(args, "link", None):
166
+ return _usage("--arg and --link go with repro: mobster test repro CRASH_FILE --link myapp://cart")
167
+ stray = next((flag for flag, name in (("--bundle", "bundle"), ("--app", "app"),
168
+ ("--skip-unsupported", "skip_unsupported"), ("--dry-run", "dry_run"),
169
+ ("--force", "force"))
170
+ if getattr(args, name, None)), None)
171
+ if stray:
172
+ return _usage(f"{stray} goes with import: mobster test import maestro PATH {stray}")
173
+ if getattr(args, "add_list", None) or getattr(args, "add_set", None):
174
+ return _usage("--list and --set go with add: mobster test add RECIPE")
175
+ with signals_as_interrupt():
176
+ try:
177
+ return suite(args, paths)
178
+ except KeyboardInterrupt:
179
+ print("mobster test: stopped", file=sys.stderr, flush=True)
180
+ return 130
181
+ except Exception as error: # a last resort: never a traceback, never "failed" (exit 1)
182
+ print(f"mobster test: unexpected error ({type(error).__name__}: {error}). Report it at "
183
+ "github.com/RadishSoftware/mobster/issues with the command you ran.", file=sys.stderr, flush=True)
184
+ return USAGE_EXIT
185
+
186
+
187
+ def _command(args):
188
+ return "mobster test " + " ".join(shlex.quote(item) for item in sys.argv[2:]) if len(sys.argv) > 2 and \
189
+ sys.argv[1] == "test" else "mobster test"
190
+
191
+
192
+ def suite(args, paths, *, execute=None, plan=None, out=None, err=None):
193
+ from .api import Options, UsageError, parse_locales
194
+ out, err = out or sys.stdout, err or sys.stderr
195
+ options = Options(paths=tuple(paths), sims=tuple(args.sim or ()), devices=tuple(args.device or ()),
196
+ matrix=args.matrix, parallel=args.parallel, retries=args.retries, repeat=args.repeat,
197
+ quarantine=args.quarantine, tags=tuple(args.tag or ()), shard=args.shard, junit=args.junit,
198
+ html=args.html, strict=args.strict, keyless=args.keyless, video=args.video,
199
+ assert_timeout=args.assert_timeout, command=_command(args),
200
+ testflight=tuple(getattr(args, "testflight", None) or ()),
201
+ audit=tuple(getattr(args, "audit", None) or ()), a11y=getattr(args, "a11y", None),
202
+ android=getattr(args, "android", None), **snapshots_cli.options(args),
203
+ **{flag: bool(getattr(args, flag, False)) for flag in (
204
+ "fresh", "no_heal", "update_a11y_baseline", "spread", "auto_quarantine", "xctest")},
205
+ xctest_scheme=getattr(args, "scheme", None), xctest_project=getattr(args, "project", None),
206
+ xctest_plan=getattr(args, "test_plan", None))
207
+ if _only_testing(args, options):
208
+ return USAGE_EXIT
209
+ try:
210
+ options.locales = parse_locales(getattr(args, "locale", None))
211
+ except UsageError as error:
212
+ return _usage(str(error))
213
+ if options.testflight and not options.devices:
214
+ return _usage("--testflight goes with --device: a TestFlight build is on an iPhone, not a simulator")
215
+ if plan is None:
216
+ from .api import plan
217
+ if execute is None:
218
+ from .api import execute
219
+ try:
220
+ planned = plan(options)
221
+ except UsageError as error:
222
+ return _usage(str(error))
223
+ try:
224
+ from ..devtools import ProgressLines
225
+ progress = ProgressLines(err)
226
+ except ImportError:
227
+ def progress(line):
228
+ print(line, file=err, flush=True)
229
+ for note in planned.notes:
230
+ progress(note)
231
+ runs = len(planned.entries) * len(planned.targets) * options.repeat
232
+ if not runs and not options.xctest:
233
+ print("No checks to run.", file=out, flush=True)
234
+ return 0
235
+ progress(f"mobster test: {len(planned.entries)} check{'' if len(planned.entries) == 1 else 's'} on "
236
+ f"{_targets(planned.targets)}" + (f", {options.repeat} times each" if options.repeat > 1 else "")
237
+ + (", each on whichever is free" if options.spread else "")
238
+ + (f", {options.parallel} at a time" if options.parallel > 1 else "")
239
+ + (", then your XCTest targets" if options.xctest else ""))
240
+ try:
241
+ doc, report_paths = execute(planned, progress=progress)
242
+ finally:
243
+ getattr(progress, "end", lambda: None)()
244
+ shared = None
245
+ if getattr(args, "share", False) and report_paths.get("folder"):
246
+ shared = _share(report_paths["folder"], args.share_days, err)
247
+ _tell_slack(planned.root, report_paths, doc, shared, err)
248
+ if args.json:
249
+ try:
250
+ print(json.dumps(doc, ensure_ascii=False, allow_nan=False), file=out, flush=True)
251
+ except BrokenPipeError:
252
+ os.dup2(os.open(os.devnull, os.O_WRONLY), out.fileno())
253
+ else:
254
+ from ..style import color_enabled
255
+ print(summary_text(doc, report_paths, color=color_enabled(out)), file=out, flush=True)
256
+ return int(doc.get("exitCode", USAGE_EXIT))
257
+
258
+
259
+ def _share(folder, days, err):
260
+ """Upload the suite's report, sealed (teams/report_share.py), and say its link; or say why not."""
261
+ try:
262
+ from ..teams import report_share
263
+ except ModuleNotFoundError as error:
264
+ if error.name != "mobile_agent.teams":
265
+ raise
266
+ print(f"mobster test: sharing reports needs the Mobster app's team extension. Your report is in {folder}.",
267
+ file=err, flush=True)
268
+ return None
269
+ try:
270
+ shared = report_share.share_folder(folder, days=days)
271
+ except report_share.ShareRefused as refused:
272
+ print(f"mobster test: {refused}", file=err, flush=True)
273
+ return None
274
+ except (OSError, ValueError):
275
+ # The error could name a file, and a file's path names a check or a task: say what happened, not the path.
276
+ print(f"mobster test: the report wasn't shared. Your report is in {folder}.", file=err, flush=True)
277
+ return None
278
+ print(f"Shared with your team for {days} days: {shared.link}", file=err, flush=True)
279
+ return shared
280
+
281
+
282
+ def _targets(targets):
283
+ if len(targets) == 1:
284
+ return targets[0].label if targets[0].kind != "default" else "each check's simulator"
285
+ return f"{len(targets)} devices"
286
+
287
+
288
+ def _relative(path, root=None):
289
+ if not path:
290
+ return path
291
+ try:
292
+ return str(Path(path).resolve().relative_to(Path(root or Path.cwd()).resolve()))
293
+ except ValueError:
294
+ return str(path)
295
+
296
+
297
+ def schedule(args, rest, *, cwd=None, out=None, err=None, schedules=None):
298
+ """`mobster test schedule [list | run-now NAME | device NAME DEVICE|none]`: the scheduled health checks."""
299
+ import time
300
+
301
+ from ..verify.checks import project_root
302
+ from . import health
303
+ out, err = out or sys.stdout, err or sys.stderr
304
+ cwd = Path(cwd or Path.cwd())
305
+ root = project_root(cwd)
306
+ verb = rest[0] if rest else "list"
307
+ if verb == "list":
308
+ found = health.listing(root, round(time.time() * 1000))
309
+ if args.json:
310
+ print(json.dumps(found, ensure_ascii=False, allow_nan=False), file=out, flush=True)
311
+ return 0
312
+ for problem in found["problems"]:
313
+ print(f"mobster test: {problem}", file=err, flush=True)
314
+ if not found["schedules"]:
315
+ print("No scheduled checks. Add `schedule: every 1h` to a check, or list schedules in "
316
+ ".mobster/schedule.yaml.", file=out, flush=True)
317
+ return 0
318
+ for row in found["schedules"]:
319
+ last = row["last"]
320
+ ended = "never ran" if last is None else f"last {last['outcome']}" + (
321
+ f" {last['verdict']}" if last["verdict"] else "") + (f" ({last['reason']})" if last["reason"] else "")
322
+ target = f"phone {row['device']}" if row["device"] else row["sim"] or "each check's simulator"
323
+ nxt = time.strftime("%a %H:%M", time.localtime(row["nextAt"] / 1000)) if row["nextAt"] else "-"
324
+ nxt = f"next {nxt}" if row["on"] else "off on this Mac"
325
+ print(f"{row['name']:<24} {row['when']:<14} {nxt} on {target} {ended}", file=out)
326
+ print(file=out, flush=True)
327
+ return 0
328
+ if verb in ("run-now", "device") and len(rest) < 2:
329
+ return _usage(f"{verb} needs the schedule's name: mobster test schedule {verb} NAME")
330
+ specs, problems = health.load(root)
331
+ spec = next((item for item in specs if len(rest) > 1 and item.name == rest[1]), None)
332
+ if verb not in ("run-now", "device"):
333
+ return _usage(f"schedule takes list, run-now NAME or device NAME DEVICE, not {verb}")
334
+ if spec is None:
335
+ known = ", ".join(item.name for item in specs) or "none"
336
+ return _usage(f"no schedule named {rest[1]!r} (known: {known})")
337
+ if verb == "device":
338
+ if len(rest) < 3:
339
+ return _usage("device needs a device, or none: mobster test schedule device NAME \"Sam's iPhone\"")
340
+ health.set_device(root, spec.name, None if rest[2] == "none" else rest[2])
341
+ print(f"{spec.name} " + ("runs on its simulator again." if rest[2] == "none" else
342
+ f"runs on {rest[2]} while Mobster is running."), file=out, flush=True)
343
+ return 0
344
+ # a phone only by --device: its plan gate runs first
345
+ runner = schedules or health.TestSchedules(devices=lambda: {})
346
+ device = (args.device or [None])[0]
347
+ sim = (args.sim or [None])[0]
348
+ answer = runner.start(spec, trigger="run-now", device=device, sim=sim, wait=True,
349
+ progress=lambda line: print(line, file=err, flush=True))
350
+ if answer["outcome"] == "skipped":
351
+ print(f"mobster test: {spec.name} was skipped. {answer['reason']}", file=err, flush=True)
352
+ return USAGE_EXIT
353
+ print(f"{spec.name}: {answer['verdict'].replace('_', ' ')} (suite {answer['suite']})", file=out, flush=True)
354
+ return answer["exitCode"] or 0
355
+
356
+
357
+ def history(args, paths, *, out=None):
358
+ """`mobster test --history [PATH]`: the project's per-check history, flaky first. Runs nothing."""
359
+ from ..verify import perf
360
+ from ..verify.checks import project_root
361
+ from .history import report
362
+ out = out or sys.stdout
363
+ start = Path(paths[0]) if paths else Path.cwd()
364
+ root = project_root(start if start.exists() else Path.cwd())
365
+ found = report(root)
366
+ if args.json:
367
+ print(json.dumps(found, ensure_ascii=False, allow_nan=False), file=out, flush=True)
368
+ return 0
369
+ if not found["ok"]:
370
+ print("mobster test: the history file couldn't be read, so it starts again with the next run. Delete "
371
+ ".mobster/test-results/history.json.corrupt when you no longer need it.", file=sys.stderr, flush=True)
372
+ if not found["checks"]:
373
+ print("No test history yet. Run `mobster test` and each check's results are kept here.", file=out, flush=True)
374
+ return 0
375
+ marks = {"passed": "✓", "failed": "✗", "needs_review": "!"}
376
+ for row in found["checks"]:
377
+ trend = f", {row['trend']}" if row["trend"] and row["trend"] != "steady" else ""
378
+ line = (f"{row['state']:<7} {row['name']} on {dots(row['device'])} "
379
+ f"{''.join(marks[o] for o in row['outcomes'])} {round((row['passRate'] or 0) * 100)}% of "
380
+ f"{row['runs']} runs, flake score {row['flakeScore']:.2f}{trend}{perf.last_run(row)}")
381
+ print(line, file=out)
382
+ if row["suggestQuarantine"]:
383
+ print(f" Flaky: add {row['file']} to .mobster/quarantine.yaml, or run with --auto-quarantine.", file=out)
384
+ print(file=out, flush=True)
385
+ return 0
386
+
387
+
388
+ def summary_text(doc, paths, color=False):
389
+ """The human summary: one line per check and device that didn't pass cleanly, the counts, the reports."""
390
+ from .reports import counts_text, duration_text
391
+ words = {"passed": "✓ passed", "failed": "✗ failed", "needs_review": "! needs review",
392
+ "couldnt_run": "✗ couldn't run", "flaky": "~ flaky"}
393
+
394
+ def paint(text, tone):
395
+ return _paint(color)(text, TONES[tone], "bold")
396
+
397
+ lines = []
398
+ for check in doc.get("checks") or ():
399
+ for result in check.get("results") or ():
400
+ status = result.get("status")
401
+ tone = "flaky" if status == "passed" and result.get("flaky") else status
402
+ line = f"{paint(words.get(tone, status), tone)} {check.get('name')} on {dots(result.get('device') or '')}"
403
+ if result.get("passRate") is not None:
404
+ line += f" ({round(result['passRate'] * 100)}% of {doc.get('options', {}).get('repeat')} runs)"
405
+ elif (result.get("attempts") or 0) > 1:
406
+ line += f" ({result['attempts']} attempts)"
407
+ if check.get("quarantined"):
408
+ line += " [quarantined]"
409
+ lines.append(line)
410
+ reason = result.get("reason") or (result.get("flakyReason") if result.get("flaky") else None) or {}
411
+ if status != "passed" or result.get("flaky"):
412
+ for text in (reason.get("message"), reason.get("fix")):
413
+ if text:
414
+ lines.append(f" {text}")
415
+ for item in result.get("healed") or ():
416
+ if isinstance(item, dict):
417
+ lines.append(f" healed step {item.get('step')}: {item.get('from')} → {item.get('to')}")
418
+ lines += xctest.text_lines(doc, paint)
419
+ for item in (doc.get("history") or {}).get("suggestQuarantine") or ():
420
+ lines.append(f"~ {item['check']} on {dots(item['device'] or '')} is flaky over its last 20 runs. Quarantine "
421
+ f"it: add {item['file']} to .mobster/quarantine.yaml.")
422
+ summary = doc.get("summary") or {}
423
+ head = f"{counts_text(summary)} in {duration_text(doc.get('durationMs'))}"
424
+ if summary.get("costUsd"):
425
+ head += f", ${summary['costUsd']:.2f}"
426
+ if summary.get("replayed"):
427
+ head += f", {summary['replayed']} replayed with no model"
428
+ if summary.get("linkFirst"):
429
+ head += (f", {summary['linkFirst']['steps']} link-first, "
430
+ f"{summary['linkFirst']['modelCallsSaved']} model calls saved")
431
+ lines += ["", head]
432
+ if summary.get("healed"):
433
+ lines.append(f"{summary['healed']} healed step{'' if summary['healed'] == 1 else 's'} wait for review: "
434
+ "mobster test heal")
435
+ from .a11y_baseline import counts as a11y_counts
436
+ found = a11y_counts(doc)
437
+ if any(found.values()):
438
+ lines.append(f"Accessibility: {found['new']} new, {found['warnings']} warning"
439
+ f"{'' if found['warnings'] == 1 else 's'}, {found['known']} in the baseline")
440
+ if doc.get("a11yBaseline") is not None:
441
+ lines.append(f"Wrote .mobster/a11y-baseline.json with {doc['a11yBaseline']} finding"
442
+ f"{'' if doc['a11yBaseline'] == 1 else 's'}.")
443
+ if doc.get("stopped"):
444
+ lines.append("Stopped before every check ran.")
445
+ if paths.get("html"):
446
+ lines.append(f"Report {_relative(paths.get('htmlCopy') or paths['html'])}")
447
+ if paths.get("junitCopy") or paths.get("junit"):
448
+ lines.append(f"JUnit {_relative(paths.get('junitCopy') or paths['junit'])}")
449
+ return "\n".join(lines)
450
+
451
+
452
+ def _tell_slack(root, paths, doc, shared, err):
453
+ """Post the suite to Slack when the project's rules ask (notify.py), and say what happened in one line."""
454
+ if not paths.get("folder"):
455
+ return
456
+ from .notify import notify
457
+ line = notify(root, paths["folder"], doc, link=getattr(shared, "link", None))
458
+ if line:
459
+ print(line, file=err, flush=True)
460
+
461
+
462
+ def pr(args, rest, *, run=None, out=None, err=None, cwd=None):
463
+ """`mobster test pr [--base REF] [options]`: the checks the branch's changes need, built, run and reported."""
464
+ from .api import Options, UsageError
465
+ from .pr import PrError
466
+ from .prrun import PrOptions
467
+ out, err = out or sys.stdout, err or sys.stderr
468
+ if rest:
469
+ return _usage(f"pr takes no paths ({' '.join(rest)}): it picks the checks from the diff")
470
+ if getattr(args, "record_run", None) or getattr(args, "record_name", None) or \
471
+ getattr(args, "record_output", None):
472
+ return _usage("--run, --name and -o go with record: mobster test record --run RUN_ID")
473
+ for flag, name in (("--matrix", "matrix"), ("--shard", "shard"), ("--tag", "tag")):
474
+ if getattr(args, name, None):
475
+ return _usage(f"{flag} doesn't go with pr: pr picks the checks from the diff")
476
+ options = Options(sims=tuple(args.sim or ()), devices=tuple(args.device or ()), parallel=args.parallel,
477
+ retries=args.retries, repeat=args.repeat, quarantine=args.quarantine, junit=args.junit,
478
+ html=args.html, strict=args.strict, keyless=args.keyless, video=args.video,
479
+ assert_timeout=args.assert_timeout, command=_command(args),
480
+ testflight=tuple(getattr(args, "testflight", None) or ()),
481
+ fresh=bool(getattr(args, "fresh", False)), no_heal=bool(getattr(args, "no_heal", False)),
482
+ auto_quarantine=bool(getattr(args, "auto_quarantine", False)),
483
+ spread=bool(getattr(args, "spread", False)),
484
+ xctest=bool(getattr(args, "xctest", False)), xctest_scheme=args.scheme,
485
+ xctest_project=args.project, xctest_plan=getattr(args, "test_plan", None))
486
+ if _only_testing(args, options):
487
+ return USAGE_EXIT
488
+ pr_options = PrOptions(base=args.base, build=not args.no_build, scheme=args.scheme, project=args.project,
489
+ drafts=args.drafts or "run", gifs=not args.no_gifs, tested_with=args.tested_with,
490
+ github=getattr(args, "github", None) or "check")
491
+ if run is None:
492
+ from .prrun import test_pr as run
493
+ from ..verify.checks import project_root
494
+ root = project_root(Path(cwd or Path.cwd()))
495
+ try:
496
+ from ..devtools import ProgressLines
497
+ progress = ProgressLines(err)
498
+ except ImportError:
499
+ def progress(line):
500
+ print(line, file=err, flush=True)
501
+ try:
502
+ result = run(pr_options, options, root=root, progress=progress)
503
+ except (PrError, UsageError) as error:
504
+ return _usage(str(error))
505
+ finally:
506
+ getattr(progress, "end", lambda: None)()
507
+ shared = None
508
+ if getattr(args, "share", False) and result.paths.get("folder"):
509
+ shared = _share(result.paths["folder"], args.share_days, err)
510
+ if shared is not None:
511
+ from ..paths import write_text_atomic
512
+ from .pr_report import comment, hosted_pages, write
513
+ try:
514
+ # A write that fails leaves the comment linking the local files, and the exit code as it was.
515
+ write({**result.pr, "hostedReport": shared.link}, result.paths["folder"])
516
+ write_text_atomic(Path(result.paths["comment"]), comment(result.pr, artifact_url=shared.link,
517
+ file_url=hosted_pages(shared.link)))
518
+ result.pr["hostedReport"] = shared.link
519
+ except (OSError, ValueError):
520
+ print("mobster test: the report is shared, but its link couldn't be written into the comment.",
521
+ file=err, flush=True)
522
+ _tell_slack(root, result.paths, result.doc, shared, err)
523
+ if args.comment:
524
+ target = Path(args.comment).expanduser()
525
+ try:
526
+ target.parent.mkdir(parents=True, exist_ok=True)
527
+ target.write_text(Path(result.paths["comment"]).read_text(encoding="utf-8"), encoding="utf-8")
528
+ except OSError as error:
529
+ return _usage(f"--comment {args.comment}: can't write it ({error.strerror or type(error).__name__})")
530
+ if args.json:
531
+ print(json.dumps(result.pr, ensure_ascii=False), file=out, flush=True)
532
+ else:
533
+ from ..style import color_enabled
534
+ print(pr_text(result, color=color_enabled(out)), file=out, flush=True)
535
+ return result.exit_code
536
+
537
+
538
+ def pr_text(result, color=False):
539
+ """The human summary of `mobster test pr`: the changed screens, a line per check, the drafts, the verdict and where
540
+ the comment and GIFs are."""
541
+ pr = result.pr
542
+ tones = TONES
543
+ words = {"passed": "✓ passed", "failed": "✗ failed", "needs_review": "! needs review",
544
+ "couldnt_run": "✗ couldn't run", "draft": "~ draft"}
545
+
546
+ def paint(text, tone):
547
+ return _paint(color)(text, tones[tone], "bold")
548
+
549
+ lines = []
550
+ screens = [screen.get("name") for screen in (pr.get("plan") or {}).get("screens") or ()]
551
+ if screens:
552
+ lines.append("Changed screens: " + ", ".join(screens[:8]) + (" …" if len(screens) > 8 else ""))
553
+ for row in pr.get("rows") or ():
554
+ status = row.get("status")
555
+ if row.get("draft"):
556
+ lines.append(f"{paint(words['draft'], 'draft')} {row.get('file')} on {dots(row.get('device') or '')}, "
557
+ f"{(status or '').replace('_', ' ')}")
558
+ continue
559
+ lines.append(f"{paint(words.get(status, status), status if status in tones else 'couldnt_run')} "
560
+ f"{row.get('check')} on {dots(row.get('device') or '')}")
561
+ if status != "passed":
562
+ lines.append(f" {row.get('expected')}: {row.get('found')}")
563
+ lines += xctest.text_lines(result.doc, paint)
564
+ lines += ["", pr.get("headline") or ""]
565
+ lines += list(pr.get("notes") or ())
566
+ folder = result.paths.get("folder")
567
+ if result.paths.get("comment"):
568
+ lines.append(f"Comment {_relative(result.paths['comment'])}")
569
+ gifs = [row.get("gif") for row in pr.get("rows") or () if row.get("gif")]
570
+ if gifs and folder:
571
+ lines.append(f"GIFs {_relative(Path(folder) / 'pr' / 'gifs')} ({len(gifs)})")
572
+ return "\n".join(lines).strip("\n")
573
+
574
+
575
+ def add(args, rest, *, cwd=None, out=None, err=None):
576
+ from . import recipes
577
+ out, err = out or sys.stdout, err or sys.stderr
578
+ if getattr(args, "add_list", False):
579
+ if rest:
580
+ return _usage("--list takes no recipe")
581
+ width = max(len(name) for name in recipes.names())
582
+ for recipe in recipes.BUILTIN:
583
+ print(f"{recipe.name.ljust(width)} {recipe.summary}", file=out)
584
+ return 0
585
+ if len(rest) != 1:
586
+ return _usage("add takes one recipe: mobster test add RECIPE (mobster test add --list shows them)")
587
+ given = {}
588
+ for item in getattr(args, "add_set", None) or ():
589
+ key, equals, value = item.partition("=")
590
+ if not equals or not key.strip():
591
+ return _usage(f"--set {item}: write it as KEY=VALUE")
592
+ given[key.strip().replace("-", "_")] = value
593
+ if getattr(args, "bundle", None):
594
+ given["bundle"] = args.bundle
595
+ try:
596
+ recipe, data = recipes.build(rest[0], given)
597
+ except recipes.RecipeError as error:
598
+ return _usage(str(error))
599
+ from ..verify.checks import project_root
600
+ base = Path(cwd or os.getcwd())
601
+ target = project_root(base) / ".mobster" / "checks" / f"{recipe.name}.yaml"
602
+ if target.exists() and not getattr(args, "force", False):
603
+ return _usage(f"{_relative(target)} exists, so Mobster left it as it is: pass --force to replace it")
604
+ import yaml
605
+ text = (f"# A Mobster check from the {recipe.name} recipe. Run it with: mobster test {_relative(target)}\n"
606
+ + yaml.safe_dump(data, sort_keys=False, allow_unicode=True, default_flow_style=False, width=100))
607
+ try:
608
+ write_text_atomic(target, text)
609
+ except OSError as error:
610
+ return _usage(f"{_relative(target)}: can't write it ({error.strerror or type(error).__name__})")
611
+ print(f"Saved {_relative(target)} from the {recipe.name} recipe. Run it with: mobster test "
612
+ f"{shlex.quote(_relative(target))}" + (f". {recipe.notes}" if recipe.notes else ""), file=err, flush=True)
613
+ return 0
614
+
615
+
616
+ def record(args, rest, *, find=None, out=None, err=None):
617
+ """`mobster test record --run RUN_ID [--name NAME] [-o PATH]`."""
618
+ from .record import RecordError, check_data, check_yaml, find_run
619
+ out, err = out or sys.stdout, err or sys.stderr
620
+ if rest:
621
+ return _usage(f"record takes no paths ({' '.join(rest)}); write the check where you like with -o PATH")
622
+ run_id = getattr(args, "record_run", None)
623
+ if not run_id:
624
+ return _usage("record needs the task: mobster test record --run RUN_ID (the Mobster app's task id)")
625
+ output = getattr(args, "record_output", None)
626
+ name = getattr(args, "record_name", None)
627
+ if name is not None and (not name.strip() or len(name) > LIMITS["name"]):
628
+ return _usage(f"--name is 1 to {LIMITS['name']} characters")
629
+ target = Path(output).expanduser() if output else None
630
+ shown = _relative(target) if target else ".mobster/checks/<name>.yaml"
631
+ try:
632
+ found = (find or find_run)(run_id)
633
+ data = check_data(found, name=name)
634
+ text = check_yaml(found, name=name, path=shown)
635
+ except RecordError as error:
636
+ return _usage(error.message)
637
+ if target is None:
638
+ print(text, end="", file=out, flush=True)
639
+ return 0
640
+ if target.suffix.lower() not in (".yaml", ".yml"):
641
+ return _usage(f"-o {output}: a check file ends in .yaml")
642
+ existed = target.exists()
643
+ try:
644
+ write_text_atomic(target, text)
645
+ except OSError as error:
646
+ return _usage(f"-o {output}: can't write it ({error.strerror or type(error).__name__})")
647
+ steps = len(data.get("steps") or ())
648
+ print(f"{'Replaced' if existed else 'Saved'} {_relative(target)}: {steps} step{'' if steps == 1 else 's'}, "
649
+ f"{len(data['expect'])} expectation{'' if len(data['expect']) == 1 else 's'}. Run it with: mobster test "
650
+ f"{shlex.quote(_relative(target))}", file=err, flush=True)
651
+ return 0
652
+
653
+
654
+ def import_flows(args, rest, *, out=None, err=None, cwd=None):
655
+ """`mobster test import maestro PATH… [-o DIR]`: Maestro flows as checks."""
656
+ from . import maestro
657
+ out, err = out or sys.stdout, err or sys.stderr
658
+ if not rest or rest[0] != "maestro":
659
+ return _usage("import needs the format: mobster test import maestro .maestro")
660
+ sources = rest[1:]
661
+ if not sources:
662
+ return _usage("import maestro needs the flows: mobster test import maestro .maestro")
663
+ cwd = Path(cwd or os.getcwd())
664
+ for flag, name in (("--run", "record_run"), ("--name", "record_name")):
665
+ if getattr(args, name, None):
666
+ return _usage(f"{flag} goes with record, not import")
667
+ bundle = getattr(args, "bundle", None)
668
+ if bundle:
669
+ from ..state import validate_bundle_id
670
+ try:
671
+ validate_bundle_id(bundle)
672
+ except ValueError:
673
+ return _usage(f"--bundle {bundle!r} is not a bundle ID such as com.example.app")
674
+ folder = Path(getattr(args, "record_output", None) or ".mobster/checks").expanduser()
675
+ if not folder.is_absolute():
676
+ folder = cwd / folder
677
+ root = cwd
678
+ app = getattr(args, "app", None)
679
+ try:
680
+ results = maestro.convert_tree([Path(item) if Path(item).is_absolute() else cwd / item for item in sources],
681
+ bundle=bundle, app_path=app,
682
+ skip_unsupported=getattr(args, "skip_unsupported", False))
683
+ except maestro.MaestroError as error:
684
+ return _usage(str(error))
685
+ dry, force = getattr(args, "dry_run", False), getattr(args, "force", False)
686
+ written, refused = _write_imports(results, folder, root, dry=dry, force=force)
687
+ if getattr(args, "json", False):
688
+ print(json.dumps({"flows": [item.to_dict() for item in results], "folder": _relative(folder, root)},
689
+ indent=2, ensure_ascii=False), file=out, flush=True)
690
+ else:
691
+ print(import_text(results, folder, root, dry=dry), file=out, flush=True)
692
+ return 1 if refused else 0
693
+
694
+
695
+ def _write_imports(results, folder, root, *, dry, force):
696
+ """Write every imported check; a check that exists is kept unless ``force``. (checks written, flows refused)."""
697
+ from . import maestro
698
+ refused = sum(1 for item in results if item.outcome == "refused")
699
+ written = 0
700
+ for item in results:
701
+ if item.outcome != "imported":
702
+ continue
703
+ for stem, data in item.checks:
704
+ target = folder / f"{stem}.yaml"
705
+ text = maestro.render(data, item.source)
706
+ if target.exists() and not force:
707
+ if target.read_text(encoding="utf-8", errors="replace") == text:
708
+ item.same.append(stem) # importing twice gives the same file: nothing to replace or report
709
+ continue
710
+ item.notes.append(f"{_relative(target, root)} exists, so it was not replaced; use --force")
711
+ item.outcome = "kept"
712
+ continue
713
+ if dry:
714
+ continue
715
+ try:
716
+ write_text_atomic(target, text)
717
+ written += 1
718
+ except OSError as error:
719
+ item.outcome = "refused"
720
+ item.reason = f"can't write {_relative(target, root)} ({error.strerror or type(error).__name__})"
721
+ refused += 1
722
+ break
723
+ return written, refused
724
+
725
+
726
+ def _import_table(results):
727
+ rows = [(item.source, item.converted, len(item.placeholders),
728
+ len(item.problems) if item.outcome == "refused" else 0) for item in results if item.outcome != "skipped"]
729
+ if not rows:
730
+ return []
731
+ rows.append(("Total", *(sum(row[n] for row in rows) for n in (1, 2, 3))))
732
+ width = max(len(row[0]) for row in rows + [("Flow",)])
733
+ head = ("Flow", "Converted", "Needs a first run", "Unsupported")
734
+ out = [f"{head[0]:<{width}} {head[1]:>9} {head[2]:>17} {head[3]:>11}"]
735
+ out += [f"{name:<{width}} {a:>9} {b:>17} {c:>11}" for name, a, b, c in rows]
736
+ return out
737
+
738
+
739
+ def import_text(results, folder, root, *, dry=False):
740
+ """What the import did, flow by flow: the checks written, what was refused and why, and what to run."""
741
+ lines = []
742
+ imported = [item for item in results if item.outcome in ("imported", "kept")]
743
+ refused = [item for item in results if item.outcome == "refused"]
744
+ checks = sum(len(item.checks) for item in imported)
745
+ where = _relative(folder, root)
746
+ verb = "Would import" if dry else "Imported"
747
+ lines.append(f"{verb} {len(imported)} of {len(results)} Maestro flow{'' if len(results) == 1 else 's'} "
748
+ f"({checks} check{'' if checks == 1 else 's'}) into {where}")
749
+ for item in results:
750
+ lines.append("")
751
+ lines.append(item.source)
752
+ if item.outcome == "skipped":
753
+ lines.append(f" skipped: {item.reason}")
754
+ elif item.outcome == "refused":
755
+ lines.append(f" refused: {item.reason}" if item.reason
756
+ else " refused: it has commands Mobster can't run, so no check was written")
757
+ for problem in item.problems[:IMPORT_SHOWN]:
758
+ lines.append(f" {problem.where}: {problem.why}")
759
+ if problem.instead:
760
+ lines.append(f" instead: {problem.instead}")
761
+ if len(item.problems) > IMPORT_SHOWN:
762
+ lines.append(f" and {len(item.problems) - IMPORT_SHOWN} more; fix these, then import again")
763
+ else:
764
+ for stem, data in item.checks:
765
+ steps, expects = len(data.get("steps") or ()), len(data["expect"])
766
+ verb = "unchanged" if stem in item.same else "would write" if dry else "wrote"
767
+ lines.append(f" {verb} {_relative(folder / (stem + '.yaml'), root)}: "
768
+ f"{steps} step{'' if steps == 1 else 's'}, "
769
+ f"{expects} expectation{'' if expects == 1 else 's'}")
770
+ for problem in item.placeholders:
771
+ lines.append(f" needs a first run, {problem.where}: {problem.why}")
772
+ for problem in item.dropped:
773
+ lines.append(f" dropped {problem.where}: {problem.why}")
774
+ if problem.instead:
775
+ lines.append(f" instead: {problem.instead}")
776
+ for note in item.notes:
777
+ lines.append(f" note: {note}")
778
+ lines.append("")
779
+ lines.extend(_import_table(results))
780
+ lines.append("")
781
+ if refused:
782
+ lines.append(f"{len(refused)} flow{'' if len(refused) == 1 else 's'} refused: Mobster won't write a check "
783
+ "that covers less than the flow does. Fix the commands named above, or add --skip-unsupported "
784
+ "to drop the ones that aren't assertions and list each.")
785
+ if checks and not dry:
786
+ lines.append(f"Run them with: mobster test {shlex.quote(where)}")
787
+ return "\n".join(lines)
788
+
789
+
790
+ def heal(args, rest, *, cwd=None, out=None, err=None):
791
+ """`mobster test heal [CHECK...] [--accept | --reject] [--json]`: list or settle the heals waiting for review."""
792
+ from ..replay_core import heal as heals
793
+ from ..replay_core.checks import cache_dir_for
794
+ from ..verify.checks import project_root
795
+ from .discover import DiscoveryError, discover
796
+ out, err = out or sys.stdout, err or sys.stderr
797
+ accept, reject = bool(getattr(args, "accept", False)), bool(getattr(args, "reject", False))
798
+ if accept and reject:
799
+ return _usage("heal takes --accept or --reject, not both")
800
+ cwd = Path(cwd or Path.cwd())
801
+ root = project_root(cwd / rest[0] if rest and (cwd / rest[0]).exists() else cwd)
802
+ try:
803
+ entries = discover(_heal_paths(rest, root, cwd), root, cwd=cwd)
804
+ except DiscoveryError as error:
805
+ return _usage(str(error))
806
+ cache = cache_dir_for(root / ".mobster" / "runs")
807
+ found = []
808
+ for entry in entries:
809
+ check_file = Path(entry.path)
810
+ try:
811
+ document = heals.read(check_file)
812
+ except heals.HealError as error:
813
+ print(f"mobster test heal: {error}", file=err, flush=True)
814
+ continue
815
+ if document is not None:
816
+ found.append((check_file, document))
817
+ if getattr(args, "json", False) and not (accept or reject):
818
+ print(json.dumps({"heals": [{"check": _relative(str(file)), "diff": _relative(str(heals.heal_file(file))),
819
+ **document} for file, document in found]}, ensure_ascii=False, default=str),
820
+ file=out, flush=True)
821
+ return 0
822
+ if not found:
823
+ print("No heals wait for review.", file=out, flush=True)
824
+ return 0
825
+ if accept or reject:
826
+ total = 0
827
+ for check_file, _ in found:
828
+ try:
829
+ count = (heals.accept if accept else heals.reject)(check_file, cache)
830
+ except heals.HealError as error:
831
+ print(f"mobster test heal: {_relative(str(check_file))}: {error}", file=err, flush=True)
832
+ continue
833
+ total += count
834
+ word = "Accepted" if accept else "Kept failing"
835
+ print(f"{word}: {_relative(str(check_file))} ({count} step{'' if count == 1 else 's'})", file=out,
836
+ flush=True)
837
+ if reject and total:
838
+ print("Those steps now fail the check when their screen changes again.", file=out, flush=True)
839
+ return 0
840
+ lines = []
841
+ for check_file, document in found:
842
+ app = document.get("app") or {}
843
+ where = ", ".join(filter(None, [f"run {document.get('run')}" if document.get("run") else None,
844
+ f"app {app.get('version')}" if isinstance(app, dict) and app.get("version")
845
+ else None, f"iOS {document.get('ios')}" if document.get("ios") else None]))
846
+ lines.append(f"{_relative(str(check_file))}" + (f" ({where})" if where else ""))
847
+ for item in document["heals"]:
848
+ if not isinstance(item, dict):
849
+ continue
850
+ lines.append(f" step {item.get('step')} {item.get('from')} → {item.get('to')}")
851
+ if item.get("why"):
852
+ lines.append(f" {item['why']}")
853
+ for name in ("before", "after"):
854
+ frame = (item.get("frames") or {}).get(name)
855
+ if frame:
856
+ lines.append(f" {name:<6} {frame}")
857
+ count = sum(len(document["heals"]) for _, document in found)
858
+ lines += ["", f"{count} heal{'' if count == 1 else 's'} wait{'s' if count == 1 else ''} for review. Accept: "
859
+ "mobster test heal --accept Keep failing: mobster test heal --reject"]
860
+ print("\n".join(lines), file=out, flush=True)
861
+ return 0
862
+
863
+
864
+ def _heal_paths(rest, root, cwd):
865
+ """Check paths for `heal`: as given (a name like "paywall" is .mobster/checks/paywall.yaml), else none (all)."""
866
+ out = []
867
+ for item in rest:
868
+ path = Path(item)
869
+ if not path.suffix and not (cwd / path).exists():
870
+ for suffix in (".yaml", ".yml", ".json"):
871
+ candidate = root / ".mobster" / "checks" / (item + suffix)
872
+ if candidate.exists():
873
+ path = candidate
874
+ break
875
+ out.append(str(path))
876
+ return out
877
+
878
+
879
+ NIGHTLY_FLAGS = (("sim", "--sim"), ("device", "--device"), ("tag", "--tag"), ("testflight", "--testflight"))
880
+
881
+
882
+ def _night_flags(args):
883
+ """The `mobster test` flags `nightly on` keeps for each night's run."""
884
+ flags = []
885
+ for name, flag in NIGHTLY_FLAGS:
886
+ for value in getattr(args, name, None) or ():
887
+ flags += [flag, value]
888
+ if getattr(args, "parallel", 1) and args.parallel > 1:
889
+ flags += ["--parallel", str(args.parallel)]
890
+ if getattr(args, "retries", None) is not None:
891
+ flags += ["--retries", str(args.retries)]
892
+ for name, flag in (("keyless", "--keyless"), ("spread", "--spread")):
893
+ if getattr(args, name, False):
894
+ flags.append(flag)
895
+ for value in getattr(args, "audit", None) or ():
896
+ flags += ["--audit", value]
897
+ if getattr(args, "a11y", None):
898
+ flags.append(f"--a11y={args.a11y}")
899
+ return flags
900
+
901
+
902
+ def nightly(args, rest, *, cwd=None, out=None, err=None):
903
+ """`mobster test nightly on --at HH:MM [flags] | off | status | run [flags]`."""
904
+ from ..devkit import DevError
905
+ from ..devkit import nightly as night
906
+ from ..verify.checks import project_root
907
+ out, err = out or sys.stdout, err or sys.stderr
908
+ verb = rest[0] if rest else "status"
909
+ root = project_root(Path(cwd or Path.cwd()))
910
+ try:
911
+ if verb == "on":
912
+ if not getattr(args, "at", None):
913
+ return _usage("nightly on needs --at HH:MM, such as --at 02:00")
914
+ path = night.turn_on(root, args.at, _night_flags(args))
915
+ print(f"Every night at {args.at}, Mobster runs the checks of {root.name}"
916
+ + (f" ({' '.join(_night_flags(args))})" if _night_flags(args) else "")
917
+ + f". The morning report goes to {night.FOLDER}/<date>/morning.md, with a notification.\n"
918
+ f"Keep this Mac on power and signed in. Stop it with: mobster test nightly off\n({path})",
919
+ file=out, flush=True)
920
+ return 0
921
+ if verb == "off":
922
+ print("Turned the nightly run off." if night.turn_off(root) else "No nightly run is set up here.",
923
+ file=out, flush=True)
924
+ return 0
925
+ if verb == "run":
926
+ code, text = night.run_night(root, _night_flags(args))
927
+ print(text, file=out, flush=True)
928
+ return code
929
+ if verb == "status":
930
+ state = night.status(root)
931
+ if getattr(args, "json", False):
932
+ print(json.dumps(state), file=out, flush=True)
933
+ return 0
934
+ lines = [f"Every night at {state['at']}" + (f" ({' '.join(state['flags'])})" if state["flags"] else "")
935
+ if state["on"]
936
+ else "No nightly run is set up here. Turn it on: mobster test nightly on --at 02:00"]
937
+ if state["last"]:
938
+ lines.append(f"Last run {state['last'].get('date')}: {state['last'].get('headline')}. "
939
+ f"{state['last'].get('morning')}")
940
+ print("\n".join(lines), file=out, flush=True)
941
+ return 0
942
+ except DevError as error:
943
+ return _usage(error.line())
944
+ return _usage(f"nightly takes on, off, status or run, not {verb}")
945
+
946
+
947
+ __all__ = ["add_arguments", "run", "summary_text", "suite", "pr", "pr_text", "add", "record", "import_flows", "heal",
948
+ "nightly"]