claude-dev-env 2.11.0 → 2.13.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 (203) hide show
  1. package/_shared/advisor/reference/sol-rung.md +5 -2
  2. package/_shared/advisor/reference/warm-up.md +1 -0
  3. package/_shared/advisor/scripts/codex_sol_advisor.py +96 -31
  4. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +5 -0
  5. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +173 -11
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  8. package/agents/CLAUDE.md +1 -2
  9. package/agents/pr-description-writer.md +11 -81
  10. package/bin/install.mjs +48 -9
  11. package/bin/install.prune.test.mjs +26 -0
  12. package/bin/install.settings-defaults.test.mjs +60 -33
  13. package/bin/install.test.mjs +2 -4
  14. package/commands/CLAUDE.md +1 -0
  15. package/commands/sr-loop.md +52 -0
  16. package/docs/CLAUDE.md +0 -1
  17. package/docs/references/CLAUDE.md +0 -1
  18. package/hooks/CLAUDE.md +1 -1
  19. package/hooks/blocking/CLAUDE.md +2 -27
  20. package/hooks/blocking/config/CLAUDE.md +1 -13
  21. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  22. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  23. package/hooks/blocking/stop_dispatcher.py +4 -4
  24. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  26. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  27. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  28. package/hooks/diagnostic/CLAUDE.md +3 -3
  29. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  30. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  31. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  32. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  33. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  34. package/hooks/git-hooks/pre_push.py +3 -85
  35. package/hooks/git-hooks/test_pre_push.py +0 -130
  36. package/hooks/hooks.json +0 -35
  37. package/hooks/hooks_constants/CLAUDE.md +0 -9
  38. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  39. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  40. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  41. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  42. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  43. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  46. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  47. package/hooks/lifecycle/CLAUDE.md +1 -3
  48. package/package.json +1 -1
  49. package/rules/CLAUDE.md +0 -2
  50. package/rules/gh-cli-conventions.md +0 -1
  51. package/rules/git-workflow.md +1 -3
  52. package/scripts/CLAUDE.md +0 -1
  53. package/scripts/_code_review_test_support.py +0 -95
  54. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  55. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  56. package/scripts/invoke_code_review.py +8 -451
  57. package/scripts/test_invoke_code_review.py +5 -61
  58. package/scripts/test_invoke_code_review_cli.py +1 -45
  59. package/scripts/test_invoke_code_review_contract.py +1 -118
  60. package/settings.json +1 -10
  61. package/skills/CLAUDE.md +8 -0
  62. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  63. package/skills/autoconverge/reference/convergence.md +7 -7
  64. package/skills/autoconverge/reference/gotchas.md +3 -10
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  66. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  67. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  68. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  69. package/skills/autoconverge/workflow/converge.mjs +201 -238
  70. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  71. package/skills/bugteam/CONSTRAINTS.md +1 -1
  72. package/skills/bugteam/SKILL.md +9 -0
  73. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  74. package/skills/bugteam/test_skill_additions.py +9 -0
  75. package/skills/comments/SKILL.md +65 -0
  76. package/skills/descriptions/SKILL.md +51 -0
  77. package/skills/emergencies/SKILL.md +42 -0
  78. package/skills/imagegen/SKILL.md +21 -0
  79. package/skills/imagegen/scripts/config/__init__.py +1 -0
  80. package/skills/imagegen/scripts/config/constants.py +28 -0
  81. package/skills/imagegen/scripts/imagegen.py +45 -0
  82. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  83. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  84. package/skills/orchestrator/SKILL.md +2 -4
  85. package/skills/plan-to-pr/SKILL.md +6 -7
  86. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  87. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  88. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  89. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  90. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  91. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  92. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  93. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  94. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  95. package/skills/pr-converge/SKILL.md +13 -16
  96. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  97. package/skills/pr-converge/reference/state-schema.md +1 -27
  98. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  99. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  100. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  101. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  102. package/skills/prototype/SKILL.md +4 -4
  103. package/skills/prototype/reference/honest-limitations.md +4 -4
  104. package/skills/prototype/reference/promotion-tasks.md +2 -2
  105. package/skills/prototype/workflows/promotion.md +4 -6
  106. package/skills/reviews/SKILL.md +85 -0
  107. package/skills/small-cl/SKILL.md +52 -0
  108. package/skills/usage-pause/SKILL.md +1 -1
  109. package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
  110. package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
  111. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
  112. package/agents/code-verifier.md +0 -74
  113. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  114. package/docs/references/code-review-enforcement.md +0 -132
  115. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  116. package/hooks/blocking/code_review_gate_deny.py +0 -74
  117. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  118. package/hooks/blocking/code_review_push_gate.py +0 -145
  119. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  120. package/hooks/blocking/code_review_stamp_store.py +0 -233
  121. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  122. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  123. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  124. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  125. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  126. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  127. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  128. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  129. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  130. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  131. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  132. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  133. package/hooks/blocking/conftest.py +0 -123
  134. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  135. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  136. package/hooks/blocking/pr_description_body_audit.py +0 -148
  137. package/hooks/blocking/pr_description_command_parser.py +0 -237
  138. package/hooks/blocking/pr_description_enforcer.py +0 -227
  139. package/hooks/blocking/pr_description_pr_number.py +0 -155
  140. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  141. package/hooks/blocking/pr_description_readability.py +0 -364
  142. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  143. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  144. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  145. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  146. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  147. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  148. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  149. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  150. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  151. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  152. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  153. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  154. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  155. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  156. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  157. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  158. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  159. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  160. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  161. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  162. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  163. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  164. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  165. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  166. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  167. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  168. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  169. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  170. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  171. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  172. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  173. package/hooks/blocking/verification_verdict_store.py +0 -1039
  174. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  175. package/hooks/blocking/verified_commit_gate.py +0 -113
  176. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  177. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  178. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  179. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  180. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  181. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  182. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  183. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  184. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  185. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  186. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  187. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  188. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  189. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  190. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  191. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  192. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  193. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  194. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  195. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  196. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  197. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  198. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  199. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  200. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  201. package/rules/proof-of-work-pr-comments.md +0 -22
  202. package/rules/verified-commit-gate-skip.md +0 -4
  203. package/scripts/Show-Asset.ps1 +0 -106
@@ -1,313 +0,0 @@
1
- """Behavior tests for the proof-of-work comment audit and gh pr ready gate.
2
-
3
- Each test drives the public audit surface of pr_description_proof_of_work
4
- with real comment bodies. The single gh subprocess boundary is patched with
5
- a fake runner, so parsing, part detection, and gate decisions all run the
6
- production code paths.
7
- """
8
-
9
- import json
10
- import pathlib
11
- import sys
12
-
13
- import pytest
14
-
15
- _HOOK_DIR = pathlib.Path(__file__).parent
16
- _HOOKS_ROOT = _HOOK_DIR.parent
17
- if str(_HOOKS_ROOT) not in sys.path:
18
- sys.path.insert(0, str(_HOOKS_ROOT))
19
-
20
- from blocking import convergence_gate_blocker as convergence_module # noqa: E402
21
- from blocking import pr_description_proof_of_work as proof_module # noqa: E402
22
- from blocking.pr_description_proof_of_work import ( # noqa: E402
23
- audit_proof_comment_body,
24
- evaluate_pr_ready_gate,
25
- is_pr_ready_command,
26
- is_proof_shaped_body,
27
- )
28
-
29
- PASSING_PROOF_COMMENT_BODY = (
30
- "## Summary\n\n"
31
- "Recolored the launcher icons and checked every produced asset by hand "
32
- "against the palette the plan names.\n\n"
33
- "## Verification\n\n"
34
- "```\n"
35
- "C:\\Python313\\python.exe scripts/recolor.py --input themes/dawn\n"
36
- "```\n\n"
37
- "| Artifact | Measured |\n"
38
- "| --- | --- |\n"
39
- "| icons.zip | 48 files, 412,993 bytes |\n\n"
40
- "This advances phase 2 of issue #12.\n\n"
41
- "Known gap: the offline proof cannot show the on-device rendering; the "
42
- "store preview covers that.\n"
43
- )
44
-
45
- VISUAL_PROOF_COMMENT_BODY = (
46
- PASSING_PROOF_COMMENT_BODY + "\n![](https://placehold.co/20x20/AABBCC/AABBCC.png)\n"
47
- )
48
-
49
- BARE_PROOF_COMMENT_BODY = "## Verification\n\nLooks good overall to me.\n"
50
-
51
- ORDINARY_COMMENT_BODY = (
52
- "The retry logic here mirrors what the uploader does for chunked "
53
- "transfers, so the two stay in step."
54
- )
55
-
56
-
57
- def _install_fake_gh(monkeypatch, all_comment_bodies, all_diff_names, diff_text):
58
- all_comment_records = [{"body": each_body} for each_body in all_comment_bodies]
59
-
60
- def _fake_run_gh_command(all_gh_arguments):
61
- if all_gh_arguments and all_gh_arguments[0] == "api":
62
- return json.dumps([all_comment_records])
63
- if list(all_gh_arguments[:2]) == ["pr", "view"]:
64
- return json.dumps({"number": 77})
65
- if "--name-only" in all_gh_arguments:
66
- return "\n".join(all_diff_names)
67
- if list(all_gh_arguments[:2]) == ["pr", "diff"]:
68
- return diff_text
69
- return None
70
-
71
- monkeypatch.setattr(proof_module, "_run_gh_command", _fake_run_gh_command)
72
-
73
-
74
- def _install_recording_fake_gh(monkeypatch, all_comment_bodies, all_diff_names, diff_text):
75
- all_comment_records = [{"body": each_body} for each_body in all_comment_bodies]
76
- all_recorded_argument_lists: list[list[str]] = []
77
-
78
- def _fake_run_gh_command(all_gh_arguments):
79
- all_recorded_argument_lists.append(list(all_gh_arguments))
80
- if all_gh_arguments and all_gh_arguments[0] == "api":
81
- return json.dumps([all_comment_records])
82
- if list(all_gh_arguments[:2]) == ["pr", "view"]:
83
- return json.dumps({"number": 77})
84
- if "--name-only" in all_gh_arguments:
85
- return "\n".join(all_diff_names)
86
- if list(all_gh_arguments[:2]) == ["pr", "diff"]:
87
- return diff_text
88
- return None
89
-
90
- monkeypatch.setattr(proof_module, "_run_gh_command", _fake_run_gh_command)
91
- return all_recorded_argument_lists
92
-
93
-
94
- def _argument_lists_with_leading(all_recorded_argument_lists, first_token, second_token):
95
- return [
96
- each_arguments
97
- for each_arguments in all_recorded_argument_lists
98
- if list(each_arguments[:2]) == [first_token, second_token]
99
- ]
100
-
101
-
102
- def test_is_proof_shaped_body_detects_verification_heading() -> None:
103
- assert is_proof_shaped_body(PASSING_PROOF_COMMENT_BODY)
104
-
105
-
106
- def test_is_proof_shaped_body_skips_ordinary_comment() -> None:
107
- assert not is_proof_shaped_body(ORDINARY_COMMENT_BODY)
108
-
109
-
110
- def test_is_pr_ready_command_matches_ready() -> None:
111
- assert is_pr_ready_command("gh pr ready 123")
112
-
113
-
114
- def test_is_pr_ready_command_skips_undo() -> None:
115
- assert not is_pr_ready_command("gh pr ready 123 --undo")
116
-
117
-
118
- def test_is_pr_ready_command_matches_after_shell_operator() -> None:
119
- assert is_pr_ready_command("git fetch && gh pr ready 5")
120
-
121
-
122
- def test_is_pr_ready_command_ignores_phrase_inside_commit_message() -> None:
123
- assert not is_pr_ready_command(
124
- 'git commit -m "block gh pr ready without a proof comment"'
125
- )
126
-
127
-
128
- def test_is_pr_ready_command_ignores_phrase_inside_search_argument() -> None:
129
- assert not is_pr_ready_command('grep -r "gh pr ready" packages/')
130
-
131
-
132
- def test_audit_passes_complete_proof_on_non_visual_change(monkeypatch: pytest.MonkeyPatch) -> None:
133
- _install_fake_gh(monkeypatch, [], ["src/parser.py"], "+ plain code line")
134
- assert audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123) == []
135
-
136
-
137
- def test_audit_flags_missing_visual_on_image_diff(monkeypatch: pytest.MonkeyPatch) -> None:
138
- _install_fake_gh(monkeypatch, [], ["assets/icon.png"], "")
139
- all_missing_parts = audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123)
140
- assert any("visual" in each_part for each_part in all_missing_parts)
141
-
142
-
143
- def test_audit_accepts_image_embed_on_image_diff(monkeypatch: pytest.MonkeyPatch) -> None:
144
- _install_fake_gh(monkeypatch, [], ["assets/icon.png"], "")
145
- assert audit_proof_comment_body(VISUAL_PROOF_COMMENT_BODY, 123) == []
146
-
147
-
148
- def test_audit_detects_visual_change_from_hex_colors(monkeypatch: pytest.MonkeyPatch) -> None:
149
- _install_fake_gh(monkeypatch, [], ["src/theme.py"], '+ accent = "#AABBCC"')
150
- all_missing_parts = audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123)
151
- assert any("visual" in each_part for each_part in all_missing_parts)
152
-
153
-
154
- def test_audit_names_every_missing_part_of_bare_proof(monkeypatch: pytest.MonkeyPatch) -> None:
155
- _install_fake_gh(monkeypatch, [], ["src/parser.py"], "")
156
- all_missing_parts = audit_proof_comment_body(BARE_PROOF_COMMENT_BODY, 123)
157
- assert len(all_missing_parts) == 4
158
-
159
-
160
- def test_ready_gate_blocks_when_no_proof_comment_exists(monkeypatch: pytest.MonkeyPatch) -> None:
161
- _install_fake_gh(monkeypatch, [ORDINARY_COMMENT_BODY], ["src/parser.py"], "")
162
- denial_reason = evaluate_pr_ready_gate("gh pr ready 123")
163
- assert denial_reason is not None
164
- assert "proof" in denial_reason.lower()
165
-
166
-
167
- def test_ready_gate_allows_with_passing_proof_comment(monkeypatch: pytest.MonkeyPatch) -> None:
168
- _install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
169
- assert evaluate_pr_ready_gate("gh pr ready 123") is None
170
-
171
-
172
- def test_ready_gate_blocks_when_proof_comment_is_incomplete(
173
- monkeypatch: pytest.MonkeyPatch,
174
- ) -> None:
175
- _install_fake_gh(monkeypatch, [BARE_PROOF_COMMENT_BODY], ["src/parser.py"], "")
176
- denial_reason = evaluate_pr_ready_gate("gh pr ready 123")
177
- assert denial_reason is not None
178
- assert "123" in denial_reason
179
- assert "proof" in denial_reason.lower()
180
-
181
-
182
- def test_ready_gate_fails_open_when_gh_is_unavailable(monkeypatch: pytest.MonkeyPatch) -> None:
183
- monkeypatch.setattr(proof_module, "_run_gh_command", lambda all_gh_arguments: None)
184
- assert evaluate_pr_ready_gate("gh pr ready 123") is None
185
-
186
-
187
- def test_ready_gate_resolves_pr_number_from_current_branch(
188
- monkeypatch: pytest.MonkeyPatch,
189
- ) -> None:
190
- _install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
191
- monkeypatch.setattr(
192
- convergence_module,
193
- "_resolve_current_branch_pr_number",
194
- lambda all_target_repo, cwd: 77,
195
- )
196
- assert evaluate_pr_ready_gate("gh pr ready") is None
197
-
198
-
199
- def test_ready_gate_reads_comments_from_repo_named_by_flag(
200
- monkeypatch: pytest.MonkeyPatch,
201
- ) -> None:
202
- all_recorded = _install_recording_fake_gh(
203
- monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
204
- )
205
- evaluate_pr_ready_gate("gh pr ready 42 --repo other-owner/other-repo")
206
- all_api_calls = _argument_lists_with_leading(
207
- all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
208
- )
209
- assert all_api_calls
210
- for each_arguments in all_api_calls:
211
- assert "{owner}" not in each_arguments[1]
212
- assert "{repo}" not in each_arguments[1]
213
-
214
-
215
- def test_ready_gate_resolves_pr_number_from_repo_named_by_flag(
216
- monkeypatch: pytest.MonkeyPatch,
217
- ) -> None:
218
- _install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
219
- all_captured_repo_pairs: list[tuple[str, str] | None] = []
220
-
221
- def fake_resolver(all_target_repo: tuple[str, str] | None, cwd: str | None) -> int:
222
- all_captured_repo_pairs.append(all_target_repo)
223
- return 77
224
-
225
- monkeypatch.setattr(convergence_module, "_resolve_current_branch_pr_number", fake_resolver)
226
- evaluate_pr_ready_gate("gh pr ready --repo other-owner/other-repo")
227
- assert all_captured_repo_pairs == [("other-owner", "other-repo")]
228
-
229
-
230
- def test_ready_gate_reads_comments_from_repo_named_by_url(
231
- monkeypatch: pytest.MonkeyPatch,
232
- ) -> None:
233
- all_recorded = _install_recording_fake_gh(
234
- monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
235
- )
236
- evaluate_pr_ready_gate("gh pr ready https://github.com/url-owner/url-repo/pull/99")
237
- all_api_calls = _argument_lists_with_leading(
238
- all_recorded, "api", "repos/url-owner/url-repo/issues/99/comments"
239
- )
240
- assert all_api_calls
241
- all_view_calls = _argument_lists_with_leading(all_recorded, "pr", "view")
242
- assert not all_view_calls
243
-
244
-
245
- def test_ready_gate_leaves_bare_command_bound_to_current_directory(
246
- monkeypatch: pytest.MonkeyPatch,
247
- ) -> None:
248
- all_recorded = _install_recording_fake_gh(
249
- monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
250
- )
251
- evaluate_pr_ready_gate("gh pr ready 123")
252
- all_api_calls = _argument_lists_with_leading(
253
- all_recorded, "api", "repos/{owner}/{repo}/issues/123/comments"
254
- )
255
- assert all_api_calls
256
- for each_arguments in all_recorded:
257
- assert "--repo" not in each_arguments
258
-
259
-
260
- def test_ready_gate_ignores_repo_flag_of_chained_command(
261
- monkeypatch: pytest.MonkeyPatch,
262
- ) -> None:
263
- all_recorded = _install_recording_fake_gh(
264
- monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
265
- )
266
- evaluate_pr_ready_gate(
267
- "gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo"
268
- )
269
- all_api_calls = _argument_lists_with_leading(
270
- all_recorded, "api", "repos/{owner}/{repo}/issues/161/comments"
271
- )
272
- assert all_api_calls
273
- for each_arguments in all_recorded:
274
- assert "other-owner/other-repo" not in each_arguments
275
-
276
-
277
- def test_ready_gate_reads_comments_from_repo_named_by_full_url_flag(
278
- monkeypatch: pytest.MonkeyPatch,
279
- ) -> None:
280
- all_recorded = _install_recording_fake_gh(
281
- monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
282
- )
283
- evaluate_pr_ready_gate("gh pr ready 42 --repo https://github.com/other-owner/other-repo")
284
- all_api_calls = _argument_lists_with_leading(
285
- all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
286
- )
287
- assert all_api_calls
288
-
289
-
290
- def test_ready_gate_reads_comments_from_repo_named_by_short_url_flag(
291
- monkeypatch: pytest.MonkeyPatch,
292
- ) -> None:
293
- all_recorded = _install_recording_fake_gh(
294
- monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
295
- )
296
- evaluate_pr_ready_gate("gh pr ready 42 -R https://github.com/other-owner/other-repo")
297
- all_api_calls = _argument_lists_with_leading(
298
- all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
299
- )
300
- assert all_api_calls
301
-
302
-
303
- def test_ready_gate_reads_comments_from_repo_named_by_ssh_flag(
304
- monkeypatch: pytest.MonkeyPatch,
305
- ) -> None:
306
- all_recorded = _install_recording_fake_gh(
307
- monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
308
- )
309
- evaluate_pr_ready_gate("gh pr ready 42 --repo git@github.com:other-owner/other-repo")
310
- all_api_calls = _argument_lists_with_leading(
311
- all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
312
- )
313
- assert all_api_calls
@@ -1,230 +0,0 @@
1
- """Unit tests for reviewer_spawn_gate PreToolUse hook."""
2
-
3
- import importlib.util
4
- import io
5
- import json
6
- import pathlib
7
- import sys
8
- from contextlib import redirect_stderr, redirect_stdout
9
-
10
- import pytest
11
-
12
- _HOOK_DIR = pathlib.Path(__file__).parent
13
- if str(_HOOK_DIR) not in sys.path:
14
- sys.path.insert(0, str(_HOOK_DIR))
15
-
16
- hook_spec = importlib.util.spec_from_file_location(
17
- "reviewer_spawn_gate",
18
- _HOOK_DIR / "reviewer_spawn_gate.py",
19
- )
20
- assert hook_spec is not None
21
- assert hook_spec.loader is not None
22
- hook_module = importlib.util.module_from_spec(hook_spec)
23
- hook_spec.loader.exec_module(hook_module)
24
-
25
- _matches_copilot_trigger = hook_module._matches_copilot_trigger
26
- _matches_bugbot_trigger = hook_module._matches_bugbot_trigger
27
- _reviewer_trigger = hook_module._reviewer_trigger
28
-
29
- _SENTINEL_MARKER = hook_module.GATE_SENTINEL_MARKER
30
- _AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME = hook_module.AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME
31
-
32
- _COPILOT_TRIGGER_COMMAND = (
33
- f"{_SENTINEL_MARKER} gh api repos/o/r/pulls/1/requested_reviewers "
34
- f'-f "reviewers[]=copilot-pull-request-reviewer[bot]"'
35
- )
36
- _BUGBOT_TRIGGER_COMMAND = f'{_SENTINEL_MARKER} python post_fix_reply.py --body "bugbot run"'
37
-
38
-
39
- def _write_fake_availability_script(tmp_path: pathlib.Path, exit_code: int) -> pathlib.Path:
40
- fake_script_path = tmp_path / "fake_reviewer_availability.py"
41
- fake_script_path.write_text(f"import sys\nsys.exit({exit_code})\n", encoding="utf-8")
42
- return fake_script_path
43
-
44
-
45
- def _run_hook_with_stdin_text(stdin_text: str) -> tuple[str, str, int]:
46
- captured_stdout = io.StringIO()
47
- captured_stderr = io.StringIO()
48
- exit_code = 0
49
- sys.stdin = io.StringIO(stdin_text)
50
- try:
51
- with redirect_stdout(captured_stdout), redirect_stderr(captured_stderr):
52
- try:
53
- hook_module.main()
54
- except SystemExit as exit_signal:
55
- exit_code = exit_signal.code if isinstance(exit_signal.code, int) else 0
56
- finally:
57
- sys.stdin = sys.__stdin__
58
- return captured_stdout.getvalue(), captured_stderr.getvalue(), exit_code
59
-
60
-
61
- def _run_hook(hook_input: dict) -> tuple[str, int]:
62
- stdout_text, _stderr_text, exit_code = _run_hook_with_stdin_text(json.dumps(hook_input))
63
- return stdout_text, exit_code
64
-
65
-
66
- def test_matches_copilot_trigger_true_for_request_reviewers_command() -> None:
67
- assert _matches_copilot_trigger(_COPILOT_TRIGGER_COMMAND)
68
-
69
-
70
- def test_matches_copilot_trigger_false_for_unrelated_command() -> None:
71
- assert not _matches_copilot_trigger("gh pr list")
72
-
73
-
74
- def test_matches_bugbot_trigger_true_for_rerun_comment_command() -> None:
75
- assert _matches_bugbot_trigger(_BUGBOT_TRIGGER_COMMAND)
76
-
77
-
78
- def test_matches_bugbot_trigger_false_for_unrelated_command() -> None:
79
- assert not _matches_bugbot_trigger("python post_fix_reply.py --body 'looks good'")
80
-
81
-
82
- def test_reviewer_trigger_returns_none_for_unrecognized_command() -> None:
83
- assert _reviewer_trigger("gh pr list") is None
84
-
85
-
86
- def test_main_allows_copilot_shaped_command_with_no_sentinel_marker(
87
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
88
- ) -> None:
89
- fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
90
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
91
- stdout_text, exit_code = _run_hook(
92
- {
93
- "tool_name": "Bash",
94
- "tool_input": {
95
- "command": (
96
- "gh api repos/o/r/pulls/1/requested_reviewers "
97
- '-f "reviewers[]=copilot-pull-request-reviewer[bot]"'
98
- )
99
- },
100
- }
101
- )
102
- assert exit_code == 0
103
- assert stdout_text == ""
104
-
105
-
106
- def test_main_allows_copilot_trigger_when_available(
107
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
108
- ) -> None:
109
- fake_script_path = _write_fake_availability_script(tmp_path, exit_code=0)
110
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
111
- stdout_text, exit_code = _run_hook(
112
- {"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
113
- )
114
- assert exit_code == 0
115
- assert stdout_text == ""
116
-
117
-
118
- def test_main_denies_copilot_trigger_when_down(
119
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
120
- ) -> None:
121
- fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
122
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
123
- stdout_text, exit_code = _run_hook(
124
- {"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
125
- )
126
- assert exit_code == 0
127
- response_payload = json.loads(stdout_text)
128
- decision_block = response_payload["hookSpecificOutput"]
129
- assert decision_block["permissionDecision"] == "deny"
130
- assert "GitHub Copilot" in decision_block["permissionDecisionReason"]
131
-
132
-
133
- def test_main_allows_copilot_trigger_on_unexpected_exit_code(
134
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
135
- ) -> None:
136
- fake_script_path = _write_fake_availability_script(tmp_path, exit_code=2)
137
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
138
- stdout_text, exit_code = _run_hook(
139
- {"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
140
- )
141
- assert exit_code == 0
142
- assert stdout_text == ""
143
-
144
-
145
- def test_main_allows_copilot_trigger_when_availability_script_crashes(
146
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
147
- ) -> None:
148
- fake_script_path = tmp_path / "fake_reviewer_availability.py"
149
- fake_script_path.write_text(
150
- "import module_that_does_not_exist_anywhere\n", encoding="utf-8"
151
- )
152
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
153
- stdout_text, exit_code = _run_hook(
154
- {"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
155
- )
156
- assert exit_code == 0
157
- assert stdout_text == ""
158
-
159
-
160
- def test_main_allows_bugbot_trigger_when_available(
161
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
162
- ) -> None:
163
- fake_script_path = _write_fake_availability_script(tmp_path, exit_code=0)
164
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
165
- stdout_text, exit_code = _run_hook(
166
- {"tool_name": "Bash", "tool_input": {"command": _BUGBOT_TRIGGER_COMMAND}}
167
- )
168
- assert exit_code == 0
169
- assert stdout_text == ""
170
-
171
-
172
- def test_main_denies_bugbot_trigger_when_opted_out(
173
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
174
- ) -> None:
175
- fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
176
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
177
- stdout_text, exit_code = _run_hook(
178
- {"tool_name": "Bash", "tool_input": {"command": _BUGBOT_TRIGGER_COMMAND}}
179
- )
180
- assert exit_code == 0
181
- response_payload = json.loads(stdout_text)
182
- decision_block = response_payload["hookSpecificOutput"]
183
- assert decision_block["permissionDecision"] == "deny"
184
- assert "Cursor Bugbot" in decision_block["permissionDecisionReason"]
185
-
186
-
187
- def test_main_allows_marker_command_with_no_recognized_trigger(
188
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
189
- ) -> None:
190
- fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
191
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
192
- stdout_text, exit_code = _run_hook(
193
- {"tool_name": "Bash", "tool_input": {"command": f"{_SENTINEL_MARKER} gh pr list"}}
194
- )
195
- assert exit_code == 0
196
- assert stdout_text == ""
197
-
198
-
199
- def test_main_allows_when_availability_script_missing(
200
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
201
- ) -> None:
202
- missing_script_path = tmp_path / "does_not_exist.py"
203
- monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(missing_script_path))
204
- stdout_text, exit_code = _run_hook(
205
- {"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
206
- )
207
- assert exit_code == 0
208
- assert stdout_text == ""
209
-
210
-
211
- def test_main_allows_non_bash_tool() -> None:
212
- stdout_text, exit_code = _run_hook(
213
- {"tool_name": "Write", "tool_input": {"content": _COPILOT_TRIGGER_COMMAND}}
214
- )
215
- assert exit_code == 0
216
- assert stdout_text == ""
217
-
218
-
219
- def test_main_with_empty_stdin_exits_silently() -> None:
220
- stdout_text, stderr_text, exit_code = _run_hook_with_stdin_text("")
221
- assert exit_code == 0
222
- assert stdout_text == ""
223
- assert stderr_text == ""
224
-
225
-
226
- def test_main_with_invalid_json_stdin_exits_silently() -> None:
227
- stdout_text, stderr_text, exit_code = _run_hook_with_stdin_text("{broken")
228
- assert exit_code == 0
229
- assert stdout_text == ""
230
- assert stderr_text == ""