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.
Files changed (274) hide show
  1. {wade_cli-0.30.1 → wade_cli-0.30.2}/CHANGELOG.md +6 -0
  2. {wade_cli-0.30.1 → wade_cli-0.30.2}/KNOWLEDGE.md +16 -0
  3. {wade_cli-0.30.1 → wade_cli-0.30.2}/KNOWLEDGE.ratings.yml +3 -3
  4. {wade_cli-0.30.1 → wade_cli-0.30.2}/PKG-INFO +1 -1
  5. {wade_cli-0.30.1 → wade_cli-0.30.2}/pyproject.toml +1 -1
  6. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/__init__.py +1 -1
  7. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/init_service.py +23 -1
  8. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/install.py +5 -0
  9. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/update_check.py +48 -21
  10. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_init.py +107 -0
  11. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_update_check.py +117 -0
  12. {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/auto-version.yml +0 -0
  13. {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/ci.yml +0 -0
  14. {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/pr-title-lint.yml +0 -0
  15. {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/publish.yml +0 -0
  16. {wade_cli-0.30.1 → wade_cli-0.30.2}/.github/workflows/release.yml +0 -0
  17. {wade_cli-0.30.1 → wade_cli-0.30.2}/.gitignore +0 -0
  18. {wade_cli-0.30.1 → wade_cli-0.30.2}/.pre-commit-config.yaml +0 -0
  19. {wade_cli-0.30.1 → wade_cli-0.30.2}/.wade.yml +0 -0
  20. {wade_cli-0.30.1 → wade_cli-0.30.2}/AGENTS.md +0 -0
  21. {wade_cli-0.30.1 → wade_cli-0.30.2}/CONTRIBUTING.md +0 -0
  22. {wade_cli-0.30.1 → wade_cli-0.30.2}/README.md +0 -0
  23. {wade_cli-0.30.1 → wade_cli-0.30.2}/assets/wade.png +0 -0
  24. {wade_cli-0.30.1 → wade_cli-0.30.2}/docker-compose.e2e.yml +0 -0
  25. {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/architecture.md +0 -0
  26. {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/documentation-policies.md +0 -0
  27. {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/extending.md +0 -0
  28. {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/skills-system.md +0 -0
  29. {wade_cli-0.30.1 → wade_cli-0.30.2}/docs/dev/testing.md +0 -0
  30. {wade_cli-0.30.1 → wade_cli-0.30.2}/install.sh +0 -0
  31. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/auto_version.py +0 -0
  32. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/changelog.py +0 -0
  33. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/check-all.sh +0 -0
  34. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/check.sh +0 -0
  35. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/fmt.sh +0 -0
  36. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/hooks/pre-push +0 -0
  37. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/install-hooks.sh +0 -0
  38. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/release.sh +0 -0
  39. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/setup-worktree.sh +0 -0
  40. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-e2e-docker.sh +0 -0
  41. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-e2e.sh +0 -0
  42. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-live-ai-taskr.sh +0 -0
  43. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-live-ai.sh +0 -0
  44. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test-live-gh.sh +0 -0
  45. {wade_cli-0.30.1 → wade_cli-0.30.2}/scripts/test.sh +0 -0
  46. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/__main__.py +0 -0
  47. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/__init__.py +0 -0
  48. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/admin.py +0 -0
  49. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/autocomplete.py +0 -0
  50. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/implementation_session.py +0 -0
  51. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/knowledge.py +0 -0
  52. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/main.py +0 -0
  53. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/plan_session.py +0 -0
  54. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/review.py +0 -0
  55. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/review_pr_comments_session.py +0 -0
  56. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/session_shared.py +0 -0
  57. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/task.py +0 -0
  58. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/cli/worktree.py +0 -0
  59. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/config/__init__.py +0 -0
  60. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/config/loader.py +0 -0
  61. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/config/migrations.py +0 -0
  62. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/config/schema.py +0 -0
  63. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/db/__init__.py +0 -0
  64. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/db/engine.py +0 -0
  65. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/db/repositories.py +0 -0
  66. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/db/tables.py +0 -0
  67. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/__init__.py +0 -0
  68. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/branch.py +0 -0
  69. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/pr.py +0 -0
  70. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/repo.py +0 -0
  71. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/stash.py +0 -0
  72. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/sync.py +0 -0
  73. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/git/worktree.py +0 -0
  74. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/hooks/__init__.py +0 -0
  75. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/hooks/plan_write_guard.py +0 -0
  76. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/hooks/worktree_guard.py +0 -0
  77. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/logging/__init__.py +0 -0
  78. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/logging/context.py +0 -0
  79. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/logging/setup.py +0 -0
  80. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/__init__.py +0 -0
  81. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/batch.py +0 -0
  82. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/config.py +0 -0
  83. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/delegation.py +0 -0
  84. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/deps.py +0 -0
  85. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/events.py +0 -0
  86. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/review.py +0 -0
  87. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/session.py +0 -0
  88. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/models/task.py +0 -0
  89. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/__init__.py +0 -0
  90. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/_pr_delegate.py +0 -0
  91. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/base.py +0 -0
  92. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/clickup.py +0 -0
  93. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/github.py +0 -0
  94. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/markdown.py +0 -0
  95. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/providers/registry.py +0 -0
  96. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/__init__.py +0 -0
  97. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/ai_resolution.py +0 -0
  98. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/batch_review_service.py +0 -0
  99. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/check_service.py +0 -0
  100. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/delegation_service.py +0 -0
  101. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/deps_service.py +0 -0
  102. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/__init__.py +0 -0
  103. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/batch.py +0 -0
  104. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/bootstrap.py +0 -0
  105. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/core.py +0 -0
  106. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/implementation_service/usage_tracking.py +0 -0
  107. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/knowledge_search.py +0 -0
  108. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/knowledge_service.py +0 -0
  109. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/plan_service.py +0 -0
  110. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/prompt_delivery.py +0 -0
  111. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/review_delegation_service.py +0 -0
  112. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/review_service.py +0 -0
  113. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/review_settle.py +0 -0
  114. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/smart_start.py +0 -0
  115. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/services/task_service.py +0 -0
  116. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/skills/__init__.py +0 -0
  117. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/skills/installer.py +0 -0
  118. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/skills/pointer.py +0 -0
  119. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/ui/__init__.py +0 -0
  120. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/ui/console.py +0 -0
  121. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/ui/prompts.py +0 -0
  122. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/__init__.py +0 -0
  123. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/clipboard.py +0 -0
  124. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/http.py +0 -0
  125. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/markdown.py +0 -0
  126. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/process.py +0 -0
  127. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/slug.py +0 -0
  128. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/terminal.py +0 -0
  129. {wade_cli-0.30.1 → wade_cli-0.30.2}/src/wade/utils/token_usage_markdown.py +0 -0
  130. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/agents-pointer.md +0 -0
  131. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/deps-analysis.md +0 -0
  132. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/implement-context.md +0 -0
  133. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/plan-session.md +0 -0
  134. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/review-batch.md +0 -0
  135. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/review-code.md +0 -0
  136. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/review-plan.md +0 -0
  137. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/prompts/review-pr-comments.md +0 -0
  138. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/setup-worktree.sh.example +0 -0
  139. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/_partials/review-enforcement-rule.md +0 -0
  140. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/_partials/review-implementation-closing-step.md +0 -0
  141. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/_partials/review-plan-step.md +0 -0
  142. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/_partials/user-interaction.md +0 -0
  143. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/deps/SKILL.md +0 -0
  144. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/implementation-session/SKILL.md +0 -0
  145. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/knowledge/SKILL.md +0 -0
  146. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/plan-session/SKILL.md +0 -0
  147. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/review-pr-comments-session/SKILL.md +0 -0
  148. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/task/SKILL.md +0 -0
  149. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/task/examples.md +0 -0
  150. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/skills/task/plan-format.md +0 -0
  151. {wade_cli-0.30.1 → wade_cli-0.30.2}/templates/statusline-command.sh +0 -0
  152. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/__init__.py +0 -0
  153. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/conftest.py +0 -0
  154. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/__init__.py +0 -0
  155. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/_support.py +0 -0
  156. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/conftest.py +0 -0
  157. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/mock_gh_script.py +0 -0
  158. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_admin_contract.py +0 -0
  159. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_check_contract.py +0 -0
  160. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_implement_work_done_contract.py +0 -0
  161. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_knowledge_contract.py +0 -0
  162. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_mock_gh_contract.py +0 -0
  163. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_plan_contract.py +0 -0
  164. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_review_contract.py +0 -0
  165. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_smart_start_contract.py +0 -0
  166. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_task_contract.py +0 -0
  167. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/e2e/test_work_sync_list_contract.py +0 -0
  168. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/live/taskr_header_hi_body.md +0 -0
  169. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/live/taskr_header_howdy_body.md +0 -0
  170. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/claude_session.txt +0 -0
  171. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/codex_session.txt +0 -0
  172. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/copilot_session.txt +0 -0
  173. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/gemini_session.txt +0 -0
  174. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/fixtures/transcripts/generic_session.txt +0 -0
  175. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/__init__.py +0 -0
  176. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_autostash.py +0 -0
  177. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_check.py +0 -0
  178. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_git.py +0 -0
  179. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_implementation_lifecycle.py +0 -0
  180. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_init.py +0 -0
  181. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_knowledge.py +0 -0
  182. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_markdown_provider.py +0 -0
  183. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/integration/test_skill_install.py +0 -0
  184. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/live/__init__.py +0 -0
  185. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/live/test_wade_live_ai.py +0 -0
  186. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/live/test_wade_live_ai_taskr.py +0 -0
  187. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/live/test_wade_live_gh.py +0 -0
  188. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/test_cli_basics.py +0 -0
  189. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/__init__.py +0 -0
  190. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/conftest.py +0 -0
  191. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/__init__.py +0 -0
  192. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_done_review_hint.py +0 -0
  193. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_knowledge_cli.py +0 -0
  194. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_review_cli.py +0 -0
  195. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_session_subapps.py +0 -0
  196. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_shell_init.py +0 -0
  197. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_task_cli.py +0 -0
  198. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_work_ai_flags.py +0 -0
  199. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_cli/test_work_cli.py +0 -0
  200. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_config/__init__.py +0 -0
  201. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_config/test_loader.py +0 -0
  202. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_config/test_migrations.py +0 -0
  203. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_config/test_migrations_atomic.py +0 -0
  204. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_console.py +0 -0
  205. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_db/__init__.py +0 -0
  206. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_db/test_repository_transactions.py +0 -0
  207. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_effort.py +0 -0
  208. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/__init__.py +0 -0
  209. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_branch_slugify.py +0 -0
  210. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_detached_worktree.py +0 -0
  211. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_pr_base.py +0 -0
  212. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_repo.py +0 -0
  213. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_scaffold_commit.py +0 -0
  214. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_sync_conflicted_files.py +0 -0
  215. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_git/test_worktree_remove.py +0 -0
  216. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_hooks/__init__.py +0 -0
  217. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_hooks/test_plan_write_guard.py +0 -0
  218. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_hooks/test_worktree_guard.py +0 -0
  219. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/__init__.py +0 -0
  220. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_batch.py +0 -0
  221. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_config_models.py +0 -0
  222. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_db.py +0 -0
  223. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_delegation.py +0 -0
  224. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_deps_models.py +0 -0
  225. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_deps_partition.py +0 -0
  226. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_review.py +0 -0
  227. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_models/test_task_models.py +0 -0
  228. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/__init__.py +0 -0
  229. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/test_clickup.py +0 -0
  230. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/test_github.py +0 -0
  231. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/test_github_pr.py +0 -0
  232. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_providers/test_markdown.py +0 -0
  233. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/__init__.py +0 -0
  234. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_ai_resolution.py +0 -0
  235. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_batch_review_service.py +0 -0
  236. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_bootstrap_allowlist.py +0 -0
  237. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_bootstrap_hook_timeout.py +0 -0
  238. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_catchup.py +0 -0
  239. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_check.py +0 -0
  240. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_delegation_service.py +0 -0
  241. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_deps_service.py +0 -0
  242. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_done_plan_flag.py +0 -0
  243. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_done_via_direct_cleanup.py +0 -0
  244. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_done_sync.py +0 -0
  245. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_service.py +0 -0
  246. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_implementation_service_resume.py +0 -0
  247. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_init_shell_integration.py +0 -0
  248. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_knowledge_search.py +0 -0
  249. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_knowledge_service.py +0 -0
  250. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_plan_service.py +0 -0
  251. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_poll_for_reviews.py +0 -0
  252. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_post_implementation_lifecycle.py +0 -0
  253. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_pr_summary_path.py +0 -0
  254. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_preserve_session_data.py +0 -0
  255. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_prompt_delivery.py +0 -0
  256. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_review_delegation_service.py +0 -0
  257. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_review_service.py +0 -0
  258. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_smart_start.py +0 -0
  259. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_smart_start_resume.py +0 -0
  260. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_sync_base_branch.py +0 -0
  261. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_task_service.py +0 -0
  262. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_tracked_managed_files.py +0 -0
  263. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_services/test_worktree_gitignore.py +0 -0
  264. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_skills/test_managed_gitignore.py +0 -0
  265. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/__init__.py +0 -0
  266. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_http.py +0 -0
  267. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_install.py +0 -0
  268. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_markdown.py +0 -0
  269. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_process.py +0 -0
  270. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_prompts.py +0 -0
  271. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_slug.py +0 -0
  272. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_terminal.py +0 -0
  273. {wade_cli-0.30.1 → wade_cli-0.30.2}/tests/unit/test_utils/test_terminal_launch.py +0 -0
  274. {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: 1
12
+ up: 2
13
13
  54326c98:
14
14
  down: 1
15
15
  up: 0
16
16
  b61e247e:
17
17
  down: 0
18
- up: 2
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: 2
30
+ up: 3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wade-cli
3
- Version: 0.30.1
3
+ Version: 0.30.2
4
4
  Summary: WADE — Workflow for AI-Driven Engineering
5
5
  Project-URL: Homepage, https://github.com/ivanviragine/wade
6
6
  Project-URL: Repository, https://github.com/ivanviragine/wade
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wade-cli"
7
- version = "0.30.1"
7
+ version = "0.30.2"
8
8
  description = "WADE — Workflow for AI-Driven Engineering"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """WADE (Workflow for AI-Driven Engineering) — AI-agent-driven git workflow management CLI."""
2
2
 
3
- __version__ = "0.30.1"
3
+ __version__ = "0.30.2"
@@ -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 _is_newer(candidate: str, current: str) -> bool:
30
- """Return True if candidate version is strictly newer than current.
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 comparison — sufficient for standard semver.
33
- Falls back to False on any parse error.
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
- return to_tuple(candidate) > to_tuple(current)
41
- except (ValueError, TypeError):
42
- return False
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
- version = data["info"]["version"]
81
- if isinstance(version, str):
82
- return version
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
- cached = _load_cache()
102
- if cached is not None:
103
- age = now - float(str(cached["timestamp"]))
104
- if age < CHECK_INTERVAL_SECS:
105
- latest = str(cached["latest_version"])
106
- return latest if _is_newer(latest, current_version) else None
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