claude-dev-env 2.31.1 → 2.33.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/agents/clean-coder.md +8 -7
- package/.agents/agents/code-quality-agent.md +4 -2
- package/.agents/agents/pr-description-writer.md +8 -3
- package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
- package/.agents/agents/test_agent_frontmatter.py +0 -9
- package/.agents/agents/test_pr_description_writer_style.py +24 -1
- package/.agents/skills/e-simplify/SKILL.md +1 -15
- package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
- package/.agents/skills/pr-refinement/SKILL.md +2 -2
- package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
- package/.agents/skills/pr-title-description/SKILL.md +6 -2
- package/.agents/skills/review-tier/SKILL.md +1 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
- package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
- package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
- package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
- package/AGENTS.md +83 -0
- package/_shared/pr-loop/code-rules-gate.md +5 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
- package/audit-rubrics/audit-categories.json +2 -2
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
- package/audit-rubrics/prompts/category-e-dead-code.md +6 -6
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
- package/bin/install.test.mjs +11 -3
- package/docs/CODE_RULES.md +8 -9
- package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
- package/hooks/blocking/code_rules_comments.py +118 -59
- package/hooks/blocking/code_rules_duplicate_body.py +13 -7
- package/hooks/blocking/code_rules_enforcer.py +19 -7
- package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
- package/hooks/blocking/code_rules_type_escape.py +16 -23
- package/hooks/blocking/javascript_comment_scanner.py +99 -0
- package/hooks/blocking/shell_substitution_blocker.py +175 -8
- package/hooks/blocking/stop_dispatcher.py +6 -9
- package/hooks/blocking/tdd_enforcer.py +3 -1
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
- package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
- package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +8 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +7 -2
- package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
- package/hooks/blocking/test_state_description_blocker.py +56 -71
- package/hooks/blocking/test_tdd_enforcer.py +67 -1
- package/hooks/git-hooks/pull_request_handoff.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +8 -0
- package/hooks/hooks.json +2 -2
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
- package/hooks/hooks_constants/state_file_lock.py +76 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
- package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
- package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
- package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
- package/hooks/observability/session_file_edit_tracker.py +2 -55
- package/hooks/observability/test_failure_recorder.py +219 -0
- package/hooks/observability/test_test_failure_recorder.py +173 -0
- package/hooks/validators/comment_checks.py +2 -2
- package/hooks/validators/config/directory_exemption_constants.py +0 -9
- package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
- package/hooks/validators/fast_save_validators.py +0 -9
- package/hooks/validators/git_checks.py +0 -84
- package/hooks/validators/mypy_integration.py +10 -36
- package/hooks/validators/project_roots.py +58 -0
- package/hooks/validators/run_all_validators.py +89 -42
- package/hooks/validators/system_temporary_roots.py +1 -1
- package/hooks/validators/test_fast_save_validators.py +2 -8
- package/hooks/validators/test_git_checks.py +0 -249
- package/hooks/validators/test_run_all_validators.py +22 -0
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
- package/hooks/validators/test_run_all_validators_reporting.py +66 -0
- package/hooks/validators/test_todo_checks.py +1 -0
- package/hooks/validators/todo_checks.py +1 -1
- package/package.json +1 -1
- package/rules/git-workflow.md +0 -8
- package/rules/shell-invocation.md +1 -1
- package/rules/testing.md +4 -0
- package/scripts/check.ps1 +35 -1
- package/scripts/codex_compat_materializer.py +1 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +9 -5
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
- package/scripts/shared_tree_paths.py +59 -25
- package/scripts/test_shared_tree_paths.py +30 -0
- package/scripts/tests/test_rules.py +111 -0
- package/system-prompts/software-engineer.xml +6 -19
- package/.agents/skills-archived/split-pr/SKILL.md +0 -85
- package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
- package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
- package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
- package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
- package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
- package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
- package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
- package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
- package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
- package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
- package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
- package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
- package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
- package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
- package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
- package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
- package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
- package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
- package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
- package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
- package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
- package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
- package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
- package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
- package/.agents/skills-archived/split-pr/scripts/verify_plan.py +0 -139
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
"""Behavioral tests for the content_hash_store parts module.
|
|
1
|
+
"""Behavioral tests for the content_hash_store parts module.
|
|
2
|
+
|
|
3
|
+
Covers the recorded-test-failure evidence path: a genuinely failing test
|
|
4
|
+
command outranks a stale mtime, never satisfies a candidate it did not
|
|
5
|
+
name, expires after the freshness window, and applies only while the
|
|
6
|
+
candidate's content still matches what actually failed. Also covers the
|
|
7
|
+
private decision helpers by name, so a rename keeps its own paired test.
|
|
8
|
+
"""
|
|
2
9
|
|
|
3
10
|
import json
|
|
4
11
|
import os
|
|
@@ -141,3 +148,109 @@ def test_denies_when_no_candidate_file_exists(tmp_path: Path) -> None:
|
|
|
141
148
|
)
|
|
142
149
|
|
|
143
150
|
assert is_allowed is False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_recorded_failure_allows_a_stale_candidate(tmp_path: Path) -> None:
|
|
154
|
+
"""RED-evidence: a genuinely recorded failure outranks a stale mtime."""
|
|
155
|
+
candidate = _write_fresh_test_file(tmp_path)
|
|
156
|
+
stale_timestamp = time.time() - 700
|
|
157
|
+
os.utime(candidate, (stale_timestamp, stale_timestamp))
|
|
158
|
+
session_id, repository_root = "session-failure-1", str(tmp_path)
|
|
159
|
+
content_hash_store.record_test_command_failure(
|
|
160
|
+
[candidate], "pytest test_orders.py", 1, session_id, repository_root
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
is_allowed = content_hash_store.has_recorded_or_fresh_test(
|
|
164
|
+
[candidate], session_id, repository_root, 600
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
assert is_allowed is True
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_recorded_failure_stores_the_command_and_exit_status(tmp_path: Path) -> None:
|
|
171
|
+
candidate = _write_fresh_test_file(tmp_path)
|
|
172
|
+
session_id, repository_root = "session-failure-2", str(tmp_path)
|
|
173
|
+
|
|
174
|
+
content_hash_store.record_test_command_failure(
|
|
175
|
+
[candidate], "pytest test_orders.py -q", 1, session_id, repository_root
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
state_file = content_hash_store._state_file_path(session_id, repository_root)
|
|
179
|
+
state = json.loads(state_file.read_text())
|
|
180
|
+
entry = state[content_hash_store._state_key_for(candidate)]
|
|
181
|
+
assert entry[content_hash_store.STORED_FAILURE_COMMAND_KEY] == "pytest test_orders.py -q"
|
|
182
|
+
assert entry[content_hash_store.STORED_FAILURE_EXIT_STATUS_KEY] == 1
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_recorded_failure_does_not_satisfy_a_different_candidate(tmp_path: Path) -> None:
|
|
186
|
+
"""Anti-bypass: a failure recorded for one test file must not unlock a sibling."""
|
|
187
|
+
failing_candidate = tmp_path / "test_bar.py"
|
|
188
|
+
failing_candidate.write_text("def test_bar(): pass\n")
|
|
189
|
+
unrelated_candidate = tmp_path / "test_foo.py"
|
|
190
|
+
unrelated_candidate.write_text("def test_foo(): pass\n")
|
|
191
|
+
stale_timestamp = time.time() - 700
|
|
192
|
+
os.utime(unrelated_candidate, (stale_timestamp, stale_timestamp))
|
|
193
|
+
session_id, repository_root = "session-failure-3", str(tmp_path)
|
|
194
|
+
content_hash_store.record_test_command_failure(
|
|
195
|
+
[failing_candidate], "pytest test_bar.py", 1, session_id, repository_root
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
is_allowed = content_hash_store.has_recorded_or_fresh_test(
|
|
199
|
+
[unrelated_candidate], session_id, repository_root, 600
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
assert is_allowed is False
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_recorded_failure_expires_after_the_freshness_window(tmp_path: Path) -> None:
|
|
206
|
+
candidate = _write_fresh_test_file(tmp_path)
|
|
207
|
+
stale_timestamp = time.time() - 700
|
|
208
|
+
os.utime(candidate, (stale_timestamp, stale_timestamp))
|
|
209
|
+
session_id, repository_root = "session-failure-4", str(tmp_path)
|
|
210
|
+
state_file = content_hash_store._state_file_path(session_id, repository_root)
|
|
211
|
+
candidate_key = content_hash_store._state_key_for(candidate)
|
|
212
|
+
stale_failed_at = time.time() - 700
|
|
213
|
+
state_file.write_text(
|
|
214
|
+
json.dumps(
|
|
215
|
+
{
|
|
216
|
+
candidate_key: {
|
|
217
|
+
content_hash_store.STORED_FAILURE_HASH_KEY: content_hash_store._content_hash(
|
|
218
|
+
candidate.read_text()
|
|
219
|
+
),
|
|
220
|
+
content_hash_store.STORED_FAILED_AT_KEY: stale_failed_at,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
is_allowed = content_hash_store.has_recorded_or_fresh_test(
|
|
227
|
+
[candidate], session_id, repository_root, 600
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
assert is_allowed is False
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_recorded_failure_does_not_apply_once_content_no_longer_matches(
|
|
234
|
+
tmp_path: Path,
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Anti-bypass: editing the test after a recorded RED clears that RED as evidence.
|
|
237
|
+
|
|
238
|
+
A bare failure-only entry (no prior content-hash tracking) must not fall
|
|
239
|
+
through to the unconditional hash-differs allow either -- it still needs a
|
|
240
|
+
real qualifying first sight (fresh mtime plus a real git-visible edit).
|
|
241
|
+
"""
|
|
242
|
+
candidate = tmp_path / "test_orders.py"
|
|
243
|
+
candidate.write_text("def test_fulfill(): pass\n")
|
|
244
|
+
session_id, repository_root = "session-failure-5", str(tmp_path)
|
|
245
|
+
content_hash_store.record_test_command_failure(
|
|
246
|
+
[candidate], "pytest test_orders.py", 1, session_id, repository_root
|
|
247
|
+
)
|
|
248
|
+
candidate.write_text("def test_fulfill(): assert True\n")
|
|
249
|
+
stale_timestamp = time.time() - 700
|
|
250
|
+
os.utime(candidate, (stale_timestamp, stale_timestamp))
|
|
251
|
+
|
|
252
|
+
is_allowed = content_hash_store.has_recorded_or_fresh_test(
|
|
253
|
+
[candidate], session_id, repository_root, 600
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
assert is_allowed is False
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Behavior tests for the Bash PostToolUse dispatcher.
|
|
2
|
+
|
|
3
|
+
One interpreter start runs every hosted PostToolUse hook a Bash call fires.
|
|
4
|
+
The unit tests pin selection, call order, and crash isolation with a fake
|
|
5
|
+
runner; the end-to-end test drives the real hosted hooks through a real
|
|
6
|
+
payload and confirms the recorder's own real side effect lands.
|
|
7
|
+
``hooks/conftest.py`` already puts the hooks and blocking directories on
|
|
8
|
+
sys.path for test collection, so this file needs no bootstrap of its own.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from bash_post_call_dispatcher import dispatch, main, select_applicable_entries
|
|
19
|
+
from hooks_constants.bash_pre_tool_use_dispatcher_constants import BASH_TOOL_NAME
|
|
20
|
+
from hooks_constants.hosted_hook_runner import HostedHookRun
|
|
21
|
+
from tdd_enforcer_parts import content_hash_store
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_select_applicable_entries_returns_both_hosted_hooks_for_bash() -> None:
|
|
25
|
+
all_entries = select_applicable_entries(BASH_TOOL_NAME)
|
|
26
|
+
all_script_paths = [each_entry.script_relative_path for each_entry in all_entries]
|
|
27
|
+
assert all_script_paths == [
|
|
28
|
+
"blocking/gh_pr_author_restore.py",
|
|
29
|
+
"observability/test_failure_recorder.py",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_select_applicable_entries_returns_none_for_a_write_tool() -> None:
|
|
34
|
+
assert select_applicable_entries("Write") == []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_dispatch_runs_every_hosted_hook_in_roster_order(
|
|
38
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
39
|
+
) -> None:
|
|
40
|
+
all_call_paths: list[str] = []
|
|
41
|
+
|
|
42
|
+
def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
|
|
43
|
+
del payload_text
|
|
44
|
+
all_call_paths.append(Path(script_path).name)
|
|
45
|
+
return HostedHookRun(captured_stdout="", did_crash=False)
|
|
46
|
+
|
|
47
|
+
monkeypatch.setattr("bash_post_call_dispatcher.run_hook_capturing_output", _fake_run_hook)
|
|
48
|
+
|
|
49
|
+
dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
|
|
50
|
+
|
|
51
|
+
assert all_call_paths == ["gh_pr_author_restore.py", "test_failure_recorder.py"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_dispatch_runs_the_later_hook_after_an_earlier_one_crashes(
|
|
55
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
56
|
+
) -> None:
|
|
57
|
+
all_call_paths: list[str] = []
|
|
58
|
+
|
|
59
|
+
def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
|
|
60
|
+
del payload_text
|
|
61
|
+
script_name = Path(script_path).name
|
|
62
|
+
all_call_paths.append(script_name)
|
|
63
|
+
if script_name == "gh_pr_author_restore.py":
|
|
64
|
+
return HostedHookRun(captured_stdout="", did_crash=True)
|
|
65
|
+
return HostedHookRun(captured_stdout="", did_crash=False)
|
|
66
|
+
|
|
67
|
+
monkeypatch.setattr("bash_post_call_dispatcher.run_hook_capturing_output", _fake_run_hook)
|
|
68
|
+
|
|
69
|
+
dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
|
|
70
|
+
|
|
71
|
+
assert all_call_paths == ["gh_pr_author_restore.py", "test_failure_recorder.py"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_main_exits_zero_and_writes_nothing_to_stdout(
|
|
75
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
76
|
+
) -> None:
|
|
77
|
+
payload = json.dumps({"tool_name": "Write", "tool_input": {}})
|
|
78
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(payload))
|
|
79
|
+
|
|
80
|
+
with pytest.raises(SystemExit) as raised_exit:
|
|
81
|
+
main()
|
|
82
|
+
|
|
83
|
+
assert int(raised_exit.value.code or 0) == 0
|
|
84
|
+
assert capsys.readouterr().out == ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_end_to_end_real_hosted_hooks_record_a_real_pytest_failure(tmp_path: Path) -> None:
|
|
88
|
+
"""Drives the real, unfaked roster: both scripts run in one process."""
|
|
89
|
+
test_file = tmp_path / "test_orders.py"
|
|
90
|
+
test_file.write_text("def test_fulfill(): assert False\n")
|
|
91
|
+
session_id = "post-dispatcher-session"
|
|
92
|
+
failing_output = (
|
|
93
|
+
"Error: Exit code 1\n"
|
|
94
|
+
"FAILED test_orders.py::test_fulfill - AssertionError\n"
|
|
95
|
+
"1 failed in 0.02s"
|
|
96
|
+
)
|
|
97
|
+
payload = json.dumps(
|
|
98
|
+
{
|
|
99
|
+
"session_id": session_id,
|
|
100
|
+
"cwd": str(tmp_path),
|
|
101
|
+
"tool_name": "Bash",
|
|
102
|
+
"tool_input": {"command": "pytest test_orders.py"},
|
|
103
|
+
"tool_response": failing_output,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
dispatch(payload, BASH_TOOL_NAME)
|
|
108
|
+
|
|
109
|
+
state_file = content_hash_store._state_file_path(session_id, str(tmp_path))
|
|
110
|
+
state = json.loads(state_file.read_text())
|
|
111
|
+
entry = state[content_hash_store._state_key_for(test_file)]
|
|
112
|
+
assert entry[content_hash_store.STORED_FAILURE_EXIT_STATUS_KEY] == 1
|
|
@@ -56,6 +56,7 @@ def test_should_flag_bare_type_ignore() -> None:
|
|
|
56
56
|
source = "x = 1 # type: ignore\n"
|
|
57
57
|
issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
|
|
58
58
|
assert any("type: ignore" in issue for issue in issues)
|
|
59
|
+
assert any("remove it and use a typed boundary or real type" in issue for issue in issues)
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
def test_should_flag_coded_type_ignore_without_justification() -> None:
|
|
@@ -64,16 +65,35 @@ def test_should_flag_coded_type_ignore_without_justification() -> None:
|
|
|
64
65
|
assert any("type: ignore" in issue for issue in issues)
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
def
|
|
68
|
+
def test_should_flag_justified_type_ignore() -> None:
|
|
68
69
|
source = "x = 1 # type: ignore[misc] # stubs missing in foo library\n"
|
|
69
70
|
issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
|
|
70
|
-
assert
|
|
71
|
+
assert any("type: ignore" in issue for issue in issues)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_should_flag_justified_type_ignore_in_test_files() -> None:
|
|
75
|
+
source = "x = 1 # type: ignore[misc] # stubs missing in foo library\n"
|
|
76
|
+
issues = check_type_escape_hatches(source, TEST_FILE_PATH)
|
|
77
|
+
assert any("type: ignore" in issue for issue in issues)
|
|
71
78
|
|
|
72
79
|
|
|
73
|
-
def
|
|
80
|
+
def test_should_flag_type_ignore_in_test_files() -> None:
|
|
74
81
|
source = "def foo(x: Any) -> Any:\n y: Any = 1 # type: ignore\n return y\n"
|
|
75
82
|
issues = check_type_escape_hatches(source, TEST_FILE_PATH)
|
|
76
|
-
assert
|
|
83
|
+
assert any("type: ignore" in issue for issue in issues)
|
|
84
|
+
assert not any("Any annotation" in issue for issue in issues)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_edit_lane_does_not_report_type_ignore_comment_policy() -> None:
|
|
88
|
+
source = "total = 1 # type: ignore[misc]\n"
|
|
89
|
+
issues = hook_module.validate_content_for_edit_lane(source, TEST_FILE_PATH)
|
|
90
|
+
assert not any("type: ignore" in issue for issue in issues)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_edit_lane_does_not_report_production_type_ignore_comment_policy() -> None:
|
|
94
|
+
source = "total = 1 # type: ignore[misc]\n"
|
|
95
|
+
issues = hook_module.validate_content_for_edit_lane(source, PRODUCTION_FILE_PATH)
|
|
96
|
+
assert not any("type: ignore" in issue for issue in issues)
|
|
77
97
|
|
|
78
98
|
|
|
79
99
|
def test_should_flag_any_on_positional_only_parameter() -> None:
|
|
@@ -68,7 +68,6 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
|
|
|
68
68
|
{
|
|
69
69
|
"check_boolean_naming",
|
|
70
70
|
"check_collection_prefix",
|
|
71
|
-
"check_comment_changes",
|
|
72
71
|
"check_constant_equality_tests",
|
|
73
72
|
"check_constants_outside_config",
|
|
74
73
|
"check_existence_check_tests",
|
|
@@ -14,6 +14,7 @@ a string still flag.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import sys
|
|
17
|
+
import importlib
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from types import SimpleNamespace
|
|
19
20
|
|
|
@@ -31,12 +32,12 @@ from code_rules_comments import ( # noqa: E402
|
|
|
31
32
|
check_comments_python,
|
|
32
33
|
extract_comment_texts,
|
|
33
34
|
)
|
|
34
|
-
|
|
35
35
|
code_rules_enforcer = SimpleNamespace(
|
|
36
36
|
check_comment_changes=check_comment_changes,
|
|
37
37
|
check_comments_python=check_comments_python,
|
|
38
38
|
extract_comment_texts=extract_comment_texts,
|
|
39
39
|
)
|
|
40
|
+
code_rules_enforcer_module = importlib.import_module("code_rules_enforcer")
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def test_python_check_should_not_flag_hex_color_literal() -> None:
|
|
@@ -121,7 +122,7 @@ def test_check_comment_changes_skips_removal_when_old_python_un_parseable() -> N
|
|
|
121
122
|
assert issues == []
|
|
122
123
|
|
|
123
124
|
|
|
124
|
-
def
|
|
125
|
+
def test_check_comment_changes_allows_comment_removal_without_advisory(
|
|
125
126
|
capsys: pytest.CaptureFixture[str],
|
|
126
127
|
) -> None:
|
|
127
128
|
old_content = 'x = 1 # existing comment\n'
|
|
@@ -129,7 +130,7 @@ def test_check_comment_changes_advises_without_blocking_on_removal_on_parseable_
|
|
|
129
130
|
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
130
131
|
assert not any("Existing comment removed" in each_issue for each_issue in issues)
|
|
131
132
|
captured = capsys.readouterr()
|
|
132
|
-
assert
|
|
133
|
+
assert captured.err == ""
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
def test_python_check_should_exempt_directive_without_space_after_hash() -> None:
|
|
@@ -194,3 +195,189 @@ def test_extract_should_classify_inline_shebang_lookalike_as_inline_comment() ->
|
|
|
194
195
|
inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
|
|
195
196
|
assert inline != set()
|
|
196
197
|
assert standalone == set()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_full_gate_blocks_python_inline_comment_in_test_file() -> None:
|
|
201
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
202
|
+
content="total = 1 # added\n",
|
|
203
|
+
old_content="total = 1\n",
|
|
204
|
+
effective_content="total = 1 # added\n",
|
|
205
|
+
file_path="test_totals.py",
|
|
206
|
+
all_changed_lines=None,
|
|
207
|
+
defer_scope_to_caller=False,
|
|
208
|
+
sibling_directory=None,
|
|
209
|
+
phase="edit",
|
|
210
|
+
)
|
|
211
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
212
|
+
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_full_gate_blocks_javascript_inline_comment_in_test_file() -> None:
|
|
216
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
217
|
+
content="const total = 1; // added\n",
|
|
218
|
+
old_content="const total = 1;\n",
|
|
219
|
+
effective_content="const total = 1; // added\n",
|
|
220
|
+
file_path="totals.test.js",
|
|
221
|
+
all_changed_lines=None,
|
|
222
|
+
defer_scope_to_caller=False,
|
|
223
|
+
sibling_directory=None,
|
|
224
|
+
phase="edit",
|
|
225
|
+
)
|
|
226
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
227
|
+
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_full_gate_blocks_javascript_inline_block_comment_in_test_file() -> None:
|
|
231
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
232
|
+
content="const total = 1; /* added */\n",
|
|
233
|
+
old_content="const total = 1;\n",
|
|
234
|
+
effective_content="const total = 1; /* added */\n",
|
|
235
|
+
file_path="totals.test.js",
|
|
236
|
+
all_changed_lines=None,
|
|
237
|
+
defer_scope_to_caller=False,
|
|
238
|
+
sibling_directory=None,
|
|
239
|
+
phase="edit",
|
|
240
|
+
)
|
|
241
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
242
|
+
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_full_gate_blocks_javascript_directive_comment_in_test_file() -> None:
|
|
246
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
247
|
+
content="const total = 1; // eslint-disable-next-line\n",
|
|
248
|
+
old_content="const total = 1;\n",
|
|
249
|
+
effective_content="const total = 1; // eslint-disable-next-line\n",
|
|
250
|
+
file_path="totals.test.js",
|
|
251
|
+
all_changed_lines=None,
|
|
252
|
+
defer_scope_to_caller=False,
|
|
253
|
+
sibling_directory=None,
|
|
254
|
+
phase="edit",
|
|
255
|
+
)
|
|
256
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
257
|
+
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_full_gate_blocks_python_directive_comment_in_test_file() -> None:
|
|
261
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
262
|
+
content="total = 1 # noqa\n",
|
|
263
|
+
old_content="total = 1\n",
|
|
264
|
+
effective_content="total = 1 # noqa\n",
|
|
265
|
+
file_path="test_totals.py",
|
|
266
|
+
all_changed_lines=None,
|
|
267
|
+
defer_scope_to_caller=False,
|
|
268
|
+
sibling_directory=None,
|
|
269
|
+
phase="edit",
|
|
270
|
+
)
|
|
271
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
272
|
+
assert any("comment added" in each_issue.lower() for each_issue in issues)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@pytest.mark.parametrize(
|
|
276
|
+
"each_marker", ("# TODO #999: track", "# FIXME #999: track", "# HACK #999: track", "# XXX #999: track", "# type: ignore[misc]")
|
|
277
|
+
)
|
|
278
|
+
def test_full_gate_blocks_changed_marker_comments_in_test_file(each_marker: str) -> None:
|
|
279
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
280
|
+
content=f"total = 1 {each_marker}\n",
|
|
281
|
+
old_content="total = 1\n",
|
|
282
|
+
effective_content=f"total = 1 {each_marker}\n",
|
|
283
|
+
file_path="test_totals.py",
|
|
284
|
+
all_changed_lines=None,
|
|
285
|
+
defer_scope_to_caller=False,
|
|
286
|
+
sibling_directory=None,
|
|
287
|
+
phase="edit",
|
|
288
|
+
)
|
|
289
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
290
|
+
assert any("comment added" in each_issue.lower() for each_issue in issues)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_full_gate_blocks_standalone_comment_in_test_file() -> None:
|
|
294
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
295
|
+
content="# added\ntotal = 1\n",
|
|
296
|
+
old_content="total = 1\n",
|
|
297
|
+
effective_content="# added\ntotal = 1\n",
|
|
298
|
+
file_path="test_totals.py",
|
|
299
|
+
all_changed_lines=None,
|
|
300
|
+
defer_scope_to_caller=False,
|
|
301
|
+
sibling_directory=None,
|
|
302
|
+
phase="edit",
|
|
303
|
+
)
|
|
304
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
305
|
+
assert any("Standalone comment added" in each_issue for each_issue in issues)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_full_gate_preserves_shebang_and_docstring_in_test_file() -> None:
|
|
309
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
310
|
+
content='#!/usr/bin/env python3\n"""Describe totals."""\ntotal = 1\n',
|
|
311
|
+
old_content="#!/usr/bin/env python3\n",
|
|
312
|
+
effective_content='#!/usr/bin/env python3\n"""Describe totals."""\ntotal = 1\n',
|
|
313
|
+
file_path="test_totals.py",
|
|
314
|
+
all_changed_lines=None,
|
|
315
|
+
defer_scope_to_caller=False,
|
|
316
|
+
sibling_directory=None,
|
|
317
|
+
phase="edit",
|
|
318
|
+
)
|
|
319
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
320
|
+
assert not any("comment added" in each_issue.lower() for each_issue in issues)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_full_gate_allows_existing_comment_removal_without_advisory(
|
|
324
|
+
capsys: pytest.CaptureFixture[str],
|
|
325
|
+
) -> None:
|
|
326
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
327
|
+
content="total = 1\n",
|
|
328
|
+
old_content="total = 1 # existing\n",
|
|
329
|
+
effective_content="total = 1\n",
|
|
330
|
+
file_path="test_totals.py",
|
|
331
|
+
all_changed_lines=None,
|
|
332
|
+
defer_scope_to_caller=False,
|
|
333
|
+
sibling_directory=None,
|
|
334
|
+
phase="edit",
|
|
335
|
+
)
|
|
336
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
337
|
+
assert not any("Existing comment removed" in each_issue for each_issue in issues)
|
|
338
|
+
assert capsys.readouterr().err == ""
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@pytest.mark.parametrize(
|
|
342
|
+
("file_path", "old_content", "new_content"),
|
|
343
|
+
(
|
|
344
|
+
("totals.py", "total = 1\n", "total = 2 # TODO #999: track\n"),
|
|
345
|
+
("test_totals.py", "total = 1\n", "total = 2 # TODO #999: track\n"),
|
|
346
|
+
("totals.js", "const total = 1;\n", "const total = 2; // eslint-disable-next-line\n"),
|
|
347
|
+
("totals.test.js", "const total = 1;\n", "const total = 2; // eslint-disable-next-line\n"),
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
def test_edit_lane_does_not_report_comment_policy(
|
|
351
|
+
file_path: str, old_content: str, new_content: str
|
|
352
|
+
) -> None:
|
|
353
|
+
issues = code_rules_enforcer_module.validate_content_for_edit_lane(
|
|
354
|
+
new_content, file_path, old_content
|
|
355
|
+
)
|
|
356
|
+
assert not any("comment" in each_issue.lower() for each_issue in issues)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@pytest.mark.parametrize(
|
|
360
|
+
("file_path", "old_content", "new_content"),
|
|
361
|
+
(
|
|
362
|
+
("totals.py", "total = 1 # TODO #999: track\n", "total = 2 # TODO #999: track\n"),
|
|
363
|
+
("totals.py", "# TODO #999: track\ntotal = 1\n", "# TODO #999: track\ntotal = 2\n"),
|
|
364
|
+
("totals.py", "total = 1 # noqa\n", "total = 2 # noqa\n"),
|
|
365
|
+
(
|
|
366
|
+
"totals.js",
|
|
367
|
+
"const total = 1; // eslint-disable-next-line\n",
|
|
368
|
+
"const total = 2; // eslint-disable-next-line\n",
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
def test_check_comment_changes_blocks_retained_comments_on_changed_code(
|
|
373
|
+
file_path: str, old_content: str, new_content: str
|
|
374
|
+
) -> None:
|
|
375
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, file_path)
|
|
376
|
+
assert any("retained on changed code" in each_issue for each_issue in issues)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_check_comment_changes_accepts_distant_untouched_comment() -> None:
|
|
380
|
+
old_content = "# TODO #999: distant note\n\ntotal = 1\n"
|
|
381
|
+
new_content = "# TODO #999: distant note\n\ntotal = 2\n"
|
|
382
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "totals.py")
|
|
383
|
+
assert not any("retained on changed code" in each_issue for each_issue in issues)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Tests for JavaScript comment extraction and diff occurrence reporting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
+
|
|
16
|
+
check_comment_changes = importlib.import_module(
|
|
17
|
+
"code_rules_comments"
|
|
18
|
+
).check_comment_changes
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_check_comment_changes_ignores_javascript_url_string() -> None:
|
|
22
|
+
issues = check_comment_changes(
|
|
23
|
+
"", 'const docsUrl = "https://example.com/guide";\n', "docs.js"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert issues == []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_check_comment_changes_reports_javascript_comment_line() -> None:
|
|
30
|
+
issues = check_comment_changes(
|
|
31
|
+
"", 'const total = 1;\nconst next = 2; // added\n', "totals.js"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert any("Line 2: Inline comment added" in each_issue for each_issue in issues)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_check_comment_changes_reports_duplicate_identical_comment_occurrence() -> None:
|
|
38
|
+
issues = check_comment_changes(
|
|
39
|
+
"total = 1 # repeated\n",
|
|
40
|
+
"total = 1 # repeated\ntotal = 2 # repeated\n",
|
|
41
|
+
"totals.py",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert any("Line 2: Inline comment added" in each_issue for each_issue in issues)
|
|
@@ -74,7 +74,6 @@ def test_should_grandfather_unchanged_join_separator_in_wide_edit() -> None:
|
|
|
74
74
|
)
|
|
75
75
|
proposed_fragment = (
|
|
76
76
|
"def render(all_paths: list) -> str:\n"
|
|
77
|
-
" # keep the join separator the same\n"
|
|
78
77
|
" return ', '.join(str(each_path) for each_path in all_paths)\n"
|
|
79
78
|
)
|
|
80
79
|
issues = code_rules_enforcer.validate_content_for_full_gate(
|
|
@@ -648,8 +648,11 @@ def test_should_read_no_function_when_the_file_holds_no_qualifying_helper() -> N
|
|
|
648
648
|
pins the verdict such a file gets.
|
|
649
649
|
"""
|
|
650
650
|
all_module_functions = ast.parse(NO_INLINE_DUPLICATE_SOURCE).body
|
|
651
|
+
dump_by_statement: dict[ast.stmt, str] = {}
|
|
651
652
|
all_windows = [
|
|
652
|
-
_duplicate_body_module._helper_match_window_dumps(
|
|
653
|
+
_duplicate_body_module._helper_match_window_dumps(
|
|
654
|
+
each_function, dump_by_statement
|
|
655
|
+
)
|
|
653
656
|
for each_function in all_module_functions
|
|
654
657
|
if isinstance(each_function, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
655
658
|
]
|
|
@@ -657,6 +660,10 @@ def test_should_read_no_function_when_the_file_holds_no_qualifying_helper() -> N
|
|
|
657
660
|
"this source must hold no qualifying helper for the skip to be exercised, "
|
|
658
661
|
f"got windows: {all_windows}"
|
|
659
662
|
)
|
|
663
|
+
assert dump_by_statement == {}, (
|
|
664
|
+
"a source with no qualifying helper must take no statement dumps, "
|
|
665
|
+
f"got {len(dump_by_statement)} memo entries"
|
|
666
|
+
)
|
|
660
667
|
issues = check_same_file_inline_duplicate_body(
|
|
661
668
|
NO_INLINE_DUPLICATE_SOURCE, "module.py"
|
|
662
669
|
)
|
|
@@ -46,8 +46,13 @@ import state_description_blocker # noqa: E402, I001
|
|
|
46
46
|
_STATE_DESCRIPTION_SCRIPT = str(_BLOCKING_DIR / "state_description_blocker.py")
|
|
47
47
|
_DISPATCHER_SCRIPT = str(_BLOCKING_DIR / "pre_tool_use_dispatcher.py")
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
from code_rules_enforcer_test_support import ( # noqa: E402
|
|
50
|
+
repository_file_path,
|
|
51
|
+
repository_root,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_REPOSITORY_ROOT = str(repository_root())
|
|
55
|
+
_MARKDOWN_PATH = repository_file_path("docs/native_equivalence_probe.md")
|
|
51
56
|
_STATE_DESCRIPTION_ALLOW_CONTENT = "# Guide\n\nThe API uses port 8080.\n"
|
|
52
57
|
_STATE_DESCRIPTION_DENY_CONTENT = "# Guide\n\nPreviously the system used port 8080.\n"
|
|
53
58
|
|