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,976 @@
|
|
|
1
|
+
"""One `mobster verify` run: the verdict comes from the assertions, never a model; mobile_agent.sim loads lazily."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .. import project_env
|
|
11
|
+
from ..transport import request_once
|
|
12
|
+
from ..wda import timeouts as wda_timeouts
|
|
13
|
+
from . import device_audit, snapshots, wire
|
|
14
|
+
from .a11y import NAME as A11Y
|
|
15
|
+
from .a11y import Collector as A11yCollector
|
|
16
|
+
from .actions import ActionError, ActionJournal, ActionNotDispatched
|
|
17
|
+
from .assertions import evaluate_on
|
|
18
|
+
from .business_run import BusinessRun
|
|
19
|
+
from .checks import project_root
|
|
20
|
+
from .flow import Flow
|
|
21
|
+
from .l10n import Collector as L10nCollector
|
|
22
|
+
from .replay_admission import ReplayAdmission
|
|
23
|
+
from .run_cleanup import RunCleanup
|
|
24
|
+
from .run_links import RunLinks
|
|
25
|
+
from .run_perf import PerfMeasure
|
|
26
|
+
from .run_secrets import ProjectSecrets
|
|
27
|
+
from .runs import make_run_dir
|
|
28
|
+
from .screen_coverage import ScreenCoverage
|
|
29
|
+
from .smart import is_real_device, no_smart_key
|
|
30
|
+
from .sound import Signal as SoundSignal
|
|
31
|
+
from .tree import GLANCE_SOURCE_PATH, TreeError
|
|
32
|
+
from .tree import read_tree as read_tree_from
|
|
33
|
+
from .verdict import CRASH_EXPLAINS, CouldntRun, Verdict, mobster_version
|
|
34
|
+
|
|
35
|
+
MODES = ("smart", "keyless", "launch")
|
|
36
|
+
FOREGROUND_WAIT_SECONDS = 15
|
|
37
|
+
BASELINE_SETTLE_SECONDS = 3
|
|
38
|
+
SETTLE_FIRST_PAUSE, SETTLE_PAUSE = .3, .5
|
|
39
|
+
FRAME_QUALITY = 75
|
|
40
|
+
CRASH_FIX = "The report's Logs and Crash pane shows the stack and the app's log around the crash."
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _now():
|
|
44
|
+
return time.monotonic()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _pause(seconds):
|
|
48
|
+
time.sleep(seconds)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def wda_session(wda_url):
|
|
52
|
+
# Plain capabilities, the benchmark's way (bench/iosworld.wda_session): Smart's simulator numbers were measured so.
|
|
53
|
+
status = request_once(wda_url, "GET", "/status", timeout=10)
|
|
54
|
+
if status.get("sessionId"):
|
|
55
|
+
return status["sessionId"]
|
|
56
|
+
created = request_once(wda_url, "POST", "/session", {"capabilities": {"alwaysMatch": {}}}, timeout=30)
|
|
57
|
+
return created["sessionId"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def front_app(wda_url, timeout=5):
|
|
61
|
+
# An app with a big tree can hold this read for 17-50 s, so it never waits past the probe bound.
|
|
62
|
+
timeout = min(timeout, wda_timeouts.WDA_CALL_TIMEOUTS["probe"])
|
|
63
|
+
value = request_once(wda_url, "GET", "/wda/activeAppInfo", timeout=timeout).get("value")
|
|
64
|
+
value = value if isinstance(value, dict) else {}
|
|
65
|
+
bundle, pid = value.get("bundleId"), value.get("pid")
|
|
66
|
+
return (bundle if isinstance(bundle, str) and bundle else None,
|
|
67
|
+
pid if isinstance(pid, int) and not isinstance(pid, bool) and pid > 0 else None)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_driver(target, manager, run_dir, mode):
|
|
71
|
+
own = getattr(manager, "build_driver", None) # a manager that is not the simulator's (Android) builds its own
|
|
72
|
+
if own is not None:
|
|
73
|
+
return own(target, run_dir, mode)
|
|
74
|
+
from ..compose import build_target_driver
|
|
75
|
+
from ..wda.frame_clock import attach_frame_clock
|
|
76
|
+
url = target.wda_url
|
|
77
|
+
session = wda_session(url)
|
|
78
|
+
driver = build_target_driver(wda_url=url, session=session)
|
|
79
|
+
try:
|
|
80
|
+
driver.restarter = manager.restarter(target)
|
|
81
|
+
except Exception:
|
|
82
|
+
driver.restarter = None # recovery then waits for the runner instead of restarting it
|
|
83
|
+
driver.new_session = lambda: wda_session(url)
|
|
84
|
+
if mode != "launch":
|
|
85
|
+
attach_frame_clock(driver, wda_url=url, session=session, mjpeg_url=target.mjpeg_url,
|
|
86
|
+
log_path=str(Path(run_dir) / "frameclock.jsonl"))
|
|
87
|
+
if mode != "smart":
|
|
88
|
+
driver.tune(rich=True, glide=True)
|
|
89
|
+
return driver
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _sim_error(error):
|
|
93
|
+
"""A simulator manager error (duck-typed by ``kind``) as CouldntRun."""
|
|
94
|
+
kind = getattr(error, "kind", None)
|
|
95
|
+
if isinstance(kind, str):
|
|
96
|
+
return CouldntRun(kind, str(error), getattr(error, "fix", "") or "")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class VerifyRun(ScreenCoverage, Flow, BusinessRun, ReplayAdmission, RunCleanup, RunLinks, ProjectSecrets,
|
|
101
|
+
PerfMeasure, Verdict):
|
|
102
|
+
"""One run. run_id and run_dir exist from __init__, so the MCP server hands out the id before prepare() ends."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, check, *, mode, runs_dir, manager=None, progress=None, key=None, assert_timeout=5,
|
|
105
|
+
replay="off", simctl=None, business_binding=None):
|
|
106
|
+
if mode not in MODES:
|
|
107
|
+
raise ValueError(f"mode must be one of {', '.join(MODES)}")
|
|
108
|
+
self.check, self.mode, self.manager, self.key = check, mode, manager, key
|
|
109
|
+
self.assert_timeout = assert_timeout
|
|
110
|
+
self._progress = progress
|
|
111
|
+
self.runs_dir = Path(runs_dir).expanduser().resolve()
|
|
112
|
+
self.run_id, self.run_dir = make_run_dir(self.runs_dir)
|
|
113
|
+
self.actions = ActionJournal(self.run_dir)
|
|
114
|
+
self.state = "new"
|
|
115
|
+
self.secrets = {} # {NAME: value} of the project's ${NAME}s this check uses (project_env.py)
|
|
116
|
+
self._live = check # the check with those expanded: only launches and Smart's request read it
|
|
117
|
+
self.steps = []
|
|
118
|
+
self.error = None
|
|
119
|
+
self.lock = threading.RLock()
|
|
120
|
+
self.driver = self.target = self.app = self.lease = None
|
|
121
|
+
self.frames = [] # every frame, relative to run_dir, in order
|
|
122
|
+
self.step_frames = [] # the step frames among them (the 60-frame cap applies to these)
|
|
123
|
+
self.timing = {}
|
|
124
|
+
self.baseline = {"taken": False, "held": []}
|
|
125
|
+
self.business_binding = business_binding
|
|
126
|
+
self.business_fixture = None
|
|
127
|
+
self.business_result = ({"adapter": check.business.adapter, "fixture": check.business.fixture,
|
|
128
|
+
"status": "unavailable", "schemaVersion": "mobster.business/1",
|
|
129
|
+
"assertions": [], "setup": "not_started", "cleanup": "not_needed"}
|
|
130
|
+
if check.business is not None else None)
|
|
131
|
+
self._business_baseline = None
|
|
132
|
+
self._business_cleanup_done = False
|
|
133
|
+
self.agent = None # Smart's {status, note, model, turns}
|
|
134
|
+
self.cost_usd = 0.0
|
|
135
|
+
self.result = None
|
|
136
|
+
self._t0 = time.monotonic()
|
|
137
|
+
self._t0_wall = time.time() # the same moment on the wall clock: log lines and crash reports carry it
|
|
138
|
+
self.evidence = None # the app's log and crash reports (evidence.py), from just before the launch
|
|
139
|
+
self._failed_ms = self._verdict_ms = None
|
|
140
|
+
self._ready_at = None
|
|
141
|
+
self._launch = {} # perf: interactiveMs and foregroundMs, from the launch call (perf.py)
|
|
142
|
+
self._sampler = None
|
|
143
|
+
self._app_pid = None
|
|
144
|
+
self.perf = None # perf.build, once the verdict read starts
|
|
145
|
+
self._frame_n = 0
|
|
146
|
+
self._frame_width = None
|
|
147
|
+
self._finish_lock = threading.Lock()
|
|
148
|
+
self._stop = threading.Event()
|
|
149
|
+
self._smart_status = None
|
|
150
|
+
self._smart_reason = ""
|
|
151
|
+
self._left_app = None # on a real device: the app Smart opened outside the check's (StayInApp)
|
|
152
|
+
self._model_failed = False
|
|
153
|
+
self._write_lock = threading.Lock()
|
|
154
|
+
self._driver_closed = False
|
|
155
|
+
self._preparing = False # prepare() is in flight on some thread
|
|
156
|
+
self._release_lock = threading.Lock() # a leaf lock: nothing is taken while it is held
|
|
157
|
+
self._awaiting_frame = [] # Smart steps whose after-frame hasn't been taken yet
|
|
158
|
+
self._held_somewhere = set()
|
|
159
|
+
self._unframed_findings = {}
|
|
160
|
+
self._screen_coverage_lock = threading.RLock()
|
|
161
|
+
self._screen_frames_pending = 0
|
|
162
|
+
self._screen_frames_sealed = False
|
|
163
|
+
self._screen_result_sealed = False
|
|
164
|
+
self._screen_coverage_final = None
|
|
165
|
+
self._verdict_thread = None
|
|
166
|
+
self._screen_verdict_audited = False
|
|
167
|
+
self._screen_coverage_issue = None
|
|
168
|
+
self.source_observation = None
|
|
169
|
+
level = "strict" if A11Y in check.audit else check.a11y
|
|
170
|
+
self._a11y = A11yCollector((check.conditions or {}).get("content_size"), level=level,
|
|
171
|
+
baseline=check.a11y_baseline, scope=check.a11y_scope) if level else None
|
|
172
|
+
self._ax_device = device_audit.for_run(self, check.audit)
|
|
173
|
+
self._sound = SoundSignal(check.sound, self) if check.sound else None
|
|
174
|
+
locale = (check.conditions or {}).get("locale")
|
|
175
|
+
self._l10n = L10nCollector(locale) if check.l10n and locale else None
|
|
176
|
+
self._simctl = simctl # conditions.simctl, or a fake in tests
|
|
177
|
+
self._wire = None # wire.Judge when the check has request, no_request or storage expectations
|
|
178
|
+
self._conditions = None # conditions.Applier, once the device is held and the app is in place
|
|
179
|
+
self.dialogs = None # dialogs.Policy, once the device is held: how this run answers iOS's own dialogs
|
|
180
|
+
self.snapshot = None # snapshot_run.take's result: the screen against its baseline
|
|
181
|
+
self._status_bar_set = False # the status bar is overridden (9:41, full bars), so a snapshot needn't mask it
|
|
182
|
+
self._conditions_restored = False
|
|
183
|
+
self.conditions_cleanup = "not_needed"
|
|
184
|
+
self.findings = [] # audit findings and screens that broke `throughout` (see _findings)
|
|
185
|
+
self._replay_drift = None # a replay that stopped at a changed step: the failure's sentence
|
|
186
|
+
self.replay = None
|
|
187
|
+
if replay != "off" and mode == "smart":
|
|
188
|
+
from ..replay_core.checks import CheckReplay
|
|
189
|
+
self.replay = CheckReplay(self, replay)
|
|
190
|
+
self.log(f"Mobster {mobster_version()} verify, run {self.run_id}, mode {mode}"
|
|
191
|
+
+ (f", replay {replay}" if self.replay is not None else ""))
|
|
192
|
+
|
|
193
|
+
def _scrub(self, text):
|
|
194
|
+
if self.secrets:
|
|
195
|
+
text = project_env.mask_text(text, self.secrets)
|
|
196
|
+
return text.replace(self.key, "[key]") if self.key and isinstance(text, str) else text
|
|
197
|
+
|
|
198
|
+
def log(self, line):
|
|
199
|
+
with self._write_lock:
|
|
200
|
+
try:
|
|
201
|
+
with open(self.run_dir / "run.log", "a", encoding="utf-8") as handle:
|
|
202
|
+
handle.write(f"{time.strftime('%H:%M:%S')} {self._scrub(line)}\n")
|
|
203
|
+
except OSError:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
def progress(self, line):
|
|
207
|
+
"""A progress line: stderr (through the caller's ``progress``) and run.log."""
|
|
208
|
+
self.log(line)
|
|
209
|
+
if self._progress is not None:
|
|
210
|
+
try:
|
|
211
|
+
self._progress(self._scrub(line))
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
def event(self, event):
|
|
216
|
+
"""One line of events.jsonl: Smart's frontier events (no prompts, no images) or key-less actions."""
|
|
217
|
+
with self._write_lock:
|
|
218
|
+
try:
|
|
219
|
+
line = self._scrub(json.dumps(project_env.mask_deep(event, self.secrets), ensure_ascii=False,
|
|
220
|
+
default=str))
|
|
221
|
+
with open(self.run_dir / "events.jsonl", "a", encoding="utf-8") as handle:
|
|
222
|
+
handle.write(line + "\n")
|
|
223
|
+
except (OSError, TypeError, ValueError):
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
def _sleep(self, seconds):
|
|
227
|
+
if seconds > 0:
|
|
228
|
+
_pause(seconds)
|
|
229
|
+
|
|
230
|
+
def _ms(self):
|
|
231
|
+
return round((time.monotonic() - self._t0) * 1000)
|
|
232
|
+
|
|
233
|
+
def _check_stop(self):
|
|
234
|
+
if self._stop.is_set():
|
|
235
|
+
raise CouldntRun("stopped", "The run was stopped")
|
|
236
|
+
|
|
237
|
+
def _fail(self, error):
|
|
238
|
+
self.error = {"klass": error.klass, "message": error.message, "fix": error.fix}
|
|
239
|
+
if self._failed_ms is None:
|
|
240
|
+
self._failed_ms = self._ms()
|
|
241
|
+
self.log(f"couldn't run ({error.klass}): {error.message}")
|
|
242
|
+
return error
|
|
243
|
+
|
|
244
|
+
def _manager(self):
|
|
245
|
+
if self.manager is None:
|
|
246
|
+
try:
|
|
247
|
+
from ..sim import SimulatorManager
|
|
248
|
+
except ImportError as error:
|
|
249
|
+
if not str(getattr(error, "name", "") or "").startswith(__package__.rsplit(".", 1)[0] + ".sim"):
|
|
250
|
+
raise
|
|
251
|
+
raise CouldntRun("environment", "This build of Mobster has no simulator manager.",
|
|
252
|
+
"Install the full Mobster CLI (docs/verify.md).") from None
|
|
253
|
+
self.manager = SimulatorManager(progress=self.progress)
|
|
254
|
+
return self.manager
|
|
255
|
+
|
|
256
|
+
def _call(self, what, *args, **kwargs):
|
|
257
|
+
"""A manager call; its SimError becomes CouldntRun."""
|
|
258
|
+
try:
|
|
259
|
+
return what(*args, **kwargs)
|
|
260
|
+
except ActionNotDispatched:
|
|
261
|
+
raise
|
|
262
|
+
except CouldntRun:
|
|
263
|
+
raise
|
|
264
|
+
except Exception as error:
|
|
265
|
+
converted = _sim_error(error)
|
|
266
|
+
if converted is not None:
|
|
267
|
+
raise converted from None
|
|
268
|
+
raise
|
|
269
|
+
|
|
270
|
+
def prepare(self):
|
|
271
|
+
# abort() decides under _finish_lock who releases the simulator, so each stop check and the state change it
|
|
272
|
+
# guards are one step under that lock: an abort between them once left the lease held and the driver open.
|
|
273
|
+
with self._finish_lock:
|
|
274
|
+
if self._stop.is_set() or self.result is not None: # the abort that stopped it writes the result
|
|
275
|
+
raise CouldntRun("stopped", "The run was stopped")
|
|
276
|
+
self.state = "preparing"
|
|
277
|
+
self._preparing = True
|
|
278
|
+
try:
|
|
279
|
+
try:
|
|
280
|
+
self._prepare()
|
|
281
|
+
except CouldntRun as error:
|
|
282
|
+
error = self._cleanup_after_run() or error
|
|
283
|
+
if self.result is None: # an abort's result already carries its own reason
|
|
284
|
+
self._fail(error)
|
|
285
|
+
self._release()
|
|
286
|
+
raise error from None
|
|
287
|
+
except ActionError as action_error:
|
|
288
|
+
error = self._cleanup_after_run() or CouldntRun(action_error.klass, str(action_error))
|
|
289
|
+
if self.result is None:
|
|
290
|
+
self._fail(error)
|
|
291
|
+
self._release()
|
|
292
|
+
raise error from None
|
|
293
|
+
except Exception as unexpected:
|
|
294
|
+
self.log("".join(traceback.format_exception(type(unexpected), unexpected,
|
|
295
|
+
unexpected.__traceback__)))
|
|
296
|
+
failure = self._cleanup_after_run()
|
|
297
|
+
if failure is not None and self.result is None:
|
|
298
|
+
self._fail(failure)
|
|
299
|
+
self._release()
|
|
300
|
+
if failure is not None:
|
|
301
|
+
raise failure from None
|
|
302
|
+
raise
|
|
303
|
+
except BaseException:
|
|
304
|
+
self._cleanup_after_run()
|
|
305
|
+
self._release()
|
|
306
|
+
raise
|
|
307
|
+
with self._finish_lock:
|
|
308
|
+
if self._stop.is_set() or self.result is not None:
|
|
309
|
+
self._cleanup_after_run()
|
|
310
|
+
self._release()
|
|
311
|
+
stopped = CouldntRun("stopped", "The run was stopped")
|
|
312
|
+
raise self._fail(stopped) if self.result is None else stopped # an abort's result stands
|
|
313
|
+
self.state = "ready"
|
|
314
|
+
self._ready_at = time.monotonic()
|
|
315
|
+
finally:
|
|
316
|
+
self._preparing = False
|
|
317
|
+
|
|
318
|
+
def _prepare(self):
|
|
319
|
+
from . import conditions
|
|
320
|
+
if conditions.is_matrix(self.check.conditions): # before any simulator is touched
|
|
321
|
+
raise CouldntRun("usage", "This check lists several appearances, text sizes or locales, and one run has "
|
|
322
|
+
"one device state.", "Run it with `mobster test`, which runs each "
|
|
323
|
+
"combination, or write one value.")
|
|
324
|
+
self._apply_fixtures()
|
|
325
|
+
self._load_secrets()
|
|
326
|
+
check = self._live
|
|
327
|
+
self._load_business()
|
|
328
|
+
self._pin_assets()
|
|
329
|
+
manager = self._manager()
|
|
330
|
+
if is_real_device(manager):
|
|
331
|
+
self._refuse_fixtures_on_phone()
|
|
332
|
+
if self.business_binding is not None:
|
|
333
|
+
raise CouldntRun("business", "App-state fixtures require a managed simulator.")
|
|
334
|
+
started = time.monotonic()
|
|
335
|
+
try:
|
|
336
|
+
with self.actions.action("ACQUIRE"):
|
|
337
|
+
self.lease = self._call(manager.acquire, check.device, check.runtime,
|
|
338
|
+
cancelled=self._stop.is_set)
|
|
339
|
+
except ActionNotDispatched as rejected:
|
|
340
|
+
raise _sim_error(rejected.error) or rejected.error from None
|
|
341
|
+
self.target = self.lease.target
|
|
342
|
+
from .dialogs import for_run
|
|
343
|
+
self.dialogs = for_run(self.check, manager, self.target, actions=self.actions)
|
|
344
|
+
self.timing["simulator"] = round(time.monotonic() - started, 2)
|
|
345
|
+
self.log(f"simulator {self.target.name} {self.target.udid} {self.target.wda_url}")
|
|
346
|
+
self._check_stop()
|
|
347
|
+
|
|
348
|
+
started = time.monotonic()
|
|
349
|
+
if check.app_path:
|
|
350
|
+
self.app = self._call(manager.app_info, check.app_path)
|
|
351
|
+
if check.bundle_id and check.bundle_id != self.app.bundle_id:
|
|
352
|
+
raise CouldntRun("install", f"{check.app_path} is {self.app.bundle_id}, not {check.bundle_id}.",
|
|
353
|
+
"Pass the .app of the app the check names, or drop the bundle ID.")
|
|
354
|
+
else:
|
|
355
|
+
self.app = self._call(manager.installed_app, self.target, check.bundle_id)
|
|
356
|
+
bundle = self.app.bundle_id
|
|
357
|
+
if self.business_binding is not None and bundle != self.business_binding.bundle:
|
|
358
|
+
raise CouldntRun("business", "The installed app does not match the enrolled app-state adapter.",
|
|
359
|
+
"Enroll a fixture for the app's actual bundle ID.")
|
|
360
|
+
self._pin_replay_before_effects()
|
|
361
|
+
# The log starts while the app installs, so its process is up before the launch (and never competes with it).
|
|
362
|
+
self._start_evidence()
|
|
363
|
+
system = getattr(manager, "system_prefixes", ("com.apple.",)) # apps that ship with the OS keep their data
|
|
364
|
+
level = check.reset_level() if not bundle.startswith(system) else "none"
|
|
365
|
+
if check.app_path:
|
|
366
|
+
self.progress(f"Installing {self.app.name} {self.app.version}")
|
|
367
|
+
if level == "reinstall":
|
|
368
|
+
with self.actions.action("RESET"):
|
|
369
|
+
self._call(manager.reset, self.target, bundle, "reinstall", app_path=check.app_path)
|
|
370
|
+
else:
|
|
371
|
+
with self.actions.action("INSTALL"):
|
|
372
|
+
self.app = self._call(manager.install, self.target, check.app_path)
|
|
373
|
+
if level == "data":
|
|
374
|
+
with self.actions.action("RESET"):
|
|
375
|
+
self._call(manager.reset, self.target, bundle, "data")
|
|
376
|
+
elif level != "none":
|
|
377
|
+
with self.actions.action("RESET"):
|
|
378
|
+
self._call(manager.reset, self.target, bundle, level)
|
|
379
|
+
self.log(f"app {bundle} {self.app.version}, reset {level}")
|
|
380
|
+
self._seed(manager, bundle)
|
|
381
|
+
self._setup_business(manager)
|
|
382
|
+
if self.app.icon:
|
|
383
|
+
(self.run_dir / "app-icon.png").write_bytes(self.app.icon)
|
|
384
|
+
self.timing["install"] = round(time.monotonic() - started, 2)
|
|
385
|
+
self._check_stop()
|
|
386
|
+
|
|
387
|
+
launch_args = self._set_conditions(manager, bundle)
|
|
388
|
+
started, launched = time.monotonic(), _now()
|
|
389
|
+
stale = self._stale_process()
|
|
390
|
+
# The driver's session and settings are made while simctl launches the app (28 Sep: 0.3 s and 0.5-0.8 s).
|
|
391
|
+
built = {}
|
|
392
|
+
|
|
393
|
+
def build():
|
|
394
|
+
try:
|
|
395
|
+
with self.actions.action("NEW_SESSION"):
|
|
396
|
+
built["driver"] = build_driver(self.target, manager, self.run_dir, self.mode)
|
|
397
|
+
except BaseException as error: # handed to the preparing thread below
|
|
398
|
+
built["error"] = error
|
|
399
|
+
builder = threading.Thread(target=build, name="mobster-verify-driver", daemon=True)
|
|
400
|
+
builder.start()
|
|
401
|
+
try:
|
|
402
|
+
if self.business_fixture is not None:
|
|
403
|
+
launch_args = (*launch_args, "-MobsterBusinessRun", self.business_fixture.token)
|
|
404
|
+
with self.actions.action("LAUNCH"):
|
|
405
|
+
self._call(manager.launch, self.target, bundle, launch_args, self._launch_env())
|
|
406
|
+
finally:
|
|
407
|
+
builder.join()
|
|
408
|
+
self.driver = built.get("driver")
|
|
409
|
+
if self.driver is not None:
|
|
410
|
+
self.actions.guard_driver(self.driver)
|
|
411
|
+
if check.throughout or check.somewhere or self._a11y or self._l10n:
|
|
412
|
+
from .source_observation import SourceObservation
|
|
413
|
+
self.source_observation = SourceObservation(self)
|
|
414
|
+
self.source_observation.attach(self.driver)
|
|
415
|
+
error = built.get("error")
|
|
416
|
+
if isinstance(error, CouldntRun):
|
|
417
|
+
raise error
|
|
418
|
+
if error is not None:
|
|
419
|
+
raise CouldntRun("wda", f"WebDriverAgent didn't answer on the simulator ({type(error).__name__}).",
|
|
420
|
+
"Run `mobster sim doctor --fix`, then try again.") from None
|
|
421
|
+
in_front = self._await_foreground(stale)
|
|
422
|
+
self._measure_launch(launched, in_front)
|
|
423
|
+
self._send_push()
|
|
424
|
+
if check.open_url:
|
|
425
|
+
self._open_link(check.open_url)
|
|
426
|
+
self.timing["launch"] = round(time.monotonic() - started, 2)
|
|
427
|
+
from ..intents import harvest # the link atlas fills as the simulator is ready (dark; never raises)
|
|
428
|
+
harvest.on_device_ready(self.target.udid, self.log)
|
|
429
|
+
self._check_stop()
|
|
430
|
+
self.frame("launch")
|
|
431
|
+
if self._wire is not None:
|
|
432
|
+
self._wire.baseline()
|
|
433
|
+
if check.steps:
|
|
434
|
+
self._take_baseline()
|
|
435
|
+
if self._sound is not None:
|
|
436
|
+
self._sound.start()
|
|
437
|
+
|
|
438
|
+
def _apply_fixtures(self):
|
|
439
|
+
"""Flatten the check's fixtures (.mobster/fixtures/) into it, or refuse before any simulator is touched."""
|
|
440
|
+
from . import fixtures
|
|
441
|
+
try:
|
|
442
|
+
self.check = self._live = fixtures.for_run(self.check, self.runs_dir)
|
|
443
|
+
except fixtures.FixtureError as error:
|
|
444
|
+
raise CouldntRun("usage", str(error), "`mobster fixtures list` shows the project's fixtures.") from None
|
|
445
|
+
if self.check.fixture:
|
|
446
|
+
self.log(f"fixtures: {', '.join(self.check.fixture)}")
|
|
447
|
+
|
|
448
|
+
def _refuse_fixtures_on_phone(self):
|
|
449
|
+
from . import fixtures
|
|
450
|
+
try:
|
|
451
|
+
fixtures.refuse_on_phone(self.check)
|
|
452
|
+
except fixtures.FixtureError as error:
|
|
453
|
+
raise CouldntRun("environment", str(error), fixtures.PHONE_FIX) from None
|
|
454
|
+
|
|
455
|
+
def _seed(self, manager, bundle):
|
|
456
|
+
"""Put the fixtures' keychain reset, files and pasteboard on the simulator,
|
|
457
|
+
after the reset and before the launch."""
|
|
458
|
+
if not self.check.seed:
|
|
459
|
+
return
|
|
460
|
+
if getattr(manager, "platform", "ios") != "ios":
|
|
461
|
+
raise CouldntRun("environment", "Fixtures seed iOS simulators only.",
|
|
462
|
+
"Remove seed from the check, or run it on an iOS simulator with --sim.")
|
|
463
|
+
from . import conditions, fixtures
|
|
464
|
+
try:
|
|
465
|
+
did = fixtures.seed_simulator(self.check.seed, udid=self.target.udid, bundle=bundle,
|
|
466
|
+
run=self._simctl or conditions.simctl, actions=self.actions)
|
|
467
|
+
except fixtures.FixtureError as error:
|
|
468
|
+
raise CouldntRun("environment", str(error), "Fix the fixture, then run the check again.") from None
|
|
469
|
+
self.log(f"fixtures seeded: {', '.join(did) or 'nothing'}")
|
|
470
|
+
|
|
471
|
+
def _set_conditions(self, manager, bundle):
|
|
472
|
+
# A condition that can't be set is couldn't run: the check never runs in the wrong state.
|
|
473
|
+
from . import conditions
|
|
474
|
+
args = tuple(self._live.launch_args)
|
|
475
|
+
spec = dict(self.check.conditions or {})
|
|
476
|
+
own = getattr(manager, "conditions", None) # a manager that is not the simulator's sets its own
|
|
477
|
+
device = manager if getattr(manager, "real_device", False) else None
|
|
478
|
+
if self.check.snapshot and device is None and own is None and "status_bar" not in spec:
|
|
479
|
+
spec["status_bar"] = snapshots.SNAPSHOT_STATUS_BAR # the same bar in every baseline
|
|
480
|
+
self._status_bar_set = device is None and own is None and "status_bar" in spec
|
|
481
|
+
if not (spec or self.check.stubs or self.check.wire):
|
|
482
|
+
return args
|
|
483
|
+
if own is not None:
|
|
484
|
+
if self.check.stubs or self.check.wire: # the proxy, shim and container reads are an iOS simulator's
|
|
485
|
+
raise CouldntRun("environment", "Request stubs and wire assertions run on an iOS simulator only, so "
|
|
486
|
+
"this check can't run here.", "Run it on a simulator, or remove them from the check.")
|
|
487
|
+
self._conditions = own(spec, self.target, bundle, actions=self.actions)
|
|
488
|
+
else:
|
|
489
|
+
self._conditions = conditions.Applier(spec, udid=self.target.udid, bundle=bundle,
|
|
490
|
+
device=device, run=self._simctl or conditions.simctl,
|
|
491
|
+
actions=self.actions, stubs=self.check.stubs,
|
|
492
|
+
watch=wire.watch_hosts(self.check.wire))
|
|
493
|
+
self._wire = wire.Judge(self.check.wire, run=self._simctl or conditions.simctl, udid=self.target.udid,
|
|
494
|
+
bundle=bundle) if self.check.wire else None
|
|
495
|
+
self.conditions_cleanup = "pending"
|
|
496
|
+
try:
|
|
497
|
+
self._conditions.before_launch()
|
|
498
|
+
except conditions.ConditionError as error:
|
|
499
|
+
raise CouldntRun("environment", error.message, error.fix) from None
|
|
500
|
+
for item in self._conditions.skipped:
|
|
501
|
+
self.log(f"condition {item['key']} skipped: {item['why']}")
|
|
502
|
+
self.log(f"conditions set: {', '.join(self._conditions.applied) or 'none'}")
|
|
503
|
+
return args + self._conditions.launch_args()
|
|
504
|
+
|
|
505
|
+
def _launch_env(self):
|
|
506
|
+
# The shim's variables win over the check's own launch_env: a check can't turn its network conditions off.
|
|
507
|
+
return {**self._live.launch_env, **(self._conditions.launch_env() if self._conditions is not None else {})}
|
|
508
|
+
|
|
509
|
+
def _send_push(self):
|
|
510
|
+
if self._conditions is None:
|
|
511
|
+
return
|
|
512
|
+
from . import conditions
|
|
513
|
+
try:
|
|
514
|
+
self._conditions.after_launch()
|
|
515
|
+
except conditions.ConditionError as error:
|
|
516
|
+
raise CouldntRun("environment", error.message, error.fix) from None
|
|
517
|
+
|
|
518
|
+
def _stale_process(self):
|
|
519
|
+
"""The app's process ID when it is WDA's foreground app now, before a launch replaces it; else None."""
|
|
520
|
+
bundle, pid = self._front()
|
|
521
|
+
return pid if bundle == self.app.bundle_id else None
|
|
522
|
+
|
|
523
|
+
def _await_foreground(self, stale=None):
|
|
524
|
+
# Just after a launch WDA can name the replaced process, or the app with no process (28 Sep, Settings reruns).
|
|
525
|
+
bundle = self.app.bundle_id
|
|
526
|
+
started = _now()
|
|
527
|
+
deadline = started + FOREGROUND_WAIT_SECONDS
|
|
528
|
+
while True:
|
|
529
|
+
remaining = deadline - _now()
|
|
530
|
+
front, pid = self._front(timeout=max(1.0, remaining))
|
|
531
|
+
if front == bundle and (stale is None or pid not in (None, stale)):
|
|
532
|
+
if stale is not None:
|
|
533
|
+
self.log(f"{bundle} in front in process {pid} (was {stale}) after "
|
|
534
|
+
f"{_now() - started:.2f} s")
|
|
535
|
+
return True
|
|
536
|
+
if _now() >= deadline:
|
|
537
|
+
self.log(f"{bundle} was not in front {FOREGROUND_WAIT_SECONDS} s after launch"
|
|
538
|
+
+ (f" (still process {stale})" if front == bundle else ""))
|
|
539
|
+
return False
|
|
540
|
+
self._sleep(.2)
|
|
541
|
+
|
|
542
|
+
def _front(self, timeout=2):
|
|
543
|
+
"""WDA's foreground app as (bundle ID, process ID); (None, None) when WDA doesn't answer."""
|
|
544
|
+
try:
|
|
545
|
+
own = getattr(self.manager, "front_app", None)
|
|
546
|
+
bundle, pid = own(self.target) if own is not None else front_app(self.target.wda_url, timeout=timeout)
|
|
547
|
+
except Exception:
|
|
548
|
+
return None, None
|
|
549
|
+
if self.app is not None and bundle == self.app.bundle_id:
|
|
550
|
+
self._app_pid = pid
|
|
551
|
+
if self.evidence is not None:
|
|
552
|
+
self.evidence.saw(pid)
|
|
553
|
+
return bundle, pid
|
|
554
|
+
|
|
555
|
+
def _start_evidence(self):
|
|
556
|
+
# Evidence never costs a run: a failure here is logged and the run goes on without it.
|
|
557
|
+
from . import evidence
|
|
558
|
+
try:
|
|
559
|
+
root = project_root(self.run_dir) if self.runs_dir.parent.name == ".mobster" else None
|
|
560
|
+
self.evidence = evidence.for_run(self.manager, self.target, self.app, self.run_dir,
|
|
561
|
+
t0_wall=self._t0_wall, project_root=root, log=self.log)
|
|
562
|
+
if self.evidence is not None:
|
|
563
|
+
self.evidence.start()
|
|
564
|
+
except Exception as error:
|
|
565
|
+
self.evidence = None
|
|
566
|
+
self.log(f"evidence: not started ({type(error).__name__}: {error})")
|
|
567
|
+
|
|
568
|
+
def _stop_evidence(self):
|
|
569
|
+
if self.evidence is not None:
|
|
570
|
+
try:
|
|
571
|
+
self.evidence.stop()
|
|
572
|
+
except Exception as error:
|
|
573
|
+
self.log(f"evidence: stop failed ({type(error).__name__})")
|
|
574
|
+
|
|
575
|
+
def _crash(self, *, suspect):
|
|
576
|
+
"""The app's crash in this run (evidence.Evidence.crash), or None."""
|
|
577
|
+
if self.evidence is None:
|
|
578
|
+
return None
|
|
579
|
+
try:
|
|
580
|
+
return self.evidence.crash(suspect=suspect)
|
|
581
|
+
except Exception as error:
|
|
582
|
+
self.log(f"evidence: crash lookup failed ({type(error).__name__}: {error})")
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
def _crashed(self, crash, decided=None):
|
|
586
|
+
"""The verdict for a run whose app crashed: failed, class app_crashed, the crash in one sentence."""
|
|
587
|
+
from .crashes import headline
|
|
588
|
+
decided = decided or {"results": [], "tree": None, "stable": None, "frame": None, "alert": None}
|
|
589
|
+
return {**decided, "verdict": "failed", "klass": "app_crashed",
|
|
590
|
+
"message": headline(crash, self.app.name if self.app is not None else "The app"), "fix": CRASH_FIX}
|
|
591
|
+
|
|
592
|
+
def _foreground(self, timeout=5):
|
|
593
|
+
try:
|
|
594
|
+
return self.driver.active_app(timeout=timeout)
|
|
595
|
+
except Exception:
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
def _take_baseline(self):
|
|
599
|
+
# The first glance that agrees with the one before it (at most 3 s), so a launch screen isn't the flow's start.
|
|
600
|
+
deadline = _now() + BASELINE_SETTLE_SECONDS
|
|
601
|
+
tree = self.read_tree(glance=True)
|
|
602
|
+
while _now() < deadline:
|
|
603
|
+
self._sleep(SETTLE_FIRST_PAUSE)
|
|
604
|
+
again = self.read_tree(glance=True)
|
|
605
|
+
same = again.layout() == tree.layout()
|
|
606
|
+
tree = again
|
|
607
|
+
if same:
|
|
608
|
+
break
|
|
609
|
+
tree = self.read_tree()
|
|
610
|
+
held = [result.ok for result in evaluate_on(self.check.expect, tree)]
|
|
611
|
+
self.baseline = {"taken": True, "held": held}
|
|
612
|
+
self._take_business_baseline()
|
|
613
|
+
self.log(f"baseline: {sum(held)} of {len(held)} expectations held before the flow")
|
|
614
|
+
|
|
615
|
+
def read_tree(self, glance=False):
|
|
616
|
+
"""One evidence read of the app's tree; a glance (no ``visible``) never decides and falls back to one."""
|
|
617
|
+
if self.driver is None:
|
|
618
|
+
raise CouldntRun("wda", "The run has no WebDriverAgent connection yet")
|
|
619
|
+
try:
|
|
620
|
+
if glance:
|
|
621
|
+
try:
|
|
622
|
+
tree = read_tree_from(self.driver, pause=self._sleep, path=GLANCE_SOURCE_PATH)
|
|
623
|
+
tree.glance = True
|
|
624
|
+
return tree
|
|
625
|
+
except Exception:
|
|
626
|
+
pass # fail closed: the evidence read below decides, or fails the run
|
|
627
|
+
return read_tree_from(self.driver, pause=self._sleep)
|
|
628
|
+
except TreeError as error:
|
|
629
|
+
raise CouldntRun("wda", str(error)) from None
|
|
630
|
+
except CouldntRun:
|
|
631
|
+
raise
|
|
632
|
+
except Exception as error:
|
|
633
|
+
raise CouldntRun("wda", f"WebDriverAgent stopped answering ({type(error).__name__}).",
|
|
634
|
+
"Run `mobster sim doctor --fix`, then try again.") from None
|
|
635
|
+
|
|
636
|
+
def _frame_path(self, label):
|
|
637
|
+
self._frame_n += 1
|
|
638
|
+
label = re.sub(r"[^a-z0-9-]+", "-", str(label).lower()).strip("-") or "frame"
|
|
639
|
+
return self.run_dir / "frames" / f"{self._frame_n:02d}-{label}.jpg"
|
|
640
|
+
|
|
641
|
+
def _register_frame(self, path, label):
|
|
642
|
+
if self._late_screen_frame(label, path):
|
|
643
|
+
return None
|
|
644
|
+
relative = path.relative_to(self.run_dir).as_posix()
|
|
645
|
+
self.frames.append(relative)
|
|
646
|
+
if label == "step":
|
|
647
|
+
self.step_frames.append(relative)
|
|
648
|
+
if label in ("launch", "step"):
|
|
649
|
+
self._observe_screen(relative)
|
|
650
|
+
return path
|
|
651
|
+
|
|
652
|
+
def frame(self, label):
|
|
653
|
+
"""A screenshot as frames/NN-label.jpg (JPEG, quality 75, half the simulator's pixel width)."""
|
|
654
|
+
with self._screen_frame(label) as admitted:
|
|
655
|
+
if not admitted:
|
|
656
|
+
return None
|
|
657
|
+
path = self._frame_path(label)
|
|
658
|
+
try:
|
|
659
|
+
self._call(self.manager.screenshot, self.target, path, max_width=self._frame_width,
|
|
660
|
+
quality=FRAME_QUALITY)
|
|
661
|
+
if self._late_screen_frame(label, path):
|
|
662
|
+
return None
|
|
663
|
+
if self._frame_width is None:
|
|
664
|
+
self._frame_width = _halve(path)
|
|
665
|
+
except CouldntRun as error:
|
|
666
|
+
self.log(f"no {label} frame: {error.message}")
|
|
667
|
+
if label in ("launch", "step") and not self._late_screen_frame(label, path):
|
|
668
|
+
self._observe_screen(None)
|
|
669
|
+
return None
|
|
670
|
+
except Exception as error:
|
|
671
|
+
self.log(f"no {label} frame: {type(error).__name__}")
|
|
672
|
+
if label in ("launch", "step") and not self._late_screen_frame(label, path):
|
|
673
|
+
self._observe_screen(None)
|
|
674
|
+
return None
|
|
675
|
+
return self._register_frame(path, label)
|
|
676
|
+
|
|
677
|
+
def frame_from_bytes(self, data, label):
|
|
678
|
+
"""A frame from JPEG or PNG bytes (the image Smart saw), at the run's frame width."""
|
|
679
|
+
with self._screen_frame(label) as admitted:
|
|
680
|
+
if not admitted:
|
|
681
|
+
return None
|
|
682
|
+
path = self._frame_path(label)
|
|
683
|
+
try:
|
|
684
|
+
from io import BytesIO
|
|
685
|
+
|
|
686
|
+
from PIL import Image
|
|
687
|
+
image = Image.open(BytesIO(data)).convert("RGB")
|
|
688
|
+
if self._frame_width and image.width > self._frame_width:
|
|
689
|
+
image = image.resize((self._frame_width, round(image.height * self._frame_width / image.width)))
|
|
690
|
+
image.save(path, "JPEG", quality=FRAME_QUALITY)
|
|
691
|
+
if self._late_screen_frame(label, path):
|
|
692
|
+
return None
|
|
693
|
+
except Exception as error:
|
|
694
|
+
self.log(f"no {label} frame: {type(error).__name__}")
|
|
695
|
+
if label in ("launch", "step") and not self._late_screen_frame(label, path):
|
|
696
|
+
self._observe_screen(None)
|
|
697
|
+
return None
|
|
698
|
+
return self._register_frame(path, label)
|
|
699
|
+
|
|
700
|
+
def record_step(self, *, op, text, target=None, typed=None, changed=None, frame=None):
|
|
701
|
+
"""One step (§5.3) for the result and `wait`'s steps_so_far; key-less actions also go to events.jsonl."""
|
|
702
|
+
text, typed = self._scrub(text), self._scrub(typed)
|
|
703
|
+
step = {"index": len(self.steps) + 1, "op": op, "text": text,
|
|
704
|
+
"target": ({"id": target.get("id"), "label": target.get("label"), "role": target.get("role")}
|
|
705
|
+
if target else None),
|
|
706
|
+
"typed": typed, "changed": changed, "frame": self._relative(frame), "at_ms": self._ms()}
|
|
707
|
+
step = project_env.mask_deep(step, self.secrets)
|
|
708
|
+
self.steps.append(step)
|
|
709
|
+
if self.mode != "smart":
|
|
710
|
+
self.event({"event": "step", **step})
|
|
711
|
+
self.log(f"step {step['index']}: {text}")
|
|
712
|
+
return step
|
|
713
|
+
|
|
714
|
+
def _relative(self, frame):
|
|
715
|
+
"""A frame path as the result writes it: relative to run_dir."""
|
|
716
|
+
if not frame:
|
|
717
|
+
return None
|
|
718
|
+
path = Path(frame)
|
|
719
|
+
try:
|
|
720
|
+
return (path.relative_to(self.run_dir) if path.is_absolute() else path).as_posix()
|
|
721
|
+
except ValueError:
|
|
722
|
+
return str(path)
|
|
723
|
+
|
|
724
|
+
def relaunch(self):
|
|
725
|
+
"""Terminate the app and launch it again with the same arguments and environment, keeping its data."""
|
|
726
|
+
with self.lock:
|
|
727
|
+
bundle = self.app.bundle_id
|
|
728
|
+
stale = self._stale_process()
|
|
729
|
+
with self.actions.action("TERMINATE"):
|
|
730
|
+
self._call(self.manager.terminate, self.target, bundle)
|
|
731
|
+
args = tuple(self._live.launch_args)
|
|
732
|
+
if self._conditions is not None:
|
|
733
|
+
args += self._conditions.launch_args()
|
|
734
|
+
if self.business_fixture is not None:
|
|
735
|
+
args += ("-MobsterBusinessRun", self.business_fixture.token)
|
|
736
|
+
with self.actions.action("LAUNCH"):
|
|
737
|
+
self._call(self.manager.launch, self.target, bundle, args, self._launch_env())
|
|
738
|
+
self._await_foreground(stale)
|
|
739
|
+
|
|
740
|
+
def open_url(self, url):
|
|
741
|
+
"""A deep link into the running app."""
|
|
742
|
+
from .checks import URL_SCHEME
|
|
743
|
+
if not isinstance(url, str) or not URL_SCHEME.match(url) or len(url) > 2000:
|
|
744
|
+
from .checks import CheckError
|
|
745
|
+
raise CheckError("the link needs a scheme, such as yourapp://lesson")
|
|
746
|
+
with self.lock:
|
|
747
|
+
self._open_link(url)
|
|
748
|
+
|
|
749
|
+
def evaluate(self, assertions, *, timeout=5):
|
|
750
|
+
with self.lock:
|
|
751
|
+
results, _, _ = self._settle(assertions, timeout)
|
|
752
|
+
return results
|
|
753
|
+
|
|
754
|
+
def _settle(self, assertions, timeout):
|
|
755
|
+
timeout = max(0.0, float(timeout))
|
|
756
|
+
deadline = _now() + timeout
|
|
757
|
+
previous = self._timed_read(0, glance=True)
|
|
758
|
+
self._sleep(SETTLE_FIRST_PAUSE)
|
|
759
|
+
reads = 1
|
|
760
|
+
while True:
|
|
761
|
+
tree = self._timed_read(reads)
|
|
762
|
+
reads += 1
|
|
763
|
+
# A glance has no ``visible``, so the first pair compares layouts; evidence pairs compare exactly.
|
|
764
|
+
stable = (tree.layout() == previous.layout() if previous.glance
|
|
765
|
+
else tree.fingerprint() == previous.fingerprint())
|
|
766
|
+
results = evaluate_on(assertions, tree)
|
|
767
|
+
held = sum(result.ok for result in results)
|
|
768
|
+
self.log(f" {'same screen' if stable else 'screen changed'}, {held} of {len(results)} held")
|
|
769
|
+
if stable and held == len(results):
|
|
770
|
+
return results, tree, True
|
|
771
|
+
now = _now()
|
|
772
|
+
if now >= deadline:
|
|
773
|
+
return results, tree, stable
|
|
774
|
+
self._sleep(min(SETTLE_PAUSE, deadline - now))
|
|
775
|
+
previous = tree
|
|
776
|
+
|
|
777
|
+
def _timed_read(self, index, glance=False):
|
|
778
|
+
started = time.monotonic()
|
|
779
|
+
tree = self.read_tree(glance=glance)
|
|
780
|
+
self.log(f"read {index + 1}: {round((time.monotonic() - started) * 1000)} ms, {len(tree.shown())} shown"
|
|
781
|
+
f"{' (glance)' if tree.glance else ''}")
|
|
782
|
+
return tree
|
|
783
|
+
|
|
784
|
+
def _audit_findings(self):
|
|
785
|
+
"""The findings of the check's static audits, run on the built app."""
|
|
786
|
+
names = [name for name in self.check.audit if name not in (A11Y, device_audit.NAME)]
|
|
787
|
+
if not names:
|
|
788
|
+
return []
|
|
789
|
+
from . import audit
|
|
790
|
+
if not self.check.app_path:
|
|
791
|
+
return [{**audit.Finding("audit-needs-app", "2.1", "review",
|
|
792
|
+
"The audit reads a built app, and the check names none. Add app.path.",
|
|
793
|
+
"the check").to_dict(), "frame": None}]
|
|
794
|
+
out = []
|
|
795
|
+
for name in names:
|
|
796
|
+
out.extend({**finding.to_dict(), "frame": None} for finding in audit.run_audit(name, self.check.app_path))
|
|
797
|
+
return out
|
|
798
|
+
|
|
799
|
+
def _give_after_frame(self, path):
|
|
800
|
+
"""``path`` becomes the frame of every Smart step still waiting for the screen after it."""
|
|
801
|
+
# A turn's actions share one frame: Smart sees no image between them.
|
|
802
|
+
for step in self._awaiting_frame:
|
|
803
|
+
step["frame"] = self._relative(path)
|
|
804
|
+
self._awaiting_frame.clear()
|
|
805
|
+
|
|
806
|
+
def abort(self, klass, message, fix=""):
|
|
807
|
+
"""End the run as couldnt_run with ``klass``, release the simulator; a preparing run stops at its next step."""
|
|
808
|
+
self._stop.set()
|
|
809
|
+
dispatching = self.actions.request_stop()
|
|
810
|
+
action_failure = self._action_failure()
|
|
811
|
+
if action_failure is not None:
|
|
812
|
+
klass, message, fix = action_failure.klass, action_failure.message, action_failure.fix
|
|
813
|
+
elif dispatching:
|
|
814
|
+
klass, message, fix = ("unknown_outcome", "An action may have reached the device before the run stopped.",
|
|
815
|
+
"Inspect the run's actions.jsonl and device before another run.")
|
|
816
|
+
with self._finish_lock: # prepare() changes the state under this lock, so the check below holds
|
|
817
|
+
if self.state == "preparing":
|
|
818
|
+
try:
|
|
819
|
+
if self.result is None:
|
|
820
|
+
if (self.business_fixture is not None and self.business_result is not None
|
|
821
|
+
and (self.business_fixture.seeded or self.business_result["setup"] == "pending")):
|
|
822
|
+
self.business_result["cleanup"] = "needs_reconciliation"
|
|
823
|
+
if self._conditions is not None and self.conditions_cleanup == "pending":
|
|
824
|
+
self.conditions_cleanup = "needs_reconciliation"
|
|
825
|
+
self.error = {"klass": klass, "message": message, "fix": fix or ""}
|
|
826
|
+
self.log(f"couldn't run ({klass}): {message}")
|
|
827
|
+
result = self._compose(None)
|
|
828
|
+
self._write(result)
|
|
829
|
+
self.result = result
|
|
830
|
+
self.state = "finished" # the preparing thread releases the simulator at its next phase
|
|
831
|
+
return self.result
|
|
832
|
+
finally:
|
|
833
|
+
self._cancel_build()
|
|
834
|
+
with self.lock:
|
|
835
|
+
if self.result is None and self.error is None:
|
|
836
|
+
self._fail(CouldntRun(klass, message, fix))
|
|
837
|
+
return self.finish()
|
|
838
|
+
|
|
839
|
+
def _cancel_build(self):
|
|
840
|
+
# The build (up to 20 minutes the first time) runs in its own process group, so nothing else would stop it.
|
|
841
|
+
cancel = getattr(getattr(self.manager, "wda", None), "cancel", None)
|
|
842
|
+
if callable(cancel):
|
|
843
|
+
try:
|
|
844
|
+
cancel()
|
|
845
|
+
except Exception as error:
|
|
846
|
+
self.log(f"stopping the WebDriverAgent build failed: {type(error).__name__}")
|
|
847
|
+
|
|
848
|
+
def fail_internal(self, error):
|
|
849
|
+
"""An unexpected exception: couldnt_run, class internal; the traceback goes to run.log only."""
|
|
850
|
+
self.log("".join(traceback.format_exception(type(error), error, error.__traceback__)))
|
|
851
|
+
self.error = {"klass": "internal", "message": f"Mobster hit an unexpected error ({type(error).__name__}).",
|
|
852
|
+
"fix": "run.log in the run folder has the details; please report it."}
|
|
853
|
+
|
|
854
|
+
def finish(self):
|
|
855
|
+
"""The verdict (§4) and every artifact: result.json, report.html, check.yaml, the verdict frames."""
|
|
856
|
+
# Always lock, then _finish_lock: abort() is called with lock held while another thread may call finish().
|
|
857
|
+
with self.lock, self._finish_lock:
|
|
858
|
+
if self.result is not None:
|
|
859
|
+
if not self._preparing: # else the preparing thread releases it, at its next phase
|
|
860
|
+
self._release() # idempotent: a result written while preparing still frees the simulator
|
|
861
|
+
return self.result
|
|
862
|
+
self._seal_screen_frames()
|
|
863
|
+
interrupted = None
|
|
864
|
+
try:
|
|
865
|
+
decided = self._decide()
|
|
866
|
+
except CouldntRun as error:
|
|
867
|
+
# WebDriverAgent losing an app that crashed during the verdict read is the crash.
|
|
868
|
+
crash = self._crash(suspect=True) if error.klass in CRASH_EXPLAINS else None
|
|
869
|
+
decided = self._crashed(crash) if crash else None
|
|
870
|
+
if decided is None:
|
|
871
|
+
self._fail(error)
|
|
872
|
+
except KeyboardInterrupt as error:
|
|
873
|
+
# ctrl+c during the verdict read: the run still ends as stopped with every artifact, then it goes on.
|
|
874
|
+
if self.error is None:
|
|
875
|
+
self._fail(CouldntRun("stopped", "Stopped with ctrl+c."))
|
|
876
|
+
decided, interrupted = None, error
|
|
877
|
+
except Exception as error:
|
|
878
|
+
self.fail_internal(error)
|
|
879
|
+
decided = None
|
|
880
|
+
self._stop_evidence() # the log ends with the verdict read
|
|
881
|
+
primary = (
|
|
882
|
+
{"verdict": decided["verdict"], "class": decided["klass"]} if decided is not None
|
|
883
|
+
else {"verdict": "couldnt_run", "class": self.error["klass"]} if self.error is not None
|
|
884
|
+
else None
|
|
885
|
+
)
|
|
886
|
+
cleanup = self._cleanup_after_run()
|
|
887
|
+
failure = self._action_failure() or cleanup
|
|
888
|
+
if self.error is not None and self.error["klass"] == "unknown_outcome":
|
|
889
|
+
failure = None # a later cleanup failure cannot replace a prior unknown effect
|
|
890
|
+
decided = None
|
|
891
|
+
elif failure is not None or cleanup is not None:
|
|
892
|
+
if self.business_result is not None and primary is not None:
|
|
893
|
+
self.business_result["primary"] = primary
|
|
894
|
+
failed = self._fail(failure or cleanup)
|
|
895
|
+
if decided is not None:
|
|
896
|
+
decided = {**decided, "verdict": "couldnt_run", "klass": failed.klass,
|
|
897
|
+
"message": failed.message, "fix": failed.fix}
|
|
898
|
+
result = self._compose(decided)
|
|
899
|
+
if self.replay is not None:
|
|
900
|
+
try:
|
|
901
|
+
self.replay.after(result)
|
|
902
|
+
except Exception as error: # the cache never costs a run its verdict
|
|
903
|
+
self.log("".join(traceback.format_exception(type(error), error, error.__traceback__)))
|
|
904
|
+
try:
|
|
905
|
+
self._write(result)
|
|
906
|
+
finally:
|
|
907
|
+
self._release()
|
|
908
|
+
self.result = result
|
|
909
|
+
self.state = "finished"
|
|
910
|
+
if interrupted is not None:
|
|
911
|
+
raise interrupted
|
|
912
|
+
return result
|
|
913
|
+
|
|
914
|
+
def _halve(path):
|
|
915
|
+
"""Resize the first frame to half its pixel width (603 px from an iPhone 17 Pro's 1206); returns that width."""
|
|
916
|
+
from PIL import Image
|
|
917
|
+
with Image.open(path) as image:
|
|
918
|
+
image = image.convert("RGB")
|
|
919
|
+
width = max(1, image.width // 2)
|
|
920
|
+
image.resize((width, max(1, round(image.height * width / image.width)))).save(path, "JPEG",
|
|
921
|
+
quality=FRAME_QUALITY)
|
|
922
|
+
return width
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def resolve_mode(check, mode):
|
|
926
|
+
"""The mode a run uses: auto means Smart with steps, launch-only without."""
|
|
927
|
+
if mode == "auto":
|
|
928
|
+
return "smart" if check.steps else "launch"
|
|
929
|
+
if mode not in MODES:
|
|
930
|
+
raise ValueError(f"mode must be auto or one of {', '.join(MODES)}")
|
|
931
|
+
return mode
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def verify(check, *, mode="auto", runs_dir, manager=None, progress=None, key=None, cancelled=None,
|
|
935
|
+
assert_timeout=5):
|
|
936
|
+
"""The whole run -> the result dict (§5.3)."""
|
|
937
|
+
from . import fixtures
|
|
938
|
+
from .system_surfaces.play import drive as drive_surfaces
|
|
939
|
+
try:
|
|
940
|
+
check = fixtures.for_run(check, runs_dir)
|
|
941
|
+
except fixtures.FixtureError as error:
|
|
942
|
+
run = VerifyRun(check, mode=resolve_mode(check, mode), runs_dir=runs_dir, manager=manager,
|
|
943
|
+
progress=progress, key=key, assert_timeout=assert_timeout)
|
|
944
|
+
return run.abort("usage", str(error), "`mobster fixtures list` shows the project's fixtures.")
|
|
945
|
+
mode = resolve_mode(check, mode)
|
|
946
|
+
run = VerifyRun(check, mode=mode, runs_dir=runs_dir, manager=manager, progress=progress, key=key,
|
|
947
|
+
assert_timeout=assert_timeout)
|
|
948
|
+
if check.steps and mode == "launch":
|
|
949
|
+
return run.abort("usage", "--keyless never calls a model, so it can't run steps.",
|
|
950
|
+
"Drop the steps for a launch-only check, or let your coding agent drive through "
|
|
951
|
+
"`mobster mcp`.")
|
|
952
|
+
if check.steps and mode == "keyless":
|
|
953
|
+
return run.abort("usage", "A key-less run with steps needs a coding agent to drive it.",
|
|
954
|
+
"Run it through `mobster mcp`, or drop the steps for a launch-only check.")
|
|
955
|
+
if mode == "smart":
|
|
956
|
+
from ..engines import smart_key
|
|
957
|
+
run.key = key or smart_key()
|
|
958
|
+
if not run.key:
|
|
959
|
+
why, fix = no_smart_key()
|
|
960
|
+
return run.abort("usage", f"These steps need Smart, and {why}.", fix)
|
|
961
|
+
try:
|
|
962
|
+
run.prepare()
|
|
963
|
+
if mode == "smart":
|
|
964
|
+
run.run_smart(cancelled)
|
|
965
|
+
drive_surfaces(run)
|
|
966
|
+
except CouldntRun as error:
|
|
967
|
+
if run.error is None:
|
|
968
|
+
run._fail(error)
|
|
969
|
+
except KeyboardInterrupt as stop:
|
|
970
|
+
# devtools.Terminated (SIGTERM, SIGHUP) is a KeyboardInterrupt too; the result rides on it for the CLI's JSON.
|
|
971
|
+
name = getattr(stop, "signum", None)
|
|
972
|
+
stop.result = run.abort("stopped", "Stopped with ctrl+c." if name is None else f"Stopped by {stop}.")
|
|
973
|
+
raise
|
|
974
|
+
except Exception as error:
|
|
975
|
+
run.fail_internal(error)
|
|
976
|
+
return run.finish()
|