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,305 @@
|
|
|
1
|
+
"""Fixtures (.mobster/fixtures/<name>.yaml): what changes a device's state is for simulators, refused on a phone."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from contextlib import nullcontext
|
|
8
|
+
from dataclasses import dataclass, field, replace
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import dialogs as dialog_policy
|
|
12
|
+
from .assertions import CheckError
|
|
13
|
+
|
|
14
|
+
FOLDER = "fixtures"
|
|
15
|
+
SUFFIXES = (".yaml", ".yml")
|
|
16
|
+
NAME = re.compile(r"[a-z0-9][a-z0-9-]{0,59}")
|
|
17
|
+
KEYS = ("extends", "reset", "launch", "files", "keychain", "pasteboard", "steps", "dialogs")
|
|
18
|
+
DEPTH = 6
|
|
19
|
+
FILES_LIMIT = 50
|
|
20
|
+
BYTES_LIMIT = 20 * 1024 * 1024
|
|
21
|
+
PASTEBOARD_LIMIT = 10_000
|
|
22
|
+
PHONE_REFUSAL = "{what} changes the state of a phone, and fixtures do that on simulators only (from {name})."
|
|
23
|
+
PHONE_FIX = "Run the check on a simulator, or use a fixture with only launch args, env and steps on the phone."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FixtureError(Exception):
|
|
27
|
+
"""A fixture can't be read, found or applied: one sentence that says what to do."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Effective:
|
|
32
|
+
"""A fixture and what it extends, flattened."""
|
|
33
|
+
names: tuple = ()
|
|
34
|
+
reset: str = "none"
|
|
35
|
+
args: tuple = ()
|
|
36
|
+
env: dict = field(default_factory=dict)
|
|
37
|
+
steps: tuple = ()
|
|
38
|
+
files: tuple = () # of (absolute source, destination relative to the container)
|
|
39
|
+
keychain: bool = False
|
|
40
|
+
pasteboard: str = ""
|
|
41
|
+
dialogs: dict = field(default_factory=dict) # how a run answers iOS's own dialogs (dialogs.py)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def changes_state(self):
|
|
45
|
+
"""What a real iPhone must not get: a reset, files, a keychain reset or a clipboard."""
|
|
46
|
+
return [word for word, held in (("a reset", self.reset != "none"), ("seed files", bool(self.files)),
|
|
47
|
+
("a keychain reset", self.keychain), ("a pasteboard", bool(self.pasteboard)))
|
|
48
|
+
if held]
|
|
49
|
+
|
|
50
|
+
def public(self):
|
|
51
|
+
return {"names": list(self.names), "reset": self.reset, "args": list(self.args), "env": dict(self.env),
|
|
52
|
+
"steps": list(self.steps), "files": [{"from": str(src), "to": dest} for src, dest in self.files],
|
|
53
|
+
"keychain": self.keychain, "pasteboard": bool(self.pasteboard), "dialogs": dict(self.dialogs),
|
|
54
|
+
"destructive": bool(self.changes_state)}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def folder(root):
|
|
58
|
+
return Path(root) / ".mobster" / FOLDER
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def path_of(root, name):
|
|
62
|
+
for suffix in SUFFIXES:
|
|
63
|
+
path = folder(root) / f"{name}{suffix}"
|
|
64
|
+
if path.is_file():
|
|
65
|
+
return path
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def names(root):
|
|
70
|
+
"""Every fixture's name in the project, sorted."""
|
|
71
|
+
base = folder(root)
|
|
72
|
+
if not base.is_dir():
|
|
73
|
+
return []
|
|
74
|
+
return sorted({path.stem for path in base.iterdir() if path.suffix in SUFFIXES and NAME.fullmatch(path.stem)})
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load(root, name):
|
|
78
|
+
"""One fixture file as a dict of validated parts (not yet flattened)."""
|
|
79
|
+
from . import checks
|
|
80
|
+
path = path_of(root, name)
|
|
81
|
+
if path is None:
|
|
82
|
+
have = ", ".join(names(root)[:8])
|
|
83
|
+
raise FixtureError(f"There's no fixture called {name}." + (f" You have: {have}." if have else
|
|
84
|
+
" Add .mobster/fixtures/NAME.yaml."))
|
|
85
|
+
import yaml
|
|
86
|
+
try:
|
|
87
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
88
|
+
except (OSError, UnicodeDecodeError):
|
|
89
|
+
raise FixtureError(f"{path.name} can't be read. Check that it is a text file.") from None
|
|
90
|
+
except yaml.YAMLError:
|
|
91
|
+
raise FixtureError(f"{path.name} isn't valid YAML.") from None
|
|
92
|
+
if data is None:
|
|
93
|
+
data = {}
|
|
94
|
+
if not isinstance(data, dict) or set(data) - set(KEYS):
|
|
95
|
+
bad = sorted(set(data) - set(KEYS))[0] if isinstance(data, dict) else None
|
|
96
|
+
raise FixtureError(f"{path.name}: " + (f"`{str(bad)[:40]}` isn't a fixture key. Use {', '.join(KEYS)}."
|
|
97
|
+
if bad is not None else "a fixture is a mapping of keys."))
|
|
98
|
+
where = path.name
|
|
99
|
+
try:
|
|
100
|
+
extends = data.get("extends") or []
|
|
101
|
+
extends = [extends] if isinstance(extends, str) else extends
|
|
102
|
+
if not isinstance(extends, list) or not all(isinstance(item, str) and NAME.fullmatch(item) for item in extends):
|
|
103
|
+
raise CheckError("extends is a fixture name or a list of them")
|
|
104
|
+
reset = data.get("reset")
|
|
105
|
+
if reset is not None and reset not in checks.RESETS:
|
|
106
|
+
raise CheckError(f"reset is one of {', '.join(checks.RESETS)}, not {reset!r}")
|
|
107
|
+
launch = data.get("launch") or {}
|
|
108
|
+
if not isinstance(launch, dict) or set(launch) - {"args", "env"}:
|
|
109
|
+
raise CheckError("launch holds args and env")
|
|
110
|
+
args = checks._strings(launch.get("args"), "launch.args", checks.LIMITS["args"])
|
|
111
|
+
env = checks._env(launch.get("env"), "launch.env")
|
|
112
|
+
steps = checks._steps(data.get("steps"))
|
|
113
|
+
keychain = data.get("keychain")
|
|
114
|
+
if keychain not in (None, "reset", False):
|
|
115
|
+
raise CheckError("keychain is reset")
|
|
116
|
+
pasteboard = data.get("pasteboard") or ""
|
|
117
|
+
if not isinstance(pasteboard, str) or len(pasteboard) > PASTEBOARD_LIMIT:
|
|
118
|
+
raise CheckError(f"pasteboard is text of at most {PASTEBOARD_LIMIT:,} characters")
|
|
119
|
+
files = _files(data.get("files"), folder(root))
|
|
120
|
+
answers = dialog_policy.parse(data.get("dialogs"))
|
|
121
|
+
except CheckError as error:
|
|
122
|
+
raise FixtureError(f"{where}: {error}") from None
|
|
123
|
+
return {"extends": extends, "reset": reset, "args": args, "env": env, "steps": steps, "files": files,
|
|
124
|
+
"keychain": keychain == "reset", "pasteboard": pasteboard, "dialogs": answers}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _files(value, base):
|
|
128
|
+
if value is None:
|
|
129
|
+
return ()
|
|
130
|
+
if not isinstance(value, list) or len(value) > FILES_LIMIT:
|
|
131
|
+
raise CheckError(f"files is a list of at most {FILES_LIMIT} entries with from and to")
|
|
132
|
+
out = []
|
|
133
|
+
for index, item in enumerate(value, 1):
|
|
134
|
+
if (not isinstance(item, dict) or set(item) != {"from", "to"}
|
|
135
|
+
or not all(isinstance(v, str) for v in item.values())):
|
|
136
|
+
raise CheckError(f"files[{index}] needs from and to, both text")
|
|
137
|
+
source = (Path(base) / item["from"])
|
|
138
|
+
real = Path(os.path.realpath(source))
|
|
139
|
+
if Path(item["from"]).is_absolute() or ".." in Path(item["from"]).parts or not real.exists() or \
|
|
140
|
+
not (real == Path(os.path.realpath(base)) or Path(os.path.realpath(base)) in real.parents):
|
|
141
|
+
raise CheckError(f"files[{index}].from {item['from']!r} must be a file or folder inside .mobster/{FOLDER}/")
|
|
142
|
+
dest = Path(item["to"])
|
|
143
|
+
if not item["to"].strip() or dest.is_absolute() or ".." in dest.parts or str(dest) in (".", ""):
|
|
144
|
+
raise CheckError(f"files[{index}].to {item['to']!r} must be a path inside the app's container, such as "
|
|
145
|
+
"Documents/cart.json")
|
|
146
|
+
out.append((real, dest.as_posix()))
|
|
147
|
+
return tuple(out)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def resolve(root, wanted):
|
|
151
|
+
"""The flattened Effective of the fixture names in ``wanted``, in order."""
|
|
152
|
+
state = {"names": [], "reset": "none", "args": [], "env": {}, "steps": [], "files": {}, "keychain": False,
|
|
153
|
+
"pasteboard": "", "dialogs": {}}
|
|
154
|
+
done = []
|
|
155
|
+
|
|
156
|
+
def add(name, chain):
|
|
157
|
+
if name in chain:
|
|
158
|
+
raise FixtureError(f"Fixtures extend each other in a circle: {' → '.join([*chain, name])}.")
|
|
159
|
+
if len(chain) >= DEPTH:
|
|
160
|
+
raise FixtureError(f"Fixtures extend more than {DEPTH} deep, ending at {name}.")
|
|
161
|
+
if name in done:
|
|
162
|
+
return
|
|
163
|
+
part = _load(root, name)
|
|
164
|
+
for parent in part["extends"]:
|
|
165
|
+
add(parent, (*chain, name))
|
|
166
|
+
done.append(name)
|
|
167
|
+
state["names"].append(name)
|
|
168
|
+
if part["reset"] is not None:
|
|
169
|
+
state["reset"] = part["reset"]
|
|
170
|
+
state["args"] += part["args"]
|
|
171
|
+
state["env"].update(part["env"])
|
|
172
|
+
state["steps"] += part["steps"]
|
|
173
|
+
for source, dest in part["files"]:
|
|
174
|
+
state["files"][dest] = source
|
|
175
|
+
state["keychain"] = state["keychain"] or part["keychain"]
|
|
176
|
+
state["pasteboard"] = part["pasteboard"] or state["pasteboard"]
|
|
177
|
+
state["dialogs"] = dialog_policy.merge(state["dialogs"], part["dialogs"])
|
|
178
|
+
|
|
179
|
+
for name in wanted:
|
|
180
|
+
if not isinstance(name, str) or not NAME.fullmatch(name):
|
|
181
|
+
raise FixtureError(
|
|
182
|
+
"A fixture name is 1 to 60 lowercase letters, digits or hyphens, starting with a letter or digit.")
|
|
183
|
+
add(name, ())
|
|
184
|
+
return Effective(names=tuple(state["names"]), reset=state["reset"], args=tuple(state["args"]),
|
|
185
|
+
env=dict(state["env"]), steps=tuple(state["steps"]),
|
|
186
|
+
files=tuple((source, dest) for dest, source in state["files"].items()),
|
|
187
|
+
keychain=state["keychain"], pasteboard=state["pasteboard"], dialogs=dict(state["dialogs"]))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def with_fixtures(check, root):
|
|
191
|
+
if not check.fixture or check.fixture_origin is not None:
|
|
192
|
+
return check
|
|
193
|
+
effective = resolve(root, check.fixture)
|
|
194
|
+
return replace(check, fixture_origin=check,
|
|
195
|
+
reset=effective.reset if check.reset == "auto" and effective.reset != "none" else check.reset,
|
|
196
|
+
launch_args=effective.args + check.launch_args,
|
|
197
|
+
launch_env={**effective.env, **check.launch_env}, steps=effective.steps + check.steps,
|
|
198
|
+
dialogs=dialog_policy.merge(effective.dialogs, check.dialogs),
|
|
199
|
+
seed=Seed(effective.files, effective.keychain, effective.pasteboard, tuple(effective.changes_state)))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def for_run(check, runs_dir):
|
|
203
|
+
"""The check with its fixtures flattened in. The project is the folder that holds ``.mobster/`` beside the runs,
|
|
204
|
+
else the folder the check was read from; a check that names a fixture with no project to take it from is refused
|
|
205
|
+
before any simulator is touched."""
|
|
206
|
+
if not check.fixture or check.fixture_origin is not None:
|
|
207
|
+
return check
|
|
208
|
+
from .checks import project_root
|
|
209
|
+
runs_dir = Path(runs_dir)
|
|
210
|
+
root = runs_dir.parent.parent if runs_dir.parent.name == ".mobster" else None
|
|
211
|
+
if root is None and check.source:
|
|
212
|
+
root = project_root(check.source)
|
|
213
|
+
if root is None:
|
|
214
|
+
raise FixtureError(f"This check uses the fixture {', '.join(check.fixture)}, which comes from a project's "
|
|
215
|
+
".mobster/fixtures/. Run it from a project folder that has .mobster/, with --out left at "
|
|
216
|
+
"its default.")
|
|
217
|
+
return with_fixtures(check, root)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass(frozen=True)
|
|
221
|
+
class Seed:
|
|
222
|
+
files: tuple = ()
|
|
223
|
+
keychain: bool = False
|
|
224
|
+
pasteboard: str = ""
|
|
225
|
+
risky: tuple = () # what the fixtures change that a real iPhone must not get (Effective.changes_state)
|
|
226
|
+
|
|
227
|
+
def __bool__(self):
|
|
228
|
+
return bool(self.files or self.keychain or self.pasteboard)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def refuse_on_phone(check):
|
|
232
|
+
"""FixtureError when the check's fixtures would change a real iPhone's state."""
|
|
233
|
+
if check.seed.risky:
|
|
234
|
+
raise FixtureError(PHONE_REFUSAL.format(what=check.seed.risky[0], name=", ".join(check.fixture))
|
|
235
|
+
+ " " + PHONE_FIX)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def seed_simulator(seed, *, udid, bundle, run, pbcopy=None, actions=None):
|
|
239
|
+
"""Put the seed on simulator ``udid``: the keychain emptied, the files copied into ``bundle``'s data container and
|
|
240
|
+
the pasteboard set."""
|
|
241
|
+
did = []
|
|
242
|
+
if seed.keychain:
|
|
243
|
+
with actions.action("FIXTURE_KEYCHAIN") if actions is not None else nullcontext():
|
|
244
|
+
code, out = run(["keychain", udid, "reset"])
|
|
245
|
+
if code != 0:
|
|
246
|
+
raise FixtureError(f"The simulator's keychain wouldn't reset ({out[:120] or code}).")
|
|
247
|
+
did.append("keychain reset")
|
|
248
|
+
if seed.files:
|
|
249
|
+
code, container = run(["get_app_container", udid, bundle, "data"])
|
|
250
|
+
if code != 0 or not container.strip() or container.strip() == "(null)":
|
|
251
|
+
raise FixtureError(f"{bundle} isn't installed on the simulator, so there is no container to put files in.")
|
|
252
|
+
count = _copy(seed.files, container.strip(), udid, actions=actions)
|
|
253
|
+
did.append(f"{count} file{'' if count == 1 else 's'} copied")
|
|
254
|
+
if seed.pasteboard:
|
|
255
|
+
with actions.action("FIXTURE_PASTEBOARD") if actions is not None else nullcontext():
|
|
256
|
+
(pbcopy or _pbcopy)(udid, seed.pasteboard)
|
|
257
|
+
did.append("pasteboard set")
|
|
258
|
+
return did
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _pbcopy(udid, text):
|
|
262
|
+
try:
|
|
263
|
+
done = subprocess.run(["xcrun", "simctl", "pbcopy", udid], input=text, text=True, capture_output=True,
|
|
264
|
+
timeout=30)
|
|
265
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
266
|
+
raise FixtureError("The simulator's pasteboard wouldn't set.") from None
|
|
267
|
+
if done.returncode != 0:
|
|
268
|
+
raise FixtureError("The simulator's pasteboard wouldn't set.")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _copy(files, container, udid, *, actions=None):
|
|
272
|
+
from ..sim import apps
|
|
273
|
+
from ..sim.api import SimError
|
|
274
|
+
try:
|
|
275
|
+
root = apps.guard_container(container, udid)
|
|
276
|
+
except SimError as error:
|
|
277
|
+
raise FixtureError(str(error)) from None
|
|
278
|
+
total = count = 0
|
|
279
|
+
plan = []
|
|
280
|
+
for source, dest in files:
|
|
281
|
+
target = (root / dest)
|
|
282
|
+
for path in ([source] if source.is_file() else sorted(p for p in source.rglob("*"))):
|
|
283
|
+
if path.is_symlink():
|
|
284
|
+
raise FixtureError(f"{path.name} is a symbolic link, so Mobster didn't copy it.")
|
|
285
|
+
if path.is_file():
|
|
286
|
+
total += path.stat().st_size
|
|
287
|
+
count += 1
|
|
288
|
+
plan.append((path, target if source.is_file() else target / path.relative_to(source)))
|
|
289
|
+
if count > FILES_LIMIT * 4 or total > BYTES_LIMIT:
|
|
290
|
+
raise FixtureError(f"The seed files are {count} files and {total // 1024} KB; the limit is "
|
|
291
|
+
f"{FILES_LIMIT * 4} files and {BYTES_LIMIT // 1024 // 1024} MB.")
|
|
292
|
+
for _, target in plan:
|
|
293
|
+
for parent in [target, *target.parents]:
|
|
294
|
+
if parent == root:
|
|
295
|
+
break
|
|
296
|
+
if parent.is_symlink():
|
|
297
|
+
raise FixtureError(f"{target.relative_to(root)} goes through a symbolic link in the container, so "
|
|
298
|
+
"Mobster didn't copy it.")
|
|
299
|
+
if root not in Path(os.path.realpath(target)).parents:
|
|
300
|
+
raise FixtureError(f"{target.relative_to(root)} leaves the app's container, so Mobster didn't copy it.")
|
|
301
|
+
for path, target in plan:
|
|
302
|
+
with actions.action("FIXTURE_FILE") if actions is not None else nullcontext():
|
|
303
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
shutil.copyfile(path, target)
|
|
305
|
+
return count
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from .smart import LEFT_APP, LEFT_APP_FIX, check_approver, check_guard, no_smart_key, smart_request
|
|
5
|
+
from .verdict import CouldntRun
|
|
6
|
+
|
|
7
|
+
NOTE_LIMIT = 500
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _safe(cancelled):
|
|
11
|
+
try:
|
|
12
|
+
return bool(cancelled())
|
|
13
|
+
except Exception:
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _data_url_bytes(url):
|
|
18
|
+
if not isinstance(url, str) or not url.startswith("data:") or "," not in url:
|
|
19
|
+
return None
|
|
20
|
+
try:
|
|
21
|
+
return base64.b64decode(url.split(",", 1)[1])
|
|
22
|
+
except (ValueError, TypeError):
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Flow:
|
|
27
|
+
def run_smart(self, cancelled=None):
|
|
28
|
+
from .. import costs, engines, smart_events
|
|
29
|
+
from ..model_clients import cost_usd
|
|
30
|
+
if self.mode != "smart":
|
|
31
|
+
raise ValueError("run_smart needs mode smart")
|
|
32
|
+
key = self.key or engines.smart_key()
|
|
33
|
+
if not key:
|
|
34
|
+
why, fix = no_smart_key()
|
|
35
|
+
raise self._fail(CouldntRun("model", f"Smart can't run: {why}.", fix))
|
|
36
|
+
self.key = key
|
|
37
|
+
self.state = "running"
|
|
38
|
+
spent = {"usd": 0.0}
|
|
39
|
+
started = time.monotonic()
|
|
40
|
+
deadline = started + float(self.check.max_seconds) + 60 # a backstop past the loop's own time limit
|
|
41
|
+
names = {self.app.bundle_id: self.app.name}
|
|
42
|
+
|
|
43
|
+
def stopping():
|
|
44
|
+
if self._stop.is_set() or time.monotonic() > deadline:
|
|
45
|
+
return True
|
|
46
|
+
try:
|
|
47
|
+
return bool(cancelled and cancelled())
|
|
48
|
+
except Exception:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def on_inference(event):
|
|
52
|
+
if event.get("event") == "inference_finished" and isinstance(event.get("estimated_usd"), (int, float)):
|
|
53
|
+
spent["usd"] += event["estimated_usd"]
|
|
54
|
+
self.event(event)
|
|
55
|
+
|
|
56
|
+
def on_event(event):
|
|
57
|
+
kind = event.get("event")
|
|
58
|
+
if kind == "frontier_prompt":
|
|
59
|
+
data = _data_url_bytes(event.get("image"))
|
|
60
|
+
path = self.frame_from_bytes(data, "step") if data else None
|
|
61
|
+
if not data:
|
|
62
|
+
self._observe_screen(None)
|
|
63
|
+
if path is not None:
|
|
64
|
+
self._give_after_frame(path)
|
|
65
|
+
self.event({"event": kind, "step": event.get("step"), "operations": event.get("operations"),
|
|
66
|
+
"out": event.get("out")})
|
|
67
|
+
return
|
|
68
|
+
self.event(event)
|
|
69
|
+
if kind == "frontier_action":
|
|
70
|
+
op = event.get("operation") or ""
|
|
71
|
+
label = event.get("target_label") or ""
|
|
72
|
+
text = smart_events.step_text(op, label, names, text=event.get("text"))
|
|
73
|
+
typed = event.get("text") if op in smart_events.TYPING else None
|
|
74
|
+
step = self.record_step(op=op, text=text, target={"id": None, "label": label or None, "role": None},
|
|
75
|
+
typed=typed, changed=event.get("changed"))
|
|
76
|
+
# Its frame is the screen after it: the image Smart sees next turn, or the verdict frame.
|
|
77
|
+
self._awaiting_frame.append(step)
|
|
78
|
+
self.progress(f" {text}")
|
|
79
|
+
|
|
80
|
+
with self.lock:
|
|
81
|
+
client = engines.build_client(key=key)
|
|
82
|
+
costs.meter(client, on_inference)
|
|
83
|
+
guard = check_guard(self.manager, self.app.bundle_id)
|
|
84
|
+
driver, recording = self.driver, {}
|
|
85
|
+
if self.replay is not None and self.replay.mode != "off":
|
|
86
|
+
recorder = self.replay.recorder() # the path a later run replays, if this one passes
|
|
87
|
+
driver, recording = recorder.wrap(self.driver), {"step_observer": recorder.observe_step}
|
|
88
|
+
agent = engines.build_frontier(driver, client, apps=names, emit=on_event,
|
|
89
|
+
max_cost_usd=self.check.max_usd, approve=check_approver(self.manager),
|
|
90
|
+
cancelled=stopping, guard=guard, dialogs=True,
|
|
91
|
+
dialog_policy=self.dialogs, **recording)
|
|
92
|
+
agent.max_seconds = float(self.check.max_seconds) # the check's time limit, not the app's 15 minutes
|
|
93
|
+
from .. import intents
|
|
94
|
+
if intents.enabled() and getattr(self.target, "udid", None):
|
|
95
|
+
from ..intents.plan import LinkFirst
|
|
96
|
+
agent.link_first = LinkFirst.for_device(self.target.udid)
|
|
97
|
+
request = smart_request(self.app.name, self._live.steps)
|
|
98
|
+
self.progress(f"Smart: {request}")
|
|
99
|
+
outcome = agent.run(request)
|
|
100
|
+
self.timing["flow"] = round(time.monotonic() - started, 2)
|
|
101
|
+
status = outcome.get("status")
|
|
102
|
+
if outcome.get("code") == "unknown_outcome":
|
|
103
|
+
raise self._fail(CouldntRun("unknown_outcome", "A phone action's outcome is unknown.",
|
|
104
|
+
"Inspect the phone before another run; Mobster did not retry the action."))
|
|
105
|
+
if status == "stopped" and not self._stop.is_set() and not (cancelled and _safe(cancelled)):
|
|
106
|
+
status = "timeout" # the backstop, not the user
|
|
107
|
+
self._smart_status, self._smart_reason = status, str(outcome.get("reason") or "")
|
|
108
|
+
if guard is not None and guard.left and outcome.get("code") == "blocked_app":
|
|
109
|
+
self._left_app = guard.left
|
|
110
|
+
self._model_failed = bool(getattr(client, "model_failed", False))
|
|
111
|
+
loop_cost = cost_usd(getattr(client, "model", ""), outcome.get("usage") or {}) or 0.0
|
|
112
|
+
self.cost_usd = round(max(spent["usd"], loop_cost), 5)
|
|
113
|
+
note = outcome.get("answer") or outcome.get("reason") or ""
|
|
114
|
+
self.agent = {"status": status, "note": self._scrub(str(note))[:NOTE_LIMIT] or None,
|
|
115
|
+
"model": getattr(client, "model", None), "turns": outcome.get("steps"),
|
|
116
|
+
**_link_first(outcome)}
|
|
117
|
+
self.log(f"Smart ended {status} after {outcome.get('steps')} turns, ${self.cost_usd:.4f}")
|
|
118
|
+
self.state = "ready"
|
|
119
|
+
|
|
120
|
+
def run_flow(self, cancelled=None):
|
|
121
|
+
"""The check's steps: replayed from the recorded path when one is cached and replay is on, else Smart."""
|
|
122
|
+
if (self.replay is not None and not (self.check.throughout or self.check.somewhere)
|
|
123
|
+
and self.replay.lookup() is not None):
|
|
124
|
+
return self.run_replay(cancelled) # a replay shows no screens to sweep, so `throughout` runs Smart
|
|
125
|
+
if self.replay is not None and self.replay.reason:
|
|
126
|
+
self.progress(f"Smart runs every step ({self.replay.reason}); a pass records the path to replay")
|
|
127
|
+
return self.run_smart(cancelled)
|
|
128
|
+
|
|
129
|
+
def run_replay(self, cancelled=None):
|
|
130
|
+
"""Replay the recorded path with no model (replay_core); Smart heals a step that changed, for that step only,
|
|
131
|
+
unless replay is strict."""
|
|
132
|
+
from .. import engines
|
|
133
|
+
if self.mode != "smart" or self.replay is None:
|
|
134
|
+
raise ValueError("run_replay needs mode smart with replay on")
|
|
135
|
+
self.state = "running"
|
|
136
|
+
started = time.monotonic()
|
|
137
|
+
steps = len(self.replay.path["steps"])
|
|
138
|
+
if (steps and (self._a11y is not None or self._l10n is not None)
|
|
139
|
+
and not (self.source_observation is not None and self.source_observation.active)):
|
|
140
|
+
self._incomplete_screen("Replay did not read each intermediate accessibility screen")
|
|
141
|
+
self.progress(f"Replaying {steps} recorded step{'' if steps == 1 else 's'} (no model)")
|
|
142
|
+
with self.lock:
|
|
143
|
+
tune = getattr(self.driver, "tune", None)
|
|
144
|
+
if callable(tune):
|
|
145
|
+
try:
|
|
146
|
+
tune(rich=True, glide=True)
|
|
147
|
+
except Exception as error:
|
|
148
|
+
self.log(f"tune failed: {type(error).__name__}")
|
|
149
|
+
result = self.replay.replay(cancelled)
|
|
150
|
+
self.timing["flow"] = round(time.monotonic() - started, 2)
|
|
151
|
+
self.cost_usd = round(result.cost_usd, 5)
|
|
152
|
+
status = {"done": result.model_status or "done", "declined": "approval_denied", "stopped": "stopped",
|
|
153
|
+
"drift": "done"}.get(result.status)
|
|
154
|
+
if result.status == "drift":
|
|
155
|
+
self._replay_drift = result.reason
|
|
156
|
+
if result.status == "heal_failed" and result.model_status == "budget":
|
|
157
|
+
status = "budget"
|
|
158
|
+
healed = len(result.heals)
|
|
159
|
+
note = (f"Replayed {result.replayed} step{'' if result.replayed == 1 else 's'} with no model"
|
|
160
|
+
+ (f", healed {healed}" if healed else "") + (f": {result.reason}" if result.reason else ""))
|
|
161
|
+
self.agent = {"status": status or result.status, "note": self._scrub(note)[:NOTE_LIMIT],
|
|
162
|
+
"model": engines.smart_model() if result.cost_usd else None, "turns": None, "replayed": True}
|
|
163
|
+
self.log(f"replay ended {result.status} after {result.replayed} steps, {healed} healed, "
|
|
164
|
+
f"${self.cost_usd:.4f}")
|
|
165
|
+
self.state = "ready"
|
|
166
|
+
if self._left_app:
|
|
167
|
+
raise self._fail(self._smart_failure())
|
|
168
|
+
if result.status == "dialog":
|
|
169
|
+
raise self._fail(CouldntRun("system_dialog", f"The replay stopped: {result.reason}",
|
|
170
|
+
"Name an answer in the check's dialogs, or answer it by hand and run the check "
|
|
171
|
+
"again; the recorded steps are kept."))
|
|
172
|
+
if status is None:
|
|
173
|
+
if result.model_status == "unknown_outcome":
|
|
174
|
+
raise self._fail(CouldntRun("unknown_outcome", f"The replay stopped: {result.reason}",
|
|
175
|
+
"Check the app before starting another run; the action may have happened."))
|
|
176
|
+
raise self._fail(CouldntRun("replay", f"The replay stopped: {result.reason}",
|
|
177
|
+
"Run it again: the next run records the flow afresh (or use --fresh)."))
|
|
178
|
+
self._smart_status, self._smart_reason = status, result.reason or ""
|
|
179
|
+
|
|
180
|
+
def _smart_failure(self):
|
|
181
|
+
"""CouldntRun for a Smart run that ended in error or was stopped (§4 rule 4), else None."""
|
|
182
|
+
status = self._smart_status
|
|
183
|
+
if self._left_app:
|
|
184
|
+
return CouldntRun("usage", LEFT_APP.format(app=self.app.name, other=self._left_app), LEFT_APP_FIX)
|
|
185
|
+
if status == "stopped":
|
|
186
|
+
return CouldntRun("stopped", "The run was stopped before the flow finished.")
|
|
187
|
+
if status != "error":
|
|
188
|
+
return None
|
|
189
|
+
from .. import engines
|
|
190
|
+
reason = self._smart_reason
|
|
191
|
+
model = (self.agent or {}).get("model") or engines.smart_model()
|
|
192
|
+
found = engines.PROVIDER_ERROR.search(reason)
|
|
193
|
+
provider = found.group(1) if found else engines.provider_name(model)
|
|
194
|
+
ok, why = engines.model_failure(reason, model=model)
|
|
195
|
+
if ok is False:
|
|
196
|
+
return CouldntRun("model", why, f"Check your {provider} key and credit, then run again.")
|
|
197
|
+
if self._model_failed or found or "OpenAI" in reason or "Anthropic" in reason:
|
|
198
|
+
reach = engines.NO_ANTHROPIC_REACH if provider == "Anthropic" else engines.NO_OPENAI_REACH
|
|
199
|
+
return CouldntRun("model", engines.plain_error(reason, model_failed=True, model=model) or reach)
|
|
200
|
+
from ..frontier import CONNECTION_LOST
|
|
201
|
+
if any(name in reason for name in CONNECTION_LOST + ("TimeoutError", "timed out", "TransportError")):
|
|
202
|
+
return CouldntRun("wda", "WebDriverAgent stopped answering during the flow and didn't come back.",
|
|
203
|
+
"Run `mobster sim doctor --fix`, then try again.")
|
|
204
|
+
return CouldntRun("internal", "Smart stopped with an unexpected error. run.log has the details.")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _link_first(outcome):
|
|
208
|
+
"""{"linkFirst": {"taken", "landed", "modelCallsSaved"}} when the run opened a link before any model call."""
|
|
209
|
+
timing = outcome.get("timing") or {}
|
|
210
|
+
if not timing.get("link_first"):
|
|
211
|
+
return {}
|
|
212
|
+
return {"linkFirst": {"taken": timing["link_first"], "landed": timing.get("link_first_landed", 0),
|
|
213
|
+
"modelCallsSaved": timing.get("model_calls_saved", 0)}}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""The locale audit: what `mobster test --locale de,ja,ar` reads in the trees of a check run in a language."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .a11y import EDGE, TEXT, _clipped, _describe
|
|
6
|
+
from .tree import SCROLL_CONTAINERS
|
|
7
|
+
|
|
8
|
+
NAME = "l10n"
|
|
9
|
+
RTL = frozenset({"ar", "he", "fa", "ur", "ps", "sd", "ug", "yi", "dv"})
|
|
10
|
+
# Languages whose own script isn't Latin: an ASCII sentence in one of them was not translated.
|
|
11
|
+
NON_LATIN = RTL | frozenset({"ja", "ko", "zh", "ru", "uk", "bg", "sr", "mk", "be", "el", "th", "hi", "bn", "ta", "te",
|
|
12
|
+
"ml", "kn", "gu", "pa", "mr", "ne", "si", "my", "km", "lo", "ka", "hy", "am", "mn"})
|
|
13
|
+
GUIDELINES = {"text-truncated": "Localisation: long strings",
|
|
14
|
+
"untranslated-string": "Localisation: missing translation",
|
|
15
|
+
"rtl-not-mirrored": "Localisation: right-to-left layout"}
|
|
16
|
+
ELLIPSIS = re.compile(r".*(…|\.\.\.)\s*$", re.S)
|
|
17
|
+
ASCII_WORDS = re.compile(r"[A-Za-z]{2,}(?:[ '\-][A-Za-z]{2,})+|[A-Za-z]{5,}")
|
|
18
|
+
RTL_SCRIPT = re.compile(r"[-ࣿיִ-﷿ﹰ-]")
|
|
19
|
+
WIDE = 0.6 # a container at least this share of the screen's width counts as a full-width row
|
|
20
|
+
NARROW = 0.7 # text narrower than this share of its row can show which edge it is flush to
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def language(locale):
|
|
24
|
+
return (locale or "").replace("-", "_").split("_")[0].lower()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_rtl(locale):
|
|
28
|
+
return language(locale) in RTL
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _finding(rule, node, message, frame):
|
|
32
|
+
name, where = _describe(node)
|
|
33
|
+
x, y, w, h = node.rect
|
|
34
|
+
return {"id": rule, "audit": NAME, "guideline": GUIDELINES[rule], "level": "warn", "frame": frame,
|
|
35
|
+
"message": f"{name} {where}: {message}", "evidence": f"{name} {where}", "baseline": False,
|
|
36
|
+
"element": {"role": node.role, "label": node.label, "identifier": node.identifier, "path": node.path,
|
|
37
|
+
"rect": [x, y, w, h]}}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _ancestors(path):
|
|
41
|
+
while "/" in path.strip("/"):
|
|
42
|
+
path = path.rsplit("/", 1)[0]
|
|
43
|
+
yield path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def audit_tree(tree, locale, *, frame=None):
|
|
47
|
+
"""The findings of one screen's tree in ``locale``."""
|
|
48
|
+
shown = tree.shown()
|
|
49
|
+
by_path = {node.path: node for node in tree.nodes}
|
|
50
|
+
width = tree.size[0]
|
|
51
|
+
lang = language(locale)
|
|
52
|
+
out = []
|
|
53
|
+
for node in shown:
|
|
54
|
+
label = node.label.strip()
|
|
55
|
+
x, y, w, h = node.rect
|
|
56
|
+
if node.role in TEXT and label and w > 0 and h > 0:
|
|
57
|
+
if ELLIPSIS.fullmatch(label):
|
|
58
|
+
out.append(_finding("text-truncated", node,
|
|
59
|
+
"the text ends in an ellipsis, so it was cut short. Let it wrap or give it room "
|
|
60
|
+
"(SwiftUI: remove .lineLimit; UIKit: numberOfLines = 0).", frame))
|
|
61
|
+
else:
|
|
62
|
+
clipped = _clipped(node, by_path, width)
|
|
63
|
+
if clipped:
|
|
64
|
+
out.append(_finding("text-truncated", node,
|
|
65
|
+
f"in {locale} it is {clipped}. Let the text wrap or the container grow.",
|
|
66
|
+
frame))
|
|
67
|
+
if lang in NON_LATIN and node.role in TEXT and label and _ascii_sentence(label):
|
|
68
|
+
out.append(_finding("untranslated-string", node,
|
|
69
|
+
f"the text is plain English in {locale}. Translate it, or ignore this if it is a "
|
|
70
|
+
"brand name.", frame))
|
|
71
|
+
if is_rtl(locale):
|
|
72
|
+
out.extend(_not_mirrored(shown, by_path, width, frame))
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ascii_sentence(label):
|
|
77
|
+
letters = re.sub(r"[^A-Za-z]", "", label)
|
|
78
|
+
return bool(ASCII_WORDS.fullmatch(" ".join(label.split()))) and len(letters) >= 5 \
|
|
79
|
+
and len(re.sub(r"[A-Za-z '\-]", "", label)) == 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _not_mirrored(shown, by_path, width, frame):
|
|
83
|
+
out = []
|
|
84
|
+
for bar in (node for node in shown if node.role == "NavigationBar"):
|
|
85
|
+
bx, _, bw, _ = bar.rect
|
|
86
|
+
buttons = [node for node in shown if node.role == "Button" and node.path.startswith(bar.path + "/")
|
|
87
|
+
and node.rect[2] > 0]
|
|
88
|
+
if buttons and all(node.rect[0] + node.rect[2] / 2 < bx + bw / 2 for node in buttons) and bw >= width * WIDE:
|
|
89
|
+
out.append(_finding("rtl-not-mirrored", bar,
|
|
90
|
+
"every button sits in the left half, so the back button didn't move to the right. "
|
|
91
|
+
"Use leading and trailing edges, not left and right.", frame))
|
|
92
|
+
for node in shown:
|
|
93
|
+
label = node.label.strip()
|
|
94
|
+
x, y, w, h = node.rect
|
|
95
|
+
if node.role != "StaticText" or not label or not RTL_SCRIPT.search(label) or w <= 0 or h <= 0:
|
|
96
|
+
continue
|
|
97
|
+
row = next((by_path[path] for path in _ancestors(node.path) if path in by_path
|
|
98
|
+
and by_path[path].rect[2] >= width * WIDE and by_path[path].role not in SCROLL_CONTAINERS), None)
|
|
99
|
+
if row is None or w > row.rect[2] * NARROW:
|
|
100
|
+
continue
|
|
101
|
+
left, right = x - row.rect[0], (row.rect[0] + row.rect[2]) - (x + w)
|
|
102
|
+
if left + EDGE < right and left < row.rect[2] * 0.1:
|
|
103
|
+
out.append(_finding("rtl-not-mirrored", node,
|
|
104
|
+
"right-to-left text is flush to the left edge of its row. Align it to the leading "
|
|
105
|
+
"edge (SwiftUI: .multilineTextAlignment(.leading); UIKit: .natural).", frame))
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Collector:
|
|
110
|
+
"""A run's screens, audited once each (``add(tree, frame)``), like a11y.Collector."""
|
|
111
|
+
|
|
112
|
+
LIMIT = 60
|
|
113
|
+
|
|
114
|
+
def __init__(self, locale):
|
|
115
|
+
self.locale = locale
|
|
116
|
+
self._seen, self._found, self._keys = {}, [], set()
|
|
117
|
+
|
|
118
|
+
def add(self, tree, frame=None):
|
|
119
|
+
fingerprint = tree.audit_fingerprint()
|
|
120
|
+
if fingerprint in self._seen:
|
|
121
|
+
if frame is not None:
|
|
122
|
+
start, end = self._seen[fingerprint]
|
|
123
|
+
for finding in self._found[start:end]:
|
|
124
|
+
if finding["frame"] is None:
|
|
125
|
+
finding["frame"] = frame
|
|
126
|
+
return True
|
|
127
|
+
if len(self._seen) >= self.LIMIT:
|
|
128
|
+
return False
|
|
129
|
+
start = len(self._found)
|
|
130
|
+
for finding in audit_tree(tree, self.locale, frame=frame):
|
|
131
|
+
element = finding["element"]
|
|
132
|
+
key = (finding["id"], element["role"], element["label"], element["identifier"],
|
|
133
|
+
tuple(round(n) for n in element["rect"]))
|
|
134
|
+
if key not in self._keys:
|
|
135
|
+
self._keys.add(key)
|
|
136
|
+
self._found.append(finding)
|
|
137
|
+
self._seen[fingerprint] = (start, len(self._found))
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
def findings(self):
|
|
141
|
+
return list(self._found)
|