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,570 @@
|
|
|
1
|
+
"""The assertion language (docs/checks.md), decided on one read of the accessibility tree: no model, OCR or pixels."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
import unicodedata
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from .tree import STRUCTURAL
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CheckError(ValueError):
|
|
14
|
+
"""The check or one of its assertions is invalid: the run can't start (couldnt_run, class "usage").
|
|
15
|
+
``checks.CheckError`` is this class."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_QUOTES = str.maketrans({
|
|
19
|
+
"‘": "'", "’": "'", "‚": "'", "‛": "'", "′": "'", "´": "'",
|
|
20
|
+
"“": '"', "”": '"', "„": '"', "‟": '"', "″": '"',
|
|
21
|
+
"‐": "-", "‑": "-", "‒": "-", "–": "-", "—": "-", "―": "-", "−": "-"})
|
|
22
|
+
_INVISIBLE = re.compile("[\u200b-\u200f\u202a-\u202e\u2060\ufeff\u00ad]")
|
|
23
|
+
_SPACE = re.compile(r"\s+")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def normalize(text):
|
|
27
|
+
"""NFKC; curly quotes and dashes as ASCII; invisible marks dropped; each run of whitespace (NBSP too) one space."""
|
|
28
|
+
if text is None:
|
|
29
|
+
return ""
|
|
30
|
+
text = unicodedata.normalize("NFKC", str(text)).translate(_QUOTES)
|
|
31
|
+
return _SPACE.sub(" ", _INVISIBLE.sub("", text)).strip()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def folded(text):
|
|
35
|
+
return normalize(text).casefold()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
REGEX_MAX = 500
|
|
39
|
+
_REGEX = re.compile(r"^/(.*)/(i?)$", re.S)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Matcher:
|
|
44
|
+
"""One string field: an exact (normalized) string, or a /regex/ (``pattern`` set)."""
|
|
45
|
+
raw: str
|
|
46
|
+
text: str = ""
|
|
47
|
+
pattern: Optional[re.Pattern] = None
|
|
48
|
+
|
|
49
|
+
def exact(self, value):
|
|
50
|
+
"""Selector rule: exact and case-sensitive after normalization, or the regex."""
|
|
51
|
+
value = normalize(value)
|
|
52
|
+
return bool(self.pattern.search(value)) if self.pattern is not None else value == self.text
|
|
53
|
+
|
|
54
|
+
def contains(self, value):
|
|
55
|
+
"""``text``/``no_text`` rule: a case-insensitive substring, or the regex."""
|
|
56
|
+
if self.pattern is not None:
|
|
57
|
+
return bool(self.pattern.search(normalize(value)))
|
|
58
|
+
return bool(self.text) and self.text.casefold() in folded(value)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_regex(self):
|
|
62
|
+
return self.pattern is not None
|
|
63
|
+
|
|
64
|
+
def render(self):
|
|
65
|
+
return self.raw if self.is_regex else self.text
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_matcher(value, where):
|
|
69
|
+
"""A string field of a selector or a text assertion. ``where`` names it in errors."""
|
|
70
|
+
if isinstance(value, bool) or not isinstance(value, str):
|
|
71
|
+
hint = "; quote it" if isinstance(value, (bool, int, float)) else ""
|
|
72
|
+
raise CheckError(f"{where} must be text, not {_type_name(value)}{hint}")
|
|
73
|
+
found = _REGEX.match(value) if len(value) >= 2 else None
|
|
74
|
+
if found is None:
|
|
75
|
+
text = normalize(value)
|
|
76
|
+
if not text:
|
|
77
|
+
raise CheckError(f"{where} is empty")
|
|
78
|
+
if len(value) > 2000:
|
|
79
|
+
raise CheckError(f"{where} is longer than 2,000 characters")
|
|
80
|
+
return Matcher(value, text)
|
|
81
|
+
pattern, flags = found.group(1), found.group(2)
|
|
82
|
+
if not pattern:
|
|
83
|
+
raise CheckError(f"{where} is an empty regex (//), which matches everything")
|
|
84
|
+
if len(pattern) > REGEX_MAX:
|
|
85
|
+
raise CheckError(f"{where}: the regex is longer than {REGEX_MAX} characters")
|
|
86
|
+
try:
|
|
87
|
+
compiled = re.compile(pattern, re.IGNORECASE if flags == "i" else 0)
|
|
88
|
+
except re.error as error:
|
|
89
|
+
raise CheckError(f"{where}: {value} is not a valid regex ({error})") from None
|
|
90
|
+
return Matcher(value, "", compiled)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
FRIENDLY_ROLES = {
|
|
94
|
+
"button": frozenset({"Button"}),
|
|
95
|
+
"text": frozenset({"StaticText"}),
|
|
96
|
+
"field": frozenset({"TextField", "SecureTextField", "SearchField", "TextView"}),
|
|
97
|
+
"switch": frozenset({"Switch", "Toggle"}),
|
|
98
|
+
"toggle": frozenset({"Switch", "Toggle"}),
|
|
99
|
+
"cell": frozenset({"Cell"}),
|
|
100
|
+
"image": frozenset({"Image", "Icon"}),
|
|
101
|
+
"link": frozenset({"Link"}),
|
|
102
|
+
"tab": frozenset({"Tab"}), # plus a Button inside a TabBar (see Selector.role_matches)
|
|
103
|
+
"slider": frozenset({"Slider"}),
|
|
104
|
+
"stepper": frozenset({"Stepper"}),
|
|
105
|
+
"picker": frozenset({"Picker", "PickerWheel", "DatePicker"}),
|
|
106
|
+
"segment": frozenset({"SegmentedControl"}),
|
|
107
|
+
"navbar": frozenset({"NavigationBar"}),
|
|
108
|
+
"alert": frozenset({"Alert"}),
|
|
109
|
+
}
|
|
110
|
+
ELEMENT_TYPES = frozenset("""
|
|
111
|
+
Any Other Application Group Window Sheet Drawer Alert Dialog Button RadioButton RadioGroup CheckBox DisclosureTriangle
|
|
112
|
+
PopUpButton ComboBox MenuButton ToolbarButton Popover Keyboard Key NavigationBar TabBar TabGroup Toolbar StatusBar
|
|
113
|
+
Table TableRow TableColumn Outline OutlineRow Browser CollectionView Slider PageIndicator ProgressIndicator
|
|
114
|
+
ActivityIndicator SegmentedControl Picker PickerWheel Switch Toggle Link Image Icon SearchField ScrollView ScrollBar
|
|
115
|
+
StaticText TextField SecureTextField DatePicker TextView Menu MenuItem MenuBar MenuBarItem Map WebView IncrementArrow
|
|
116
|
+
DecrementArrow Timeline RatingIndicator ValueIndicator SplitGroup Splitter RelevanceIndicator ColorWell HelpTag Matte
|
|
117
|
+
DockItem Ruler RulerMarker Grid LevelIndicator Cell LayoutArea LayoutItem Handle Stepper Tab TouchBar StatusItem
|
|
118
|
+
""".split())
|
|
119
|
+
_TYPES_FOLDED = {name.casefold(): name for name in ELEMENT_TYPES}
|
|
120
|
+
ROLE_NAMES = {"StaticText": "text", "Button": "button", "TextField": "field", "SecureTextField": "field",
|
|
121
|
+
"SearchField": "field", "TextView": "field", "Switch": "switch", "Toggle": "switch", "Cell": "cell",
|
|
122
|
+
"Image": "image", "Icon": "image", "Link": "link", "Tab": "tab", "Slider": "slider",
|
|
123
|
+
"Stepper": "stepper", "Picker": "picker", "PickerWheel": "picker", "DatePicker": "picker",
|
|
124
|
+
"SegmentedControl": "segment", "NavigationBar": "navbar", "Alert": "alert"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def friendly_role(role):
|
|
128
|
+
return ROLE_NAMES.get(role, role)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_role(value, where):
|
|
132
|
+
"""(the role as written, the XCUIElementType names it matches, whether it is the friendly ``tab``)."""
|
|
133
|
+
if isinstance(value, bool) or not isinstance(value, str) or not value.strip():
|
|
134
|
+
raise CheckError(f"{where} must be a role name such as button, text or field")
|
|
135
|
+
name = value.strip()
|
|
136
|
+
if name in FRIENDLY_ROLES:
|
|
137
|
+
return name, FRIENDLY_ROLES[name], name == "tab"
|
|
138
|
+
bare = name.removeprefix("XCUIElementType")
|
|
139
|
+
known = _TYPES_FOLDED.get(bare.casefold())
|
|
140
|
+
if known is None:
|
|
141
|
+
close = difflib.get_close_matches(name.casefold(), list(FRIENDLY_ROLES) + sorted(_TYPES_FOLDED), n=1)
|
|
142
|
+
hint = f"; did you mean {close[0]}?" if close else ""
|
|
143
|
+
raise CheckError(f"{where}: {name!r} is not a role{hint} Use one of {', '.join(FRIENDLY_ROLES)}, "
|
|
144
|
+
"or an XCUIElementType name")
|
|
145
|
+
return name, frozenset({known}), False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
SELECTOR_KEYS = ("id", "label", "value", "role", "enabled", "selected")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True)
|
|
152
|
+
class Selector:
|
|
153
|
+
id: Optional[Matcher] = None
|
|
154
|
+
label: Optional[Matcher] = None
|
|
155
|
+
value: Optional[Matcher] = None
|
|
156
|
+
role: Optional[str] = None
|
|
157
|
+
roles: frozenset = frozenset()
|
|
158
|
+
tab: bool = False
|
|
159
|
+
enabled: Optional[bool] = None
|
|
160
|
+
selected: Optional[bool] = None
|
|
161
|
+
|
|
162
|
+
def role_matches(self, node):
|
|
163
|
+
if self.role is None:
|
|
164
|
+
return True
|
|
165
|
+
if node.role in self.roles:
|
|
166
|
+
return True
|
|
167
|
+
# The friendly `tab`: a tab bar's items are Buttons inside the TabBar.
|
|
168
|
+
return self.tab and node.role == "Button" and "/XCUIElementTypeTabBar[" in node.path
|
|
169
|
+
|
|
170
|
+
def matches(self, node):
|
|
171
|
+
return ((self.id is None or (bool(node.identifier) and self.id.exact(node.identifier)))
|
|
172
|
+
and (self.label is None or self.label.exact(node.label))
|
|
173
|
+
and (self.value is None or self.value.exact(node.value))
|
|
174
|
+
and self.role_matches(node)
|
|
175
|
+
and (self.enabled is None or node.enabled == self.enabled)
|
|
176
|
+
and (self.selected is None or node.selected == self.selected))
|
|
177
|
+
|
|
178
|
+
def render(self):
|
|
179
|
+
parts = []
|
|
180
|
+
for key in ("id", "label", "value"):
|
|
181
|
+
matcher = getattr(self, key)
|
|
182
|
+
if matcher is not None:
|
|
183
|
+
parts.append(f"{key}={matcher.render()}")
|
|
184
|
+
if self.role is not None:
|
|
185
|
+
parts.append(f"role={self.role}")
|
|
186
|
+
for key in ("enabled", "selected"):
|
|
187
|
+
flag = getattr(self, key)
|
|
188
|
+
if flag is not None:
|
|
189
|
+
parts.append(f"{key}={'true' if flag else 'false'}")
|
|
190
|
+
return " ".join(parts)
|
|
191
|
+
|
|
192
|
+
def to_dict(self):
|
|
193
|
+
out = {}
|
|
194
|
+
for key in ("id", "label", "value"):
|
|
195
|
+
matcher = getattr(self, key)
|
|
196
|
+
if matcher is not None:
|
|
197
|
+
out[key] = matcher.raw
|
|
198
|
+
if self.role is not None:
|
|
199
|
+
out["role"] = self.role
|
|
200
|
+
for key in ("enabled", "selected"):
|
|
201
|
+
if getattr(self, key) is not None:
|
|
202
|
+
out[key] = getattr(self, key)
|
|
203
|
+
return out
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def parse_selector(obj, where="selector"):
|
|
207
|
+
"""A selector object: at least one of id, label, value, role, enabled and selected. Raises CheckError."""
|
|
208
|
+
if not isinstance(obj, dict):
|
|
209
|
+
raise CheckError(f"{where} must be an object such as {{label: Continue}}, not {_type_name(obj)}")
|
|
210
|
+
reject_unknown_keys(obj, SELECTOR_KEYS, where)
|
|
211
|
+
if not obj:
|
|
212
|
+
raise CheckError(f"{where} needs at least one of {', '.join(SELECTOR_KEYS)}")
|
|
213
|
+
fields = {}
|
|
214
|
+
for key in ("id", "label", "value"):
|
|
215
|
+
if key in obj:
|
|
216
|
+
fields[key] = parse_matcher(obj[key], f"{where}.{key}")
|
|
217
|
+
if "role" in obj:
|
|
218
|
+
fields["role"], fields["roles"], fields["tab"] = parse_role(obj["role"], f"{where}.role")
|
|
219
|
+
for key in ("enabled", "selected"):
|
|
220
|
+
if key in obj:
|
|
221
|
+
if not isinstance(obj[key], bool):
|
|
222
|
+
raise CheckError(f"{where}.{key} must be true or false, not {obj[key]!r}")
|
|
223
|
+
fields[key] = obj[key]
|
|
224
|
+
return Selector(**fields)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
KINDS = ("text", "no_text", "visible", "absent", "value", "count")
|
|
228
|
+
BOUNDS = ("equals", "at_least", "at_most")
|
|
229
|
+
NAME_MAX = 120
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass(frozen=True)
|
|
233
|
+
class Assertion:
|
|
234
|
+
kind: str
|
|
235
|
+
needle: Optional[Matcher] = None # text, no_text
|
|
236
|
+
selector: Optional[Selector] = None # visible, absent, value, count
|
|
237
|
+
equals: Any = None # value: text, a number or true/false; count: a whole number
|
|
238
|
+
at_least: Optional[int] = None
|
|
239
|
+
at_most: Optional[int] = None
|
|
240
|
+
name: Optional[str] = None
|
|
241
|
+
|
|
242
|
+
def render(self):
|
|
243
|
+
"""One line, such as ``count id=/^plan_/ == 3`` or ``text "Choose your plan"``."""
|
|
244
|
+
if self.kind in ("text", "no_text"):
|
|
245
|
+
shown = self.needle.raw if self.needle.is_regex else f'"{self.needle.text}"'
|
|
246
|
+
return f"{self.kind} {shown}"
|
|
247
|
+
selector = self.selector.render()
|
|
248
|
+
if self.kind == "value":
|
|
249
|
+
return f"value {selector} == {_render_expected(self.equals)}"
|
|
250
|
+
if self.kind == "count":
|
|
251
|
+
if self.equals is not None:
|
|
252
|
+
return f"count {selector} == {self.equals}"
|
|
253
|
+
if self.at_least is not None:
|
|
254
|
+
return f"count {selector} >= {self.at_least}"
|
|
255
|
+
return f"count {selector} <= {self.at_most}"
|
|
256
|
+
return f"{self.kind} {selector}"
|
|
257
|
+
|
|
258
|
+
def to_dict(self):
|
|
259
|
+
"""The assertion as a check file writes it (the canonical form)."""
|
|
260
|
+
out = {}
|
|
261
|
+
if self.kind in ("text", "no_text"):
|
|
262
|
+
out[self.kind] = self.needle.raw
|
|
263
|
+
else:
|
|
264
|
+
out[self.kind] = self.selector.to_dict()
|
|
265
|
+
for key in BOUNDS:
|
|
266
|
+
if getattr(self, key) is not None:
|
|
267
|
+
out[key] = getattr(self, key)
|
|
268
|
+
if self.name:
|
|
269
|
+
out["name"] = self.name
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _render_expected(value):
|
|
274
|
+
if isinstance(value, bool):
|
|
275
|
+
return "true" if value else "false"
|
|
276
|
+
if isinstance(value, (int, float)):
|
|
277
|
+
return repr(value) if isinstance(value, float) else str(value)
|
|
278
|
+
return f'"{value}"'
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def parse_assertion(obj, where="assertion"):
|
|
282
|
+
"""One assertion object (§3.1)."""
|
|
283
|
+
if not isinstance(obj, dict):
|
|
284
|
+
raise CheckError(f"{where} must be an object such as {{text: Continue}}, not {_type_name(obj)}")
|
|
285
|
+
kinds = [key for key in KINDS if key in obj]
|
|
286
|
+
reject_unknown_keys(obj, KINDS + BOUNDS + ("name",), where)
|
|
287
|
+
if len(kinds) != 1:
|
|
288
|
+
raise CheckError(f"{where} needs exactly one of {', '.join(KINDS)}"
|
|
289
|
+
+ (f"; it has {', '.join(kinds)}" if kinds else ""))
|
|
290
|
+
kind = kinds[0]
|
|
291
|
+
name = obj.get("name")
|
|
292
|
+
if name is not None and (not isinstance(name, str) or not name.strip() or len(name) > NAME_MAX):
|
|
293
|
+
raise CheckError(f"{where}.name must be text of 1 to {NAME_MAX} characters")
|
|
294
|
+
bounds = {key: obj[key] for key in BOUNDS if key in obj}
|
|
295
|
+
fields = {"kind": kind, "name": name.strip() if name else None}
|
|
296
|
+
if kind in ("text", "no_text"):
|
|
297
|
+
if isinstance(obj[kind], list):
|
|
298
|
+
raise CheckError(f"{where}.{kind} takes one string here; a check file may list several")
|
|
299
|
+
if bounds:
|
|
300
|
+
raise CheckError(f"{where}: {', '.join(bounds)} goes with value or count, not {kind}")
|
|
301
|
+
fields["needle"] = parse_matcher(obj[kind], f"{where}.{kind}")
|
|
302
|
+
else:
|
|
303
|
+
fields["selector"] = parse_selector(obj[kind], f"{where}.{kind}")
|
|
304
|
+
if kind in ("visible", "absent") and bounds:
|
|
305
|
+
raise CheckError(f"{where}: {', '.join(bounds)} goes with value or count, not {kind}")
|
|
306
|
+
if kind == "value":
|
|
307
|
+
if set(bounds) != {"equals"}:
|
|
308
|
+
raise CheckError(f"{where}: value needs equals (the text, number or true/false it must show)"
|
|
309
|
+
+ (f", and takes no {', '.join(k for k in bounds if k != 'equals')}"
|
|
310
|
+
if set(bounds) - {"equals"} else ""))
|
|
311
|
+
fields["equals"] = _parse_expected(bounds["equals"], f"{where}.equals")
|
|
312
|
+
if kind == "count":
|
|
313
|
+
if len(bounds) != 1:
|
|
314
|
+
raise CheckError(f"{where}: count needs exactly one of equals, at_least and at_most")
|
|
315
|
+
(key, number), = bounds.items()
|
|
316
|
+
if isinstance(number, bool) or not isinstance(number, int) or number < 0:
|
|
317
|
+
raise CheckError(f"{where}.{key} must be a whole number of at least 0, not {number!r}")
|
|
318
|
+
fields[key] = number
|
|
319
|
+
return Assertion(**fields)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def expand_assertion(obj, where="assertion"):
|
|
323
|
+
"""A check file's assertion: ``text`` and ``no_text`` may list several strings, one assertion each."""
|
|
324
|
+
if isinstance(obj, dict):
|
|
325
|
+
for kind in ("text", "no_text"):
|
|
326
|
+
if isinstance(obj.get(kind), list):
|
|
327
|
+
items = obj[kind]
|
|
328
|
+
if not items:
|
|
329
|
+
raise CheckError(f"{where}.{kind} is an empty list")
|
|
330
|
+
rest = {key: value for key, value in obj.items() if key != kind}
|
|
331
|
+
return [parse_assertion({**rest, kind: item}, f"{where}.{kind}[{index}]")
|
|
332
|
+
for index, item in enumerate(items)]
|
|
333
|
+
return [parse_assertion(obj, where)]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _parse_expected(value, where):
|
|
337
|
+
if isinstance(value, bool):
|
|
338
|
+
return value
|
|
339
|
+
if isinstance(value, (int, float)):
|
|
340
|
+
if not math.isfinite(value):
|
|
341
|
+
raise CheckError(f"{where} must be a finite number")
|
|
342
|
+
return value
|
|
343
|
+
if isinstance(value, str):
|
|
344
|
+
if len(value) > 2000:
|
|
345
|
+
raise CheckError(f"{where} is longer than 2,000 characters")
|
|
346
|
+
return value
|
|
347
|
+
raise CheckError(f"{where} must be text, a number or true/false, not {_type_name(value)}")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def reject_unknown_keys(obj, allowed, where):
|
|
351
|
+
for key in obj:
|
|
352
|
+
if key not in allowed:
|
|
353
|
+
close = difflib.get_close_matches(str(key), allowed, n=1)
|
|
354
|
+
hint = f"; did you mean {close[0]}?" if close else f"; use {', '.join(allowed)}"
|
|
355
|
+
raise CheckError(f"{where} has an unknown key {key!r}{hint}")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _type_name(value):
|
|
359
|
+
return {dict: "an object", list: "a list", bool: "true/false", type(None): "nothing",
|
|
360
|
+
int: "a number", float: "a number", str: "text"}.get(type(value), type(value).__name__)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
ON = frozenset({"1", "true", "on", "yes"})
|
|
364
|
+
OFF = frozenset({"0", "false", "off", "no"})
|
|
365
|
+
# The first number in a value: thousands groups of exactly three digits (1,234.5 or 1 234), else plain digits.
|
|
366
|
+
_NUMBER = re.compile(r"-?\d{1,3}(?:[, ]\d{3})+(?:\.\d+)?|-?\d+(?:\.\d+)?")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def first_number(text):
|
|
370
|
+
found = _NUMBER.search(normalize(text))
|
|
371
|
+
if found is None:
|
|
372
|
+
return None
|
|
373
|
+
try:
|
|
374
|
+
return float(re.sub(r"[, ]", "", found.group(0)))
|
|
375
|
+
except ValueError:
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def same_number(got, expected):
|
|
380
|
+
# At the expected number's own precision (bench/checks.py:_same_number): 3.4 reads as 3, 39.99 never as 39.
|
|
381
|
+
want = float(expected)
|
|
382
|
+
places = 0
|
|
383
|
+
if isinstance(expected, float):
|
|
384
|
+
text = repr(expected)
|
|
385
|
+
places = len(text.split(".")[1].rstrip("0")) if "." in text and "e" not in text else 0
|
|
386
|
+
return got == want or round(got, places) == want
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def value_equals(node_value, expected):
|
|
390
|
+
"""(ok, how the value reads) for ``value … equals: expected``."""
|
|
391
|
+
if isinstance(expected, bool):
|
|
392
|
+
word = folded(node_value)
|
|
393
|
+
if word in ON:
|
|
394
|
+
return expected is True, "on"
|
|
395
|
+
if word in OFF:
|
|
396
|
+
return expected is False, "off"
|
|
397
|
+
return False, f'"{normalize(node_value)}" (neither on nor off)'
|
|
398
|
+
if isinstance(expected, (int, float)):
|
|
399
|
+
number = first_number(node_value)
|
|
400
|
+
if number is None:
|
|
401
|
+
return False, f'"{normalize(node_value)}" (no number)'
|
|
402
|
+
return same_number(number, expected), f'"{normalize(node_value)}"'
|
|
403
|
+
return normalize(node_value) == normalize(expected), f'"{normalize(node_value)}"'
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
MATCHES_KEPT = 10
|
|
407
|
+
NEAR_MISSES = 5
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@dataclass(frozen=True)
|
|
411
|
+
class AssertionResult:
|
|
412
|
+
ok: bool
|
|
413
|
+
text: str
|
|
414
|
+
observed: str
|
|
415
|
+
matches: tuple = () # of dicts {id, label, value, role, rect}
|
|
416
|
+
paths: tuple = field(default=(), compare=False) # the matched nodes' paths (the overlay outlines them)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def node_dict(node):
|
|
420
|
+
return {"id": node.identifier, "label": node.label, "value": node.value, "role": node.role,
|
|
421
|
+
"rect": [round(n, 1) for n in node.rect]}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def node_name(node, tree=None):
|
|
425
|
+
"""How a node is named in ``observed``: its identifier, else its quoted label, else its role."""
|
|
426
|
+
if node.identifier:
|
|
427
|
+
return node.identifier
|
|
428
|
+
if node.label:
|
|
429
|
+
return f'"{_short(node.label, 60)}"'
|
|
430
|
+
role = friendly_role(node.role)
|
|
431
|
+
# A Settings row is a Cell whose text sits on a child: it is named by its first labelled node.
|
|
432
|
+
inner = _labelled_descendant(node, tree) if tree is not None else None
|
|
433
|
+
if inner is None:
|
|
434
|
+
return role
|
|
435
|
+
return f'{role} "{_short(inner.label, 60)}"' if inner.label else f"{role} {inner.identifier}"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _labelled_descendant(node, tree):
|
|
439
|
+
"""The first node inside ``node`` (in document order) with a label or an identifier, else None."""
|
|
440
|
+
prefix = node.path + "/"
|
|
441
|
+
return next((inner for inner in tree.nodes if inner.path.startswith(prefix) and (inner.label or inner.identifier)),
|
|
442
|
+
None)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def field_value(node):
|
|
446
|
+
"""A node's value as ``value`` compares it: a field showing its placeholder is empty."""
|
|
447
|
+
if node.placeholder and node.value == node.placeholder and friendly_role(node.role) == "field":
|
|
448
|
+
return ""
|
|
449
|
+
return node.value
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _short(text, limit):
|
|
453
|
+
"""The app's text as shown (whitespace collapsed, invisible marks removed), cut at ``limit``."""
|
|
454
|
+
text = _SPACE.sub(" ", _INVISIBLE.sub("", str(text or ""))).strip()
|
|
455
|
+
return text if len(text) <= limit else text[:limit - 1].rstrip() + "…"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _names(nodes, limit=5, tree=None):
|
|
459
|
+
names = [node_name(node, tree) for node in nodes[:limit]]
|
|
460
|
+
more = len(nodes) - limit
|
|
461
|
+
return ", ".join(names) + (f" and {more} more" if more > 0 else "")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _close(needle, candidates):
|
|
465
|
+
"""Up to NEAR_MISSES of ``candidates`` closest to ``needle`` (difflib)."""
|
|
466
|
+
folded_map = {}
|
|
467
|
+
for candidate in candidates:
|
|
468
|
+
text = normalize(candidate)
|
|
469
|
+
if text and len(text) <= 300:
|
|
470
|
+
folded_map.setdefault(text.casefold(), candidate)
|
|
471
|
+
target = folded(needle)
|
|
472
|
+
close = difflib.get_close_matches(target, list(folded_map), n=NEAR_MISSES, cutoff=.6)
|
|
473
|
+
if not close:
|
|
474
|
+
ranked = sorted(folded_map, key=lambda key: -difflib.SequenceMatcher(None, target, key).ratio())
|
|
475
|
+
close = [key for key in ranked[:3] if difflib.SequenceMatcher(None, target, key).ratio() >= .3]
|
|
476
|
+
return [folded_map[key] for key in close]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _closest_text(needle, candidates):
|
|
480
|
+
close = _close(needle, candidates)
|
|
481
|
+
return "closest: " + ", ".join(f'"{_short(text, 60)}"' for text in close) if close else "nothing like it"
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _near_selector(selector, shown):
|
|
485
|
+
"""Near misses for a selector that matched nothing: by its id, else label, else value."""
|
|
486
|
+
for key, attribute in (("id", "identifier"), ("label", "label"), ("value", "value")):
|
|
487
|
+
matcher = getattr(selector, key)
|
|
488
|
+
if matcher is not None and not matcher.is_regex:
|
|
489
|
+
return _closest_text(matcher.text, [getattr(node, attribute) for node in shown])
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _hidden_matches(selector, tree, shown_paths):
|
|
494
|
+
"""A note on nodes that match but aren't shown (hidden, or off screen)."""
|
|
495
|
+
hidden = [node for node in tree.nodes if node.path not in shown_paths and selector.matches(node)
|
|
496
|
+
and node.role not in STRUCTURAL]
|
|
497
|
+
if not hidden:
|
|
498
|
+
return None
|
|
499
|
+
off = [node for node in hidden if node.visible and not tree.on_screen(node)]
|
|
500
|
+
if off:
|
|
501
|
+
return f"{len(off)} more off screen: swipe to {'it' if len(off) == 1 else 'them'}"
|
|
502
|
+
return f"{len(hidden)} more not visible"
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _result(assertion, ok, observed, nodes=()):
|
|
506
|
+
return AssertionResult(ok, assertion.render(), observed, tuple(node_dict(n) for n in nodes[:MATCHES_KEPT]),
|
|
507
|
+
tuple(n.path for n in nodes[:MATCHES_KEPT]))
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def evaluate_one(assertion, tree, shown=None):
|
|
511
|
+
shown = tree.shown() if shown is None else shown
|
|
512
|
+
if assertion.kind in ("text", "no_text"):
|
|
513
|
+
needle = assertion.needle
|
|
514
|
+
hits = [node for node in shown if needle.contains(node.label) or needle.contains(node.value)]
|
|
515
|
+
if assertion.kind == "text":
|
|
516
|
+
if hits:
|
|
517
|
+
first = hits[0]
|
|
518
|
+
where = first.label if needle.contains(first.label) else first.value
|
|
519
|
+
more = f" and {len(hits) - 1} more" if len(hits) > 1 else ""
|
|
520
|
+
return _result(assertion, True, f'found in {friendly_role(first.role)} "{_short(where, 80)}"{more}',
|
|
521
|
+
hits)
|
|
522
|
+
candidates = [node.label for node in shown] + [node.value for node in shown]
|
|
523
|
+
return _result(assertion, False, "not on screen; " + (
|
|
524
|
+
_closest_text(needle.text, candidates) if not needle.is_regex else "no label or value matches"))
|
|
525
|
+
if hits:
|
|
526
|
+
return _result(assertion, False, f"on screen in {_names(hits, tree=tree)}", hits)
|
|
527
|
+
return _result(assertion, True, "not on screen")
|
|
528
|
+
|
|
529
|
+
selector = assertion.selector
|
|
530
|
+
matched = [node for node in shown if selector.matches(node)]
|
|
531
|
+
shown_paths = {node.path for node in shown}
|
|
532
|
+
if assertion.kind == "visible":
|
|
533
|
+
if matched:
|
|
534
|
+
return _result(assertion, True, f"found {len(matched)}: {_names(matched, tree=tree)}", matched)
|
|
535
|
+
notes = [note for note in (_hidden_matches(selector, tree, shown_paths),
|
|
536
|
+
_near_selector(selector, shown)) if note]
|
|
537
|
+
return _result(assertion, False, "no shown element matches" + ("; " + "; ".join(notes) if notes else ""))
|
|
538
|
+
if assertion.kind == "absent":
|
|
539
|
+
if matched:
|
|
540
|
+
return _result(assertion, False, f"found {len(matched)}: {_names(matched, tree=tree)}", matched)
|
|
541
|
+
return _result(assertion, True, "none shown")
|
|
542
|
+
if assertion.kind == "value":
|
|
543
|
+
if not matched:
|
|
544
|
+
notes = [note for note in (_hidden_matches(selector, tree, shown_paths),
|
|
545
|
+
_near_selector(selector, shown)) if note]
|
|
546
|
+
return _result(assertion, False, "not found" + ("; " + "; ".join(notes) if notes else ""))
|
|
547
|
+
if len(matched) > 1:
|
|
548
|
+
return _result(assertion, False, f"ambiguous: {len(matched)} matches ({_names(matched, tree=tree)})",
|
|
549
|
+
matched)
|
|
550
|
+
node = matched[0]
|
|
551
|
+
ok, reads = value_equals(field_value(node), assertion.equals)
|
|
552
|
+
return _result(assertion, ok, reads if ok else f"was {reads}", matched)
|
|
553
|
+
count = len(matched)
|
|
554
|
+
if assertion.equals is not None:
|
|
555
|
+
ok = count == assertion.equals
|
|
556
|
+
elif assertion.at_least is not None:
|
|
557
|
+
ok = count >= assertion.at_least
|
|
558
|
+
else:
|
|
559
|
+
ok = count <= assertion.at_most
|
|
560
|
+
observed = f"found {count}" + (f": {_names(matched, tree=tree)}" if matched else "")
|
|
561
|
+
if not ok and not matched:
|
|
562
|
+
hidden = _hidden_matches(selector, tree, shown_paths)
|
|
563
|
+
observed += f"; {hidden}" if hidden else ""
|
|
564
|
+
return _result(assertion, ok, observed, matched)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def evaluate_on(assertions, tree):
|
|
568
|
+
"""Every assertion on one read of the tree, in order: a list of AssertionResult. Pure."""
|
|
569
|
+
shown = tree.shown()
|
|
570
|
+
return [evaluate_one(assertion, tree, shown) for assertion in assertions]
|