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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- 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())
|