wade-cli 0.30.1__tar.gz → 0.30.2__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.
- {wade_cli-0.30.1 → wade_cli-0.30.2}/CHANGELOG.md +6 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/KNOWLEDGE.md +16 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/KNOWLEDGE.ratings.yml +3 -3
- {wade_cli-0.30.1 → wade_cli-0.30.2}/PKG-INFO +1 -1
- {wade_cli-0.30.1 → wade_cli-0.30.2}/pyproject.toml +1 -1
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/__init__.py +1 -1
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/init_service.py +23 -1
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/install.py +5 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/update_check.py +48 -21
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_init.py +107 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_update_check.py +117 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/auto-version.yml +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/ci.yml +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/pr-title-lint.yml +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/publish.yml +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/release.yml +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/.gitignore +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/.pre-commit-config.yaml +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/.wade.yml +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/AGENTS.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/CONTRIBUTING.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/README.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/assets/wade.png +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/docker-compose.e2e.yml +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/architecture.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/documentation-policies.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/extending.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/skills-system.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/testing.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/install.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/auto_version.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/changelog.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/check-all.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/check.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/fmt.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/hooks/pre-push +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/install-hooks.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/release.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/setup-worktree.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-e2e-docker.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-e2e.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-live-ai-taskr.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-live-ai.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-live-gh.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/__main__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/admin.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/autocomplete.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/implementation_session.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/knowledge.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/main.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/plan_session.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/review.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/review_pr_comments_session.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/session_shared.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/task.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/worktree.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/config/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/config/loader.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/config/migrations.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/config/schema.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/db/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/db/engine.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/db/repositories.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/db/tables.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/branch.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/pr.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/repo.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/stash.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/sync.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/worktree.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/hooks/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/hooks/plan_write_guard.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/hooks/worktree_guard.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/logging/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/logging/context.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/logging/setup.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/batch.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/config.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/delegation.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/deps.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/events.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/review.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/session.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/task.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/_pr_delegate.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/base.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/clickup.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/github.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/markdown.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/registry.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/ai_resolution.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/batch_review_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/check_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/delegation_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/deps_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/batch.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/bootstrap.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/core.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/usage_tracking.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/knowledge_search.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/knowledge_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/plan_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/prompt_delivery.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/review_delegation_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/review_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/review_settle.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/smart_start.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/task_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/skills/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/skills/installer.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/skills/pointer.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/ui/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/ui/console.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/ui/prompts.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/clipboard.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/http.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/markdown.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/process.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/slug.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/terminal.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/token_usage_markdown.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/agents-pointer.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/deps-analysis.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/implement-context.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/plan-session.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/review-batch.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/review-code.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/review-plan.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/review-pr-comments.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/setup-worktree.sh.example +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/_partials/review-enforcement-rule.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/_partials/review-implementation-closing-step.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/_partials/review-plan-step.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/_partials/user-interaction.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/deps/SKILL.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/implementation-session/SKILL.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/knowledge/SKILL.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/plan-session/SKILL.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/review-pr-comments-session/SKILL.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/task/SKILL.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/task/examples.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/task/plan-format.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/statusline-command.sh +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/conftest.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/_support.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/conftest.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/mock_gh_script.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_admin_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_check_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_implement_work_done_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_knowledge_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_mock_gh_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_plan_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_review_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_smart_start_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_task_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_work_sync_list_contract.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/live/taskr_header_hi_body.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/live/taskr_header_howdy_body.md +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/claude_session.txt +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/codex_session.txt +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/copilot_session.txt +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/gemini_session.txt +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/generic_session.txt +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_autostash.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_check.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_git.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_implementation_lifecycle.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_init.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_knowledge.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_markdown_provider.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_skill_install.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/live/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/live/test_wade_live_ai.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/live/test_wade_live_ai_taskr.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/live/test_wade_live_gh.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/test_cli_basics.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/conftest.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_done_review_hint.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_knowledge_cli.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_review_cli.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_session_subapps.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_shell_init.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_task_cli.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_work_ai_flags.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_work_cli.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_config/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_config/test_loader.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_config/test_migrations.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_config/test_migrations_atomic.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_console.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_db/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_db/test_repository_transactions.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_effort.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_branch_slugify.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_detached_worktree.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_pr_base.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_repo.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_scaffold_commit.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_sync_conflicted_files.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_worktree_remove.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_hooks/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_hooks/test_plan_write_guard.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_hooks/test_worktree_guard.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_batch.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_config_models.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_db.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_delegation.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_deps_models.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_deps_partition.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_review.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_task_models.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/test_clickup.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/test_github.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/test_github_pr.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/test_markdown.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_ai_resolution.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_batch_review_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_bootstrap_allowlist.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_bootstrap_hook_timeout.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_catchup.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_check.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_delegation_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_deps_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_done_plan_flag.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_done_via_direct_cleanup.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_done_sync.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_service_resume.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_init_shell_integration.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_knowledge_search.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_knowledge_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_plan_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_poll_for_reviews.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_post_implementation_lifecycle.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_pr_summary_path.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_preserve_session_data.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_prompt_delivery.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_review_delegation_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_review_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_smart_start.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_smart_start_resume.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_sync_base_branch.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_task_service.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_tracked_managed_files.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_worktree_gitignore.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_skills/test_managed_gitignore.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/__init__.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_http.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_install.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_markdown.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_process.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_prompts.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_slug.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_terminal.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_terminal_launch.py +0 -0
- {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_yolo.py +0 -0
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Conventional Commits](https://conventionalcommits.org/).
|
|
6
6
|
|
|
7
|
+
## [v0.30.2] — 2026-07-16
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- prevent infinite self-upgrade re-exec loop in `wade update` (#322) (1e7afc7)
|
|
12
|
+
|
|
7
13
|
## [v0.30.1] — 2026-07-16
|
|
8
14
|
|
|
9
15
|
### CI/CD
|
|
@@ -113,3 +113,19 @@ and keep `bot_status` as a secondary input only.
|
|
|
113
113
|
When a service does 'from datetime import UTC, datetime' and calls datetime.now(UTC), patch the module-level class reference (e.g. wade.services.review_service.datetime). Set mock_datetime.now.return_value = fixed_now. Avoid naming local variables 'now' as datetime when the same function later uses 'now = time.time()' (float) — mypy strict mode flags the type mismatch even across mutually-exclusive code paths (use a distinct name like 'settle_now').
|
|
114
114
|
|
|
115
115
|
---
|
|
116
|
+
|
|
117
|
+
## 4863036c | 2026-07-16 | plan | tags: refactoring, testing, mocking, architecture
|
|
118
|
+
|
|
119
|
+
When splitting a wade service module into submodules, moving a function to a new module breaks tests that patch its dependencies: the suite patches fully-qualified paths heavily (e.g. mock.patch("wade.services.implementation_service.core.git_sync"), ~200+ such sites), and mock.patch binds where a name is looked up. Repoint every moved function's patch targets to its new submodule. Also note: the implementation_service package re-exports via "from .sub import *", which does NOT re-export underscore-prefixed helpers — re-export those explicitly in __init__.py.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## a24d65b0 | 2026-07-16 | plan | tags: update, self-upgrade, gotcha
|
|
124
|
+
|
|
125
|
+
`wade update` self-upgrade can infinitely re-exec. Three defects combine:
|
|
126
|
+
(1) `utils/install._run_upgrade` treats package-manager exit code 0 as a successful upgrade — but `uv tool upgrade wade-cli` exits 0 for no-ops too (already up to date, or the target version is yanked/unavailable), so a no-op is reported as success;
|
|
127
|
+
(2) `services/init_service._maybe_self_upgrade` calls `re_exec()` with no loop guard — after re-exec it re-runs the same check with no memory it just tried and no check that the version actually advanced;
|
|
128
|
+
(3) `utils/update_check.check_for_update` trusts the 2-hour nag cache at `~/.local/share/wade/update-check.json`. A stale/yanked `latest_version` (observed: cache said 0.30.1 while 0.29.6 was installed and was repo HEAD) makes every re-exec take the identical upgrade path → infinite "wade upgraded — restarting..." loop for the whole cache TTL.
|
|
129
|
+
Fix: add a re-exec breadcrumb env var (WADE_SELF_UPGRADE_FROM=<old version>) so a post-re-exec process detects the version did NOT advance and stops with a warning; force a cache-bypassing fresh PyPI check for the explicit self-upgrade path; and skip yanked releases when selecting the latest version.
|
|
130
|
+
|
|
131
|
+
---
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
up: 2
|
|
10
10
|
3463fbd4:
|
|
11
11
|
down: 0
|
|
12
|
-
up:
|
|
12
|
+
up: 2
|
|
13
13
|
54326c98:
|
|
14
14
|
down: 1
|
|
15
15
|
up: 0
|
|
16
16
|
b61e247e:
|
|
17
17
|
down: 0
|
|
18
|
-
up:
|
|
18
|
+
up: 3
|
|
19
19
|
b6a3a637:
|
|
20
20
|
down: 0
|
|
21
21
|
up: 1
|
|
@@ -27,4 +27,4 @@ cedeaad0:
|
|
|
27
27
|
up: 1
|
|
28
28
|
e2a49ac1:
|
|
29
29
|
down: 0
|
|
30
|
-
up:
|
|
30
|
+
up: 3
|
|
@@ -795,11 +795,32 @@ def _maybe_self_upgrade() -> bool:
|
|
|
795
795
|
Checks PyPI first — only upgrades if a newer version is actually available.
|
|
796
796
|
If upgrade is applied, calls re_exec() which replaces this process.
|
|
797
797
|
Returns True if an upgrade was triggered, False if skipped or up to date.
|
|
798
|
+
|
|
799
|
+
Guards against an infinite re-exec loop: immediately before re-exec'ing,
|
|
800
|
+
the pre-upgrade version is recorded in WADE_SELF_UPGRADE_FROM, which
|
|
801
|
+
os.execv inherits into the replacement process. On entry, if that
|
|
802
|
+
breadcrumb is present this call is a post-re-exec retry, so it is always
|
|
803
|
+
cleared and the function returns False without attempting another
|
|
804
|
+
upgrade — regardless of whether the version actually advanced.
|
|
798
805
|
"""
|
|
799
806
|
from wade import __version__
|
|
800
807
|
from wade.utils.install import InstallMethod, detect_install_method, re_exec, self_upgrade
|
|
801
808
|
from wade.utils.update_check import check_for_update
|
|
802
809
|
|
|
810
|
+
breadcrumb = os.environ.pop("WADE_SELF_UPGRADE_FROM", None)
|
|
811
|
+
if breadcrumb is not None:
|
|
812
|
+
if breadcrumb == __version__:
|
|
813
|
+
console.warn(
|
|
814
|
+
f"self-upgrade did not change the installed version ({__version__}); "
|
|
815
|
+
"the reported latest may be unavailable — continuing with "
|
|
816
|
+
f"{__version__}. Use --skip-self-upgrade to skip."
|
|
817
|
+
)
|
|
818
|
+
else:
|
|
819
|
+
logger.info(
|
|
820
|
+
"_maybe_self_upgrade.upgraded", from_version=breadcrumb, to_version=__version__
|
|
821
|
+
)
|
|
822
|
+
return False
|
|
823
|
+
|
|
803
824
|
method = detect_install_method()
|
|
804
825
|
|
|
805
826
|
if method in (InstallMethod.EDITABLE, InstallMethod.UNKNOWN):
|
|
@@ -808,12 +829,13 @@ def _maybe_self_upgrade() -> bool:
|
|
|
808
829
|
|
|
809
830
|
console.step("Checking for WADE updates...")
|
|
810
831
|
|
|
811
|
-
latest = check_for_update(__version__)
|
|
832
|
+
latest = check_for_update(__version__, force=True)
|
|
812
833
|
if not latest:
|
|
813
834
|
return False # Already at the latest version
|
|
814
835
|
|
|
815
836
|
if self_upgrade():
|
|
816
837
|
console.success("wade upgraded — restarting...")
|
|
838
|
+
os.environ["WADE_SELF_UPGRADE_FROM"] = __version__
|
|
817
839
|
re_exec() # Does not return
|
|
818
840
|
return True # pragma: no cover
|
|
819
841
|
|
|
@@ -106,6 +106,11 @@ def re_exec() -> None:
|
|
|
106
106
|
|
|
107
107
|
Replaces the current process image so the newly installed code is loaded.
|
|
108
108
|
Does not return.
|
|
109
|
+
|
|
110
|
+
Uses os.execv (not execve), so the replacement process inherits the
|
|
111
|
+
calling process's environment unchanged — callers rely on this to pass
|
|
112
|
+
state across the re-exec (e.g. init_service._maybe_self_upgrade sets
|
|
113
|
+
WADE_SELF_UPGRADE_FROM beforehand to guard against re-exec loops).
|
|
109
114
|
"""
|
|
110
115
|
executable = sys.argv[0]
|
|
111
116
|
|
|
@@ -26,20 +26,22 @@ PYPI_URL = "https://pypi.org/pypi/wade-cli/json"
|
|
|
26
26
|
CACHE_FILE = Path.home() / ".local" / "share" / "wade" / "update-check.json"
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def
|
|
30
|
-
"""
|
|
29
|
+
def _version_tuple(v: str) -> tuple[int, ...]:
|
|
30
|
+
"""Parse a dotted version string into a comparable integer tuple.
|
|
31
31
|
|
|
32
|
-
Uses simple numeric tuple
|
|
33
|
-
|
|
32
|
+
Uses simple numeric tuple parsing — sufficient for standard semver.
|
|
33
|
+
Non-numeric segments are dropped; falls back to an empty tuple on any
|
|
34
|
+
parse error.
|
|
34
35
|
"""
|
|
35
36
|
try:
|
|
37
|
+
return tuple(int(x) for x in v.split(".")[:3] if x.isdigit())
|
|
38
|
+
except (ValueError, TypeError):
|
|
39
|
+
return ()
|
|
36
40
|
|
|
37
|
-
def to_tuple(v: str) -> tuple[int, ...]:
|
|
38
|
-
return tuple(int(x) for x in v.split(".")[:3] if x.isdigit())
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
def _is_newer(candidate: str, current: str) -> bool:
|
|
43
|
+
"""Return True if candidate version is strictly newer than current."""
|
|
44
|
+
return _version_tuple(candidate) > _version_tuple(current)
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
def _load_cache() -> dict[str, object] | None:
|
|
@@ -72,38 +74,63 @@ def _save_cache(latest_version: str, timestamp: float) -> None:
|
|
|
72
74
|
pass
|
|
73
75
|
|
|
74
76
|
|
|
77
|
+
def _select_latest_non_yanked(releases: dict[str, object]) -> str | None:
|
|
78
|
+
"""Return the highest version among releases with at least one non-yanked file.
|
|
79
|
+
|
|
80
|
+
A release with no files, or where every file is yanked, is skipped —
|
|
81
|
+
PyPI's `info.version` does not account for yanking, so it can point at a
|
|
82
|
+
release that is no longer installable.
|
|
83
|
+
"""
|
|
84
|
+
candidates = [
|
|
85
|
+
version
|
|
86
|
+
for version, files in releases.items()
|
|
87
|
+
if isinstance(files, list)
|
|
88
|
+
and files
|
|
89
|
+
and not all(isinstance(f, dict) and f.get("yanked") for f in files)
|
|
90
|
+
]
|
|
91
|
+
if not candidates:
|
|
92
|
+
return None
|
|
93
|
+
return max(candidates, key=_version_tuple)
|
|
94
|
+
|
|
95
|
+
|
|
75
96
|
def _fetch_latest_version() -> str | None:
|
|
76
|
-
"""Fetch the latest wade-cli version from PyPI. Returns None on any error."""
|
|
97
|
+
"""Fetch the latest non-yanked wade-cli version from PyPI. Returns None on any error."""
|
|
77
98
|
try:
|
|
78
99
|
with urlopen(PYPI_URL, timeout=5) as resp:
|
|
79
100
|
data = json.loads(resp.read())
|
|
80
|
-
|
|
81
|
-
if isinstance(
|
|
82
|
-
return
|
|
101
|
+
releases = data["releases"]
|
|
102
|
+
if not isinstance(releases, dict):
|
|
103
|
+
return None
|
|
104
|
+
return _select_latest_non_yanked(releases)
|
|
83
105
|
except (URLError, OSError, json.JSONDecodeError, KeyError, TypeError):
|
|
84
106
|
pass
|
|
85
107
|
return None
|
|
86
108
|
|
|
87
109
|
|
|
88
|
-
def check_for_update(current_version: str) -> str | None:
|
|
110
|
+
def check_for_update(current_version: str, force: bool = False) -> str | None:
|
|
89
111
|
"""Return latest PyPI version if newer than current, else None.
|
|
90
112
|
|
|
91
|
-
Uses a 2-hour file cache to avoid hammering PyPI.
|
|
113
|
+
Uses a 2-hour file cache to avoid hammering PyPI, unless force=True.
|
|
92
114
|
|
|
93
115
|
Args:
|
|
94
116
|
current_version: The currently installed version string.
|
|
117
|
+
force: When True, bypass the cache and fetch live from PyPI (the
|
|
118
|
+
fresh result still refreshes the cache). Used by the explicit
|
|
119
|
+
self-upgrade path so a stale or yanked cached "latest" cannot
|
|
120
|
+
drive the upgrade decision.
|
|
95
121
|
|
|
96
122
|
Returns:
|
|
97
123
|
The latest version string if an update is available, otherwise None.
|
|
98
124
|
"""
|
|
99
125
|
now = datetime.now(UTC).timestamp()
|
|
100
126
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
if not force:
|
|
128
|
+
cached = _load_cache()
|
|
129
|
+
if cached is not None:
|
|
130
|
+
age = now - float(str(cached["timestamp"]))
|
|
131
|
+
if age < CHECK_INTERVAL_SECS:
|
|
132
|
+
latest = str(cached["latest_version"])
|
|
133
|
+
return latest if _is_newer(latest, current_version) else None
|
|
107
134
|
|
|
108
135
|
fetched = _fetch_latest_version()
|
|
109
136
|
if fetched:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from unittest.mock import MagicMock, patch
|
|
7
8
|
|
|
@@ -18,6 +19,7 @@ from wade.services.init_service import (
|
|
|
18
19
|
_check_gh_auth,
|
|
19
20
|
_clean_gitignore,
|
|
20
21
|
_ensure_wade_dir_self_ignoring,
|
|
22
|
+
_maybe_self_upgrade,
|
|
21
23
|
_migrate_gitignore_block,
|
|
22
24
|
_patch_config,
|
|
23
25
|
_prompt_ai_section,
|
|
@@ -48,6 +50,7 @@ from wade.skills.pointer import (
|
|
|
48
50
|
remove_pointer,
|
|
49
51
|
write_pointer,
|
|
50
52
|
)
|
|
53
|
+
from wade.utils.install import InstallMethod
|
|
51
54
|
|
|
52
55
|
# ---------------------------------------------------------------------------
|
|
53
56
|
# Pointer tests
|
|
@@ -1432,6 +1435,110 @@ class TestUpdateExtended:
|
|
|
1432
1435
|
assert success # update should succeed and detect version difference
|
|
1433
1436
|
|
|
1434
1437
|
|
|
1438
|
+
# ---------------------------------------------------------------------------
|
|
1439
|
+
# _maybe_self_upgrade — re-exec loop guard (issue #321)
|
|
1440
|
+
# ---------------------------------------------------------------------------
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
class TestMaybeSelfUpgrade:
|
|
1444
|
+
"""A WADE_SELF_UPGRADE_FROM breadcrumb must cap re-exec at one attempt."""
|
|
1445
|
+
|
|
1446
|
+
def setup_method(self) -> None:
|
|
1447
|
+
os.environ.pop("WADE_SELF_UPGRADE_FROM", None)
|
|
1448
|
+
|
|
1449
|
+
def teardown_method(self) -> None:
|
|
1450
|
+
os.environ.pop("WADE_SELF_UPGRADE_FROM", None)
|
|
1451
|
+
|
|
1452
|
+
def test_breadcrumb_unchanged_warns_and_clears_env(self) -> None:
|
|
1453
|
+
"""breadcrumb == current version → no re-exec, a warning, env cleared."""
|
|
1454
|
+
from wade import __version__
|
|
1455
|
+
|
|
1456
|
+
os.environ["WADE_SELF_UPGRADE_FROM"] = __version__
|
|
1457
|
+
|
|
1458
|
+
with (
|
|
1459
|
+
patch("wade.utils.install.detect_install_method") as mock_detect,
|
|
1460
|
+
patch("wade.services.init_service.console") as mock_console,
|
|
1461
|
+
):
|
|
1462
|
+
result = _maybe_self_upgrade()
|
|
1463
|
+
|
|
1464
|
+
assert result is False
|
|
1465
|
+
mock_console.warn.assert_called_once()
|
|
1466
|
+
assert "WADE_SELF_UPGRADE_FROM" not in os.environ
|
|
1467
|
+
mock_detect.assert_not_called()
|
|
1468
|
+
|
|
1469
|
+
def test_breadcrumb_differs_clears_env_without_warning(self) -> None:
|
|
1470
|
+
"""breadcrumb != current version → upgrade succeeded, no re-exec, env cleared."""
|
|
1471
|
+
os.environ["WADE_SELF_UPGRADE_FROM"] = "0.0.1"
|
|
1472
|
+
|
|
1473
|
+
with (
|
|
1474
|
+
patch("wade.utils.install.detect_install_method") as mock_detect,
|
|
1475
|
+
patch("wade.services.init_service.console") as mock_console,
|
|
1476
|
+
):
|
|
1477
|
+
result = _maybe_self_upgrade()
|
|
1478
|
+
|
|
1479
|
+
assert result is False
|
|
1480
|
+
mock_console.warn.assert_not_called()
|
|
1481
|
+
assert "WADE_SELF_UPGRADE_FROM" not in os.environ
|
|
1482
|
+
mock_detect.assert_not_called()
|
|
1483
|
+
|
|
1484
|
+
def test_no_breadcrumb_runs_normal_flow(self) -> None:
|
|
1485
|
+
"""No breadcrumb present → proceeds to the ordinary upgrade check."""
|
|
1486
|
+
with patch(
|
|
1487
|
+
"wade.utils.install.detect_install_method", return_value=InstallMethod.UNKNOWN
|
|
1488
|
+
) as mock_detect:
|
|
1489
|
+
result = _maybe_self_upgrade()
|
|
1490
|
+
|
|
1491
|
+
assert result is False
|
|
1492
|
+
mock_detect.assert_called_once()
|
|
1493
|
+
|
|
1494
|
+
def test_newer_version_sets_breadcrumb_before_re_exec(self) -> None:
|
|
1495
|
+
"""A genuinely newer version sets the breadcrumb before calling re_exec()."""
|
|
1496
|
+
from wade import __version__
|
|
1497
|
+
|
|
1498
|
+
captured: dict[str, str] = {}
|
|
1499
|
+
|
|
1500
|
+
def fake_re_exec() -> None:
|
|
1501
|
+
captured["breadcrumb"] = os.environ.get("WADE_SELF_UPGRADE_FROM", "")
|
|
1502
|
+
|
|
1503
|
+
with (
|
|
1504
|
+
patch("wade.utils.install.detect_install_method", return_value=InstallMethod.UV_TOOL),
|
|
1505
|
+
patch("wade.utils.update_check.check_for_update", return_value="99.0.0") as mock_check,
|
|
1506
|
+
patch("wade.utils.install.self_upgrade", return_value=True),
|
|
1507
|
+
patch("wade.utils.install.re_exec", side_effect=fake_re_exec),
|
|
1508
|
+
):
|
|
1509
|
+
result = _maybe_self_upgrade()
|
|
1510
|
+
|
|
1511
|
+
assert result is True
|
|
1512
|
+
assert captured["breadcrumb"] == __version__
|
|
1513
|
+
mock_check.assert_called_once_with(__version__, force=True)
|
|
1514
|
+
|
|
1515
|
+
def test_at_most_one_re_exec_across_check_upgrade_recheck_chain(self) -> None:
|
|
1516
|
+
"""End-to-end: check → upgrade → re-exec → post-re-exec re-check triggers
|
|
1517
|
+
at most one re_exec call across the whole chain."""
|
|
1518
|
+
|
|
1519
|
+
re_exec_calls = 0
|
|
1520
|
+
|
|
1521
|
+
def fake_re_exec() -> None:
|
|
1522
|
+
nonlocal re_exec_calls
|
|
1523
|
+
re_exec_calls += 1
|
|
1524
|
+
# os.execv inherits the current environment into the "new" process —
|
|
1525
|
+
# simulate that by leaving WADE_SELF_UPGRADE_FROM set for the next call.
|
|
1526
|
+
|
|
1527
|
+
with (
|
|
1528
|
+
patch("wade.utils.install.detect_install_method", return_value=InstallMethod.UV_TOOL),
|
|
1529
|
+
patch("wade.utils.update_check.check_for_update", return_value="99.0.0"),
|
|
1530
|
+
patch("wade.utils.install.self_upgrade", return_value=True),
|
|
1531
|
+
patch("wade.utils.install.re_exec", side_effect=fake_re_exec),
|
|
1532
|
+
):
|
|
1533
|
+
first = _maybe_self_upgrade() # simulates the pre-upgrade invocation
|
|
1534
|
+
second = _maybe_self_upgrade() # simulates the post-re-exec invocation
|
|
1535
|
+
|
|
1536
|
+
assert first is True
|
|
1537
|
+
assert second is False
|
|
1538
|
+
assert re_exec_calls == 1
|
|
1539
|
+
assert "WADE_SELF_UPGRADE_FROM" not in os.environ
|
|
1540
|
+
|
|
1541
|
+
|
|
1435
1542
|
# ---------------------------------------------------------------------------
|
|
1436
1543
|
# Gitignore entries coverage
|
|
1437
1544
|
# ---------------------------------------------------------------------------
|
|
@@ -14,9 +14,11 @@ import pytest
|
|
|
14
14
|
|
|
15
15
|
from wade.utils.update_check import (
|
|
16
16
|
CHECK_INTERVAL_SECS,
|
|
17
|
+
_fetch_latest_version,
|
|
17
18
|
_is_newer,
|
|
18
19
|
_load_cache,
|
|
19
20
|
_save_cache,
|
|
21
|
+
_select_latest_non_yanked,
|
|
20
22
|
check_for_update,
|
|
21
23
|
maybe_print_update_hint,
|
|
22
24
|
)
|
|
@@ -185,6 +187,121 @@ class TestCheckForUpdate:
|
|
|
185
187
|
assert result == "2.0.0"
|
|
186
188
|
mock_fetch.assert_not_called()
|
|
187
189
|
|
|
190
|
+
def test_force_bypasses_fresh_cache_and_calls_fetcher(self, tmp_path: Path) -> None:
|
|
191
|
+
"""force=True skips a fresh cache entirely and hits the fetcher."""
|
|
192
|
+
cache_file = tmp_path / "update-check.json"
|
|
193
|
+
now = datetime.now(UTC).timestamp()
|
|
194
|
+
cache_file.write_text(
|
|
195
|
+
json.dumps({"timestamp": now, "latest_version": "2.0.0"}), encoding="utf-8"
|
|
196
|
+
)
|
|
197
|
+
with (
|
|
198
|
+
patch("wade.utils.update_check.CACHE_FILE", cache_file),
|
|
199
|
+
patch("wade.utils.update_check._fetch_latest_version", return_value="1.3.0"),
|
|
200
|
+
):
|
|
201
|
+
result = check_for_update("1.0.0", force=True)
|
|
202
|
+
|
|
203
|
+
assert result == "1.3.0"
|
|
204
|
+
|
|
205
|
+
def test_force_refreshes_cache_with_fetched_result(self, tmp_path: Path) -> None:
|
|
206
|
+
"""force=True still writes the freshly fetched result back to the cache."""
|
|
207
|
+
cache_file = tmp_path / "update-check.json"
|
|
208
|
+
now = datetime.now(UTC).timestamp()
|
|
209
|
+
cache_file.write_text(
|
|
210
|
+
json.dumps({"timestamp": now, "latest_version": "9.9.9"}), encoding="utf-8"
|
|
211
|
+
)
|
|
212
|
+
with (
|
|
213
|
+
patch("wade.utils.update_check.CACHE_FILE", cache_file),
|
|
214
|
+
patch("wade.utils.update_check._fetch_latest_version", return_value="1.3.0"),
|
|
215
|
+
):
|
|
216
|
+
check_for_update("1.0.0", force=True)
|
|
217
|
+
cached = _load_cache()
|
|
218
|
+
|
|
219
|
+
assert cached is not None
|
|
220
|
+
assert cached["latest_version"] == "1.3.0"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# _select_latest_non_yanked / _fetch_latest_version
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class TestSelectLatestNonYanked:
|
|
229
|
+
def test_picks_highest_version(self) -> None:
|
|
230
|
+
releases = {
|
|
231
|
+
"1.0.0": [{"yanked": False}],
|
|
232
|
+
"1.2.0": [{"yanked": False}],
|
|
233
|
+
"1.1.0": [{"yanked": False}],
|
|
234
|
+
}
|
|
235
|
+
assert _select_latest_non_yanked(releases) == "1.2.0"
|
|
236
|
+
|
|
237
|
+
def test_skips_yanked_highest_version(self) -> None:
|
|
238
|
+
"""A yanked highest version is not selected; the next-highest wins."""
|
|
239
|
+
releases = {
|
|
240
|
+
"1.0.0": [{"yanked": False}],
|
|
241
|
+
"1.2.0": [{"yanked": True}],
|
|
242
|
+
"1.1.0": [{"yanked": False}],
|
|
243
|
+
}
|
|
244
|
+
assert _select_latest_non_yanked(releases) == "1.1.0"
|
|
245
|
+
|
|
246
|
+
def test_all_yanked_returns_none(self) -> None:
|
|
247
|
+
releases = {
|
|
248
|
+
"1.0.0": [{"yanked": True}],
|
|
249
|
+
"1.1.0": [{"yanked": True}],
|
|
250
|
+
}
|
|
251
|
+
assert _select_latest_non_yanked(releases) is None
|
|
252
|
+
|
|
253
|
+
def test_release_with_no_files_is_skipped(self) -> None:
|
|
254
|
+
releases = {
|
|
255
|
+
"1.0.0": [{"yanked": False}],
|
|
256
|
+
"1.1.0": [],
|
|
257
|
+
}
|
|
258
|
+
assert _select_latest_non_yanked(releases) == "1.0.0"
|
|
259
|
+
|
|
260
|
+
def test_release_with_any_non_yanked_file_counts(self) -> None:
|
|
261
|
+
"""A release is usable if at least one of its files isn't yanked."""
|
|
262
|
+
releases = {
|
|
263
|
+
"1.0.0": [{"yanked": True}, {"yanked": False}],
|
|
264
|
+
}
|
|
265
|
+
assert _select_latest_non_yanked(releases) == "1.0.0"
|
|
266
|
+
|
|
267
|
+
def test_empty_releases_returns_none(self) -> None:
|
|
268
|
+
assert _select_latest_non_yanked({}) is None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TestFetchLatestVersion:
|
|
272
|
+
def test_ignores_yanked_release_from_pypi(self) -> None:
|
|
273
|
+
"""A yanked info.version is not treated as the latest — the fetcher
|
|
274
|
+
selects the highest non-yanked release instead."""
|
|
275
|
+
payload = json.dumps(
|
|
276
|
+
{
|
|
277
|
+
"info": {"version": "1.2.0"},
|
|
278
|
+
"releases": {
|
|
279
|
+
"1.0.0": [{"yanked": False}],
|
|
280
|
+
"1.2.0": [{"yanked": True}],
|
|
281
|
+
"1.1.0": [{"yanked": False}],
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
).encode("utf-8")
|
|
285
|
+
mock_resp = MagicMock()
|
|
286
|
+
mock_resp.read.return_value = payload
|
|
287
|
+
mock_resp.__enter__.return_value = mock_resp
|
|
288
|
+
mock_resp.__exit__.return_value = False
|
|
289
|
+
with patch("wade.utils.update_check.urlopen", return_value=mock_resp):
|
|
290
|
+
result = _fetch_latest_version()
|
|
291
|
+
|
|
292
|
+
assert result == "1.1.0"
|
|
293
|
+
|
|
294
|
+
def test_returns_none_when_releases_missing(self) -> None:
|
|
295
|
+
payload = json.dumps({"info": {"version": "1.0.0"}}).encode("utf-8")
|
|
296
|
+
mock_resp = MagicMock()
|
|
297
|
+
mock_resp.read.return_value = payload
|
|
298
|
+
mock_resp.__enter__.return_value = mock_resp
|
|
299
|
+
mock_resp.__exit__.return_value = False
|
|
300
|
+
with patch("wade.utils.update_check.urlopen", return_value=mock_resp):
|
|
301
|
+
result = _fetch_latest_version()
|
|
302
|
+
|
|
303
|
+
assert result is None
|
|
304
|
+
|
|
188
305
|
|
|
189
306
|
# ---------------------------------------------------------------------------
|
|
190
307
|
# maybe_print_update_hint
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|