claude-dev-env 2.36.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/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/docs/references/team-advisor-skill.md +3 -3
- package/hooks/validators/README.md +35 -1
- package/package.json +1 -1
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/adapter_detectors.py +42 -2
- package/scripts/policy_lint/adapters.py +24 -0
- package/scripts/policy_lint/config/constants.py +11 -0
- package/scripts/policy_lint/engine.py +1 -1
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +10 -1
- 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/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +25 -1
- 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_registry.py +117 -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,89 @@
|
|
|
1
|
+
"""Shared fixtures for repository-policy behavior tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
13
|
+
UTF8_ENCODING = "utf-8"
|
|
14
|
+
|
|
15
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
17
|
+
|
|
18
|
+
import repository_policy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def initialize_repository(repository_root: Path) -> None:
|
|
22
|
+
repository_root.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
run_git(repository_root, ("init", "--initial-branch=main"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def commit_tracked_files(repository_root: Path) -> None:
|
|
27
|
+
run_git(repository_root, ("add", "-A"))
|
|
28
|
+
run_git(repository_root, ("commit", "--quiet", "-m", "seed"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write_text(file_path: Path, content: str) -> None:
|
|
32
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
file_path.write_text(content, encoding=UTF8_ENCODING)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def patch_unreadable_named_file(
|
|
37
|
+
monkeypatch: pytest.MonkeyPatch, filename: str, error_text: str
|
|
38
|
+
) -> None:
|
|
39
|
+
original_read_text = Path.read_text
|
|
40
|
+
|
|
41
|
+
def refuse_named_file(
|
|
42
|
+
file_path: Path,
|
|
43
|
+
encoding: str | None = None,
|
|
44
|
+
errors: str | None = None,
|
|
45
|
+
) -> str:
|
|
46
|
+
if file_path.name == filename:
|
|
47
|
+
raise OSError(error_text)
|
|
48
|
+
return original_read_text(file_path, encoding=encoding, errors=errors)
|
|
49
|
+
|
|
50
|
+
monkeypatch.setattr(Path, "read_text", refuse_named_file)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def seed_clean_repository(repository_root: Path) -> Path:
|
|
54
|
+
initialize_repository(repository_root)
|
|
55
|
+
write_text(repository_root / "README.md", "clean tree\n")
|
|
56
|
+
commit_tracked_files(repository_root)
|
|
57
|
+
return repository_root
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_policy(
|
|
61
|
+
repository_root: Path, all_arguments: list[str] | None = None
|
|
62
|
+
) -> tuple[int, str, str]:
|
|
63
|
+
stdout = io.StringIO()
|
|
64
|
+
stderr = io.StringIO()
|
|
65
|
+
exit_code = repository_policy.main(
|
|
66
|
+
all_arguments or [],
|
|
67
|
+
repository_root=repository_root,
|
|
68
|
+
stdout=stdout,
|
|
69
|
+
stderr=stderr,
|
|
70
|
+
)
|
|
71
|
+
return exit_code, stdout.getvalue(), stderr.getvalue()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run_git(repository_root: Path, all_arguments: tuple[str, ...]) -> None:
|
|
75
|
+
subprocess.run(
|
|
76
|
+
(
|
|
77
|
+
"git",
|
|
78
|
+
"-c",
|
|
79
|
+
"user.email=ci@example.com",
|
|
80
|
+
"-c",
|
|
81
|
+
"user.name=CI",
|
|
82
|
+
"-c",
|
|
83
|
+
"core.hooksPath=",
|
|
84
|
+
*all_arguments,
|
|
85
|
+
),
|
|
86
|
+
cwd=repository_root,
|
|
87
|
+
check=True,
|
|
88
|
+
capture_output=True,
|
|
89
|
+
)
|
|
@@ -304,7 +304,9 @@ def test_staged_clean_reports_keep_legacy_advisories_outside_renderer(
|
|
|
304
304
|
|
|
305
305
|
assert json_exit_code == 0
|
|
306
306
|
assert json_stderr == ""
|
|
307
|
-
|
|
307
|
+
json_report = json.loads(json_stdout)
|
|
308
|
+
assert json_report["diagnostics"] == []
|
|
309
|
+
assert "terminology-sweep" in json_report["executed_rules"]
|
|
308
310
|
assert text_exit_code == 0
|
|
309
311
|
assert text_stdout == ""
|
|
310
312
|
assert text_stderr == ""
|
|
@@ -313,6 +315,28 @@ def test_staged_clean_reports_keep_legacy_advisories_outside_renderer(
|
|
|
313
315
|
assert editor_stderr == ""
|
|
314
316
|
|
|
315
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
|
+
|
|
316
340
|
def test_should_exit_one_when_diagnostics_exist(tmp_path: Path) -> None:
|
|
317
341
|
exit_code, stdout_text, _stderr_text = _run_cli(
|
|
318
342
|
["--files", "src/app.py"],
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Behavior tests for committed package inventories."""
|
|
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_PACKAGE_INVENTORY,
|
|
16
|
+
FAILED_CHECK_EXIT_CODE,
|
|
17
|
+
FINDINGS_EXIT_CODE,
|
|
18
|
+
)
|
|
19
|
+
from repository_policy_test_support import (
|
|
20
|
+
commit_tracked_files,
|
|
21
|
+
initialize_repository,
|
|
22
|
+
patch_unreadable_named_file,
|
|
23
|
+
run_policy,
|
|
24
|
+
write_text,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_SKILL_INVENTORY_MARKDOWN = (
|
|
28
|
+
"# Skill\n\n"
|
|
29
|
+
"| File | Role |\n"
|
|
30
|
+
"|---|---|\n"
|
|
31
|
+
"| `listed_alpha.py` | Alpha helper |\n"
|
|
32
|
+
"| `listed_beta.py` | Beta helper |\n"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_should_flag_a_stale_package_inventory(tmp_path: Path) -> None:
|
|
37
|
+
repository_root = tmp_path / "repo"
|
|
38
|
+
initialize_repository(repository_root)
|
|
39
|
+
package_directory = repository_root / "pipeline"
|
|
40
|
+
_write_package_inventory(package_directory)
|
|
41
|
+
commit_tracked_files(repository_root)
|
|
42
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
43
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
44
|
+
assert CHECK_ID_PACKAGE_INVENTORY in stdout_text
|
|
45
|
+
assert "pipeline/check_dialer_seam_cli.py" in stdout_text
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_should_check_active_skills_and_exclude_archived_skills(tmp_path: Path) -> None:
|
|
49
|
+
repository_root = tmp_path / "repo"
|
|
50
|
+
initialize_repository(repository_root)
|
|
51
|
+
_seed_skill_package(repository_root / ".agents" / "skills" / "live-skill")
|
|
52
|
+
_seed_skill_package(repository_root / ".agents" / "skills-archived" / "old-skill")
|
|
53
|
+
commit_tracked_files(repository_root)
|
|
54
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
55
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
56
|
+
assert CHECK_ID_PACKAGE_INVENTORY in stdout_text
|
|
57
|
+
assert ".agents/skills/live-skill/scripts/unlisted_gamma.py" in stdout_text
|
|
58
|
+
assert (
|
|
59
|
+
".agents/skills-archived/old-skill/scripts/unlisted_gamma.py" not in stdout_text
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_fail_closed_when_a_package_inventory_document_cannot_be_read(
|
|
64
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
65
|
+
) -> None:
|
|
66
|
+
repository_root = tmp_path / "repo"
|
|
67
|
+
initialize_repository(repository_root)
|
|
68
|
+
_write_package_inventory(repository_root / "pipeline")
|
|
69
|
+
commit_tracked_files(repository_root)
|
|
70
|
+
patch_unreadable_named_file(monkeypatch, "README.md", "inventory unreadable")
|
|
71
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
72
|
+
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
73
|
+
assert CHECK_ID_PACKAGE_INVENTORY in stdout_text
|
|
74
|
+
assert "error: rule failed:" in stdout_text
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _write_package_inventory(package_directory: Path) -> None:
|
|
78
|
+
write_text(
|
|
79
|
+
package_directory / "README.md",
|
|
80
|
+
"# Pipeline\n\n"
|
|
81
|
+
"| Path | Role |\n"
|
|
82
|
+
"|---|---|\n"
|
|
83
|
+
"| `dialer_compose.py` | Composes a dialer strip. |\n"
|
|
84
|
+
"| `compose_dialer_cli.py` | CLI for the dialer strip. |\n",
|
|
85
|
+
)
|
|
86
|
+
write_text(package_directory / "dialer_compose.py", "x = 1\n")
|
|
87
|
+
write_text(package_directory / "compose_dialer_cli.py", "x = 1\n")
|
|
88
|
+
write_text(package_directory / "check_dialer_seam_cli.py", "x = 1\n")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _seed_skill_package(skill_root: Path) -> None:
|
|
92
|
+
write_text(skill_root / "SKILL.md", _SKILL_INVENTORY_MARKDOWN)
|
|
93
|
+
scripts_directory = skill_root / "scripts"
|
|
94
|
+
write_text(scripts_directory / "listed_alpha.py", "x = 1\n")
|
|
95
|
+
write_text(scripts_directory / "listed_beta.py", "x = 1\n")
|
|
96
|
+
write_text(scripts_directory / "unlisted_gamma.py", "x = 1\n")
|
|
@@ -11,6 +11,7 @@ import pytest
|
|
|
11
11
|
from policy_lint import adapters, registry
|
|
12
12
|
from policy_lint.engine import lint
|
|
13
13
|
from policy_lint.model import (
|
|
14
|
+
ChangeSetRule,
|
|
14
15
|
Document,
|
|
15
16
|
DocumentRule,
|
|
16
17
|
DocumentSet,
|
|
@@ -41,6 +42,60 @@ def _ensure_git_repository(repository_root: Path) -> None:
|
|
|
41
42
|
)
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def _commit_repository(repository_root: Path, message: str) -> None:
|
|
46
|
+
subprocess.run(
|
|
47
|
+
("git", "config", "user.email", "test@example.com"),
|
|
48
|
+
cwd=repository_root,
|
|
49
|
+
check=True,
|
|
50
|
+
capture_output=True,
|
|
51
|
+
)
|
|
52
|
+
subprocess.run(
|
|
53
|
+
("git", "config", "user.name", "Test"),
|
|
54
|
+
cwd=repository_root,
|
|
55
|
+
check=True,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
)
|
|
58
|
+
subprocess.run(
|
|
59
|
+
("git", "add", "-A"), cwd=repository_root, check=True, capture_output=True
|
|
60
|
+
)
|
|
61
|
+
subprocess.run(
|
|
62
|
+
("git", "commit", "-m", message),
|
|
63
|
+
cwd=repository_root,
|
|
64
|
+
check=True,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _stage_terminology_change(
|
|
70
|
+
repository_root: Path,
|
|
71
|
+
base_source: str,
|
|
72
|
+
current_source: str,
|
|
73
|
+
documentation_text: str,
|
|
74
|
+
) -> None:
|
|
75
|
+
_ensure_git_repository(repository_root)
|
|
76
|
+
source_path = repository_root / "src" / "quota.py"
|
|
77
|
+
source_path.parent.mkdir()
|
|
78
|
+
source_path.write_text(base_source, encoding="utf-8")
|
|
79
|
+
_commit_repository(repository_root, "base")
|
|
80
|
+
source_path.write_text(current_source, encoding="utf-8")
|
|
81
|
+
documentation_path = repository_root / "docs" / "README.md"
|
|
82
|
+
documentation_path.parent.mkdir()
|
|
83
|
+
documentation_path.write_text(documentation_text, encoding="utf-8")
|
|
84
|
+
subprocess.run(
|
|
85
|
+
("git", "add", "-A"), cwd=repository_root, check=True, capture_output=True
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _terminology_rule() -> ChangeSetRule:
|
|
90
|
+
terminology_rule = next(
|
|
91
|
+
each_rule
|
|
92
|
+
for each_rule in registry.default_registry()
|
|
93
|
+
if each_rule.rule_id == "terminology-sweep"
|
|
94
|
+
)
|
|
95
|
+
assert isinstance(terminology_rule, ChangeSetRule)
|
|
96
|
+
return terminology_rule
|
|
97
|
+
|
|
98
|
+
|
|
44
99
|
def test_hook_format_accepts_claude_settings() -> None:
|
|
45
100
|
settings_document = Document.from_text(".claude/settings.json", "{}")
|
|
46
101
|
hooks_document = Document.from_text("hooks/hooks.json", "{}")
|
|
@@ -233,6 +288,68 @@ def test_registry_registers_unique_python_validator_rule() -> None:
|
|
|
233
288
|
assert not validator_rule.accepts(Document.from_text("module.ts", _FUNCTION_SOURCE))
|
|
234
289
|
|
|
235
290
|
|
|
291
|
+
def test_registry_registers_terminology_change_set_rule() -> None:
|
|
292
|
+
terminology_rule = _terminology_rule()
|
|
293
|
+
assert terminology_rule.rule_sets == _CHANGED_RULE_SETS
|
|
294
|
+
assert terminology_rule.selections == frozenset({SelectionKind.STAGED})
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_registry_runs_test_pairing_for_staged_and_base_changes() -> None:
|
|
298
|
+
test_pairing_rule = next(
|
|
299
|
+
each_rule
|
|
300
|
+
for each_rule in registry.default_registry()
|
|
301
|
+
if each_rule.rule_id == "test-pairing"
|
|
302
|
+
)
|
|
303
|
+
assert isinstance(test_pairing_rule, ChangeSetRule)
|
|
304
|
+
assert test_pairing_rule.selections == frozenset(
|
|
305
|
+
{SelectionKind.STAGED, SelectionKind.BASE}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_terminology_rule_reports_cross_surface_path_and_line(
|
|
310
|
+
tmp_path: Path,
|
|
311
|
+
) -> None:
|
|
312
|
+
_stage_terminology_change(
|
|
313
|
+
tmp_path,
|
|
314
|
+
"baseline = 1\n",
|
|
315
|
+
"premium_request_interactions = 5\n",
|
|
316
|
+
"Quota fields use one name.\n"
|
|
317
|
+
"The premium-request-budget field gates the run.\n",
|
|
318
|
+
)
|
|
319
|
+
lint_report = lint(LintRequest.staged(tmp_path), all_registry=(_terminology_rule(),))
|
|
320
|
+
|
|
321
|
+
assert len(lint_report.diagnostics) == 1
|
|
322
|
+
diagnostic = lint_report.diagnostics[0]
|
|
323
|
+
assert diagnostic.rule_id == "terminology-sweep"
|
|
324
|
+
assert diagnostic.location is not None
|
|
325
|
+
assert diagnostic.location.path == PurePosixPath("docs/README.md")
|
|
326
|
+
assert diagnostic.location.start_line == 2
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_terminology_rule_suppresses_identifier_present_in_prior_tree(
|
|
330
|
+
tmp_path: Path,
|
|
331
|
+
) -> None:
|
|
332
|
+
_stage_terminology_change(
|
|
333
|
+
tmp_path,
|
|
334
|
+
"premium_request_interactions = 5\n",
|
|
335
|
+
"premium_request_interactions = 5\n"
|
|
336
|
+
"premium_request_interactions = 6\n",
|
|
337
|
+
"The premium-request-budget field gates the run.\n",
|
|
338
|
+
)
|
|
339
|
+
lint_report = lint(LintRequest.staged(tmp_path), all_registry=(_terminology_rule(),))
|
|
340
|
+
|
|
341
|
+
assert lint_report.diagnostics == ()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_terminology_rule_skips_file_selection(tmp_path: Path) -> None:
|
|
345
|
+
document_set = DocumentSet(
|
|
346
|
+
(Document.from_text("docs/README.md", "text\n"),),
|
|
347
|
+
SelectionKind.FILES,
|
|
348
|
+
tmp_path,
|
|
349
|
+
)
|
|
350
|
+
assert adapters.terminology_diagnostics(document_set) == ()
|
|
351
|
+
|
|
352
|
+
|
|
236
353
|
def test_lint_keeps_skipped_rules_out_of_executed_rules(tmp_path: Path) -> None:
|
|
237
354
|
skipped_rule = DocumentRule(
|
|
238
355
|
"skipped",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Behavior tests for committed pytest testpaths."""
|
|
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_PYTEST_TESTPATHS,
|
|
16
|
+
FAILED_CHECK_EXIT_CODE,
|
|
17
|
+
FINDINGS_EXIT_CODE,
|
|
18
|
+
)
|
|
19
|
+
from repository_policy_test_support import (
|
|
20
|
+
commit_tracked_files,
|
|
21
|
+
initialize_repository,
|
|
22
|
+
patch_unreadable_named_file,
|
|
23
|
+
run_policy,
|
|
24
|
+
write_text,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_should_flag_a_pytest_testpath_discovery_miss(tmp_path: Path) -> None:
|
|
29
|
+
repository_root = tmp_path / "repo"
|
|
30
|
+
initialize_repository(repository_root)
|
|
31
|
+
package_root = repository_root / "shared_utils"
|
|
32
|
+
_write_pytest_configuration(package_root)
|
|
33
|
+
write_text(
|
|
34
|
+
package_root / "theme_assets" / "tests" / "test_palette.py",
|
|
35
|
+
"def test_palette() -> None:\n assert True\n",
|
|
36
|
+
)
|
|
37
|
+
commit_tracked_files(repository_root)
|
|
38
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
39
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
40
|
+
assert CHECK_ID_PYTEST_TESTPATHS in stdout_text
|
|
41
|
+
assert "shared_utils/theme_assets/tests/test_palette.py" in stdout_text
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_should_fail_closed_when_pytest_config_cannot_be_read(
|
|
45
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
46
|
+
) -> None:
|
|
47
|
+
repository_root = tmp_path / "repo"
|
|
48
|
+
initialize_repository(repository_root)
|
|
49
|
+
_write_pytest_configuration(repository_root / "shared_utils")
|
|
50
|
+
commit_tracked_files(repository_root)
|
|
51
|
+
patch_unreadable_named_file(monkeypatch, "pyproject.toml", "pyproject unreadable")
|
|
52
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
53
|
+
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
54
|
+
assert CHECK_ID_PYTEST_TESTPATHS in stdout_text
|
|
55
|
+
assert "error: rule failed:" in stdout_text
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _write_pytest_configuration(package_root: Path) -> None:
|
|
59
|
+
write_text(
|
|
60
|
+
package_root / "pyproject.toml",
|
|
61
|
+
'[tool.pytest.ini_options]\ntestpaths = ["tests"]\n',
|
|
62
|
+
)
|
|
63
|
+
write_text(
|
|
64
|
+
package_root / "tests" / "test_root_behavior.py",
|
|
65
|
+
"def test_ok() -> None:\n assert True\n",
|
|
66
|
+
)
|