claude-dev-env 8.69.0 → 8.69.1
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/pr-lifecycle/SKILL.md +3 -1
- package/.agents/skills/second-account-workers/SKILL.md +21 -3
- package/docs/rule-guides/features-start-with-an-eval.md +40 -0
- package/hooks/blocking/followup_pr_dedupe.py +55 -5
- package/hooks/blocking/pr_lifecycle_skill_gate.py +62 -11
- package/hooks/blocking/pull_request_proof.py +86 -20
- package/hooks/blocking/test_artifact_look_gate.py +1 -0
- package/hooks/blocking/test_followup_pr_dedupe.py +33 -0
- package/hooks/blocking/test_pr_lifecycle_skill_gate.py +117 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +1 -5
- package/hooks/blocking/test_pull_request_proof.py +124 -1
- package/hooks/features/conduct-gates.md +1 -1
- package/hooks/features/write-edit-blocking.md +3 -5
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/artifact_look_gate_constants.py +1 -2
- package/hooks/hooks_constants/followup_pr_dedupe_constants.py +4 -0
- package/hooks/hooks_constants/pr_lifecycle_skill_gate_constants.py +3 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/pull_request_proof_constants.py +20 -1
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +4 -11
- package/hooks/hooks_constants/transcript_skill_scan_constants.py +1 -0
- package/hooks/test_transcript_skill_scan.py +75 -1
- package/hooks/transcript_skill_scan.py +87 -15
- package/package.json +1 -1
- package/rules/anti-corollary-tests.md +2 -2
- package/rules/archiving-agent-config.md +2 -2
- package/rules/asd-ste100-language.md +2 -2
- package/rules/bdd.md +1 -1
- package/rules/build-needs-a-user.md +1 -1
- package/rules/cleanup-temp-files.md +3 -3
- package/rules/code-standards.md +1 -1
- package/rules/correction-lens.md +3 -3
- package/rules/destructive-commands.md +3 -3
- package/rules/doc-inventory-integrity.md +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/explore-thoroughly.md +1 -1
- package/rules/failure-blast-radius.md +1 -1
- package/rules/falsify-before-green.md +2 -2
- package/rules/features-start-with-an-eval.md +9 -0
- package/rules/filesystem-search.md +3 -3
- package/rules/flag-non-breaking-findings.md +2 -2
- package/rules/long-horizon-autonomy.md +2 -2
- package/rules/memory-stores-durable-facts.md +2 -2
- package/rules/no-contrast-framing.md +1 -1
- package/rules/no-cross-skill-duplicate-helpers.md +1 -1
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/plain-illustrative-docstrings.md +1 -1
- package/rules/prefer-existing-tools.md +2 -2
- package/rules/proof-before-pull-request.md +1 -1
- package/rules/request-scope.md +1 -1
- package/rules/research-mode.md +2 -2
- package/rules/retired-hook-prose.md +1 -1
- package/rules/session-title.md +1 -1
- package/rules/shell-invocation.md +2 -2
- package/rules/testing.md +1 -1
- package/rules/verify-before-asking.md +3 -3
- package/rules/verify-runtime-state.md +2 -2
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +2 -2
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/account_broker.py +10 -3
- package/scripts/account_broker_support.py +7 -3
- package/scripts/claude_account_worker.py +24 -5
- package/scripts/claude_account_worker_process.py +5 -0
- package/scripts/claude_account_worker_report.py +16 -1
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +12 -0
- package/scripts/test_account_broker.py +38 -0
- package/scripts/test_account_broker_support.py +24 -0
- package/scripts/test_claude_account_worker.py +47 -0
- package/scripts/test_claude_account_worker_process.py +20 -0
- package/scripts/test_claude_account_worker_report.py +15 -0
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +4 -4
- package/scripts/tests/test_rule_load_scopes.py +1 -0
- package/hooks/advisory/conftest.py +0 -10
- package/hooks/advisory/refactor_guard.py +0 -385
- package/hooks/advisory/refactor_guard_test_support.py +0 -46
- package/hooks/advisory/test_refactor_guard.py +0 -148
- package/hooks/advisory/test_refactor_guard_advisory.py +0 -242
- package/hooks/advisory/test_refactor_guard_eligibility.py +0 -166
- package/hooks/hooks_constants/refactor_guard_constants.py +0 -77
- package/hooks/hooks_constants/test_refactor_guard_constants.py +0 -21
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
"""Tests for direct and dispatched refactor guidance."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import subprocess
|
|
8
|
-
import sys
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
|
-
ADVISORY_DIRECTORY = Path(__file__).resolve().parent
|
|
14
|
-
HOOK_SCRIPT_PATH = ADVISORY_DIRECTORY / "refactor_guard.py"
|
|
15
|
-
DISPATCHER_SCRIPT_PATH = ADVISORY_DIRECTORY.parent / "blocking" / "pre_tool_use_dispatcher.py"
|
|
16
|
-
if str(ADVISORY_DIRECTORY) not in sys.path:
|
|
17
|
-
sys.path.insert(0, str(ADVISORY_DIRECTORY))
|
|
18
|
-
|
|
19
|
-
import refactor_guard # noqa: E402
|
|
20
|
-
from refactor_guard_test_support import commit_file, stage_file # noqa: E402
|
|
21
|
-
|
|
22
|
-
pytestmark = pytest.mark.usefixtures("ephemeral_exempt_off")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _refactor_payload(file_path: Path) -> str:
|
|
26
|
-
return json.dumps(
|
|
27
|
-
{
|
|
28
|
-
"tool_name": "Edit",
|
|
29
|
-
"tool_input": {
|
|
30
|
-
"file_path": str(file_path),
|
|
31
|
-
"old_string": "def calculate_total(amount: int) -> int:\n return amount",
|
|
32
|
-
"new_string": "def compute_total(amount: int) -> int:\n return amount",
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _ordinary_edit_payload(file_path: Path) -> str:
|
|
39
|
-
return json.dumps(
|
|
40
|
-
{
|
|
41
|
-
"tool_name": "Edit",
|
|
42
|
-
"tool_input": {
|
|
43
|
-
"file_path": str(file_path),
|
|
44
|
-
"old_string": "return amount",
|
|
45
|
-
"new_string": "return amount + 1",
|
|
46
|
-
},
|
|
47
|
-
}
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _run_hook(
|
|
52
|
-
script_path: Path, repository_path: Path, payload_text: str
|
|
53
|
-
) -> subprocess.CompletedProcess[str]:
|
|
54
|
-
environment_by_key = os.environ.copy()
|
|
55
|
-
environment_by_key["HOME"] = str(repository_path)
|
|
56
|
-
environment_by_key["USERPROFILE"] = str(repository_path)
|
|
57
|
-
return subprocess.run(
|
|
58
|
-
[sys.executable, str(script_path)],
|
|
59
|
-
cwd=repository_path,
|
|
60
|
-
input=payload_text,
|
|
61
|
-
capture_output=True,
|
|
62
|
-
text=True,
|
|
63
|
-
env=environment_by_key,
|
|
64
|
-
check=False,
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_direct_hook_emits_edit_stage_guidance_for_eligible_refactor(
|
|
69
|
-
git_repository: Path,
|
|
70
|
-
) -> None:
|
|
71
|
-
source_path = git_repository / "module.py"
|
|
72
|
-
commit_file(
|
|
73
|
-
git_repository,
|
|
74
|
-
source_path,
|
|
75
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
76
|
-
)
|
|
77
|
-
stage_file(
|
|
78
|
-
git_repository,
|
|
79
|
-
source_path,
|
|
80
|
-
"def calculate_total(amount: int) -> int:\n return amount + 1\n",
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
completed_hook = _run_hook(HOOK_SCRIPT_PATH, git_repository, _refactor_payload(source_path))
|
|
84
|
-
advisory_payload = json.loads(completed_hook.stdout)
|
|
85
|
-
hook_specific_output = advisory_payload["hookSpecificOutput"]
|
|
86
|
-
|
|
87
|
-
assert hook_specific_output["permissionDecision"] == "allow"
|
|
88
|
-
assert "Edit-stage" in advisory_payload["systemMessage"]
|
|
89
|
-
assert "current git diff" in hook_specific_output["additionalContext"]
|
|
90
|
-
assert str(source_path) in advisory_payload["systemMessage"]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_direct_hook_stays_silent_for_ordinary_edit(git_repository: Path) -> None:
|
|
94
|
-
source_path = git_repository / "module.py"
|
|
95
|
-
commit_file(
|
|
96
|
-
git_repository,
|
|
97
|
-
source_path,
|
|
98
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
completed_hook = _run_hook(
|
|
102
|
-
HOOK_SCRIPT_PATH,
|
|
103
|
-
git_repository,
|
|
104
|
-
_ordinary_edit_payload(source_path),
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
assert completed_hook.returncode == 0
|
|
108
|
-
assert completed_hook.stdout == ""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def test_direct_hook_stays_silent_for_write_payload(git_repository: Path) -> None:
|
|
112
|
-
source_path = git_repository / "module.py"
|
|
113
|
-
commit_file(
|
|
114
|
-
git_repository,
|
|
115
|
-
source_path,
|
|
116
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
117
|
-
)
|
|
118
|
-
write_payload = json.dumps(
|
|
119
|
-
{
|
|
120
|
-
"tool_name": "Write",
|
|
121
|
-
"tool_input": {"file_path": str(source_path), "content": "new content"},
|
|
122
|
-
}
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
completed_hook = _run_hook(HOOK_SCRIPT_PATH, git_repository, write_payload)
|
|
126
|
-
|
|
127
|
-
assert completed_hook.returncode == 0
|
|
128
|
-
assert completed_hook.stdout == ""
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def test_dispatcher_preserves_edit_stage_guidance(git_repository: Path) -> None:
|
|
132
|
-
"""A red step (staged, uncommitted test) plus a staged refactor still allows."""
|
|
133
|
-
source_path = git_repository / "module.py"
|
|
134
|
-
matching_test_path = git_repository / "test_module.py"
|
|
135
|
-
commit_file(
|
|
136
|
-
git_repository,
|
|
137
|
-
source_path,
|
|
138
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
139
|
-
)
|
|
140
|
-
stage_file(
|
|
141
|
-
git_repository,
|
|
142
|
-
matching_test_path,
|
|
143
|
-
"def calculate_total(amount: int) -> int:\n return amount\n\n"
|
|
144
|
-
"def test_calculate_total() -> None:\n assert calculate_total(1) == 1\n",
|
|
145
|
-
)
|
|
146
|
-
stage_file(
|
|
147
|
-
git_repository,
|
|
148
|
-
source_path,
|
|
149
|
-
"def calculate_total(amount: int) -> int:\n return amount + 1\n",
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
completed_dispatch = _run_hook(
|
|
153
|
-
DISPATCHER_SCRIPT_PATH,
|
|
154
|
-
git_repository,
|
|
155
|
-
_refactor_payload(source_path),
|
|
156
|
-
)
|
|
157
|
-
dispatcher_payload = json.loads(completed_dispatch.stdout)
|
|
158
|
-
hook_specific_output = dispatcher_payload["hookSpecificOutput"]
|
|
159
|
-
|
|
160
|
-
assert hook_specific_output["permissionDecision"] == "allow", (
|
|
161
|
-
f"Dispatcher output: {completed_dispatch.stdout!r}; stderr: {completed_dispatch.stderr!r}"
|
|
162
|
-
)
|
|
163
|
-
assert "Edit-stage" in dispatcher_payload["systemMessage"]
|
|
164
|
-
assert "Refactor guard" in dispatcher_payload["systemMessage"]
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def test_bypass_token_is_consumed_once(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
168
|
-
bypass_token_path = tmp_path / "refactor-bypass-token"
|
|
169
|
-
monkeypatch.setattr(refactor_guard, "REFACTOR_BYPASS_TOKEN_PATH", bypass_token_path)
|
|
170
|
-
bypass_token_path.write_text("approved", encoding="utf-8")
|
|
171
|
-
|
|
172
|
-
assert refactor_guard.is_bypass_approved()
|
|
173
|
-
assert not bypass_token_path.exists()
|
|
174
|
-
assert not refactor_guard.is_bypass_approved()
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _multi_edit_payload(file_path: Path, all_edits: list[dict[str, str]]) -> str:
|
|
178
|
-
return json.dumps(
|
|
179
|
-
{
|
|
180
|
-
"tool_name": "MultiEdit",
|
|
181
|
-
"tool_input": {"file_path": str(file_path), "edits": all_edits},
|
|
182
|
-
}
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _commit_and_stage_the_renamed_module(git_repository: Path, source_path: Path) -> None:
|
|
187
|
-
"""Commit the base module, then stage an unrelated numeric change to it."""
|
|
188
|
-
commit_file(
|
|
189
|
-
git_repository,
|
|
190
|
-
source_path,
|
|
191
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
192
|
-
)
|
|
193
|
-
stage_file(
|
|
194
|
-
git_repository,
|
|
195
|
-
source_path,
|
|
196
|
-
"def calculate_total(amount: int) -> int:\n return amount + 1\n",
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def test_direct_hook_flags_refactor_in_second_multi_edit(git_repository: Path) -> None:
|
|
201
|
-
"""A rename buried in the second edit of a MultiEdit is caught, not only the first.
|
|
202
|
-
|
|
203
|
-
The first edit is an ordinary numeric change; only the second edit renames
|
|
204
|
-
calculate_total to compute_total outside the currently staged diff.
|
|
205
|
-
"""
|
|
206
|
-
source_path = git_repository / "module.py"
|
|
207
|
-
_commit_and_stage_the_renamed_module(git_repository, source_path)
|
|
208
|
-
payload_text = _multi_edit_payload(
|
|
209
|
-
source_path,
|
|
210
|
-
[
|
|
211
|
-
{"old_string": "return amount", "new_string": "return amount + 1"},
|
|
212
|
-
{
|
|
213
|
-
"old_string": "def calculate_total(amount: int) -> int:\n return amount",
|
|
214
|
-
"new_string": "def compute_total(amount: int) -> int:\n return amount",
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
completed_hook = _run_hook(HOOK_SCRIPT_PATH, git_repository, payload_text)
|
|
220
|
-
advisory_payload = json.loads(completed_hook.stdout)
|
|
221
|
-
hook_specific_output = advisory_payload["hookSpecificOutput"]
|
|
222
|
-
|
|
223
|
-
assert hook_specific_output["permissionDecision"] == "allow"
|
|
224
|
-
assert "Edit-stage" in advisory_payload["systemMessage"]
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def test_direct_hook_stays_silent_for_ordinary_multi_edit(git_repository: Path) -> None:
|
|
228
|
-
source_path = git_repository / "module.py"
|
|
229
|
-
commit_file(
|
|
230
|
-
git_repository,
|
|
231
|
-
source_path,
|
|
232
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
233
|
-
)
|
|
234
|
-
payload_text = _multi_edit_payload(
|
|
235
|
-
source_path,
|
|
236
|
-
[{"old_string": "return amount", "new_string": "return amount + 1"}],
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
completed_hook = _run_hook(HOOK_SCRIPT_PATH, git_repository, payload_text)
|
|
240
|
-
|
|
241
|
-
assert completed_hook.returncode == 0
|
|
242
|
-
assert completed_hook.stdout == ""
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
"""Tests for refactor candidate eligibility against a temporary Git repository."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
ADVISORY_DIRECTORY = Path(__file__).resolve().parent
|
|
11
|
-
if str(ADVISORY_DIRECTORY) not in sys.path:
|
|
12
|
-
sys.path.insert(0, str(ADVISORY_DIRECTORY))
|
|
13
|
-
|
|
14
|
-
import refactor_guard # noqa: E402
|
|
15
|
-
from refactor_guard_test_support import commit_file, stage_file # noqa: E402
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_refactor_candidate_is_eligible_when_old_lines_are_outside_changed_surface(
|
|
19
|
-
git_repository: Path,
|
|
20
|
-
) -> None:
|
|
21
|
-
source_path = git_repository / "module.py"
|
|
22
|
-
commit_file(
|
|
23
|
-
git_repository,
|
|
24
|
-
source_path,
|
|
25
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
26
|
-
)
|
|
27
|
-
stage_file(
|
|
28
|
-
git_repository,
|
|
29
|
-
source_path,
|
|
30
|
-
"def calculate_total(amount: int) -> int:\n return amount + 1\n",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
old_function = "def calculate_total(amount: int) -> int:\n return amount"
|
|
34
|
-
renamed_function = "def compute_total(amount: int) -> int:\n return amount"
|
|
35
|
-
|
|
36
|
-
assert refactor_guard.is_refactor_eligible(str(source_path), old_function, renamed_function)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_refactor_candidate_is_ineligible_when_old_lines_are_in_changed_surface(
|
|
40
|
-
git_repository: Path,
|
|
41
|
-
) -> None:
|
|
42
|
-
source_path = git_repository / "module.py"
|
|
43
|
-
commit_file(git_repository, source_path, "pass\n")
|
|
44
|
-
old_function = "def calculate_total(amount: int) -> int:\n return amount"
|
|
45
|
-
stage_file(git_repository, source_path, f"{old_function}\n")
|
|
46
|
-
renamed_function = "def compute_total(amount: int) -> int:\n return amount"
|
|
47
|
-
|
|
48
|
-
assert not refactor_guard.is_refactor_eligible(str(source_path), old_function, renamed_function)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_changed_surface_reads_staged_and_unstaged_lines(
|
|
52
|
-
git_repository: Path, monkeypatch: pytest.MonkeyPatch
|
|
53
|
-
) -> None:
|
|
54
|
-
source_path = git_repository / "module.py"
|
|
55
|
-
commit_file(git_repository, source_path, "baseline = 1\n")
|
|
56
|
-
|
|
57
|
-
stage_file(git_repository, source_path, "staged_line = 1\n")
|
|
58
|
-
source_path.write_text("unstaged_line = 1\n", encoding="utf-8")
|
|
59
|
-
|
|
60
|
-
monkeypatch.setenv("GIT_DIR", str(git_repository / "missing-git-dir"))
|
|
61
|
-
monkeypatch.setenv("GIT_WORK_TREE", str(git_repository / "missing-work-tree"))
|
|
62
|
-
monkeypatch.setenv("GIT_INDEX_FILE", str(git_repository / "missing-index"))
|
|
63
|
-
monkeypatch.setenv("GIT_COMMON_DIR", str(git_repository / "missing-common-dir"))
|
|
64
|
-
monkeypatch.setenv("GIT_PREFIX", "adversarial-prefix")
|
|
65
|
-
scrubbed_environment = refactor_guard._git_environment()
|
|
66
|
-
assert not any(each_name.startswith("GIT_") for each_name in scrubbed_environment)
|
|
67
|
-
all_added_lines = refactor_guard.get_git_diff_added_lines(str(source_path))
|
|
68
|
-
|
|
69
|
-
assert all_added_lines == {"staged_line = 1", "unstaged_line = 1"}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def test_duplicate_old_lines_require_duplicate_changed_occurrences(
|
|
73
|
-
git_repository: Path, monkeypatch: pytest.MonkeyPatch
|
|
74
|
-
) -> None:
|
|
75
|
-
source_path = git_repository / "module.py"
|
|
76
|
-
commit_file(git_repository, source_path, "pass\n")
|
|
77
|
-
old_function = "def calculate_total(amount: int) -> int:\n return amount\n return amount"
|
|
78
|
-
stage_file(
|
|
79
|
-
git_repository,
|
|
80
|
-
source_path,
|
|
81
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
82
|
-
)
|
|
83
|
-
renamed_function = old_function.replace("calculate_total", "compute_total")
|
|
84
|
-
monkeypatch.setattr(
|
|
85
|
-
refactor_guard,
|
|
86
|
-
"_get_added_line_occurrences",
|
|
87
|
-
lambda _file_path: ["def calculate_total(amount: int) -> int:", " return amount"],
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
assert refactor_guard.is_refactor_edit(old_function, renamed_function)
|
|
91
|
-
assert not refactor_guard.is_edit_within_changed_surface(str(source_path), old_function)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_ordinary_edit_is_not_a_refactor_candidate(git_repository: Path) -> None:
|
|
95
|
-
source_path = git_repository / "module.py"
|
|
96
|
-
commit_file(git_repository, source_path, "return_amount = 1\n")
|
|
97
|
-
|
|
98
|
-
assert not refactor_guard.is_refactor_eligible(
|
|
99
|
-
str(source_path), "return_amount = 1", "return_amount = 2"
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def test_new_file_is_not_a_refactor_candidate(git_repository: Path) -> None:
|
|
104
|
-
source_path = git_repository / "new_module.py"
|
|
105
|
-
source_path.write_text(
|
|
106
|
-
"def calculate_total(amount: int) -> int:\n return amount\n",
|
|
107
|
-
encoding="utf-8",
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
assert refactor_guard.is_new_file(str(source_path))
|
|
111
|
-
assert not refactor_guard.is_refactor_eligible(
|
|
112
|
-
str(source_path),
|
|
113
|
-
"def calculate_total(amount: int) -> int:\n return amount",
|
|
114
|
-
"def compute_total(amount: int) -> int:\n return amount",
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_hook_infrastructure_is_not_a_refactor_candidate(
|
|
119
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
120
|
-
) -> None:
|
|
121
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
122
|
-
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
123
|
-
hook_path = str(Path.home() / ".claude" / "settings.json")
|
|
124
|
-
|
|
125
|
-
assert refactor_guard.is_hook_infrastructure(hook_path)
|
|
126
|
-
assert not refactor_guard.is_refactor_eligible(
|
|
127
|
-
hook_path,
|
|
128
|
-
"def calculate_total(amount: int) -> int:\n return amount",
|
|
129
|
-
"def compute_total(amount: int) -> int:\n return amount",
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_changed_surface_below_half_is_false(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
134
|
-
monkeypatch.setattr(
|
|
135
|
-
refactor_guard,
|
|
136
|
-
"_get_added_line_occurrences",
|
|
137
|
-
lambda _file_path: ["changed line"],
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
assert not refactor_guard.is_edit_within_changed_surface(
|
|
141
|
-
"module.py", "changed line\noriginal line\noriginal line"
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def test_changed_surface_at_half_is_true(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
146
|
-
monkeypatch.setattr(
|
|
147
|
-
refactor_guard,
|
|
148
|
-
"_get_added_line_occurrences",
|
|
149
|
-
lambda _file_path: ["changed one", "changed two"],
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
assert refactor_guard.is_edit_within_changed_surface(
|
|
153
|
-
"module.py", "changed one\nchanged two\noriginal one\noriginal two"
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def test_changed_surface_above_half_is_true(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
158
|
-
monkeypatch.setattr(
|
|
159
|
-
refactor_guard,
|
|
160
|
-
"_get_added_line_occurrences",
|
|
161
|
-
lambda _file_path: ["changed one", "changed two", "changed three"],
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
assert refactor_guard.is_edit_within_changed_surface(
|
|
165
|
-
"module.py", "changed one\nchanged two\nchanged three\noriginal line"
|
|
166
|
-
)
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"""Constants for the changed-surface refactor advisory."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
GIT_EXECUTABLE_TOKEN: str = "git"
|
|
6
|
-
GIT_COMMAND_TIMEOUT_SECONDS: int = 5
|
|
7
|
-
MAXIMUM_REFACTOR_LINE_DELTA: int = 3
|
|
8
|
-
REFACTOR_LINE_DELTA_DIVISOR: int = 2
|
|
9
|
-
CHANGED_SURFACE_MATCH_RATIO: float = 0.5
|
|
10
|
-
ALL_PYTHON_KEYWORDS: frozenset[str] = frozenset(
|
|
11
|
-
{
|
|
12
|
-
"def",
|
|
13
|
-
"class",
|
|
14
|
-
"return",
|
|
15
|
-
"import",
|
|
16
|
-
"from",
|
|
17
|
-
"if",
|
|
18
|
-
"elif",
|
|
19
|
-
"else",
|
|
20
|
-
"for",
|
|
21
|
-
"while",
|
|
22
|
-
"try",
|
|
23
|
-
"except",
|
|
24
|
-
"finally",
|
|
25
|
-
"with",
|
|
26
|
-
"as",
|
|
27
|
-
"yield",
|
|
28
|
-
"raise",
|
|
29
|
-
"pass",
|
|
30
|
-
"break",
|
|
31
|
-
"continue",
|
|
32
|
-
"and",
|
|
33
|
-
"or",
|
|
34
|
-
"not",
|
|
35
|
-
"in",
|
|
36
|
-
"is",
|
|
37
|
-
"lambda",
|
|
38
|
-
"None",
|
|
39
|
-
"True",
|
|
40
|
-
"False",
|
|
41
|
-
"self",
|
|
42
|
-
"cls",
|
|
43
|
-
"async",
|
|
44
|
-
"await",
|
|
45
|
-
"global",
|
|
46
|
-
"nonlocal",
|
|
47
|
-
"assert",
|
|
48
|
-
"del",
|
|
49
|
-
"print",
|
|
50
|
-
"len",
|
|
51
|
-
"range",
|
|
52
|
-
"list",
|
|
53
|
-
"dict",
|
|
54
|
-
"set",
|
|
55
|
-
"str",
|
|
56
|
-
"int",
|
|
57
|
-
"float",
|
|
58
|
-
"bool",
|
|
59
|
-
"type",
|
|
60
|
-
"isinstance",
|
|
61
|
-
"hasattr",
|
|
62
|
-
"getattr",
|
|
63
|
-
"setattr",
|
|
64
|
-
"super",
|
|
65
|
-
"property",
|
|
66
|
-
"staticmethod",
|
|
67
|
-
"classmethod",
|
|
68
|
-
"abstractmethod",
|
|
69
|
-
"Optional",
|
|
70
|
-
"Union",
|
|
71
|
-
"List",
|
|
72
|
-
"Dict",
|
|
73
|
-
"Set",
|
|
74
|
-
"Tuple",
|
|
75
|
-
"Any",
|
|
76
|
-
}
|
|
77
|
-
)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"""Tests for refactor advisory constants."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
_HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
7
|
-
if str(_HOOKS_ROOT) not in sys.path:
|
|
8
|
-
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
9
|
-
|
|
10
|
-
from hooks_constants.refactor_guard_constants import (
|
|
11
|
-
ALL_PYTHON_KEYWORDS,
|
|
12
|
-
CHANGED_SURFACE_MATCH_RATIO,
|
|
13
|
-
MAXIMUM_REFACTOR_LINE_DELTA,
|
|
14
|
-
REFACTOR_LINE_DELTA_DIVISOR,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_refactor_threshold_constants_define_a_half_boundary() -> None:
|
|
19
|
-
assert CHANGED_SURFACE_MATCH_RATIO * REFACTOR_LINE_DELTA_DIVISOR == 1
|
|
20
|
-
assert MAXIMUM_REFACTOR_LINE_DELTA > REFACTOR_LINE_DELTA_DIVISOR
|
|
21
|
-
assert "return" in ALL_PYTHON_KEYWORDS
|