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.
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
- package/agents/CLAUDE.md +1 -2
- package/agents/pr-description-writer.md +11 -81
- package/bin/install.mjs +48 -9
- package/bin/install.prune.test.mjs +26 -0
- package/bin/install.settings-defaults.test.mjs +60 -33
- package/bin/install.test.mjs +2 -4
- package/commands/sr-loop.md +9 -5
- package/docs/CLAUDE.md +0 -1
- package/docs/references/CLAUDE.md +0 -1
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -27
- package/hooks/blocking/config/CLAUDE.md +1 -13
- package/hooks/blocking/conventional_pr_title_gate.py +1 -2
- package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +4 -4
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
- package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
- package/hooks/diagnostic/CLAUDE.md +3 -3
- package/hooks/diagnostic/hook_log_extractor.py +2 -36
- package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
- package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
- package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
- package/hooks/git-hooks/pre_push.py +3 -85
- package/hooks/git-hooks/test_pre_push.py +0 -130
- package/hooks/hooks.json +0 -35
- package/hooks/hooks_constants/CLAUDE.md +0 -9
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/lifecycle/CLAUDE.md +1 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +0 -2
- package/rules/gh-cli-conventions.md +0 -1
- package/rules/git-workflow.md +1 -3
- package/scripts/CLAUDE.md +0 -1
- package/scripts/_code_review_test_support.py +0 -95
- package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
- package/scripts/invoke_code_review.py +8 -451
- package/scripts/test_invoke_code_review.py +5 -61
- package/scripts/test_invoke_code_review_cli.py +1 -45
- package/scripts/test_invoke_code_review_contract.py +1 -118
- package/settings.json +1 -10
- package/skills/CLAUDE.md +8 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -1
- package/skills/autoconverge/reference/convergence.md +7 -7
- package/skills/autoconverge/reference/gotchas.md +3 -10
- package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
- package/skills/autoconverge/workflow/converge.mjs +201 -238
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/SKILL.md +9 -0
- package/skills/bugteam/reference/audit-and-teammates.md +10 -6
- package/skills/bugteam/test_skill_additions.py +9 -0
- package/skills/comments/SKILL.md +65 -0
- package/skills/descriptions/SKILL.md +51 -0
- package/skills/emergencies/SKILL.md +42 -0
- package/skills/imagegen/SKILL.md +21 -0
- package/skills/imagegen/scripts/config/__init__.py +1 -0
- package/skills/imagegen/scripts/config/constants.py +28 -0
- package/skills/imagegen/scripts/imagegen.py +45 -0
- package/skills/imagegen/scripts/imagegen_core.py +528 -0
- package/skills/imagegen/scripts/test_imagegen.py +204 -0
- package/skills/orchestrator/SKILL.md +2 -4
- package/skills/plan-to-pr/SKILL.md +6 -7
- package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
- package/skills/plan-to-pr/reference/process-inventory.md +2 -2
- package/skills/plan-to-pr/reference/review-loop.md +2 -2
- package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
- package/skills/plan-to-pr/reference/task-seeds.md +1 -1
- package/skills/plan-to-pr/reference/task-ticket.md +3 -4
- package/skills/plan-to-pr/scripts/config/constants.py +1 -2
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
- package/skills/plan-to-pr/test_skill_contract.py +2 -2
- package/skills/pr-converge/SKILL.md +13 -16
- package/skills/pr-converge/reference/progress-checklist.md +6 -5
- package/skills/pr-converge/reference/state-schema.md +1 -27
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
- package/skills/pr-loop-lifecycle/SKILL.md +1 -1
- package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
- package/skills/prototype/SKILL.md +4 -4
- package/skills/prototype/reference/honest-limitations.md +4 -4
- package/skills/prototype/reference/promotion-tasks.md +2 -2
- package/skills/prototype/workflows/promotion.md +4 -6
- package/skills/reviews/SKILL.md +85 -0
- package/skills/small-cl/SKILL.md +52 -0
- package/agents/code-verifier.md +0 -74
- package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
- package/docs/references/code-review-enforcement.md +0 -132
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
- package/hooks/blocking/code_review_gate_deny.py +0 -74
- package/hooks/blocking/code_review_pr_create_gate.py +0 -198
- package/hooks/blocking/code_review_push_gate.py +0 -145
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
- package/hooks/blocking/code_review_stamp_store.py +0 -233
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
- package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
- package/hooks/blocking/config/verified_commit_constants.py +0 -160
- package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
- package/hooks/blocking/conftest.py +0 -123
- package/hooks/blocking/convergence_gate_blocker.py +0 -310
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
- package/hooks/blocking/pr_description_body_audit.py +0 -148
- package/hooks/blocking/pr_description_command_parser.py +0 -237
- package/hooks/blocking/pr_description_enforcer.py +0 -227
- package/hooks/blocking/pr_description_pr_number.py +0 -155
- package/hooks/blocking/pr_description_proof_of_work.py +0 -385
- package/hooks/blocking/pr_description_readability.py +0 -364
- package/hooks/blocking/reviewer_spawn_gate.py +0 -182
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
- package/hooks/blocking/test_code_review_gate_deny.py +0 -54
- package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
- package/hooks/blocking/test_code_review_push_gate.py +0 -205
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
- package/hooks/blocking/test_code_review_stamp_store.py +0 -205
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
- package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
- package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
- package/hooks/blocking/test_pr_description_enforcer.py +0 -221
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
- package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
- package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
- package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
- package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
- package/hooks/blocking/test_verification_verdict_store.py +0 -974
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
- package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
- package/hooks/blocking/test_verified_commit_gate.py +0 -581
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
- package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
- package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
- package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
- package/hooks/blocking/verification_verdict_store.py +0 -1039
- package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
- package/hooks/blocking/verified_commit_gate.py +0 -113
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
- package/hooks/blocking/verifier_verdict_minter.py +0 -280
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
- package/rules/proof-of-work-pr-comments.md +0 -22
- package/rules/verified-commit-gate-skip.md +0 -4
- package/scripts/Show-Asset.ps1 +0 -106
|
@@ -1,345 +1,17 @@
|
|
|
1
|
-
"""Tests for
|
|
1
|
+
"""Tests for the disabled hook_log_stop_wrapper Stop hook."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import os
|
|
6
|
-
import subprocess
|
|
7
5
|
import sys
|
|
8
|
-
import time
|
|
9
6
|
from pathlib import Path
|
|
10
7
|
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
8
|
_HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
14
9
|
if str(_HOOKS_ROOT) not in sys.path:
|
|
15
10
|
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
16
11
|
|
|
17
12
|
from diagnostic import hook_log_stop_wrapper
|
|
18
|
-
from hooks_constants.hook_log_extractor_constants import (
|
|
19
|
-
BWS_ACCESS_TOKEN_ENV_VAR,
|
|
20
|
-
BWS_EXECUTABLE_NAME,
|
|
21
|
-
FLAG_INCREMENTAL,
|
|
22
|
-
STOP_WRAPPER_DEBOUNCE_SECONDS,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _redirect_timestamp_path(
|
|
27
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
28
|
-
) -> Path:
|
|
29
|
-
timestamp_file = tmp_path / "stop_wrapper_last_run.txt"
|
|
30
|
-
monkeypatch.setattr(
|
|
31
|
-
hook_log_stop_wrapper,
|
|
32
|
-
"_last_run_timestamp_path",
|
|
33
|
-
lambda: timestamp_file,
|
|
34
|
-
)
|
|
35
|
-
return timestamp_file
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_main_returns_zero_when_extractor_spawn_raises(
|
|
39
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
40
|
-
tmp_path: Path,
|
|
41
|
-
) -> None:
|
|
42
|
-
timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
|
|
43
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
44
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
45
|
-
|
|
46
|
-
def _raise(*_args: object, **_kwargs: object) -> None:
|
|
47
|
-
raise RuntimeError("boom")
|
|
48
13
|
|
|
49
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _raise)
|
|
50
14
|
|
|
15
|
+
def test_main_returns_zero_without_side_effects() -> None:
|
|
16
|
+
"""Disabled main exits success and does no work."""
|
|
51
17
|
assert hook_log_stop_wrapper.main() == 0
|
|
52
|
-
assert not timestamp_file.exists()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_main_writes_timestamp_before_spawn_to_narrow_toctou_window(
|
|
56
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
57
|
-
tmp_path: Path,
|
|
58
|
-
) -> None:
|
|
59
|
-
timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
|
|
60
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
61
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
62
|
-
|
|
63
|
-
timestamp_file_existed_at_spawn_time: list[bool] = []
|
|
64
|
-
|
|
65
|
-
def _capture_timestamp_state(*_args: object, **_kwargs: object) -> object:
|
|
66
|
-
timestamp_file_existed_at_spawn_time.append(timestamp_file.exists())
|
|
67
|
-
return object()
|
|
68
|
-
|
|
69
|
-
monkeypatch.setattr(
|
|
70
|
-
hook_log_stop_wrapper.subprocess, "Popen", _capture_timestamp_state
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
hook_log_stop_wrapper.main()
|
|
74
|
-
|
|
75
|
-
assert timestamp_file_existed_at_spawn_time == [True]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_main_removes_timestamp_when_spawn_fails_to_allow_retry(
|
|
79
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
80
|
-
tmp_path: Path,
|
|
81
|
-
) -> None:
|
|
82
|
-
timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
|
|
83
|
-
timestamp_file.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
-
stale_timestamp = time.time() - STOP_WRAPPER_DEBOUNCE_SECONDS - 1
|
|
85
|
-
timestamp_file.write_text(str(stale_timestamp))
|
|
86
|
-
|
|
87
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
88
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
89
|
-
|
|
90
|
-
def _raise(*_args: object, **_kwargs: object) -> None:
|
|
91
|
-
raise RuntimeError("boom")
|
|
92
|
-
|
|
93
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _raise)
|
|
94
|
-
|
|
95
|
-
assert hook_log_stop_wrapper.main() == 0
|
|
96
|
-
assert not timestamp_file.exists()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_main_uses_bws_when_token_and_binary_present(
|
|
100
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
101
|
-
tmp_path: Path,
|
|
102
|
-
) -> None:
|
|
103
|
-
_redirect_timestamp_path(monkeypatch, tmp_path)
|
|
104
|
-
monkeypatch.setenv(BWS_ACCESS_TOKEN_ENV_VAR, "secret-value")
|
|
105
|
-
monkeypatch.setattr(
|
|
106
|
-
hook_log_stop_wrapper.shutil,
|
|
107
|
-
"which",
|
|
108
|
-
lambda _name: "/usr/local/bin/bws",
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
captured_commands: list[list[str]] = []
|
|
112
|
-
|
|
113
|
-
def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
|
|
114
|
-
captured_commands.append(list(command_list))
|
|
115
|
-
return object()
|
|
116
|
-
|
|
117
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
118
|
-
|
|
119
|
-
exit_code = hook_log_stop_wrapper.main()
|
|
120
|
-
|
|
121
|
-
assert exit_code == 0
|
|
122
|
-
assert len(captured_commands) == 1
|
|
123
|
-
assert captured_commands[0][0] == BWS_EXECUTABLE_NAME
|
|
124
|
-
assert FLAG_INCREMENTAL in captured_commands[0]
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def test_main_skips_bws_when_token_missing(
|
|
128
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
129
|
-
tmp_path: Path,
|
|
130
|
-
) -> None:
|
|
131
|
-
_redirect_timestamp_path(monkeypatch, tmp_path)
|
|
132
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
133
|
-
monkeypatch.setattr(
|
|
134
|
-
hook_log_stop_wrapper.shutil,
|
|
135
|
-
"which",
|
|
136
|
-
lambda _name: "/usr/local/bin/bws",
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
captured_commands: list[list[str]] = []
|
|
140
|
-
|
|
141
|
-
def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
|
|
142
|
-
captured_commands.append(list(command_list))
|
|
143
|
-
return object()
|
|
144
|
-
|
|
145
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
146
|
-
|
|
147
|
-
exit_code = hook_log_stop_wrapper.main()
|
|
148
|
-
|
|
149
|
-
assert exit_code == 0
|
|
150
|
-
assert len(captured_commands) == 1
|
|
151
|
-
assert BWS_EXECUTABLE_NAME not in captured_commands[0]
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def test_main_skips_bws_when_binary_missing(
|
|
155
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
156
|
-
tmp_path: Path,
|
|
157
|
-
) -> None:
|
|
158
|
-
_redirect_timestamp_path(monkeypatch, tmp_path)
|
|
159
|
-
monkeypatch.setenv(BWS_ACCESS_TOKEN_ENV_VAR, "secret-value")
|
|
160
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
161
|
-
|
|
162
|
-
captured_commands: list[list[str]] = []
|
|
163
|
-
|
|
164
|
-
def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
|
|
165
|
-
captured_commands.append(list(command_list))
|
|
166
|
-
return object()
|
|
167
|
-
|
|
168
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
169
|
-
|
|
170
|
-
exit_code = hook_log_stop_wrapper.main()
|
|
171
|
-
|
|
172
|
-
assert exit_code == 0
|
|
173
|
-
assert len(captured_commands) == 1
|
|
174
|
-
assert BWS_EXECUTABLE_NAME not in captured_commands[0]
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def test_main_skips_spawn_when_recent_timestamp_within_debounce_window(
|
|
178
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
179
|
-
tmp_path: Path,
|
|
180
|
-
) -> None:
|
|
181
|
-
timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
|
|
182
|
-
timestamp_file.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
-
timestamp_file.write_text(str(time.time()))
|
|
184
|
-
|
|
185
|
-
captured_commands: list[list[str]] = []
|
|
186
|
-
|
|
187
|
-
def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
|
|
188
|
-
captured_commands.append(list(command_list))
|
|
189
|
-
return object()
|
|
190
|
-
|
|
191
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
192
|
-
|
|
193
|
-
exit_code = hook_log_stop_wrapper.main()
|
|
194
|
-
|
|
195
|
-
assert exit_code == 0
|
|
196
|
-
assert len(captured_commands) == 0
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def test_main_spawns_when_timestamp_older_than_debounce_window(
|
|
200
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
201
|
-
tmp_path: Path,
|
|
202
|
-
) -> None:
|
|
203
|
-
timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
|
|
204
|
-
timestamp_file.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
-
stale_timestamp = time.time() - STOP_WRAPPER_DEBOUNCE_SECONDS - 1
|
|
206
|
-
timestamp_file.write_text(str(stale_timestamp))
|
|
207
|
-
|
|
208
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
209
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
210
|
-
|
|
211
|
-
captured_commands: list[list[str]] = []
|
|
212
|
-
|
|
213
|
-
def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
|
|
214
|
-
captured_commands.append(list(command_list))
|
|
215
|
-
return object()
|
|
216
|
-
|
|
217
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
218
|
-
|
|
219
|
-
exit_code = hook_log_stop_wrapper.main()
|
|
220
|
-
|
|
221
|
-
assert exit_code == 0
|
|
222
|
-
assert len(captured_commands) == 1
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def test_main_writes_current_timestamp_to_file(
|
|
226
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
227
|
-
tmp_path: Path,
|
|
228
|
-
) -> None:
|
|
229
|
-
timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
|
|
230
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
231
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
232
|
-
monkeypatch.setattr(
|
|
233
|
-
hook_log_stop_wrapper.subprocess,
|
|
234
|
-
"Popen",
|
|
235
|
-
lambda *_args, **_kwargs: object(),
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
timestamp_before_call = time.time()
|
|
239
|
-
hook_log_stop_wrapper.main()
|
|
240
|
-
timestamp_after_call = time.time()
|
|
241
|
-
|
|
242
|
-
assert timestamp_file.exists()
|
|
243
|
-
written_timestamp = float(timestamp_file.read_text().strip())
|
|
244
|
-
assert timestamp_before_call <= written_timestamp <= timestamp_after_call
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def test_main_passes_devnull_streams_to_detached_spawn(
|
|
248
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
249
|
-
tmp_path: Path,
|
|
250
|
-
) -> None:
|
|
251
|
-
_redirect_timestamp_path(monkeypatch, tmp_path)
|
|
252
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
253
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
254
|
-
|
|
255
|
-
captured_kwargs: dict[str, object] = {}
|
|
256
|
-
|
|
257
|
-
def _fake_popen(command_list: list[str], **kwargs: object) -> object:
|
|
258
|
-
captured_kwargs.update(kwargs)
|
|
259
|
-
return object()
|
|
260
|
-
|
|
261
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
262
|
-
|
|
263
|
-
hook_log_stop_wrapper.main()
|
|
264
|
-
|
|
265
|
-
assert captured_kwargs.get("stdin") is hook_log_stop_wrapper.subprocess.DEVNULL
|
|
266
|
-
assert captured_kwargs.get("stdout") is hook_log_stop_wrapper.subprocess.DEVNULL
|
|
267
|
-
assert captured_kwargs.get("stderr") is hook_log_stop_wrapper.subprocess.DEVNULL
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def test_main_recovers_when_timestamp_file_is_corrupted(
|
|
271
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
272
|
-
tmp_path: Path,
|
|
273
|
-
) -> None:
|
|
274
|
-
timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
|
|
275
|
-
timestamp_file.parent.mkdir(parents=True, exist_ok=True)
|
|
276
|
-
timestamp_file.write_text("not-a-float")
|
|
277
|
-
|
|
278
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
279
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
280
|
-
|
|
281
|
-
captured_commands: list[list[str]] = []
|
|
282
|
-
|
|
283
|
-
def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
|
|
284
|
-
captured_commands.append(list(command_list))
|
|
285
|
-
return object()
|
|
286
|
-
|
|
287
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
288
|
-
|
|
289
|
-
exit_code = hook_log_stop_wrapper.main()
|
|
290
|
-
|
|
291
|
-
assert exit_code == 0
|
|
292
|
-
assert len(captured_commands) == 1
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def test_main_treats_future_timestamp_as_not_debounced(
|
|
296
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
297
|
-
tmp_path: Path,
|
|
298
|
-
) -> None:
|
|
299
|
-
timestamp_file = _redirect_timestamp_path(monkeypatch, tmp_path)
|
|
300
|
-
timestamp_file.parent.mkdir(parents=True, exist_ok=True)
|
|
301
|
-
far_future_timestamp = time.time() + 3600
|
|
302
|
-
timestamp_file.write_text(str(far_future_timestamp))
|
|
303
|
-
|
|
304
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
305
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
306
|
-
|
|
307
|
-
captured_commands: list[list[str]] = []
|
|
308
|
-
|
|
309
|
-
def _fake_popen(command_list: list[str], **_kwargs: object) -> object:
|
|
310
|
-
captured_commands.append(list(command_list))
|
|
311
|
-
return object()
|
|
312
|
-
|
|
313
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
314
|
-
|
|
315
|
-
exit_code = hook_log_stop_wrapper.main()
|
|
316
|
-
|
|
317
|
-
assert exit_code == 0
|
|
318
|
-
assert len(captured_commands) == 1
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
def test_main_passes_hidden_startupinfo_on_windows(
|
|
322
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
323
|
-
tmp_path: Path,
|
|
324
|
-
) -> None:
|
|
325
|
-
_redirect_timestamp_path(monkeypatch, tmp_path)
|
|
326
|
-
monkeypatch.delenv(BWS_ACCESS_TOKEN_ENV_VAR, raising=False)
|
|
327
|
-
monkeypatch.setattr(hook_log_stop_wrapper.shutil, "which", lambda _name: None)
|
|
328
|
-
|
|
329
|
-
captured_kwargs: dict[str, object] = {}
|
|
330
|
-
|
|
331
|
-
def _fake_popen(command_list: list[str], **kwargs: object) -> object:
|
|
332
|
-
captured_kwargs.update(kwargs)
|
|
333
|
-
return object()
|
|
334
|
-
|
|
335
|
-
monkeypatch.setattr(hook_log_stop_wrapper.subprocess, "Popen", _fake_popen)
|
|
336
|
-
|
|
337
|
-
hook_log_stop_wrapper.main()
|
|
338
|
-
|
|
339
|
-
if os.name == "nt":
|
|
340
|
-
startup_info = captured_kwargs.get("startupinfo")
|
|
341
|
-
assert startup_info is not None
|
|
342
|
-
assert startup_info.dwFlags & subprocess.STARTF_USESHOWWINDOW
|
|
343
|
-
assert startup_info.wShowWindow == subprocess.SW_HIDE
|
|
344
|
-
else:
|
|
345
|
-
assert captured_kwargs.get("start_new_session") is True
|
|
@@ -76,12 +76,6 @@ ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX: tuple[str, ...] = (
|
|
|
76
76
|
)
|
|
77
77
|
ALL_GIT_MERGE_BASE_COMMAND_PREFIX: tuple[str, ...] = ("git", "merge-base")
|
|
78
78
|
GIT_REFERENCE_QUERY_TIMEOUT_SECONDS: int = 15
|
|
79
|
-
CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR: str = "CODE_REVIEW_PUSH_GATE_PATH"
|
|
80
|
-
CODE_REVIEW_PUSH_GATE_MODULE_NAME: str = "code_review_push_gate"
|
|
81
|
-
CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME: str = "code_review_push_gate.py"
|
|
82
|
-
CODE_REVIEW_DENY_REASON_FUNCTION_NAME: str = "deny_reason_for_directory"
|
|
83
|
-
BLOCKING_DIRECTORY_NAME: str = "blocking"
|
|
84
|
-
CODE_REVIEW_STAMP_BLOCK_EXIT_CODE: int = 1
|
|
85
79
|
ALL_PROTECTED_BRANCH_PUSH_NAMES: tuple[str, ...] = ("main", "master")
|
|
86
80
|
PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE: int = 1
|
|
87
81
|
PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE: str = (
|
|
@@ -44,8 +44,6 @@ Exit codes:
|
|
|
44
44
|
|
|
45
45
|
from __future__ import annotations
|
|
46
46
|
|
|
47
|
-
import importlib.util
|
|
48
|
-
import os
|
|
49
47
|
import subprocess
|
|
50
48
|
import sys
|
|
51
49
|
from pathlib import Path
|
|
@@ -59,12 +57,6 @@ from git_hooks_constants import (
|
|
|
59
57
|
ALL_PROTECTED_BRANCH_PUSH_NAMES,
|
|
60
58
|
ALL_ZEROS_OBJECT_NAME_CHARACTER,
|
|
61
59
|
BASE_REFERENCE_ARGUMENT,
|
|
62
|
-
BLOCKING_DIRECTORY_NAME,
|
|
63
|
-
CODE_REVIEW_DENY_REASON_FUNCTION_NAME,
|
|
64
|
-
CODE_REVIEW_PUSH_GATE_MODULE_NAME,
|
|
65
|
-
CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR,
|
|
66
|
-
CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME,
|
|
67
|
-
CODE_REVIEW_STAMP_BLOCK_EXIT_CODE,
|
|
68
60
|
DEFAULT_REMOTE_BASE_REFERENCE,
|
|
69
61
|
GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE,
|
|
70
62
|
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
@@ -509,80 +501,6 @@ def invoke_gate(gate_script_path: Path, base_reference: str) -> int:
|
|
|
509
501
|
return completion.returncode
|
|
510
502
|
|
|
511
503
|
|
|
512
|
-
def resolve_code_review_gate_path() -> tuple[Path, Path | None]:
|
|
513
|
-
"""Return the code-review push gate script path and its exact-allow override.
|
|
514
|
-
|
|
515
|
-
The override environment variable, when set, names the only path the trust
|
|
516
|
-
check accepts; otherwise the gate script resolves to the ``blocking``
|
|
517
|
-
directory beside this hook's ``git-hooks`` directory, which holds in both
|
|
518
|
-
the repository layout and the installed ``~/.claude/hooks`` layout.
|
|
519
|
-
|
|
520
|
-
Returns:
|
|
521
|
-
A pair of the resolved gate script path and either the exact override
|
|
522
|
-
path (override set) or None (trust-root case).
|
|
523
|
-
"""
|
|
524
|
-
override_path_raw = os.environ.get(
|
|
525
|
-
CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR, ""
|
|
526
|
-
).strip()
|
|
527
|
-
if override_path_raw:
|
|
528
|
-
exact_override = Path(override_path_raw).resolve()
|
|
529
|
-
return exact_override, exact_override
|
|
530
|
-
blocking_directory = Path(__file__).resolve().parent.parent / BLOCKING_DIRECTORY_NAME
|
|
531
|
-
return blocking_directory / CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME, None
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
def load_code_review_deny_reason(gate_script_path: Path, work_tree_directory: str) -> str | None:
|
|
535
|
-
"""Import the gate script and read its push deny reason for a work tree.
|
|
536
|
-
|
|
537
|
-
Any import or evaluation failure reads as no deny reason.
|
|
538
|
-
|
|
539
|
-
Args:
|
|
540
|
-
gate_script_path: The resolved code-review push gate script path.
|
|
541
|
-
work_tree_directory: The work tree the push targets.
|
|
542
|
-
|
|
543
|
-
Returns:
|
|
544
|
-
The gate's deny reason when the surface lacks a covering low stamp;
|
|
545
|
-
None when the gate allows the push or could not be evaluated.
|
|
546
|
-
"""
|
|
547
|
-
try:
|
|
548
|
-
gate_specification = importlib.util.spec_from_file_location(
|
|
549
|
-
CODE_REVIEW_PUSH_GATE_MODULE_NAME, str(gate_script_path)
|
|
550
|
-
)
|
|
551
|
-
if gate_specification is None or gate_specification.loader is None:
|
|
552
|
-
return None
|
|
553
|
-
gate_module = importlib.util.module_from_spec(gate_specification)
|
|
554
|
-
gate_specification.loader.exec_module(gate_module)
|
|
555
|
-
deny_reason_function = getattr(gate_module, CODE_REVIEW_DENY_REASON_FUNCTION_NAME, None)
|
|
556
|
-
if deny_reason_function is None:
|
|
557
|
-
return None
|
|
558
|
-
deny_reason = deny_reason_function(work_tree_directory)
|
|
559
|
-
except Exception:
|
|
560
|
-
return None
|
|
561
|
-
if isinstance(deny_reason, str) and deny_reason:
|
|
562
|
-
return deny_reason
|
|
563
|
-
return None
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
def code_review_stamp_block_exit_code() -> int:
|
|
567
|
-
"""Block the push when no clean low code-review stamp covers the surface.
|
|
568
|
-
|
|
569
|
-
Runs the gate decision only when the gate script sits at a trusted
|
|
570
|
-
installed location (or the exact override path). When the gate is absent
|
|
571
|
-
from a trusted location, the check is skipped and the push is allowed,
|
|
572
|
-
matching the CODE_RULES gate's fail-open posture.
|
|
573
|
-
|
|
574
|
-
Returns:
|
|
575
|
-
The block exit code when a covering low stamp is missing; 0 when the
|
|
576
|
-
surface is covered or the gate is not installed at a trusted location.
|
|
577
|
-
"""
|
|
578
|
-
gate_script_path, exact_allowed_path = resolve_code_review_gate_path()
|
|
579
|
-
if not is_safe_regular_file(gate_script_path, exact_allowed_path):
|
|
580
|
-
return 0
|
|
581
|
-
deny_reason = load_code_review_deny_reason(gate_script_path, os.getcwd())
|
|
582
|
-
if deny_reason is None:
|
|
583
|
-
return 0
|
|
584
|
-
sys.stderr.write(deny_reason + "\n")
|
|
585
|
-
return CODE_REVIEW_STAMP_BLOCK_EXIT_CODE
|
|
586
504
|
|
|
587
505
|
|
|
588
506
|
def main() -> int:
|
|
@@ -623,7 +541,7 @@ def main() -> int:
|
|
|
623
541
|
pre_push_gate_script_not_found_message.format(path=gate_script_path),
|
|
624
542
|
file=sys.stderr,
|
|
625
543
|
)
|
|
626
|
-
return
|
|
544
|
+
return 0
|
|
627
545
|
base_reference = _resolve_gate_base_from_parsed(parsed_stdin)
|
|
628
546
|
if base_reference is None:
|
|
629
547
|
return 0
|
|
@@ -632,7 +550,7 @@ def main() -> int:
|
|
|
632
550
|
return gate_infrastructure_failure_exit_code
|
|
633
551
|
if base_reference == unresolvable_merge_base_sentinel:
|
|
634
552
|
print(unresolvable_merge_base_message, file=sys.stderr)
|
|
635
|
-
return
|
|
553
|
+
return 0
|
|
636
554
|
remote_name = resolve_remote_name_from_arguments(sys.argv)
|
|
637
555
|
try:
|
|
638
556
|
usable_base_reference = resolve_usable_base_reference(
|
|
@@ -645,7 +563,7 @@ def main() -> int:
|
|
|
645
563
|
code_rules_exit_code = invoke_gate(gate_script_path, usable_base_reference)
|
|
646
564
|
if code_rules_exit_code != 0:
|
|
647
565
|
return code_rules_exit_code
|
|
648
|
-
return
|
|
566
|
+
return 0
|
|
649
567
|
|
|
650
568
|
|
|
651
569
|
if __name__ == "__main__":
|