claude-dev-env 2.35.0 → 4.0.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/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
- package/.agents/skills/team-advisor/SKILL.md +11 -11
- package/_shared/advisor/advisor-protocol.md +51 -51
- package/_shared/advisor/reference/advisor-block.md +9 -9
- package/_shared/advisor/reference/astra-rung.md +39 -0
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +6 -6
- package/_shared/advisor/reference/spawn-walk-log.md +9 -9
- package/_shared/advisor/reference/third-party-bind.md +11 -11
- package/_shared/advisor/reference/warm-up.md +13 -13
- package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
- package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
- package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
- package/bin/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/README.md +35 -1
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +368 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +376 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +388 -0
- package/scripts/policy_lint/registry.py +171 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- package/scripts/repository_checks/__init__.py +1 -0
- package/scripts/repository_checks/claude_md.py +108 -0
- package/scripts/repository_checks/config/__init__.py +1 -0
- package/scripts/repository_checks/config/constants.py +150 -0
- package/scripts/repository_checks/env_var_documentation.py +243 -0
- package/scripts/repository_checks/hook_modules.py +34 -0
- package/scripts/repository_checks/models.py +37 -0
- package/scripts/repository_checks/package_inventory.py +118 -0
- package/scripts/repository_checks/pytest_testpaths.py +84 -0
- package/scripts/repository_checks/reporting.py +39 -0
- package/scripts/repository_checks/runner.py +93 -0
- package/scripts/repository_checks/tracked_secrets.py +147 -0
- package/scripts/repository_checks/tracked_tree.py +28 -0
- package/scripts/repository_policy.py +120 -0
- package/scripts/select_pstack_models.mjs +304 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +398 -0
- package/scripts/tests/test_claude_md.py +68 -0
- package/scripts/tests/test_env_var_documentation.py +133 -0
- package/scripts/tests/test_hook_modules.py +17 -0
- package/scripts/tests/test_models.py +26 -0
- package/scripts/tests/test_package_inventory.py +96 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
- package/scripts/tests/test_policy_lint_selection.py +373 -0
- package/scripts/tests/test_pytest_testpaths.py +66 -0
- package/scripts/tests/test_reporting.py +54 -0
- package/scripts/tests/test_repository_policy.py +71 -0
- package/scripts/tests/test_runner.py +59 -0
- package/scripts/tests/test_tracked_secrets.py +175 -0
- package/scripts/tests/test_tracked_tree.py +28 -0
- package/_shared/advisor/reference/sol-rung.md +0 -39
- package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Behavioral tests for the public policy-lint command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path, PurePosixPath
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
16
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
18
|
+
|
|
19
|
+
import cde_lint
|
|
20
|
+
from policy_lint import render as render_module
|
|
21
|
+
from policy_lint.model import (
|
|
22
|
+
BaseChanges,
|
|
23
|
+
Diagnostic,
|
|
24
|
+
ExplicitFiles,
|
|
25
|
+
LintReport,
|
|
26
|
+
LintRequest,
|
|
27
|
+
Location,
|
|
28
|
+
RepositoryTree,
|
|
29
|
+
Severity,
|
|
30
|
+
StagedChanges,
|
|
31
|
+
TextDocument,
|
|
32
|
+
)
|
|
33
|
+
from policy_lint.selection import SelectionRunFatal
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cli_module() -> ModuleType:
|
|
37
|
+
return cde_lint
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _render_module() -> ModuleType:
|
|
41
|
+
return render_module
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _clean_report() -> LintReport:
|
|
45
|
+
return LintReport(1, (), (), ("rule-a",), (), ())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _located_diagnostic() -> Diagnostic:
|
|
49
|
+
return Diagnostic(
|
|
50
|
+
"rule-a",
|
|
51
|
+
Severity.ERROR,
|
|
52
|
+
"located finding",
|
|
53
|
+
Location(PurePosixPath("src/app.py"), 4, 2),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _unlocated_diagnostic() -> Diagnostic:
|
|
58
|
+
return Diagnostic("rule-b", Severity.WARNING, "unlocated finding")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _mixed_report() -> LintReport:
|
|
62
|
+
return LintReport(
|
|
63
|
+
1,
|
|
64
|
+
(_located_diagnostic(), _unlocated_diagnostic()),
|
|
65
|
+
(PurePosixPath("src/app.py"),),
|
|
66
|
+
("rule-a", "rule-b"),
|
|
67
|
+
(),
|
|
68
|
+
(),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _run_cli(
|
|
73
|
+
all_arguments: list[str],
|
|
74
|
+
*,
|
|
75
|
+
stdin_text: str = "",
|
|
76
|
+
lint_runner: Callable[[LintRequest], LintReport],
|
|
77
|
+
repository_root: Path,
|
|
78
|
+
) -> tuple[int, str, str]:
|
|
79
|
+
stdout = io.StringIO()
|
|
80
|
+
stderr = io.StringIO()
|
|
81
|
+
exit_code = _cli_module().main(
|
|
82
|
+
all_arguments,
|
|
83
|
+
stdin=io.StringIO(stdin_text),
|
|
84
|
+
stdout=stdout,
|
|
85
|
+
stderr=stderr,
|
|
86
|
+
lint_runner=lint_runner,
|
|
87
|
+
repository_root=repository_root,
|
|
88
|
+
)
|
|
89
|
+
return exit_code, stdout.getvalue(), stderr.getvalue()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _capture_request(
|
|
93
|
+
all_arguments: list[str],
|
|
94
|
+
repository_root: Path,
|
|
95
|
+
stdin_text: str = "",
|
|
96
|
+
) -> LintRequest:
|
|
97
|
+
captured_request: dict[str, LintRequest] = {}
|
|
98
|
+
|
|
99
|
+
def fake_lint(request: LintRequest) -> LintReport:
|
|
100
|
+
captured_request["request"] = request
|
|
101
|
+
return _clean_report()
|
|
102
|
+
|
|
103
|
+
exit_code, _, stderr_text = _run_cli(
|
|
104
|
+
all_arguments,
|
|
105
|
+
stdin_text=stdin_text,
|
|
106
|
+
lint_runner=fake_lint,
|
|
107
|
+
repository_root=repository_root,
|
|
108
|
+
)
|
|
109
|
+
assert exit_code == 0, stderr_text
|
|
110
|
+
return captured_request["request"]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_should_build_explicit_files_request(tmp_path: Path) -> None:
|
|
114
|
+
request = _capture_request(
|
|
115
|
+
["--files", "src/a.py", "src/b.py"],
|
|
116
|
+
repository_root=tmp_path,
|
|
117
|
+
)
|
|
118
|
+
assert isinstance(request.source, ExplicitFiles)
|
|
119
|
+
assert request.source.paths == (Path("src/a.py"), Path("src/b.py"))
|
|
120
|
+
assert request.repository_root == tmp_path
|
|
121
|
+
assert request.rule_sets == frozenset({"changed"})
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_should_build_staged_request(tmp_path: Path) -> None:
|
|
125
|
+
request = _capture_request(["--staged"], repository_root=tmp_path)
|
|
126
|
+
assert isinstance(request.source, StagedChanges)
|
|
127
|
+
assert request.rule_sets == frozenset({"changed"})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_should_build_base_request(tmp_path: Path) -> None:
|
|
131
|
+
request = _capture_request(
|
|
132
|
+
["--base", "origin/main"],
|
|
133
|
+
repository_root=tmp_path,
|
|
134
|
+
)
|
|
135
|
+
assert isinstance(request.source, BaseChanges)
|
|
136
|
+
assert request.source.revision == "origin/main"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_should_build_repository_request(tmp_path: Path) -> None:
|
|
140
|
+
request = _capture_request(["--repository"], repository_root=tmp_path)
|
|
141
|
+
assert isinstance(request.source, RepositoryTree)
|
|
142
|
+
assert request.rule_sets == frozenset({"repository"})
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_should_read_text_as_from_stdin_not_disk(tmp_path: Path) -> None:
|
|
146
|
+
buffer_path = tmp_path / "draft.py"
|
|
147
|
+
buffer_path.write_text("DISK CONTENTS\n", encoding="utf-8")
|
|
148
|
+
request = _capture_request(
|
|
149
|
+
["--text-as", str(buffer_path)],
|
|
150
|
+
repository_root=tmp_path,
|
|
151
|
+
stdin_text="EDITOR BUFFER",
|
|
152
|
+
)
|
|
153
|
+
assert isinstance(request.source, TextDocument)
|
|
154
|
+
assert request.source.path == buffer_path
|
|
155
|
+
assert request.source.text == "EDITOR BUFFER"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_should_reject_combined_source_flags(tmp_path: Path) -> None:
|
|
159
|
+
calls: list[LintRequest] = []
|
|
160
|
+
|
|
161
|
+
def fake_lint(request: LintRequest) -> LintReport:
|
|
162
|
+
calls.append(request)
|
|
163
|
+
return _clean_report()
|
|
164
|
+
|
|
165
|
+
exit_code, _, stderr_text = _run_cli(
|
|
166
|
+
["--files", "a.py", "--staged"],
|
|
167
|
+
lint_runner=fake_lint,
|
|
168
|
+
repository_root=tmp_path,
|
|
169
|
+
)
|
|
170
|
+
assert exit_code == 2
|
|
171
|
+
assert calls == []
|
|
172
|
+
assert stderr_text != ""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_should_exit_two_when_no_source_flag(tmp_path: Path) -> None:
|
|
176
|
+
exit_code, _, stderr_text = _run_cli(
|
|
177
|
+
[],
|
|
178
|
+
lint_runner=lambda _request: _clean_report(),
|
|
179
|
+
repository_root=tmp_path,
|
|
180
|
+
)
|
|
181
|
+
assert exit_code == 2
|
|
182
|
+
assert stderr_text != ""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_should_discover_repository_root_from_nested_directory(tmp_path: Path) -> None:
|
|
186
|
+
subprocess.run(
|
|
187
|
+
["git", "init", "--quiet"],
|
|
188
|
+
cwd=tmp_path,
|
|
189
|
+
check=True,
|
|
190
|
+
capture_output=True,
|
|
191
|
+
)
|
|
192
|
+
nested_directory = tmp_path / "nested"
|
|
193
|
+
nested_directory.mkdir()
|
|
194
|
+
assert _cli_module()._discover_repository_root(nested_directory) == tmp_path.resolve()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_should_resolve_files_from_the_caller_directory(tmp_path: Path) -> None:
|
|
198
|
+
all_arguments = _cli_module()._resolve_shell_file_arguments(
|
|
199
|
+
["--files", "nested.py", "--format", "json"], tmp_path
|
|
200
|
+
)
|
|
201
|
+
assert all_arguments == [
|
|
202
|
+
"--files",
|
|
203
|
+
str(tmp_path / "nested.py"),
|
|
204
|
+
"--format",
|
|
205
|
+
"json",
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_should_resolve_text_path_from_the_caller_directory(tmp_path: Path) -> None:
|
|
210
|
+
all_arguments = _cli_module()._resolve_shell_file_arguments(
|
|
211
|
+
["--text-as", "nested.py", "--format", "editor"], tmp_path
|
|
212
|
+
)
|
|
213
|
+
assert all_arguments == [
|
|
214
|
+
"--text-as",
|
|
215
|
+
str(tmp_path / "nested.py"),
|
|
216
|
+
"--format",
|
|
217
|
+
"editor",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_should_report_missing_git_as_invalid_input(
|
|
222
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
223
|
+
) -> None:
|
|
224
|
+
def missing_git(_repository_root: Path, _all_arguments: tuple[str, ...]) -> bytes:
|
|
225
|
+
raise FileNotFoundError("git")
|
|
226
|
+
|
|
227
|
+
monkeypatch.setattr(_cli_module(), "git_bytes_for", missing_git)
|
|
228
|
+
with pytest.raises(_cli_module()._LintUsageError, match="Git repository"):
|
|
229
|
+
_cli_module()._discover_repository_root(tmp_path)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_should_exit_two_for_unknown_format(tmp_path: Path) -> None:
|
|
233
|
+
exit_code, _, stderr_text = _run_cli(
|
|
234
|
+
["--files", "a.py", "--format", "xml"],
|
|
235
|
+
lint_runner=lambda _request: _clean_report(),
|
|
236
|
+
repository_root=tmp_path,
|
|
237
|
+
)
|
|
238
|
+
assert exit_code == 2
|
|
239
|
+
assert stderr_text != ""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_should_exit_two_when_selection_fails(tmp_path: Path) -> None:
|
|
243
|
+
def fake_lint(request: LintRequest) -> LintReport:
|
|
244
|
+
del request
|
|
245
|
+
raise SelectionRunFatal("File does not exist: missing.py")
|
|
246
|
+
|
|
247
|
+
exit_code, _, stderr_text = _run_cli(
|
|
248
|
+
["--files", "missing.py"],
|
|
249
|
+
lint_runner=fake_lint,
|
|
250
|
+
repository_root=tmp_path,
|
|
251
|
+
)
|
|
252
|
+
assert exit_code == 2
|
|
253
|
+
assert "File does not exist: missing.py" in stderr_text
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_should_exit_zero_when_the_report_is_clean(tmp_path: Path) -> None:
|
|
257
|
+
exit_code, stdout_text, stderr_text = _run_cli(
|
|
258
|
+
["--staged"],
|
|
259
|
+
lint_runner=lambda _request: _clean_report(),
|
|
260
|
+
repository_root=tmp_path,
|
|
261
|
+
)
|
|
262
|
+
assert exit_code == 0
|
|
263
|
+
assert stderr_text == ""
|
|
264
|
+
assert stdout_text == ""
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _stage_oversized_test_file(repository_root: Path) -> None:
|
|
268
|
+
subprocess.run(
|
|
269
|
+
["git", "init", "--quiet"],
|
|
270
|
+
cwd=repository_root,
|
|
271
|
+
check=True,
|
|
272
|
+
capture_output=True,
|
|
273
|
+
)
|
|
274
|
+
oversized_path = repository_root / "tests" / "oversized.mjs"
|
|
275
|
+
oversized_path.parent.mkdir()
|
|
276
|
+
oversized_path.write_bytes(b"\n" * 1001)
|
|
277
|
+
subprocess.run(
|
|
278
|
+
["git", "add", "--", "tests/oversized.mjs"],
|
|
279
|
+
cwd=repository_root,
|
|
280
|
+
check=True,
|
|
281
|
+
capture_output=True,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _run_staged_format(
|
|
286
|
+
report_format: str, repository_root: Path
|
|
287
|
+
) -> tuple[int, str, str]:
|
|
288
|
+
return _run_cli(
|
|
289
|
+
["--staged", "--format", report_format],
|
|
290
|
+
lint_runner=_cli_module().lint,
|
|
291
|
+
repository_root=repository_root,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_staged_clean_reports_keep_legacy_advisories_outside_renderer(
|
|
296
|
+
tmp_path: Path,
|
|
297
|
+
) -> None:
|
|
298
|
+
_stage_oversized_test_file(tmp_path)
|
|
299
|
+
json_exit_code, json_stdout, json_stderr = _run_staged_format("json", tmp_path)
|
|
300
|
+
text_exit_code, text_stdout, text_stderr = _run_staged_format("text", tmp_path)
|
|
301
|
+
editor_exit_code, editor_stdout, editor_stderr = _run_staged_format(
|
|
302
|
+
"editor", tmp_path
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
assert json_exit_code == 0
|
|
306
|
+
assert json_stderr == ""
|
|
307
|
+
json_report = json.loads(json_stdout)
|
|
308
|
+
assert json_report["diagnostics"] == []
|
|
309
|
+
assert "terminology-sweep" in json_report["executed_rules"]
|
|
310
|
+
assert text_exit_code == 0
|
|
311
|
+
assert text_stdout == ""
|
|
312
|
+
assert text_stderr == ""
|
|
313
|
+
assert editor_exit_code == 0
|
|
314
|
+
assert editor_stdout == ""
|
|
315
|
+
assert editor_stderr == ""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_staged_git_failure_exits_three_with_valid_json(
|
|
319
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
320
|
+
) -> None:
|
|
321
|
+
_stage_oversized_test_file(tmp_path)
|
|
322
|
+
adapter_module = sys.modules["policy_lint.adapters"]
|
|
323
|
+
terminology_module = adapter_module._pr_loop_script_module("terminology_sweep")
|
|
324
|
+
|
|
325
|
+
def fail_git(
|
|
326
|
+
_repository_root: Path, _all_arguments: object
|
|
327
|
+
) -> subprocess.CompletedProcess[str]:
|
|
328
|
+
raise RuntimeError("simulated Git failure")
|
|
329
|
+
|
|
330
|
+
monkeypatch.setattr(terminology_module, "_run_strict_git", fail_git)
|
|
331
|
+
exit_code, stdout_text, stderr_text = _run_staged_format("json", tmp_path)
|
|
332
|
+
parsed_report = json.loads(stdout_text)
|
|
333
|
+
|
|
334
|
+
assert exit_code == 3
|
|
335
|
+
assert stderr_text == ""
|
|
336
|
+
assert parsed_report["diagnostics"] == []
|
|
337
|
+
assert parsed_report["failed_rules"] == ["terminology-sweep"]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_should_exit_one_when_diagnostics_exist(tmp_path: Path) -> None:
|
|
341
|
+
exit_code, stdout_text, _stderr_text = _run_cli(
|
|
342
|
+
["--files", "src/app.py"],
|
|
343
|
+
lint_runner=lambda _request: _mixed_report(),
|
|
344
|
+
repository_root=tmp_path,
|
|
345
|
+
)
|
|
346
|
+
assert exit_code == 1
|
|
347
|
+
assert "located finding" in stdout_text
|
|
348
|
+
assert "unlocated finding" in stdout_text
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_should_exit_three_when_a_rule_fails(tmp_path: Path) -> None:
|
|
352
|
+
failed_report = LintReport(1, (), (), (), ("rule-a",), ())
|
|
353
|
+
exit_code, stdout_text, _stderr_text = _run_cli(
|
|
354
|
+
["--staged"],
|
|
355
|
+
lint_runner=lambda _request: failed_report,
|
|
356
|
+
repository_root=tmp_path,
|
|
357
|
+
)
|
|
358
|
+
assert exit_code == 3
|
|
359
|
+
assert "rule-a" in stdout_text
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_should_emit_schema_versioned_deterministic_json(tmp_path: Path) -> None:
|
|
363
|
+
render_module = _render_module()
|
|
364
|
+
first_text = render_module.render(_mixed_report(), render_module.ReportFormat.JSON)
|
|
365
|
+
second_text = render_module.render(_mixed_report(), render_module.ReportFormat.JSON)
|
|
366
|
+
assert first_text == second_text
|
|
367
|
+
parsed_report = json.loads(first_text)
|
|
368
|
+
assert parsed_report["schema_version"] == 1
|
|
369
|
+
assert parsed_report == json.loads(
|
|
370
|
+
json.dumps(parsed_report, sort_keys=True)
|
|
371
|
+
)
|
|
372
|
+
exit_code, stdout_text, _stderr_text = _run_cli(
|
|
373
|
+
["--files", "src/app.py", "--format", "json"],
|
|
374
|
+
lint_runner=lambda _request: _mixed_report(),
|
|
375
|
+
repository_root=tmp_path,
|
|
376
|
+
)
|
|
377
|
+
assert exit_code == 1
|
|
378
|
+
assert json.loads(stdout_text) == parsed_report
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_should_emit_only_located_diagnostics_in_editor_format(
|
|
382
|
+
tmp_path: Path,
|
|
383
|
+
) -> None:
|
|
384
|
+
render_module = _render_module()
|
|
385
|
+
rendered_text = render_module.render(
|
|
386
|
+
_mixed_report(), render_module.ReportFormat.EDITOR
|
|
387
|
+
)
|
|
388
|
+
assert "src/app.py:4:2:" in rendered_text
|
|
389
|
+
assert "located finding" in rendered_text
|
|
390
|
+
assert "unlocated finding" not in rendered_text
|
|
391
|
+
exit_code, stdout_text, _stderr_text = _run_cli(
|
|
392
|
+
["--files", "src/app.py", "--format", "editor"],
|
|
393
|
+
lint_runner=lambda _request: _mixed_report(),
|
|
394
|
+
repository_root=tmp_path,
|
|
395
|
+
)
|
|
396
|
+
assert exit_code == 1
|
|
397
|
+
assert "src/app.py:4:2:" in stdout_text
|
|
398
|
+
assert "unlocated finding" not in stdout_text
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Behavior tests for committed CLAUDE.md references."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
12
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
14
|
+
|
|
15
|
+
from repository_checks.config.constants import (
|
|
16
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
17
|
+
FAILED_CHECK_EXIT_CODE,
|
|
18
|
+
FINDINGS_EXIT_CODE,
|
|
19
|
+
)
|
|
20
|
+
from repository_policy_test_support import (
|
|
21
|
+
commit_tracked_files,
|
|
22
|
+
initialize_repository,
|
|
23
|
+
run_policy,
|
|
24
|
+
write_text,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_should_flag_a_broken_claude_md_file_reference(tmp_path: Path) -> None:
|
|
29
|
+
repository_root = tmp_path / "repo"
|
|
30
|
+
initialize_repository(repository_root)
|
|
31
|
+
write_text(
|
|
32
|
+
repository_root / "notes" / "CLAUDE.md",
|
|
33
|
+
"# notes\n\n"
|
|
34
|
+
"| File | Role |\n"
|
|
35
|
+
"|---|---|\n"
|
|
36
|
+
"| `absent_helper.py` | Missing helper |\n",
|
|
37
|
+
)
|
|
38
|
+
commit_tracked_files(repository_root)
|
|
39
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
40
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
41
|
+
assert CHECK_ID_CLAUDE_MD_ORPHANS in stdout_text
|
|
42
|
+
assert "notes/CLAUDE.md" in stdout_text
|
|
43
|
+
assert "absent_helper.py" in stdout_text
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_should_fail_closed_when_claude_md_walk_fails_after_first_entry(
|
|
47
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
48
|
+
) -> None:
|
|
49
|
+
repository_root = tmp_path / "repo"
|
|
50
|
+
initialize_repository(repository_root)
|
|
51
|
+
write_text(
|
|
52
|
+
repository_root / "notes" / "CLAUDE.md",
|
|
53
|
+
"# notes\n\n"
|
|
54
|
+
"| File | Role |\n"
|
|
55
|
+
"|---|---|\n"
|
|
56
|
+
"| `absent_helper.py` | Missing helper |\n",
|
|
57
|
+
)
|
|
58
|
+
commit_tracked_files(repository_root)
|
|
59
|
+
|
|
60
|
+
def fail_after_first_entry(_file_path: Path, _pattern: str = "*") -> Iterator[Path]:
|
|
61
|
+
yield repository_root / "notes" / "CLAUDE.md"
|
|
62
|
+
raise OSError("scan root unreadable")
|
|
63
|
+
|
|
64
|
+
monkeypatch.setattr(Path, "rglob", fail_after_first_entry)
|
|
65
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
66
|
+
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
67
|
+
assert CHECK_ID_CLAUDE_MD_ORPHANS in stdout_text
|
|
68
|
+
assert "error: rule failed:" in stdout_text
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Behavior tests for environment-variable documentation checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
11
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
13
|
+
|
|
14
|
+
from repository_checks.config.constants import (
|
|
15
|
+
CHECK_ID_ENV_VAR_DOCUMENTATION,
|
|
16
|
+
FAILED_CHECK_EXIT_CODE,
|
|
17
|
+
FINDINGS_EXIT_CODE,
|
|
18
|
+
SUCCESS_EXIT_CODE,
|
|
19
|
+
)
|
|
20
|
+
from repository_policy_test_support import (
|
|
21
|
+
commit_tracked_files,
|
|
22
|
+
initialize_repository,
|
|
23
|
+
patch_unreadable_named_file,
|
|
24
|
+
run_policy,
|
|
25
|
+
write_text,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_should_flag_environment_variable_documentation_drift(tmp_path: Path) -> None:
|
|
30
|
+
repository_root = tmp_path / "repo"
|
|
31
|
+
initialize_repository(repository_root)
|
|
32
|
+
write_text(
|
|
33
|
+
repository_root / "auth" / "google_auth.py", "def load() -> None:\n return\n"
|
|
34
|
+
)
|
|
35
|
+
write_text(
|
|
36
|
+
repository_root / "docs" / "configuration.md",
|
|
37
|
+
"## Summary: Environment Variables\n\n"
|
|
38
|
+
"| Variable | Used By | Purpose |\n"
|
|
39
|
+
"|----------|---------|---------|\n"
|
|
40
|
+
"| `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | Path to JSON |\n",
|
|
41
|
+
)
|
|
42
|
+
commit_tracked_files(repository_root)
|
|
43
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
44
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
45
|
+
assert CHECK_ID_ENV_VAR_DOCUMENTATION in stdout_text
|
|
46
|
+
assert "docs/configuration.md" in stdout_text
|
|
47
|
+
assert "GOOGLE_APPLICATION_CREDENTIALS -> auth/google_auth.py" in stdout_text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_should_ignore_long_code_reference_inside_code_fence(
|
|
51
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
52
|
+
) -> None:
|
|
53
|
+
repository_root = tmp_path / "repo"
|
|
54
|
+
initialize_repository(repository_root)
|
|
55
|
+
long_code_reference = ("x" * 300) + ".py"
|
|
56
|
+
write_text(
|
|
57
|
+
repository_root / "docs" / "review.md",
|
|
58
|
+
f"# Review\n\n~~~diff\n+ `{long_code_reference}`\n~~~\n",
|
|
59
|
+
)
|
|
60
|
+
commit_tracked_files(repository_root)
|
|
61
|
+
original_is_file = Path.is_file
|
|
62
|
+
|
|
63
|
+
def reject_long_filename(candidate_path: Path) -> bool:
|
|
64
|
+
if len(candidate_path.name) > 255:
|
|
65
|
+
raise OSError(candidate_path)
|
|
66
|
+
return original_is_file(candidate_path)
|
|
67
|
+
|
|
68
|
+
monkeypatch.setattr(Path, "is_file", reject_long_filename)
|
|
69
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
70
|
+
assert exit_code == SUCCESS_EXIT_CODE
|
|
71
|
+
assert CHECK_ID_ENV_VAR_DOCUMENTATION not in stdout_text
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_should_fail_closed_when_an_env_var_code_file_cannot_be_read(
|
|
75
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
76
|
+
) -> None:
|
|
77
|
+
repository_root = tmp_path / "repo"
|
|
78
|
+
initialize_repository(repository_root)
|
|
79
|
+
write_text(
|
|
80
|
+
repository_root / "packages" / "service" / "auth" / "google_auth.py",
|
|
81
|
+
"def load() -> None:\n return\n",
|
|
82
|
+
)
|
|
83
|
+
write_text(
|
|
84
|
+
repository_root / "docs" / "configuration.md",
|
|
85
|
+
"## Summary: Environment Variables\n\n"
|
|
86
|
+
"| Variable | Used By | Purpose |\n"
|
|
87
|
+
"|----------|---------|---------|\n"
|
|
88
|
+
"| `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | Path to JSON |\n",
|
|
89
|
+
)
|
|
90
|
+
commit_tracked_files(repository_root)
|
|
91
|
+
patch_unreadable_named_file(monkeypatch, "google_auth.py", "code file unreadable")
|
|
92
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
93
|
+
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
94
|
+
assert CHECK_ID_ENV_VAR_DOCUMENTATION in stdout_text
|
|
95
|
+
assert "error: rule failed:" in stdout_text
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_should_fail_closed_before_reading_env_var_code_file_outside_repository(
|
|
99
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
100
|
+
) -> None:
|
|
101
|
+
repository_root = tmp_path / "repo"
|
|
102
|
+
initialize_repository(repository_root)
|
|
103
|
+
outside_path = tmp_path / "outside.py"
|
|
104
|
+
write_text(outside_path, "def load() -> None:\n return\n")
|
|
105
|
+
write_text(
|
|
106
|
+
repository_root / "docs" / "configuration.md",
|
|
107
|
+
"## Summary: Environment Variables\n\n"
|
|
108
|
+
"| Variable | Used By | Purpose |\n"
|
|
109
|
+
"|----------|---------|---------|\n"
|
|
110
|
+
"| `EXTERNAL_TOKEN` | `../outside.py` | External consumer |\n",
|
|
111
|
+
)
|
|
112
|
+
commit_tracked_files(repository_root)
|
|
113
|
+
all_read_paths = record_read_paths(monkeypatch)
|
|
114
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
115
|
+
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
116
|
+
assert CHECK_ID_ENV_VAR_DOCUMENTATION in stdout_text
|
|
117
|
+
assert outside_path not in all_read_paths
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def record_read_paths(monkeypatch: pytest.MonkeyPatch) -> list[Path]:
|
|
121
|
+
original_read_text = Path.read_text
|
|
122
|
+
all_read_paths: list[Path] = []
|
|
123
|
+
|
|
124
|
+
def record_read_path(
|
|
125
|
+
file_path: Path,
|
|
126
|
+
encoding: str | None = None,
|
|
127
|
+
errors: str | None = None,
|
|
128
|
+
) -> str:
|
|
129
|
+
all_read_paths.append(file_path.resolve())
|
|
130
|
+
return original_read_text(file_path, encoding=encoding, errors=errors)
|
|
131
|
+
|
|
132
|
+
monkeypatch.setattr(Path, "read_text", record_read_path)
|
|
133
|
+
return all_read_paths
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Behavior tests for repository-check hook module loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
9
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
11
|
+
|
|
12
|
+
from repository_checks.hook_modules import load_hooks_module
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_should_load_a_hooks_module_by_its_dotted_name() -> None:
|
|
16
|
+
loaded_module = load_hooks_module("hooks_constants.pii_prevention_constants")
|
|
17
|
+
assert loaded_module.__name__ == "hooks_constants.pii_prevention_constants"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Behavior tests for repository-check report models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
9
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
11
|
+
|
|
12
|
+
from repository_checks.config.constants import (
|
|
13
|
+
FAILED_CHECK_EXIT_CODE,
|
|
14
|
+
FINDINGS_EXIT_CODE,
|
|
15
|
+
SUCCESS_EXIT_CODE,
|
|
16
|
+
)
|
|
17
|
+
from repository_checks.models import RepositoryCheckReport, RepositoryFinding
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_should_choose_exit_code_by_report_severity() -> None:
|
|
21
|
+
finding = RepositoryFinding("check", "file.py", "message")
|
|
22
|
+
assert RepositoryCheckReport((), ()).exit_code == SUCCESS_EXIT_CODE
|
|
23
|
+
assert RepositoryCheckReport((finding,), ()).exit_code == FINDINGS_EXIT_CODE
|
|
24
|
+
assert RepositoryCheckReport((finding,), ("check",)).exit_code == (
|
|
25
|
+
FAILED_CHECK_EXIT_CODE
|
|
26
|
+
)
|