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,952 @@
|
|
|
1
|
+
"""result.json (``mobster.verify/1``) and report.html, which makes no requests and escapes every app string."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import html
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .. import brand as tokens
|
|
12
|
+
from ..brand_fonts import FONT_FACES
|
|
13
|
+
from .overlay import pill_numbers
|
|
14
|
+
|
|
15
|
+
CSP = "default-src 'none'; img-src data:; font-src data:; style-src 'unsafe-inline'"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
THUMB_WIDTH = 160
|
|
19
|
+
FRAME_WIDTH = 600 # px of the frames embedded at FRAME_CSS_WIDTH (2x)
|
|
20
|
+
VERDICTS = {"passed": ("Passed", "pass"), "failed": ("Failed", "fail"),
|
|
21
|
+
"needs_review": ("Needs review", "review"), "couldnt_run": ("Couldn't run", "none")}
|
|
22
|
+
MODES = {"smart": "Smart", "keyless": "Key-less", "launch": "Launch-only"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
FINDING_WORDS = {"warn": "Warning. ", "known": "In the baseline. "}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _short(text, limit):
|
|
29
|
+
text = " ".join(str(text or "").split())
|
|
30
|
+
return text if len(text) <= limit else text[:limit - 1] + "…"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _e(value):
|
|
34
|
+
return html.escape("" if value is None else str(value), quote=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _png_url(path, limit=200_000):
|
|
38
|
+
"""A small PNG the run saved (the app's icon) as a data URL, or None when there is none."""
|
|
39
|
+
try:
|
|
40
|
+
data = Path(path).read_bytes()
|
|
41
|
+
except OSError:
|
|
42
|
+
return None
|
|
43
|
+
return "data:image/png;base64," + base64.b64encode(data).decode() if data.startswith(b"\x89PNG") and len(
|
|
44
|
+
data) <= limit else None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _data_url(path, max_width=None, quality=72):
|
|
48
|
+
path = Path(path)
|
|
49
|
+
try:
|
|
50
|
+
if max_width is None:
|
|
51
|
+
data = path.read_bytes()
|
|
52
|
+
else:
|
|
53
|
+
from PIL import Image
|
|
54
|
+
with Image.open(path) as image:
|
|
55
|
+
image = image.convert("RGB")
|
|
56
|
+
if image.width > max_width:
|
|
57
|
+
image = image.resize((max_width, max(1, round(image.height * max_width / image.width))),
|
|
58
|
+
Image.LANCZOS)
|
|
59
|
+
out = BytesIO()
|
|
60
|
+
image.save(out, "JPEG", quality=quality)
|
|
61
|
+
data = out.getvalue()
|
|
62
|
+
except (OSError, ValueError):
|
|
63
|
+
return None
|
|
64
|
+
return "data:image/jpeg;base64," + base64.b64encode(data).decode()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
ICONS = {
|
|
68
|
+
"check": '<path d="M3.5 8.5l3 3 6-7"/>',
|
|
69
|
+
"cross": '<path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/>',
|
|
70
|
+
"review": '<path d="M8 4.5v4.25M8 11.4v.1"/>',
|
|
71
|
+
"dash": '<path d="M4.5 8h7"/>',
|
|
72
|
+
"phone": '<rect x="4.25" y="1.75" width="7.5" height="12.5" rx="2"/><path d="M7 4h2"/>',
|
|
73
|
+
"app": '<rect x="2.25" y="2.25" width="4.75" height="4.75" rx="1.4"/><rect x="9" y="2.25" width="4.75" '
|
|
74
|
+
'height="4.75" rx="1.4"/><rect x="2.25" y="9" width="4.75" height="4.75" rx="1.4"/><rect x="9" y="9" '
|
|
75
|
+
'width="4.75" height="4.75" rx="1.4"/>',
|
|
76
|
+
"sim": '<rect x="1.75" y="2.5" width="12.5" height="8.5" rx="1.5"/><path d="M5.5 13.5h5M8 11v2.5"/>',
|
|
77
|
+
"clock": '<circle cx="8" cy="8" r="6"/><path d="M8 4.75V8l2 1.5"/>',
|
|
78
|
+
"calendar": '<rect x="2.25" y="3" width="11.5" height="10.75" rx="2"/>'
|
|
79
|
+
'<path d="M2.25 6.5h11.5M5.5 1.5v3M10.5 1.5v3"/>',
|
|
80
|
+
"chevron": '<path d="M4.5 6.25l3.5 3.5 3.5-3.5"/>',
|
|
81
|
+
"coin": '<circle cx="8" cy="8" r="6"/><path d="M9.75 5.9C9.4 5.35 8.75 5 8 5c-1 0-1.75.6-1.75 1.4 0 1.9 3.5.9 3.5 '
|
|
82
|
+
'2.9 0 .85-.8 1.45-1.75 1.45-.8 0-1.5-.4-1.8-1M8 4v1M8 10.75v1"/>',
|
|
83
|
+
}
|
|
84
|
+
STATUS_ICON = {"pass": "check", "fail": "cross", "review": "review", "none": "dash"}
|
|
85
|
+
MARK_SVG = ('<span class="mark" aria-hidden="true">'
|
|
86
|
+
+ "".join(tokens.MARK[theme].replace("<svg ", f'<svg class="mark-{theme}" ', 1)
|
|
87
|
+
for theme in ("light", "dark"))
|
|
88
|
+
+ "</span>")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def icon(name, cls="icon"):
|
|
92
|
+
return (f'<svg class="{cls}" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" '
|
|
93
|
+
f'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{ICONS[name]}</svg>')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def verdict_chip(word, tone):
|
|
97
|
+
return f'<span class="verdict {tone}">{icon(STATUS_ICON.get(tone, "dash"))}{_e(word)}</span>'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def brand(tool, ident=None):
|
|
101
|
+
return (f'<div class="brand">{MARK_SVG}<span class="brand-name">Mobster</span><span class="brand-tool">'
|
|
102
|
+
f'{_e(tool)}</span>' + (f'<span class="brand-id">{_e(ident)}</span>' if ident else "") + "</div>")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def pill(number, ok, on_frame=True):
|
|
106
|
+
tone = "pass" if ok else "fail"
|
|
107
|
+
mark = icon("check" if ok else "cross")
|
|
108
|
+
where = "" if on_frame else ", not outlined on the frame"
|
|
109
|
+
words = f"{number}, {'held' if ok else 'failed'}{where}"
|
|
110
|
+
return f'<span class="pill {tone}" role="img" aria-label="{_e(words)}">{mark}{_e(number)}</span>'
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def has_node(item):
|
|
114
|
+
"""Whether the overlay outlines an assertion's element: it matched one with an area (a suite's copy of the assertion
|
|
115
|
+
says so in ``outlined``)."""
|
|
116
|
+
if "outlined" in item:
|
|
117
|
+
return bool(item["outlined"])
|
|
118
|
+
return any(len(match.get("rect") or ()) == 4 and match["rect"][2] > 0 and match["rect"][3] > 0
|
|
119
|
+
for match in item.get("matches") or () if isinstance(match, dict))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def expectations(items, *, frame=True, quiet_held=False):
|
|
123
|
+
"""The numbered list of assertions: ``items`` are result.json's assertions (index, text, ok, observed, matches)."""
|
|
124
|
+
numbered = pill_numbers([(item.get("ok"), has_node(item)) for item in items]) if frame else set()
|
|
125
|
+
rows = []
|
|
126
|
+
for position, item in enumerate(items):
|
|
127
|
+
ok = bool(item.get("ok"))
|
|
128
|
+
number = (item.get("index") if isinstance(item.get("index"), int) else position) + 1
|
|
129
|
+
observed = item.get("observed")
|
|
130
|
+
rows.append(f'<li class="{"held" if ok else "failed"}">{pill(number, ok, position in numbered)}'
|
|
131
|
+
f'<code class="assert">{_e(item.get("text"))}</code>'
|
|
132
|
+
+ (f'<span class="observed">{_e(observed)}</span>' if observed and not (ok and quiet_held)
|
|
133
|
+
else "") + "</li>")
|
|
134
|
+
return '<ol class="expect">' + "".join(rows) + "</ol>"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def business_section(result):
|
|
138
|
+
business = result.get("business")
|
|
139
|
+
if not isinstance(business, dict):
|
|
140
|
+
return ""
|
|
141
|
+
expected = {item.get("field"): item.get("equals") for item in
|
|
142
|
+
(result.get("check") or {}).get("business", {}).get("expect", ()) if isinstance(item, dict)}
|
|
143
|
+
|
|
144
|
+
def shown(value):
|
|
145
|
+
return str(value).lower() if value is None or type(value) is bool else f"[{type(value).__name__} redacted]"
|
|
146
|
+
|
|
147
|
+
rows = []
|
|
148
|
+
for item in business.get("assertions", ()):
|
|
149
|
+
field = item.get("field")
|
|
150
|
+
status = "held" if item.get("ok") else "failed"
|
|
151
|
+
observed = shown(item.get("observed")) if item.get("present") else "missing"
|
|
152
|
+
rows.append(f'<li class="{status}"><code class="assert">{_e(field)}</code>'
|
|
153
|
+
f'<span class="observed">expected {_e(shown(expected.get(field)))}; '
|
|
154
|
+
f'observed {_e(observed)}</span></li>')
|
|
155
|
+
lifecycle = (f'Setup: {business.get("setup", "not_started")}; '
|
|
156
|
+
f'Observation: {business.get("status", "unavailable")}; '
|
|
157
|
+
f'Cleanup: {business.get("cleanup", "not_needed")}')
|
|
158
|
+
detail = f'<p>{_e(lifecycle)}</p>'
|
|
159
|
+
if business.get("error"):
|
|
160
|
+
detail += f'<p>{_e(business["error"])}</p>'
|
|
161
|
+
if business.get("primary"):
|
|
162
|
+
detail += f'<p>Primary result: {_e(business["primary"].get("verdict"))} '
|
|
163
|
+
detail += f'({_e(business["primary"].get("class"))}).</p>'
|
|
164
|
+
return ('<h2>Persisted app state</h2><div class="card">' + detail
|
|
165
|
+
+ ('<ol class="expect">' + "".join(rows) + "</ol>" if rows else "") + "</div>")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
EVIDENCE_TEXT = 300 # characters of a log line the page shows; logs/app.log has the whole line
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _seconds(ms):
|
|
172
|
+
return f"{(ms or 0) / 1000:.1f} s"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def crash_title(crash):
|
|
176
|
+
"""'Crashed: EXC_BREAKPOINT (SIGTRAP)', or 'Crashed: SIGTRAP' before the report is in."""
|
|
177
|
+
what = " ".join(filter(None, [crash.get("type"), f"({crash['signal']})" if crash.get("type") and crash.get("signal")
|
|
178
|
+
else crash.get("signal")]))
|
|
179
|
+
return "Crashed" + (f": {what}" if what else "")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def evidence_pane(evidence, *, step=None):
|
|
183
|
+
from .crashes import frame_text
|
|
184
|
+
evidence = evidence if isinstance(evidence, dict) else {}
|
|
185
|
+
crash, window, log = evidence.get("crash"), evidence.get("window"), evidence.get("log")
|
|
186
|
+
if not crash and not window:
|
|
187
|
+
return ""
|
|
188
|
+
at = evidence.get("atMs")
|
|
189
|
+
after = f", step {step}" if step else ""
|
|
190
|
+
out = ['<div class="evidence">']
|
|
191
|
+
if crash:
|
|
192
|
+
link = (f'<a class="ev-file" href="{_e(crash["report"])}">Crash report</a>' if crash.get("report")
|
|
193
|
+
else "")
|
|
194
|
+
out.append(f'<div class="ev-part fail"><p class="ev-head"><strong>{_e(crash_title(crash))}</strong>'
|
|
195
|
+
f'<span>at {_e(_seconds(crash.get("atMs")))}{_e(after)}</span>{link}</p>')
|
|
196
|
+
if crash.get("message"):
|
|
197
|
+
out.append(f'<p class="ev-msg">{_e(crash["message"])}</p>')
|
|
198
|
+
frames = crash.get("frames") or []
|
|
199
|
+
if frames:
|
|
200
|
+
out.append('<ol class="frames" aria-label="The crashed thread\'s top frames">')
|
|
201
|
+
for frame in frames:
|
|
202
|
+
where = f"{frame['file']}:{frame['line']}" if frame.get("file") and frame.get("line") else ""
|
|
203
|
+
symbol = frame_text({**frame, "file": None}, image=False)
|
|
204
|
+
out.append(f'<li class="{"own" if frame.get("own") else ""}"><span class="fn">{_e(frame.get("index"))}'
|
|
205
|
+
f'</span><span class="fimg">{_e(frame.get("image"))}</span><span><code>{_e(symbol)}</code>'
|
|
206
|
+
+ (f'<span class="floc">{_e(where)}</span>' if where else "") + "</span></li>")
|
|
207
|
+
out.append("</ol>")
|
|
208
|
+
if crash.get("symbolicated"):
|
|
209
|
+
out.append('<p class="ev-note">Your frames are named from the dSYM in .mobster/build.</p>')
|
|
210
|
+
else:
|
|
211
|
+
out.append('<p class="ev-note">The app exited on this signal. macOS hadn\'t written its crash report '
|
|
212
|
+
"when the run ended, so there is no stack here.</p>")
|
|
213
|
+
out.append("</div>")
|
|
214
|
+
if window is not None:
|
|
215
|
+
lines = window.get("lines") or []
|
|
216
|
+
link = f'<a class="ev-file" href="{_e(log["path"])}">Full log</a>' if log and log.get("path") else ""
|
|
217
|
+
count = f"{len(lines)} line{'' if len(lines) == 1 else 's'}" + (
|
|
218
|
+
f" of {log['lines']}" if log and log.get("lines") and log["lines"] > len(lines) else "")
|
|
219
|
+
out.append(f'<div class="ev-part"><p class="ev-head"><strong>Log, 10 s either side</strong><span>{_e(count)}'
|
|
220
|
+
f"</span>{link}</p>")
|
|
221
|
+
if lines:
|
|
222
|
+
out.append('<ol class="ev-lines">')
|
|
223
|
+
marked = False
|
|
224
|
+
for line in lines:
|
|
225
|
+
if not marked and at is not None and (line.get("atMs") or 0) > at:
|
|
226
|
+
out.append(_at_row(crash, at))
|
|
227
|
+
marked = True
|
|
228
|
+
level = line.get("level") or ""
|
|
229
|
+
text = str(line.get("text") or "")
|
|
230
|
+
text = text if len(text) <= EVIDENCE_TEXT else text[:EVIDENCE_TEXT] + "…"
|
|
231
|
+
out.append(f'<li class="{_e(level)}"><span class="t">{_e(f"{(line.get("atMs") or 0) / 1000:.3f}")}'
|
|
232
|
+
f'</span><span class="lv">{_e(level)}</span><span class="tx">{_e(text)}</span></li>')
|
|
233
|
+
if not marked and at is not None:
|
|
234
|
+
out.append(_at_row(crash, at))
|
|
235
|
+
out.append("</ol>")
|
|
236
|
+
else:
|
|
237
|
+
out.append('<p class="ev-note">The app logged nothing in those 20 seconds.</p>')
|
|
238
|
+
out.append("</div>")
|
|
239
|
+
out.append("</div>")
|
|
240
|
+
return "".join(out)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
SIDES = {"app": "App problem", "check": "Check problem", "environment": "Environment problem"}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def cause_line(proof):
|
|
247
|
+
"""'App problem: app crashed', for a run that didn't pass."""
|
|
248
|
+
cause = (proof or {}).get("cause")
|
|
249
|
+
if not isinstance(cause, dict) or not cause.get("line"):
|
|
250
|
+
return ""
|
|
251
|
+
return f'<p class="cause"><strong>{_e(SIDES.get(cause.get("side"), "Cause"))}</strong> {_e(cause["line"])}</p>'
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _clock(seconds):
|
|
255
|
+
seconds = int(seconds)
|
|
256
|
+
return f"{seconds // 60}:{seconds % 60:02d}"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def proof_pane(entry, *, clip=None, video=None, title="", opened=False):
|
|
260
|
+
"""One step's proof: its clip (when the recording is known), the app's log lines in its span and the requests the
|
|
261
|
+
log shows. ``clip`` is (from, to) seconds into ``video``."""
|
|
262
|
+
if not isinstance(entry, dict):
|
|
263
|
+
return ""
|
|
264
|
+
log, network = entry.get("log") or {}, entry.get("network") or {}
|
|
265
|
+
lines, rows = log.get("lines") or [], network.get("rows") or []
|
|
266
|
+
if not lines and not rows and not (clip and video):
|
|
267
|
+
return ""
|
|
268
|
+
counts = [f"{log.get('count') or 0} log line{'' if log.get('count') == 1 else 's'}",
|
|
269
|
+
f"{network.get('count') or 0} request{'' if network.get('count') == 1 else 's'}"]
|
|
270
|
+
if clip and video:
|
|
271
|
+
counts.insert(0, f"clip {_clock(clip[0])} to {_clock(clip[1])}")
|
|
272
|
+
out = [f'<details class="proof"{" open" if opened else ""}><summary>{_e(title or "Proof")}'
|
|
273
|
+
f"<span>{_e(', '.join(counts))}</span></summary>"]
|
|
274
|
+
if clip and video:
|
|
275
|
+
out.append(f'<p class="ev-note"><a href="{_e(video)}#t={clip[0]},{clip[1]}">Play this step</a> '
|
|
276
|
+
f"({clip[0]} to {clip[1]} s of the recording)</p>")
|
|
277
|
+
if rows:
|
|
278
|
+
out.append('<table class="net"><tbody>')
|
|
279
|
+
for row in rows:
|
|
280
|
+
size = f"{row['bytes']:,} B" if isinstance(row.get("bytes"), int) else ""
|
|
281
|
+
took = f"{row['durationMs']} ms" if isinstance(row.get("durationMs"), int) else ""
|
|
282
|
+
out.append(f'<tr><td>{_e(row.get("method") or "")}</td><td>{_e(row.get("host"))}</td>'
|
|
283
|
+
f'<td>{_e(row.get("status") or "")}</td><td>{_e(took)}</td><td>{_e(size)}</td></tr>')
|
|
284
|
+
out.append("</tbody></table>")
|
|
285
|
+
if lines:
|
|
286
|
+
out.append('<ol class="ev-lines">')
|
|
287
|
+
for line in lines:
|
|
288
|
+
text = str(line.get("text") or "")
|
|
289
|
+
text = text if len(text) <= EVIDENCE_TEXT else text[:EVIDENCE_TEXT] + "…"
|
|
290
|
+
out.append(f'<li class="{_e(line.get("level") or "")}"><span class="t">'
|
|
291
|
+
f'{_e(f"{(line.get("atMs") or 0) / 1000:.3f}")}</span><span class="lv">'
|
|
292
|
+
f'{_e(line.get("level") or "")}</span><span class="tx">{_e(text)}</span></li>')
|
|
293
|
+
out.append("</ol>")
|
|
294
|
+
out.append("</details>")
|
|
295
|
+
return "".join(out)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _at_row(crash, at):
|
|
299
|
+
words = ("Crash" if crash else "Failure") + f" at {at / 1000:.3f} s"
|
|
300
|
+
return f'<li class="at" role="separator">{_e(words)}</li>'
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
OVERLAY_LEGEND = ('<p class="legend" aria-hidden="true"><span class="key read"></span>Read by Mobster '
|
|
304
|
+
'<span class="key held"></span>Held <span class="key failed"></span>Failed</p>')
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def command(text):
|
|
308
|
+
"""A shell command, highlighted as on mobster.dev: flags violet, quoted strings green, arguments grey."""
|
|
309
|
+
if not text:
|
|
310
|
+
return ""
|
|
311
|
+
tokens = re.findall(r"""'[^']*'|"(?:\\.|[^"\\])*"|\S+|\s+""", str(text))
|
|
312
|
+
out, words = [], 0
|
|
313
|
+
for token in tokens:
|
|
314
|
+
if token.isspace():
|
|
315
|
+
out.append(_e(token))
|
|
316
|
+
continue
|
|
317
|
+
words += 1
|
|
318
|
+
if token.startswith("-"):
|
|
319
|
+
kind = "flag"
|
|
320
|
+
elif token[:1] in "'\"":
|
|
321
|
+
kind = "string"
|
|
322
|
+
elif words <= 2:
|
|
323
|
+
kind = "cmd"
|
|
324
|
+
else:
|
|
325
|
+
kind = "arg"
|
|
326
|
+
out.append(f'<span class="hl-{kind}">{_e(token)}</span>')
|
|
327
|
+
return ('<pre class="cmd"><span class="hl-prompt" aria-hidden="true">$ </span><code>' + "".join(out)
|
|
328
|
+
+ "</code></pre>")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def dots(text):
|
|
332
|
+
"""A device's label for the page: 'iPhone 17 Pro · iOS 26.4' reads 'iPhone 17 Pro, iOS 26.4'."""
|
|
333
|
+
# The suite keeps the dotted label (JUnit names it); the page has no middle dots.
|
|
334
|
+
return None if text is None else str(text).replace(" · ", ", ")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def home(text):
|
|
338
|
+
"""Paths under the home folder as ~/…, so a shared report doesn't spell out whose Mac ran it."""
|
|
339
|
+
if not text:
|
|
340
|
+
return text
|
|
341
|
+
folder = str(Path.home())
|
|
342
|
+
# Not inside quotes: a shell doesn't expand a quoted ~.
|
|
343
|
+
return re.sub(r"(^|[\s=])" + re.escape(folder) + r"(?=/|$|\s)", lambda m: m.group(1) + "~", str(text))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def when(ms=None, run_id=None):
|
|
347
|
+
"""'8 Oct 2026, 12:59' from epoch ms, else from a run id's leading YYYYMMDD-HHMMSS."""
|
|
348
|
+
stamp = None
|
|
349
|
+
if ms:
|
|
350
|
+
stamp = time.localtime(ms / 1000)
|
|
351
|
+
elif run_id:
|
|
352
|
+
match = re.match(r"(\d{8}-\d{6})", str(run_id))
|
|
353
|
+
if match:
|
|
354
|
+
try:
|
|
355
|
+
stamp = time.strptime(match.group(1), "%Y%m%d-%H%M%S")
|
|
356
|
+
except ValueError:
|
|
357
|
+
stamp = None
|
|
358
|
+
if stamp is None:
|
|
359
|
+
return None
|
|
360
|
+
return f"{stamp.tm_mday} {time.strftime('%b %Y, %H:%M', stamp)}"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def seconds_text(seconds):
|
|
364
|
+
seconds = float(seconds or 0)
|
|
365
|
+
if seconds < 60:
|
|
366
|
+
return f"{seconds:.1f} s" if seconds < 10 and seconds % 1 else f"{round(seconds)} s"
|
|
367
|
+
minutes, rest = divmod(round(seconds), 60)
|
|
368
|
+
return f"{minutes} min {rest:02d} s"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# Tokens only (test_brand: no colour literals); pills keep the light tones in both schemes, as the overlay does.
|
|
372
|
+
COMPONENTS = """
|
|
373
|
+
:root { --pass: var(--green); --fail: var(--coral); --review: var(--violet-text); --none: var(--ink-2);
|
|
374
|
+
--read: var(--violet); --bezel: var(--ink);
|
|
375
|
+
--fill-quiet: color-mix(in srgb, var(--ink) 7%, transparent);
|
|
376
|
+
--fill-input: color-mix(in srgb, var(--ink) 5%, transparent);
|
|
377
|
+
--hover: color-mix(in srgb, var(--ink) 6%, transparent);
|
|
378
|
+
--pass-solid: PASS_SOLID; --fail-solid: FAIL_SOLID; --review-solid: var(--violet-solid); --none-solid: var(--ink-3); }
|
|
379
|
+
@media (prefers-color-scheme: dark) { :root { --bezel: var(--line-strong); } }
|
|
380
|
+
* { box-sizing: border-box; }
|
|
381
|
+
html { background: var(--paper); -webkit-text-size-adjust: 100%; }
|
|
382
|
+
body { margin: 0; background: var(--paper); color: var(--ink); font: 400 14px/22px var(--font-sans);
|
|
383
|
+
font-synthesis: none; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
|
384
|
+
main { max-width: 1040px; margin: 0 auto; padding: 32px 24px 64px; }
|
|
385
|
+
:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
|
|
386
|
+
a:focus-visible { border-radius: var(--radius-xs); }
|
|
387
|
+
::selection { background: var(--violet-wash); color: var(--ink); }
|
|
388
|
+
.sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
|
|
389
|
+
clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
390
|
+
.icon { width: 16px; height: 16px; flex: none; }
|
|
391
|
+
code, pre, .mono { font-family: var(--font-mono); }
|
|
392
|
+
a { color: inherit; text-decoration-line: underline; text-decoration-thickness: 1px; text-underline-offset: .2em;
|
|
393
|
+
text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
|
|
394
|
+
transition: text-decoration-color var(--duration-medium) var(--ease-standard); }
|
|
395
|
+
a:hover { text-decoration-color: currentColor; }
|
|
396
|
+
.pass { --tone: var(--pass); --solid: var(--pass-solid); } .fail { --tone: var(--fail); --solid: var(--fail-solid); }
|
|
397
|
+
.review { --tone: var(--review); --solid: var(--review-solid); } .none { --tone: var(--none); --solid: var(--none-solid); }
|
|
398
|
+
.warn { --tone: var(--amber); }
|
|
399
|
+
|
|
400
|
+
/* The page's head: the mark and the tool, the verdict, the one line that matters, then the facts. */
|
|
401
|
+
.brand { display: flex; align-items: center; gap: 8px; font-size: 13px; line-height: 20px; color: var(--ink-3);
|
|
402
|
+
min-width: 0; }
|
|
403
|
+
.mark { display: block; width: 22px; height: 22px; flex: none; }
|
|
404
|
+
.mark svg { display: block; width: 100%; height: 100%; }
|
|
405
|
+
.mark .mark-dark { display: none; }
|
|
406
|
+
@media (prefers-color-scheme: dark) { .mark .mark-light { display: none; } .mark .mark-dark { display: block; } }
|
|
407
|
+
.brand-name { margin-left: -2px; font-size: 15px; font-weight: var(--weight-medium); letter-spacing: -.01em;
|
|
408
|
+
color: var(--ink); }
|
|
409
|
+
.brand-tool::before { content: "/"; margin-right: 8px; color: var(--ink-4); }
|
|
410
|
+
.brand-id { margin-left: auto; font: 12px/18px var(--font-mono); color: var(--ink-3); overflow-wrap: anywhere;
|
|
411
|
+
text-align: right; }
|
|
412
|
+
header.head { padding-top: 32px; }
|
|
413
|
+
.chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
|
|
414
|
+
.verdict { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; line-height: 20px;
|
|
415
|
+
font-weight: var(--weight-medium); color: var(--tone); }
|
|
416
|
+
.verdict .icon { stroke-width: 2; }
|
|
417
|
+
.tag { display: inline-flex; align-items: center; gap: 6px; height: 20px; padding: 0 7px;
|
|
418
|
+
border-radius: var(--radius-xs); font-size: 12px; line-height: 18px; font-weight: var(--weight-medium);
|
|
419
|
+
color: var(--ink-2); background: var(--fill-quiet); white-space: nowrap; }
|
|
420
|
+
.tag.tone::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--tone); }
|
|
421
|
+
.tag .icon { width: 12px; height: 12px; stroke-width: 2; color: var(--tone, var(--ink-3)); }
|
|
422
|
+
h1 { font-size: 28px; line-height: 36px; letter-spacing: -.015em; font-weight: var(--weight-medium);
|
|
423
|
+
margin: 12px 0 4px; overflow-wrap: anywhere; text-wrap: balance; }
|
|
424
|
+
h1 .quiet { color: var(--ink-3); margin-left: 12px; }
|
|
425
|
+
.lede { margin: 0; font-size: 16px; line-height: 24px; color: var(--ink-2); overflow-wrap: anywhere; }
|
|
426
|
+
ul.meta { display: flex; flex-wrap: wrap; gap: 8px 20px; list-style: none; margin: 16px 0 0; padding: 0;
|
|
427
|
+
font-size: 13px; line-height: 20px; color: var(--ink-2); }
|
|
428
|
+
ul.meta li { display: inline-flex; align-items: center; gap: 8px; min-width: 0; overflow-wrap: anywhere; }
|
|
429
|
+
ul.meta .icon { color: var(--ink-3); }
|
|
430
|
+
/* The app's own icon, read from its .app (sim/apps.py icon_png), in the squircle iOS draws it in, with the app's
|
|
431
|
+
hairline ring (10% ink) so a dark icon keeps its edge on Night as in the Mac app. */
|
|
432
|
+
ul.meta .app-icon { width: 16px; height: 16px; border-radius: 23%; flex: none;
|
|
433
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 10%, transparent); }
|
|
434
|
+
header.head pre.cmd { margin-top: 20px; }
|
|
435
|
+
|
|
436
|
+
/* Sections and cards: the app's flat card fill, no outline on Paper, a faint ring on Night. */
|
|
437
|
+
section { margin-top: 40px; }
|
|
438
|
+
h2 { display: flex; align-items: baseline; gap: 8px; margin: 0 0 12px; font-size: 16px; line-height: 24px;
|
|
439
|
+
font-weight: var(--weight-title); letter-spacing: -.012em; }
|
|
440
|
+
h2 .count { font-size: 14px; font-weight: var(--weight-body); color: var(--ink-3);
|
|
441
|
+
font-variant-numeric: tabular-nums; }
|
|
442
|
+
.card { background: var(--card); border-radius: var(--radius-card); box-shadow: var(--shadow-card); }
|
|
443
|
+
.empty { color: var(--ink-3); margin: 0; }
|
|
444
|
+
|
|
445
|
+
/* A shell command in a sunken well, highlighted like mobster.dev's code. */
|
|
446
|
+
pre.cmd { margin: 0; width: fit-content; max-width: 100%; padding: 10px 16px; background: var(--sunken);
|
|
447
|
+
border-radius: var(--radius-input); font: 13px/20px var(--font-mono); color: var(--ink); white-space: pre-wrap;
|
|
448
|
+
overflow-wrap: break-word; }
|
|
449
|
+
pre.cmd code { font: inherit; }
|
|
450
|
+
.hl-prompt { color: var(--ink-3); user-select: none; -webkit-user-select: none; }
|
|
451
|
+
.hl-flag { color: var(--violet-text); } .hl-string { color: var(--green); } .hl-arg { color: var(--ink-2); }
|
|
452
|
+
.hl-cmd { color: var(--ink); }
|
|
453
|
+
|
|
454
|
+
/* The numbered assertions, rows inside the card; the numbers are the overlay's pills. */
|
|
455
|
+
ol.expect { list-style: none; margin: 0; padding: 4px; }
|
|
456
|
+
ol.expect li { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 2px 12px; align-items: baseline;
|
|
457
|
+
padding: 12px 16px; border-radius: var(--radius-container); }
|
|
458
|
+
ol.expect li.failed { background: color-mix(in srgb, var(--fail) 7%, transparent); }
|
|
459
|
+
code.assert { font: 13px/20px var(--font-mono); color: var(--ink); overflow-wrap: break-word; min-width: 0; }
|
|
460
|
+
.observed { grid-column: 2; font-size: 13px; line-height: 20px; color: var(--ink-3); overflow-wrap: break-word; }
|
|
461
|
+
li.failed .observed { color: var(--ink-2); }
|
|
462
|
+
.pill { position: relative; top: -1px; display: inline-flex; align-items: center; gap: 3px; height: 20px;
|
|
463
|
+
min-width: 34px; padding: 0 8px 0 6px; border-radius: var(--radius-full); background: var(--solid); color: white;
|
|
464
|
+
font: var(--weight-medium) 12px/1 var(--font-sans); font-variant-numeric: tabular-nums;
|
|
465
|
+
print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
|
466
|
+
.pill .icon { width: 11px; height: 11px; stroke-width: 2.4; }
|
|
467
|
+
|
|
468
|
+
/* A frame, in a phone-shaped bezel (drawn hardware keeps its own radii). */
|
|
469
|
+
.phone { padding: 6px; border-radius: 46px; background: var(--bezel); box-shadow: var(--shadow-shot); }
|
|
470
|
+
.phone img { display: block; width: 100%; height: auto; border-radius: 40px; background: var(--sunken); }
|
|
471
|
+
.legend { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 4px 6px;
|
|
472
|
+
margin: 12px 0 0; font-size: 12px; line-height: 18px; color: var(--ink-3); }
|
|
473
|
+
.key { display: inline-block; width: 12px; height: 9px; border-radius: 3px; border: 1.5px solid; }
|
|
474
|
+
.legend .key:not(:first-child) { margin-left: 8px; }
|
|
475
|
+
.key.read { border-color: color-mix(in srgb, var(--read) 60%, transparent); }
|
|
476
|
+
.key.held { border-color: var(--pass-solid); background: color-mix(in srgb, var(--pass-solid) 14%, transparent); }
|
|
477
|
+
.key.failed { border-color: var(--fail-solid); background: color-mix(in srgb, var(--fail-solid) 14%, transparent); }
|
|
478
|
+
|
|
479
|
+
/* Facts, as a quiet grid. */
|
|
480
|
+
dl.facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); gap: 16px 24px; margin: 0;
|
|
481
|
+
padding: 20px; }
|
|
482
|
+
dl.facts div { min-width: 0; }
|
|
483
|
+
dl.facts div.wide { grid-column: 1 / -1; }
|
|
484
|
+
dl.facts dt { font-size: 12px; line-height: 18px; color: var(--ink-3); margin-bottom: 2px; }
|
|
485
|
+
dl.facts dd { margin: 0; font-size: 14px; line-height: 20px; overflow-wrap: break-word; }
|
|
486
|
+
dl.facts dd.mono { font-size: 13px; }
|
|
487
|
+
footer.foot { margin-top: 56px; font-size: 12px; line-height: 18px; color: var(--ink-3); display: flex;
|
|
488
|
+
flex-wrap: wrap; gap: 4px 16px; }
|
|
489
|
+
|
|
490
|
+
/* The replay core: the "Replayed in 4 s, $0" tag, and healed steps with their frames side by side. */
|
|
491
|
+
.heals { padding: 4px; }
|
|
492
|
+
.heal { padding: 16px; border-radius: var(--radius-container); }
|
|
493
|
+
.heal-title { margin: 0; font-weight: var(--weight-medium); overflow-wrap: anywhere; }
|
|
494
|
+
.heal-why { display: flex; flex-wrap: wrap; gap: 0 12px; margin: 2px 0 0; font-size: 13px; line-height: 20px;
|
|
495
|
+
color: var(--ink-3); overflow-wrap: anywhere; }
|
|
496
|
+
.heal-frames { display: grid; grid-template-columns: repeat(2, minmax(0, 180px)); gap: 16px; margin: 16px 0 0; }
|
|
497
|
+
.heal-frames figure { margin: 0; }
|
|
498
|
+
.heal-frames figcaption { margin-bottom: 8px; font-size: 12px; line-height: 18px; font-weight: var(--weight-medium);
|
|
499
|
+
color: var(--ink-3); }
|
|
500
|
+
.heal-frames .phone { padding: 4px; border-radius: 30px; }
|
|
501
|
+
.heal-frames .phone img { border-radius: 26px; }
|
|
502
|
+
.heal-review { display: grid; gap: 8px; margin: 16px 0 0; }
|
|
503
|
+
.heal-review div { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; min-width: 0; }
|
|
504
|
+
.heal-review .label { width: 96px; font-size: 13px; font-weight: var(--weight-medium); }
|
|
505
|
+
.heal-review pre.cmd { padding: 6px 12px; font-size: 12px; }
|
|
506
|
+
|
|
507
|
+
/* Failure evidence (evidence.py): the crash's stack and the app's log around the failure, beside the step. */
|
|
508
|
+
.evidence { display: grid; gap: 12px; min-width: 0; }
|
|
509
|
+
.ev-part { min-width: 0; padding: 12px 16px; border-radius: var(--radius-container); background: var(--sunken); }
|
|
510
|
+
.ev-part.fail { background: color-mix(in srgb, var(--fail) 7%, transparent); }
|
|
511
|
+
.ev-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; margin: 0; font-size: 14px;
|
|
512
|
+
line-height: 20px; }
|
|
513
|
+
.ev-head strong { font-weight: var(--weight-medium); color: var(--ink); overflow-wrap: anywhere; }
|
|
514
|
+
.ev-head span { font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
515
|
+
.ev-head a { margin-left: auto; font-size: 13px; font-weight: var(--weight-medium); }
|
|
516
|
+
.ev-msg { margin: 4px 0 0; font: 13px/20px var(--font-mono); color: var(--ink); overflow-wrap: anywhere; }
|
|
517
|
+
.ev-note { margin: 8px 0 0; font-size: 13px; line-height: 20px; color: var(--ink-3); overflow-wrap: anywhere; }
|
|
518
|
+
ol.frames { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 2px; }
|
|
519
|
+
ol.frames li { display: grid; grid-template-columns: 20px minmax(96px, 168px) minmax(0, 1fr); gap: 12px;
|
|
520
|
+
align-items: baseline; font: 12px/18px var(--font-mono); color: var(--ink-3); }
|
|
521
|
+
ol.frames li .fn { text-align: right; font-variant-numeric: tabular-nums; }
|
|
522
|
+
ol.frames li .fimg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
523
|
+
ol.frames li code { color: var(--ink-2); overflow-wrap: anywhere; }
|
|
524
|
+
ol.frames li.own, ol.frames li.own code { color: var(--ink); }
|
|
525
|
+
ol.frames li .floc { display: block; color: var(--ink-3); }
|
|
526
|
+
.ev-lines { margin: 8px 0 0; padding: 0; list-style: none; display: grid; font: 12px/18px var(--font-mono); }
|
|
527
|
+
.ev-lines li { display: grid; grid-template-columns: 64px 44px minmax(0, 1fr); gap: 8px; color: var(--ink-2); }
|
|
528
|
+
.ev-lines li .t { text-align: right; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
529
|
+
.ev-lines li .lv { color: var(--ink-3); }
|
|
530
|
+
.ev-lines li.error .lv, .ev-lines li.fault .lv { color: var(--fail); }
|
|
531
|
+
.ev-lines li.error .tx, .ev-lines li.fault .tx { color: var(--ink); }
|
|
532
|
+
.ev-lines li .tx { overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
533
|
+
.ev-lines li.at { display: flex; align-items: center; gap: 8px; margin: 4px 0; color: var(--fail);
|
|
534
|
+
font-family: var(--font-sans); font-weight: var(--weight-medium); }
|
|
535
|
+
.ev-lines li.at::after { content: ""; flex: 1; height: 1px; background: color-mix(in srgb, var(--fail) 40%, transparent); }
|
|
536
|
+
ol.steps > li.failing { background: color-mix(in srgb, var(--fail) 7%, transparent); }
|
|
537
|
+
ol.steps > li.ev-row { display: block; padding: 4px 12px 12px 76px; }
|
|
538
|
+
.cause { margin: 12px 0 0; font-size: 14px; line-height: 22px; color: var(--ink-2); }
|
|
539
|
+
.cause strong { margin-right: 8px; font-weight: var(--weight-medium); color: var(--ink); }
|
|
540
|
+
details.proof { min-width: 0; }
|
|
541
|
+
details.proof > summary { cursor: pointer; font-size: 13px; line-height: 20px; color: var(--ink-2); }
|
|
542
|
+
details.proof > summary span { margin-left: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
543
|
+
table.net { margin: 8px 0 0; border-collapse: collapse; font: 12px/18px var(--font-mono); color: var(--ink-2); }
|
|
544
|
+
table.net td { padding: 0 16px 0 0; overflow-wrap: anywhere; }
|
|
545
|
+
.judged { margin: 8px 0 0; font-size: 13px; line-height: 20px; color: var(--ink-3); overflow-wrap: anywhere; }
|
|
546
|
+
|
|
547
|
+
@media (max-width: 640px) {
|
|
548
|
+
main { padding: 20px 16px 48px; }
|
|
549
|
+
.heal-review .label { width: auto; }
|
|
550
|
+
header.head { padding-top: 24px; }
|
|
551
|
+
h1 { font-size: 22px; line-height: 28px; }
|
|
552
|
+
.lede { font-size: 14px; line-height: 22px; }
|
|
553
|
+
ul.meta { gap: 6px 16px; }
|
|
554
|
+
ol.expect li { padding: 12px; }
|
|
555
|
+
dl.facts { padding: 16px; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
556
|
+
ol.steps > li.ev-row { padding: 4px 4px 12px; }
|
|
557
|
+
ol.frames li { grid-template-columns: 20px minmax(0, 1fr); }
|
|
558
|
+
ol.frames li .fimg { display: none; }
|
|
559
|
+
.ev-lines li { grid-template-columns: 56px minmax(0, 1fr); }
|
|
560
|
+
.ev-lines li .lv { display: none; }
|
|
561
|
+
.brand-id { display: none; }
|
|
562
|
+
}
|
|
563
|
+
@media (max-width: 480px) { dl.facts { grid-template-columns: minmax(0, 1fr); } }
|
|
564
|
+
@media print {
|
|
565
|
+
:root { --bezel: var(--ink); }
|
|
566
|
+
body { font-size: 12px; background: white; }
|
|
567
|
+
main { max-width: none; padding: 0; }
|
|
568
|
+
.mark .mark-light { display: block; } .mark .mark-dark { display: none; }
|
|
569
|
+
section, li, .result, .shot { break-inside: avoid; }
|
|
570
|
+
a { text-decoration: none; }
|
|
571
|
+
* { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
|
572
|
+
}
|
|
573
|
+
""" # noqa: E501
|
|
574
|
+
BASE_STYLE = ("".join(FONT_FACES) + tokens.STYLESHEET + tokens.PRINT_STYLESHEET
|
|
575
|
+
+ COMPONENTS.replace("PASS_SOLID", tokens.LIGHT["green"]).replace("FAIL_SOLID", tokens.LIGHT["coral"]))
|
|
576
|
+
|
|
577
|
+
VERIFY_STYLE = """
|
|
578
|
+
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 40px 48px; align-items: start;
|
|
579
|
+
margin-top: 40px; }
|
|
580
|
+
.layout > section { grid-column: 1; margin: 0; min-width: 0; }
|
|
581
|
+
.shot { grid-column: 2; position: sticky; top: 24px; width: 320px; }
|
|
582
|
+
.shot .phone { width: 320px; border-radius: 50px; }
|
|
583
|
+
.shot .phone img { border-radius: 44px; }
|
|
584
|
+
.snaps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 0; }
|
|
585
|
+
.snaps figure { margin: 0; display: grid; gap: 6px; }
|
|
586
|
+
.snaps img { width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--line); }
|
|
587
|
+
.snaps figcaption { font-size: 13px; line-height: 20px; color: var(--ink-2); }
|
|
588
|
+
.notes { margin: 12px 4px 0; display: grid; gap: 4px; }
|
|
589
|
+
.notes p { margin: 0; font-size: 13px; line-height: 20px; color: var(--ink-2); overflow-wrap: anywhere; }
|
|
590
|
+
.seg { display: flex; width: max-content; margin: 0 auto 12px; padding: 2px; gap: 2px;
|
|
591
|
+
border-radius: var(--radius-full); background: var(--fill-input); }
|
|
592
|
+
.seg label { display: inline-flex; align-items: center; height: 28px; padding: 0 12px;
|
|
593
|
+
border-radius: var(--radius-full); font-size: 13px; font-weight: var(--weight-medium); color: var(--ink-2);
|
|
594
|
+
cursor: pointer; user-select: none; -webkit-user-select: none;
|
|
595
|
+
transition: color var(--duration-short) var(--ease-standard), background-color var(--duration-short) var(--ease-standard); }
|
|
596
|
+
#frame-ax:checked ~ .seg label[for=frame-ax], #frame-plain:checked ~ .seg label[for=frame-plain] {
|
|
597
|
+
background: var(--surface); color: var(--ink); box-shadow: var(--shadow-thumb); }
|
|
598
|
+
#frame-ax:focus-visible ~ .seg label[for=frame-ax], #frame-plain:focus-visible ~ .seg label[for=frame-plain] {
|
|
599
|
+
outline: 2px solid var(--focus-ring); outline-offset: 1px; }
|
|
600
|
+
#frame-ax:checked ~ .phone .plain, #frame-plain:checked ~ .phone .ax { display: none; }
|
|
601
|
+
#frame-plain:checked ~ .legend { visibility: hidden; }
|
|
602
|
+
.callout { margin: 16px 0 0; max-width: 720px; padding: 12px 16px; border-radius: var(--radius-container);
|
|
603
|
+
font-size: 14px; line-height: 20px; background: color-mix(in srgb, var(--tone) 10%, transparent);
|
|
604
|
+
overflow-wrap: anywhere; }
|
|
605
|
+
.callout p { margin: 0; } .callout p + p { margin-top: 4px; color: var(--ink-2); }
|
|
606
|
+
ol.steps { list-style: none; margin: 0; padding: 4px; }
|
|
607
|
+
ol.steps > li { display: grid; grid-template-columns: 18px 40px minmax(0, 1fr) auto; gap: 14px; align-items: center;
|
|
608
|
+
padding: 8px 12px; border-radius: var(--radius-container); }
|
|
609
|
+
.step-n { font: 12px/1 var(--font-mono); color: var(--ink-3); text-align: right; }
|
|
610
|
+
.thumb { width: 40px; aspect-ratio: 3 / 4; border-radius: var(--radius-inner); overflow: hidden;
|
|
611
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 8%, transparent); background: var(--sunken); }
|
|
612
|
+
.thumb img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top; }
|
|
613
|
+
.op { font-weight: var(--weight-medium); overflow-wrap: anywhere; }
|
|
614
|
+
.step-meta { font-size: 13px; line-height: 20px; color: var(--ink-3); overflow-wrap: anywhere; }
|
|
615
|
+
.at { font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
616
|
+
dl.facts dd .sub { display: block; margin-top: 2px; font-size: 12px; line-height: 18px; color: var(--ink-3); }
|
|
617
|
+
.note { margin: 0; padding: 16px 20px; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink-2); }
|
|
618
|
+
.note-meta { display: block; margin-top: 8px; font-size: 13px; color: var(--ink-3); }
|
|
619
|
+
@media (max-width: 860px) {
|
|
620
|
+
.layout { grid-template-columns: minmax(0, 1fr); margin-top: 32px; }
|
|
621
|
+
.shot { grid-column: 1; grid-row: auto !important; position: static; justify-self: center; max-width: 100%; }
|
|
622
|
+
.shot .phone { max-width: 100%; }
|
|
623
|
+
}
|
|
624
|
+
@media (max-width: 640px) {
|
|
625
|
+
ol.steps > li { grid-template-columns: 40px minmax(0, 1fr) auto; gap: 12px; }
|
|
626
|
+
.step-n { display: none; }
|
|
627
|
+
}
|
|
628
|
+
@media print { .seg { display: none; } .layout { grid-template-columns: minmax(0, 1fr) 220px; gap: 28px 32px; }
|
|
629
|
+
.shot, .shot .phone { width: 220px; position: static; } }
|
|
630
|
+
""" # noqa: E501
|
|
631
|
+
|
|
632
|
+
STYLE = BASE_STYLE + VERIFY_STYLE
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def replay_chip(replay, *, healed=0):
|
|
636
|
+
replay = replay if isinstance(replay, dict) else {}
|
|
637
|
+
mode = replay.get("mode")
|
|
638
|
+
if mode in ("replayed", "healed"):
|
|
639
|
+
cost = float(replay.get("costUsd") or 0)
|
|
640
|
+
words = f"Replayed in {seconds_text(replay.get('seconds'))}, " + (f"${cost:.2f}" if cost >= .005 else "$0")
|
|
641
|
+
chip = f'<span class="tag pass">{icon("check")}{_e(words)}</span>'
|
|
642
|
+
count = healed or replay.get("healed") or 0
|
|
643
|
+
if count:
|
|
644
|
+
chip += f'<span class="tag tone review">{_e(count)} step{"" if count == 1 else "s"} healed</span>'
|
|
645
|
+
return chip
|
|
646
|
+
if replay.get("saved"):
|
|
647
|
+
return '<span class="tag">Recorded for replay</span>'
|
|
648
|
+
return ""
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def heal_title(item):
|
|
652
|
+
"""'"Continue" is now "Next"' when one labelled step became another, else 'Tap "Continue" → Tap "Next"'."""
|
|
653
|
+
old, new = item.get("old") or [], item.get("new") or []
|
|
654
|
+
if len(old) == 1 and len(new) == 1 and old[0].get("label") and new[0].get("label") \
|
|
655
|
+
and old[0].get("op") == new[0].get("op"):
|
|
656
|
+
return f'"{old[0]["label"]}" is now "{new[0]["label"]}"'
|
|
657
|
+
if not old and new:
|
|
658
|
+
return f"Added: {item.get('to')}"
|
|
659
|
+
return f"{item.get('from')} → {item.get('to')}"
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def heal_command(source):
|
|
663
|
+
"""The name `mobster test heal` takes for a check: its file's stem in .mobster/checks, else its path."""
|
|
664
|
+
if not source:
|
|
665
|
+
return "CHECK"
|
|
666
|
+
path = Path(source)
|
|
667
|
+
return path.stem if path.parent.name == "checks" and path.parent.parent.name == ".mobster" else home(str(path))
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def healed_section(healed, base_dir, source=None, *, heading="Healed steps", ident="healed"):
|
|
671
|
+
"""The heal diff review: each healed step, why, its frames before and after, and how to accept or reject it."""
|
|
672
|
+
items = [item for item in healed or () if isinstance(item, dict)]
|
|
673
|
+
if not items:
|
|
674
|
+
return ""
|
|
675
|
+
name = heal_command(source)
|
|
676
|
+
out = [f'<section aria-labelledby="{_e(ident)}"><h2 id="{_e(ident)}">{_e(heading)}<span class="count">'
|
|
677
|
+
f'{len(items)}</span></h2><div class="card heals">']
|
|
678
|
+
for item in items:
|
|
679
|
+
out.append('<div class="heal">')
|
|
680
|
+
out.append(f'<p class="heal-title">Healed: {_e(heal_title(item))}</p>')
|
|
681
|
+
why = [f"Step {item.get('step')}" if item.get("step") else None, item.get("why")]
|
|
682
|
+
if any(why):
|
|
683
|
+
out.append('<p class="heal-why">' + "".join(f"<span>{_e(part)}</span>" for part in why if part) + "</p>")
|
|
684
|
+
frames = item.get("frames") or {}
|
|
685
|
+
shots = []
|
|
686
|
+
for label, key in (("Before", "before"), ("After", "after")):
|
|
687
|
+
path = frames.get(key)
|
|
688
|
+
url = _data_url(Path(base_dir) / path if path and not Path(path).is_absolute() else Path(path),
|
|
689
|
+
THUMB_WIDTH * 2) if path else None
|
|
690
|
+
if url:
|
|
691
|
+
shots.append(f'<figure><figcaption>{label}</figcaption><div class="phone"><img src="{url}" '
|
|
692
|
+
f'alt="The screen {label.lower()} the heal"></div></figure>')
|
|
693
|
+
if shots:
|
|
694
|
+
out.append('<div class="heal-frames">' + "".join(shots) + "</div>")
|
|
695
|
+
out.append('<div class="heal-review">'
|
|
696
|
+
f'<div><span class="label">Accept heal</span>{command(f"mobster test heal --accept {name}")}'
|
|
697
|
+
'</div>'
|
|
698
|
+
f'<div><span class="label">Keep failing</span>{command(f"mobster test heal --reject {name}")}'
|
|
699
|
+
"</div></div>")
|
|
700
|
+
out.append("</div>")
|
|
701
|
+
out.append("</div></section>")
|
|
702
|
+
return "".join(out)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
SNAPSHOT_WORDS = {"matched": "matched the baseline", "changed": "changed", "size_changed": "has a different size",
|
|
706
|
+
"recorded": "baseline recorded", "updated": "baseline updated"}
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def snapshot_section(snapshot, run_dir):
|
|
710
|
+
"""The screenshot comparison: one line, then the baseline, the current screen and the diff side by side."""
|
|
711
|
+
if not isinstance(snapshot, dict) or not snapshot.get("status"):
|
|
712
|
+
return ""
|
|
713
|
+
line = f"{snapshot.get('name')} {SNAPSHOT_WORDS.get(snapshot['status'], snapshot['status'])}"
|
|
714
|
+
if snapshot["status"] in ("matched", "changed"):
|
|
715
|
+
line += (f": {snapshot.get('changedPercent', 0):.2f} % of the screen differs "
|
|
716
|
+
f"(limit {snapshot.get('thresholdPercent'):g} %)")
|
|
717
|
+
figures = []
|
|
718
|
+
for key, label in (("baseline", "Baseline"), ("current", "Current"), ("diff", "Diff")):
|
|
719
|
+
url = _data_url(Path(run_dir) / snapshot[key], FRAME_WIDTH, 80) if snapshot.get(key) else None
|
|
720
|
+
if url:
|
|
721
|
+
figures.append(f'<figure><img src="{url}" alt="{label}: {_e(snapshot.get("name"))}">'
|
|
722
|
+
f"<figcaption>{label}</figcaption></figure>")
|
|
723
|
+
return (f'<h2>Screenshot</h2><div class="card"><p class="lede">{_e(line)}</p>'
|
|
724
|
+
+ (f'<div class="snaps">{"".join(figures)}</div>' if figures else "") + "</div>")
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def render_html(result, run_dir):
|
|
728
|
+
"""The report for ``result`` (§5.5), as one HTML string."""
|
|
729
|
+
run_dir = Path(run_dir)
|
|
730
|
+
verdict = result.get("verdict")
|
|
731
|
+
word, tone = VERDICTS.get(verdict, VERDICTS["couldnt_run"])
|
|
732
|
+
reason = result.get("reason") or {}
|
|
733
|
+
check = result.get("check") or {}
|
|
734
|
+
device = result.get("device") or {}
|
|
735
|
+
app = result.get("app") or {}
|
|
736
|
+
assertions = result.get("assertions") or []
|
|
737
|
+
held = sum(bool(item.get("ok")) for item in assertions)
|
|
738
|
+
parts = [
|
|
739
|
+
"<!doctype html>",
|
|
740
|
+
'<html lang="en"><head><meta charset="utf-8">',
|
|
741
|
+
f'<meta http-equiv="Content-Security-Policy" content="{CSP}">',
|
|
742
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
743
|
+
'<meta name="color-scheme" content="light dark">',
|
|
744
|
+
f"<title>{_e(VERDICTS.get(verdict, VERDICTS['couldnt_run'])[0])}: {_e(check.get('name'))}</title>",
|
|
745
|
+
f"<style>{STYLE}</style></head><body><main>",
|
|
746
|
+
brand("verify", result.get("run_id")),
|
|
747
|
+
'<header class="head"><div class="chips">', verdict_chip(word, tone),
|
|
748
|
+
replay_chip(result.get("replay"), healed=len(result.get("healed") or ())),
|
|
749
|
+
]
|
|
750
|
+
parts.append(f'</div><h1>{_e(check.get("name"))}</h1>')
|
|
751
|
+
if assertions and reason.get("class") in (None, "assertion"):
|
|
752
|
+
lede = (f"All {len(assertions)} expectations held" if held == len(assertions) else
|
|
753
|
+
f"{len(assertions) - held} of {len(assertions)} expectations failed")
|
|
754
|
+
parts.append(f'<p class="lede">{_e(lede)}</p>')
|
|
755
|
+
# Why, when the assertions don't say (couldn't run, needs review, blocked), and the fix.
|
|
756
|
+
failing = [item for item in assertions if not item.get("ok")]
|
|
757
|
+
lines = []
|
|
758
|
+
if reason.get("class") not in (None, "assertion") or not assertions:
|
|
759
|
+
lines.append(reason.get("message") or result.get("summary"))
|
|
760
|
+
if result.get("summary") and result.get("summary") != reason.get("message") and not failing:
|
|
761
|
+
lines.append(result.get("summary"))
|
|
762
|
+
if reason.get("fix"):
|
|
763
|
+
lines.append(reason.get("fix"))
|
|
764
|
+
lines = [line for line in lines if line]
|
|
765
|
+
if lines:
|
|
766
|
+
parts.append(f'<div class="callout {tone}">' + "".join(f"<p>{_e(line)}</p>" for line in lines) + "</div>")
|
|
767
|
+
parts.append(cause_line(result.get("step_proof")))
|
|
768
|
+
device_line = ", ".join(filter(None, [device.get("type"), device.get("runtime")])) or dots(device.get("name"))
|
|
769
|
+
app_line = " ".join(filter(None, [app.get("name") or app.get("bundle_id"), app.get("version")]))
|
|
770
|
+
app_icon = _png_url(run_dir / "app-icon.png")
|
|
771
|
+
meta = [(icon("sim"), device_line),
|
|
772
|
+
(f'<img class="app-icon" src="{app_icon}" alt="">' if app_icon else icon("app"), app_line),
|
|
773
|
+
(icon("clock"), seconds_text(result.get("seconds")) if result.get("seconds") is not None else None),
|
|
774
|
+
(icon("calendar"), when(run_id=result.get("run_id"))), (icon("dash"), MODES.get(result.get("mode")))]
|
|
775
|
+
parts.append('<ul class="meta">' + "".join(f"<li>{mark}{_e(dots(value))}</li>" for mark, value in meta
|
|
776
|
+
if value) + "</ul></header>")
|
|
777
|
+
|
|
778
|
+
frames = result.get("frames") or []
|
|
779
|
+
ax = next((f for f in reversed(frames) if f.endswith("-verdict-ax.jpg")), None)
|
|
780
|
+
plain = next((f for f in reversed(frames) if f.endswith("-verdict.jpg")), None)
|
|
781
|
+
ax_url = _data_url(run_dir / ax, FRAME_WIDTH, 80) if ax else None
|
|
782
|
+
plain_url = _data_url(run_dir / plain, FRAME_WIDTH, 80) if plain else None
|
|
783
|
+
|
|
784
|
+
has_steps = bool(result.get("steps"))
|
|
785
|
+
healed = healed_section(result.get("healed"), run_dir, check.get("source"))
|
|
786
|
+
evidence = result.get("evidence") or {}
|
|
787
|
+
failing = evidence.get("step") if has_steps else None
|
|
788
|
+
pane = evidence_pane(evidence, step=failing)
|
|
789
|
+
alone = bool(pane) and not any(step.get("index") == failing for step in result.get("steps") or ())
|
|
790
|
+
sections = (2 + has_steps + bool(result.get("repro")) + bool(result.get("agent")) + bool(healed)
|
|
791
|
+
+ alone)
|
|
792
|
+
parts.append('<div class="layout"><section aria-labelledby="expectations"><h2 id="expectations">Expectations'
|
|
793
|
+
"</h2>")
|
|
794
|
+
if assertions:
|
|
795
|
+
parts.append('<div class="card">' + expectations(assertions, frame=bool(ax_url)) + "</div>")
|
|
796
|
+
elif check.get("expect") and not result.get("findings"):
|
|
797
|
+
parts.append('<p class="empty">Not evaluated: the run ended before the verdict read.</p>')
|
|
798
|
+
elif result.get("business") is not None:
|
|
799
|
+
parts.append('<p class="empty">This check has no accessibility-tree expectations.</p>')
|
|
800
|
+
else:
|
|
801
|
+
parts.append('<p class="empty">The check has no expectations.</p>')
|
|
802
|
+
if result.get("business") is not None:
|
|
803
|
+
parts.append(business_section(result))
|
|
804
|
+
if result.get("findings"):
|
|
805
|
+
parts.append("<h2>Findings</h2>")
|
|
806
|
+
parts.append('<div class="card"><ol class="expect">' + "".join(
|
|
807
|
+
f'<li class="{"failed" if item.get("level") == "fail" else "review"}"><code class="assert">'
|
|
808
|
+
f'{_e((item.get("guideline") + " " if item.get("guideline") else "") + str(item.get("id")))}'
|
|
809
|
+
f'</code><span class="observed">{_e(FINDING_WORDS.get(item.get("level"), ""))}{_e(item.get("message"))}'
|
|
810
|
+
f' Evidence: {_e(item.get("evidence"))}</span></li>'
|
|
811
|
+
for item in result["findings"]) + "</ol></div>")
|
|
812
|
+
parts.append(snapshot_section(result.get("snapshot"), run_dir))
|
|
813
|
+
notes = []
|
|
814
|
+
if result.get("stable") is False:
|
|
815
|
+
notes.append("The screen was still changing when Mobster read it.")
|
|
816
|
+
if result.get("alert"):
|
|
817
|
+
notes.append(f"An alert was showing: {result.get('alert')}")
|
|
818
|
+
if notes:
|
|
819
|
+
parts.append('<div class="notes">' + "".join(f"<p>{_e(note)}</p>" for note in notes) + "</div>")
|
|
820
|
+
parts.append("</section>")
|
|
821
|
+
if alone:
|
|
822
|
+
parts.append(f'<section aria-labelledby="logs"><h2 id="logs">Logs and crash</h2>{pane}</section>')
|
|
823
|
+
if ax_url or plain_url:
|
|
824
|
+
parts.append(f'<aside class="shot" style="grid-row: 1 / span {sections}" aria-labelledby="frame">'
|
|
825
|
+
'<h2 class="sr" id="frame">The verdict frame</h2>')
|
|
826
|
+
if ax_url and plain_url:
|
|
827
|
+
parts.append('<input class="sr" type="radio" name="frame" id="frame-ax" checked '
|
|
828
|
+
'aria-label="The frame with the overlay">'
|
|
829
|
+
'<input class="sr" type="radio" name="frame" id="frame-plain" aria-label="The plain frame">'
|
|
830
|
+
'<div class="seg" aria-hidden="true"><label for="frame-ax">Outlined</label>'
|
|
831
|
+
'<label for="frame-plain">Plain</label></div>')
|
|
832
|
+
parts.append('<div class="phone">')
|
|
833
|
+
if ax_url:
|
|
834
|
+
parts.append(f'<img class="ax" src="{ax_url}" alt="The screen at the verdict, each asserted element '
|
|
835
|
+
f'outlined and numbered as in the list: green held, red failed">')
|
|
836
|
+
if plain_url:
|
|
837
|
+
parts.append(f'<img class="plain" src="{plain_url}" alt="The screen at the verdict">')
|
|
838
|
+
judged = (result.get("step_proof") or {}).get("judged") or {}
|
|
839
|
+
caption = (f'<p class="judged">Judged on {_e(Path(judged["frame"]).stem)}'
|
|
840
|
+
+ (f', screen {_e(judged["treeHash"][:12])}' if judged.get("treeHash") else "")
|
|
841
|
+
+ (f", at {_e(_seconds(judged['verdictAtMs']))}" if judged.get("verdictAtMs") is not None else "")
|
|
842
|
+
+ "</p>") if judged.get("frame") else ""
|
|
843
|
+
parts.append("</div>" + (OVERLAY_LEGEND if ax_url else "") + caption + "</aside>")
|
|
844
|
+
|
|
845
|
+
if healed:
|
|
846
|
+
parts.append(healed)
|
|
847
|
+
if has_steps:
|
|
848
|
+
parts.append('<section aria-labelledby="steps"><h2 id="steps">Steps</h2>')
|
|
849
|
+
launch = next((f for f in frames if f.endswith("-launch.jpg")), None)
|
|
850
|
+
rows = []
|
|
851
|
+
by_step = {entry.get("index"): entry for entry in (result.get("step_proof") or {}).get("steps") or ()}
|
|
852
|
+
if launch:
|
|
853
|
+
rows.append((launch, "Launched the app", None, None, None))
|
|
854
|
+
for step in result.get("steps") or []:
|
|
855
|
+
target = step.get("target") or {}
|
|
856
|
+
who = ", ".join(f"{key} {_short(value, 60)}" for key, value in (
|
|
857
|
+
("id", target.get("id")), ("label", target.get("label")), ("role", target.get("role"))) if value)
|
|
858
|
+
changed = step.get("changed")
|
|
859
|
+
meta_line = ", ".join(filter(None, [who or None, None if changed is None else (
|
|
860
|
+
"the screen changed" if changed else "no visible change")]))
|
|
861
|
+
rows.append((step.get("frame"), step.get("text") or step.get("op"), meta_line,
|
|
862
|
+
f"{(step.get('at_ms') or 0) / 1000:.1f} s", step.get("index")))
|
|
863
|
+
if rows and has_steps:
|
|
864
|
+
parts.append('<div class="card"><ol class="steps">')
|
|
865
|
+
for number, (frame, text, meta_line, at, index) in enumerate(rows, 1):
|
|
866
|
+
url = _data_url(run_dir / frame, THUMB_WIDTH) if frame else None
|
|
867
|
+
image = (f'<div class="thumb"><img src="{url}" alt=""></div>' if url
|
|
868
|
+
else '<div class="thumb noimg"></div>')
|
|
869
|
+
here = bool(pane) and not alone and index == failing
|
|
870
|
+
mark = ' class="failing"' if here else ""
|
|
871
|
+
parts.append(f'<li{mark}><span class="step-n">{number}</span>{image}<div><div class="op">{_e(text)}</div>'
|
|
872
|
+
+ (f'<div class="step-meta">{_e(meta_line)}</div>' if meta_line else "") + "</div>"
|
|
873
|
+
+ (f'<span class="at">{_e(at)}</span>' if at else "<span></span>") + "</li>")
|
|
874
|
+
if here:
|
|
875
|
+
parts.append(f'<li class="ev-row" aria-label="Logs and crash">{pane}</li>')
|
|
876
|
+
detail = proof_pane(by_step.get(index), title="Proof for this step", opened=bool(here))
|
|
877
|
+
if detail:
|
|
878
|
+
parts.append(f'<li class="ev-row" aria-label="Proof for step {_e(index)}">{detail}</li>')
|
|
879
|
+
tail = proof_pane((result.get("step_proof") or {}).get("after"), title="Until the verdict")
|
|
880
|
+
if tail:
|
|
881
|
+
parts.append(f'<li class="ev-row" aria-label="Proof until the verdict">{tail}</li>')
|
|
882
|
+
parts.append("</ol></div></section>")
|
|
883
|
+
|
|
884
|
+
timing = result.get("timing") or {}
|
|
885
|
+
facts = [
|
|
886
|
+
("App", " ".join(filter(None, [app.get("name"), app.get("version"), f"({app.get('bundle_id')})"
|
|
887
|
+
if app.get("bundle_id") else None])) or None, ""),
|
|
888
|
+
("Simulator", device_line, ""),
|
|
889
|
+
("Mode", {"smart": "Smart", "keyless": "Key-less (your coding agent drove)",
|
|
890
|
+
"launch": "Launch-only"}.get(result.get("mode"), result.get("mode")), ""),
|
|
891
|
+
("Duration", f"{result.get('seconds', 0)} s", ("sub", ", ".join(
|
|
892
|
+
f"{key}\u00a0{value}\u00a0s" for key, value in timing.items()))),
|
|
893
|
+
("Model cost", f"${result.get('cost_usd'):.4f}" if result.get("cost_usd") else "$0", ""),
|
|
894
|
+
("Replay", _replay_fact(result.get("replay")), ""),
|
|
895
|
+
("Stable read", {True: "Yes", False: "No, the screen was still changing", None: "Not read"}.get(
|
|
896
|
+
result.get("stable")), ""),
|
|
897
|
+
("Run", result.get("run_id"), "mono"),
|
|
898
|
+
("Mobster", (result.get("mobster") or {}).get("version"), ""),
|
|
899
|
+
("App path", home(app.get("path")), "mono wide"),
|
|
900
|
+
]
|
|
901
|
+
parts.append('<section aria-labelledby="run"><h2 id="run">Run</h2><div class="card"><dl class="facts">')
|
|
902
|
+
for name, value, kind in facts:
|
|
903
|
+
if value:
|
|
904
|
+
sub = f'<span class="sub">{_e(kind[1])}</span>' if isinstance(kind, tuple) and kind[1] else ""
|
|
905
|
+
kind = "" if isinstance(kind, tuple) else kind
|
|
906
|
+
wide = ' class="wide"' if "wide" in kind else ""
|
|
907
|
+
mono = ' class="mono"' if "mono" in kind else ""
|
|
908
|
+
parts.append(f"<div{wide}><dt>{_e(name)}</dt><dd{mono}>{_e(value)}{sub}</dd></div>")
|
|
909
|
+
parts.append("</dl></div></section>")
|
|
910
|
+
|
|
911
|
+
if result.get("repro"):
|
|
912
|
+
parts.append(f'<section aria-labelledby="again"><h2 id="again">Run it again</h2>'
|
|
913
|
+
f'{command(home(result["repro"]))}</section>')
|
|
914
|
+
agent = result.get("agent")
|
|
915
|
+
if agent:
|
|
916
|
+
ended = "Smart ended " + str(agent.get("status")) + (f" after {agent.get('turns')} turns"
|
|
917
|
+
if agent.get("turns") else "")
|
|
918
|
+
parts.append('<section aria-labelledby="note"><h2 id="note">The model\'s note (not the verdict)</h2>'
|
|
919
|
+
f'<div class="card"><p class="note">{_e(agent.get("note") or "No note.")}'
|
|
920
|
+
f'<span class="note-meta">{_e(ended)}</span></p></div></section>')
|
|
921
|
+
parts.append(f'</div><footer class="foot"><span>Mobster {_e((result.get("mobster") or {}).get("version"))}</span>'
|
|
922
|
+
f'<span>Run <span class="mono">{_e(result.get("run_id"))}</span></span></footer>')
|
|
923
|
+
parts.append("</main></body></html>")
|
|
924
|
+
return "\n".join(parts)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _replay_fact(replay):
|
|
928
|
+
if not isinstance(replay, dict) or not replay:
|
|
929
|
+
return None
|
|
930
|
+
if replay.get("status") == "proposal_unavailable":
|
|
931
|
+
return replay.get("reason") or "The healed replay path could not be staged for review."
|
|
932
|
+
mode = replay.get("mode")
|
|
933
|
+
if mode in ("replayed", "healed"):
|
|
934
|
+
parts = [f"{replay.get('replayed') or 0} of {replay.get('steps') or 0} steps with no model"]
|
|
935
|
+
if replay.get("healed"):
|
|
936
|
+
parts.append(f"{replay['healed']} healed")
|
|
937
|
+
if replay.get("live"):
|
|
938
|
+
parts.append(f"{replay['live']} by the model")
|
|
939
|
+
return ", ".join(parts)
|
|
940
|
+
if replay.get("saved"):
|
|
941
|
+
return f"Recorded {replay.get('steps') or 0} steps; the next run replays them"
|
|
942
|
+
return f"Smart ran every step ({replay['reason']})" if replay.get("reason") else None
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def write_report(run_dir, result, *, scrub=lambda text: text):
|
|
946
|
+
"""report.html in ``run_dir`` for ``result``. result.json is written by the run itself."""
|
|
947
|
+
run_dir = Path(run_dir)
|
|
948
|
+
text = scrub(render_html(result, run_dir))
|
|
949
|
+
temporary = run_dir / "report.html.tmp"
|
|
950
|
+
temporary.write_text(text, encoding="utf-8")
|
|
951
|
+
os.replace(temporary, run_dir / "report.html")
|
|
952
|
+
return run_dir / "report.html"
|