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,255 @@
|
|
|
1
|
+
"""Per-check test history; a file that can't be read is set aside as history.json.corrupt and never stops a suite."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from statistics import median
|
|
7
|
+
|
|
8
|
+
from ..env_file import writer
|
|
9
|
+
from ..paths import write_text_atomic
|
|
10
|
+
from ..verify import perf
|
|
11
|
+
|
|
12
|
+
SCHEMA = "mobster.test-history/1"
|
|
13
|
+
XCTEST_FILE = "xctest:" # the file of a history entry that is an XCTest case: xctest:Target/Class/test
|
|
14
|
+
FILE = "history.json"
|
|
15
|
+
KEEP, MAX_CHECKS, MAX_TESTS = 20, 400, 2000
|
|
16
|
+
MIN_RUNS, FLAKY_AT = 5, 0.15
|
|
17
|
+
TREND_AT, TREND_MIN = 0.25, 4
|
|
18
|
+
KEPT = ("passed", "failed", "needs_review")
|
|
19
|
+
# A suite's folder in .mobster/test-results (reports.make_suite_dir): its name sorts by when it started.
|
|
20
|
+
SUITE_FOLDER = re.compile(r"^[0-9]{8}-[0-9]{6}-[0-9a-f]{4}$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def path_of(root):
|
|
24
|
+
return Path(root) / ".mobster" / "test-results" / FILE
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def key_of(check, deviceKey):
|
|
28
|
+
return (f"{check.get('file') or check.get('name')}|{deviceKey}"
|
|
29
|
+
+ (f"|{check['locale']}" if check.get("locale") else ""))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _run(item):
|
|
33
|
+
"""A stored run, checked at the boundary; None when it isn't one."""
|
|
34
|
+
if not isinstance(item, dict) or item.get("status") not in KEPT:
|
|
35
|
+
return None
|
|
36
|
+
ms, attempts = item.get("ms"), item.get("attempts")
|
|
37
|
+
return {"suite": str(item.get("suite") or "")[:40], "at": item["at"] if isinstance(item.get("at"), int) else None,
|
|
38
|
+
"status": item["status"], "flaky": item.get("flaky") is True,
|
|
39
|
+
# A budget the run couldn't measure (a missed reading) is neither a pass nor a failure of the app.
|
|
40
|
+
"unmeasured": item.get("unmeasured") is True,
|
|
41
|
+
"ms": ms if isinstance(ms, int) and ms >= 0 else None,
|
|
42
|
+
"attempts": attempts if isinstance(attempts, int) and attempts > 0 else 1,
|
|
43
|
+
"perf": perf.clean(item.get("perf"))}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load(root):
|
|
47
|
+
"""(history, ok): {key: {"name", "file", "device", "runs": [...]}} (oldest run first) and whether the file was
|
|
48
|
+
absent or readable."""
|
|
49
|
+
try:
|
|
50
|
+
text = path_of(root).read_text(encoding="utf-8")
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
return {}, True
|
|
53
|
+
except (OSError, UnicodeError):
|
|
54
|
+
return {}, False
|
|
55
|
+
try:
|
|
56
|
+
data = json.loads(text)
|
|
57
|
+
checks = data["checks"]
|
|
58
|
+
if data.get("schema") != SCHEMA or not isinstance(checks, dict):
|
|
59
|
+
raise ValueError
|
|
60
|
+
except (ValueError, KeyError, TypeError, AttributeError):
|
|
61
|
+
return {}, False
|
|
62
|
+
out = {}
|
|
63
|
+
for key, entry in checks.items():
|
|
64
|
+
if not isinstance(key, str) or not isinstance(entry, dict) or not isinstance(entry.get("runs"), list):
|
|
65
|
+
continue
|
|
66
|
+
runs = [run for run in map(_run, entry["runs"]) if run is not None][-KEEP:]
|
|
67
|
+
if runs:
|
|
68
|
+
out[key] = {"name": str(entry.get("name") or "")[:200], "file": entry.get("file") if isinstance(
|
|
69
|
+
entry.get("file"), str) else None, "device": str(entry.get("device") or "")[:200], "runs": runs}
|
|
70
|
+
return out, True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_test(entry):
|
|
74
|
+
return str(entry.get("file") or "").startswith(XCTEST_FILE)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _save(root, history):
|
|
78
|
+
# A project's tests are many and its checks few: each kind keeps its newest, so tests never push checks out.
|
|
79
|
+
for is_test, limit in ((False, MAX_CHECKS), (True, MAX_TESTS)):
|
|
80
|
+
keys = [key for key, entry in history.items() if _is_test(entry) is is_test]
|
|
81
|
+
for key in sorted(keys, key=lambda k: history[k]["runs"][-1].get("at") or 0)[:max(0, len(keys) - limit)]:
|
|
82
|
+
del history[key]
|
|
83
|
+
write_text_atomic(path_of(root), json.dumps({"schema": SCHEMA, "checks": history}, ensure_ascii=False) + "\n")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _set_aside(root):
|
|
87
|
+
source = path_of(root)
|
|
88
|
+
try:
|
|
89
|
+
source.replace(source.with_name(FILE + ".corrupt"))
|
|
90
|
+
except OSError:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def stats(runs):
|
|
95
|
+
"""One check's numbers over its kept runs (oldest first)."""
|
|
96
|
+
runs = list(runs)[-KEEP:]
|
|
97
|
+
judged = [run for run in runs if not run.get("unmeasured")]
|
|
98
|
+
n = len(judged)
|
|
99
|
+
passed = [run["status"] == "passed" for run in judged]
|
|
100
|
+
flaky_runs = sum(run["flaky"] for run in judged)
|
|
101
|
+
changes = sum(a != b for a, b in zip(passed, passed[1:]))
|
|
102
|
+
score = round(max(flaky_runs / n, changes / (n - 1) if n > 1 else 0.0), 3) if n else 0.0
|
|
103
|
+
if n < MIN_RUNS:
|
|
104
|
+
state = "new"
|
|
105
|
+
elif not passed[-1]:
|
|
106
|
+
state = "failing"
|
|
107
|
+
elif score >= FLAKY_AT:
|
|
108
|
+
state = "flaky"
|
|
109
|
+
else:
|
|
110
|
+
state = "stable"
|
|
111
|
+
return {"runs": n, "passRate": round(sum(passed) / n, 4) if n else None, "flakyRuns": flaky_runs,
|
|
112
|
+
"flakeScore": score, "state": state, "lastStatus": runs[-1]["status"] if runs else None,
|
|
113
|
+
"lastAt": runs[-1]["at"] if runs else None,
|
|
114
|
+
"durationMs": [run["ms"] for run in runs], "trend": _trend([run["ms"] for run in runs]),
|
|
115
|
+
"outcomes": [run["status"] for run in runs], "perf": perf.trend(runs), "unmeasured": len(runs) - n,
|
|
116
|
+
"perfLatest": (runs[-1].get("perf") or {}) if runs else {}}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _trend(durations):
|
|
120
|
+
"""slower, faster or steady: the newer half's median time against the older half's; None with too few times."""
|
|
121
|
+
known = [ms for ms in durations if ms is not None]
|
|
122
|
+
if len(known) < TREND_MIN:
|
|
123
|
+
return None
|
|
124
|
+
half = len(known) // 2
|
|
125
|
+
before, after = median(known[:half]), median(known[-half:])
|
|
126
|
+
if before <= 0:
|
|
127
|
+
return None
|
|
128
|
+
change = (after - before) / before
|
|
129
|
+
return "slower" if change > TREND_AT else "faster" if change < -TREND_AT else "steady"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def describe(entry):
|
|
133
|
+
"""An entry with its stats, and whether to suggest quarantine; what the CLI, the API and the report show."""
|
|
134
|
+
found = stats(entry["runs"])
|
|
135
|
+
return {"name": entry["name"], "file": entry["file"], "device": entry["device"], **found,
|
|
136
|
+
"suggestQuarantine": found["state"] == "flaky"}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _items(value):
|
|
140
|
+
"""The dicts in ``value`` when it is a list; results.json is external data, so anything else is skipped."""
|
|
141
|
+
items = value if isinstance(value, list) else []
|
|
142
|
+
return [item for item in items if isinstance(item, dict)]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _cells(doc):
|
|
146
|
+
"""(check, result) for every check result in ``doc``, and for every XCTest case: a case is the result of a check
|
|
147
|
+
named for its class and test, whose file is xctest:Target/Class/test."""
|
|
148
|
+
for check in _items(doc.get("checks")):
|
|
149
|
+
for result in _items(check.get("results")):
|
|
150
|
+
yield check, result
|
|
151
|
+
xctest = doc.get("xctest")
|
|
152
|
+
for target in _items(xctest.get("targets") if isinstance(xctest, dict) else None):
|
|
153
|
+
for case in _items(target.get("cases")):
|
|
154
|
+
if not all(isinstance(case.get(key), str) for key in ("classname", "name")) or not isinstance(
|
|
155
|
+
target.get("name"), str):
|
|
156
|
+
continue
|
|
157
|
+
yield {"name": f"{case['classname']}.{case['name']}",
|
|
158
|
+
"file": f"{XCTEST_FILE}{target['name']}/{case['classname']}/{case['name']}"}, case
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _add(history, doc):
|
|
162
|
+
"""Fold the finished suite ``doc`` into ``history`` once (a finished suite is immutable); the keys it added to."""
|
|
163
|
+
added = set()
|
|
164
|
+
for check, result in _cells(doc):
|
|
165
|
+
reason = result.get("reason")
|
|
166
|
+
found = _run({"suite": doc.get("suite"), "at": doc.get("startedAt"), "status": result.get("status"),
|
|
167
|
+
"flaky": bool(result.get("flaky")), "ms": result.get("durationMs"),
|
|
168
|
+
"unmeasured": isinstance(reason, dict) and reason.get("class") == "budget_unmeasured",
|
|
169
|
+
"attempts": result.get("attempts"), "perf": perf.numbers(result.get("perf"))})
|
|
170
|
+
if found is None:
|
|
171
|
+
continue
|
|
172
|
+
key = key_of(check, result.get("deviceKey"))
|
|
173
|
+
entry = history.setdefault(key, {"runs": []})
|
|
174
|
+
entry.update(name=check.get("name"), file=check.get("file"), device=result.get("device"))
|
|
175
|
+
if found["suite"] and any(run["suite"] == found["suite"] for run in entry["runs"]):
|
|
176
|
+
continue
|
|
177
|
+
entry["runs"] = (entry["runs"] + [found])[-KEEP:]
|
|
178
|
+
added.add(key)
|
|
179
|
+
return added
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _unrecorded(root, history):
|
|
183
|
+
"""The project's last KEEP suites that ``history`` has no run of (from before the file existed), oldest first."""
|
|
184
|
+
folder = path_of(root).parent
|
|
185
|
+
try:
|
|
186
|
+
names = sorted(item.name for item in folder.iterdir() if SUITE_FOLDER.match(item.name))[-KEEP:]
|
|
187
|
+
except OSError:
|
|
188
|
+
return []
|
|
189
|
+
known = {run["suite"] for entry in history.values() for run in entry["runs"]}
|
|
190
|
+
docs = []
|
|
191
|
+
for name in names:
|
|
192
|
+
if name in known:
|
|
193
|
+
continue
|
|
194
|
+
try:
|
|
195
|
+
doc = json.loads((folder / name / "results.json").read_text(encoding="utf-8"))
|
|
196
|
+
except (OSError, ValueError, UnicodeError):
|
|
197
|
+
continue
|
|
198
|
+
if isinstance(doc, dict) and str(doc.get("suite") or name)[:40] not in known:
|
|
199
|
+
docs.append({**doc, "suite": doc.get("suite") or name})
|
|
200
|
+
return docs
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def record(root, doc):
|
|
204
|
+
try:
|
|
205
|
+
with writer(path_of(root)):
|
|
206
|
+
history, ok = load(root)
|
|
207
|
+
if not ok:
|
|
208
|
+
_set_aside(root)
|
|
209
|
+
_add(history, doc)
|
|
210
|
+
_save(root, history)
|
|
211
|
+
suggested = []
|
|
212
|
+
for check, result in _cells(doc):
|
|
213
|
+
entry = history.get(key_of(check, result.get("deviceKey")))
|
|
214
|
+
if entry is None or result.get("status") not in KEPT:
|
|
215
|
+
continue
|
|
216
|
+
result["history"] = stats(entry["runs"])
|
|
217
|
+
# A test isn't a check: the quarantine file can't name it, so its flake score is shown and not suggested.
|
|
218
|
+
if (result["history"]["state"] == "flaky" and not check.get("quarantined")
|
|
219
|
+
and not _is_test(check)):
|
|
220
|
+
suggested.append({"check": check.get("name"), "file": check.get("file"),
|
|
221
|
+
"device": result.get("device"), "flakeScore": result["history"]["flakeScore"]})
|
|
222
|
+
doc["history"] = {"suggestQuarantine": suggested}
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
return doc
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def flaky_reasons(root):
|
|
229
|
+
"""{check file: reason} for checks whose history is flaky on any device, for --auto-quarantine."""
|
|
230
|
+
history, _ = load(root)
|
|
231
|
+
entries = [(entry, stats(entry["runs"])) for entry in history.values()]
|
|
232
|
+
# Quarantine applies to the whole check, so a failure on any recorded device vetoes it.
|
|
233
|
+
failing = {entry["file"] for entry, found in entries if found["lastStatus"] != "passed"}
|
|
234
|
+
return {entry["file"]: (f"flaky: passed {round(found['passRate'] * 100)}% of its last "
|
|
235
|
+
f"{found['runs']} runs on {entry['device']}")
|
|
236
|
+
for entry, found in entries if found["state"] == "flaky" and entry["file"]
|
|
237
|
+
and entry["file"] not in failing}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def report(root, *, only=None):
|
|
241
|
+
"""Every check's history under ``root`` as ``describe`` shows it, flaky first, then by name; ``ok`` False when the
|
|
242
|
+
file couldn't be read. Suites the file has none of are folded in, by when they started (never written back)."""
|
|
243
|
+
history, ok = load(root)
|
|
244
|
+
added = set()
|
|
245
|
+
for doc in _unrecorded(root, history):
|
|
246
|
+
added |= _add(history, doc)
|
|
247
|
+
for key in added:
|
|
248
|
+
runs = sorted(history[key]["runs"], key=lambda run: run["at"] or 0)
|
|
249
|
+
history[key]["runs"] = runs[-KEEP:]
|
|
250
|
+
rows = [describe(entry) for entry in history.values() if only is None or only(entry)]
|
|
251
|
+
rows.sort(key=lambda row: (row["state"] != "flaky", -row["flakeScore"], str(row["name"]), row["device"]))
|
|
252
|
+
return {"checks": rows, "ok": ok}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = ["record", "load", "stats", "describe", "report", "flaky_reasons", "KEEP", "FILE", "SCHEMA"]
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Bounded, text-only suite results for a hosted caller.
|
|
2
|
+
|
|
3
|
+
The source document may contain app text, secrets, device IDs and paths. Only fields named in ``project`` enter the
|
|
4
|
+
export. The original results, reports, screenshots and logs remain on the enrolled Mac. The caller authorizes a suite
|
|
5
|
+
before passing its frozen artifact bytes to ``manifest`` or ``chunk``; this module never reads a file.
|
|
6
|
+
The caller binds expected targets; a green admitted export proves coverage only of targets declared in its document.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import binascii
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from . import reports
|
|
17
|
+
from .suite import SAFETY_EXIT_STOPS
|
|
18
|
+
|
|
19
|
+
SCHEMA = "mobster.test.hosted/1"
|
|
20
|
+
MANIFEST_SCHEMA = "mobster.test.artifacts/1"
|
|
21
|
+
MAX_CHECKS = 500
|
|
22
|
+
MAX_TARGETS = 20
|
|
23
|
+
MAX_ARTIFACT = 2 * 1024 * 1024
|
|
24
|
+
CHUNK_BYTES = 32 * 1024
|
|
25
|
+
MAX_DURATION_MS = 30 * 86_400_000
|
|
26
|
+
MAX_TIME_MS = 10**13
|
|
27
|
+
KINDS = {"json": ("results.json", "application/json"), "junit": ("junit.xml", "application/xml")}
|
|
28
|
+
VERDICTS = frozenset({"passed", "failed", "needs_review", "couldnt_run"})
|
|
29
|
+
REASONS = frozenset({"assertion", "blocked", "budget", "drift", "app_not_running", "no_assertions",
|
|
30
|
+
"app_crashed",
|
|
31
|
+
"held_before_flow", "usage", "model", "environment", "simulator", "wda", "install",
|
|
32
|
+
"launch", "device", "stopped", "journal", "unknown_outcome", "internal",
|
|
33
|
+
"not_developer_build", "build", "business", "business_evidence", "business_cleanup"})
|
|
34
|
+
STOP_REASONS = frozenset((*SAFETY_EXIT_STOPS, "stopped"))
|
|
35
|
+
MESSAGES = {"failed": "Check failed.", "needs_review": "Check needs review.",
|
|
36
|
+
"couldnt_run": "Check could not run."}
|
|
37
|
+
SUITE_ID = re.compile(r"[0-9]{8}-[0-9]{6}-[0-9a-f]{4}\Z")
|
|
38
|
+
ARTIFACT_ID = re.compile(r"[a-f0-9]{24}\Z")
|
|
39
|
+
ENROLLED_ID = re.compile(r"[a-f0-9]{12}\Z")
|
|
40
|
+
DIGEST = re.compile(r"[a-f0-9]{64}\Z")
|
|
41
|
+
MODEL = re.compile(r"(?:iPhone (?:[0-9]{1,2}(?: Pro Max| Pro| Plus| mini)?|SE(?: \([1-9](?:st|nd|rd|th) "
|
|
42
|
+
r"generation\))?)|iPad(?: Air| mini| Pro)?(?: [0-9]{1,2}(?:-inch)?)?"
|
|
43
|
+
r"(?: \([A-Z][0-9]\))?)\Z")
|
|
44
|
+
RUNTIME = re.compile(r"iOS [0-9]{1,2}(?:\.[0-9]{1,2}){0,2}\Z")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExportError(ValueError):
|
|
48
|
+
"""A canonical result cannot be safely exported."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InputsChanged(ExportError):
|
|
52
|
+
"""A check ran on inputs other than the admitted suite's: it can't be reported under that identity."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ArtifactTooLarge(ExportError):
|
|
56
|
+
"""A safe artifact would exceed the hosted size limit."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ArtifactNotFound(KeyError):
|
|
60
|
+
"""An artifact ID does not name one of this suite's frozen exports."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _integer(value, maximum):
|
|
64
|
+
if type(value) is not int or not 0 <= value <= maximum:
|
|
65
|
+
raise ExportError("Invalid suite result number")
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ratio(value):
|
|
70
|
+
if type(value) not in (float, int) or not 0 <= value <= 1 or not math.isfinite(value):
|
|
71
|
+
raise ExportError("Invalid suite pass rate")
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _identity(identity, count, source_checks=None):
|
|
76
|
+
if identity is None:
|
|
77
|
+
return None
|
|
78
|
+
if not isinstance(identity, dict) or set(identity) != {"projectId", "revision", "provenance", "cases"} or \
|
|
79
|
+
not isinstance(identity.get("projectId"), str) or not ENROLLED_ID.fullmatch(identity["projectId"]) or \
|
|
80
|
+
type(identity.get("revision")) is not int or identity["revision"] < 1 or \
|
|
81
|
+
not isinstance(identity.get("provenance"), str) or not DIGEST.fullmatch(identity["provenance"]) or \
|
|
82
|
+
not isinstance(identity.get("cases"), (list, tuple)) or not identity["cases"] or \
|
|
83
|
+
len(identity["cases"]) != count:
|
|
84
|
+
raise ExportError("Invalid admitted suite identity")
|
|
85
|
+
ids = set()
|
|
86
|
+
for number, case in enumerate(identity["cases"]):
|
|
87
|
+
if not isinstance(case, dict) or set(case) != {"id", "sourceFile", "checkSpecDigest", "appDigest",
|
|
88
|
+
"baselineDigest", "fixtureDigest"} or \
|
|
89
|
+
not isinstance(case["id"], str) or not ENROLLED_ID.fullmatch(case["id"]) or case["id"] in ids or \
|
|
90
|
+
not isinstance(case["sourceFile"], str) or not 1 <= len(case["sourceFile"]) <= 4096 or \
|
|
91
|
+
(source_checks is not None and (not isinstance(source_checks[number], dict) or
|
|
92
|
+
source_checks[number].get("file") != case["sourceFile"])) or \
|
|
93
|
+
not isinstance(case["checkSpecDigest"], str) or not DIGEST.fullmatch(case["checkSpecDigest"]) or \
|
|
94
|
+
not isinstance(case["appDigest"], str) or not DIGEST.fullmatch(case["appDigest"]) or \
|
|
95
|
+
(case["baselineDigest"] is not None and (not isinstance(case["baselineDigest"], str) or
|
|
96
|
+
not DIGEST.fullmatch(case["baselineDigest"]))) or \
|
|
97
|
+
(case["fixtureDigest"] is not None and (not isinstance(case["fixtureDigest"], str) or
|
|
98
|
+
not DIGEST.fullmatch(case["fixtureDigest"]))):
|
|
99
|
+
raise ExportError("Invalid admitted case identity")
|
|
100
|
+
ids.add(case["id"])
|
|
101
|
+
return identity
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _frozen(value):
|
|
105
|
+
"""A report's "none" (the check has no such input) as the identity writes it."""
|
|
106
|
+
return None if value == "none" else value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def project(doc, suite_id, *, identity=None):
|
|
110
|
+
"""A strict allowlist document suitable for both JSON and canonical JUnit generation."""
|
|
111
|
+
if not isinstance(doc, dict) or doc.get("schema") != reports.SCHEMA or doc.get("suite") != suite_id or \
|
|
112
|
+
not isinstance(suite_id, str) or not SUITE_ID.fullmatch(suite_id):
|
|
113
|
+
raise ExportError("Invalid suite result")
|
|
114
|
+
source_checks, source_devices = doc.get("checks"), doc.get("devices")
|
|
115
|
+
if not isinstance(source_checks, list) or not isinstance(source_devices, list) or \
|
|
116
|
+
len(source_checks) > MAX_CHECKS or len(source_devices) > MAX_TARGETS:
|
|
117
|
+
raise ExportError("Invalid suite result size")
|
|
118
|
+
identity = _identity(identity, len(source_checks), source_checks)
|
|
119
|
+
devices, device_keys, device_names = [], {}, {}
|
|
120
|
+
for number, source in enumerate(source_devices, 1):
|
|
121
|
+
if not isinstance(source, dict) or not isinstance(source.get("key"), str) or \
|
|
122
|
+
source["key"] in device_keys:
|
|
123
|
+
raise ExportError("Invalid suite target")
|
|
124
|
+
key = f"target-{number}"
|
|
125
|
+
device_keys[source["key"]] = key
|
|
126
|
+
device_names[key] = f"Target {number}"
|
|
127
|
+
devices.append({"key": key, "name": device_names[key]})
|
|
128
|
+
checks, cells = [], []
|
|
129
|
+
for number, source in enumerate(source_checks, 1):
|
|
130
|
+
if not isinstance(source, dict) or not isinstance(source.get("results"), list) or \
|
|
131
|
+
len(source["results"]) > len(source_devices) or \
|
|
132
|
+
type(source.get("quarantined")) is not bool:
|
|
133
|
+
raise ExportError("Invalid suite check")
|
|
134
|
+
results, seen = [], set()
|
|
135
|
+
for item in source["results"]:
|
|
136
|
+
if not isinstance(item, dict) or not isinstance(item.get("deviceKey"), str) or \
|
|
137
|
+
item["deviceKey"] not in device_keys or item["deviceKey"] in seen or \
|
|
138
|
+
not isinstance(item.get("status"), str) or \
|
|
139
|
+
item["status"] not in VERDICTS or type(item.get("flaky")) is not bool or \
|
|
140
|
+
type(item.get("quarantined")) is not bool or item["quarantined"] != source["quarantined"]:
|
|
141
|
+
raise ExportError("Invalid suite cell")
|
|
142
|
+
seen.add(item["deviceKey"])
|
|
143
|
+
status = item["status"]
|
|
144
|
+
reason = item.get("reason")
|
|
145
|
+
reason_class = reason.get("class") if isinstance(reason, dict) else None
|
|
146
|
+
safety_stop = item.get("safetyStop")
|
|
147
|
+
if "safetyStop" in item and (not isinstance(safety_stop, str) or
|
|
148
|
+
safety_stop not in SAFETY_EXIT_STOPS or
|
|
149
|
+
status != "couldnt_run"):
|
|
150
|
+
raise ExportError("Invalid suite safety stop")
|
|
151
|
+
safe_key = device_keys[item["deviceKey"]]
|
|
152
|
+
result = {"deviceKey": safe_key, "device": device_names[safe_key],
|
|
153
|
+
"status": status, "attempts": _integer(item.get("attempts"), 100_000),
|
|
154
|
+
"flaky": item["flaky"],
|
|
155
|
+
"quarantined": source["quarantined"],
|
|
156
|
+
"durationMs": _integer(item.get("durationMs"), MAX_DURATION_MS),
|
|
157
|
+
"reason": None if status == "passed" else {"class": reason_class if isinstance(reason_class,
|
|
158
|
+
str) and reason_class in REASONS
|
|
159
|
+
else "other", "message": MESSAGES[status]}}
|
|
160
|
+
environment = item.get("environment")
|
|
161
|
+
environment = environment if isinstance(environment, dict) else {}
|
|
162
|
+
kind, model, runtime = (environment.get(key) for key in ("kind", "model", "runtime"))
|
|
163
|
+
result["environment"] = {"kind": kind if kind in ("simulator", "iphone") else None,
|
|
164
|
+
"model": model if isinstance(model, str) and MODEL.fullmatch(model) else None,
|
|
165
|
+
"runtime": runtime if isinstance(runtime, str) and RUNTIME.fullmatch(runtime)
|
|
166
|
+
else None}
|
|
167
|
+
business = item.get("business")
|
|
168
|
+
assertions = business.get("assertionFields") if isinstance(business, dict) else None
|
|
169
|
+
failed = business.get("failedFields") if isinstance(business, dict) else None
|
|
170
|
+
if isinstance(business, dict) and business.get("status") in ("observed", "unavailable") and \
|
|
171
|
+
isinstance(assertions, list) and len(assertions) <= 100 and \
|
|
172
|
+
isinstance(failed, list) and len(failed) <= len(assertions) and \
|
|
173
|
+
all(isinstance(field, str) for field in assertions + failed) and \
|
|
174
|
+
len(set(assertions)) == len(assertions) and len(set(failed)) == len(failed) and \
|
|
175
|
+
set(failed).issubset(assertions):
|
|
176
|
+
result["business"] = {"status": business["status"], "assertions": len(assertions),
|
|
177
|
+
"failedAssertions": len(failed)}
|
|
178
|
+
if "passRate" in item:
|
|
179
|
+
result["passRate"] = _ratio(item["passRate"])
|
|
180
|
+
if safety_stop is not None:
|
|
181
|
+
result["safetyStop"] = safety_stop
|
|
182
|
+
results.append(result)
|
|
183
|
+
cells.append(result)
|
|
184
|
+
check = {"name": f"Check {number}", "caseId": f"check-{number}", "classname": "checks",
|
|
185
|
+
"quarantined": source["quarantined"], "results": results}
|
|
186
|
+
if identity is not None:
|
|
187
|
+
case = identity["cases"][number - 1]
|
|
188
|
+
# api.execute hashes each check again when the suite ends: a drift, or digests other than the approved
|
|
189
|
+
# ones, means this result came from inputs the identity doesn't name.
|
|
190
|
+
if source.get("provenanceDrift") or source.get("checkSpecDigest") != case["checkSpecDigest"] or \
|
|
191
|
+
any(_frozen(source.get(field)) != case[field] for field in ("baselineDigest", "fixtureDigest")) \
|
|
192
|
+
or any(item.get("appDigest") != case["appDigest"] for item in source["results"]):
|
|
193
|
+
raise InputsChanged("A check's inputs changed during the admitted suite")
|
|
194
|
+
check.update({"caseId": case["id"], "checkSpecDigest": case["checkSpecDigest"],
|
|
195
|
+
"appDigest": case["appDigest"], "baselineDigest": case["baselineDigest"],
|
|
196
|
+
"fixtureDigest": case["fixtureDigest"]})
|
|
197
|
+
checks.append(check)
|
|
198
|
+
counted = [cell for cell in cells if not cell["quarantined"]]
|
|
199
|
+
has_safety_stop = any(cell.get("safetyStop") for cell in cells)
|
|
200
|
+
summary = {"passed": sum(cell["status"] == "passed" for cell in counted),
|
|
201
|
+
"failed": sum(cell["status"] == "failed" for cell in counted),
|
|
202
|
+
"needsReview": sum(cell["status"] == "needs_review" for cell in counted),
|
|
203
|
+
"couldntRun": sum(cell["status"] == "couldnt_run" for cell in counted),
|
|
204
|
+
"flaky": sum(cell["flaky"] for cell in counted),
|
|
205
|
+
"quarantined": len(cells) - len(counted), "total": len(cells)}
|
|
206
|
+
exit_code = _integer(doc.get("exitCode"), 255)
|
|
207
|
+
if identity is not None and exit_code == 0 and (not devices or any(
|
|
208
|
+
len(check["results"]) != len(devices) for check in checks)):
|
|
209
|
+
raise ExportError("Successful admitted suite has a missing target result")
|
|
210
|
+
options = doc.get("options")
|
|
211
|
+
if not isinstance(options, dict) or type(options.get("strict")) is not bool or \
|
|
212
|
+
type(doc.get("stopped", False)) is not bool:
|
|
213
|
+
raise ExportError("Invalid suite options")
|
|
214
|
+
stop_reason = doc.get("stopReason")
|
|
215
|
+
if stop_reason is not None and (not isinstance(stop_reason, str) or stop_reason not in STOP_REASONS):
|
|
216
|
+
raise ExportError("Invalid suite stop reason")
|
|
217
|
+
if exit_code == 0 and (summary["failed"] or summary["needsReview"] or summary["couldntRun"] or
|
|
218
|
+
(options["strict"] and summary["flaky"]) or stop_reason is not None or
|
|
219
|
+
doc.get("stopped", False) or doc.get("error") or has_safety_stop):
|
|
220
|
+
raise ExportError("Inconsistent suite verdict")
|
|
221
|
+
safe = {"schema": SCHEMA, "suite": suite_id, "startedAt": _integer(doc.get("startedAt"), MAX_TIME_MS),
|
|
222
|
+
"finishedAt": _integer(doc.get("finishedAt"), MAX_TIME_MS),
|
|
223
|
+
"durationMs": _integer(doc.get("durationMs"), MAX_DURATION_MS), "exitCode": exit_code,
|
|
224
|
+
"stopped": doc.get("stopped", False), "stopReason": stop_reason,
|
|
225
|
+
"options": {"strict": options["strict"]},
|
|
226
|
+
"devices": devices, "checks": checks, "summary": summary, "mobster": {"version": "hosted/1"}}
|
|
227
|
+
if identity is not None:
|
|
228
|
+
safe["identity"] = {key: identity[key] for key in ("projectId", "revision", "provenance")}
|
|
229
|
+
return safe
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def build(doc, suite_id, *, identity=None):
|
|
233
|
+
"""Freeze safe JSON and JUnit bytes from one terminal canonical result document."""
|
|
234
|
+
safe = project(doc, suite_id, identity=identity)
|
|
235
|
+
data = {"json": (json.dumps(safe, ensure_ascii=False, allow_nan=False, sort_keys=True,
|
|
236
|
+
separators=(",", ":")) + "\n").encode("utf-8"),
|
|
237
|
+
"junit": reports.junit(safe, strict=safe["options"]["strict"])}
|
|
238
|
+
if any(len(value) > MAX_ARTIFACT for value in data.values()):
|
|
239
|
+
raise ArtifactTooLarge("Hosted test result exceeds 2 MiB; inspect the local report")
|
|
240
|
+
return data
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _checked(artifacts):
|
|
244
|
+
if not isinstance(artifacts, dict) or set(artifacts) != set(KINDS) or \
|
|
245
|
+
any(type(artifacts[key]) is not bytes or len(artifacts[key]) > MAX_ARTIFACT for key in KINDS):
|
|
246
|
+
raise ExportError("Invalid frozen test artifacts")
|
|
247
|
+
return artifacts
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _id(kind, data):
|
|
251
|
+
return hashlib.sha256(b"mobster.test.artifact/1\0" + kind.encode() + b"\0" + data).hexdigest()[:24]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def manifest(artifacts):
|
|
255
|
+
"""Integrity and download metadata for already-authorized frozen artifact bytes."""
|
|
256
|
+
artifacts = _checked(artifacts)
|
|
257
|
+
return {"schema": MANIFEST_SCHEMA, "artifacts": [
|
|
258
|
+
{"id": _id(kind, artifacts[kind]), "name": KINDS[kind][0], "mediaType": KINDS[kind][1],
|
|
259
|
+
"size": len(artifacts[kind]), "sha256": hashlib.sha256(artifacts[kind]).hexdigest(),
|
|
260
|
+
"chunkBytes": CHUNK_BYTES}
|
|
261
|
+
for kind in KINDS]}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def valid_manifest(value):
|
|
265
|
+
"""Recognize the exact metadata envelope emitted by ``manifest``."""
|
|
266
|
+
if not isinstance(value, dict) or set(value) != {"schema", "artifacts"} or \
|
|
267
|
+
value["schema"] != MANIFEST_SCHEMA or not isinstance(value["artifacts"], list) or \
|
|
268
|
+
len(value["artifacts"]) != len(KINDS):
|
|
269
|
+
return False
|
|
270
|
+
ids = set()
|
|
271
|
+
for item, (_, (name, media_type)) in zip(value["artifacts"], KINDS.items()):
|
|
272
|
+
if not isinstance(item, dict) or set(item) != {"id", "name", "mediaType", "size", "sha256", "chunkBytes"} or \
|
|
273
|
+
item["name"] != name or item["mediaType"] != media_type or \
|
|
274
|
+
not isinstance(item["id"], str) or not ARTIFACT_ID.fullmatch(item["id"]) or \
|
|
275
|
+
not isinstance(item["sha256"], str) or not re.fullmatch(r"[a-f0-9]{64}", item["sha256"]) or \
|
|
276
|
+
type(item["size"]) is not int or not 1 <= item["size"] <= MAX_ARTIFACT or \
|
|
277
|
+
type(item["chunkBytes"]) is not int or item["chunkBytes"] != CHUNK_BYTES or item["id"] in ids:
|
|
278
|
+
return False
|
|
279
|
+
ids.add(item["id"])
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def chunk(artifacts, artifact_id, offset=0, limit=CHUNK_BYTES):
|
|
284
|
+
"""One bounded base64 slice. Authorization and frozen-byte ownership belong to the caller."""
|
|
285
|
+
artifacts = _checked(artifacts)
|
|
286
|
+
if type(offset) is not int or type(limit) is not int or offset < 0 or not 1 <= limit <= CHUNK_BYTES:
|
|
287
|
+
raise ExportError("Use a nonnegative offset and a limit of 1 to 32768")
|
|
288
|
+
if not isinstance(artifact_id, str) or not ARTIFACT_ID.fullmatch(artifact_id):
|
|
289
|
+
raise ArtifactNotFound(artifact_id)
|
|
290
|
+
found = next(((kind, data) for kind, data in artifacts.items() if _id(kind, data) == artifact_id), None)
|
|
291
|
+
if found is None:
|
|
292
|
+
raise ArtifactNotFound(artifact_id)
|
|
293
|
+
_, data = found
|
|
294
|
+
if offset >= len(data):
|
|
295
|
+
raise ExportError("Artifact offset is out of range")
|
|
296
|
+
part = data[offset:offset + limit]
|
|
297
|
+
return {"id": artifact_id, "offset": offset, "size": len(data), "length": len(part),
|
|
298
|
+
"sha256": hashlib.sha256(data).hexdigest(), "encoding": "base64",
|
|
299
|
+
"data": base64.b64encode(part).decode("ascii"), "eof": offset + len(part) == len(data)}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def valid_chunk(value, *, artifact_id, offset, limit):
|
|
303
|
+
"""Recognize a bounded chunk of the requested frozen artifact, including canonical base64."""
|
|
304
|
+
if not isinstance(value, dict) or set(value) != {
|
|
305
|
+
"id", "offset", "size", "length", "sha256", "encoding", "data", "eof"} or \
|
|
306
|
+
type(offset) is not int or type(limit) is not int or offset < 0 or not 1 <= limit <= CHUNK_BYTES or \
|
|
307
|
+
value["id"] != artifact_id or type(value["offset"]) is not int or value["offset"] != offset or \
|
|
308
|
+
type(value["size"]) is not int or not 1 <= value["size"] <= MAX_ARTIFACT or \
|
|
309
|
+
type(value["length"]) is not int or not 1 <= value["length"] <= limit or \
|
|
310
|
+
value["offset"] + value["length"] > value["size"] or type(value["eof"]) is not bool or \
|
|
311
|
+
value["eof"] != (value["offset"] + value["length"] == value["size"]) or \
|
|
312
|
+
not isinstance(value["id"], str) or not ARTIFACT_ID.fullmatch(value["id"]) or \
|
|
313
|
+
not isinstance(value["sha256"], str) or not re.fullmatch(r"[a-f0-9]{64}", value["sha256"]) or \
|
|
314
|
+
value["encoding"] != "base64" or not isinstance(value["data"], str) or \
|
|
315
|
+
len(value["data"]) > ((limit + 2) // 3) * 4:
|
|
316
|
+
return False
|
|
317
|
+
try:
|
|
318
|
+
decoded = base64.b64decode(value["data"], validate=True)
|
|
319
|
+
except (ValueError, binascii.Error):
|
|
320
|
+
return False
|
|
321
|
+
return len(decoded) == value["length"] and base64.b64encode(decoded).decode("ascii") == value["data"]
|