claude-dev-env 8.4.3 → 8.6.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.
@@ -0,0 +1,270 @@
1
+ """Behavior tests for the follow-up command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from dev_env_scripts_constants.followup_constants import (
11
+ BACKLOG_EXCEEDED_EXIT_CODE,
12
+ FOLLOWUP_BACKLOG_THRESHOLD,
13
+ )
14
+ from followup_cli import main
15
+
16
+ _hooks_directory = str(Path(__file__).resolve().parents[1] / "hooks")
17
+ if _hooks_directory not in sys.path:
18
+ sys.path.insert(0, _hooks_directory)
19
+
20
+ from followup_ledger import (
21
+ FollowupFinding,
22
+ all_recorded_findings,
23
+ record_followup_finding,
24
+ )
25
+
26
+
27
+ def run_command(all_arguments: list[str]) -> tuple[int, str]:
28
+ output_stream = io.StringIO()
29
+ exit_code = main(all_arguments, stdout=output_stream)
30
+ return exit_code, output_stream.getvalue()
31
+
32
+
33
+ def write_lint_report(
34
+ report_path: Path, all_diagnostics: list[dict[str, object]]
35
+ ) -> None:
36
+ report_path.write_text(
37
+ json.dumps(
38
+ {"schema_version": 1, "diagnostics": all_diagnostics, "failed_rules": []}
39
+ ),
40
+ encoding="utf-8",
41
+ )
42
+
43
+
44
+ def test_list_reports_nothing_outstanding_for_an_empty_ledger(tmp_path: Path) -> None:
45
+ exit_code, output_text = run_command(["list", "--repository-root", str(tmp_path)])
46
+
47
+ assert exit_code == 0
48
+ assert "no follow-ups recorded" in output_text
49
+
50
+
51
+ def test_list_names_each_recorded_finding(tmp_path: Path) -> None:
52
+ record_followup_finding(
53
+ tmp_path, FollowupFinding("instruction-git-mode", "CLAUDE.md", "wrong mode")
54
+ )
55
+
56
+ exit_code, output_text = run_command(["list", "--repository-root", str(tmp_path)])
57
+
58
+ assert exit_code == 0
59
+ assert "instruction-git-mode" in output_text
60
+ assert "CLAUDE.md" in output_text
61
+
62
+
63
+ def test_ingest_records_every_located_diagnostic(tmp_path: Path) -> None:
64
+ report_path = tmp_path / "lint.json"
65
+ write_lint_report(
66
+ report_path,
67
+ [
68
+ {
69
+ "rule_id": "line-length",
70
+ "severity": "error",
71
+ "message": "line runs past the limit",
72
+ "location": {"path": "src/app.py", "start_line": 4, "start_column": 1},
73
+ }
74
+ ],
75
+ )
76
+
77
+ exit_code, _ = run_command(
78
+ ["ingest", str(report_path), "--repository-root", str(tmp_path)]
79
+ )
80
+
81
+ assert exit_code == 0
82
+ assert all_recorded_findings(tmp_path) == (
83
+ FollowupFinding(
84
+ "line-length", "src/app.py", "line runs past the limit", "line-length"
85
+ ),
86
+ )
87
+
88
+
89
+ def test_ingest_records_a_diagnostic_without_a_location(tmp_path: Path) -> None:
90
+ report_path = tmp_path / "lint.json"
91
+ write_lint_report(
92
+ report_path,
93
+ [
94
+ {
95
+ "rule_id": "inventory-drift",
96
+ "severity": "error",
97
+ "message": "inventory row missing",
98
+ "location": None,
99
+ }
100
+ ],
101
+ )
102
+
103
+ run_command(["ingest", str(report_path), "--repository-root", str(tmp_path)])
104
+
105
+ assert all_recorded_findings(tmp_path) == (
106
+ FollowupFinding(
107
+ "inventory-drift", "", "inventory row missing", "inventory-drift"
108
+ ),
109
+ )
110
+
111
+
112
+ def test_ingest_reports_an_unreadable_report(tmp_path: Path) -> None:
113
+ exit_code, output_text = run_command(
114
+ ["ingest", str(tmp_path / "absent.json"), "--repository-root", str(tmp_path)]
115
+ )
116
+
117
+ assert exit_code == 2
118
+ assert "absent.json" in output_text
119
+
120
+
121
+ def test_brief_names_every_recorded_finding_and_the_repository(tmp_path: Path) -> None:
122
+ record_followup_finding(
123
+ tmp_path, FollowupFinding("instruction-git-mode", "CLAUDE.md", "wrong mode")
124
+ )
125
+
126
+ exit_code, output_text = run_command(["brief", "--repository-root", str(tmp_path)])
127
+
128
+ assert exit_code == 0
129
+ assert "instruction-git-mode" in output_text
130
+ assert "wrong mode" in output_text
131
+ assert "draft pull request" in output_text
132
+
133
+
134
+ def test_brief_asks_for_no_work_on_an_empty_ledger(tmp_path: Path) -> None:
135
+ exit_code, output_text = run_command(["brief", "--repository-root", str(tmp_path)])
136
+
137
+ assert exit_code == 0
138
+ assert "no follow-ups recorded" in output_text
139
+
140
+
141
+ def test_clear_empties_the_ledger(tmp_path: Path) -> None:
142
+ record_followup_finding(
143
+ tmp_path, FollowupFinding("instruction-git-mode", "CLAUDE.md", "wrong mode")
144
+ )
145
+
146
+ exit_code, _ = run_command(["clear", "--repository-root", str(tmp_path)])
147
+
148
+ assert exit_code == 0
149
+ assert all_recorded_findings(tmp_path) == ()
150
+
151
+
152
+ def test_clear_on_an_empty_ledger_succeeds(tmp_path: Path) -> None:
153
+ exit_code, _ = run_command(["clear", "--repository-root", str(tmp_path)])
154
+
155
+ assert exit_code == 0
156
+
157
+
158
+ def test_an_unknown_command_reports_usage(tmp_path: Path) -> None:
159
+ exit_code, output_text = run_command(
160
+ ["stampede", "--repository-root", str(tmp_path)]
161
+ )
162
+
163
+ assert exit_code == 2
164
+ assert "Usage" in output_text
165
+
166
+
167
+ def test_ingest_carries_the_check_identifier_a_diagnostic_names(
168
+ tmp_path: Path,
169
+ ) -> None:
170
+ report_path = tmp_path / "lint.json"
171
+ write_lint_report(
172
+ report_path,
173
+ [
174
+ {
175
+ "rule_id": "code-rules",
176
+ "check_id": "code-rules/constant-outside-config",
177
+ "severity": "error",
178
+ "message": "Line 3: Constant TIMEOUT - move to config/",
179
+ "location": {"path": "scripts/run.py", "start_line": 3},
180
+ }
181
+ ],
182
+ )
183
+
184
+ run_command(["ingest", str(report_path), "--repository-root", str(tmp_path)])
185
+
186
+ recorded_finding = all_recorded_findings(tmp_path)[0]
187
+ assert recorded_finding.check_id == "code-rules/constant-outside-config"
188
+ assert recorded_finding.severity == "smell"
189
+
190
+
191
+ def test_ingest_records_the_revision_the_repository_has_checked_out(
192
+ tmp_path: Path,
193
+ ) -> None:
194
+ (tmp_path / ".git").mkdir()
195
+ (tmp_path / ".git" / "HEAD").write_text("f00dcafe\n", encoding="utf-8")
196
+ report_path = tmp_path / "lint.json"
197
+ write_lint_report(
198
+ report_path,
199
+ [
200
+ {
201
+ "rule_id": "code-rules",
202
+ "check_id": "code-rules/function-length",
203
+ "message": "Line 9: Function 'run' is 74 lines",
204
+ "location": {"path": "scripts/run.py", "start_line": 9},
205
+ }
206
+ ],
207
+ )
208
+
209
+ run_command(["ingest", str(report_path), "--repository-root", str(tmp_path)])
210
+
211
+ assert all_recorded_findings(tmp_path)[0].origin_commit == "f00dcafe"
212
+
213
+
214
+ def test_count_reports_an_empty_backlog(tmp_path: Path) -> None:
215
+ exit_code, output_text = run_command(["count", "--repository-root", str(tmp_path)])
216
+
217
+ assert exit_code == 0
218
+ assert "0" in output_text
219
+
220
+
221
+ def test_count_passes_while_the_backlog_sits_under_the_threshold(
222
+ tmp_path: Path,
223
+ ) -> None:
224
+ record_followup_finding(
225
+ tmp_path, FollowupFinding("code-rules", "run.py", "one smell", "code-rules/x")
226
+ )
227
+
228
+ exit_code, output_text = run_command(["count", "--repository-root", str(tmp_path)])
229
+
230
+ assert exit_code == 0
231
+ assert "1" in output_text
232
+
233
+
234
+ def test_count_escalates_once_the_backlog_passes_the_threshold(
235
+ tmp_path: Path,
236
+ ) -> None:
237
+ for each_index in range(FOLLOWUP_BACKLOG_THRESHOLD + 1):
238
+ record_followup_finding(
239
+ tmp_path,
240
+ FollowupFinding(
241
+ "code-rules",
242
+ f"run{each_index}.py",
243
+ "one smell",
244
+ f"code-rules/x{each_index}",
245
+ ),
246
+ )
247
+
248
+ exit_code, output_text = run_command(["count", "--repository-root", str(tmp_path)])
249
+
250
+ assert exit_code == BACKLOG_EXCEEDED_EXIT_CODE
251
+ assert str(FOLLOWUP_BACKLOG_THRESHOLD) in output_text
252
+
253
+
254
+ def test_list_names_the_check_behind_each_finding(tmp_path: Path) -> None:
255
+ record_followup_finding(
256
+ tmp_path,
257
+ FollowupFinding(
258
+ "code-rules",
259
+ "run.py",
260
+ "one smell",
261
+ "code-rules/constant-outside-config",
262
+ "smell",
263
+ "f00dcafe",
264
+ ),
265
+ )
266
+
267
+ _, output_text = run_command(["list", "--repository-root", str(tmp_path)])
268
+
269
+ assert "code-rules/constant-outside-config" in output_text
270
+ assert "f00dcafe" in output_text
@@ -1,7 +1,22 @@
1
1
  import subprocess
2
2
  from pathlib import Path
3
3
 
4
- from validate_instruction_pairs import validate_repository
4
+ import sys
5
+
6
+ from validate_instruction_pairs import (
7
+ GATE_FAILED_EXIT_CODE,
8
+ GATE_PASSED_EXIT_CODE,
9
+ all_instruction_findings,
10
+ run_gate,
11
+ validate_repository,
12
+ )
13
+
14
+ _hooks_directory = str(Path(__file__).resolve().parents[1] / "hooks")
15
+ if _hooks_directory not in sys.path:
16
+ sys.path.insert(0, _hooks_directory)
17
+
18
+ from followup_ledger import all_recorded_findings, head_commit
19
+ from hooks_constants.followup_ledger_constants import SEVERITY_SMELL
5
20
 
6
21
 
7
22
  def read_workflow(workflow_filename: str) -> str:
@@ -145,3 +160,153 @@ def test_release_please_short_circuit_exists_in_workflow() -> None:
145
160
  assert "chore(.*): release " in workflow_text
146
161
  assert "autorelease: pending" in workflow_text
147
162
  assert "skip-validation:" in workflow_text
163
+
164
+
165
+ def write_valid_pair(repository_root: Path) -> None:
166
+ (repository_root / "AGENTS.md").write_bytes(b"# Canonical guidance\n")
167
+ (repository_root / "CLAUDE.md").write_bytes(b"@AGENTS.md\n")
168
+ subprocess.run(
169
+ ["git", "add", "AGENTS.md", "CLAUDE.md"], cwd=repository_root, check=True
170
+ )
171
+
172
+
173
+ def make_import_text_wrong(repository_root: Path) -> None:
174
+ (repository_root / "CLAUDE.md").write_bytes(b"@AGENTS.md\n\nExtra guidance.\n")
175
+
176
+
177
+ def make_git_mode_executable(repository_root: Path) -> None:
178
+ subprocess.run(
179
+ ["git", "update-index", "--chmod=+x", "CLAUDE.md"],
180
+ cwd=repository_root,
181
+ check=True,
182
+ )
183
+
184
+
185
+ def test_a_wrong_import_text_is_a_breaking_finding(tmp_path: Path) -> None:
186
+ initialize_repository(tmp_path)
187
+ write_valid_pair(tmp_path)
188
+ make_import_text_wrong(tmp_path)
189
+
190
+ all_breaking = [
191
+ each_finding
192
+ for each_finding in all_instruction_findings(tmp_path)
193
+ if each_finding.severity == "breaking"
194
+ ]
195
+
196
+ assert [each_finding.rule_id for each_finding in all_breaking] == [
197
+ "instruction-import-text"
198
+ ]
199
+
200
+
201
+ def test_a_wrong_git_mode_is_a_smell_finding(tmp_path: Path) -> None:
202
+ initialize_repository(tmp_path)
203
+ write_valid_pair(tmp_path)
204
+ make_git_mode_executable(tmp_path)
205
+
206
+ all_smells = [
207
+ each_finding
208
+ for each_finding in all_instruction_findings(tmp_path)
209
+ if each_finding.severity == "smell"
210
+ ]
211
+
212
+ assert [each_finding.rule_id for each_finding in all_smells] == [
213
+ "instruction-git-mode"
214
+ ]
215
+
216
+
217
+ def test_run_gate_fails_on_a_breaking_finding(tmp_path: Path) -> None:
218
+ initialize_repository(tmp_path)
219
+ write_valid_pair(tmp_path)
220
+ make_import_text_wrong(tmp_path)
221
+
222
+ assert run_gate(tmp_path) == GATE_FAILED_EXIT_CODE
223
+
224
+
225
+ def test_run_gate_passes_a_smell_and_records_it(tmp_path: Path) -> None:
226
+ initialize_repository(tmp_path)
227
+ write_valid_pair(tmp_path)
228
+ make_git_mode_executable(tmp_path)
229
+
230
+ exit_code = run_gate(tmp_path)
231
+
232
+ assert exit_code == GATE_PASSED_EXIT_CODE
233
+ assert [
234
+ each_finding.rule_id for each_finding in all_recorded_findings(tmp_path)
235
+ ] == ["instruction-git-mode"]
236
+
237
+
238
+ def test_a_recorded_smell_carries_its_severity_and_the_checked_out_revision(
239
+ tmp_path: Path,
240
+ ) -> None:
241
+ initialize_repository(tmp_path)
242
+ write_valid_pair(tmp_path)
243
+ make_git_mode_executable(tmp_path)
244
+ subprocess.run(
245
+ ["git", "commit", "--quiet", "-m", "instruction pair"],
246
+ cwd=tmp_path,
247
+ check=True,
248
+ )
249
+
250
+ run_gate(tmp_path)
251
+
252
+ recorded_finding = all_recorded_findings(tmp_path)[0]
253
+ assert recorded_finding.severity == SEVERITY_SMELL
254
+ assert recorded_finding.check_id == "instruction-git-mode"
255
+ assert recorded_finding.origin_commit == head_commit(tmp_path)
256
+ assert recorded_finding.origin_commit
257
+
258
+
259
+ def test_run_gate_records_a_smell_it_finds_beside_a_breaking_finding(
260
+ tmp_path: Path,
261
+ ) -> None:
262
+ initialize_repository(tmp_path)
263
+ write_valid_pair(tmp_path)
264
+ make_import_text_wrong(tmp_path)
265
+ make_git_mode_executable(tmp_path)
266
+
267
+ exit_code = run_gate(tmp_path)
268
+
269
+ assert exit_code == GATE_FAILED_EXIT_CODE
270
+ assert [
271
+ each_finding.rule_id for each_finding in all_recorded_findings(tmp_path)
272
+ ] == ["instruction-git-mode"]
273
+
274
+
275
+ def test_run_gate_passes_a_valid_tree_and_records_nothing(tmp_path: Path) -> None:
276
+ initialize_repository(tmp_path)
277
+ write_valid_pair(tmp_path)
278
+
279
+ exit_code = run_gate(tmp_path)
280
+
281
+ assert exit_code == GATE_PASSED_EXIT_CODE
282
+ assert all_recorded_findings(tmp_path) == ()
283
+
284
+
285
+ def read_sparse_checkout_paths() -> list[str]:
286
+ workflow_text = read_workflow("instruction-pairs-reusable.yml")
287
+ block_start = workflow_text.index("sparse-checkout: |")
288
+ block_end = workflow_text.index("sparse-checkout-cone-mode", block_start)
289
+ return [
290
+ each_line.strip()
291
+ for each_line in workflow_text[block_start:block_end].splitlines()
292
+ if each_line.strip().startswith("packages/")
293
+ ]
294
+
295
+
296
+ def test_sparse_checkout_carries_every_module_the_validator_imports() -> None:
297
+ package_root = Path(__file__).resolve().parents[1]
298
+ all_checked_out_paths = set(read_sparse_checkout_paths())
299
+
300
+ all_required_paths = {
301
+ "packages/claude-dev-env/scripts/validate_instruction_pairs.py",
302
+ "packages/claude-dev-env/scripts/subprocess_window_access.py",
303
+ "packages/claude-dev-env/scripts/dev_env_scripts_constants/__init__.py",
304
+ "packages/claude-dev-env/scripts/dev_env_scripts_constants/followup_constants.py",
305
+ "packages/claude-dev-env/hooks/followup_ledger.py",
306
+ "packages/claude-dev-env/hooks/hooks_constants/__init__.py",
307
+ "packages/claude-dev-env/hooks/hooks_constants/followup_ledger_constants.py",
308
+ }
309
+
310
+ assert all_required_paths <= all_checked_out_paths
311
+ for each_path in all_checked_out_paths:
312
+ assert (package_root.parents[1] / each_path).is_file()
@@ -0,0 +1,151 @@
1
+ """Synchronization tests for the per-check identifier catalog.
2
+
3
+ Each sample below is a message copied from the check that raises it. A check
4
+ whose wording moves fails its sample here, so the catalog and the checks stay
5
+ in step.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+ from policy_lint.check_catalog import check_id_for_message
14
+ from policy_lint.config.check_catalog_constants import (
15
+ ALL_CHECK_CATALOG_ENTRIES,
16
+ UNCLASSIFIED_CHECK_NAME,
17
+ )
18
+ from policy_lint.model import Diagnostic, Severity
19
+
20
+ _BUNDLING_RULE_ID = "code-rules"
21
+ _HOOKS_DIRECTORY = Path(__file__).resolve().parents[2] / "hooks"
22
+ _TEST_FILE_PREFIX = "test_"
23
+ _PYTHON_GLOB = "*.py"
24
+ _SOURCE_ENCODING = "utf-8"
25
+
26
+ ALL_MESSAGE_SAMPLES: tuple[tuple[str, str], ...] = (
27
+ (
28
+ "Line 20: Function 'run' is 74 lines - exceeds blocking threshold - "
29
+ "split into helpers",
30
+ "function-length",
31
+ ),
32
+ (
33
+ "Line 8: Function-local constant TIMEOUT - consider moving to config/",
34
+ "function-local-constant",
35
+ ),
36
+ ("Line 3: Constant TIMEOUT - move to config/", "constant-outside-config"),
37
+ (
38
+ "Line 5: Collection parameter paths - prefix with all_ (CODE_RULES §5)",
39
+ "collection-name-prefix",
40
+ ),
41
+ (
42
+ "Line 6: loop variable 'path' - prefix with each_ (CODE_RULES §5)",
43
+ "loop-variable-prefix",
44
+ ),
45
+ (
46
+ "Line 7: Stuttering collection prefix 'all_all_paths'",
47
+ "stuttering-collection-prefix",
48
+ ),
49
+ ("Line 9: string separator '\\n' - name it", "unnamed-string-separator"),
50
+ ("Line 10: string magic value 'utf-8'", "string-magic-value"),
51
+ (
52
+ "Line 11: Block comment found - refactor to self-documenting code",
53
+ "block-comment-added",
54
+ ),
55
+ (
56
+ "Line 12: Comment found - refactor to self-documenting code",
57
+ "comment-added",
58
+ ),
59
+ ("Line 13: TODO comment added: fix later", "comment-diff"),
60
+ (
61
+ "Line 14: module docstring carries a 44-word run-on sentence",
62
+ "docstring-runon-sentence",
63
+ ),
64
+ ("Line 15: module summary runs 9 prose lines", "docstring-prose-wall"),
65
+ (
66
+ "Line 16: parameter 'count' on 'run' missing type annotation (CODE_RULES §6)",
67
+ "missing-type-annotation",
68
+ ),
69
+ (
70
+ "Line 17: function 'run' missing return type annotation (CODE_RULES §6)",
71
+ "missing-return-annotation",
72
+ ),
73
+ ("src/app.py:3: Single-letter variable 'p'", "single-letter-variable"),
74
+ )
75
+
76
+
77
+ def _all_hook_source_texts() -> tuple[str, ...]:
78
+ return tuple(
79
+ each_path.read_text(encoding=_SOURCE_ENCODING)
80
+ for each_path in _HOOKS_DIRECTORY.rglob(_PYTHON_GLOB)
81
+ if not each_path.name.startswith(_TEST_FILE_PREFIX)
82
+ )
83
+
84
+
85
+ @pytest.mark.parametrize(("sample_message", "expected_name"), ALL_MESSAGE_SAMPLES)
86
+ def test_a_message_sample_resolves_to_its_check_name(
87
+ sample_message: str, expected_name: str
88
+ ) -> None:
89
+ resolved_check_id = check_id_for_message(_BUNDLING_RULE_ID, sample_message)
90
+
91
+ assert resolved_check_id == f"{_BUNDLING_RULE_ID}/{expected_name}"
92
+
93
+
94
+ def test_every_catalog_entry_carries_a_message_sample() -> None:
95
+ all_sampled_names = {each_sample[1] for each_sample in ALL_MESSAGE_SAMPLES}
96
+ all_catalog_names = {
97
+ each_entry.check_name for each_entry in ALL_CHECK_CATALOG_ENTRIES
98
+ }
99
+
100
+ assert all_catalog_names == all_sampled_names
101
+
102
+
103
+ @pytest.mark.parametrize("catalog_entry", ALL_CHECK_CATALOG_ENTRIES)
104
+ def test_each_catalog_marker_is_still_written_by_a_check(catalog_entry) -> None:
105
+ all_source_texts = _all_hook_source_texts()
106
+
107
+ assert any(
108
+ catalog_entry.message_marker in each_text for each_text in all_source_texts
109
+ )
110
+
111
+
112
+ def test_every_catalog_check_name_is_distinct_from_its_neighbours() -> None:
113
+ all_names = [each_entry.check_name for each_entry in ALL_CHECK_CATALOG_ENTRIES]
114
+
115
+ assert len(set(all_names)) == len(all_names)
116
+
117
+
118
+ def test_an_unmatched_message_stays_unclassified() -> None:
119
+ resolved_check_id = check_id_for_message(
120
+ _BUNDLING_RULE_ID, "Line 4: a finding no catalog entry names"
121
+ )
122
+
123
+ assert resolved_check_id == f"{_BUNDLING_RULE_ID}/{UNCLASSIFIED_CHECK_NAME}"
124
+
125
+
126
+ def test_a_rule_outside_the_bundling_set_keeps_its_own_identifier() -> None:
127
+ resolved_check_id = check_id_for_message(
128
+ "rmtree-safety", "Line 9: shutil.rmtree with ignore_errors=True"
129
+ )
130
+
131
+ assert resolved_check_id == "rmtree-safety"
132
+
133
+
134
+ def test_a_diagnostic_reports_its_rule_identifier_when_it_carries_no_check_name() -> (
135
+ None
136
+ ):
137
+ diagnostic = Diagnostic("test-pairing", Severity.ERROR, "no matching test")
138
+
139
+ assert diagnostic.as_dict()["check_id"] == "test-pairing"
140
+
141
+
142
+ def test_a_diagnostic_reports_the_check_identifier_it_carries() -> None:
143
+ diagnostic = Diagnostic(
144
+ "code-rules",
145
+ Severity.ERROR,
146
+ "Line 3: Constant TIMEOUT - move to config/",
147
+ None,
148
+ "code-rules/constant-outside-config",
149
+ )
150
+
151
+ assert diagnostic.as_dict()["check_id"] == "code-rules/constant-outside-config"