claude-dev-env 2.12.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 (193) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  2. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  3. package/agents/CLAUDE.md +1 -2
  4. package/agents/pr-description-writer.md +11 -81
  5. package/bin/install.mjs +48 -9
  6. package/bin/install.prune.test.mjs +26 -0
  7. package/bin/install.settings-defaults.test.mjs +60 -33
  8. package/bin/install.test.mjs +2 -4
  9. package/commands/sr-loop.md +9 -5
  10. package/docs/CLAUDE.md +0 -1
  11. package/docs/references/CLAUDE.md +0 -1
  12. package/hooks/CLAUDE.md +1 -1
  13. package/hooks/blocking/CLAUDE.md +2 -27
  14. package/hooks/blocking/config/CLAUDE.md +1 -13
  15. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  16. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  17. package/hooks/blocking/stop_dispatcher.py +4 -4
  18. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  19. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  20. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  21. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  22. package/hooks/diagnostic/CLAUDE.md +3 -3
  23. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  24. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  25. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  26. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  28. package/hooks/git-hooks/pre_push.py +3 -85
  29. package/hooks/git-hooks/test_pre_push.py +0 -130
  30. package/hooks/hooks.json +0 -35
  31. package/hooks/hooks_constants/CLAUDE.md +0 -9
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  33. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  34. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  35. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  36. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  37. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  40. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  41. package/hooks/lifecycle/CLAUDE.md +1 -3
  42. package/package.json +1 -1
  43. package/rules/CLAUDE.md +0 -2
  44. package/rules/gh-cli-conventions.md +0 -1
  45. package/rules/git-workflow.md +1 -3
  46. package/scripts/CLAUDE.md +0 -1
  47. package/scripts/_code_review_test_support.py +0 -95
  48. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  49. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  50. package/scripts/invoke_code_review.py +8 -451
  51. package/scripts/test_invoke_code_review.py +5 -61
  52. package/scripts/test_invoke_code_review_cli.py +1 -45
  53. package/scripts/test_invoke_code_review_contract.py +1 -118
  54. package/settings.json +1 -10
  55. package/skills/CLAUDE.md +8 -0
  56. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  57. package/skills/autoconverge/reference/convergence.md +7 -7
  58. package/skills/autoconverge/reference/gotchas.md +3 -10
  59. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  60. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  61. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  62. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  63. package/skills/autoconverge/workflow/converge.mjs +201 -238
  64. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  65. package/skills/bugteam/CONSTRAINTS.md +1 -1
  66. package/skills/bugteam/SKILL.md +9 -0
  67. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  68. package/skills/bugteam/test_skill_additions.py +9 -0
  69. package/skills/comments/SKILL.md +65 -0
  70. package/skills/descriptions/SKILL.md +51 -0
  71. package/skills/emergencies/SKILL.md +42 -0
  72. package/skills/imagegen/SKILL.md +21 -0
  73. package/skills/imagegen/scripts/config/__init__.py +1 -0
  74. package/skills/imagegen/scripts/config/constants.py +28 -0
  75. package/skills/imagegen/scripts/imagegen.py +45 -0
  76. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  77. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  78. package/skills/orchestrator/SKILL.md +2 -4
  79. package/skills/plan-to-pr/SKILL.md +6 -7
  80. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  81. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  82. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  83. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  84. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  85. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  86. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  87. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  88. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  89. package/skills/pr-converge/SKILL.md +13 -16
  90. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  91. package/skills/pr-converge/reference/state-schema.md +1 -27
  92. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  93. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  94. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  95. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  96. package/skills/prototype/SKILL.md +4 -4
  97. package/skills/prototype/reference/honest-limitations.md +4 -4
  98. package/skills/prototype/reference/promotion-tasks.md +2 -2
  99. package/skills/prototype/workflows/promotion.md +4 -6
  100. package/skills/reviews/SKILL.md +85 -0
  101. package/skills/small-cl/SKILL.md +52 -0
  102. package/agents/code-verifier.md +0 -74
  103. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  104. package/docs/references/code-review-enforcement.md +0 -132
  105. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  106. package/hooks/blocking/code_review_gate_deny.py +0 -74
  107. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  108. package/hooks/blocking/code_review_push_gate.py +0 -145
  109. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  110. package/hooks/blocking/code_review_stamp_store.py +0 -233
  111. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  112. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  113. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  114. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  115. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  116. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  117. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  118. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  119. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  120. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  121. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  122. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  123. package/hooks/blocking/conftest.py +0 -123
  124. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  125. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  126. package/hooks/blocking/pr_description_body_audit.py +0 -148
  127. package/hooks/blocking/pr_description_command_parser.py +0 -237
  128. package/hooks/blocking/pr_description_enforcer.py +0 -227
  129. package/hooks/blocking/pr_description_pr_number.py +0 -155
  130. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  131. package/hooks/blocking/pr_description_readability.py +0 -364
  132. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  133. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  134. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  135. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  136. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  137. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  138. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  139. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  140. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  141. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  142. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  143. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  144. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  145. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  146. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  147. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  148. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  149. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  150. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  151. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  152. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  153. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  154. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  155. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  156. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  157. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  158. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  159. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  160. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  161. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  162. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  163. package/hooks/blocking/verification_verdict_store.py +0 -1039
  164. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  165. package/hooks/blocking/verified_commit_gate.py +0 -113
  166. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  167. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  168. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  169. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  170. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  171. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  172. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  173. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  174. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  175. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  176. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  177. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  178. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  179. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  180. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  181. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  182. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  183. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  184. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  185. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  186. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  187. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  188. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  189. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  190. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  191. package/rules/proof-of-work-pr-comments.md +0 -22
  192. package/rules/verified-commit-gate-skip.md +0 -4
  193. 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