claude-dev-env 2.36.0 → 4.1.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/syncing-submodules/SKILL.md +101 -0
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
- package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
- package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -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/ever-shipped-skills.mjs +1 -0
- package/bin/ever-shipped-skills.test.mjs +30 -0
- package/bin/git_hooks_installer.mjs +0 -1
- package/bin/git_hooks_installer.test.mjs +16 -1
- package/bin/install.mjs +5 -2
- package/bin/install.prune.test.mjs +36 -24
- package/bin/install.test.mjs +7 -0
- 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
- package/hooks/git-hooks/post_commit.py +0 -241
- package/hooks/git-hooks/test_post_commit.py +0 -298
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Behavior tests for repository-check report rendering."""
|
|
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
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
10
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
12
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
14
|
+
|
|
15
|
+
from repository_checks.models import RepositoryCheckReport, RepositoryFinding
|
|
16
|
+
from repository_checks.reporting import render_report
|
|
17
|
+
from repository_policy_test_support import (
|
|
18
|
+
commit_tracked_files,
|
|
19
|
+
initialize_repository,
|
|
20
|
+
run_policy,
|
|
21
|
+
write_text,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_should_render_findings_and_failed_checks_in_report_order() -> None:
|
|
26
|
+
report = RepositoryCheckReport(
|
|
27
|
+
(RepositoryFinding("check", "relative/file.py", "message"),),
|
|
28
|
+
("failed-check",),
|
|
29
|
+
)
|
|
30
|
+
assert render_report(report) == (
|
|
31
|
+
"check: relative/file.py: message\nerror: rule failed: failed-check\n"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_should_render_an_empty_clean_report() -> None:
|
|
36
|
+
assert render_report(RepositoryCheckReport((), ())) == ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_should_print_repository_relative_paths_only(tmp_path: Path) -> None:
|
|
40
|
+
repository_root = tmp_path / "repo"
|
|
41
|
+
initialize_repository(repository_root)
|
|
42
|
+
write_text(
|
|
43
|
+
repository_root / "notes" / "CLAUDE.md",
|
|
44
|
+
"# notes\n\n"
|
|
45
|
+
"| File | Role |\n"
|
|
46
|
+
"|---|---|\n"
|
|
47
|
+
"| `absent_helper.py` | Missing helper |\n",
|
|
48
|
+
)
|
|
49
|
+
commit_tracked_files(repository_root)
|
|
50
|
+
_exit_code, stdout_text, stderr_text = run_policy(repository_root)
|
|
51
|
+
absolute_root_text = str(repository_root.resolve())
|
|
52
|
+
assert absolute_root_text not in stdout_text
|
|
53
|
+
assert absolute_root_text not in stderr_text
|
|
54
|
+
assert "notes/CLAUDE.md" in stdout_text
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Command behavior for the committed-tree repository checker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
10
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
11
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
13
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
15
|
+
|
|
16
|
+
import repository_policy
|
|
17
|
+
from repository_checks.config.constants import (
|
|
18
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
19
|
+
CHECK_ID_ENV_VAR_DOCUMENTATION,
|
|
20
|
+
CHECK_ID_PACKAGE_INVENTORY,
|
|
21
|
+
CHECK_ID_PYTEST_TESTPATHS,
|
|
22
|
+
CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
23
|
+
)
|
|
24
|
+
from repository_policy_test_support import run_policy, seed_clean_repository
|
|
25
|
+
|
|
26
|
+
_POLICY_SCRIPT_PATH = _SCRIPTS_DIRECTORY / "repository_policy.py"
|
|
27
|
+
_CHECK_SCRIPT_PATH = _SCRIPTS_DIRECTORY / "check.ps1"
|
|
28
|
+
_UTF8_ENCODING = "utf-8"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_should_exit_zero_on_a_clean_tracked_tree(tmp_path: Path) -> None:
|
|
32
|
+
repository_root = seed_clean_repository(tmp_path / "repo")
|
|
33
|
+
exit_code, stdout_text, stderr_text = run_policy(repository_root)
|
|
34
|
+
assert exit_code == 0
|
|
35
|
+
assert stdout_text == ""
|
|
36
|
+
assert stderr_text == ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_should_keep_stable_check_identifiers() -> None:
|
|
40
|
+
assert repository_policy.ALL_CHECK_IDS == (
|
|
41
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
42
|
+
CHECK_ID_ENV_VAR_DOCUMENTATION,
|
|
43
|
+
CHECK_ID_PACKAGE_INVENTORY,
|
|
44
|
+
CHECK_ID_PYTEST_TESTPATHS,
|
|
45
|
+
CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_should_invoke_the_checker_from_check_script() -> None:
|
|
50
|
+
script_text = _CHECK_SCRIPT_PATH.read_text(encoding=_UTF8_ENCODING)
|
|
51
|
+
assert "Invoke-Tool -Label 'repository-policy'" in script_text
|
|
52
|
+
assert "repository_policy.py" in script_text
|
|
53
|
+
assert "--repository-root" in script_text
|
|
54
|
+
assert "SkipRepositoryPolicy" in script_text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_should_run_the_checker_as_a_subprocess(tmp_path: Path) -> None:
|
|
58
|
+
repository_root = seed_clean_repository(tmp_path / "repo")
|
|
59
|
+
completed = subprocess.run(
|
|
60
|
+
[
|
|
61
|
+
sys.executable,
|
|
62
|
+
str(_POLICY_SCRIPT_PATH),
|
|
63
|
+
"--repository-root",
|
|
64
|
+
str(repository_root),
|
|
65
|
+
],
|
|
66
|
+
check=False,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
)
|
|
70
|
+
assert completed.returncode == 0
|
|
71
|
+
assert completed.stdout == ""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Behavior tests for repository-check orchestration."""
|
|
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
|
+
FAILED_CHECK_EXIT_CODE,
|
|
16
|
+
FINDINGS_EXIT_CODE,
|
|
17
|
+
)
|
|
18
|
+
from repository_policy_test_support import (
|
|
19
|
+
commit_tracked_files,
|
|
20
|
+
initialize_repository,
|
|
21
|
+
run_policy,
|
|
22
|
+
seed_clean_repository,
|
|
23
|
+
write_text,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_should_fail_closed_when_tracked_paths_cannot_be_read(
|
|
28
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
29
|
+
) -> None:
|
|
30
|
+
repository_root = seed_clean_repository(tmp_path / "repo")
|
|
31
|
+
|
|
32
|
+
def fail_tracked_paths(_repository_root: Path) -> tuple[str, ...]:
|
|
33
|
+
raise OSError("git ls-files failed")
|
|
34
|
+
|
|
35
|
+
monkeypatch.setattr(
|
|
36
|
+
"repository_checks.tracked_tree.tracked_relative_paths", fail_tracked_paths
|
|
37
|
+
)
|
|
38
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
39
|
+
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
40
|
+
assert "error: rule failed:" in stdout_text
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_should_emit_stable_sorted_findings(tmp_path: Path) -> None:
|
|
44
|
+
repository_root = tmp_path / "repo"
|
|
45
|
+
initialize_repository(repository_root)
|
|
46
|
+
write_text(
|
|
47
|
+
repository_root / "zeta" / "CLAUDE.md",
|
|
48
|
+
"# zeta\n\n| File | Role |\n|---|---|\n| `missing_zeta.py` | Missing |\n",
|
|
49
|
+
)
|
|
50
|
+
write_text(
|
|
51
|
+
repository_root / "alpha" / "CLAUDE.md",
|
|
52
|
+
"# alpha\n\n| File | Role |\n|---|---|\n| `missing_alpha.py` | Missing |\n",
|
|
53
|
+
)
|
|
54
|
+
commit_tracked_files(repository_root)
|
|
55
|
+
first_exit_code, first_stdout, _first_stderr = run_policy(repository_root)
|
|
56
|
+
_second_exit_code, second_stdout, _second_stderr = run_policy(repository_root)
|
|
57
|
+
assert first_exit_code == FINDINGS_EXIT_CODE
|
|
58
|
+
assert first_stdout == second_stdout
|
|
59
|
+
assert first_stdout.index("alpha/CLAUDE.md") < first_stdout.index("zeta/CLAUDE.md")
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Behavior tests for tracked-secret checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from repository_checks.config.constants import (
|
|
11
|
+
ALL_TRACKED_SECRET_EXACT_EXEMPTIONS,
|
|
12
|
+
CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
13
|
+
FINDINGS_EXIT_CODE,
|
|
14
|
+
)
|
|
15
|
+
from repository_checks.hook_modules import load_hooks_module
|
|
16
|
+
from repository_checks.tracked_secrets import collect_tracked_secret_findings
|
|
17
|
+
|
|
18
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
19
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
21
|
+
|
|
22
|
+
from repository_policy_test_support import (
|
|
23
|
+
commit_tracked_files,
|
|
24
|
+
initialize_repository,
|
|
25
|
+
run_policy,
|
|
26
|
+
write_text,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_SYNTHETIC_GITHUB_TOKEN = "ghp_" + ("A" * 36)
|
|
30
|
+
_UTF8_ENCODING = "utf-8"
|
|
31
|
+
_REPOSITORY_ROOT = Path(__file__).resolve().parents[4]
|
|
32
|
+
_EXEMPT_FIXTURE_EMAIL = "fixture-owner@company.io"
|
|
33
|
+
_OTHER_FIXTURE_EMAIL = "other-owner@company.io"
|
|
34
|
+
_OTHER_FIXTURE_HOME_PATH = "C:/Users/realname/notes.txt"
|
|
35
|
+
_FIXTURE_NOTES_RELATIVE_PATH = "src/notes.py"
|
|
36
|
+
_OTHER_NOTES_RELATIVE_PATH = "src/other_notes.py"
|
|
37
|
+
_PII_PREVENTION_CONSTANTS = load_hooks_module(
|
|
38
|
+
"hooks_constants.pii_prevention_constants"
|
|
39
|
+
)
|
|
40
|
+
_PII_CATEGORY_EMAIL = _PII_PREVENTION_CONSTANTS.CATEGORY_EMAIL
|
|
41
|
+
_PII_CATEGORY_HOME_PATH = _PII_PREVENTION_CONSTANTS.CATEGORY_HOME_PATH
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_should_flag_a_tracked_secret(tmp_path: Path) -> None:
|
|
45
|
+
repository_root = tmp_path / "repo"
|
|
46
|
+
initialize_repository(repository_root)
|
|
47
|
+
write_text(
|
|
48
|
+
repository_root / "src" / "app.py", f"token = '{_SYNTHETIC_GITHUB_TOKEN}'\n"
|
|
49
|
+
)
|
|
50
|
+
commit_tracked_files(repository_root)
|
|
51
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
52
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
53
|
+
assert CHECK_ID_TRACKED_PERSONAL_DATA in stdout_text
|
|
54
|
+
assert "src/app.py" in stdout_text
|
|
55
|
+
assert _SYNTHETIC_GITHUB_TOKEN not in stdout_text
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_should_fail_closed_before_reading_a_tracked_symlink_outside_repository(
|
|
59
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
60
|
+
) -> None:
|
|
61
|
+
repository_root = tmp_path / "repo"
|
|
62
|
+
repository_root.mkdir()
|
|
63
|
+
outside_path = tmp_path / "outside.py"
|
|
64
|
+
write_text(outside_path, f"token = '{_SYNTHETIC_GITHUB_TOKEN}'\n")
|
|
65
|
+
tracked_symlink = repository_root / "linked.py"
|
|
66
|
+
tracked_symlink.symlink_to(outside_path)
|
|
67
|
+
original_read_text = Path.read_text
|
|
68
|
+
all_read_paths: list[Path] = []
|
|
69
|
+
|
|
70
|
+
def record_read_path(
|
|
71
|
+
file_path: Path,
|
|
72
|
+
encoding: str | None = None,
|
|
73
|
+
errors: str | None = None,
|
|
74
|
+
) -> str:
|
|
75
|
+
all_read_paths.append(file_path)
|
|
76
|
+
return original_read_text(file_path, encoding=encoding, errors=errors)
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(Path, "read_text", record_read_path)
|
|
79
|
+
with pytest.raises(OSError):
|
|
80
|
+
collect_tracked_secret_findings(repository_root, ["linked.py"])
|
|
81
|
+
assert tracked_symlink not in all_read_paths
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_should_flag_a_different_email_in_the_same_exact_exempted_file(
|
|
85
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
86
|
+
) -> None:
|
|
87
|
+
repository_root = tmp_path / "repo"
|
|
88
|
+
initialize_repository(repository_root)
|
|
89
|
+
_write_exact_exemption_fixture(repository_root)
|
|
90
|
+
commit_tracked_files(repository_root)
|
|
91
|
+
_install_exact_email_exemption(monkeypatch)
|
|
92
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
93
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
94
|
+
assert _pii_categories_for_path(stdout_text, _FIXTURE_NOTES_RELATIVE_PATH) == [
|
|
95
|
+
_PII_CATEGORY_EMAIL,
|
|
96
|
+
_PII_CATEGORY_HOME_PATH,
|
|
97
|
+
_PII_PREVENTION_CONSTANTS.CATEGORY_SECRET,
|
|
98
|
+
]
|
|
99
|
+
assert _EXEMPT_FIXTURE_EMAIL not in stdout_text
|
|
100
|
+
assert _SYNTHETIC_GITHUB_TOKEN not in stdout_text
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_should_flag_an_exact_exempt_value_at_a_different_path(
|
|
104
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
105
|
+
) -> None:
|
|
106
|
+
repository_root = tmp_path / "repo"
|
|
107
|
+
initialize_repository(repository_root)
|
|
108
|
+
_write_exact_exemption_fixture(repository_root)
|
|
109
|
+
commit_tracked_files(repository_root)
|
|
110
|
+
_install_exact_email_exemption(monkeypatch)
|
|
111
|
+
_exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
112
|
+
assert _PII_CATEGORY_EMAIL in _pii_categories_for_path(
|
|
113
|
+
stdout_text, _OTHER_NOTES_RELATIVE_PATH
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_should_store_tracked_secret_exemptions_as_digests() -> None:
|
|
118
|
+
sha256_hex_digest_length = hashlib.sha256(b"").digest_size * 2
|
|
119
|
+
assert ALL_TRACKED_SECRET_EXACT_EXEMPTIONS
|
|
120
|
+
for _relative_path, _category, each_digest in ALL_TRACKED_SECRET_EXACT_EXEMPTIONS:
|
|
121
|
+
assert len(each_digest) == sha256_hex_digest_length
|
|
122
|
+
bytes.fromhex(each_digest)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_should_keep_owned_checker_modules_free_of_tracked_secret_findings() -> None:
|
|
126
|
+
checker_directory = (
|
|
127
|
+
_REPOSITORY_ROOT
|
|
128
|
+
/ "packages"
|
|
129
|
+
/ "claude-dev-env"
|
|
130
|
+
/ "scripts"
|
|
131
|
+
/ "repository_checks"
|
|
132
|
+
)
|
|
133
|
+
all_relative_paths = [
|
|
134
|
+
each_path.relative_to(_REPOSITORY_ROOT).as_posix()
|
|
135
|
+
for each_path in checker_directory.rglob("*.py")
|
|
136
|
+
if each_path.is_file()
|
|
137
|
+
]
|
|
138
|
+
assert collect_tracked_secret_findings(_REPOSITORY_ROOT, all_relative_paths) == []
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _write_exact_exemption_fixture(repository_root: Path) -> None:
|
|
142
|
+
write_text(
|
|
143
|
+
repository_root / _FIXTURE_NOTES_RELATIVE_PATH,
|
|
144
|
+
f"contact = '{_EXEMPT_FIXTURE_EMAIL}'\nalt = '{_OTHER_FIXTURE_EMAIL}'\nhome = '{_OTHER_FIXTURE_HOME_PATH}'\ntoken = '{_SYNTHETIC_GITHUB_TOKEN}'\n",
|
|
145
|
+
)
|
|
146
|
+
write_text(
|
|
147
|
+
repository_root / _OTHER_NOTES_RELATIVE_PATH,
|
|
148
|
+
f"contact = '{_EXEMPT_FIXTURE_EMAIL}'\n",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _pii_categories_for_path(stdout_text: str, relative_path: str) -> list[str]:
|
|
153
|
+
finding_prefix = f"{CHECK_ID_TRACKED_PERSONAL_DATA}: {relative_path}:"
|
|
154
|
+
return sorted(
|
|
155
|
+
each_line.split("[", 1)[1].split("]", 1)[0]
|
|
156
|
+
for each_line in stdout_text.splitlines()
|
|
157
|
+
if each_line.startswith(finding_prefix)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _install_exact_email_exemption(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
162
|
+
monkeypatch.setattr(
|
|
163
|
+
"repository_checks.tracked_secrets.repository_constants.ALL_TRACKED_SECRET_EXACT_EXEMPTIONS",
|
|
164
|
+
frozenset(
|
|
165
|
+
{
|
|
166
|
+
(
|
|
167
|
+
_FIXTURE_NOTES_RELATIVE_PATH,
|
|
168
|
+
_PII_CATEGORY_EMAIL,
|
|
169
|
+
hashlib.sha256(
|
|
170
|
+
_EXEMPT_FIXTURE_EMAIL.encode(_UTF8_ENCODING)
|
|
171
|
+
).hexdigest(),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Behavior tests for tracked repository path discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_TESTS_DIRECTORY = Path(__file__).resolve().parent
|
|
9
|
+
if str(_TESTS_DIRECTORY) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(_TESTS_DIRECTORY))
|
|
11
|
+
|
|
12
|
+
from repository_checks.tracked_tree import tracked_relative_paths
|
|
13
|
+
from repository_policy_test_support import run_policy, seed_clean_repository, write_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_should_list_only_committed_relative_paths(tmp_path: Path) -> None:
|
|
17
|
+
repository_root = seed_clean_repository(tmp_path / "repo")
|
|
18
|
+
write_text(repository_root / "untracked.txt", "not committed\n")
|
|
19
|
+
assert tracked_relative_paths(repository_root) == ("README.md",)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_should_skip_a_tracked_path_absent_from_the_worktree(tmp_path: Path) -> None:
|
|
23
|
+
repository_root = seed_clean_repository(tmp_path / "repo")
|
|
24
|
+
(repository_root / "README.md").unlink()
|
|
25
|
+
exit_code, stdout_text, stderr_text = run_policy(repository_root)
|
|
26
|
+
assert exit_code == 0
|
|
27
|
+
assert stdout_text == ""
|
|
28
|
+
assert stderr_text == ""
|