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,378 @@
|
|
|
1
|
+
"""`mobster test --xctest`: the project's own XCTest and Swift Testing targets, run beside the checks and read from the
|
|
2
|
+
.xcresult into the suite's one results model (doc["xctest"]).
|
|
3
|
+
|
|
4
|
+
This is not a general XCTest runner: one simulator, no retries of a test, no device runs, and the project's schemes
|
|
5
|
+
and test plans are read, never changed."""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import plistlib
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..sim.models import SimError
|
|
13
|
+
from .prbuild import BuildError, find_project, pick_scheme, project_arg, run_command
|
|
14
|
+
from .prbuild import build as build_app
|
|
15
|
+
|
|
16
|
+
BUNDLE_DIR = "xctest"
|
|
17
|
+
BUNDLE_NAME = "tests.xcresult"
|
|
18
|
+
ATTACHMENTS = "attachments"
|
|
19
|
+
RUN_LOG = "xctest.log"
|
|
20
|
+
RUN_TIMEOUT = 30 * 60
|
|
21
|
+
READ_TIMEOUT = 120
|
|
22
|
+
FAILURE_EXIT = 65 # xcodebuild's code for "the tests ran and some failed"
|
|
23
|
+
MAX_FAILURES = 5 # failure messages kept per test
|
|
24
|
+
MAX_MESSAGE = 600
|
|
25
|
+
ONLY_TESTING = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_ .+()-]*(/[A-Za-z0-9_ .+()-]+){0,2}$")
|
|
26
|
+
LOCATION = re.compile(r"^(?P<file>[^\n:]+\.(?:swift|m|mm|h|c|cpp)):(?P<line>\d+):?\s*(?P<text>.*)$", re.S)
|
|
27
|
+
EXPORT_SUFFIX = re.compile(r"_\d+_[0-9A-Fa-f-]{36}(?=\.[^.]*$|$)") # xcresulttool appends _<n>_<UUID> to a name
|
|
28
|
+
RESULTS = {"passed": "passed", "failed": "failed", "skipped": "skipped", "expected failure": "passed"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class XctestError(Exception):
|
|
32
|
+
"""The XCTest half of the run couldn't be done: what happened, and what to do (the checks still ran)."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message, fix=""):
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.message, self.fix = message, fix
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def only_testing_ok(items):
|
|
40
|
+
"""``--only-testing`` values: Target, Target/Class or Target/Class/test. XCTestError otherwise."""
|
|
41
|
+
for item in items:
|
|
42
|
+
if not ONLY_TESTING.match(item):
|
|
43
|
+
raise XctestError(f"--only-testing {item!r} isn't a test identifier.",
|
|
44
|
+
"Write Target, Target/Class or Target/Class/testName.")
|
|
45
|
+
return tuple(items)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def find_xctestrun(derived):
|
|
49
|
+
"""The .xctestrun file build-for-testing wrote under ``derived``, or None."""
|
|
50
|
+
found = sorted((Path(derived) / "Build" / "Products").glob("*.xctestrun"))
|
|
51
|
+
return found[-1] if found else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def targets_of(xctestrun):
|
|
55
|
+
"""The test targets an .xctestrun names, in the order it lists them."""
|
|
56
|
+
try:
|
|
57
|
+
with open(xctestrun, "rb") as handle:
|
|
58
|
+
data = plistlib.load(handle)
|
|
59
|
+
except (OSError, plistlib.InvalidFileException, ValueError):
|
|
60
|
+
return []
|
|
61
|
+
blocks = []
|
|
62
|
+
for config in data.get("TestConfigurations") or ():
|
|
63
|
+
blocks += [block for block in config.get("TestTargets") or () if isinstance(block, dict)]
|
|
64
|
+
blocks += [block for key, block in data.items() if key != "__xctestrun_metadata__" and isinstance(block, dict)
|
|
65
|
+
and "TestBundlePath" in block]
|
|
66
|
+
names = [str(block.get("BlueprintName") or block.get("ProductModuleName") or "") for block in blocks]
|
|
67
|
+
return list(dict.fromkeys(name for name in names if name))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def select_targets(targets, changed, only=()):
|
|
71
|
+
"""(targets to run, {target: why}). ``only`` (--only-testing) wins; else the targets whose folder holds a changed
|
|
72
|
+
file; else every target, because a change that touches no test file can still break one."""
|
|
73
|
+
if only:
|
|
74
|
+
named = {item.split("/", 1)[0] for item in only}
|
|
75
|
+
return [name for name in targets if name in named], {name: "named with --only-testing" for name in named
|
|
76
|
+
if name in targets}
|
|
77
|
+
if changed is None:
|
|
78
|
+
return list(targets), {}
|
|
79
|
+
hit = {}
|
|
80
|
+
for target in targets:
|
|
81
|
+
files = [path for path in changed if target in Path(path).parts]
|
|
82
|
+
if files:
|
|
83
|
+
hit[target] = f"{files[0]} changed" + (f" and {len(files) - 1} more" if len(files) > 1 else "")
|
|
84
|
+
if hit:
|
|
85
|
+
return [name for name in targets if name in hit], hit
|
|
86
|
+
return list(targets), {name: "no test file changed, so every target runs" for name in targets}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build(root, *, scheme, project, test_plan=None, runner=None, progress=None, bundle=None, configuration="Debug"):
|
|
90
|
+
"""build-for-testing into .mobster/build: the app the checks drive, the test bundles and the .xctestrun. The
|
|
91
|
+
result is prbuild.build's, plus "xctestrun" and "targets". BuildError."""
|
|
92
|
+
built = build_app(root, scheme=scheme, project=project, platform="sim", configuration=configuration,
|
|
93
|
+
runner=runner, progress=progress, bundle=bundle, for_testing=True, test_plan=test_plan)
|
|
94
|
+
derived = Path(root) / ".mobster" / "build"
|
|
95
|
+
run_file = find_xctestrun(derived)
|
|
96
|
+
if run_file is None:
|
|
97
|
+
raise BuildError(f"{scheme} built, but it has no test target to run.",
|
|
98
|
+
"Add a unit test target to the scheme's Test action, or leave --xctest out.")
|
|
99
|
+
return {**built, "scheme": scheme, "xctestrun": str(run_file), "targets": targets_of(run_file)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def prepare(planned, progress, *, runner=None):
|
|
103
|
+
"""The build for a run that didn't build for testing first (`mobster test --xctest`): the project's own scheme.
|
|
104
|
+
BuildError."""
|
|
105
|
+
options, root = planned.options, planned.root
|
|
106
|
+
found = project_arg(Path(root) / options.xctest_project) if options.xctest_project else find_project(root)
|
|
107
|
+
if found is None:
|
|
108
|
+
raise BuildError(f"{root} has no Xcode project to build the tests from.",
|
|
109
|
+
"Pass --project YourApp.xcodeproj, or leave --xctest out.")
|
|
110
|
+
scheme = options.xctest_scheme or pick_scheme(found, runner=runner, cwd=root)
|
|
111
|
+
return build(root, scheme=scheme, project=found, test_plan=options.xctest_plan, runner=runner, progress=progress)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_argv(xctestrun, udid, bundle_path, only):
|
|
115
|
+
argv = ["xcodebuild", "test-without-building", "-xctestrun", str(xctestrun), "-destination", f"id={udid}",
|
|
116
|
+
"-resultBundlePath", str(bundle_path)]
|
|
117
|
+
for item in only:
|
|
118
|
+
argv.append(f"-only-testing:{item}")
|
|
119
|
+
return argv
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def run_tests(xctestrun, udid, folder, only, *, runner=None, progress=None):
|
|
123
|
+
"""Run the built tests on simulator ``udid``; the result bundle goes under ``folder``/xctest/. The bundle's path,
|
|
124
|
+
or XctestError when xcodebuild stopped before running tests."""
|
|
125
|
+
progress = progress or (lambda line: None)
|
|
126
|
+
bundle = Path(folder) / BUNDLE_DIR / BUNDLE_NAME
|
|
127
|
+
bundle.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
progress("Running your XCTest targets")
|
|
129
|
+
code, output = (runner or run_command)(run_argv(xctestrun, udid, bundle, only), Path(folder),
|
|
130
|
+
bundle.parent / RUN_LOG, RUN_TIMEOUT)
|
|
131
|
+
if code not in (0, FAILURE_EXIT) or not (bundle / "Info.plist").is_file():
|
|
132
|
+
errors = [line.strip() for line in output.splitlines() if "error:" in line or "Testing failed" in line][:3]
|
|
133
|
+
detail = "; ".join(errors)[:500] if errors else f"xcodebuild exited {code}"
|
|
134
|
+
raise XctestError(f"The tests didn't run: {detail}.", f"Read {bundle.parent / RUN_LOG}, then run it again.")
|
|
135
|
+
return bundle
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _json(argv, runner):
|
|
139
|
+
code, output = (runner or run_command)(argv, Path.cwd(), None, READ_TIMEOUT)
|
|
140
|
+
start = output.find("{")
|
|
141
|
+
try:
|
|
142
|
+
data = json.JSONDecoder().raw_decode(output[start:])[0] if code == 0 and start >= 0 else None
|
|
143
|
+
except ValueError:
|
|
144
|
+
data = None
|
|
145
|
+
if not isinstance(data, dict):
|
|
146
|
+
raise XctestError("Mobster couldn't read the test results.", "Run `xcrun xcresulttool get test-results "
|
|
147
|
+
"summary --path <the .xcresult>` to see why; it needs Xcode 16 or later.")
|
|
148
|
+
return data
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def read_bundle(bundle, *, runner=None):
|
|
152
|
+
"""(tests, summary): xcresulttool's JSON for ``bundle``. XctestError when it can't be read."""
|
|
153
|
+
base = ["xcrun", "xcresulttool", "get", "test-results"]
|
|
154
|
+
tail = ["--path", str(bundle), "--format", "json"]
|
|
155
|
+
return _json(base + ["tests"] + tail, runner), _json(base + ["summary"] + tail, runner)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _seconds(node):
|
|
159
|
+
value = node.get("durationInSeconds")
|
|
160
|
+
if isinstance(value, (int, float)):
|
|
161
|
+
return value
|
|
162
|
+
match = re.match(r"^\s*([0-9.]+)\s*(ms|s|m)\b", str(node.get("duration") or ""))
|
|
163
|
+
if not match:
|
|
164
|
+
return 0
|
|
165
|
+
return float(match.group(1)) * {"ms": 0.001, "s": 1, "m": 60}[match.group(2)]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _failures(node):
|
|
169
|
+
"""The case's failure messages, each as {"message", "file", "line"}."""
|
|
170
|
+
out = []
|
|
171
|
+
for child in node.get("children") or ():
|
|
172
|
+
if child.get("nodeType") == "Failure Message":
|
|
173
|
+
text = str(child.get("name") or "").strip()
|
|
174
|
+
found = LOCATION.match(text)
|
|
175
|
+
entry = {"message": (found.group("text") if found else text)[:MAX_MESSAGE], "file": None, "line": None}
|
|
176
|
+
if found:
|
|
177
|
+
entry["file"], entry["line"] = Path(found.group("file")).name, int(found.group("line"))
|
|
178
|
+
out.append(entry)
|
|
179
|
+
elif isinstance(child.get("children"), list):
|
|
180
|
+
out += _failures(child)
|
|
181
|
+
return out[:MAX_FAILURES]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _cases(nodes, bundle=None, suites=()):
|
|
185
|
+
"""Every Test Case under ``nodes`` as (bundle, suite names, node)."""
|
|
186
|
+
for node in nodes:
|
|
187
|
+
kind = str(node.get("nodeType") or "")
|
|
188
|
+
if kind == "Test Case":
|
|
189
|
+
yield bundle, suites, node
|
|
190
|
+
elif isinstance(node.get("children"), list):
|
|
191
|
+
name = str(node.get("name") or "")
|
|
192
|
+
yield from _cases(node["children"], name if "test bundle" in kind.lower() else bundle,
|
|
193
|
+
suites + (name,) if kind == "Test Suite" else suites)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def parse(tests, summary=None, device=None):
|
|
197
|
+
"""The targets of an xcresulttool ``tests`` document as the suite's results model:
|
|
198
|
+
[{"name", "status", "tests", "passed", "failed", "skipped", "durationMs", "cases": [...]}]. A case has name,
|
|
199
|
+
classname, status (passed, failed or skipped), durationMs, failures [{message, file, line}] and attachments []."""
|
|
200
|
+
targets = {}
|
|
201
|
+
where = {} if device is None else {"device": device["name"], "deviceKey": device["key"]}
|
|
202
|
+
for bundle, suites, node in _cases(tests.get("testNodes") or ()):
|
|
203
|
+
target = targets.setdefault(bundle or "Tests", {"name": bundle or "Tests", "cases": []})
|
|
204
|
+
status = RESULTS.get(str(node.get("result") or "").lower(), "failed")
|
|
205
|
+
name = re.sub(r"\(\)$", "", str(node.get("name") or ""))
|
|
206
|
+
failures = _failures(node)
|
|
207
|
+
if status == "failed" and not failures:
|
|
208
|
+
failures = [{"message": "The test failed without a message.", "file": None, "line": None}]
|
|
209
|
+
target["cases"].append({
|
|
210
|
+
"id": str(node.get("nodeIdentifier") or "/".join(filter(None, [suites[-1] if suites else None, name]))),
|
|
211
|
+
"name": name, "classname": suites[-1] if suites else target["name"], "status": status,
|
|
212
|
+
"durationMs": round(_seconds(node) * 1000), "failures": failures, "attachments": [], **where})
|
|
213
|
+
out = []
|
|
214
|
+
for target in targets.values():
|
|
215
|
+
cases = sorted(target["cases"], key=lambda case: (case["status"] != "failed", case["classname"], case["name"]))
|
|
216
|
+
counts = {key: sum(case["status"] == key for case in cases) for key in ("passed", "failed", "skipped")}
|
|
217
|
+
out.append({"name": target["name"], "status": "failed" if counts["failed"] else "passed", "tests": len(cases),
|
|
218
|
+
**counts, "durationMs": sum(case["durationMs"] for case in cases), "cases": cases})
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def export_attachments(bundle, folder, targets, *, runner=None):
|
|
223
|
+
"""Export the screenshots and files the tests attached into ``folder``/xctest/attachments/ and name them on their
|
|
224
|
+
cases (paths relative to ``folder``). Best effort: a test's verdict never depends on it."""
|
|
225
|
+
folder = Path(folder)
|
|
226
|
+
out_dir = folder / BUNDLE_DIR / ATTACHMENTS
|
|
227
|
+
(runner or run_command)(["xcrun", "xcresulttool", "export", "attachments", "--path", str(bundle),
|
|
228
|
+
"--output-path", str(out_dir)], folder, None, READ_TIMEOUT)
|
|
229
|
+
try:
|
|
230
|
+
manifest = json.loads((out_dir / "manifest.json").read_text(encoding="utf-8"))
|
|
231
|
+
except (OSError, ValueError):
|
|
232
|
+
return 0
|
|
233
|
+
wanted = {case["id"]: case for target in targets for case in target["cases"]}
|
|
234
|
+
count = 0
|
|
235
|
+
for entry in manifest if isinstance(manifest, list) else ():
|
|
236
|
+
case = wanted.get(str(entry.get("testIdentifier") or ""))
|
|
237
|
+
for item in entry.get("attachments") or ():
|
|
238
|
+
name = str(item.get("exportedFileName") or "")
|
|
239
|
+
if case is None or not name or not (out_dir / name).is_file() or Path(name).name != name:
|
|
240
|
+
continue
|
|
241
|
+
label = EXPORT_SUFFIX.sub("", str(item.get("suggestedHumanReadableName") or name))[:120]
|
|
242
|
+
case["attachments"].append({"name": label, "path": f"{BUNDLE_DIR}/{ATTACHMENTS}/{name}"})
|
|
243
|
+
count += 1
|
|
244
|
+
return count
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def section(targets, *, scheme, test_plan, device, bundle=None, why=None):
|
|
248
|
+
"""doc["xctest"]: the targets plus their totals, the device they ran on and why each target ran."""
|
|
249
|
+
totals = {key: sum(target[key] for target in targets) for key in ("tests", "passed", "failed", "skipped")}
|
|
250
|
+
status = "failed" if totals["failed"] else "passed"
|
|
251
|
+
return {"status": status, "scheme": scheme, "testPlan": test_plan, "device": device["name"],
|
|
252
|
+
"deviceKey": device["key"], "resultBundle": bundle, "why": dict(why or {}), "summary": totals,
|
|
253
|
+
"durationMs": sum(target["durationMs"] for target in targets), "targets": targets}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def milliseconds(ms):
|
|
257
|
+
""""38 ms", "1.2 s": a test's time."""
|
|
258
|
+
return f"{ms} ms" if ms < 1000 else f"{ms / 1000:.1f} s"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def couldnt_run(error, *, scheme=None, test_plan=None, why=None):
|
|
262
|
+
return {"status": "couldnt_run", "scheme": scheme, "testPlan": test_plan, "reason": {
|
|
263
|
+
"message": error.message, "fix": error.fix}, "why": dict(why or {}), "summary": {
|
|
264
|
+
"tests": 0, "passed": 0, "failed": 0, "skipped": 0}, "durationMs": 0, "targets": []}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def words(found):
|
|
268
|
+
""""XCTest 3 of 4 passed", "XCTest 1 failed of 4", "XCTest couldn't run"."""
|
|
269
|
+
totals = found.get("summary") or {}
|
|
270
|
+
if found.get("status") == "couldnt_run":
|
|
271
|
+
return "XCTest couldn't run"
|
|
272
|
+
if totals.get("failed"):
|
|
273
|
+
return f"XCTest {totals['failed']} failed of {totals['tests']}"
|
|
274
|
+
return f"XCTest {totals.get('passed', 0)} of {totals.get('tests', 0)} passed"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def failed_cases(doc):
|
|
278
|
+
"""(target name, case) for every failed XCTest case in a suite's results document."""
|
|
279
|
+
return [(target["name"], case) for target in (doc.get("xctest") or {}).get("targets") or ()
|
|
280
|
+
for case in target.get("cases") or () if case.get("status") == "failed"]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def failure_line(case):
|
|
284
|
+
""""PlanTests.swift:14: XCTAssertEqual failed ..." for a failed case."""
|
|
285
|
+
first = (case.get("failures") or [{}])[0]
|
|
286
|
+
where = f"{first['file']}:{first['line']}: " if first.get("file") else ""
|
|
287
|
+
return (where + str(first.get("message") or "failed")).strip()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# The worst outcome decides the exit code: failed, couldn't run, review, passed.
|
|
291
|
+
EXIT_RANK = {1: 0, 3: 1, 2: 2, 0: 3}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def merge_summary(doc):
|
|
295
|
+
"""Fold the XCTest result into the suite's summary and exit code (130, a stopped run, stays)."""
|
|
296
|
+
found = doc.get("xctest")
|
|
297
|
+
if not found:
|
|
298
|
+
return doc
|
|
299
|
+
summary = doc.setdefault("summary", {})
|
|
300
|
+
summary["xctest"] = dict(found["summary"], status=found["status"])
|
|
301
|
+
own = {"failed": 1, "couldnt_run": 3}.get(found["status"], 0)
|
|
302
|
+
current = doc.get("exitCode")
|
|
303
|
+
if current in EXIT_RANK and EXIT_RANK[own] < EXIT_RANK[current]:
|
|
304
|
+
doc["exitCode"] = own
|
|
305
|
+
elif current is None:
|
|
306
|
+
doc["exitCode"] = own
|
|
307
|
+
return doc
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def run_suite(planned, attempts, folder, progress, *, runner=None):
|
|
311
|
+
"""Build for testing (unless `test pr` did), run the targets on a simulator leased from ``attempts`` and return
|
|
312
|
+
doc["xctest"].
|
|
313
|
+
|
|
314
|
+
The simulator is the first simulator target of the run (the lease the checks just used is free again, so a booted
|
|
315
|
+
one is reused); a run of iPhones only has none, and says so."""
|
|
316
|
+
options = planned.options
|
|
317
|
+
labels = {"scheme": options.xctest_scheme, "test_plan": options.xctest_plan, "why": {}}
|
|
318
|
+
built = planned.xctest_build
|
|
319
|
+
if built is None:
|
|
320
|
+
try:
|
|
321
|
+
built = prepare(planned, progress, runner=runner)
|
|
322
|
+
except BuildError as error:
|
|
323
|
+
return couldnt_run(XctestError(error.message, error.fix), **labels)
|
|
324
|
+
chosen, why = select_targets(built.get("targets") or [], planned.changed, tuple(options.xctest_only))
|
|
325
|
+
labels = {"scheme": built.get("scheme"), "test_plan": options.xctest_plan, "why": why}
|
|
326
|
+
target = next((t for t in planned.targets if t.kind in ("default", "simulator") and t.record is None), None)
|
|
327
|
+
if target is None:
|
|
328
|
+
return couldnt_run(XctestError("--xctest runs on a simulator, and this run names only iPhones.",
|
|
329
|
+
"Add --sim, or run the XCTest targets in another run."), **labels)
|
|
330
|
+
if not chosen:
|
|
331
|
+
return couldnt_run(XctestError("None of the scheme's test targets matches --only-testing.",
|
|
332
|
+
f"The scheme's targets are {', '.join(built.get('targets') or []) or 'none'}."),
|
|
333
|
+
**labels)
|
|
334
|
+
only = [item for item in options.xctest_only if item.split("/", 1)[0] in chosen]
|
|
335
|
+
if not only and set(chosen) != set(built.get("targets") or []):
|
|
336
|
+
only = list(chosen) # the targets a changed file picked
|
|
337
|
+
try:
|
|
338
|
+
lease = attempts.simulators().acquire(target.device_type, target.runtime)
|
|
339
|
+
except SimError as error:
|
|
340
|
+
return couldnt_run(XctestError(f"No simulator was free for the tests: {error}",
|
|
341
|
+
error.fix or "Run `mobster sim doctor`."), **labels)
|
|
342
|
+
try:
|
|
343
|
+
sim = lease.target
|
|
344
|
+
device = {"name": sim.name, "key": f"sim:{sim.device_type}@{sim.runtime}"}
|
|
345
|
+
try:
|
|
346
|
+
bundle = run_tests(built["xctestrun"], sim.udid, folder, only, runner=runner, progress=progress)
|
|
347
|
+
tests, summary = read_bundle(bundle, runner=runner)
|
|
348
|
+
except XctestError as error:
|
|
349
|
+
return couldnt_run(error, **labels)
|
|
350
|
+
targets = parse(tests, summary, device)
|
|
351
|
+
if not targets:
|
|
352
|
+
return couldnt_run(XctestError("The scheme ran no tests.", "Check --only-testing and the scheme's Test "
|
|
353
|
+
"action."), **labels)
|
|
354
|
+
export_attachments(bundle, folder, targets, runner=runner)
|
|
355
|
+
return section(targets, scheme=built.get("scheme"), test_plan=options.xctest_plan, device=device,
|
|
356
|
+
bundle=Path(bundle).relative_to(folder).as_posix(), why=why)
|
|
357
|
+
finally:
|
|
358
|
+
lease.release()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def text_lines(doc, paint):
|
|
362
|
+
"""One line per failed XCTest case, with its file and line, and the XCTest totals."""
|
|
363
|
+
found = doc.get("xctest")
|
|
364
|
+
if not found:
|
|
365
|
+
return []
|
|
366
|
+
lines = [f"{paint('✗ failed', 'failed')} {target}: {case['classname']}.{case['name']}\n "
|
|
367
|
+
f"{failure_line(case)}" for target, case in failed_cases(doc)]
|
|
368
|
+
totals = found["summary"]
|
|
369
|
+
if found["status"] == "couldnt_run":
|
|
370
|
+
reason = found.get("reason") or {}
|
|
371
|
+
lines.append(f"{paint('✗ couldn\'t run', 'couldnt_run')} your XCTest targets\n "
|
|
372
|
+
f"{reason.get('message')} {reason.get('fix')}".rstrip())
|
|
373
|
+
else:
|
|
374
|
+
lines.append(f"XCTest: {totals['passed']} of {totals['tests']} passed"
|
|
375
|
+
+ (f", {totals['failed']} failed" if totals["failed"] else "")
|
|
376
|
+
+ (f", {totals['skipped']} skipped" if totals["skipped"] else "")
|
|
377
|
+
+ f" in {len(found['targets'])} target{'' if len(found['targets']) == 1 else 's'}")
|
|
378
|
+
return lines
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Track `conversations`."""
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""`mobster chat "message"` and `mobster chat --workflow`: the scripted path."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from ..narrate import approval_title, completion_message, status_label
|
|
8
|
+
from .client import NoService, ServiceError
|
|
9
|
+
from .recent import remember_thread
|
|
10
|
+
|
|
11
|
+
EXIT_DONE, EXIT_NOT_DONE, EXIT_NEEDS_YOU, EXIT_COULDNT, EXIT_RUNNING = 0, 1, 2, 3, 4
|
|
12
|
+
DONE = frozenset({"completed", "completed_unverified", "user_condition_met"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def outcome(run, thread_id):
|
|
16
|
+
summary = run.get("summary") or {}
|
|
17
|
+
data = summary.get("data")
|
|
18
|
+
out = {"threadId": thread_id, "runId": run.get("id"), "status": run.get("status"),
|
|
19
|
+
"outcome": summary.get("outcome"), "answer": summary.get("answer"),
|
|
20
|
+
"costUsd": run.get("costUsd"), "elapsedMs": summary.get("elapsed_ms")}
|
|
21
|
+
if data is not None:
|
|
22
|
+
out["data"] = data
|
|
23
|
+
if run.get("status") not in DONE and summary.get("reason"):
|
|
24
|
+
out["reason"] = summary["reason"]
|
|
25
|
+
return out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def waiting(run, thread_id):
|
|
29
|
+
approval = run.get("approval") or {}
|
|
30
|
+
if approval.get("kind") == "clarify":
|
|
31
|
+
return {"threadId": thread_id, "runId": run.get("id"), "status": "waiting_for_answer",
|
|
32
|
+
"question": approval.get("question") or approval.get("label"),
|
|
33
|
+
"choices": [c.get("label") for c in approval.get("choices") or ()]}
|
|
34
|
+
out = {"threadId": thread_id, "runId": run.get("id"), "status": "waiting_for_approval",
|
|
35
|
+
"approval": {"title": approval_title(approval), "text": approval.get("text"), "app": approval.get("app")}}
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def once(client, chat, text, *, as_json, wait, out, keep=False):
|
|
40
|
+
"""`mobster chat "message"`."""
|
|
41
|
+
tty = chat.interactive
|
|
42
|
+
if tty:
|
|
43
|
+
response = chat.send(text) # prints the task's first line and follows it
|
|
44
|
+
if response is None:
|
|
45
|
+
return EXIT_COULDNT
|
|
46
|
+
else:
|
|
47
|
+
body = {"text": text, **({"device": chat.device} if chat.device else {})}
|
|
48
|
+
try:
|
|
49
|
+
chat.thread_id, response = client.send(chat.thread_id, body)
|
|
50
|
+
except ServiceError as error:
|
|
51
|
+
if as_json:
|
|
52
|
+
print(json.dumps({"status": "refused", "error": str(error), "code": error.code}), file=out,
|
|
53
|
+
flush=True)
|
|
54
|
+
else:
|
|
55
|
+
chat.say(str(error))
|
|
56
|
+
return EXIT_COULDNT
|
|
57
|
+
remember_thread(client.url, chat.thread_id)
|
|
58
|
+
routed = routed_as(response)
|
|
59
|
+
if routed == "remember":
|
|
60
|
+
return offered(chat, response, tty=tty, as_json=as_json, wait=wait, out=out)
|
|
61
|
+
run_id = (response.get("run") or {}).get("id") or (response.get("item") or {}).get("runId")
|
|
62
|
+
if routed in ("steer", "answer"):
|
|
63
|
+
if as_json:
|
|
64
|
+
print(json.dumps({"threadId": chat.thread_id, "runId": run_id, "routed": routed}), file=out, flush=True)
|
|
65
|
+
else:
|
|
66
|
+
chat.say(" Mobster has your message; the task goes on. Follow it with: mobster chat --thread "
|
|
67
|
+
f"{chat.thread_id}", "faint")
|
|
68
|
+
return EXIT_DONE
|
|
69
|
+
if not tty:
|
|
70
|
+
# --json, or no terminal: never answers an approval; waits until the task ends or needs someone (with
|
|
71
|
+
# --keep-waiting, until it ends: someone answers in Mobster).
|
|
72
|
+
return finish(client.wait_run(run_id, wait, through=keep), chat.thread_id, as_json, out, chat)
|
|
73
|
+
return follow_here(chat, run_id, wait, out)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def follow_here(chat, run_id, wait, out):
|
|
77
|
+
"""On a terminal: the steps as they happen; an approval waits for a typed y, n or x. Returns the exit code."""
|
|
78
|
+
deadline = time.monotonic() + wait
|
|
79
|
+
term = chat.term
|
|
80
|
+
try:
|
|
81
|
+
while time.monotonic() < deadline:
|
|
82
|
+
chat.pump(0.1)
|
|
83
|
+
if bool(chat.pending) != term.prompting:
|
|
84
|
+
term.clear()
|
|
85
|
+
term.prompting = bool(chat.pending)
|
|
86
|
+
term.show()
|
|
87
|
+
done = chat.finished.get(run_id)
|
|
88
|
+
if done is not None:
|
|
89
|
+
return finish(done, chat.thread_id, False, out, chat, printed=True)
|
|
90
|
+
if chat.pending and chat.pending.get("kind") == "clarify":
|
|
91
|
+
chat.say(f" Answer with: mobster chat --thread {chat.thread_id} \"your answer\"", "faint")
|
|
92
|
+
return EXIT_NEEDS_YOU
|
|
93
|
+
if chat.pending:
|
|
94
|
+
chat.feed(chat.term.keys(0.1) or "")
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
chat.stop()
|
|
97
|
+
return 130
|
|
98
|
+
chat.say(f" Still working. Follow it with: mobster chat --thread {chat.thread_id}", "faint")
|
|
99
|
+
return EXIT_RUNNING
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def routed_as(response):
|
|
103
|
+
"""How Mobster took a message: new_run, steer, answer or remember."""
|
|
104
|
+
return response.get("routed") or (response.get("item") or {}).get("routed") or "new_run"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def proposals_of(client, thread_id, response):
|
|
108
|
+
"""The suggestions to remember a message made, [{id, text, pin}]. Where the answer leaves them out (that older
|
|
109
|
+
Mobster), the conversation's pending ones."""
|
|
110
|
+
if "proposals" in response:
|
|
111
|
+
return response.get("proposals") or []
|
|
112
|
+
from urllib.parse import quote
|
|
113
|
+
try:
|
|
114
|
+
listed = client.call("GET", f"/api/memory/proposals?thread={quote(str(thread_id), safe='')}")["proposals"]
|
|
115
|
+
except (ServiceError, NoService, KeyError, TypeError):
|
|
116
|
+
return []
|
|
117
|
+
return [{"id": p.get("id"), "text": p.get("text"), "scope": p.get("scope") or "global",
|
|
118
|
+
"pin": bool(p.get("pin"))} for p in listed
|
|
119
|
+
if isinstance(p, dict)]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def where_to_remember(proposal):
|
|
123
|
+
scope = proposal.get("scope") or "global"
|
|
124
|
+
command = ["mobster", "memory", "add", str(proposal.get("text") or "")]
|
|
125
|
+
if scope != "global":
|
|
126
|
+
command.extend(("--scope", scope))
|
|
127
|
+
return "Answer it in Mobster for Mac, or save it with: " + shlex.join(command)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def offered(chat, response, *, tty, as_json, wait, out):
|
|
131
|
+
"""A message that only asked Mobster to remember something: no task started, so there's no run to follow."""
|
|
132
|
+
if not tty:
|
|
133
|
+
proposals = proposals_of(chat.client, chat.thread_id, response)
|
|
134
|
+
if as_json:
|
|
135
|
+
print(json.dumps({"threadId": chat.thread_id, "status": "remember", "proposals": proposals},
|
|
136
|
+
ensure_ascii=False), file=out, flush=True)
|
|
137
|
+
else:
|
|
138
|
+
chat.offer(proposals)
|
|
139
|
+
return EXIT_DONE
|
|
140
|
+
# chat.send showed the first suggestion already.
|
|
141
|
+
term, deadline = chat.term, time.monotonic() + wait
|
|
142
|
+
try:
|
|
143
|
+
while chat.offers and not chat.quit and time.monotonic() < deadline:
|
|
144
|
+
if not term.prompting:
|
|
145
|
+
term.prompting = True
|
|
146
|
+
term.show()
|
|
147
|
+
data = term.keys(0.1)
|
|
148
|
+
if data is None:
|
|
149
|
+
break
|
|
150
|
+
chat.feed(data)
|
|
151
|
+
except KeyboardInterrupt:
|
|
152
|
+
return 130
|
|
153
|
+
for proposal in chat.offers:
|
|
154
|
+
chat.say(" " + where_to_remember(proposal), "faint")
|
|
155
|
+
chat.offers = []
|
|
156
|
+
return EXIT_DONE
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def finish(run, thread_id, as_json, out, chat, printed=False):
|
|
160
|
+
if run.get("finishedAt") is None:
|
|
161
|
+
if run.get("approval"):
|
|
162
|
+
state = waiting(run, thread_id)
|
|
163
|
+
if as_json:
|
|
164
|
+
print(json.dumps(state, ensure_ascii=False), file=out, flush=True)
|
|
165
|
+
else:
|
|
166
|
+
if state["status"] == "waiting_for_answer":
|
|
167
|
+
chat.note(chat.paint(str(state["question"]), "bold"), glyph="?", role="accent", indent="")
|
|
168
|
+
chat.say(f" Answer with: mobster chat --thread {thread_id} \"your answer\"", "faint")
|
|
169
|
+
else:
|
|
170
|
+
chat.show_approval(run["approval"])
|
|
171
|
+
chat.note("Waiting for your approval in Mobster (or in `mobster chat` on a terminal).")
|
|
172
|
+
return EXIT_NEEDS_YOU
|
|
173
|
+
if as_json:
|
|
174
|
+
print(json.dumps({"threadId": thread_id, "runId": run.get("id"), "status": "running"}), file=out,
|
|
175
|
+
flush=True)
|
|
176
|
+
else:
|
|
177
|
+
chat.say(f"Still working. Follow it with: mobster chat --thread {thread_id}", "faint")
|
|
178
|
+
return EXIT_RUNNING
|
|
179
|
+
result = outcome(run, thread_id)
|
|
180
|
+
if as_json:
|
|
181
|
+
print(json.dumps(result, ensure_ascii=False, allow_nan=False), file=out, flush=True)
|
|
182
|
+
elif not printed:
|
|
183
|
+
chat.say(status_label(result["status"]), "bold")
|
|
184
|
+
message = result.get("data") if isinstance(result.get("data"), str) else result.get("answer")
|
|
185
|
+
if message:
|
|
186
|
+
chat.say(str(message))
|
|
187
|
+
elif result.get("reason"):
|
|
188
|
+
chat.say(completion_message(result["status"], result["reason"], summary=result))
|
|
189
|
+
return EXIT_DONE if result["status"] in DONE else EXIT_NOT_DONE
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def blank_values(pairs):
|
|
193
|
+
"""{"topic": "basketball"} from ["topic=basketball", ...]. ValueError for a pair without "="."""
|
|
194
|
+
values = {}
|
|
195
|
+
for pair in pairs or ():
|
|
196
|
+
name, sep, value = pair.partition("=")
|
|
197
|
+
if not sep or not name.strip():
|
|
198
|
+
raise ValueError(f"Give each blank as NAME=VALUE, like --set topic=basketball (not {pair!r})")
|
|
199
|
+
values[name.strip()] = value.strip()
|
|
200
|
+
return values
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def follow_graph(client, workflow, batch, thread_id, *, as_json, wait, out):
|
|
204
|
+
"""A workflow run as a graph: each step's state as it changes, then how the run ended (with --json, one object
|
|
205
|
+
with every step). 0 when it completed, 1 when it didn't, 4 while it still runs."""
|
|
206
|
+
def changed(node):
|
|
207
|
+
if as_json or str(node.get("type", "")).startswith("trigger."):
|
|
208
|
+
return
|
|
209
|
+
line = f"{node.get('instance')}: {node.get('state')}"
|
|
210
|
+
if node.get("reason") and node.get("state") in ("failed", "blocked", "skipped", "unknown", "cancelled"):
|
|
211
|
+
line += f" ({node['reason']})"
|
|
212
|
+
out.write(line + "\n")
|
|
213
|
+
ended = client.wait_batch(workflow["id"], batch["id"], wait, on_node=changed) or batch
|
|
214
|
+
status = ended.get("status")
|
|
215
|
+
if as_json:
|
|
216
|
+
nodes = {key: {k: node[k] for k in ("state", "reason", "output", "runId") if k in node}
|
|
217
|
+
for key, node in (ended.get("nodes") or {}).items()}
|
|
218
|
+
out.write(json.dumps({"workflow": workflow["id"], "threadId": thread_id, "batch": ended.get("id"),
|
|
219
|
+
"status": status, "reason": ended.get("reason"), "result": ended.get("result"),
|
|
220
|
+
"nodes": nodes}) + "\n")
|
|
221
|
+
else:
|
|
222
|
+
out.write(f"“{workflow['name']}” {str(status).replace('_', ' ')}." +
|
|
223
|
+
(f" {ended['reason']}" if ended.get("reason") else "") + "\n")
|
|
224
|
+
return 0 if status == "completed" else 4 if status == "running" else 1
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def workflow_once(client, chat, args, *, as_json, wait, out, fail):
|
|
228
|
+
"""`mobster chat --workflow NAME [--set BLANK=VALUE]...`: start it, then follow it as one message."""
|
|
229
|
+
try:
|
|
230
|
+
values = blank_values(args.set)
|
|
231
|
+
workflow = client.find_workflow(args.workflow)
|
|
232
|
+
except ValueError as error:
|
|
233
|
+
return fail(str(error), "usage")
|
|
234
|
+
except LookupError as error:
|
|
235
|
+
return fail(str(error), "not_found")
|
|
236
|
+
try:
|
|
237
|
+
thread_id, answer = client.run_workflow(workflow["id"], values=values, thread_id=chat.thread_id,
|
|
238
|
+
new_conversation=bool(args.new))
|
|
239
|
+
except ServiceError as error:
|
|
240
|
+
return fail(str(error), error.code or "refused")
|
|
241
|
+
run = answer.get("run") or {}
|
|
242
|
+
chat.thread_id = thread_id
|
|
243
|
+
if thread_id:
|
|
244
|
+
remember_thread(client.url, thread_id)
|
|
245
|
+
batch = answer.get("batch") or {}
|
|
246
|
+
if batch.get("graph") == 2 and not chat.interactive:
|
|
247
|
+
return follow_graph(client, workflow, batch, thread_id, as_json=as_json, wait=wait, out=out)
|
|
248
|
+
if not run.get("id"):
|
|
249
|
+
# A workflow of steps that waits before its first task: nothing to follow yet. Its conversation has each step.
|
|
250
|
+
batch = answer.get("batch") or {}
|
|
251
|
+
if as_json:
|
|
252
|
+
out.write(json.dumps({"workflow": workflow["id"], "threadId": thread_id, "batch": batch.get("id"),
|
|
253
|
+
"status": batch.get("status")}) + "\n")
|
|
254
|
+
else:
|
|
255
|
+
out.write(f"Started “{workflow['name']}”. It waits before its first task; its conversation shows each "
|
|
256
|
+
"step.\n")
|
|
257
|
+
return 0
|
|
258
|
+
if not chat.interactive:
|
|
259
|
+
keep = bool(getattr(args, "keep_waiting", False))
|
|
260
|
+
return finish(client.wait_run(run["id"], wait, through=keep), thread_id, as_json, out, chat)
|
|
261
|
+
where = run.get("deviceName")
|
|
262
|
+
chat.renderer(run["id"]).start(run.get("goal") or workflow["name"], f"on {where}" if where else "")
|
|
263
|
+
chat.follow(run["id"])
|
|
264
|
+
return follow_here(chat, run["id"], wait, out)
|