minitest-cli 0.36.0__tar.gz → 0.37.1__tar.gz
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.
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/.env.example +6 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/PKG-INFO +28 -3
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/README.md +27 -2
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/pyproject.toml +4 -1
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/api/client.py +9 -2
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/_response_errors.py +29 -7
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/apps.py +4 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/batch_helpers.py +22 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/build.py +26 -8
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/build_helpers.py +3 -1
- minitest_cli-0.37.1/src/minitest_cli/commands/draft.py +111 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_asks.py +148 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_conversation.py +84 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_create.py +66 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/draft_feature.py +9 -6
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/draft_feature_apply.py +1 -1
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/draft_feature_conflicts.py +1 -1
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/draft_feature_helpers.py +1 -3
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/draft_feature_show.py +1 -1
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_helpers.py +131 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_legacy.py +40 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_lifecycle.py +103 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_pr.py +49 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_render.py +110 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_update.py +45 -0
- minitest_cli-0.37.1/src/minitest_cli/commands/draft_watch.py +90 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/env_helpers.py +4 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run.py +26 -47
- minitest_cli-0.37.1/src/minitest_cli/commands/run_list.py +55 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_targets.py +61 -11
- minitest_cli-0.37.1/src/minitest_cli/core/app_context.py +68 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/core/config.py +5 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/main.py +2 -1
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/targets.py +9 -1
- minitest_cli-0.37.1/tests/conftest.py +26 -0
- minitest_cli-0.37.1/tests/draft_backend.py +98 -0
- minitest_cli-0.37.1/tests/test_app_context.py +84 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_build_commands.py +1 -1
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_deprecated_commands.py +18 -3
- minitest_cli-0.37.1/tests/test_draft_ask_commands.py +120 -0
- minitest_cli-0.37.1/tests/test_draft_commands.py +167 -0
- minitest_cli-0.37.1/tests/test_draft_conversation_commands.py +133 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_draft_feature_commands.py +1 -1
- minitest_cli-0.37.1/tests/test_draft_pr_commands.py +135 -0
- minitest_cli-0.37.1/tests/test_physical_devices.py +227 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_run_commands.py +7 -7
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/uv.lock +1 -1
- minitest_cli-0.36.0/src/minitest_cli/core/app_context.py +0 -31
- minitest_cli-0.36.0/tests/conftest.py +0 -11
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/.github/workflows/ci.yml +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/.github/workflows/install-scripts.yml +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/.github/workflows/release.yml +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/.gitignore +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/.opencode/skill/release/SKILL.md +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/AGENTS.md +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/RELEASE.md +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/install.ps1 +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/install.sh +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/pyrightconfig.json +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/renovate.json +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/__init__.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/api/__init__.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/api/apps_manager_client.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/api/errors.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/assets/__init__.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/assets/callback.html +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/__init__.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/app_knowledge.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/app_knowledge_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/app_network_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/app_skill.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/app_skill_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/app_skill_secret.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/app_skill_write.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/apps_dependencies.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/apps_get.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/apps_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/apps_table.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/auth.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/auth_api_key.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/batch.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/batch_warnings.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/build_commit.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/build_fix_prompts.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/build_get.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/capabilities.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/commit_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/deprecated_alias.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/env.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/flow_types.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/flow_types_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/flow_types_write.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/init.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/init_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/init_playbook.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/issues.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/issues_fix.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/issues_get.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/issues_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/issues_projection.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/issues_service.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/private_network.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/private_network_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/recording_timeline.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/residential_proxy.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_cancel.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_commit.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_commit_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_display.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_feedback.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_recording.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_recording_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/run_tagged.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/screens.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/screens_detail.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/screens_format.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/screens_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/screens_tree.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/skill.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/tags.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/tags_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_file.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_file_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_file_list.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_profile.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_profile_card.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_profile_default.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_profile_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_profile_list.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/test_profile_update.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/upgrade.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_app_skills.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_bindings.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_camera.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_create.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_criteria.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_criterion_edits.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_delete.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_device_count.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_modify.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_overrides.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_profiles.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_setup_commit.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/user_story_update.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/verdicts.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/verdicts_helpers.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/commands/verdicts_projection.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/core/__init__.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/core/auth.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/core/credentials.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/core/oauth.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/core/tenants.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/core/token_exchange.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/__init__.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/app.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/app_env_vars.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/app_failure.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/app_screen.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/base.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/batch.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/build.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/commit_build.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/draft_feature.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/flow_type.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/issues.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/recording.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/story_run.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/tag.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/models/user_story.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/utils/__init__.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/utils/confirm.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/utils/dependency_sim.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/utils/mermaid.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/utils/output.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/utils/skill_refresh.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/utils/update_check.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/src/minitest_cli/utils/video_frames.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/__init__.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/_commit_transport.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/_network_settings_server.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_api_client_auth.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_app_knowledge_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_app_skill_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_apps_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_apps_dependencies.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_auth.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_auth_api_key.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_auth_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_batch_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_build_fix_prompts.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_build_get_command.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_capabilities_command.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_channel_header.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_code_quality.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_commit_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_deprecated_flags.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_env.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_flow_types_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_init_command.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_issues.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_issues_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_issues_get_command.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_mermaid.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_native_device_types.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_network_errors.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_private_network_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_recording_timeline.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_residential_proxy_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_response_errors.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_run_cancel.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_run_frames_command.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_screens_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_skill_command.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_suite_change_provenance.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_tags_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_test_file_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_test_profile_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_upgrade_command.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_user_story_bindings_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_user_story_commands.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_user_story_setup_commit.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_verdicts.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_verdicts_actionable.py +0 -0
- {minitest_cli-0.36.0 → minitest_cli-0.37.1}/tests/test_version.py +0 -0
|
@@ -16,6 +16,12 @@ MINITEST_API_URL=https://testing-service.dev.minitap.ai
|
|
|
16
16
|
# Development
|
|
17
17
|
MINITEST_APPS_MANAGER_URL=https://apps-manager.dev.minitap.ai
|
|
18
18
|
|
|
19
|
+
# —— Conductor (minitest draft) ——
|
|
20
|
+
# Production
|
|
21
|
+
# MINITEST_CONDUCTOR_URL=https://conductor.app.minitap.ai
|
|
22
|
+
# Development
|
|
23
|
+
MINITEST_CONDUCTOR_URL=https://conductor.dev.minitap.ai
|
|
24
|
+
|
|
19
25
|
# —— Integrations ——
|
|
20
26
|
# Production
|
|
21
27
|
# MINITEST_INTEGRATIONS_URL=https://integrations.minitap.ai
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: minitest-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.37.1
|
|
4
4
|
Summary: Minitest CLI – command-line interface for the Minitest testing platform
|
|
5
5
|
Project-URL: Homepage, https://minitap.ai/
|
|
6
6
|
Project-URL: Source, https://github.com/minitap-ai/minitest-cli
|
|
@@ -131,6 +131,29 @@ targets, or Edge. V1 supports one form factor per OS per run; run phone and
|
|
|
131
131
|
tablet suites separately. Tablet results are labelled `iOS · Tablet` or
|
|
132
132
|
`Android · Tablet`; phone labels stay unchanged.
|
|
133
133
|
|
|
134
|
+
## Physical devices
|
|
135
|
+
|
|
136
|
+
By default builds run on virtual devices (iOS Simulator, Android emulator). If
|
|
137
|
+
physical devices are enabled for your workspace, upload a device build with
|
|
138
|
+
`--physical` and select the physical lane when you start the run:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# iOS: an Ad Hoc or Development .ipa signed for Minitap's iPhones
|
|
142
|
+
BUILD_ID=$(minitest --json --app <app-id> build upload ./MyApp.ipa --physical | jq -r .id)
|
|
143
|
+
minitest --app <app-id> run all --ios-build "$BUILD_ID" --ios-physical
|
|
144
|
+
|
|
145
|
+
# Android: the same works with --android-physical
|
|
146
|
+
minitest --app <app-id> run start --tag smoke --android-build <android-build-id> --android-physical
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- `build upload --physical` checks the build for physical devices instead of
|
|
150
|
+
virtual ones. A Simulator build is rejected (`ios.simulator_only_ipa`), and a
|
|
151
|
+
device build uploaded without `--physical` is rejected with a hint to add it.
|
|
152
|
+
- `--ios-physical` / `--android-physical` need the matching `--ios-build` /
|
|
153
|
+
`--android-build`, and can't be combined with `--*-device-type`.
|
|
154
|
+
- If physical devices aren't enabled for your workspace, the run is refused
|
|
155
|
+
with a message saying so.
|
|
156
|
+
|
|
134
157
|
An iPhone-only build may run on iPad in compatibility mode; the server warns
|
|
135
158
|
that the run does not establish native tablet-layout coverage. Unknown
|
|
136
159
|
device-family metadata produces a different warning with the same coverage
|
|
@@ -145,6 +168,7 @@ app defaults.
|
|
|
145
168
|
| `MINITEST_APP_ID` | Default app ID | No (can use `--app` flag) |
|
|
146
169
|
| `MINITEST_API_URL` | testing-service base URL | No (defaults to production) |
|
|
147
170
|
| `MINITEST_APPS_MANAGER_URL` | apps-manager base URL (used by `minitest apps create`) | No (defaults to production) |
|
|
171
|
+
| `MINITEST_CONDUCTOR_URL` | Conductor base URL (used by `minitest draft`) | No (defaults to production) |
|
|
148
172
|
| `MINITEST_INTEGRATIONS_URL` | minihands-integrations base URL (used to list tenants) | No (defaults to production) |
|
|
149
173
|
| `MINITEST_WEBAPP_URL` | Minitest webapp base URL (used for review links) | No (defaults to production) |
|
|
150
174
|
| `MINITEST_SUPABASE_URL` | Supabase project URL used for OAuth login | No (defaults to production) |
|
|
@@ -169,7 +193,8 @@ app defaults.
|
|
|
169
193
|
| `minitest apps` | List, inspect, and create apps; residential proxy and private network settings |
|
|
170
194
|
| `minitest scenario` | Create, update, list and delete scenarios |
|
|
171
195
|
| `minitest scenario-binding` | Bind test accounts, test files or app skills to scenarios |
|
|
172
|
-
| `minitest draft` |
|
|
196
|
+
| `minitest draft` | Ask Mini for a suite change, follow it, check and merge it (drafts in the webapp) |
|
|
197
|
+
| `minitest df` | Low-level suite branches Mini writes (draft features) |
|
|
173
198
|
| `minitest test-profile` | Test profiles (Test accounts in the webapp) |
|
|
174
199
|
| `minitest tags` | List, create, update and delete scenario tags |
|
|
175
200
|
| `minitest app-skill` | App skills (Skills in the webapp): procedures Mini loads to arrange test state |
|
|
@@ -190,7 +215,7 @@ stderr and leave `--json` stdout unchanged. Switch to the new names.
|
|
|
190
215
|
| `minitest user-story` | `minitest scenario` |
|
|
191
216
|
| `minitest user-story-binding` | `minitest scenario-binding` |
|
|
192
217
|
| `minitest scenario-binding set-skills` | `minitest scenario-binding set-app-skills` |
|
|
193
|
-
| `minitest
|
|
218
|
+
| `minitest draft show/apply/delete` (v0.34–v0.36) | `minitest df show/apply/delete` |
|
|
194
219
|
| `minitest flow-types` | `minitest tags` |
|
|
195
220
|
| `run from-commit --user-story` / `-u` | `run from-commit --scenario` / `-s` |
|
|
196
221
|
| `batch list --user-story-id` | `batch list --scenario` |
|
|
@@ -98,6 +98,29 @@ targets, or Edge. V1 supports one form factor per OS per run; run phone and
|
|
|
98
98
|
tablet suites separately. Tablet results are labelled `iOS · Tablet` or
|
|
99
99
|
`Android · Tablet`; phone labels stay unchanged.
|
|
100
100
|
|
|
101
|
+
## Physical devices
|
|
102
|
+
|
|
103
|
+
By default builds run on virtual devices (iOS Simulator, Android emulator). If
|
|
104
|
+
physical devices are enabled for your workspace, upload a device build with
|
|
105
|
+
`--physical` and select the physical lane when you start the run:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# iOS: an Ad Hoc or Development .ipa signed for Minitap's iPhones
|
|
109
|
+
BUILD_ID=$(minitest --json --app <app-id> build upload ./MyApp.ipa --physical | jq -r .id)
|
|
110
|
+
minitest --app <app-id> run all --ios-build "$BUILD_ID" --ios-physical
|
|
111
|
+
|
|
112
|
+
# Android: the same works with --android-physical
|
|
113
|
+
minitest --app <app-id> run start --tag smoke --android-build <android-build-id> --android-physical
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- `build upload --physical` checks the build for physical devices instead of
|
|
117
|
+
virtual ones. A Simulator build is rejected (`ios.simulator_only_ipa`), and a
|
|
118
|
+
device build uploaded without `--physical` is rejected with a hint to add it.
|
|
119
|
+
- `--ios-physical` / `--android-physical` need the matching `--ios-build` /
|
|
120
|
+
`--android-build`, and can't be combined with `--*-device-type`.
|
|
121
|
+
- If physical devices aren't enabled for your workspace, the run is refused
|
|
122
|
+
with a message saying so.
|
|
123
|
+
|
|
101
124
|
An iPhone-only build may run on iPad in compatibility mode; the server warns
|
|
102
125
|
that the run does not establish native tablet-layout coverage. Unknown
|
|
103
126
|
device-family metadata produces a different warning with the same coverage
|
|
@@ -112,6 +135,7 @@ app defaults.
|
|
|
112
135
|
| `MINITEST_APP_ID` | Default app ID | No (can use `--app` flag) |
|
|
113
136
|
| `MINITEST_API_URL` | testing-service base URL | No (defaults to production) |
|
|
114
137
|
| `MINITEST_APPS_MANAGER_URL` | apps-manager base URL (used by `minitest apps create`) | No (defaults to production) |
|
|
138
|
+
| `MINITEST_CONDUCTOR_URL` | Conductor base URL (used by `minitest draft`) | No (defaults to production) |
|
|
115
139
|
| `MINITEST_INTEGRATIONS_URL` | minihands-integrations base URL (used to list tenants) | No (defaults to production) |
|
|
116
140
|
| `MINITEST_WEBAPP_URL` | Minitest webapp base URL (used for review links) | No (defaults to production) |
|
|
117
141
|
| `MINITEST_SUPABASE_URL` | Supabase project URL used for OAuth login | No (defaults to production) |
|
|
@@ -136,7 +160,8 @@ app defaults.
|
|
|
136
160
|
| `minitest apps` | List, inspect, and create apps; residential proxy and private network settings |
|
|
137
161
|
| `minitest scenario` | Create, update, list and delete scenarios |
|
|
138
162
|
| `minitest scenario-binding` | Bind test accounts, test files or app skills to scenarios |
|
|
139
|
-
| `minitest draft` |
|
|
163
|
+
| `minitest draft` | Ask Mini for a suite change, follow it, check and merge it (drafts in the webapp) |
|
|
164
|
+
| `minitest df` | Low-level suite branches Mini writes (draft features) |
|
|
140
165
|
| `minitest test-profile` | Test profiles (Test accounts in the webapp) |
|
|
141
166
|
| `minitest tags` | List, create, update and delete scenario tags |
|
|
142
167
|
| `minitest app-skill` | App skills (Skills in the webapp): procedures Mini loads to arrange test state |
|
|
@@ -157,7 +182,7 @@ stderr and leave `--json` stdout unchanged. Switch to the new names.
|
|
|
157
182
|
| `minitest user-story` | `minitest scenario` |
|
|
158
183
|
| `minitest user-story-binding` | `minitest scenario-binding` |
|
|
159
184
|
| `minitest scenario-binding set-skills` | `minitest scenario-binding set-app-skills` |
|
|
160
|
-
| `minitest
|
|
185
|
+
| `minitest draft show/apply/delete` (v0.34–v0.36) | `minitest df show/apply/delete` |
|
|
161
186
|
| `minitest flow-types` | `minitest tags` |
|
|
162
187
|
| `run from-commit --user-story` / `-u` | `run from-commit --scenario` / `-s` |
|
|
163
188
|
| `batch list --user-story-id` | `batch list --scenario` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "minitest-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.37.1"
|
|
4
4
|
description = "Minitest CLI – command-line interface for the Minitest testing platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -128,6 +128,9 @@ python_functions = [
|
|
|
128
128
|
python_classes = [
|
|
129
129
|
"Test*"
|
|
130
130
|
]
|
|
131
|
+
markers = [
|
|
132
|
+
"real_app_lookup: resolve --app names through the API instead of the test shortcut",
|
|
133
|
+
]
|
|
131
134
|
addopts = [
|
|
132
135
|
"-v",
|
|
133
136
|
"--tb=short",
|
|
@@ -39,9 +39,16 @@ class ApiClient:
|
|
|
39
39
|
response = await client.get("/api/v1/apps")
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
|
-
def __init__(
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
settings: Settings,
|
|
45
|
+
*,
|
|
46
|
+
token_override: str | None = None,
|
|
47
|
+
base_url: str | None = None,
|
|
48
|
+
) -> None:
|
|
43
49
|
self._settings = settings
|
|
44
50
|
self._token_override = token_override
|
|
51
|
+
self._base_url = base_url or settings.api_url
|
|
45
52
|
self._client: httpx.AsyncClient | None = None
|
|
46
53
|
|
|
47
54
|
async def __aenter__(self) -> "ApiClient":
|
|
@@ -53,7 +60,7 @@ class ApiClient:
|
|
|
53
60
|
if self._settings.cause is not None:
|
|
54
61
|
headers[CAUSE_HEADER] = self._settings.cause
|
|
55
62
|
self._client = httpx.AsyncClient(
|
|
56
|
-
base_url=self.
|
|
63
|
+
base_url=self._base_url,
|
|
57
64
|
headers=headers,
|
|
58
65
|
auth=_BearerAuth(self._settings, self._token_override),
|
|
59
66
|
timeout=DEFAULT_TIMEOUT,
|
|
@@ -24,6 +24,11 @@ BUILD_INVALID_ERROR_CODE = "build_invalid"
|
|
|
24
24
|
# rollout: both).
|
|
25
25
|
_SPLIT_APKS_BACKEND_UNSUPPORTED_MARKER = "not yet supported on this execution backend"
|
|
26
26
|
|
|
27
|
+
# Rejection codes that mean "right build, wrong kind of device": the hint
|
|
28
|
+
# points at the upload flag that matches the build.
|
|
29
|
+
_DEVICE_BUILD_CODES = {"ios.device_only_ipa", "ios.device_only_sdk", "ios.no_simulator_slice"}
|
|
30
|
+
_SIMULATOR_BUILD_CODES = {"ios.simulator_only_ipa"}
|
|
31
|
+
|
|
27
32
|
# Generic FastAPI ``loc`` prefixes that add noise to a field path (they're
|
|
28
33
|
# implied by "this is a validation error" and repeat across every field).
|
|
29
34
|
_LOC_PREFIXES_TO_DROP = {"body", "query", "path"}
|
|
@@ -86,14 +91,16 @@ def extract_detail(resp: httpx.Response) -> str:
|
|
|
86
91
|
return resp.text
|
|
87
92
|
|
|
88
93
|
|
|
89
|
-
def handle_response_error(
|
|
94
|
+
def handle_response_error(
|
|
95
|
+
resp: httpx.Response, *, resource: str = "Build", physical: bool = False
|
|
96
|
+
) -> None:
|
|
90
97
|
"""Check response status; exit with proper code on errors."""
|
|
91
98
|
if resp.status_code == 404:
|
|
92
99
|
print_error(f"{resource} not found: {extract_detail(resp)}")
|
|
93
100
|
raise typer.Exit(code=EXIT_NOT_FOUND)
|
|
94
101
|
if resp.status_code == 422 and _try_handle_split_apks_backend_unsupported(resp):
|
|
95
102
|
raise typer.Exit(code=EXIT_BUILD_INVALID)
|
|
96
|
-
if resp.status_code == 422 and _try_handle_build_invalid(resp):
|
|
103
|
+
if resp.status_code == 422 and _try_handle_build_invalid(resp, physical=physical):
|
|
97
104
|
raise typer.Exit(code=EXIT_BUILD_INVALID)
|
|
98
105
|
if resp.status_code >= 400:
|
|
99
106
|
print_error(f"API error ({resp.status_code}): {extract_detail(resp)}")
|
|
@@ -122,7 +129,7 @@ def _try_handle_split_apks_backend_unsupported(resp: httpx.Response) -> bool:
|
|
|
122
129
|
return True
|
|
123
130
|
|
|
124
131
|
|
|
125
|
-
def _try_handle_build_invalid(resp: httpx.Response) -> bool:
|
|
132
|
+
def _try_handle_build_invalid(resp: httpx.Response, *, physical: bool = False) -> bool:
|
|
126
133
|
"""If response is a build-invalid error, render issues and return True."""
|
|
127
134
|
try:
|
|
128
135
|
body = resp.json()
|
|
@@ -130,17 +137,32 @@ def _try_handle_build_invalid(resp: httpx.Response) -> bool:
|
|
|
130
137
|
return False
|
|
131
138
|
if not isinstance(body, dict) or body.get("error_code") != BUILD_INVALID_ERROR_CODE:
|
|
132
139
|
return False
|
|
133
|
-
issues = body.get("issues") or []
|
|
134
|
-
|
|
140
|
+
issues = [issue for issue in body.get("issues") or [] if isinstance(issue, dict)]
|
|
141
|
+
devices = "physical-device" if physical else "virtual-device"
|
|
142
|
+
print_error(f"Build rejected: failed validation for {devices} execution.")
|
|
135
143
|
for issue in issues:
|
|
136
|
-
if not isinstance(issue, dict):
|
|
137
|
-
continue
|
|
138
144
|
code = issue.get("code", "unknown")
|
|
139
145
|
message = issue.get("message", "")
|
|
140
146
|
err_console.print(f" [red]✖[/red] {code}: {message}")
|
|
147
|
+
_print_device_kind_hint({str(issue.get("code")) for issue in issues}, physical=physical)
|
|
141
148
|
return True
|
|
142
149
|
|
|
143
150
|
|
|
151
|
+
def _print_device_kind_hint(codes: set[str], *, physical: bool) -> None:
|
|
152
|
+
if not physical and codes & _DEVICE_BUILD_CODES:
|
|
153
|
+
err_console.print(
|
|
154
|
+
" [yellow]Hint:[/yellow] this is a device build. To run it on physical iPhones,"
|
|
155
|
+
" upload it with [bold]minitest build upload <file> --physical[/bold];"
|
|
156
|
+
" for virtual devices, build for the iOS Simulator instead."
|
|
157
|
+
)
|
|
158
|
+
elif physical and codes & _SIMULATOR_BUILD_CODES:
|
|
159
|
+
err_console.print(
|
|
160
|
+
" [yellow]Hint:[/yellow] this is a Simulator build. Upload it without"
|
|
161
|
+
" [bold]--physical[/bold] to run it on virtual devices, or export a device"
|
|
162
|
+
" build (Ad Hoc or Development) for physical iPhones."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
144
166
|
def print_validation_warnings(warnings: list[dict] | None) -> None:
|
|
145
167
|
"""Print non-fatal validation warnings to stderr."""
|
|
146
168
|
if not warnings:
|
|
@@ -30,6 +30,7 @@ from minitest_cli.utils.output import (
|
|
|
30
30
|
print_table,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
+
EXIT_GENERAL_ERROR = 1
|
|
33
34
|
EXIT_NETWORK_ERROR = 3
|
|
34
35
|
|
|
35
36
|
app = typer.Typer(name="apps", help="App management.")
|
|
@@ -70,6 +71,9 @@ def list_apps() -> None:
|
|
|
70
71
|
detail = body.get("detail") or body.get("message") or detail
|
|
71
72
|
except Exception: # noqa: BLE001
|
|
72
73
|
pass
|
|
74
|
+
if resp.status_code in (401, 403):
|
|
75
|
+
print_error(f"Authentication failed ({resp.status_code}): {detail}")
|
|
76
|
+
raise typer.Exit(code=EXIT_GENERAL_ERROR)
|
|
73
77
|
print_error(f"API error ({resp.status_code}): {detail}")
|
|
74
78
|
raise typer.Exit(code=EXIT_NETWORK_ERROR)
|
|
75
79
|
return AppListResponse.model_validate(resp.json())
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
5
8
|
from minitest_cli.api.client import BATCH_CREATE_TIMEOUT, ApiClient
|
|
6
9
|
from minitest_cli.commands.run_display import _derive_run_status
|
|
10
|
+
from minitest_cli.commands._response_errors import EXIT_NETWORK_ERROR, extract_detail
|
|
7
11
|
from minitest_cli.commands.run_helpers import handle_response_error
|
|
8
12
|
from minitest_cli.commands.batch_warnings import print_compatibility_warnings
|
|
9
13
|
from minitest_cli.models.batch import BatchResponse, CreateBatchRequest
|
|
14
|
+
from minitest_cli.utils.output import err_console, print_error
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
def batches_base_path(app_id: str) -> str:
|
|
@@ -21,12 +26,29 @@ async def post_batch(client: ApiClient, app_id: str, body: CreateBatchRequest) -
|
|
|
21
26
|
json=body.model_dump(by_alias=True, exclude_none=True),
|
|
22
27
|
timeout=BATCH_CREATE_TIMEOUT,
|
|
23
28
|
)
|
|
29
|
+
if resp.status_code == 403 and _requests_physical(body):
|
|
30
|
+
_exit_physical_forbidden(resp)
|
|
24
31
|
handle_response_error(resp, resource="Batch")
|
|
25
32
|
batch = BatchResponse.model_validate(resp.json())
|
|
26
33
|
print_compatibility_warnings(batch)
|
|
27
34
|
return batch
|
|
28
35
|
|
|
29
36
|
|
|
37
|
+
def _requests_physical(body: CreateBatchRequest) -> bool:
|
|
38
|
+
return any(target.backend == "physical" for target in body.targets or [])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _exit_physical_forbidden(resp: httpx.Response) -> None:
|
|
42
|
+
"""A physical lane the workspace is not entitled to: say so, and how to proceed."""
|
|
43
|
+
print_error(extract_detail(resp))
|
|
44
|
+
err_console.print(
|
|
45
|
+
" [yellow]Fix:[/yellow] ask Minitap to enable physical devices for this workspace,"
|
|
46
|
+
" or drop [bold]--ios-physical[/bold] / [bold]--android-physical[/bold]"
|
|
47
|
+
" to run on virtual devices."
|
|
48
|
+
)
|
|
49
|
+
raise typer.Exit(code=EXIT_NETWORK_ERROR)
|
|
50
|
+
|
|
51
|
+
|
|
30
52
|
def batch_summary_payload(batch: BatchResponse) -> dict[str, Any]:
|
|
31
53
|
"""Serialise a batch to a compact JSON payload for --json output (camelCase)."""
|
|
32
54
|
return {
|
|
@@ -42,8 +42,8 @@ def upload(
|
|
|
42
42
|
Path,
|
|
43
43
|
typer.Argument(
|
|
44
44
|
help=(
|
|
45
|
-
"Path to the build file (.apk, .apks, or .
|
|
46
|
-
"
|
|
45
|
+
"Path to the build file (.apk, .apks, .ipa or .app.zip). A Simulator build"
|
|
46
|
+
" for iOS or an emulator-compatible Android build, unless --physical."
|
|
47
47
|
),
|
|
48
48
|
exists=True,
|
|
49
49
|
readable=True,
|
|
@@ -53,17 +53,34 @@ def upload(
|
|
|
53
53
|
Platform | None,
|
|
54
54
|
typer.Option(help="Target platform. Auto-detected from file extension."),
|
|
55
55
|
] = None,
|
|
56
|
+
physical: Annotated[
|
|
57
|
+
bool,
|
|
58
|
+
typer.Option(
|
|
59
|
+
"--physical",
|
|
60
|
+
help=(
|
|
61
|
+
"Upload a build for physical devices: a signed iOS device .ipa (Ad Hoc or"
|
|
62
|
+
" Development) or an Android .apk for real phones."
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
] = False,
|
|
56
66
|
) -> None:
|
|
57
|
-
"""Upload a build file (.apk or .apks for Android, .ipa for iOS).
|
|
67
|
+
"""Upload a build file (.apk or .apks for Android, .ipa or .app.zip for iOS).
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
By default builds are checked for virtual devices:
|
|
60
70
|
|
|
61
71
|
\b
|
|
62
|
-
- iOS: provide a Simulator build (.ipa built for the iOS Simulator
|
|
72
|
+
- iOS: provide a Simulator build (.ipa or .app.zip built for the iOS Simulator).
|
|
63
73
|
- Android: provide an emulator-compatible .apk or .apks archive.
|
|
74
|
+
|
|
75
|
+
With --physical, builds are checked for physical devices instead: an iOS
|
|
76
|
+
device .ipa signed for Minitap's iPhones, or an Android .apk. Start runs on
|
|
77
|
+
them with --ios-physical / --android-physical.
|
|
64
78
|
"""
|
|
65
79
|
settings, app_id, json_mode = resolve_app()
|
|
66
80
|
resolved_platform = platform.value if platform else detect_platform(file)
|
|
81
|
+
form = {"platform": resolved_platform}
|
|
82
|
+
if physical:
|
|
83
|
+
form["backend"] = "physical"
|
|
67
84
|
|
|
68
85
|
async def _run() -> BuildResponse:
|
|
69
86
|
async with ApiClient(settings) as client:
|
|
@@ -74,9 +91,9 @@ def upload(
|
|
|
74
91
|
resp = await client.upload_file(
|
|
75
92
|
f"{base_path(app_id)}/build",
|
|
76
93
|
files={"file": (file.name, f, "application/octet-stream")},
|
|
77
|
-
data=
|
|
94
|
+
data=form,
|
|
78
95
|
)
|
|
79
|
-
handle_response_error(resp)
|
|
96
|
+
handle_response_error(resp, physical=physical)
|
|
80
97
|
return BuildResponse.model_validate(resp.json())
|
|
81
98
|
|
|
82
99
|
result = run_api_call(_run())
|
|
@@ -86,7 +103,8 @@ def upload(
|
|
|
86
103
|
output(result_dict, json_mode=True)
|
|
87
104
|
else:
|
|
88
105
|
print_validation_warnings(result.validation_warnings)
|
|
89
|
-
|
|
106
|
+
devices = ", physical devices" if physical else ""
|
|
107
|
+
print_success(f"Build uploaded: {file.name} ({resolved_platform}{devices})")
|
|
90
108
|
output(result_dict, json_mode=False)
|
|
91
109
|
|
|
92
110
|
|
|
@@ -76,6 +76,7 @@ PLATFORM_EXTENSIONS: dict[str, Platform] = {
|
|
|
76
76
|
".ipa": Platform.ios,
|
|
77
77
|
".apk": Platform.android,
|
|
78
78
|
".apks": Platform.android,
|
|
79
|
+
".app.zip": Platform.ios,
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
|
|
@@ -97,7 +98,8 @@ class BuildKindFilter(StrEnum):
|
|
|
97
98
|
|
|
98
99
|
def detect_platform(file_path: Path) -> str:
|
|
99
100
|
"""Auto-detect platform from file extension."""
|
|
100
|
-
|
|
101
|
+
name = file_path.name.lower()
|
|
102
|
+
suffix = ".app.zip" if name.endswith(".app.zip") else file_path.suffix.lower()
|
|
101
103
|
platform = PLATFORM_EXTENSIONS.get(suffix)
|
|
102
104
|
if platform is None:
|
|
103
105
|
print_error(
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""`minitest draft`: ask Mini for a suite change and drive it to merge.
|
|
2
|
+
|
|
3
|
+
A draft is the workbench the webapp shows under Drafts: a branch of the suite plus
|
|
4
|
+
Mini's conversation about it. Mini is the only author of the branch; never `df apply`
|
|
5
|
+
into a draft's branch yourself.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from minitest_cli.commands import (
|
|
13
|
+
draft_asks,
|
|
14
|
+
draft_conversation,
|
|
15
|
+
draft_create,
|
|
16
|
+
draft_legacy,
|
|
17
|
+
draft_lifecycle,
|
|
18
|
+
draft_pr,
|
|
19
|
+
draft_update,
|
|
20
|
+
)
|
|
21
|
+
from minitest_cli.commands.draft_helpers import (
|
|
22
|
+
DRAFTS_PATH,
|
|
23
|
+
call,
|
|
24
|
+
draft_path,
|
|
25
|
+
resolve_target,
|
|
26
|
+
run_conductor,
|
|
27
|
+
)
|
|
28
|
+
from minitest_cli.commands.draft_render import (
|
|
29
|
+
DRAFT_TABLE_HEADERS,
|
|
30
|
+
draft_row,
|
|
31
|
+
show_draft,
|
|
32
|
+
)
|
|
33
|
+
from minitest_cli.commands.draft_watch import WatchUntil, watch
|
|
34
|
+
from minitest_cli.utils.output import (
|
|
35
|
+
print_info,
|
|
36
|
+
print_json,
|
|
37
|
+
print_table,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
app = typer.Typer(
|
|
41
|
+
name="draft",
|
|
42
|
+
help="Ask Mini for a suite change, follow the conversation, and check/merge the result.",
|
|
43
|
+
no_args_is_help=True,
|
|
44
|
+
)
|
|
45
|
+
EXIT_WATCH_TIMEOUT = 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command(name="list")
|
|
49
|
+
def list_drafts(
|
|
50
|
+
history: Annotated[
|
|
51
|
+
bool, typer.Option("--history", help="List merged and discarded drafts instead.")
|
|
52
|
+
] = False,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""List the active app's drafts (open ones by default)."""
|
|
55
|
+
target = resolve_target()
|
|
56
|
+
params = {"appId": target.app_id, "scope": "history" if history else "open"}
|
|
57
|
+
body = call(target, "GET", DRAFTS_PATH, retry=True, params=params)
|
|
58
|
+
drafts = body.get("drafts", [])
|
|
59
|
+
if target.json_mode:
|
|
60
|
+
print_json(drafts)
|
|
61
|
+
return
|
|
62
|
+
if not drafts:
|
|
63
|
+
print_info("No drafts found.")
|
|
64
|
+
return
|
|
65
|
+
print_table(
|
|
66
|
+
DRAFT_TABLE_HEADERS, [draft_row(d) for d in drafts], title=f"Drafts ({len(drafts)})"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command(name="get")
|
|
71
|
+
def get_draft(
|
|
72
|
+
draft_id: Annotated[str, typer.Argument(help="Draft ID.")],
|
|
73
|
+
watch_flag: Annotated[
|
|
74
|
+
bool, typer.Option("--watch", help="Poll until the draft is stable, then print it.")
|
|
75
|
+
] = False,
|
|
76
|
+
until: Annotated[
|
|
77
|
+
WatchUntil,
|
|
78
|
+
typer.Option(
|
|
79
|
+
"--until",
|
|
80
|
+
help="idle: Mini has nothing queued. settled: also no check or draft build running.",
|
|
81
|
+
),
|
|
82
|
+
] = WatchUntil.IDLE,
|
|
83
|
+
interval: Annotated[
|
|
84
|
+
float, typer.Option("--interval", min=1.0, help="Seconds between polls.")
|
|
85
|
+
] = 5.0,
|
|
86
|
+
timeout: Annotated[
|
|
87
|
+
float, typer.Option("--timeout", min=1.0, help="Give up after this many seconds (exit 1).")
|
|
88
|
+
] = 900.0,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Show a draft: state, Mini's status, scenarios and open asks."""
|
|
91
|
+
target = resolve_target()
|
|
92
|
+
path = draft_path(draft_id)
|
|
93
|
+
if not watch_flag:
|
|
94
|
+
show_draft(call(target, "GET", path, retry=True), json_mode=target.json_mode)
|
|
95
|
+
return
|
|
96
|
+
draft, stable = run_conductor(
|
|
97
|
+
watch(target, path, until=until, interval=interval, timeout=timeout)
|
|
98
|
+
)
|
|
99
|
+
show_draft(draft, json_mode=target.json_mode)
|
|
100
|
+
if not stable:
|
|
101
|
+
print_info(f"Still not {until.value} after {timeout:.0f}s; run the same command again.")
|
|
102
|
+
raise typer.Exit(code=EXIT_WATCH_TIMEOUT)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
draft_create.register(app)
|
|
106
|
+
draft_update.register(app)
|
|
107
|
+
draft_conversation.register(app)
|
|
108
|
+
draft_lifecycle.register(app)
|
|
109
|
+
draft_pr.register(app)
|
|
110
|
+
app.add_typer(draft_asks.app)
|
|
111
|
+
draft_legacy.register(app)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""`minitest draft ask list|answer|decline`: answer what Mini asked on a draft.
|
|
2
|
+
|
|
3
|
+
Secret fields (``secret: true`` in an ask's answerFields) are only read from a file or
|
|
4
|
+
stdin (``--field-file KEY=PATH``, ``-`` for stdin) — never from the command line, where
|
|
5
|
+
shells and process lists keep them — and are never printed back.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated, Any
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from minitest_cli.commands.draft_helpers import (
|
|
14
|
+
EXIT_GENERAL_ERROR,
|
|
15
|
+
EXIT_NOT_FOUND,
|
|
16
|
+
DraftTarget,
|
|
17
|
+
call,
|
|
18
|
+
draft_path,
|
|
19
|
+
resolve_target,
|
|
20
|
+
)
|
|
21
|
+
from minitest_cli.commands.draft_render import (
|
|
22
|
+
ASK_TABLE_HEADERS,
|
|
23
|
+
ask_fields,
|
|
24
|
+
read_file,
|
|
25
|
+
read_text,
|
|
26
|
+
show_draft,
|
|
27
|
+
)
|
|
28
|
+
from minitest_cli.commands.run_helpers import ensure_uuid
|
|
29
|
+
from minitest_cli.utils.output import (
|
|
30
|
+
print_error,
|
|
31
|
+
print_info,
|
|
32
|
+
print_json,
|
|
33
|
+
print_success,
|
|
34
|
+
print_table,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
app = typer.Typer(
|
|
38
|
+
name="ask", help="Read and answer Mini's questions on a draft.", no_args_is_help=True
|
|
39
|
+
)
|
|
40
|
+
DraftIdArg = Annotated[str, typer.Argument(help="Draft ID.")]
|
|
41
|
+
AskIdArg = Annotated[str, typer.Argument(help="Ask ID (from `draft ask list`).")]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _open_asks(target: DraftTarget, draft_id: str) -> list[dict[str, Any]]:
|
|
45
|
+
return call(target, "GET", draft_path(draft_id), retry=True).get("asks") or []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _find_ask(target: DraftTarget, draft_id: str, ask_id: str) -> dict[str, Any]:
|
|
49
|
+
ensure_uuid(ask_id, kind="ask id")
|
|
50
|
+
for ask in _open_asks(target, draft_id):
|
|
51
|
+
if ask.get("id") == ask_id:
|
|
52
|
+
return ask
|
|
53
|
+
print_error(f"No open ask {ask_id} on draft {draft_id}; see `minitest draft ask list`.")
|
|
54
|
+
raise typer.Exit(code=EXIT_NOT_FOUND)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _pair(raw: str, flag: str) -> tuple[str, str]:
|
|
58
|
+
key, sep, value = raw.partition("=")
|
|
59
|
+
if not sep or not key:
|
|
60
|
+
print_error(f"{flag} expects KEY=VALUE.")
|
|
61
|
+
raise typer.Exit(code=EXIT_GENERAL_ERROR)
|
|
62
|
+
return key, value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _fields(ask: dict[str, Any], inline: list[str], files: list[str]) -> dict[str, str]:
|
|
66
|
+
known = {f["key"]: f for f in ask.get("answerFields") or []}
|
|
67
|
+
values: dict[str, str] = {}
|
|
68
|
+
for raw in inline:
|
|
69
|
+
key, value = _pair(raw, "--field")
|
|
70
|
+
if known.get(key, {}).get("secret"):
|
|
71
|
+
print_error(f"'{key}' is secret: pass it with --field-file {key}=PATH (- = stdin).")
|
|
72
|
+
raise typer.Exit(code=EXIT_GENERAL_ERROR)
|
|
73
|
+
values[key] = value
|
|
74
|
+
if sum(1 for raw in files if _pair(raw, "--field-file")[1] == "-") > 1:
|
|
75
|
+
print_error("Only one --field-file can read stdin.")
|
|
76
|
+
raise typer.Exit(code=EXIT_GENERAL_ERROR)
|
|
77
|
+
for raw in files:
|
|
78
|
+
key, path = _pair(raw, "--field-file")
|
|
79
|
+
values[key] = read_file(Path(path)).removesuffix("\n")
|
|
80
|
+
unknown = sorted(set(values) - set(known))
|
|
81
|
+
if unknown:
|
|
82
|
+
print_error(f"Unknown field(s) {', '.join(unknown)}; this ask takes: {ask_fields(ask)}.")
|
|
83
|
+
raise typer.Exit(code=EXIT_GENERAL_ERROR)
|
|
84
|
+
return values
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command(name="list")
|
|
88
|
+
def list_asks(draft_id: DraftIdArg) -> None:
|
|
89
|
+
"""List Mini's open questions on a draft, with the fields each answer takes."""
|
|
90
|
+
target = resolve_target()
|
|
91
|
+
asks = _open_asks(target, draft_id)
|
|
92
|
+
if target.json_mode:
|
|
93
|
+
print_json(asks)
|
|
94
|
+
return
|
|
95
|
+
if not asks:
|
|
96
|
+
print_info("Mini is not asking anything.")
|
|
97
|
+
return
|
|
98
|
+
rows = [[a["id"], a.get("askType", "-"), a["question"], ask_fields(a)] for a in asks]
|
|
99
|
+
print_table(ASK_TABLE_HEADERS, rows)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command(name="answer")
|
|
103
|
+
def answer_ask(
|
|
104
|
+
draft_id: DraftIdArg,
|
|
105
|
+
ask_id: AskIdArg,
|
|
106
|
+
answer: Annotated[str | None, typer.Option("--answer", help="Free-text answer.")] = None,
|
|
107
|
+
answer_file: Annotated[
|
|
108
|
+
Path | None, typer.Option("--answer-file", help="Read the answer from a file (- = stdin).")
|
|
109
|
+
] = None,
|
|
110
|
+
field: Annotated[
|
|
111
|
+
list[str] | None, typer.Option("--field", help="KEY=VALUE for a non-secret field.")
|
|
112
|
+
] = None,
|
|
113
|
+
field_file: Annotated[
|
|
114
|
+
list[str] | None,
|
|
115
|
+
typer.Option("--field-file", help="KEY=PATH, value read from a file (- = stdin)."),
|
|
116
|
+
] = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Answer an ask; Mini resumes the draft with it. Turns auto-merge off."""
|
|
119
|
+
text = read_text(answer, answer_file, flag="answer", required=False)
|
|
120
|
+
target = resolve_target()
|
|
121
|
+
ask = _find_ask(target, draft_id, ask_id)
|
|
122
|
+
values = _fields(ask, field or [], field_file or [])
|
|
123
|
+
if text is None and not values:
|
|
124
|
+
print_error("Pass --answer/--answer-file, or the ask's fields with --field/--field-file.")
|
|
125
|
+
raise typer.Exit(code=EXIT_GENERAL_ERROR)
|
|
126
|
+
body: dict[str, object] = {}
|
|
127
|
+
if text is not None:
|
|
128
|
+
body["answer"] = text
|
|
129
|
+
if values:
|
|
130
|
+
body["fields"] = values
|
|
131
|
+
draft = call(target, "POST", draft_path(draft_id, f"/asks/{ask_id}/answer"), json=body)
|
|
132
|
+
print_success("Answered; Mini picks it up next.")
|
|
133
|
+
show_draft(draft, json_mode=target.json_mode)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command(name="decline")
|
|
137
|
+
def decline_ask(
|
|
138
|
+
draft_id: DraftIdArg,
|
|
139
|
+
ask_id: AskIdArg,
|
|
140
|
+
reason: Annotated[str | None, typer.Option("--reason", help="Why, so Mini can adapt.")] = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Decline an ask; Mini carries on without it."""
|
|
143
|
+
target = resolve_target()
|
|
144
|
+
ensure_uuid(ask_id, kind="ask id")
|
|
145
|
+
body = {"reason": reason} if reason else None
|
|
146
|
+
draft = call(target, "POST", draft_path(draft_id, f"/asks/{ask_id}/decline"), json=body)
|
|
147
|
+
print_success("Declined.")
|
|
148
|
+
show_draft(draft, json_mode=target.json_mode)
|