claude-dev-env 2.3.0 → 2.5.0

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 (290) hide show
  1. package/CLAUDE.md +53 -48
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +3 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +3 -4
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -1,507 +1,507 @@
1
- """Fixture-driven tests for the conditional Codex convergence gate.
2
-
3
- Covers: required-and-clean, required-and-dirty, skipped-by-threshold,
4
- skipped-by-token, skipped-by-down. Threshold comes from the probe constant
5
- via ``is_codex_review_required`` — this module never inlines the percent.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import importlib.util
11
- import json
12
- import sys
13
- from pathlib import Path
14
- from types import ModuleType
15
-
16
- import pytest
17
-
18
- import _pr_converge_path_setup # noqa: F401
19
- from codex_review_scripts_constants.codex_usage_probe_constants import (
20
- WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
21
- )
22
- from pr_converge_scripts_constants.convergence_gate_constants import (
23
- MINIMUM_ABBREVIATED_SHA_LENGTH,
24
- )
25
-
26
- _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
27
- _PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
28
-
29
- CLEAN_LABEL = "Clean \u2014 no findings"
30
- CLEAN_BODY = (
31
- f"**Bugteam audit completed** \u2014 {CLEAN_LABEL}\n\n"
32
- "No findings.\n"
33
- )
34
- HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
35
- OTHER_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
36
- PERCENT_ABOVE_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT) + 1.0
37
- PERCENT_AT_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT)
38
-
39
-
40
- def _load_module() -> ModuleType:
41
- for each_cached_name in [
42
- each_key
43
- for each_key in list(sys.modules)
44
- if each_key == "config" or each_key.startswith("config.")
45
- ]:
46
- sys.modules.pop(each_cached_name, None)
47
- if str(_PR_CONVERGE_DIRECTORY) in sys.path:
48
- sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
49
- sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
50
- module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
51
- spec = importlib.util.spec_from_file_location(
52
- "check_convergence_codex_under_test", module_path
53
- )
54
- assert spec is not None
55
- assert spec.loader is not None
56
- module = importlib.util.module_from_spec(spec)
57
- spec.loader.exec_module(module)
58
- return module
59
-
60
-
61
- check_convergence = _load_module()
62
-
63
-
64
- def _write_fixture(
65
- tmp_path: Path,
66
- *,
67
- codex_percent_left: float | None,
68
- codex_clean_at: str | None,
69
- filename: str = "codex-gate.json",
70
- ) -> Path:
71
- fixture_path = tmp_path / filename
72
- payload: dict[str, object] = {
73
- "head_sha": HEAD_SHA,
74
- "pr_object": {"mergeable": True, "mergeable_state": "clean"},
75
- "reviews": [
76
- {
77
- "id": 1,
78
- "body": CLEAN_BODY,
79
- "commit_id": HEAD_SHA,
80
- }
81
- ],
82
- "unresolved_bot_threads_passed": True,
83
- "unresolved_bot_threads_detail": "0 unresolved",
84
- "pending_reviews_passed": True,
85
- "pending_reviews_detail": "none pending",
86
- "codex_percent_left": codex_percent_left,
87
- "codex_clean_at": codex_clean_at,
88
- }
89
- fixture_path.write_text(json.dumps(payload), encoding="utf-8")
90
- return fixture_path
91
-
92
-
93
- def should_pass_when_codex_required_and_clean(
94
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
95
- ) -> None:
96
- fixture_path = _write_fixture(
97
- tmp_path,
98
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
99
- codex_clean_at=HEAD_SHA,
100
- filename="required-clean.json",
101
- )
102
- exit_code = check_convergence.main(
103
- [
104
- "--owner",
105
- "jl-cmd",
106
- "--repo",
107
- "claude-dev-env",
108
- "--pr-number",
109
- "111",
110
- "--fixture",
111
- str(fixture_path),
112
- "--bugbot-down",
113
- "--copilot-down",
114
- ]
115
- )
116
- captured = capsys.readouterr().out
117
- assert exit_code == 0
118
- assert "codex_clean_at == current_head: PASS" in captured
119
- assert "clean at" in captured
120
- assert "All pre-conditions met" in captured
121
-
122
-
123
- def should_pass_without_reading_usage_when_the_clean_stamp_is_on_head(
124
- monkeypatch: pytest.MonkeyPatch,
125
- ) -> None:
126
- def _probe_that_must_not_run() -> float | None:
127
- raise AssertionError(
128
- "the weekly usage probe must not run when codex_clean_at is on HEAD"
129
- )
130
-
131
- monkeypatch.setattr(
132
- check_convergence, "_probe_codex_percent_left", _probe_that_must_not_run
133
- )
134
-
135
- is_passed, detail = check_convergence._evaluate_codex_clean(
136
- read_percent_left=check_convergence._probe_codex_percent_left,
137
- codex_clean_at=HEAD_SHA,
138
- head_sha=HEAD_SHA,
139
- )
140
-
141
- assert is_passed is True
142
- assert "clean at" in detail
143
-
144
-
145
- def should_match_a_clean_stamp_whose_hex_case_differs_from_the_head_sha() -> None:
146
- assert check_convergence._sha_matches_head(HEAD_SHA.upper(), HEAD_SHA) is True
147
-
148
-
149
- def should_fail_when_codex_clean_stamp_is_shorter_than_an_abbreviated_sha(
150
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
151
- ) -> None:
152
- fixture_path = _write_fixture(
153
- tmp_path,
154
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
155
- codex_clean_at=HEAD_SHA[:1],
156
- filename="required-short-stamp.json",
157
- )
158
- exit_code = check_convergence.main(
159
- [
160
- "--owner",
161
- "jl-cmd",
162
- "--repo",
163
- "claude-dev-env",
164
- "--pr-number",
165
- "111",
166
- "--fixture",
167
- str(fixture_path),
168
- "--bugbot-down",
169
- "--copilot-down",
170
- ]
171
- )
172
- captured = capsys.readouterr().out
173
- assert exit_code == 1
174
- assert "codex_clean_at == current_head: FAIL" in captured
175
-
176
-
177
- def should_pass_when_codex_clean_stamp_is_an_abbreviated_head_sha(
178
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
179
- ) -> None:
180
- fixture_path = _write_fixture(
181
- tmp_path,
182
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
183
- codex_clean_at=HEAD_SHA[:MINIMUM_ABBREVIATED_SHA_LENGTH],
184
- filename="required-abbreviated-stamp.json",
185
- )
186
- exit_code = check_convergence.main(
187
- [
188
- "--owner",
189
- "jl-cmd",
190
- "--repo",
191
- "claude-dev-env",
192
- "--pr-number",
193
- "111",
194
- "--fixture",
195
- str(fixture_path),
196
- "--bugbot-down",
197
- "--copilot-down",
198
- ]
199
- )
200
- captured = capsys.readouterr().out
201
- assert exit_code == 0
202
- assert "codex_clean_at == current_head: PASS" in captured
203
-
204
-
205
- def should_fail_when_codex_required_and_dirty(
206
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
207
- ) -> None:
208
- fixture_path = _write_fixture(
209
- tmp_path,
210
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
211
- codex_clean_at=OTHER_SHA,
212
- filename="required-dirty.json",
213
- )
214
- exit_code = check_convergence.main(
215
- [
216
- "--owner",
217
- "jl-cmd",
218
- "--repo",
219
- "claude-dev-env",
220
- "--pr-number",
221
- "111",
222
- "--fixture",
223
- str(fixture_path),
224
- "--bugbot-down",
225
- "--copilot-down",
226
- ]
227
- )
228
- captured = capsys.readouterr().out
229
- assert exit_code == 1
230
- assert "codex_clean_at == current_head: FAIL" in captured
231
- assert "no codex clean on" in captured
232
- assert "do not mark ready" in captured
233
-
234
-
235
- def should_skip_when_usage_at_or_below_threshold(
236
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
237
- ) -> None:
238
- fixture_path = _write_fixture(
239
- tmp_path,
240
- codex_percent_left=PERCENT_AT_THRESHOLD,
241
- codex_clean_at=None,
242
- filename="skipped-threshold.json",
243
- )
244
- exit_code = check_convergence.main(
245
- [
246
- "--owner",
247
- "jl-cmd",
248
- "--repo",
249
- "claude-dev-env",
250
- "--pr-number",
251
- "111",
252
- "--fixture",
253
- str(fixture_path),
254
- "--bugbot-down",
255
- "--copilot-down",
256
- ]
257
- )
258
- captured = capsys.readouterr().out
259
- assert exit_code == 0
260
- assert "codex_clean_at == current_head: PASS" in captured
261
- assert "skipped (codex review not required)" in captured
262
-
263
-
264
- def should_skip_when_codex_percent_left_is_null(
265
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
266
- ) -> None:
267
- fixture_path = _write_fixture(
268
- tmp_path,
269
- codex_percent_left=None,
270
- codex_clean_at=None,
271
- filename="skipped-null.json",
272
- )
273
- exit_code = check_convergence.main(
274
- [
275
- "--owner",
276
- "jl-cmd",
277
- "--repo",
278
- "claude-dev-env",
279
- "--pr-number",
280
- "111",
281
- "--fixture",
282
- str(fixture_path),
283
- "--bugbot-down",
284
- "--copilot-down",
285
- ]
286
- )
287
- captured = capsys.readouterr().out
288
- assert exit_code == 0
289
- assert "skipped (codex review not required)" in captured
290
-
291
-
292
- def should_bypass_when_codex_token_disables_reviewer(
293
- tmp_path: Path,
294
- monkeypatch: pytest.MonkeyPatch,
295
- capsys: pytest.CaptureFixture[str],
296
- ) -> None:
297
- monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
298
- fixture_path = _write_fixture(
299
- tmp_path,
300
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
301
- codex_clean_at=None,
302
- filename="skipped-token.json",
303
- )
304
- exit_code = check_convergence.main(
305
- [
306
- "--owner",
307
- "jl-cmd",
308
- "--repo",
309
- "claude-dev-env",
310
- "--pr-number",
311
- "111",
312
- "--fixture",
313
- str(fixture_path),
314
- "--bugbot-down",
315
- "--copilot-down",
316
- ]
317
- )
318
- captured = capsys.readouterr().out
319
- assert exit_code == 0
320
- assert "bypassed (codex_down)" in captured
321
- assert "no codex clean on" not in captured
322
-
323
-
324
- def should_bypass_when_codex_down_flag_set(
325
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
326
- ) -> None:
327
- fixture_path = _write_fixture(
328
- tmp_path,
329
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
330
- codex_clean_at=None,
331
- filename="skipped-down.json",
332
- )
333
- exit_code = check_convergence.main(
334
- [
335
- "--owner",
336
- "jl-cmd",
337
- "--repo",
338
- "claude-dev-env",
339
- "--pr-number",
340
- "111",
341
- "--fixture",
342
- str(fixture_path),
343
- "--bugbot-down",
344
- "--copilot-down",
345
- "--codex-down",
346
- ]
347
- )
348
- captured = capsys.readouterr().out
349
- assert exit_code == 0
350
- assert "bypassed (codex_down)" in captured
351
-
352
-
353
- def should_resolve_codex_down_true_when_flag_set(
354
- monkeypatch: pytest.MonkeyPatch,
355
- ) -> None:
356
- monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
357
- monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
358
- assert check_convergence._resolve_codex_down(
359
- True, check_convergence._read_job_state()
360
- ) is True
361
-
362
-
363
- def should_resolve_codex_down_true_when_env_disables_codex(
364
- monkeypatch: pytest.MonkeyPatch,
365
- ) -> None:
366
- monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
367
- monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
368
- assert check_convergence._resolve_codex_down(
369
- False, check_convergence._read_job_state()
370
- ) is True
371
-
372
-
373
- def should_resolve_codex_down_false_when_flag_unset_and_env_empty(
374
- monkeypatch: pytest.MonkeyPatch,
375
- ) -> None:
376
- monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
377
- monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
378
- assert check_convergence._resolve_codex_down(
379
- False, check_convergence._read_job_state()
380
- ) is False
381
-
382
-
383
- def should_resolve_codex_down_true_when_job_state_sticky_down(
384
- tmp_path: Path,
385
- monkeypatch: pytest.MonkeyPatch,
386
- ) -> None:
387
- monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
388
- monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
389
- (tmp_path / "pr-converge-state.json").write_text(
390
- json.dumps({"codex_down": True}),
391
- encoding="utf-8",
392
- )
393
- assert check_convergence._resolve_codex_down(
394
- False, check_convergence._read_job_state()
395
- ) is True
396
-
397
-
398
- def should_resolve_codex_down_false_when_job_state_codex_down_is_false(
399
- tmp_path: Path,
400
- monkeypatch: pytest.MonkeyPatch,
401
- ) -> None:
402
- monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
403
- monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
404
- (tmp_path / "pr-converge-state.json").write_text(
405
- json.dumps({"codex_down": False}),
406
- encoding="utf-8",
407
- )
408
- assert check_convergence._resolve_codex_down(
409
- False, check_convergence._read_job_state()
410
- ) is False
411
-
412
-
413
- def should_bypass_when_job_state_codex_down_is_true(
414
- tmp_path: Path,
415
- monkeypatch: pytest.MonkeyPatch,
416
- capsys: pytest.CaptureFixture[str],
417
- ) -> None:
418
- monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
419
- monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
420
- (tmp_path / "pr-converge-state.json").write_text(
421
- json.dumps({"codex_down": True}),
422
- encoding="utf-8",
423
- )
424
- fixture_path = _write_fixture(
425
- tmp_path,
426
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
427
- codex_clean_at=None,
428
- filename="job-state-codex-down.json",
429
- )
430
- exit_code = check_convergence.main(
431
- [
432
- "--owner",
433
- "jl-cmd",
434
- "--repo",
435
- "claude-dev-env",
436
- "--pr-number",
437
- "111",
438
- "--fixture",
439
- str(fixture_path),
440
- "--bugbot-down",
441
- "--copilot-down",
442
- ]
443
- )
444
- captured = capsys.readouterr().out
445
- assert exit_code == 0
446
- assert "bypassed (codex_down)" in captured
447
- assert "no codex clean on" not in captured
448
-
449
-
450
- def should_fail_when_job_state_lacks_codex_down_and_clean_stamp(
451
- tmp_path: Path,
452
- monkeypatch: pytest.MonkeyPatch,
453
- capsys: pytest.CaptureFixture[str],
454
- ) -> None:
455
- monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
456
- monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
457
- (tmp_path / "pr-converge-state.json").write_text(
458
- json.dumps({}),
459
- encoding="utf-8",
460
- )
461
- fixture_path = _write_fixture(
462
- tmp_path,
463
- codex_percent_left=PERCENT_ABOVE_THRESHOLD,
464
- codex_clean_at=None,
465
- filename="job-state-no-down.json",
466
- )
467
- exit_code = check_convergence.main(
468
- [
469
- "--owner",
470
- "jl-cmd",
471
- "--repo",
472
- "claude-dev-env",
473
- "--pr-number",
474
- "111",
475
- "--fixture",
476
- str(fixture_path),
477
- "--bugbot-down",
478
- "--copilot-down",
479
- ]
480
- )
481
- captured = capsys.readouterr().out
482
- assert exit_code == 1
483
- assert "codex_clean_at == current_head: FAIL" in captured
484
- assert "no codex clean on" in captured
485
-
486
-
487
- def should_accept_codex_down_flag_in_parsed_arguments() -> None:
488
- arguments = check_convergence.parse_arguments(
489
- ["--owner", "o", "--repo", "r", "--pr-number", "1", "--codex-down"]
490
- )
491
- assert arguments.codex_down is True
492
-
493
-
494
- def should_accept_codex_clean_at_flag_in_parsed_arguments() -> None:
495
- arguments = check_convergence.parse_arguments(
496
- [
497
- "--owner",
498
- "o",
499
- "--repo",
500
- "r",
501
- "--pr-number",
502
- "1",
503
- "--codex-clean-at",
504
- HEAD_SHA,
505
- ]
506
- )
507
- assert arguments.codex_clean_at == HEAD_SHA
1
+ """Fixture-driven tests for the conditional Codex convergence gate.
2
+
3
+ Covers: required-and-clean, required-and-dirty, skipped-by-threshold,
4
+ skipped-by-token, skipped-by-down. Threshold comes from the probe constant
5
+ via ``is_codex_review_required`` — this module never inlines the percent.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+
16
+ import pytest
17
+
18
+ import _pr_converge_path_setup # noqa: F401
19
+ from codex_review_scripts_constants.codex_usage_probe_constants import (
20
+ WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
21
+ )
22
+ from pr_converge_scripts_constants.convergence_gate_constants import (
23
+ MINIMUM_ABBREVIATED_SHA_LENGTH,
24
+ )
25
+
26
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
27
+ _PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
28
+
29
+ CLEAN_LABEL = "Clean \u2014 no findings"
30
+ CLEAN_BODY = (
31
+ f"**Bugteam audit completed** \u2014 {CLEAN_LABEL}\n\n"
32
+ "No findings.\n"
33
+ )
34
+ HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
35
+ OTHER_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
36
+ PERCENT_ABOVE_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT) + 1.0
37
+ PERCENT_AT_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT)
38
+
39
+
40
+ def _load_module() -> ModuleType:
41
+ for each_cached_name in [
42
+ each_key
43
+ for each_key in list(sys.modules)
44
+ if each_key == "config" or each_key.startswith("config.")
45
+ ]:
46
+ sys.modules.pop(each_cached_name, None)
47
+ if str(_PR_CONVERGE_DIRECTORY) in sys.path:
48
+ sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
49
+ sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
50
+ module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
51
+ spec = importlib.util.spec_from_file_location(
52
+ "check_convergence_codex_under_test", module_path
53
+ )
54
+ assert spec is not None
55
+ assert spec.loader is not None
56
+ module = importlib.util.module_from_spec(spec)
57
+ spec.loader.exec_module(module)
58
+ return module
59
+
60
+
61
+ check_convergence = _load_module()
62
+
63
+
64
+ def _write_fixture(
65
+ tmp_path: Path,
66
+ *,
67
+ codex_percent_left: float | None,
68
+ codex_clean_at: str | None,
69
+ filename: str = "codex-gate.json",
70
+ ) -> Path:
71
+ fixture_path = tmp_path / filename
72
+ payload: dict[str, object] = {
73
+ "head_sha": HEAD_SHA,
74
+ "pr_object": {"mergeable": True, "mergeable_state": "clean"},
75
+ "reviews": [
76
+ {
77
+ "id": 1,
78
+ "body": CLEAN_BODY,
79
+ "commit_id": HEAD_SHA,
80
+ }
81
+ ],
82
+ "unresolved_bot_threads_passed": True,
83
+ "unresolved_bot_threads_detail": "0 unresolved",
84
+ "pending_reviews_passed": True,
85
+ "pending_reviews_detail": "none pending",
86
+ "codex_percent_left": codex_percent_left,
87
+ "codex_clean_at": codex_clean_at,
88
+ }
89
+ fixture_path.write_text(json.dumps(payload), encoding="utf-8")
90
+ return fixture_path
91
+
92
+
93
+ def should_pass_when_codex_required_and_clean(
94
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
95
+ ) -> None:
96
+ fixture_path = _write_fixture(
97
+ tmp_path,
98
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
99
+ codex_clean_at=HEAD_SHA,
100
+ filename="required-clean.json",
101
+ )
102
+ exit_code = check_convergence.main(
103
+ [
104
+ "--owner",
105
+ "jl-cmd",
106
+ "--repo",
107
+ "claude-dev-env",
108
+ "--pr-number",
109
+ "111",
110
+ "--fixture",
111
+ str(fixture_path),
112
+ "--bugbot-down",
113
+ "--copilot-down",
114
+ ]
115
+ )
116
+ captured = capsys.readouterr().out
117
+ assert exit_code == 0
118
+ assert "codex_clean_at == current_head: PASS" in captured
119
+ assert "clean at" in captured
120
+ assert "All pre-conditions met" in captured
121
+
122
+
123
+ def should_pass_without_reading_usage_when_the_clean_stamp_is_on_head(
124
+ monkeypatch: pytest.MonkeyPatch,
125
+ ) -> None:
126
+ def _probe_that_must_not_run() -> float | None:
127
+ raise AssertionError(
128
+ "the weekly usage probe must not run when codex_clean_at is on HEAD"
129
+ )
130
+
131
+ monkeypatch.setattr(
132
+ check_convergence, "_probe_codex_percent_left", _probe_that_must_not_run
133
+ )
134
+
135
+ is_passed, detail = check_convergence._evaluate_codex_clean(
136
+ read_percent_left=check_convergence._probe_codex_percent_left,
137
+ codex_clean_at=HEAD_SHA,
138
+ head_sha=HEAD_SHA,
139
+ )
140
+
141
+ assert is_passed is True
142
+ assert "clean at" in detail
143
+
144
+
145
+ def should_match_a_clean_stamp_whose_hex_case_differs_from_the_head_sha() -> None:
146
+ assert check_convergence._sha_matches_head(HEAD_SHA.upper(), HEAD_SHA) is True
147
+
148
+
149
+ def should_fail_when_codex_clean_stamp_is_shorter_than_an_abbreviated_sha(
150
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
151
+ ) -> None:
152
+ fixture_path = _write_fixture(
153
+ tmp_path,
154
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
155
+ codex_clean_at=HEAD_SHA[:1],
156
+ filename="required-short-stamp.json",
157
+ )
158
+ exit_code = check_convergence.main(
159
+ [
160
+ "--owner",
161
+ "jl-cmd",
162
+ "--repo",
163
+ "claude-dev-env",
164
+ "--pr-number",
165
+ "111",
166
+ "--fixture",
167
+ str(fixture_path),
168
+ "--bugbot-down",
169
+ "--copilot-down",
170
+ ]
171
+ )
172
+ captured = capsys.readouterr().out
173
+ assert exit_code == 1
174
+ assert "codex_clean_at == current_head: FAIL" in captured
175
+
176
+
177
+ def should_pass_when_codex_clean_stamp_is_an_abbreviated_head_sha(
178
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
179
+ ) -> None:
180
+ fixture_path = _write_fixture(
181
+ tmp_path,
182
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
183
+ codex_clean_at=HEAD_SHA[:MINIMUM_ABBREVIATED_SHA_LENGTH],
184
+ filename="required-abbreviated-stamp.json",
185
+ )
186
+ exit_code = check_convergence.main(
187
+ [
188
+ "--owner",
189
+ "jl-cmd",
190
+ "--repo",
191
+ "claude-dev-env",
192
+ "--pr-number",
193
+ "111",
194
+ "--fixture",
195
+ str(fixture_path),
196
+ "--bugbot-down",
197
+ "--copilot-down",
198
+ ]
199
+ )
200
+ captured = capsys.readouterr().out
201
+ assert exit_code == 0
202
+ assert "codex_clean_at == current_head: PASS" in captured
203
+
204
+
205
+ def should_fail_when_codex_required_and_dirty(
206
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
207
+ ) -> None:
208
+ fixture_path = _write_fixture(
209
+ tmp_path,
210
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
211
+ codex_clean_at=OTHER_SHA,
212
+ filename="required-dirty.json",
213
+ )
214
+ exit_code = check_convergence.main(
215
+ [
216
+ "--owner",
217
+ "jl-cmd",
218
+ "--repo",
219
+ "claude-dev-env",
220
+ "--pr-number",
221
+ "111",
222
+ "--fixture",
223
+ str(fixture_path),
224
+ "--bugbot-down",
225
+ "--copilot-down",
226
+ ]
227
+ )
228
+ captured = capsys.readouterr().out
229
+ assert exit_code == 1
230
+ assert "codex_clean_at == current_head: FAIL" in captured
231
+ assert "no codex clean on" in captured
232
+ assert "do not mark ready" in captured
233
+
234
+
235
+ def should_skip_when_usage_at_or_below_threshold(
236
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
237
+ ) -> None:
238
+ fixture_path = _write_fixture(
239
+ tmp_path,
240
+ codex_percent_left=PERCENT_AT_THRESHOLD,
241
+ codex_clean_at=None,
242
+ filename="skipped-threshold.json",
243
+ )
244
+ exit_code = check_convergence.main(
245
+ [
246
+ "--owner",
247
+ "jl-cmd",
248
+ "--repo",
249
+ "claude-dev-env",
250
+ "--pr-number",
251
+ "111",
252
+ "--fixture",
253
+ str(fixture_path),
254
+ "--bugbot-down",
255
+ "--copilot-down",
256
+ ]
257
+ )
258
+ captured = capsys.readouterr().out
259
+ assert exit_code == 0
260
+ assert "codex_clean_at == current_head: PASS" in captured
261
+ assert "skipped (codex review not required)" in captured
262
+
263
+
264
+ def should_skip_when_codex_percent_left_is_null(
265
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
266
+ ) -> None:
267
+ fixture_path = _write_fixture(
268
+ tmp_path,
269
+ codex_percent_left=None,
270
+ codex_clean_at=None,
271
+ filename="skipped-null.json",
272
+ )
273
+ exit_code = check_convergence.main(
274
+ [
275
+ "--owner",
276
+ "jl-cmd",
277
+ "--repo",
278
+ "claude-dev-env",
279
+ "--pr-number",
280
+ "111",
281
+ "--fixture",
282
+ str(fixture_path),
283
+ "--bugbot-down",
284
+ "--copilot-down",
285
+ ]
286
+ )
287
+ captured = capsys.readouterr().out
288
+ assert exit_code == 0
289
+ assert "skipped (codex review not required)" in captured
290
+
291
+
292
+ def should_bypass_when_codex_token_disables_reviewer(
293
+ tmp_path: Path,
294
+ monkeypatch: pytest.MonkeyPatch,
295
+ capsys: pytest.CaptureFixture[str],
296
+ ) -> None:
297
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
298
+ fixture_path = _write_fixture(
299
+ tmp_path,
300
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
301
+ codex_clean_at=None,
302
+ filename="skipped-token.json",
303
+ )
304
+ exit_code = check_convergence.main(
305
+ [
306
+ "--owner",
307
+ "jl-cmd",
308
+ "--repo",
309
+ "claude-dev-env",
310
+ "--pr-number",
311
+ "111",
312
+ "--fixture",
313
+ str(fixture_path),
314
+ "--bugbot-down",
315
+ "--copilot-down",
316
+ ]
317
+ )
318
+ captured = capsys.readouterr().out
319
+ assert exit_code == 0
320
+ assert "bypassed (codex_down)" in captured
321
+ assert "no codex clean on" not in captured
322
+
323
+
324
+ def should_bypass_when_codex_down_flag_set(
325
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
326
+ ) -> None:
327
+ fixture_path = _write_fixture(
328
+ tmp_path,
329
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
330
+ codex_clean_at=None,
331
+ filename="skipped-down.json",
332
+ )
333
+ exit_code = check_convergence.main(
334
+ [
335
+ "--owner",
336
+ "jl-cmd",
337
+ "--repo",
338
+ "claude-dev-env",
339
+ "--pr-number",
340
+ "111",
341
+ "--fixture",
342
+ str(fixture_path),
343
+ "--bugbot-down",
344
+ "--copilot-down",
345
+ "--codex-down",
346
+ ]
347
+ )
348
+ captured = capsys.readouterr().out
349
+ assert exit_code == 0
350
+ assert "bypassed (codex_down)" in captured
351
+
352
+
353
+ def should_resolve_codex_down_true_when_flag_set(
354
+ monkeypatch: pytest.MonkeyPatch,
355
+ ) -> None:
356
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
357
+ monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
358
+ assert check_convergence._resolve_codex_down(
359
+ True, check_convergence._read_job_state()
360
+ ) is True
361
+
362
+
363
+ def should_resolve_codex_down_true_when_env_disables_codex(
364
+ monkeypatch: pytest.MonkeyPatch,
365
+ ) -> None:
366
+ monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
367
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
368
+ assert check_convergence._resolve_codex_down(
369
+ False, check_convergence._read_job_state()
370
+ ) is True
371
+
372
+
373
+ def should_resolve_codex_down_false_when_flag_unset_and_env_empty(
374
+ monkeypatch: pytest.MonkeyPatch,
375
+ ) -> None:
376
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
377
+ monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
378
+ assert check_convergence._resolve_codex_down(
379
+ False, check_convergence._read_job_state()
380
+ ) is False
381
+
382
+
383
+ def should_resolve_codex_down_true_when_job_state_sticky_down(
384
+ tmp_path: Path,
385
+ monkeypatch: pytest.MonkeyPatch,
386
+ ) -> None:
387
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
388
+ monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
389
+ (tmp_path / "pr-converge-state.json").write_text(
390
+ json.dumps({"codex_down": True}),
391
+ encoding="utf-8",
392
+ )
393
+ assert check_convergence._resolve_codex_down(
394
+ False, check_convergence._read_job_state()
395
+ ) is True
396
+
397
+
398
+ def should_resolve_codex_down_false_when_job_state_codex_down_is_false(
399
+ tmp_path: Path,
400
+ monkeypatch: pytest.MonkeyPatch,
401
+ ) -> None:
402
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
403
+ monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
404
+ (tmp_path / "pr-converge-state.json").write_text(
405
+ json.dumps({"codex_down": False}),
406
+ encoding="utf-8",
407
+ )
408
+ assert check_convergence._resolve_codex_down(
409
+ False, check_convergence._read_job_state()
410
+ ) is False
411
+
412
+
413
+ def should_bypass_when_job_state_codex_down_is_true(
414
+ tmp_path: Path,
415
+ monkeypatch: pytest.MonkeyPatch,
416
+ capsys: pytest.CaptureFixture[str],
417
+ ) -> None:
418
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
419
+ monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
420
+ (tmp_path / "pr-converge-state.json").write_text(
421
+ json.dumps({"codex_down": True}),
422
+ encoding="utf-8",
423
+ )
424
+ fixture_path = _write_fixture(
425
+ tmp_path,
426
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
427
+ codex_clean_at=None,
428
+ filename="job-state-codex-down.json",
429
+ )
430
+ exit_code = check_convergence.main(
431
+ [
432
+ "--owner",
433
+ "jl-cmd",
434
+ "--repo",
435
+ "claude-dev-env",
436
+ "--pr-number",
437
+ "111",
438
+ "--fixture",
439
+ str(fixture_path),
440
+ "--bugbot-down",
441
+ "--copilot-down",
442
+ ]
443
+ )
444
+ captured = capsys.readouterr().out
445
+ assert exit_code == 0
446
+ assert "bypassed (codex_down)" in captured
447
+ assert "no codex clean on" not in captured
448
+
449
+
450
+ def should_fail_when_job_state_lacks_codex_down_and_clean_stamp(
451
+ tmp_path: Path,
452
+ monkeypatch: pytest.MonkeyPatch,
453
+ capsys: pytest.CaptureFixture[str],
454
+ ) -> None:
455
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
456
+ monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
457
+ (tmp_path / "pr-converge-state.json").write_text(
458
+ json.dumps({}),
459
+ encoding="utf-8",
460
+ )
461
+ fixture_path = _write_fixture(
462
+ tmp_path,
463
+ codex_percent_left=PERCENT_ABOVE_THRESHOLD,
464
+ codex_clean_at=None,
465
+ filename="job-state-no-down.json",
466
+ )
467
+ exit_code = check_convergence.main(
468
+ [
469
+ "--owner",
470
+ "jl-cmd",
471
+ "--repo",
472
+ "claude-dev-env",
473
+ "--pr-number",
474
+ "111",
475
+ "--fixture",
476
+ str(fixture_path),
477
+ "--bugbot-down",
478
+ "--copilot-down",
479
+ ]
480
+ )
481
+ captured = capsys.readouterr().out
482
+ assert exit_code == 1
483
+ assert "codex_clean_at == current_head: FAIL" in captured
484
+ assert "no codex clean on" in captured
485
+
486
+
487
+ def should_accept_codex_down_flag_in_parsed_arguments() -> None:
488
+ arguments = check_convergence.parse_arguments(
489
+ ["--owner", "o", "--repo", "r", "--pr-number", "1", "--codex-down"]
490
+ )
491
+ assert arguments.codex_down is True
492
+
493
+
494
+ def should_accept_codex_clean_at_flag_in_parsed_arguments() -> None:
495
+ arguments = check_convergence.parse_arguments(
496
+ [
497
+ "--owner",
498
+ "o",
499
+ "--repo",
500
+ "r",
501
+ "--pr-number",
502
+ "1",
503
+ "--codex-clean-at",
504
+ HEAD_SHA,
505
+ ]
506
+ )
507
+ assert arguments.codex_clean_at == HEAD_SHA