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,306 @@
1
+ """Screenshot baselines (docs/testing.mdx, "Screenshots that must not change"): a perceptual pixel diff, no model.
2
+
3
+ A check's ``snapshot:`` compares the screen it ends on to a PNG under ``.mobster/snapshots/``. Both images are blurred
4
+ by 2 px and a pixel counts as changed only when a colour channel moves by more than TOLERANCE, so antialiasing and
5
+ sub-pixel text never fail a check. Masked regions count in neither the changed pixels nor the total."""
6
+
7
+ import math
8
+ import os
9
+ import re
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from .assertions import CheckError, parse_selector, reject_unknown_keys
16
+
17
+ BLUR_PX = 2
18
+ TOLERANCE = 24 # of 255, per colour channel, after the blur
19
+ DEFAULT_THRESHOLD = 0.1 # percent of the unmasked pixels that may differ
20
+ MASK_FILL = (128, 128, 128)
21
+ CELL_PX = 16 # regions are found on a grid this coarse
22
+ REGION_PAD_PX = 4
23
+ MAX_REGIONS = 20
24
+ STATUS_BAR_POINTS = 59 # the top of a Dynamic Island iPhone's screen, when the tree names no status bar
25
+ NAME = re.compile(r"[a-z0-9][a-z0-9_-]{0,59}")
26
+ MASK_KEYS = ("id", "label", "rect")
27
+ MASK_LIMIT = 20
28
+ SNAPSHOT_STATUS_BAR = {"time": "9:41", "data_network": "wifi", "wifi_mode": "active", "wifi_bars": 3,
29
+ "cellular_mode": "active", "cellular_bars": 4, "battery_state": "charged",
30
+ "battery_level": 100}
31
+ TEXT_ENTRY = frozenset({"TextField", "SecureTextField", "SearchField", "TextView"})
32
+ FOLDER = "snapshots"
33
+
34
+ MATCHED, CHANGED, SIZE_CHANGED, RECORDED, UPDATED = "matched", "changed", "size_changed", "recorded", "updated"
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Mask:
39
+ """A region the diff ignores: the elements a selector matches, or a rectangle in points (x, y, width, height)."""
40
+ selector: Optional[object] = None
41
+ rect: Optional[tuple] = None
42
+
43
+ def to_dict(self):
44
+ return {"rect": list(self.rect)} if self.rect is not None else self.selector.to_dict()
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class Snapshot:
49
+ """The ``snapshot:`` entry of a check's ``expect``."""
50
+ name: str
51
+ masks: tuple = ()
52
+ threshold: float = DEFAULT_THRESHOLD # percent
53
+
54
+ def to_dict(self):
55
+ out = {"snapshot": self.name}
56
+ if self.masks:
57
+ out["mask"] = [mask.to_dict() for mask in self.masks]
58
+ if self.threshold != DEFAULT_THRESHOLD:
59
+ out["threshold"] = self.threshold
60
+ return out
61
+
62
+
63
+ def parse_mask(obj, where):
64
+ if not isinstance(obj, dict):
65
+ raise CheckError(f"{where} must be an object such as {{id: clock}}, {{label: Last sync}} or "
66
+ "{rect: [0, 100, 390, 40]}")
67
+ reject_unknown_keys(obj, MASK_KEYS, where)
68
+ if "rect" in obj:
69
+ if len(obj) != 1:
70
+ raise CheckError(f"{where}: rect goes alone, in points as [x, y, width, height]")
71
+ rect = obj["rect"]
72
+ if (not isinstance(rect, list) or len(rect) != 4 or any(
73
+ isinstance(item, bool) or not isinstance(item, (int, float)) or not math.isfinite(item)
74
+ for item in rect) or rect[2] <= 0 or rect[3] <= 0):
75
+ raise CheckError(f"{where}.rect must be four numbers in points: x, y, width and height (the last two "
76
+ "above 0)")
77
+ return Mask(rect=tuple(float(item) for item in rect))
78
+ if not obj:
79
+ raise CheckError(f"{where} needs id, label or rect")
80
+ return Mask(selector=parse_selector(obj, where))
81
+
82
+
83
+ def parse_snapshot(obj, where="snapshot"):
84
+ """The ``snapshot`` entry of ``expect``. Raises CheckError."""
85
+ reject_unknown_keys(obj, ("snapshot", "mask", "threshold"), where)
86
+ name = obj.get("snapshot")
87
+ if not isinstance(name, str) or not NAME.fullmatch(name):
88
+ raise CheckError(f"{where}.snapshot is the screen's name, 1 to 60 of a-z, 0-9, - and _, such as home; it "
89
+ "names the baseline file")
90
+ masks = obj.get("mask", [])
91
+ if not isinstance(masks, list):
92
+ raise CheckError(f"{where}.mask must be a list, such as [{{id: clock}}]")
93
+ if len(masks) > MASK_LIMIT:
94
+ raise CheckError(f"{where}.mask has more than {MASK_LIMIT} entries")
95
+ threshold = obj.get("threshold", DEFAULT_THRESHOLD)
96
+ if isinstance(threshold, bool) or not isinstance(threshold, (int, float)) or not 0 <= threshold <= 100 \
97
+ or not math.isfinite(threshold):
98
+ raise CheckError(f"{where}.threshold is the percent of the screen that may change, from 0 to 100 "
99
+ f"(default {DEFAULT_THRESHOLD:g})")
100
+ return Snapshot(name, tuple(parse_mask(item, f"{where}.mask[{index}]") for index, item in enumerate(masks)),
101
+ float(threshold))
102
+
103
+
104
+ # -- where baselines live ---------------------------------------------------------------------------------------
105
+
106
+ def slug(text):
107
+ return re.sub(r"[^a-z0-9]+", "-", str(text).lower()).strip("-") or "default"
108
+
109
+
110
+ def check_key(source, root, name=""):
111
+ """The folder a check's baselines live in: its file's path under .mobster/checks (or the project), without the
112
+ suffix; the check's name for a check with no file."""
113
+ if source and root:
114
+ path = Path(source)
115
+ for base in (Path(root) / ".mobster" / "checks", Path(root)):
116
+ try:
117
+ return path.resolve().relative_to(base.resolve()).with_suffix("").as_posix()
118
+ except ValueError:
119
+ continue
120
+ return Path(source).stem if source else slug(name)
121
+
122
+
123
+ def variant(device, conditions):
124
+ """``iphone-17-pro-dark-accessibility-large-de_DE``: the device and the three conditions a baseline is for."""
125
+ conditions = conditions or {}
126
+ parts = [slug(device)] + [str(conditions.get(key) or "default") for key in ("appearance", "content_size", "locale")]
127
+ return "-".join(part.replace("/", "_") for part in parts)
128
+
129
+
130
+ def baseline_file(root, key, name, device, conditions):
131
+ return Path(root) / ".mobster" / FOLDER / key / f"{name}@{variant(device, conditions)}.png"
132
+
133
+
134
+ # -- the diff ---------------------------------------------------------------------------------------------------
135
+
136
+ @dataclass
137
+ class Comparison:
138
+ status: str
139
+ changed_pixels: int = 0
140
+ compared_pixels: int = 0
141
+ regions: list = field(default_factory=list) # [x, y, w, h] in pixels
142
+ diff: object = None # a PIL image, only for a change
143
+ seconds: float = 0.0
144
+
145
+ @property
146
+ def changed_percent(self):
147
+ return 100.0 * self.changed_pixels / self.compared_pixels if self.compared_pixels else 0.0
148
+
149
+
150
+ def compare(baseline, current, masks=(), threshold=DEFAULT_THRESHOLD):
151
+ """Compare two PIL images. ``masks`` are [x, y, w, h] pixel rectangles. Status: matched, changed or size_changed."""
152
+ from PIL import Image, ImageChops, ImageDraw, ImageFilter
153
+ started = time.perf_counter()
154
+ if baseline.size != current.size:
155
+ return Comparison(SIZE_CHANGED, seconds=time.perf_counter() - started)
156
+ first, second = baseline.convert("RGB"), current.convert("RGB")
157
+ counted = Image.new("L", current.size, 255)
158
+ for x, y, w, h in masks:
159
+ box = [round(x), round(y), round(x + w) - 1, round(y + h) - 1]
160
+ for image in (first, second):
161
+ ImageDraw.Draw(image).rectangle(box, fill=MASK_FILL) # the same in both, so the blur leaves no halo
162
+ ImageDraw.Draw(counted).rectangle(box, fill=0)
163
+ red, green, blue = ImageChops.difference(first.filter(ImageFilter.GaussianBlur(BLUR_PX)),
164
+ second.filter(ImageFilter.GaussianBlur(BLUR_PX))).split()
165
+ moved = ImageChops.lighter(ImageChops.lighter(red, green), blue).point([255 if v > TOLERANCE else 0
166
+ for v in range(256)])
167
+ changed = moved.histogram()[255]
168
+ compared = counted.histogram()[255]
169
+ percent = 100.0 * changed / compared if compared else 0.0
170
+ if percent <= threshold:
171
+ return Comparison(MATCHED, changed, compared, seconds=time.perf_counter() - started)
172
+ regions = _regions(moved)
173
+ return Comparison(CHANGED, changed, compared, regions, _diff_image(current, moved, regions),
174
+ time.perf_counter() - started)
175
+
176
+
177
+ def _regions(moved):
178
+ """Boxes around the changed pixels: neighbouring cells of a CELL_PX grid join, each box padded a little."""
179
+ from PIL import Image
180
+ width, height = moved.size
181
+ columns, rows = -(-width // CELL_PX), -(-height // CELL_PX)
182
+ grid = moved.resize((columns, rows), Image.BOX).point([255 if v else 0 for v in range(256)])
183
+ data = grid.load()
184
+ cells = {(x, y) for y in range(rows) for x in range(columns) if data[x, y]}
185
+ boxes = []
186
+ while cells:
187
+ start = cells.pop()
188
+ stack, low, high = [start], list(start), list(start)
189
+ while stack:
190
+ cx, cy = stack.pop()
191
+ for nx in (cx - 1, cx, cx + 1):
192
+ for ny in (cy - 1, cy, cy + 1):
193
+ if (nx, ny) in cells:
194
+ cells.discard((nx, ny))
195
+ stack.append((nx, ny))
196
+ low[0], low[1] = min(low[0], cx), min(low[1], cy)
197
+ high[0], high[1] = max(high[0], cx), max(high[1], cy)
198
+ left, top = max(0, low[0] * CELL_PX - REGION_PAD_PX), max(0, low[1] * CELL_PX - REGION_PAD_PX)
199
+ right = min(width, (high[0] + 1) * CELL_PX + REGION_PAD_PX)
200
+ bottom = min(height, (high[1] + 1) * CELL_PX + REGION_PAD_PX)
201
+ boxes.append([left, top, right - left, bottom - top])
202
+ boxes.sort(key=lambda box: (box[1], box[0]))
203
+ if len(boxes) > MAX_REGIONS:
204
+ left, top = min(b[0] for b in boxes), min(b[1] for b in boxes)
205
+ boxes = [[left, top, max(b[0] + b[2] for b in boxes) - left, max(b[1] + b[3] for b in boxes) - top]]
206
+ return boxes
207
+
208
+
209
+ def _diff_image(current, moved, regions):
210
+ """The current screen faded, the changed pixels red, a box around each region."""
211
+ from PIL import Image, ImageDraw
212
+ faded = Image.blend(current.convert("RGB"), Image.new("RGB", current.size, "white"), .6)
213
+ faded.paste(Image.new("RGB", current.size, (230, 30, 30)), mask=moved)
214
+ draw = ImageDraw.Draw(faded)
215
+ line = max(2, current.width // 300)
216
+ for x, y, w, h in regions:
217
+ draw.rectangle([x, y, x + w - 1, y + h - 1], outline=(230, 30, 30), width=line)
218
+ return faded
219
+
220
+
221
+ # -- masks from the tree ----------------------------------------------------------------------------------------
222
+
223
+ def mask_rects(snapshot, tree, pixel_width, *, status_bar_set):
224
+ """Every masked region as [x, y, w, h] pixels: the check's masks, the keyboard (and the fields, whose caret blinks,
225
+ while it shows) and, when the status bar isn't set, the status bar."""
226
+ scale = pixel_width / tree.size[0] if tree.size and tree.size[0] else 1.0
227
+ shown = tree.shown()
228
+ rects = []
229
+
230
+ def add(rect):
231
+ x, y, w, h = rect
232
+ rects.append([x * scale, y * scale, w * scale, h * scale])
233
+ for mask in snapshot.masks:
234
+ if mask.rect is not None:
235
+ add(mask.rect)
236
+ else:
237
+ for node in shown:
238
+ if mask.selector.matches(node):
239
+ add(node.rect)
240
+ if any(node.role == "Keyboard" for node in shown):
241
+ for node in shown:
242
+ if node.role == "Keyboard" or node.role in TEXT_ENTRY:
243
+ add(node.rect)
244
+ if not status_bar_set:
245
+ bars = [node for node in shown if node.role == "StatusBar"]
246
+ for node in bars:
247
+ add(node.rect)
248
+ if not bars:
249
+ add((0, 0, tree.size[0], STATUS_BAR_POINTS))
250
+ return rects
251
+
252
+
253
+ def save_png(image, path):
254
+ path = Path(path)
255
+ path.parent.mkdir(parents=True, exist_ok=True)
256
+ temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
257
+ image.save(temporary, format="PNG")
258
+ os.replace(temporary, path)
259
+
260
+
261
+ # -- listing baselines ------------------------------------------------------------------------------------------
262
+
263
+ BASELINE_NAME = re.compile(r"(?P<name>[a-z0-9][a-z0-9_-]*)@(?P<variant>.+)\.png")
264
+
265
+
266
+ def baselines(root):
267
+ """Every baseline under .mobster/snapshots: [{file, check, name, variant}], sorted."""
268
+ base = Path(root) / ".mobster" / FOLDER
269
+ out = []
270
+ if not base.is_dir():
271
+ return out
272
+ for path in sorted(base.rglob("*.png")):
273
+ found = BASELINE_NAME.fullmatch(path.name)
274
+ if found is None:
275
+ continue
276
+ out.append({"file": path.relative_to(root).as_posix(),
277
+ "check": path.parent.relative_to(base).as_posix(), "name": found["name"],
278
+ "variant": found["variant"]})
279
+ return out
280
+
281
+
282
+ def audit(root, checks):
283
+ """Mark each baseline in a project. ``checks``: [(check key, Check)] of the project's checks.
284
+ orphaned: no check has that key or snapshot name. stale: the check has the snapshot, but not for this appearance,
285
+ text size or locale (a check without conditions lists "default" in each)."""
286
+ from . import conditions as conditions_module
287
+ wanted = {}
288
+ for key, check in checks:
289
+ spec = getattr(check, "snapshot", None)
290
+ if spec is None:
291
+ continue
292
+ combos = conditions_module.combinations(check.conditions)
293
+ wanted.setdefault((key, spec.name), set()).update(
294
+ "-".join(str(combo.get(k) or "default") for k in ("appearance", "content_size", "locale"))
295
+ for combo in combos)
296
+ out = []
297
+ for item in baselines(root):
298
+ sets = wanted.get((item["check"], item["name"]))
299
+ if sets is None:
300
+ state = "orphaned"
301
+ elif not any(item["variant"].endswith("-" + tail) for tail in sets):
302
+ state = "stale"
303
+ else:
304
+ state = "current"
305
+ out.append({**item, "state": state})
306
+ return out
@@ -0,0 +1,335 @@
1
+ """Sound as a verification signal (`expect: [{sound: sent}]`, experimental): matched in code, with no model.
2
+
3
+ A fingerprint is a short recording of one sound (a sent chime, an error tock) reduced to the shape of its spectrum over
4
+ time: 16 frequency bands per 64 ms frame, as shares of the frame's energy, plus each frame's loudness against the
5
+ loudest. It is recorded once per app under ``.mobster/sounds/<bundle>/<name>.json`` and matched against what the iPhone
6
+ played during a run. A match is a pass (the sound did happen) and the screen read follows as usual; a sound that isn't
7
+ heard fails the assertion and says how close the best window came. Hearing needs a real iPhone (listen.py): a
8
+ simulator's sound doesn't leave the Mac, so a sound assertion on a simulator is not met.
9
+ """
10
+
11
+ import cmath
12
+ import json
13
+ import math
14
+ import operator
15
+ import os
16
+ import re
17
+ import threading
18
+ import wave
19
+ from array import array
20
+ from pathlib import Path
21
+
22
+ from .assertions import AssertionResult
23
+
24
+ __all__ = ["SETTING", "NAME", "enabled", "fingerprint_wav", "match_wav", "save", "load", "names", "Watch"]
25
+
26
+ SETTING = "MOBSTER_EXPERIMENTAL_SOUND"
27
+ NAME = re.compile(r"[a-z0-9][a-z0-9_-]{0,39}")
28
+ FRAME = 1024
29
+ HOP = 512
30
+ BANDS = 16
31
+ LOW_HZ, HIGH_HZ = 250.0, 7000.0
32
+ TARGET_RATE = 16000
33
+ ACTIVE_DB = -35.0 # a reference keeps the frames within this many dB of its loudest
34
+ SILENT_DBFS = -55.0 # a window whose loudest frame is quieter than this heard nothing
35
+ MATCH_SCORE = 0.92
36
+ MAX_REFERENCE_SECONDS = 5
37
+ MAX_RECORDING_SECONDS = 120
38
+ WINDOW_SECONDS = 20
39
+ VERSION = 1
40
+
41
+
42
+ class SoundError(ValueError):
43
+ """What is wrong with a sound file or fingerprint, and what to do."""
44
+
45
+
46
+ def enabled():
47
+ return os.environ.get(SETTING, "").strip().lower() in ("1", "on", "true", "yes")
48
+
49
+
50
+ def _samples(path):
51
+ """(mono float samples in -1..1 decimated to about 16 kHz, the rate after decimation) from a 16-bit PCM wav."""
52
+ try:
53
+ with wave.open(str(path), "rb") as handle:
54
+ channels, width, rate, count = (
55
+ handle.getnchannels(),
56
+ handle.getsampwidth(),
57
+ handle.getframerate(),
58
+ handle.getnframes(),
59
+ )
60
+ if width != 2:
61
+ raise SoundError(f"{Path(path).name} is not 16-bit PCM. Record it as 16-bit WAV.")
62
+ if count > rate * MAX_RECORDING_SECONDS:
63
+ raise SoundError(f"{Path(path).name} is longer than {MAX_RECORDING_SECONDS} s. Use a shorter one.")
64
+ raw = array("h")
65
+ raw.frombytes(handle.readframes(count))
66
+ except (wave.Error, EOFError, OSError) as error:
67
+ raise SoundError(f"{Path(path).name} can't be read as a WAV ({error}). Record it again.") from None
68
+ if raw.itemsize != 2:
69
+ raise SoundError("this platform's 16-bit array is not 2 bytes")
70
+ if channels > 1:
71
+ raw = array("h", (sum(raw[i : i + channels]) // channels for i in range(0, len(raw) - channels + 1, channels)))
72
+ step = max(1, rate // TARGET_RATE)
73
+ mono = [sum(raw[i : i + step]) / (step * 32768.0) for i in range(0, len(raw) - step + 1, step)]
74
+ return mono, rate / step
75
+
76
+
77
+ def _fft(values):
78
+ """The discrete Fourier transform of ``values`` (a power-of-two length), by the iterative radix-2 method."""
79
+ n = len(values)
80
+ out = [values[int(format(i, f"0{n.bit_length() - 1}b")[::-1], 2)] for i in range(n)]
81
+ size = 2
82
+ while size <= n:
83
+ half = size // 2
84
+ step = [cmath.exp(-2j * math.pi * k / size) for k in range(half)]
85
+ for start in range(0, n, size):
86
+ for k in range(half):
87
+ odd = out[start + k + half] * step[k]
88
+ out[start + k + half] = out[start + k] - odd
89
+ out[start + k] = out[start + k] + odd
90
+ size *= 2
91
+ return out
92
+
93
+
94
+ def _band_of_bins(rate):
95
+ """For each bin up to Nyquist, the band it belongs to (None outside LOW_HZ..HIGH_HZ); bands are log-spaced."""
96
+ edges = [LOW_HZ * (HIGH_HZ / LOW_HZ) ** (b / BANDS) for b in range(BANDS + 1)]
97
+ out = []
98
+ for k in range(FRAME // 2):
99
+ hz = k * rate / FRAME
100
+ out.append(next((b for b in range(BANDS) if edges[b] <= hz < edges[b + 1]), None))
101
+ return out
102
+
103
+
104
+ def _frames(samples, rate):
105
+ """[(band shares, dBFS of the frame)] for every frame of ``samples``."""
106
+ hann = [0.5 - 0.5 * math.cos(2 * math.pi * n / (FRAME - 1)) for n in range(FRAME)]
107
+ bins = _band_of_bins(rate)
108
+ out = []
109
+ for start in range(0, len(samples) - FRAME + 1, HOP):
110
+ spectrum = _fft([x * w for x, w in zip(samples[start : start + FRAME], hann)])
111
+ power = [0.0] * BANDS
112
+ for k, band in enumerate(bins):
113
+ if band is not None:
114
+ power[band] += abs(spectrum[k]) ** 2
115
+ total = sum(power)
116
+ db = 10 * math.log10(total * 2 / sum(hann) ** 2 + 1e-12)
117
+ out.append(([p / total for p in power] if total > 1e-12 else [0.0] * BANDS, db))
118
+ return out
119
+
120
+
121
+ def fingerprint_wav(path):
122
+ """The fingerprint (a JSON-able dict) of the sound in the WAV at ``path``, trimmed to its audible part."""
123
+ samples, rate = _samples(path)
124
+ if len(samples) > rate * MAX_REFERENCE_SECONDS:
125
+ raise SoundError(
126
+ f"{Path(path).name} is longer than {MAX_REFERENCE_SECONDS} s. A fingerprint is one short "
127
+ "sound: cut it to the chime."
128
+ )
129
+ frames = _frames(samples, rate)
130
+ if not frames:
131
+ raise SoundError(f"{Path(path).name} is too short to fingerprint. Record at least a tenth of a second.")
132
+ peak = max(db for _, db in frames)
133
+ if peak < SILENT_DBFS:
134
+ raise SoundError(f"{Path(path).name} is silent. Record the sound with the iPhone's volume up.")
135
+ active = [i for i, (_, db) in enumerate(frames) if db >= peak + ACTIVE_DB]
136
+ kept = frames[active[0] : active[-1] + 1]
137
+ return {
138
+ "version": VERSION,
139
+ "frame": FRAME,
140
+ "hop": HOP,
141
+ "bands": BANDS,
142
+ "seconds": round(len(kept) * HOP / rate, 2),
143
+ "frames": [{"shares": [round(s, 4) for s in shares], "db": round(db - peak, 1)} for shares, db in kept],
144
+ }
145
+
146
+
147
+ def _cosine(a, b):
148
+ dot = sum(map(operator.mul, a, b))
149
+ na, nb = math.sqrt(sum(x * x for x in a)), math.sqrt(sum(x * x for x in b))
150
+ return dot / (na * nb) if na and nb else 0.0
151
+
152
+
153
+ def best_score(fingerprint, recording_frames):
154
+ """(best weighted similarity 0..1, the frame it starts at) of the fingerprint over the recording's frames; the
155
+ window's loudest frame must clear SILENT_DBFS, so silence can't match."""
156
+ ref = fingerprint["frames"]
157
+ if fingerprint.get("version") != VERSION or not ref or not recording_frames:
158
+ raise SoundError("this fingerprint is not one this version of Mobster reads. Record the sound again.")
159
+ weights = [10 ** (frame["db"] / 20) for frame in ref]
160
+ best, at = 0.0, 0
161
+ for start in range(0, len(recording_frames) - len(ref) + 1):
162
+ window = recording_frames[start : start + len(ref)]
163
+ if max(db for _, db in window) < SILENT_DBFS:
164
+ continue
165
+ score = sum(w * _cosine(frame["shares"], shares) for w, frame, (shares, _) in zip(weights, ref, window)) / sum(
166
+ weights
167
+ )
168
+ if score > best:
169
+ best, at = score, start
170
+ return best, at
171
+
172
+
173
+ def match_wav(fingerprint, path):
174
+ """(heard, score, seconds into the recording) for ``fingerprint`` against the WAV at ``path``."""
175
+ samples, rate = _samples(path)
176
+ score, start = best_score(fingerprint, _frames(samples, rate))
177
+ return score >= MATCH_SCORE, round(score, 2), round(start * HOP / rate, 1)
178
+
179
+
180
+ def folder(root, bundle):
181
+ return Path(root) / ".mobster" / "sounds" / re.sub(r"[^A-Za-z0-9._-]", "_", bundle)
182
+
183
+
184
+ def save(root, bundle, name, wav):
185
+ """Record the sound in ``wav`` as ``name`` for the app ``bundle`` under ``root``/.mobster/sounds; the path."""
186
+ if not NAME.fullmatch(name):
187
+ raise SoundError("a sound's name is lowercase letters, digits, - and _ (up to 40), such as sent")
188
+ target = folder(root, bundle) / f"{name}.json"
189
+ target.parent.mkdir(parents=True, exist_ok=True)
190
+ target.write_text(json.dumps(fingerprint_wav(wav), separators=(",", ":")) + "\n")
191
+ return target
192
+
193
+
194
+ def load(root, bundle, name):
195
+ path = folder(root, bundle) / f"{name}.json"
196
+ try:
197
+ return json.loads(path.read_text())
198
+ except FileNotFoundError:
199
+ have = ", ".join(names(root, bundle)) or "none"
200
+ raise SoundError(
201
+ f"no sound {name!r} is recorded for {bundle} (recorded: {have}). Record it with "
202
+ "`python -m mobile_agent.verify.sound record`."
203
+ ) from None
204
+ except (OSError, ValueError):
205
+ raise SoundError(f"{path} isn't a fingerprint. Record the sound again.") from None
206
+
207
+
208
+ def names(root, bundle):
209
+ return sorted(p.stem for p in folder(root, bundle).glob("*.json"))
210
+
211
+
212
+ def evaluate(root, bundle, wanted, wav, *, why=None):
213
+ """One AssertionResult per sound name in ``wanted`` against the recording ``wav`` (None when nothing was heard).
214
+ Fails closed: a sound with no recording to check, or no fingerprint, is not met."""
215
+ out = []
216
+ for name in wanted:
217
+ text = f"sound {name}"
218
+ if wav is None:
219
+ out.append(AssertionResult(False, text, "not checked: " + (why or "the iPhone's sound wasn't recorded")))
220
+ continue
221
+ try:
222
+ heard, score, at = match_wav(load(root, bundle, name), wav)
223
+ except SoundError as error:
224
+ out.append(AssertionResult(False, text, str(error)))
225
+ continue
226
+ out.append(
227
+ AssertionResult(
228
+ heard,
229
+ text,
230
+ f"heard at {at} s (match {score})" if heard else f"not heard (best match {score}, needs {MATCH_SCORE})",
231
+ )
232
+ )
233
+ return out
234
+
235
+
236
+ class Watch:
237
+ """Records the iPhone's sound in a thread while a run's steps go; ``stop`` waits and gives the WAV, or None."""
238
+
239
+ def __init__(self, capture, wav, seconds=WINDOW_SECONDS):
240
+ self._capture, self.wav, self._seconds = capture, Path(wav), seconds
241
+ self.error = None
242
+ self._thread = threading.Thread(target=self._run, daemon=True)
243
+
244
+ def _run(self):
245
+ try:
246
+ self._capture(self._seconds, keep=str(self.wav))
247
+ except Exception as error: # the run goes on; the assertions say the sound wasn't recorded
248
+ self.error = error
249
+
250
+ def start(self):
251
+ self._thread.start()
252
+ return self
253
+
254
+ def stop(self):
255
+ self._thread.join(self._seconds + 180)
256
+ return self.wav if self.wav.is_file() and self.error is None and not self._thread.is_alive() else None
257
+
258
+
259
+ class Signal:
260
+ """A run's sound assertions: ``start`` after the app is up, ``results`` once the steps are done."""
261
+
262
+ def __init__(self, wanted, run, *, capture=None):
263
+ self.wanted, self._run, self._capture, self._watch, self._dir = wanted, run, capture, None, None
264
+ self._why = None
265
+
266
+ def start(self):
267
+ from .smart import is_real_device
268
+
269
+ if not enabled():
270
+ self._why = f"sound checks are experimental. Set {SETTING}=1 to run them"
271
+ elif not is_real_device(self._run._manager()):
272
+ self._why = "a simulator's sound doesn't leave the Mac, so only a real iPhone can be heard"
273
+ else:
274
+ import tempfile
275
+
276
+ capture = self._capture
277
+ if capture is None:
278
+ from ..listen import listen
279
+
280
+ capture = lambda seconds, keep: listen(seconds, udid=self._run.target.udid, keep=keep) # noqa: E731
281
+ self._dir = tempfile.mkdtemp(prefix="mobster-sound-")
282
+ os.chmod(self._dir, 0o700)
283
+ self._watch = Watch(capture, Path(self._dir) / "heard.wav").start()
284
+
285
+ def results(self):
286
+ wav = self._watch.stop() if self._watch is not None else None
287
+ if self._watch is not None and wav is None:
288
+ self._why = "the iPhone's sound wasn't recorded" + (
289
+ f" ({type(self._watch.error).__name__})" if self._watch.error else ""
290
+ )
291
+ run = self._run
292
+ parent = run.runs_dir.parent
293
+ root = (
294
+ parent.parent
295
+ if parent.name == ".mobster"
296
+ else (Path(run.check.source).parent if run.check.source else Path.cwd())
297
+ )
298
+ try:
299
+ return evaluate(root, run.app.bundle_id, self.wanted, wav, why=self._why)
300
+ finally:
301
+ if self._dir:
302
+ import shutil
303
+
304
+ shutil.rmtree(self._dir, ignore_errors=True)
305
+
306
+
307
+ def main(argv=None):
308
+ import argparse
309
+
310
+ parser = argparse.ArgumentParser(
311
+ prog="python -m mobile_agent.verify.sound", description="record and list sound fingerprints"
312
+ )
313
+ sub = parser.add_subparsers(dest="command", required=True)
314
+ record = sub.add_parser("record", help="fingerprint a short WAV of one sound")
315
+ record.add_argument("--bundle", required=True)
316
+ record.add_argument("--name", required=True)
317
+ record.add_argument("--wav", required=True)
318
+ record.add_argument("--root", default=".")
319
+ ls = sub.add_parser("list", help="list the recorded sounds of an app")
320
+ ls.add_argument("--bundle", required=True)
321
+ ls.add_argument("--root", default=".")
322
+ args = parser.parse_args(argv)
323
+ try:
324
+ if args.command == "record":
325
+ print(f"wrote {save(args.root, args.bundle, args.name, args.wav)}")
326
+ else:
327
+ print("\n".join(names(args.root, args.bundle)))
328
+ except SoundError as error:
329
+ print(error)
330
+ return 1
331
+ return 0
332
+
333
+
334
+ if __name__ == "__main__":
335
+ raise SystemExit(main())