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,199 +0,0 @@
1
- """Behavioral tests for the PR-create code-review gate.
2
-
3
- Each test drives the gate against a real git work tree with a change surface
4
- and a real stamp store under an isolated home, so the deny/allow decision runs
5
- the same hash-and-coverage path the hook runs in production.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import importlib.util
11
- import subprocess
12
- import sys
13
- from pathlib import Path
14
- from types import ModuleType
15
-
16
- import pytest
17
-
18
- _HOOK_DIR = Path(__file__).resolve().parent
19
- if str(_HOOK_DIR) not in sys.path:
20
- sys.path.insert(0, str(_HOOK_DIR))
21
-
22
- GIT_TIMEOUT_SECONDS = 30
23
- XHIGH_EFFORT = "xhigh"
24
- LOW_EFFORT = "low"
25
- SURFACE_SOURCE = "def add(a: int, b: int) -> int:\n return a + b\n"
26
- SURFACE_CHANGE = "def add(a: int, b: int) -> int:\n return a - b\n"
27
- MCP_CREATE_PR_TOOL = "mcp__plugin_github_github__create_pull_request"
28
-
29
-
30
- def _load_module(module_name: str) -> ModuleType:
31
- module_spec = importlib.util.spec_from_file_location(
32
- module_name, _HOOK_DIR / f"{module_name}.py"
33
- )
34
- assert module_spec is not None
35
- assert module_spec.loader is not None
36
- loaded_module = importlib.util.module_from_spec(module_spec)
37
- module_spec.loader.exec_module(loaded_module)
38
- return loaded_module
39
-
40
-
41
- gate_module = _load_module("code_review_pr_create_gate")
42
- store_module = _load_module("code_review_stamp_store")
43
- deny_module = _load_module("code_review_gate_deny")
44
-
45
-
46
- @pytest.fixture(autouse=True)
47
- def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
48
- """Behavior tests exercise the gates with enforcement on."""
49
- monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
50
-
51
-
52
- def _run_git(repository_directory: Path, *git_arguments: str) -> None:
53
- subprocess.run(
54
- ["git", "-C", str(repository_directory), *git_arguments],
55
- check=True,
56
- capture_output=True,
57
- text=True,
58
- timeout=GIT_TIMEOUT_SECONDS,
59
- )
60
-
61
-
62
- def _make_repo_with_change_surface(tmp_path: Path) -> Path:
63
- origin_directory = tmp_path / "origin.git"
64
- work_directory = tmp_path / "work"
65
- work_directory.mkdir()
66
- subprocess.run(
67
- ["git", "init", "--bare", "--initial-branch=main", str(origin_directory)],
68
- check=True,
69
- capture_output=True,
70
- text=True,
71
- timeout=GIT_TIMEOUT_SECONDS,
72
- )
73
- _run_git(work_directory, "init", "--initial-branch=main")
74
- _run_git(work_directory, "config", "user.email", "tests@example.com")
75
- _run_git(work_directory, "config", "user.name", "Reviewer")
76
- (work_directory / "app.py").write_text(SURFACE_SOURCE, encoding="utf-8")
77
- _run_git(work_directory, "add", "-A")
78
- _run_git(work_directory, "commit", "-m", "base")
79
- _run_git(work_directory, "remote", "add", "origin", str(origin_directory))
80
- _run_git(work_directory, "push", "-u", "origin", "main")
81
- (work_directory / "app.py").write_text(SURFACE_CHANGE, encoding="utf-8")
82
- return work_directory
83
-
84
-
85
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
86
- home_text = str(fake_home)
87
- monkeypatch.setenv("HOME", home_text)
88
- monkeypatch.setenv("USERPROFILE", home_text)
89
- monkeypatch.delenv("HOMEDRIVE", raising=False)
90
- monkeypatch.delenv("HOMEPATH", raising=False)
91
-
92
-
93
- def _prepared_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
94
- fake_home = tmp_path / "home"
95
- fake_home.mkdir()
96
- _isolate_home(monkeypatch, fake_home)
97
- return _make_repo_with_change_surface(tmp_path)
98
-
99
-
100
- def _record_stamp(work_directory: Path, effort: str) -> None:
101
- surface_hash = store_module.live_surface_hash(str(work_directory))
102
- assert surface_hash is not None
103
- store_module.record_clean_stamp(str(work_directory), surface_hash, effort)
104
-
105
-
106
- def test_command_invokes_gh_pr_create_detects_real_invocation() -> None:
107
- assert gate_module.command_invokes_gh_pr_create("gh pr create --title T --body-file b.md")
108
-
109
-
110
- def test_gh_pr_edit_is_not_a_create() -> None:
111
- assert not gate_module.command_invokes_gh_pr_create("gh pr edit 1 --title T")
112
-
113
-
114
- def test_echoed_prose_is_not_a_create() -> None:
115
- assert not gate_module.command_invokes_gh_pr_create('echo "gh pr create"')
116
-
117
-
118
- def test_is_mcp_create_pull_request_tool_matches_configured_name() -> None:
119
- assert gate_module.is_mcp_create_pull_request_tool(MCP_CREATE_PR_TOOL)
120
- assert not gate_module.is_mcp_create_pull_request_tool("Bash")
121
-
122
-
123
- def test_uncovered_surface_denies_pr_create(
124
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
125
- ) -> None:
126
- work_directory = _prepared_repo(monkeypatch, tmp_path)
127
- deny_reason = gate_module.deny_reason_for_directory(str(work_directory))
128
- assert deny_reason is not None
129
- assert "PR_CREATE_GATE" in deny_reason
130
-
131
-
132
- def test_covering_xhigh_stamp_allows_pr_create(
133
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
134
- ) -> None:
135
- work_directory = _prepared_repo(monkeypatch, tmp_path)
136
- _record_stamp(work_directory, XHIGH_EFFORT)
137
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
138
-
139
-
140
- def test_low_only_stamp_does_not_satisfy_xhigh(
141
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
142
- ) -> None:
143
- work_directory = _prepared_repo(monkeypatch, tmp_path)
144
- _record_stamp(work_directory, LOW_EFFORT)
145
- deny_reason = gate_module.deny_reason_for_directory(str(work_directory))
146
- assert deny_reason is not None
147
- assert "PR_CREATE_GATE" in deny_reason
148
-
149
-
150
- def test_shell_gh_pr_create_payload_denies_without_stamp(
151
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
152
- ) -> None:
153
- work_directory = _prepared_repo(monkeypatch, tmp_path)
154
- payload = {
155
- "tool_name": "Bash",
156
- "tool_input": {"command": "gh pr create --title T --body-file b.md"},
157
- "cwd": str(work_directory),
158
- }
159
- deny_decision = gate_module.decision_for_payload(payload)
160
- assert deny_decision is not None
161
- assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
162
-
163
-
164
- def test_mcp_create_pull_request_payload_denies_without_stamp(
165
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
166
- ) -> None:
167
- work_directory = _prepared_repo(monkeypatch, tmp_path)
168
- payload = {
169
- "tool_name": MCP_CREATE_PR_TOOL,
170
- "tool_input": {"owner": "o", "repo": "r"},
171
- "cwd": str(work_directory),
172
- }
173
- deny_decision = gate_module.decision_for_payload(payload)
174
- assert deny_decision is not None
175
- assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
176
-
177
-
178
- def test_gh_pr_edit_payload_is_not_denied(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
179
- work_directory = _prepared_repo(monkeypatch, tmp_path)
180
- payload = {
181
- "tool_name": "Bash",
182
- "tool_input": {"command": "gh pr edit 1 --title T"},
183
- "cwd": str(work_directory),
184
- }
185
- assert gate_module.decision_for_payload(payload) is None
186
-
187
-
188
- def test_build_deny_payload_shape() -> None:
189
- deny_payload = deny_module.build_code_review_deny_payload("blocked reason")
190
- assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
191
- assert deny_payload["hookSpecificOutput"]["permissionDecisionReason"] == "blocked reason"
192
-
193
-
194
- def test_enforcement_off_allows_pr_create_without_stamp(
195
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
196
- ) -> None:
197
- monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
198
- work_directory = _prepared_repo(monkeypatch, tmp_path)
199
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
@@ -1,205 +0,0 @@
1
- """Behavioral tests for the push code-review gate.
2
-
3
- Each test drives the gate against a real git work tree with a change surface
4
- and a real stamp store under an isolated home, so the deny/allow decision runs
5
- the same hash-and-coverage path the hook runs in production.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import importlib.util
11
- import io
12
- import json
13
- import subprocess
14
- import sys
15
- from pathlib import Path
16
- from types import ModuleType
17
-
18
- import pytest
19
-
20
- _HOOK_DIR = Path(__file__).resolve().parent
21
- if str(_HOOK_DIR) not in sys.path:
22
- sys.path.insert(0, str(_HOOK_DIR))
23
-
24
- GIT_TIMEOUT_SECONDS = 30
25
- LOW_EFFORT = "low"
26
- XHIGH_EFFORT = "xhigh"
27
- CODE_SOURCE = "def add(a: int, b: int) -> int:\n return a + b\n"
28
- CODE_CHANGE = "def add(a: int, b: int) -> int:\n return a - b\n"
29
- DOCS_SOURCE = "# Notes\n\nfirst line\n"
30
- DOCS_CHANGE = "# Notes\n\nsecond line\n"
31
- BYPASS_MARKER = "# code-review-skip"
32
-
33
-
34
- def _load_module(module_name: str) -> ModuleType:
35
- module_spec = importlib.util.spec_from_file_location(
36
- module_name, _HOOK_DIR / f"{module_name}.py"
37
- )
38
- assert module_spec is not None
39
- assert module_spec.loader is not None
40
- loaded_module = importlib.util.module_from_spec(module_spec)
41
- module_spec.loader.exec_module(loaded_module)
42
- return loaded_module
43
-
44
-
45
- gate_module = _load_module("code_review_push_gate")
46
- store_module = _load_module("code_review_stamp_store")
47
-
48
-
49
- @pytest.fixture(autouse=True)
50
- def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
51
- """Behavior tests exercise the gates with enforcement on."""
52
- monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
53
-
54
-
55
- def _run_git(repository_directory: Path, *git_arguments: str) -> None:
56
- subprocess.run(
57
- ["git", "-C", str(repository_directory), *git_arguments],
58
- check=True,
59
- capture_output=True,
60
- text=True,
61
- timeout=GIT_TIMEOUT_SECONDS,
62
- )
63
-
64
-
65
- def _init_pushed_repo(tmp_path: Path, tracked_name: str, base_text: str) -> Path:
66
- origin_directory = tmp_path / "origin.git"
67
- work_directory = tmp_path / "work"
68
- work_directory.mkdir()
69
- subprocess.run(
70
- ["git", "init", "--bare", "--initial-branch=main", str(origin_directory)],
71
- check=True,
72
- capture_output=True,
73
- text=True,
74
- timeout=GIT_TIMEOUT_SECONDS,
75
- )
76
- _run_git(work_directory, "init", "--initial-branch=main")
77
- _run_git(work_directory, "config", "user.email", "tests@example.com")
78
- _run_git(work_directory, "config", "user.name", "Reviewer")
79
- (work_directory / tracked_name).write_text(base_text, encoding="utf-8")
80
- _run_git(work_directory, "add", "-A")
81
- _run_git(work_directory, "commit", "-m", "base")
82
- _run_git(work_directory, "remote", "add", "origin", str(origin_directory))
83
- _run_git(work_directory, "push", "-u", "origin", "main")
84
- return work_directory
85
-
86
-
87
- def _make_code_surface_repo(tmp_path: Path) -> Path:
88
- work_directory = _init_pushed_repo(tmp_path, "app.py", CODE_SOURCE)
89
- (work_directory / "app.py").write_text(CODE_CHANGE, encoding="utf-8")
90
- return work_directory
91
-
92
-
93
- def _make_docs_surface_repo(tmp_path: Path) -> Path:
94
- work_directory = _init_pushed_repo(tmp_path, "notes.md", DOCS_SOURCE)
95
- (work_directory / "notes.md").write_text(DOCS_CHANGE, encoding="utf-8")
96
- return work_directory
97
-
98
-
99
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
100
- home_text = str(fake_home)
101
- monkeypatch.setenv("HOME", home_text)
102
- monkeypatch.setenv("USERPROFILE", home_text)
103
- monkeypatch.delenv("HOMEDRIVE", raising=False)
104
- monkeypatch.delenv("HOMEPATH", raising=False)
105
-
106
-
107
- def _prepared_code_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
108
- fake_home = tmp_path / "home"
109
- fake_home.mkdir()
110
- _isolate_home(monkeypatch, fake_home)
111
- return _make_code_surface_repo(tmp_path)
112
-
113
-
114
- def _record_stamp(work_directory: Path, effort: str) -> None:
115
- surface_hash = store_module.live_surface_hash(str(work_directory))
116
- assert surface_hash is not None
117
- store_module.record_clean_stamp(str(work_directory), surface_hash, effort)
118
-
119
-
120
- def _run_main(monkeypatch: pytest.MonkeyPatch, command_text: str, work_directory: Path) -> str:
121
- payload_text = json.dumps(
122
- {
123
- "tool_name": "Bash",
124
- "tool_input": {"command": command_text},
125
- "cwd": str(work_directory),
126
- }
127
- )
128
- monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
129
- captured_stdout = io.StringIO()
130
- monkeypatch.setattr(sys, "stdout", captured_stdout)
131
- gate_module.main()
132
- return captured_stdout.getvalue()
133
-
134
-
135
- def test_push_without_low_stamp_denies(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
136
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
137
- deny_reason = gate_module.deny_reason_for_directory(str(work_directory))
138
- assert deny_reason is not None
139
- assert "PUSH_GATE" in deny_reason
140
-
141
-
142
- def test_covering_low_stamp_allows_push(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
143
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
144
- _record_stamp(work_directory, LOW_EFFORT)
145
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
146
-
147
-
148
- def test_covering_xhigh_stamp_allows_push(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
149
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
150
- _record_stamp(work_directory, XHIGH_EFFORT)
151
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
152
-
153
-
154
- def test_docs_only_surface_is_exempt(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
155
- fake_home = tmp_path / "home"
156
- fake_home.mkdir()
157
- _isolate_home(monkeypatch, fake_home)
158
- work_directory = _make_docs_surface_repo(tmp_path)
159
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
160
-
161
-
162
- def test_git_push_payload_denies(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
163
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
164
- emitted = _run_main(monkeypatch, "git push origin main", work_directory)
165
- assert "PUSH_GATE" in emitted
166
-
167
-
168
- def test_git_push_with_leading_flags_denies(
169
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
170
- ) -> None:
171
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
172
- emitted = _run_main(monkeypatch, "git push --force-with-lease origin main", work_directory)
173
- assert "PUSH_GATE" in emitted
174
-
175
-
176
- def test_git_commit_is_not_gated_by_push_hook(
177
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
178
- ) -> None:
179
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
180
- emitted = _run_main(monkeypatch, "git commit -m change", work_directory)
181
- assert emitted == ""
182
-
183
-
184
- def test_git_stash_push_is_not_gated(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
185
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
186
- emitted = _run_main(monkeypatch, "git stash push", work_directory)
187
- assert emitted == ""
188
-
189
-
190
- def test_code_review_skip_marker_allows_push(
191
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
192
- ) -> None:
193
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
194
- emitted = _run_main(monkeypatch, f"git push origin main {BYPASS_MARKER}", work_directory)
195
- assert emitted == ""
196
-
197
-
198
- def test_enforcement_off_allows_push_without_stamp(
199
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
200
- ) -> None:
201
- monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
202
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
203
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
204
- emitted = _run_main(monkeypatch, "git push origin main", work_directory)
205
- assert emitted == ""
@@ -1,199 +0,0 @@
1
- """Behavioral tests for the code-review stamp-directory write blocker.
2
-
3
- The blocker denies shell and file-tool access to
4
- ``~/.claude/code-review-stamps/`` and shell references to the stamp store
5
- module or its mint call, while the sanctioned invoker command and unrelated
6
- paths pass.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import importlib.util
12
- import io
13
- import json
14
- import sys
15
- from pathlib import Path
16
- from types import ModuleType
17
-
18
- import pytest
19
-
20
- _HOOK_DIR = Path(__file__).resolve().parent
21
- if str(_HOOK_DIR) not in sys.path:
22
- sys.path.insert(0, str(_HOOK_DIR))
23
-
24
- STAMP_STORE_IMPORT_FORGE = (
25
- 'python -c "from code_review_stamp_store import record_clean_stamp; '
26
- "record_clean_stamp(root, live_hash, 'low')\""
27
- )
28
- MINT_CALL_FORGE = "python -c \"record_clean_stamp(root, live_hash, 'xhigh')\""
29
- PYTEST_RUN = "python -m pytest test_code_review_stamp_store.py"
30
- SANCTIONED_INVOKER = "python invoke_code_review.py --record-stamp --cwd . --session-model opus low"
31
- ABSOLUTE_STAMP_ACCESS = "cat ~/.claude/code-review-stamps/abc1234567890abc.json"
32
- DECOY_ECHO_THEN_FORGE = (
33
- 'echo "invoke_code_review.py --record-stamp" >/dev/null; '
34
- "echo x > ~/.claude/code-review-stamps/abc1234567890123.json"
35
- )
36
- DECOY_INVOKER_THEN_FORGE = (
37
- "python invoke_code_review.py --record-stamp --cwd . low ; "
38
- "echo pwned > ~/.claude/code-review-stamps/evil.json"
39
- )
40
-
41
-
42
- def _load_module(module_name: str) -> ModuleType:
43
- module_spec = importlib.util.spec_from_file_location(
44
- module_name, _HOOK_DIR / f"{module_name}.py"
45
- )
46
- assert module_spec is not None
47
- assert module_spec.loader is not None
48
- loaded_module = importlib.util.module_from_spec(module_spec)
49
- module_spec.loader.exec_module(loaded_module)
50
- return loaded_module
51
-
52
-
53
- blocker_module = _load_module("code_review_stamp_directory_write_blocker")
54
-
55
-
56
- @pytest.fixture(autouse=True)
57
- def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
58
- """Behavior tests exercise the write-blocker with enforcement on."""
59
- monkeypatch.setattr(blocker_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
60
-
61
-
62
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
63
- home_text = str(fake_home)
64
- monkeypatch.setenv("HOME", home_text)
65
- monkeypatch.setenv("USERPROFILE", home_text)
66
- monkeypatch.delenv("HOMEDRIVE", raising=False)
67
- monkeypatch.delenv("HOMEPATH", raising=False)
68
-
69
-
70
- def test_absolute_stamp_path_is_blocked() -> None:
71
- assert blocker_module.references_stamp_directory(ABSOLUTE_STAMP_ACCESS)
72
-
73
-
74
- def test_store_module_import_forge_is_blocked() -> None:
75
- assert blocker_module.references_stamp_directory(STAMP_STORE_IMPORT_FORGE)
76
-
77
-
78
- def test_mint_call_forge_is_blocked() -> None:
79
- assert blocker_module.references_stamp_directory(MINT_CALL_FORGE)
80
-
81
-
82
- def test_pytest_run_naming_store_test_is_not_blocked() -> None:
83
- assert not blocker_module.references_stamp_directory(PYTEST_RUN)
84
-
85
-
86
- def test_sanctioned_invoker_command_is_not_blocked() -> None:
87
- assert not blocker_module.references_stamp_directory(SANCTIONED_INVOKER)
88
-
89
-
90
- def test_decoy_echo_of_invoker_then_stamp_forge_is_blocked() -> None:
91
- assert blocker_module.references_stamp_directory(DECOY_ECHO_THEN_FORGE)
92
-
93
-
94
- def test_decoy_invoker_run_then_stamp_forge_is_blocked() -> None:
95
- assert blocker_module.references_stamp_directory(DECOY_INVOKER_THEN_FORGE)
96
-
97
-
98
- def test_write_tool_to_stamp_directory_is_blocked(
99
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
100
- ) -> None:
101
- fake_home = tmp_path / "home"
102
- fake_home.mkdir()
103
- _isolate_home(monkeypatch, fake_home)
104
- stamp_path = fake_home / ".claude" / "code-review-stamps" / "abc.json"
105
- assert blocker_module.path_targets_stamp_directory(str(stamp_path))
106
-
107
-
108
- def test_write_tool_elsewhere_is_allowed(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
109
- fake_home = tmp_path / "home"
110
- fake_home.mkdir()
111
- _isolate_home(monkeypatch, fake_home)
112
- other_path = tmp_path / "src" / "module.py"
113
- assert not blocker_module.path_targets_stamp_directory(str(other_path))
114
-
115
-
116
- def test_write_payload_to_stamp_directory_denies(
117
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
118
- ) -> None:
119
- fake_home = tmp_path / "home"
120
- fake_home.mkdir()
121
- _isolate_home(monkeypatch, fake_home)
122
- stamp_path = fake_home / ".claude" / "code-review-stamps" / "abc.json"
123
- payload = {
124
- "tool_name": "Write",
125
- "tool_input": {"file_path": str(stamp_path), "content": "{}"},
126
- }
127
- deny_decision = blocker_module.decision_for_payload(payload)
128
- assert deny_decision is not None
129
- assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
130
-
131
-
132
- def test_bash_forge_payload_denies() -> None:
133
- payload = {
134
- "tool_name": "Bash",
135
- "tool_input": {"command": STAMP_STORE_IMPORT_FORGE},
136
- }
137
- deny_decision = blocker_module.decision_for_payload(payload)
138
- assert deny_decision is not None
139
- assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
140
-
141
-
142
- def test_sanctioned_invoker_payload_is_not_denied() -> None:
143
- payload = {
144
- "tool_name": "Bash",
145
- "tool_input": {"command": SANCTIONED_INVOKER},
146
- }
147
- assert blocker_module.decision_for_payload(payload) is None
148
-
149
-
150
- def test_main_emits_deny_for_bash_forge(monkeypatch: pytest.MonkeyPatch) -> None:
151
- payload_text = json.dumps({"tool_name": "Bash", "tool_input": {"command": MINT_CALL_FORGE}})
152
- monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
153
- captured_stdout = io.StringIO()
154
- monkeypatch.setattr(sys, "stdout", captured_stdout)
155
- blocker_module.main()
156
- assert "STAMP_DIRECTORY_GUARD" in captured_stdout.getvalue()
157
-
158
-
159
- STAMP_OBFUSCATION_PATH = "/.claude/code-review-stamps/a.json"
160
- BENIGN_OBFUSCATION_PATH = "/tmp/x"
161
- SPLIT_CD_FORGE = "cd ~/.claude && cd code-review-stamps && echo x > f.json"
162
-
163
-
164
- def _hex_forge(path_text: str) -> str:
165
- return f"python -c \"open(bytes.fromhex('{path_text.encode().hex()}'),'w').write('x')\""
166
-
167
-
168
- def _chr_chain_forge(path_text: str) -> str:
169
- chr_chain = "+".join(f"chr({ord(each_character)})" for each_character in path_text)
170
- return f"python -c \"open({chr_chain},'w').write('x')\""
171
-
172
-
173
- def test_split_directory_change_into_stamp_is_blocked() -> None:
174
- assert blocker_module.references_stamp_directory(SPLIT_CD_FORGE)
175
-
176
-
177
- def test_hex_obfuscated_stamp_path_is_blocked() -> None:
178
- assert blocker_module.references_stamp_directory(_hex_forge(STAMP_OBFUSCATION_PATH))
179
-
180
-
181
- def test_chr_chain_obfuscated_stamp_path_is_blocked() -> None:
182
- assert blocker_module.references_stamp_directory(_chr_chain_forge(STAMP_OBFUSCATION_PATH))
183
-
184
-
185
- def test_benign_decode_to_other_path_is_not_blocked() -> None:
186
- assert not blocker_module.references_stamp_directory(_hex_forge(BENIGN_OBFUSCATION_PATH))
187
-
188
-
189
- def test_enforcement_off_allows_stamp_directory_access(
190
- monkeypatch: pytest.MonkeyPatch,
191
- ) -> None:
192
- monkeypatch.setattr(blocker_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
193
- decision = blocker_module.decision_for_payload(
194
- {
195
- "tool_name": "Bash",
196
- "tool_input": {"command": "rm -rf ~/.claude/code-review-stamps"},
197
- }
198
- )
199
- assert decision is None