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,802 @@
|
|
|
1
|
+
"""Maestro flows as checks; the YAML is read with safe_load only and never reaches a model as free text."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..verify.checks import LIMITS, CheckError, check_from_dict, check_to_dict
|
|
9
|
+
|
|
10
|
+
FILE_LIMIT = 1_000_000
|
|
11
|
+
MAX_DEPTH = 8
|
|
12
|
+
TEXT_LIMIT = 120 # a quoted value in a step
|
|
13
|
+
INPUT_LIMIT = 300 # typed text
|
|
14
|
+
MAX_FLOWS = 500
|
|
15
|
+
REGEX_CHARS = set("*+?[](){}|^$\\")
|
|
16
|
+
ENV_REF = re.compile(r"(?<!\\)\$\{([^}]*)\}")
|
|
17
|
+
ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
18
|
+
PERCENT_POINT = re.compile(r"^\s*(\d{1,3}(?:\.\d+)?)%\s*,\s*(\d{1,3}(?:\.\d+)?)%\s*$")
|
|
19
|
+
ABS_POINT = re.compile(r"^\s*(\d{1,5}(?:\.\d+)?)\s*,\s*(\d{1,5}(?:\.\d+)?)\s*$")
|
|
20
|
+
HEADER_KEYS = ("appId", "name", "tags", "env", "onFlowStart", "onFlowComplete", "properties", "jsEngine")
|
|
21
|
+
DIRECTIONS = ("UP", "DOWN", "LEFT", "RIGHT")
|
|
22
|
+
# Options that only tune timing or retries: Mobster waits for the screen itself and verifies each action.
|
|
23
|
+
TIMING = ("retryTapIfNoChange", "waitToSettleTimeoutMs", "duration", "speed", "timeout", "label",
|
|
24
|
+
"visibilityPercentage", "centerElement", "delay", "waitUntilVisible")
|
|
25
|
+
RELATIVE = {"below": "below", "above": "above", "leftOf": "to the left of", "rightOf": "to the right of",
|
|
26
|
+
"childOf": "inside", "containsChild": "that contains", "containsDescendants": "that contains"}
|
|
27
|
+
KEYS = {"home": "Press the Home button", "enter": "Press the Return key on the keyboard",
|
|
28
|
+
"backspace": "Press the delete key on the keyboard"}
|
|
29
|
+
|
|
30
|
+
# Commands Mobster doesn't run: (why, what to do instead). An unsupported assertion refuses the whole flow.
|
|
31
|
+
UNSUPPORTED = {
|
|
32
|
+
"runScript": ("runs JavaScript, which Mobster doesn't run",
|
|
33
|
+
"move the logic into the app or the launch environment, "
|
|
34
|
+
"or write that part as a Mobster check by hand"),
|
|
35
|
+
"evalScript": ("runs JavaScript, which Mobster doesn't run", "set the value in the flow's env instead"),
|
|
36
|
+
"assertTrue": ("judges a JavaScript condition, which Mobster doesn't run",
|
|
37
|
+
"write the condition as assertVisible or assertNotVisible on what the screen shows"),
|
|
38
|
+
"assertWithAI": ("asks a model to judge the screen, and Mobster's verdicts never come from a model",
|
|
39
|
+
"write the assertion as assertVisible or assertNotVisible"),
|
|
40
|
+
"assertNoDefectsWithAI": ("asks a model to judge the screen, and Mobster's verdicts never come from a model",
|
|
41
|
+
"write the assertion as assertVisible or assertNotVisible"),
|
|
42
|
+
"assertScreenshot": ("compares pixels, and Mobster checks the accessibility tree",
|
|
43
|
+
"assert the text or element the screenshot proves with assertVisible"),
|
|
44
|
+
"repeat": ("loops, and a check is a fixed list of steps", "write the loop out as steps, or use scrollUntilVisible"),
|
|
45
|
+
"retry": ("retries commands, and a check is a fixed list of steps",
|
|
46
|
+
"remove the retry; `mobster test --retries N` retries a whole check"),
|
|
47
|
+
"takeScreenshot": ("Mobster records a frame for every step of every run", "read the frames in the report"),
|
|
48
|
+
"startRecording": ("Mobster records video itself", "run `mobster test --video`"),
|
|
49
|
+
"stopRecording": ("Mobster records video itself", "run `mobster test --video`"),
|
|
50
|
+
"setLocation": ("changing the simulated location isn't a check step",
|
|
51
|
+
"set it before the run with `xcrun simctl location`"),
|
|
52
|
+
"setPermissions": ("granting permissions isn't a check step",
|
|
53
|
+
"grant them before the run with `xcrun simctl privacy`"),
|
|
54
|
+
"travel": ("moving the simulated location isn't a check step",
|
|
55
|
+
"set it before the run with `xcrun simctl location`"),
|
|
56
|
+
"setAirplaneMode": ("changing network state isn't a check step", "set it on the device before the run"),
|
|
57
|
+
"toggleAirplaneMode": ("changing network state isn't a check step", "set it on the device before the run"),
|
|
58
|
+
"setOrientation": ("rotating the device isn't a check step", "rotate the device before the run"),
|
|
59
|
+
"setClipboard": ("the clipboard isn't a check step", "run `mobster phone clipboard set`"),
|
|
60
|
+
"copyTextFrom": ("copies screen text into a variable, and a check has no variables",
|
|
61
|
+
"assert the text with assertVisible"),
|
|
62
|
+
"pasteText": ("pastes a variable, and a check has no variables", "type the text with inputText"),
|
|
63
|
+
"addMedia": ("adding photos isn't a check step", "add them before the run with `xcrun simctl addmedia`"),
|
|
64
|
+
"clearKeychain": ("clearing the keychain isn't a check step", "run `mobster sim erase` before the run"),
|
|
65
|
+
"clearState": ("clearing app data mid-flow isn't a check step",
|
|
66
|
+
"put `clearState: true` on the first launchApp, or set `reset: data` in the check"),
|
|
67
|
+
"stopApp": ("stopping the app mid-flow isn't a check step", "use launchApp to start it again"),
|
|
68
|
+
"killApp": ("killing the app mid-flow isn't a check step", "use launchApp to start it again"),
|
|
69
|
+
"openBrowser": ("Android only", "use openLink"),
|
|
70
|
+
"inputRandomText": ("types random text, so the run isn't repeatable", "use inputText with fixed text"),
|
|
71
|
+
"inputRandomNumber": ("types random text, so the run isn't repeatable", "use inputText with fixed text"),
|
|
72
|
+
"inputRandomEmail": ("types random text, so the run isn't repeatable", "use inputText with fixed text"),
|
|
73
|
+
"inputRandomPersonName": ("types random text, so the run isn't repeatable", "use inputText with fixed text"),
|
|
74
|
+
"defineVariables": ("sets variables, and a check has none", "put the values in the flow's env"),
|
|
75
|
+
"pauseTest": ("pauses for a person", "remove it"),
|
|
76
|
+
"setDarkMode": ("changing appearance isn't a check step", "set it before the run with `xcrun simctl ui`"),
|
|
77
|
+
}
|
|
78
|
+
ASSERTIONS = ("assertTrue", "assertWithAI", "assertNoDefectsWithAI", "assertScreenshot")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MaestroError(Exception):
|
|
82
|
+
"""The file can't be read as a flow at all."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class Problem:
|
|
87
|
+
where: str # "command 4 (runScript)", with the file for a subflow
|
|
88
|
+
command: str
|
|
89
|
+
why: str
|
|
90
|
+
instead: str = ""
|
|
91
|
+
assertion: bool = False # an assertion is never dropped
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class FlowResult:
|
|
96
|
+
source: str # path shown to the user, relative to the import root
|
|
97
|
+
outcome: str = "imported" # imported | refused | skipped
|
|
98
|
+
checks: list = field(default_factory=list) # of (file stem, check dict)
|
|
99
|
+
problems: list = field(default_factory=list) # of Problem: why a flow was refused
|
|
100
|
+
placeholders: list = field(default_factory=list) # of Problem: became a marked step for the first run
|
|
101
|
+
same: list = field(default_factory=list) # of file stems: already on disk exactly as imported
|
|
102
|
+
converted: int = 0 # commands that became steps or expectations as written
|
|
103
|
+
dropped: list = field(default_factory=list) # of Problem: dropped on request
|
|
104
|
+
notes: list = field(default_factory=list) # of str: kept, with a caveat
|
|
105
|
+
reason: str = "" # for skipped, or a flow that can't be read
|
|
106
|
+
|
|
107
|
+
def to_dict(self):
|
|
108
|
+
return {"source": self.source, "outcome": self.outcome, "reason": self.reason or None,
|
|
109
|
+
"checks": [{"file": f"{stem}.yaml", "steps": len(data.get("steps") or ()),
|
|
110
|
+
"expect": len(data["expect"])} for stem, data in self.checks],
|
|
111
|
+
"refused": [vars(p) for p in self.problems] if self.outcome == "refused" else [],
|
|
112
|
+
"converted": self.converted, "needsFirstRun": [vars(p) for p in self.placeholders],
|
|
113
|
+
"dropped": [vars(p) for p in self.dropped], "notes": list(self.notes)}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _Ctx:
|
|
117
|
+
def __init__(self, root, skip_unsupported):
|
|
118
|
+
self.root, self.skip = Path(root), skip_unsupported
|
|
119
|
+
self.problems, self.dropped, self.notes, self.placeholders = [], [], [], []
|
|
120
|
+
self.converted = 0
|
|
121
|
+
self.ops = [] # ("act", text) | ("assert", dict, name) | ("launch", {...}) | ("url", url)
|
|
122
|
+
self.launch = {}
|
|
123
|
+
self.loaded = None
|
|
124
|
+
|
|
125
|
+
def problem(self, where, command, why, instead="", assertion=False):
|
|
126
|
+
assertion = assertion or command.startswith("assert") or command == "extendedWaitUntil"
|
|
127
|
+
item = Problem(where, command, why, instead, assertion)
|
|
128
|
+
if self.skip and not assertion:
|
|
129
|
+
self.dropped.append(item)
|
|
130
|
+
else:
|
|
131
|
+
self.problems.append(item)
|
|
132
|
+
|
|
133
|
+
def unsupported(self, where, command, why, instead, assertion=False):
|
|
134
|
+
"""A command Mobster can't run: refused when it is an assertion, dropped on request, else a marked step."""
|
|
135
|
+
if assertion or self.skip:
|
|
136
|
+
return self.problem(where, command, why, instead, assertion)
|
|
137
|
+
self.placeholders.append(Problem(where, command, why, instead))
|
|
138
|
+
name = command if ENV_NAME.fullmatch(command) and len(command) <= 40 else "unknown"
|
|
139
|
+
self.ops.append(("act", f"TODO (Maestro {name}, not imported): {why}. Look at the screen and do what this "
|
|
140
|
+
"point in the flow needs, or nothing if it needs nothing"))
|
|
141
|
+
|
|
142
|
+
def started(self):
|
|
143
|
+
return any(op[0] in ("act", "assert") for op in self.ops)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def read_flow(path):
|
|
147
|
+
"""(config dict, commands list) from a Maestro flow file. Raises MaestroError on a file that isn't one."""
|
|
148
|
+
import yaml
|
|
149
|
+
path = Path(path)
|
|
150
|
+
try:
|
|
151
|
+
if path.stat().st_size > FILE_LIMIT:
|
|
152
|
+
raise MaestroError("larger than 1 MB")
|
|
153
|
+
text = path.read_text(encoding="utf-8")
|
|
154
|
+
except OSError as error:
|
|
155
|
+
raise MaestroError(f"can't be read ({error.strerror or type(error).__name__})") from None
|
|
156
|
+
except UnicodeDecodeError:
|
|
157
|
+
raise MaestroError("isn't UTF-8 text") from None
|
|
158
|
+
try:
|
|
159
|
+
docs = [doc for doc in yaml.safe_load_all(text)]
|
|
160
|
+
except yaml.YAMLError as error:
|
|
161
|
+
mark = getattr(error, "problem_mark", None)
|
|
162
|
+
raise MaestroError(f"line {mark.line + 1}: {getattr(error, 'problem', None) or 'not valid YAML'}"
|
|
163
|
+
if mark is not None else "isn't valid YAML") from None
|
|
164
|
+
except (RecursionError, ValueError):
|
|
165
|
+
raise MaestroError("is nested too deeply") from None
|
|
166
|
+
docs = [doc for doc in docs if doc is not None]
|
|
167
|
+
if len(docs) == 1 and isinstance(docs[0], list):
|
|
168
|
+
return {}, docs[0]
|
|
169
|
+
if len(docs) == 1 and isinstance(docs[0], dict):
|
|
170
|
+
return docs[0], []
|
|
171
|
+
if len(docs) == 2 and isinstance(docs[0], dict) and isinstance(docs[1], list):
|
|
172
|
+
return docs[0], docs[1]
|
|
173
|
+
if not docs:
|
|
174
|
+
raise MaestroError("is empty")
|
|
175
|
+
raise MaestroError("isn't a Maestro flow: expected a config, then `---`, then a list of commands")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def discover(paths):
|
|
179
|
+
"""The flow files in ``paths`` as [(file, root)], sorted and de-duplicated."""
|
|
180
|
+
found = {}
|
|
181
|
+
for path in paths:
|
|
182
|
+
path = Path(path).expanduser()
|
|
183
|
+
if path.is_dir():
|
|
184
|
+
root = path.resolve()
|
|
185
|
+
for file in sorted(path.iterdir()):
|
|
186
|
+
if file.suffix.lower() in (".yaml", ".yml") and file.is_file() and not file.is_symlink() \
|
|
187
|
+
and not file.name.startswith("."):
|
|
188
|
+
found.setdefault(file.resolve(), root)
|
|
189
|
+
elif path.is_file():
|
|
190
|
+
found.setdefault(path.resolve(), path.resolve().parent)
|
|
191
|
+
else:
|
|
192
|
+
raise MaestroError(f"{path} doesn't exist")
|
|
193
|
+
return sorted(found.items())[:MAX_FLOWS]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _substitute(value, env, ctx, where, command):
|
|
197
|
+
"""``${NAME}`` from the flow's env. A name that isn't set, and any JavaScript expression, is a problem."""
|
|
198
|
+
if not isinstance(value, str):
|
|
199
|
+
return value
|
|
200
|
+
|
|
201
|
+
def sub(found):
|
|
202
|
+
name = found.group(1).strip()
|
|
203
|
+
if not ENV_NAME.fullmatch(name):
|
|
204
|
+
ctx.problem(where, command, f"uses the JavaScript expression ${{{name[:40]}}}, which Mobster doesn't run",
|
|
205
|
+
"write the value out, or set it in the flow's env")
|
|
206
|
+
failed.append(name)
|
|
207
|
+
return ""
|
|
208
|
+
if name not in env:
|
|
209
|
+
ctx.problem(where, command, f"uses ${{{name}}}, which the flow's env doesn't set",
|
|
210
|
+
f"add {name} to the flow's env, or write the value out")
|
|
211
|
+
failed.append(name)
|
|
212
|
+
return ""
|
|
213
|
+
return str(env[name])
|
|
214
|
+
failed = []
|
|
215
|
+
out = ENV_REF.sub(sub, value).replace("\\${", "${")
|
|
216
|
+
return None if failed else out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _quote(text, limit=TEXT_LIMIT):
|
|
220
|
+
"""A value inside a step: JSON-quoted on one line, so it reads as data."""
|
|
221
|
+
text = str(text)
|
|
222
|
+
if len(text) > limit:
|
|
223
|
+
raise ValueError(f"is longer than {limit} characters")
|
|
224
|
+
return json.dumps(text, ensure_ascii=False)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _scalar(value):
|
|
228
|
+
if isinstance(value, bool) or value is None:
|
|
229
|
+
return None
|
|
230
|
+
return str(value) if isinstance(value, (str, int, float)) else None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _regex(text):
|
|
234
|
+
return any(char in REGEX_CHARS for char in text)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _literal(text):
|
|
238
|
+
"""``text`` with every regex character escaped and nothing else touched."""
|
|
239
|
+
return re.sub(r"([\\.^$*+?()\[\]{}|/])", r"\\\1", text)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _matcher(text, *, fold=True):
|
|
243
|
+
# Ids stay exact (fold=False): that can fail a flow Maestro would pass, never pass one Maestro would fail.
|
|
244
|
+
if _regex(text):
|
|
245
|
+
return f"/^(?:{text})$/i"
|
|
246
|
+
if fold and text.lower() != text.upper():
|
|
247
|
+
return f"/^{_literal(text)}$/i"
|
|
248
|
+
return text
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass
|
|
252
|
+
class Sel:
|
|
253
|
+
text: str = None
|
|
254
|
+
id: str = None
|
|
255
|
+
enabled: bool = None
|
|
256
|
+
selected: bool = None
|
|
257
|
+
index: int = None
|
|
258
|
+
point: str = None
|
|
259
|
+
optional: bool = False
|
|
260
|
+
relative: list = field(default_factory=list) # of (word, Sel)
|
|
261
|
+
|
|
262
|
+
def describe(self):
|
|
263
|
+
"""The selector in plain English, every value quoted."""
|
|
264
|
+
if self.point is not None:
|
|
265
|
+
return f"the screen point {self.point}"
|
|
266
|
+
parts = [] if self.text is not None or self.id is not None else ["the element"]
|
|
267
|
+
if self.text is not None:
|
|
268
|
+
parts.append(f"the element whose text matches the pattern {_quote(self.text)}" if _regex(self.text)
|
|
269
|
+
else _quote(self.text))
|
|
270
|
+
if self.id is not None:
|
|
271
|
+
kind = "an accessibility id that matches the pattern" if _regex(self.id) else "accessibility id"
|
|
272
|
+
parts.append(f"{'with' if parts else 'the element with'} {kind} {_quote(self.id)}")
|
|
273
|
+
out = " ".join(parts)
|
|
274
|
+
for state, name in ((self.enabled, "enabled"), (self.selected, "selected")):
|
|
275
|
+
if state is not None:
|
|
276
|
+
out += f" that is {'' if state else 'not '}{name}"
|
|
277
|
+
for word, other in self.relative:
|
|
278
|
+
out += f" {RELATIVE[word]} {other.describe()}"
|
|
279
|
+
if self.index is not None:
|
|
280
|
+
out += f" (match number {self.index + 1}, counting from the top left)"
|
|
281
|
+
return out
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _selector(value, where, command, ctx, env, *, tap=False, nested=False):
|
|
285
|
+
"""A Sel from a Maestro selector (a string, or an object), or None after naming the problem."""
|
|
286
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
287
|
+
value = str(value)
|
|
288
|
+
if isinstance(value, str):
|
|
289
|
+
value = {"text": value}
|
|
290
|
+
if not isinstance(value, dict):
|
|
291
|
+
ctx.problem(where, command, "has a selector Mobster can't read", "give it as text, id or point")
|
|
292
|
+
return None
|
|
293
|
+
sel, bad = Sel(), False
|
|
294
|
+
for key, item in value.items():
|
|
295
|
+
if key in TIMING:
|
|
296
|
+
continue
|
|
297
|
+
if key in ("text", "id"):
|
|
298
|
+
if _scalar(item) is None or not _scalar(item).strip():
|
|
299
|
+
ctx.problem(where, command, f"has an empty or non-text {key}", "give the text or id to match")
|
|
300
|
+
bad = True
|
|
301
|
+
else:
|
|
302
|
+
item = _substitute(_scalar(item), env, ctx, where, command)
|
|
303
|
+
if item is None:
|
|
304
|
+
bad = True
|
|
305
|
+
else:
|
|
306
|
+
setattr(sel, key, item)
|
|
307
|
+
elif key in ("enabled", "selected") and isinstance(item, bool):
|
|
308
|
+
setattr(sel, key, item)
|
|
309
|
+
elif key == "index" and isinstance(item, int) and not isinstance(item, bool) and item >= 0 and tap:
|
|
310
|
+
sel.index = item
|
|
311
|
+
elif key == "optional" and isinstance(item, bool):
|
|
312
|
+
sel.optional = item
|
|
313
|
+
elif key == "longPress" and tap and isinstance(item, bool):
|
|
314
|
+
continue
|
|
315
|
+
elif key == "repeat" and tap:
|
|
316
|
+
ctx.problem(where, command, "repeats the tap", "write the taps out as steps")
|
|
317
|
+
bad = True
|
|
318
|
+
elif key == "point" and tap and not nested:
|
|
319
|
+
point = _substitute(_scalar(item), env, ctx, where, command)
|
|
320
|
+
if point is None:
|
|
321
|
+
bad = True
|
|
322
|
+
elif point and (PERCENT_POINT.match(point) or ABS_POINT.match(point)):
|
|
323
|
+
sel.point = point.replace(" ", "")
|
|
324
|
+
else:
|
|
325
|
+
ctx.problem(where, command, f"has the point {str(item)[:30]!r}, which isn't x,y or x%,y%",
|
|
326
|
+
"write it as 50%,90%")
|
|
327
|
+
bad = True
|
|
328
|
+
elif key in RELATIVE and tap and not nested:
|
|
329
|
+
other = _selector(item, where, command, ctx, env, nested=True)
|
|
330
|
+
if other is None:
|
|
331
|
+
bad = True
|
|
332
|
+
elif other.relative or other.index is not None:
|
|
333
|
+
ctx.problem(where, command, f"nests {key} more than one level",
|
|
334
|
+
"describe the element by its own text or id")
|
|
335
|
+
bad = True
|
|
336
|
+
else:
|
|
337
|
+
sel.relative.append((key, other))
|
|
338
|
+
else:
|
|
339
|
+
ctx.problem(where, command, f"uses the selector option {key}, which Mobster can't match",
|
|
340
|
+
"select by text or id" if tap else "assert on text or id; Mobster can't judge this option",
|
|
341
|
+
assertion=not tap)
|
|
342
|
+
bad = True
|
|
343
|
+
if not bad and sel.point is None and sel.text is None and sel.id is None and not (tap and sel.relative):
|
|
344
|
+
ctx.problem(where, command, "has no text, id or point", "give the text or id to match", assertion=not tap)
|
|
345
|
+
bad = True
|
|
346
|
+
if not bad and sel.point is not None and (sel.text or sel.id or sel.relative):
|
|
347
|
+
ctx.problem(where, command, "mixes a point with a text or id", "use one or the other")
|
|
348
|
+
bad = True
|
|
349
|
+
return None if bad else sel
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _expect(sel, present, name, ctx, where, command):
|
|
353
|
+
"""The Mobster assertion for a Maestro assertVisible / assertNotVisible selector."""
|
|
354
|
+
if sel.index is not None or sel.point is not None or sel.relative or sel.optional:
|
|
355
|
+
what = "optional" if sel.optional else "a relative position, index or point"
|
|
356
|
+
ctx.problem(where, command, f"asserts with {what}, which an assertion on the accessibility tree can't carry",
|
|
357
|
+
"assert on the element's text or id", assertion=True)
|
|
358
|
+
return None
|
|
359
|
+
selector = {}
|
|
360
|
+
if sel.text is not None:
|
|
361
|
+
selector["label"] = _matcher(sel.text)
|
|
362
|
+
if sel.id is not None:
|
|
363
|
+
selector["id"] = _matcher(sel.id, fold=False)
|
|
364
|
+
if sel.enabled is not None:
|
|
365
|
+
selector["enabled"] = sel.enabled
|
|
366
|
+
if sel.selected is not None:
|
|
367
|
+
selector["selected"] = sel.selected
|
|
368
|
+
verb = "visible" if present else "not visible"
|
|
369
|
+
shown = sel.text if sel.text is not None else f"id {sel.id}"
|
|
370
|
+
label = f"{shown} is {verb}"
|
|
371
|
+
return {"name": label[:120], ("visible" if present else "absent"): selector}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _commands(commands, env, ctx, base, label, depth, chain):
|
|
375
|
+
if not isinstance(commands, list):
|
|
376
|
+
ctx.problem(label, "flow", "has no list of commands", "write the commands as a YAML list after `---`")
|
|
377
|
+
return
|
|
378
|
+
for number, command in enumerate(commands, 1):
|
|
379
|
+
name, arg = _split(command)
|
|
380
|
+
where = f"{label}command {number} ({name or '?'})"
|
|
381
|
+
if name is None:
|
|
382
|
+
ctx.problem(where, "?", "isn't a command Mobster can read", "write one command per list item")
|
|
383
|
+
continue
|
|
384
|
+
_command(name, arg, env, ctx, base, where, depth, chain)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _split(command):
|
|
388
|
+
if isinstance(command, str):
|
|
389
|
+
return command, None
|
|
390
|
+
if isinstance(command, dict) and len(command) == 1:
|
|
391
|
+
name, arg = next(iter(command.items()))
|
|
392
|
+
return (name, arg) if isinstance(name, str) else (None, None)
|
|
393
|
+
return None, None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _act(ctx, text):
|
|
397
|
+
ctx.ops.append(("act", text))
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _command(name, arg, env, ctx, base, where, depth, chain):
|
|
401
|
+
def sub(value):
|
|
402
|
+
return _substitute(value, env, ctx, where, name)
|
|
403
|
+
|
|
404
|
+
before = len(ctx.problems) + len(ctx.placeholders) + len(ctx.dropped)
|
|
405
|
+
try:
|
|
406
|
+
_dispatch(name, arg, env, ctx, base, where, depth, chain, sub)
|
|
407
|
+
except ValueError as error:
|
|
408
|
+
ctx.problem(where, name, f"has a value that {error}", "shorten it, or split the flow")
|
|
409
|
+
if name != "runFlow" and before == len(ctx.problems) + len(ctx.placeholders) + len(ctx.dropped):
|
|
410
|
+
ctx.converted += 1
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _dispatch(name, arg, env, ctx, base, where, depth, chain, sub):
|
|
414
|
+
if name == "launchApp":
|
|
415
|
+
return _launch(arg, env, ctx, where, sub)
|
|
416
|
+
if name in ("tapOn", "doubleTapOn", "longPressOn"):
|
|
417
|
+
sel = _selector(arg, where, name, ctx, env, tap=True)
|
|
418
|
+
if sel is None:
|
|
419
|
+
return
|
|
420
|
+
verb = {"tapOn": "Tap", "doubleTapOn": "Double-tap", "longPressOn": "Press and hold"}[name]
|
|
421
|
+
if isinstance(arg, dict) and (arg.get("longPress") is True):
|
|
422
|
+
verb = "Press and hold"
|
|
423
|
+
target = sel.describe()
|
|
424
|
+
if sel.optional:
|
|
425
|
+
return _act(ctx, f"If {target} is on screen, {verb[0].lower() + verb[1:]} it; otherwise skip this step")
|
|
426
|
+
if sel.point is not None and ABS_POINT.match(sel.point):
|
|
427
|
+
ctx.notes.append(f"{where}: the point {sel.point} is in screen points, so it depends on the device; "
|
|
428
|
+
"write it as 50%,90% to work on any size")
|
|
429
|
+
return _act(ctx, f"{verb} {target}")
|
|
430
|
+
if name in ("assertVisible", "assertNotVisible"):
|
|
431
|
+
sel = _selector(arg, where, name, ctx, env)
|
|
432
|
+
if sel is not None:
|
|
433
|
+
found = _expect(sel, name == "assertVisible", name, ctx, where, name)
|
|
434
|
+
if found is not None:
|
|
435
|
+
ctx.ops.append(("assert", found))
|
|
436
|
+
return
|
|
437
|
+
if name == "extendedWaitUntil":
|
|
438
|
+
return _wait_until(arg, env, ctx, where)
|
|
439
|
+
if name == "inputText":
|
|
440
|
+
if not _scalar(arg):
|
|
441
|
+
return ctx.problem(where, name, "has no text to type", "give the text as a string")
|
|
442
|
+
text = sub(_scalar(arg))
|
|
443
|
+
if text is None:
|
|
444
|
+
return
|
|
445
|
+
return _act(ctx, f"Type {_quote(text, INPUT_LIMIT)} into the focused text field")
|
|
446
|
+
if name == "eraseText":
|
|
447
|
+
count = arg.get("charactersToErase") if isinstance(arg, dict) else arg
|
|
448
|
+
if count is None:
|
|
449
|
+
return _act(ctx, "Erase the text in the focused text field")
|
|
450
|
+
if isinstance(count, int) and not isinstance(count, bool) and 0 < count <= 1000:
|
|
451
|
+
return _act(ctx, f"Delete {count} characters from the focused text field")
|
|
452
|
+
return ctx.problem(where, name, "has a character count Mobster can't read", "give a whole number")
|
|
453
|
+
if name == "hideKeyboard":
|
|
454
|
+
return _act(ctx, "Dismiss the keyboard")
|
|
455
|
+
if name == "scroll":
|
|
456
|
+
return _act(ctx, "Scroll down")
|
|
457
|
+
if name == "scrollUntilVisible":
|
|
458
|
+
return _scroll_until(arg, env, ctx, where)
|
|
459
|
+
if name == "swipe":
|
|
460
|
+
return _swipe(arg, env, ctx, where)
|
|
461
|
+
if name == "back":
|
|
462
|
+
ctx.notes.append(f"{where}: iOS has no back key, so the step taps the Back button on screen or swipes from "
|
|
463
|
+
"the left edge")
|
|
464
|
+
return _act(ctx, "Go back one screen with the Back button on screen, or by swiping from the left edge")
|
|
465
|
+
if name == "pressKey":
|
|
466
|
+
key = str(arg).strip().lower() if _scalar(arg) else ""
|
|
467
|
+
if key in KEYS:
|
|
468
|
+
return _act(ctx, KEYS[key])
|
|
469
|
+
return ctx.problem(where, name, f"presses {str(arg)[:30]!r}, a key iOS doesn't have",
|
|
470
|
+
"use Home, Enter or Backspace")
|
|
471
|
+
if name == "openLink":
|
|
472
|
+
link = sub(_scalar(arg.get("link")) if isinstance(arg, dict) else _scalar(arg))
|
|
473
|
+
if link is None and "${" in str(arg):
|
|
474
|
+
return
|
|
475
|
+
if not link or not re.match(r"[A-Za-z][A-Za-z0-9+.\-]*:", link) or len(link) > LIMITS["url"]:
|
|
476
|
+
return ctx.problem(where, name, "has a link Mobster can't read",
|
|
477
|
+
"write the full link, such as yourapp://home")
|
|
478
|
+
if not ctx.started() and "url" not in ctx.launch:
|
|
479
|
+
ctx.launch["url"] = link
|
|
480
|
+
return
|
|
481
|
+
return _act(ctx, f"Open the link {_quote(link, LIMITS['url'])}")
|
|
482
|
+
if name == "waitForAnimationToEnd":
|
|
483
|
+
return _act(ctx, "Wait for animations on screen to finish")
|
|
484
|
+
if name == "runFlow":
|
|
485
|
+
return _run_flow(arg, env, ctx, base, where, depth, chain)
|
|
486
|
+
if name in UNSUPPORTED:
|
|
487
|
+
why, instead = UNSUPPORTED[name]
|
|
488
|
+
return ctx.unsupported(where, name, why, instead, assertion=name in ASSERTIONS)
|
|
489
|
+
ctx.unsupported(where, name, "isn't a Maestro command Mobster knows",
|
|
490
|
+
"check the spelling, or run this step by hand", assertion=name.startswith("assert"))
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _launch(arg, env, ctx, where, sub):
|
|
494
|
+
options = {} if arg is None else arg
|
|
495
|
+
if not isinstance(options, dict):
|
|
496
|
+
return ctx.problem(where, "launchApp", "has options Mobster can't read", "use clearState or nothing")
|
|
497
|
+
initial = not ctx.started() and not ctx.launch.get("seen")
|
|
498
|
+
app = options.get("appId")
|
|
499
|
+
resolved = sub(_scalar(app)) if app is not None else None
|
|
500
|
+
if app is not None and resolved is None:
|
|
501
|
+
return
|
|
502
|
+
if app is not None and resolved != ctx.launch.get("appId"):
|
|
503
|
+
return ctx.problem(where, "launchApp", "launches a different app", "write one flow per app")
|
|
504
|
+
for key in options:
|
|
505
|
+
if key in ("appId", "stopApp", "clearState"):
|
|
506
|
+
continue
|
|
507
|
+
why = {"arguments": "passes launch arguments, whose iOS form Mobster can't confirm",
|
|
508
|
+
"permissions": "sets permissions, which isn't a check step"}.get(key, f"uses the option {key}")
|
|
509
|
+
ctx.problem(where, "launchApp", why, "set it before the run, or in the check's launch.args by hand")
|
|
510
|
+
clear = options.get("clearState") is True
|
|
511
|
+
if initial:
|
|
512
|
+
ctx.launch["seen"] = True
|
|
513
|
+
ctx.launch["reset"] = "data" if clear else "none"
|
|
514
|
+
return
|
|
515
|
+
if clear:
|
|
516
|
+
return ctx.problem(where, "launchApp", "clears app data in the middle of the flow",
|
|
517
|
+
"put clearState on the first launchApp, or split the flow")
|
|
518
|
+
_act(ctx, "Bring the app to the front" if options.get("stopApp") is False
|
|
519
|
+
else "Force-quit the app, then open it again")
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _wait_until(arg, env, ctx, where):
|
|
523
|
+
if not isinstance(arg, dict) or ("visible" in arg) == ("notVisible" in arg):
|
|
524
|
+
return ctx.problem(where, "extendedWaitUntil", "doesn't have exactly one of visible and notVisible",
|
|
525
|
+
"give one", assertion=True)
|
|
526
|
+
present = "visible" in arg
|
|
527
|
+
sel = _selector(arg["visible" if present else "notVisible"], where, "extendedWaitUntil", ctx, env)
|
|
528
|
+
if sel is None or arg.get("optional") is True:
|
|
529
|
+
if sel is not None:
|
|
530
|
+
ctx.problem(where, "extendedWaitUntil", "is optional, so it asserts nothing", "remove optional",
|
|
531
|
+
assertion=True)
|
|
532
|
+
return
|
|
533
|
+
found = _expect(sel, present, "extendedWaitUntil", ctx, where, "extendedWaitUntil")
|
|
534
|
+
if found is None:
|
|
535
|
+
return
|
|
536
|
+
timeout = arg.get("timeout")
|
|
537
|
+
if isinstance(timeout, (int, float)) and not isinstance(timeout, bool) and timeout > 5000:
|
|
538
|
+
ctx.notes.append(f"{where}: Maestro waits up to {timeout / 1000:g} s; Mobster waits --assert-timeout "
|
|
539
|
+
"(default 5) for the screen to settle, then judges")
|
|
540
|
+
ctx.ops.append(("assert", found))
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _scroll_until(arg, env, ctx, where):
|
|
544
|
+
if not isinstance(arg, dict) or "element" not in arg:
|
|
545
|
+
return ctx.problem(where, "scrollUntilVisible", "has no element to scroll to", "give element: with text or id")
|
|
546
|
+
sel = _selector(arg["element"], where, "scrollUntilVisible", ctx, env, tap=True)
|
|
547
|
+
direction = str(arg.get("direction", "DOWN")).upper()
|
|
548
|
+
if direction not in DIRECTIONS:
|
|
549
|
+
return ctx.problem(where, "scrollUntilVisible", f"scrolls {direction[:20]!r}, not UP, DOWN, LEFT or RIGHT",
|
|
550
|
+
"fix the direction")
|
|
551
|
+
if sel is not None:
|
|
552
|
+
tail = "; continue if it never appears" if arg.get("optional") is True else ""
|
|
553
|
+
_act(ctx, f"Scroll {direction.lower()} until {sel.describe()} is visible{tail}")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _swipe(arg, env, ctx, where):
|
|
557
|
+
arg = arg if isinstance(arg, dict) else {}
|
|
558
|
+
if "direction" in arg and "from" not in arg:
|
|
559
|
+
direction = str(arg["direction"]).upper()
|
|
560
|
+
if direction in DIRECTIONS:
|
|
561
|
+
return _act(ctx, f"Swipe {direction.lower()} across the screen")
|
|
562
|
+
elif "direction" in arg and "from" in arg:
|
|
563
|
+
sel = _selector(arg["from"], where, "swipe", ctx, env, tap=True)
|
|
564
|
+
direction = str(arg["direction"]).upper()
|
|
565
|
+
if direction in DIRECTIONS and sel is not None:
|
|
566
|
+
return _act(ctx, f"Swipe {direction.lower()} starting on {sel.describe()}")
|
|
567
|
+
if sel is not None:
|
|
568
|
+
return ctx.problem(where, "swipe", "has a direction Mobster can't read", "use UP, DOWN, LEFT or RIGHT")
|
|
569
|
+
return
|
|
570
|
+
elif "start" in arg and "end" in arg:
|
|
571
|
+
points = []
|
|
572
|
+
for key in ("start", "end"):
|
|
573
|
+
text = _scalar(arg[key])
|
|
574
|
+
if text and (PERCENT_POINT.match(text) or ABS_POINT.match(text)):
|
|
575
|
+
points.append(text.replace(" ", ""))
|
|
576
|
+
if len(points) == 2:
|
|
577
|
+
return _act(ctx, f"Swipe from the screen point {points[0]} to {points[1]}")
|
|
578
|
+
ctx.problem(where, "swipe", "isn't a swipe Mobster can read",
|
|
579
|
+
"use direction, or start and end as 50%,80%")
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _run_flow(arg, env, ctx, base, where, depth, chain):
|
|
583
|
+
if isinstance(arg, str):
|
|
584
|
+
arg = {"file": arg}
|
|
585
|
+
if not isinstance(arg, dict):
|
|
586
|
+
return ctx.problem(where, "runFlow", "has options Mobster can't read", "give a file or commands")
|
|
587
|
+
if "when" in arg:
|
|
588
|
+
return ctx.problem(where, "runFlow", "runs only when a condition holds, and a check is a fixed list of steps",
|
|
589
|
+
"write the branch you want as its own flow")
|
|
590
|
+
inner = dict(env)
|
|
591
|
+
given = arg.get("env") or {}
|
|
592
|
+
if not isinstance(given, dict):
|
|
593
|
+
return ctx.problem(where, "runFlow", "has an env Mobster can't read", "write env as KEY: value")
|
|
594
|
+
for key, value in given.items():
|
|
595
|
+
inner[str(key)] = _substitute(str(value), env, ctx, where, "runFlow") or ""
|
|
596
|
+
if depth >= MAX_DEPTH:
|
|
597
|
+
return ctx.problem(where, "runFlow", f"nests more than {MAX_DEPTH} flows deep", "flatten the flows")
|
|
598
|
+
if "commands" in arg:
|
|
599
|
+
return _commands(arg["commands"], inner, ctx, base, f"{where} > ", depth + 1, chain)
|
|
600
|
+
target = _scalar(arg.get("file"))
|
|
601
|
+
if not target:
|
|
602
|
+
return ctx.problem(where, "runFlow", "has neither a file nor commands", "give one")
|
|
603
|
+
target = _substitute(target, env, ctx, where, "runFlow")
|
|
604
|
+
if target is None:
|
|
605
|
+
return
|
|
606
|
+
path = (base / target).resolve()
|
|
607
|
+
try:
|
|
608
|
+
path.relative_to(ctx.root)
|
|
609
|
+
except ValueError:
|
|
610
|
+
return ctx.problem(where, "runFlow", f"reads {target[:60]}, outside the folder being imported",
|
|
611
|
+
"keep subflows inside the folder you import")
|
|
612
|
+
if path in chain:
|
|
613
|
+
return ctx.problem(where, "runFlow", "runs a flow that runs it again", "remove the loop")
|
|
614
|
+
try:
|
|
615
|
+
config, commands = read_flow(path)
|
|
616
|
+
except MaestroError as error:
|
|
617
|
+
return ctx.problem(where, "runFlow", f"{target[:60]} {error}", "fix the file or the path")
|
|
618
|
+
for key, value in (config.get("env") or {}).items() if isinstance(config.get("env"), dict) else ():
|
|
619
|
+
if str(key) in inner and inner[str(key)] != str(value):
|
|
620
|
+
return ctx.problem(where, "runFlow", f"{target[:60]} sets {key} to a different value than the flow that "
|
|
621
|
+
"runs it, and Mobster can't tell which wins", "set it in one place")
|
|
622
|
+
inner[str(key)] = str(value)
|
|
623
|
+
app = config.get("appId")
|
|
624
|
+
if app is not None and _substitute(str(app), inner, ctx, where, "runFlow") not in (None, ctx.launch.get("appId")):
|
|
625
|
+
return ctx.problem(where, "runFlow", f"{target[:60]} is for a different app", "write one flow per app")
|
|
626
|
+
_commands(commands, inner, ctx, path.parent, f"{path.relative_to(ctx.root)} ", depth + 1, chain | {path})
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _pack(steps):
|
|
630
|
+
"""At most LIMITS['steps'] steps: join neighbours with ", then" when there are more."""
|
|
631
|
+
cap, limit = LIMITS["steps"], LIMITS["step"]
|
|
632
|
+
if len(steps) <= cap:
|
|
633
|
+
return steps
|
|
634
|
+
size = -(-len(steps) // cap)
|
|
635
|
+
out = [", then ".join(s[0].lower() + s[1:] if i else s for i, s in enumerate(steps[i:i + size]))
|
|
636
|
+
for i in range(0, len(steps), size)]
|
|
637
|
+
if any(len(step) > limit for step in out):
|
|
638
|
+
raise ValueError("long")
|
|
639
|
+
return out
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _segments(ops):
|
|
643
|
+
"""Cumulative segments: [(steps so far, assertions of this group)], one per assertion group that more actions
|
|
644
|
+
follow, then the last."""
|
|
645
|
+
steps, group, out = [], [], []
|
|
646
|
+
for op in ops:
|
|
647
|
+
if op[0] == "act":
|
|
648
|
+
if group:
|
|
649
|
+
out.append((list(steps), group))
|
|
650
|
+
group = []
|
|
651
|
+
steps.append(op[1])
|
|
652
|
+
elif op[0] == "assert":
|
|
653
|
+
group.append(op[1])
|
|
654
|
+
trailing = 0
|
|
655
|
+
if group or not out:
|
|
656
|
+
out.append((list(steps), group))
|
|
657
|
+
else:
|
|
658
|
+
trailing = len(steps) - len(out[-1][0])
|
|
659
|
+
return out, trailing
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def convert_flow(file, root, *, bundle=None, app_path=None, skip_unsupported=False, stem=None, shown=None):
|
|
663
|
+
"""The FlowResult for one Maestro flow file."""
|
|
664
|
+
file, root = Path(file).resolve(), Path(root).resolve()
|
|
665
|
+
result = FlowResult(shown or str(file.relative_to(root)))
|
|
666
|
+
stem = stem or file.stem
|
|
667
|
+
try:
|
|
668
|
+
config, commands = read_flow(file)
|
|
669
|
+
except MaestroError as error:
|
|
670
|
+
result.outcome, result.reason = "refused", f"{file.name} {error}"
|
|
671
|
+
return result
|
|
672
|
+
ctx = _Ctx(root, skip_unsupported)
|
|
673
|
+
unknown = [key for key in config if key not in HEADER_KEYS]
|
|
674
|
+
for key in unknown:
|
|
675
|
+
ctx.problem("config", str(key), "isn't a flow setting Mobster reads",
|
|
676
|
+
"remove it, or it belongs to a workspace config, not a flow")
|
|
677
|
+
env = {}
|
|
678
|
+
raw_env = config.get("env") or {}
|
|
679
|
+
if isinstance(raw_env, dict):
|
|
680
|
+
env = {str(key): str(value) for key, value in raw_env.items()}
|
|
681
|
+
for hook in ("onFlowStart", "onFlowComplete"):
|
|
682
|
+
if hook in config:
|
|
683
|
+
ctx.problem("config", hook, "runs commands before or after the flow",
|
|
684
|
+
"put those commands at the start or end of the flow")
|
|
685
|
+
app_id = _substitute(_scalar(config.get("appId")), env, ctx, "config", "appId") if config.get("appId") else None
|
|
686
|
+
app_id = bundle or app_id
|
|
687
|
+
if not app_id and ctx.problems:
|
|
688
|
+
result.outcome, result.problems = "refused", ctx.problems
|
|
689
|
+
return result
|
|
690
|
+
if not app_id:
|
|
691
|
+
result.outcome = "skipped"
|
|
692
|
+
result.reason = "no appId, so it is a subflow or can't name its app; pass --bundle to import it as a flow"
|
|
693
|
+
return result
|
|
694
|
+
ctx.launch["appId"] = app_id
|
|
695
|
+
if not commands:
|
|
696
|
+
result.outcome, result.reason = "refused", "has no commands"
|
|
697
|
+
return result
|
|
698
|
+
_commands(commands, env, ctx, file.parent, "", 0, frozenset({file}))
|
|
699
|
+
result.notes, result.dropped = ctx.notes, ctx.dropped
|
|
700
|
+
result.placeholders, result.converted = ctx.placeholders, ctx.converted
|
|
701
|
+
if ctx.placeholders:
|
|
702
|
+
result.notes.append(f"{len(ctx.placeholders)} command{'' if len(ctx.placeholders) == 1 else 's'} became a "
|
|
703
|
+
"step marked TODO: the first run (Smart) fills it, then replay keeps what it did; read "
|
|
704
|
+
"that run's report before you trust the check")
|
|
705
|
+
if ctx.problems:
|
|
706
|
+
result.outcome, result.problems = "refused", ctx.problems
|
|
707
|
+
return result
|
|
708
|
+
if not any(op[0] == "assert" for op in ctx.ops):
|
|
709
|
+
result.notes.append("the flow asserts nothing, so the check ends needs_review; add an assertVisible")
|
|
710
|
+
name = _text_name(config.get("name"), stem)
|
|
711
|
+
segments, trailing = _segments(ctx.ops)
|
|
712
|
+
if trailing:
|
|
713
|
+
result.notes.append(f"the last {trailing} step{'' if trailing == 1 else 's'} come after the final "
|
|
714
|
+
"assertion, so no check runs them; nothing judges them")
|
|
715
|
+
tags = _tags(config.get("tags"), result)
|
|
716
|
+
for index, (steps, asserts) in enumerate(segments, 1):
|
|
717
|
+
try:
|
|
718
|
+
steps = _pack(steps)
|
|
719
|
+
except ValueError:
|
|
720
|
+
result.outcome = "refused"
|
|
721
|
+
result.problems.append(Problem("flow", "steps", f"has more than {LIMITS['steps']} steps that can't be "
|
|
722
|
+
"joined into one check", "split the flow with runFlow into shorter flows"))
|
|
723
|
+
return result
|
|
724
|
+
data = {"version": 1, "name": name if len(segments) == 1 else f"{name} (part {index} of {len(segments)})",
|
|
725
|
+
"app": {"bundle": app_id}}
|
|
726
|
+
data["reset"] = ctx.launch.get("reset", "none")
|
|
727
|
+
data["tags"] = tags
|
|
728
|
+
if ctx.launch.get("url"):
|
|
729
|
+
data["launch"] = {"url": ctx.launch["url"]}
|
|
730
|
+
if steps:
|
|
731
|
+
data["steps"] = steps
|
|
732
|
+
data["expect"] = asserts
|
|
733
|
+
try:
|
|
734
|
+
canonical = check_to_dict(check_from_dict(data, base_dir=root))
|
|
735
|
+
except CheckError as error:
|
|
736
|
+
result.outcome = "refused"
|
|
737
|
+
result.problems.append(Problem("flow", "check", f"makes a check Mobster rejects: {error}",
|
|
738
|
+
"fix the flow, or write the check by hand"))
|
|
739
|
+
return result
|
|
740
|
+
if app_path:
|
|
741
|
+
canonical["app"]["path"] = str(app_path)
|
|
742
|
+
result.checks.append((stem if len(segments) == 1 else f"{stem}-part-{index}", canonical))
|
|
743
|
+
if len(segments) > 1:
|
|
744
|
+
result.notes.append(f"assertions in the middle of the flow became {len(segments)} checks; each one replays "
|
|
745
|
+
"the steps up to its assertions")
|
|
746
|
+
return result
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _text_name(value, stem):
|
|
750
|
+
text = value.strip() if isinstance(value, str) and value.strip() else stem.replace("_", " ").replace("-", " ")
|
|
751
|
+
return text[:LIMITS["name"]]
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _tags(value, result):
|
|
755
|
+
out = ["maestro"]
|
|
756
|
+
for tag in value if isinstance(value, list) else ():
|
|
757
|
+
word = re.sub(r"[^a-z0-9_-]+", "-", str(tag).lower()).strip("-")[:40]
|
|
758
|
+
if word and re.fullmatch(r"[a-z0-9][a-z0-9_-]{0,39}", word):
|
|
759
|
+
if word not in out and len(out) < LIMITS["tags"]:
|
|
760
|
+
out.append(word)
|
|
761
|
+
else:
|
|
762
|
+
result.notes.append(f"the tag {str(tag)[:30]!r} isn't a Mobster tag (a-z, 0-9, - and _), "
|
|
763
|
+
"so it was left out")
|
|
764
|
+
return out
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def convert_tree(paths, *, bundle=None, app_path=None, skip_unsupported=False):
|
|
768
|
+
"""[FlowResult] for every flow under ``paths``."""
|
|
769
|
+
files = discover(paths)
|
|
770
|
+
if not files:
|
|
771
|
+
raise MaestroError("no .yaml or .yml files found")
|
|
772
|
+
shown_names = _unique_stems(files)
|
|
773
|
+
results = []
|
|
774
|
+
for (file, root), (stem, shown) in zip(files, shown_names):
|
|
775
|
+
if file.name.lower() in ("config.yaml", "config.yml"):
|
|
776
|
+
item = FlowResult(shown, "skipped", reason="a workspace config, not a flow; its includeTags, excludeTags "
|
|
777
|
+
"and executionOrder are not applied")
|
|
778
|
+
else:
|
|
779
|
+
item = convert_flow(file, root, bundle=bundle, app_path=app_path, skip_unsupported=skip_unsupported,
|
|
780
|
+
stem=stem, shown=shown)
|
|
781
|
+
results.append(item)
|
|
782
|
+
return results
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _unique_stems(files):
|
|
786
|
+
"""(stem, shown path) per file: the file name, or the folder path joined with - when two share a name."""
|
|
787
|
+
out, seen = [], {}
|
|
788
|
+
for file, root in files:
|
|
789
|
+
rel = file.relative_to(root)
|
|
790
|
+
stem = re.sub(r"[^A-Za-z0-9_.-]+", "-", rel.with_suffix("").as_posix().replace("/", "-")).strip("-.") or "flow"
|
|
791
|
+
count = seen.get(stem.lower(), 0)
|
|
792
|
+
seen[stem.lower()] = count + 1
|
|
793
|
+
out.append((stem if not count else f"{stem}-{count + 1}", rel.as_posix()))
|
|
794
|
+
return out
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def render(check, source):
|
|
798
|
+
"""A check dict as YAML text with a header that names the Maestro flow it came from."""
|
|
799
|
+
import yaml
|
|
800
|
+
header = (f"# Imported from the Maestro flow {source}.\n"
|
|
801
|
+
"# Run it with: mobster test <this file>\n")
|
|
802
|
+
return header + yaml.safe_dump(check, sort_keys=False, allow_unicode=True, default_flow_style=False, width=100)
|