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,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
|