claude-dev-env 2.17.0 → 2.19.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/.agents/skills/AGENTS.md +4 -4
- package/.agents/skills/_shared/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +89 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +5 -0
- package/.agents/skills/orchestrator/SKILL.md +6 -4
- package/.agents/skills/orchestrator-refresh/SKILL.md +9 -5
- package/.agents/skills/pr-cleanup/SKILL.md +17 -85
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +15 -0
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +20 -0
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/SKILL.md +18 -12
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/offense-examples.md +1 -1
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/task-seeds.md +3 -2
- package/.agents/skills/pr-refinement/SKILL.md +31 -0
- package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/SKILL.md +24 -7
- package/.agents/skills/{small-cl → pr-small-cl}/SKILL.md +1 -1
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +33 -5
- package/.agents/skills/skill-builder/SKILL.md +1 -0
- package/.agents/skills/team-advisor/SKILL.md +5 -5
- package/.agents/skills-archived/bugteam/SKILL.md +1 -1
- package/.agents/skills-archived/bugteam/test_skill_additions.py +1 -1
- package/.agents/skills-archived/comments/SKILL.md +1 -1
- package/.agents/skills-archived/descriptions/SKILL.md +1 -1
- package/.agents/skills-archived/reviews/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/advisor/AGENTS.md +5 -5
- package/_shared/advisor/advisor-protocol.md +54 -31
- package/_shared/advisor/reference/advisor-block.md +5 -1
- package/_shared/advisor/reference/consult-format.md +1 -1
- package/_shared/advisor/reference/identity.md +28 -0
- package/_shared/advisor/reference/lifecycle.md +8 -1
- package/_shared/advisor/reference/sol-rung.md +12 -7
- package/_shared/advisor/reference/spawn-walk-log.md +6 -5
- package/_shared/advisor/reference/third-party-bind.md +5 -6
- package/_shared/advisor/reference/warm-up.md +9 -2
- package/_shared/advisor/scripts/codex_sol_advisor.py +66 -11
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +9 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +16 -4
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +11 -10
- package/_shared/advisor/scripts/model_tier_run_validator.py +50 -23
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +108 -3
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +171 -82
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +43 -0
- package/_shared/advisor/scripts/tier_model_ids.py +75 -7
- package/_shared/pr-loop/AGENTS.md +2 -1
- package/_shared/pr-loop/preflight-proposal.md +56 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +47 -4
- package/_shared/pr-loop/worker-spawn.md +4 -3
- package/bin/ever-shipped-skills.mjs +14 -0
- package/bin/install-constants.mjs +5 -0
- package/bin/install.agents-home.test.mjs +23 -0
- package/bin/install.codex-rules.test.mjs +61 -6
- package/bin/install.mjs +57 -9
- package/bin/install.prune.test.mjs +65 -3
- package/bin/install.test.mjs +202 -2
- package/docs/CODE_RULES.md +4 -0
- package/docs/references/team-advisor-skill.md +2 -2
- package/hooks/advisory/migration_safety_advisor.py +14 -14
- package/hooks/advisory/test_migration_safety_advisor.py +120 -0
- package/hooks/atomic_file_writer.py +81 -0
- package/hooks/blocking/AGENTS.md +2 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +55 -4
- package/hooks/blocking/code_rules_enforcer_test_support.py +92 -0
- package/hooks/blocking/destructive_command_blocker.py +8 -7
- package/hooks/blocking/pre_tool_use_dispatcher.py +11 -7
- package/hooks/blocking/precommit_code_rules_gate.py +14 -139
- package/hooks/blocking/sensitive_file_protector.py +8 -6
- package/hooks/blocking/session_edit_stage_gate.py +31 -1
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +171 -1
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +37 -12
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +54 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +15 -53
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +13 -62
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +308 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +14 -71
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +14 -16
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +31 -46
- package/hooks/blocking/test_code_rules_enforcer_stage.py +192 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +13 -59
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +42 -25
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +58 -38
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +113 -1
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +70 -3
- package/hooks/blocking/test_hook_subprocess_support.py +296 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +1 -1
- package/hooks/blocking/test_pii_scanner.py +81 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +32 -3
- package/hooks/blocking/test_precommit_code_rules_gate.py +11 -16
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +168 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +25 -18
- package/hooks/blocking/test_sensitive_file_protector.py +145 -5
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +64 -0
- package/hooks/blocking/test_session_edit_stage_gate_support.py +204 -0
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +116 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +31 -20
- package/hooks/blocking/test_test_preflight_check.py +80 -0
- package/hooks/git-hooks/pre_commit.py +6 -2
- package/hooks/git-hooks/test_pre_push.py +57 -0
- package/hooks/hooks_constants/AGENTS.md +3 -1
- package/hooks/hooks_constants/atomic_file_writer_constants.py +4 -0
- package/hooks/hooks_constants/banned_identifiers_constants.py +1 -0
- package/hooks/hooks_constants/destructive_command_environment_constants.py +18 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +1 -11
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +9 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +1 -1
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +2 -4
- package/hooks/json_file_reader.py +22 -0
- package/hooks/pending_sidecars.py +33 -0
- package/hooks/test_atomic_file_writer.py +73 -0
- package/hooks/test_json_file_reader.py +24 -0
- package/hooks/test_pending_sidecars.py +25 -0
- package/package.json +1 -1
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/fetch-commands.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/report-template.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/rule-checklist.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/examples.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/offense-taxonomy.md +0 -0
|
@@ -12,20 +12,21 @@ so the repository code rules do not govern it::
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import os
|
|
15
|
-
import
|
|
16
|
-
import sys
|
|
15
|
+
from subprocess import CompletedProcess
|
|
17
16
|
import tempfile
|
|
18
17
|
from pathlib import Path
|
|
19
18
|
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
20
21
|
from blocking import _path_setup # noqa: F401 (pins this checkout ahead of sibling worktrees)
|
|
22
|
+
import code_rules_enforcer_test_support
|
|
21
23
|
from blocking.code_rules_shared import is_agent_home_tooling, is_ephemeral_path
|
|
22
|
-
|
|
23
|
-
_ENFORCER_SCRIPT = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
24
|
+
from code_rules_enforcer_test_support import run_precheck
|
|
24
25
|
|
|
25
26
|
_VIOLATING_PRODUCTION_SOURCE = "def process_data(payload: str) -> None:\n print(payload)\n"
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def _check_as(candidate_source: str, target_path: str) ->
|
|
29
|
+
def _check_as(candidate_source: str, target_path: str) -> CompletedProcess[str]:
|
|
29
30
|
"""Run the enforcer's pre-check on a candidate judged as another path.
|
|
30
31
|
|
|
31
32
|
Args:
|
|
@@ -38,13 +39,37 @@ def _check_as(candidate_source: str, target_path: str) -> subprocess.CompletedPr
|
|
|
38
39
|
with tempfile.TemporaryDirectory() as scratch:
|
|
39
40
|
candidate = Path(scratch) / "candidate.py"
|
|
40
41
|
candidate.write_text(candidate_source, encoding="utf-8")
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
return run_precheck(candidate, target_path, None)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_run_precheck_forwards_environment_mapping(
|
|
46
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
47
|
+
) -> None:
|
|
48
|
+
expected_environment_by_name = {"CLAUDE_JOB_DIR": "/agent/jobs"}
|
|
49
|
+
captured_environment_by_name: dict[str, str] | None = None
|
|
50
|
+
|
|
51
|
+
def capture_run_enforcer_cli(
|
|
52
|
+
_script_path: Path,
|
|
53
|
+
_all_arguments: list[str],
|
|
54
|
+
extra_environment_by_name: dict[str, str] | None,
|
|
55
|
+
) -> CompletedProcess[str]:
|
|
56
|
+
nonlocal captured_environment_by_name
|
|
57
|
+
captured_environment_by_name = extra_environment_by_name
|
|
58
|
+
return CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
59
|
+
|
|
60
|
+
monkeypatch.setattr(
|
|
61
|
+
code_rules_enforcer_test_support,
|
|
62
|
+
"run_enforcer_cli",
|
|
63
|
+
capture_run_enforcer_cli,
|
|
64
|
+
)
|
|
65
|
+
completed = run_precheck(
|
|
66
|
+
Path("candidate.py"),
|
|
67
|
+
"target.py",
|
|
68
|
+
expected_environment_by_name,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
assert completed.returncode == 0
|
|
72
|
+
assert captured_environment_by_name == {"CLAUDE_JOB_DIR": "/agent/jobs"}
|
|
48
73
|
|
|
49
74
|
|
|
50
75
|
def test_posix_agent_home_path_is_agent_tooling() -> None:
|
|
@@ -107,6 +107,60 @@ def test_should_not_flag_exact_match_banned_identifier() -> None:
|
|
|
107
107
|
assert issues == []
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def test_should_allow_annotated_public_definition_name_and_parameters() -> None:
|
|
111
|
+
source = (
|
|
112
|
+
"def validate_nine_patch_output_path(output_path: str) -> str: # pragma: no-banned-noun\n"
|
|
113
|
+
" local_output_path = output_path\n"
|
|
114
|
+
" return local_output_path\n"
|
|
115
|
+
)
|
|
116
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
117
|
+
assert any("local_output_path" in each_issue for each_issue in issues)
|
|
118
|
+
assert not any(
|
|
119
|
+
"Identifier 'validate_nine_patch_output_path'" in each_issue
|
|
120
|
+
or "Identifier 'output_path'" in each_issue
|
|
121
|
+
for each_issue in issues
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_enforcer_keeps_annotation_scope_on_definition_signature() -> None:
|
|
126
|
+
source = (
|
|
127
|
+
"def validate_nine_patch_output_path(output_path: str) -> str: # pragma: no-banned-noun\n"
|
|
128
|
+
" local_output_path = output_path\n"
|
|
129
|
+
" return local_output_path\n"
|
|
130
|
+
)
|
|
131
|
+
issues = validate_content(source, PRODUCTION_FILE_PATH)
|
|
132
|
+
assert any("Identifier 'local_output_path'" in each_issue for each_issue in issues)
|
|
133
|
+
assert not any(
|
|
134
|
+
"Identifier 'validate_nine_patch_output_path'" in each_issue
|
|
135
|
+
or "Identifier 'output_path'" in each_issue
|
|
136
|
+
for each_issue in issues
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_should_keep_annotation_scoped_to_other_definitions_and_near_matches() -> None:
|
|
141
|
+
source = (
|
|
142
|
+
"def validate_nine_patch_output_paths(output_path: str) -> str:\n"
|
|
143
|
+
" return output_path\n"
|
|
144
|
+
"def other_handler(other_output_path: str) -> str:\n"
|
|
145
|
+
" return other_output_path\n"
|
|
146
|
+
)
|
|
147
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
148
|
+
assert any(
|
|
149
|
+
"validate_nine_patch_output_paths" in each_issue for each_issue in issues
|
|
150
|
+
)
|
|
151
|
+
assert any("other_output_path" in each_issue for each_issue in issues)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_should_require_the_exact_compatibility_annotation() -> None:
|
|
155
|
+
source = (
|
|
156
|
+
"def validate_nine_patch_output_path(output_path: str) -> str: # pragma: no-banned-nouns\n"
|
|
157
|
+
" return output_path\n"
|
|
158
|
+
)
|
|
159
|
+
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
160
|
+
assert any("validate_nine_patch_output_path" in each_issue for each_issue in issues)
|
|
161
|
+
assert any("output_path" in each_issue for each_issue in issues)
|
|
162
|
+
|
|
163
|
+
|
|
110
164
|
def test_should_not_flag_dunder_method_with_banned_word() -> None:
|
|
111
165
|
source = "class Foo:\n def __init_data__(self) -> None: pass\n"
|
|
112
166
|
issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
|
|
@@ -14,15 +14,12 @@ target path the same way it would for the real directory.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import io
|
|
18
17
|
import json
|
|
19
18
|
import pathlib
|
|
20
19
|
import shutil
|
|
21
|
-
import subprocess
|
|
22
20
|
import sys
|
|
23
21
|
import tempfile
|
|
24
22
|
from collections.abc import Iterator
|
|
25
|
-
from types import SimpleNamespace
|
|
26
23
|
|
|
27
24
|
import pytest
|
|
28
25
|
|
|
@@ -34,10 +31,10 @@ if str(_HOOKS_PARENT) not in sys.path:
|
|
|
34
31
|
sys.path.insert(0, str(_HOOKS_PARENT))
|
|
35
32
|
|
|
36
33
|
from code_rules_enforcer import main # noqa: E402
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
from code_rules_enforcer_test_support import ( # noqa: E402
|
|
35
|
+
run_precheck,
|
|
36
|
+
run_write_entrypoint,
|
|
37
|
+
)
|
|
41
38
|
|
|
42
39
|
SHARED_HELPER_SOURCE = (
|
|
43
40
|
"import re\n"
|
|
@@ -66,38 +63,18 @@ def hook_blocking_dir() -> Iterator[pathlib.Path]:
|
|
|
66
63
|
def _run_main_with_write_payload(
|
|
67
64
|
file_path: str,
|
|
68
65
|
content: str,
|
|
69
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
70
|
-
capsys: pytest.CaptureFixture[str],
|
|
71
66
|
) -> str:
|
|
72
|
-
"""Drive ``main()`` through its stdin entry point for a Write
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
file_path
|
|
76
|
-
content
|
|
77
|
-
monkeypatch: The fixture used to redirect ``sys.stdin``.
|
|
78
|
-
capsys: The fixture used to capture the deny payload on stdout.
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
The captured stdout, which holds the deny payload when violations fire.
|
|
82
|
-
"""
|
|
83
|
-
write_payload = json.dumps(
|
|
84
|
-
{
|
|
85
|
-
"tool_name": "Write",
|
|
86
|
-
"tool_input": {"file_path": file_path, "content": content},
|
|
87
|
-
}
|
|
67
|
+
"""Drive ``main()`` through its stdin entry point for a Write."""
|
|
68
|
+
captured_stdout, _exit_code = run_write_entrypoint(
|
|
69
|
+
main,
|
|
70
|
+
file_path,
|
|
71
|
+
content,
|
|
88
72
|
)
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
code_rules_enforcer.main([])
|
|
92
|
-
except SystemExit:
|
|
93
|
-
pass
|
|
94
|
-
return capsys.readouterr().out
|
|
73
|
+
return captured_stdout
|
|
95
74
|
|
|
96
75
|
|
|
97
76
|
def test_write_of_copied_helper_into_hook_directory_denies(
|
|
98
77
|
hook_blocking_dir: pathlib.Path,
|
|
99
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
100
|
-
capsys: pytest.CaptureFixture[str],
|
|
101
78
|
) -> None:
|
|
102
79
|
"""A Write that copies a sibling helper into a second hook file is denied.
|
|
103
80
|
|
|
@@ -106,7 +83,7 @@ def test_write_of_copied_helper_into_hook_directory_denies(
|
|
|
106
83
|
directory its module docstring names as the primary target."""
|
|
107
84
|
(hook_blocking_dir / "existing_blocker.py").write_text(SHARED_HELPER_SOURCE, encoding="utf-8")
|
|
108
85
|
new_file = hook_blocking_dir / "new_blocker.py"
|
|
109
|
-
stdout = _run_main_with_write_payload(str(new_file), SHARED_HELPER_SOURCE
|
|
86
|
+
stdout = _run_main_with_write_payload(str(new_file), SHARED_HELPER_SOURCE)
|
|
110
87
|
assert stdout != "", (
|
|
111
88
|
"A copied helper written into a second hook-infrastructure file must "
|
|
112
89
|
"produce a deny payload, got empty stdout"
|
|
@@ -122,8 +99,6 @@ def test_write_of_copied_helper_into_hook_directory_denies(
|
|
|
122
99
|
|
|
123
100
|
def test_write_of_unique_helper_into_hook_directory_allows(
|
|
124
101
|
hook_blocking_dir: pathlib.Path,
|
|
125
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
126
|
-
capsys: pytest.CaptureFixture[str],
|
|
127
102
|
) -> None:
|
|
128
103
|
"""A Write of a hook file with no sibling duplicate produces no deny payload.
|
|
129
104
|
|
|
@@ -137,13 +112,13 @@ def test_write_of_unique_helper_into_hook_directory_allows(
|
|
|
137
112
|
" return widened\n"
|
|
138
113
|
)
|
|
139
114
|
new_file = hook_blocking_dir / "unique_blocker.py"
|
|
140
|
-
stdout = _run_main_with_write_payload(str(new_file), unique_helper_source
|
|
115
|
+
stdout = _run_main_with_write_payload(str(new_file), unique_helper_source)
|
|
141
116
|
assert stdout == "", f"a unique hook helper must not be denied, got stdout: {stdout!r}"
|
|
142
117
|
|
|
143
118
|
|
|
144
119
|
def test_precheck_of_copied_helper_at_hook_target_exits_nonzero(
|
|
145
120
|
hook_blocking_dir: pathlib.Path,
|
|
146
|
-
|
|
121
|
+
tmp_path: pathlib.Path,
|
|
147
122
|
) -> None:
|
|
148
123
|
"""The pre-check CLI flags a copied helper judged at a hook-infrastructure target.
|
|
149
124
|
|
|
@@ -151,24 +126,11 @@ def test_precheck_of_copied_helper_at_hook_target_exits_nonzero(
|
|
|
151
126
|
routes a hook ``.py`` target through the duplicate-body check rather than
|
|
152
127
|
exiting clean on the blanket hook-infrastructure exemption."""
|
|
153
128
|
(hook_blocking_dir / "existing_blocker.py").write_text(SHARED_HELPER_SOURCE, encoding="utf-8")
|
|
154
|
-
staging_directory =
|
|
129
|
+
staging_directory = tmp_path
|
|
155
130
|
candidate_file = staging_directory / "candidate.py"
|
|
156
131
|
candidate_file.write_text(SHARED_HELPER_SOURCE, encoding="utf-8")
|
|
157
132
|
target_path = str(hook_blocking_dir / "new_blocker.py")
|
|
158
|
-
completed =
|
|
159
|
-
[
|
|
160
|
-
sys.executable,
|
|
161
|
-
str(_ENFORCER_SCRIPT_PATH),
|
|
162
|
-
"--check",
|
|
163
|
-
str(candidate_file),
|
|
164
|
-
"--as",
|
|
165
|
-
target_path,
|
|
166
|
-
],
|
|
167
|
-
input="",
|
|
168
|
-
capture_output=True,
|
|
169
|
-
text=True,
|
|
170
|
-
check=False,
|
|
171
|
-
)
|
|
133
|
+
completed = run_precheck(candidate_file, target_path, None)
|
|
172
134
|
assert completed.returncode == 1, (
|
|
173
135
|
"a copied helper at a hook target must exit nonzero, got: "
|
|
174
136
|
f"{completed.returncode}, stdout: {completed.stdout!r}, "
|
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import io
|
|
6
5
|
import json
|
|
7
|
-
import os
|
|
8
6
|
import subprocess
|
|
9
7
|
import sys
|
|
10
8
|
import tempfile
|
|
11
9
|
from pathlib import Path
|
|
12
|
-
from types import SimpleNamespace
|
|
13
10
|
|
|
14
11
|
import pytest
|
|
15
12
|
|
|
@@ -22,70 +19,40 @@ if _HOOKS_DIRECTORY not in sys.path:
|
|
|
22
19
|
|
|
23
20
|
from code_rules_enforcer import main as enforcer_main # noqa: E402
|
|
24
21
|
from code_rules_shared import is_ephemeral_script_path # noqa: E402
|
|
22
|
+
from code_rules_enforcer_test_support import ( # noqa: E402
|
|
23
|
+
build_write_payload,
|
|
24
|
+
run_enforcer_cli,
|
|
25
|
+
run_write_entrypoint,
|
|
26
|
+
)
|
|
25
27
|
|
|
26
28
|
_ENFORCER_SCRIPT = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
27
29
|
_TDD_SCRIPT = Path(__file__).resolve().parent / "tdd_enforcer.py"
|
|
28
30
|
|
|
29
31
|
_VIOLATING_PRODUCTION_SOURCE = "def process_data(payload: str) -> None:\n print(payload)\n"
|
|
30
32
|
|
|
31
|
-
code_rules_enforcer_module = SimpleNamespace(main=enforcer_main, sys=sys)
|
|
32
|
-
|
|
33
33
|
|
|
34
34
|
def _run_enforcer_cli(
|
|
35
35
|
all_cli_arguments: list[str],
|
|
36
|
-
|
|
36
|
+
extra_environment_by_name: dict[str, str] | None = None,
|
|
37
37
|
) -> subprocess.CompletedProcess[str]:
|
|
38
38
|
"""Drive the enforcer script through its real argv entry point.
|
|
39
39
|
|
|
40
40
|
Args:
|
|
41
41
|
all_cli_arguments: The argument vector appended after the script path.
|
|
42
|
-
|
|
42
|
+
extra_environment_by_name: Additional environment variables merged into the subprocess environment.
|
|
43
43
|
|
|
44
44
|
Returns:
|
|
45
45
|
The completed process carrying stdout, stderr, and the exit code.
|
|
46
46
|
"""
|
|
47
|
-
|
|
48
|
-
return subprocess.run(
|
|
49
|
-
[sys.executable, str(_ENFORCER_SCRIPT), *all_cli_arguments],
|
|
50
|
-
input="",
|
|
51
|
-
capture_output=True,
|
|
52
|
-
text=True,
|
|
53
|
-
check=False,
|
|
54
|
-
env=subprocess_env,
|
|
55
|
-
)
|
|
47
|
+
return run_enforcer_cli(_ENFORCER_SCRIPT, all_cli_arguments, extra_environment_by_name)
|
|
56
48
|
|
|
57
49
|
|
|
58
50
|
def _run_main_with_write_payload(
|
|
59
51
|
file_path: str,
|
|
60
52
|
content: str,
|
|
61
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
62
|
-
capsys: pytest.CaptureFixture[str],
|
|
63
53
|
) -> tuple[str, int]:
|
|
64
|
-
"""Drive enforcer_main through its stdin entry point for a Write payload.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
file_path: The destination path the Write targets.
|
|
68
|
-
content: The content of the Write.
|
|
69
|
-
monkeypatch: The pytest fixture used to redirect sys.stdin.
|
|
70
|
-
capsys: The pytest fixture used to capture the deny payload on stdout.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
A tuple of (captured_stdout, exit_code).
|
|
74
|
-
"""
|
|
75
|
-
write_payload = json.dumps(
|
|
76
|
-
{
|
|
77
|
-
"tool_name": "Write",
|
|
78
|
-
"tool_input": {"file_path": file_path, "content": content},
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
monkeypatch.setattr(code_rules_enforcer_module.sys, "stdin", io.StringIO(write_payload))
|
|
82
|
-
exit_code = 0
|
|
83
|
-
try:
|
|
84
|
-
code_rules_enforcer_module.main([])
|
|
85
|
-
except SystemExit as each_exit:
|
|
86
|
-
exit_code = int(each_exit.code or 0)
|
|
87
|
-
captured = capsys.readouterr()
|
|
88
|
-
return captured.out, exit_code
|
|
54
|
+
"""Drive enforcer_main through its stdin entry point for a Write payload."""
|
|
55
|
+
return run_write_entrypoint(enforcer_main, file_path, content)
|
|
89
56
|
|
|
90
57
|
|
|
91
58
|
def _run_tdd_with_write_payload(
|
|
@@ -101,18 +68,12 @@ def _run_tdd_with_write_payload(
|
|
|
101
68
|
Returns:
|
|
102
69
|
The completed process carrying stdout, stderr, and the exit code.
|
|
103
70
|
"""
|
|
104
|
-
write_payload =
|
|
105
|
-
{
|
|
106
|
-
"tool_name": "Write",
|
|
107
|
-
"tool_input": {"file_path": file_path, "content": content},
|
|
108
|
-
}
|
|
109
|
-
)
|
|
71
|
+
write_payload = build_write_payload(file_path, content)
|
|
110
72
|
return subprocess.run(
|
|
111
73
|
[sys.executable, str(_TDD_SCRIPT)],
|
|
112
74
|
input=write_payload,
|
|
113
75
|
capture_output=True,
|
|
114
76
|
text=True,
|
|
115
|
-
check=False,
|
|
116
77
|
)
|
|
117
78
|
|
|
118
79
|
|
|
@@ -279,7 +240,6 @@ def test_should_return_false_for_empty_path() -> None:
|
|
|
279
240
|
def test_should_exit_zero_for_ephemeral_pretooluse_target(
|
|
280
241
|
tmp_path: Path,
|
|
281
242
|
monkeypatch: pytest.MonkeyPatch,
|
|
282
|
-
capsys: pytest.CaptureFixture[str],
|
|
283
243
|
) -> None:
|
|
284
244
|
"""B11: enforcer main exits 0 with no deny payload for an ephemeral file_path."""
|
|
285
245
|
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
@@ -287,8 +247,6 @@ def test_should_exit_zero_for_ephemeral_pretooluse_target(
|
|
|
287
247
|
captured_stdout, exit_code = _run_main_with_write_payload(
|
|
288
248
|
ephemeral_path,
|
|
289
249
|
_VIOLATING_PRODUCTION_SOURCE,
|
|
290
|
-
monkeypatch,
|
|
291
|
-
capsys,
|
|
292
250
|
)
|
|
293
251
|
assert exit_code == 0
|
|
294
252
|
assert "deny" not in captured_stdout.lower()
|
|
@@ -297,7 +255,6 @@ def test_should_exit_zero_for_ephemeral_pretooluse_target(
|
|
|
297
255
|
def test_should_exit_zero_for_ephemeral_path_with_hooks_substring(
|
|
298
256
|
tmp_path: Path,
|
|
299
257
|
monkeypatch: pytest.MonkeyPatch,
|
|
300
|
-
capsys: pytest.CaptureFixture[str],
|
|
301
258
|
) -> None:
|
|
302
259
|
"""B12: an ephemeral path carrying /hooks/ exits 0 (hook-infra route short-circuited)."""
|
|
303
260
|
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
@@ -305,8 +262,6 @@ def test_should_exit_zero_for_ephemeral_path_with_hooks_substring(
|
|
|
305
262
|
captured_stdout, exit_code = _run_main_with_write_payload(
|
|
306
263
|
ephemeral_hooks_path,
|
|
307
264
|
_VIOLATING_PRODUCTION_SOURCE,
|
|
308
|
-
monkeypatch,
|
|
309
|
-
capsys,
|
|
310
265
|
)
|
|
311
266
|
assert exit_code == 0
|
|
312
267
|
assert "deny" not in captured_stdout.lower()
|
|
@@ -319,7 +274,7 @@ def test_should_return_zero_from_precheck_for_ephemeral_target(
|
|
|
319
274
|
candidate_file = tmp_path / "candidate.py"
|
|
320
275
|
candidate_file.write_text(_VIOLATING_PRODUCTION_SOURCE, encoding="utf-8")
|
|
321
276
|
ephemeral_target = "/tmp/target.py"
|
|
322
|
-
completed = _run_enforcer_cli(["--check", str(candidate_file), "--as", ephemeral_target]
|
|
277
|
+
completed = _run_enforcer_cli(["--check", str(candidate_file), "--as", ephemeral_target])
|
|
323
278
|
assert completed.returncode == 0
|
|
324
279
|
|
|
325
280
|
|
|
@@ -332,7 +287,6 @@ def test_should_run_full_suite_for_non_ephemeral_target(
|
|
|
332
287
|
non_ephemeral_target = "/repo/src/orders.py"
|
|
333
288
|
completed = _run_enforcer_cli(
|
|
334
289
|
["--check", str(candidate_file), "--as", non_ephemeral_target],
|
|
335
|
-
extra_env={},
|
|
336
290
|
)
|
|
337
291
|
assert completed.returncode == 1
|
|
338
292
|
|
|
@@ -346,7 +300,7 @@ def test_should_run_full_suite_when_override_truthy(
|
|
|
346
300
|
ephemeral_target = "/tmp/scratch.py"
|
|
347
301
|
completed = _run_enforcer_cli(
|
|
348
302
|
["--check", str(candidate_file), "--as", ephemeral_target],
|
|
349
|
-
|
|
303
|
+
extra_environment_by_name={"CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT": "1"},
|
|
350
304
|
)
|
|
351
305
|
assert completed.returncode == 1
|
|
352
306
|
|
|
@@ -354,7 +308,6 @@ def test_should_run_full_suite_when_override_truthy(
|
|
|
354
308
|
def test_should_exempt_same_path_set_on_both_gates(
|
|
355
309
|
tmp_path: Path,
|
|
356
310
|
monkeypatch: pytest.MonkeyPatch,
|
|
357
|
-
capsys: pytest.CaptureFixture[str],
|
|
358
311
|
) -> None:
|
|
359
312
|
"""B21: every ephemeral path exits 0 on both enforcer main and TDD enforcer main."""
|
|
360
313
|
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
@@ -367,8 +320,6 @@ def test_should_exempt_same_path_set_on_both_gates(
|
|
|
367
320
|
captured_stdout, exit_code = _run_main_with_write_payload(
|
|
368
321
|
each_ephemeral_path,
|
|
369
322
|
_VIOLATING_PRODUCTION_SOURCE,
|
|
370
|
-
monkeypatch,
|
|
371
|
-
capsys,
|
|
372
323
|
)
|
|
373
324
|
assert exit_code == 0, (
|
|
374
325
|
f"enforcer must exit 0 for ephemeral path {each_ephemeral_path!r}, "
|