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.
Files changed (84) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/syncing-submodules/SKILL.md +101 -0
  3. package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
  4. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
  5. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
  6. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
  8. package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
  10. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -0
  11. package/.agents/skills/team-advisor/SKILL.md +11 -11
  12. package/_shared/advisor/advisor-protocol.md +51 -51
  13. package/_shared/advisor/reference/advisor-block.md +9 -9
  14. package/_shared/advisor/reference/astra-rung.md +39 -0
  15. package/_shared/advisor/reference/identity.md +4 -4
  16. package/_shared/advisor/reference/lifecycle.md +6 -6
  17. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  18. package/_shared/advisor/reference/third-party-bind.md +11 -11
  19. package/_shared/advisor/reference/warm-up.md +13 -13
  20. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  21. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  22. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  23. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  24. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  25. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  26. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  27. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  28. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  29. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  30. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  31. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  33. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  34. package/bin/ever-shipped-skills.mjs +1 -0
  35. package/bin/ever-shipped-skills.test.mjs +30 -0
  36. package/bin/git_hooks_installer.mjs +0 -1
  37. package/bin/git_hooks_installer.test.mjs +16 -1
  38. package/bin/install.mjs +5 -2
  39. package/bin/install.prune.test.mjs +36 -24
  40. package/bin/install.test.mjs +7 -0
  41. package/docs/references/team-advisor-skill.md +3 -3
  42. package/hooks/validators/README.md +35 -1
  43. package/package.json +1 -1
  44. package/scripts/check.ps1 +18 -6
  45. package/scripts/policy_lint/adapter_detectors.py +42 -2
  46. package/scripts/policy_lint/adapters.py +24 -0
  47. package/scripts/policy_lint/config/constants.py +11 -0
  48. package/scripts/policy_lint/engine.py +1 -1
  49. package/scripts/policy_lint/model.py +1 -0
  50. package/scripts/policy_lint/registry.py +10 -1
  51. package/scripts/repository_checks/__init__.py +1 -0
  52. package/scripts/repository_checks/claude_md.py +108 -0
  53. package/scripts/repository_checks/config/__init__.py +1 -0
  54. package/scripts/repository_checks/config/constants.py +150 -0
  55. package/scripts/repository_checks/env_var_documentation.py +243 -0
  56. package/scripts/repository_checks/hook_modules.py +34 -0
  57. package/scripts/repository_checks/models.py +37 -0
  58. package/scripts/repository_checks/package_inventory.py +118 -0
  59. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  60. package/scripts/repository_checks/reporting.py +39 -0
  61. package/scripts/repository_checks/runner.py +93 -0
  62. package/scripts/repository_checks/tracked_secrets.py +147 -0
  63. package/scripts/repository_checks/tracked_tree.py +28 -0
  64. package/scripts/repository_policy.py +120 -0
  65. package/scripts/tests/repository_policy_test_support.py +89 -0
  66. package/scripts/tests/test_cde_lint.py +25 -1
  67. package/scripts/tests/test_claude_md.py +68 -0
  68. package/scripts/tests/test_env_var_documentation.py +133 -0
  69. package/scripts/tests/test_hook_modules.py +17 -0
  70. package/scripts/tests/test_models.py +26 -0
  71. package/scripts/tests/test_package_inventory.py +96 -0
  72. package/scripts/tests/test_policy_lint_rules_registry.py +117 -0
  73. package/scripts/tests/test_pytest_testpaths.py +66 -0
  74. package/scripts/tests/test_reporting.py +54 -0
  75. package/scripts/tests/test_repository_policy.py +71 -0
  76. package/scripts/tests/test_runner.py +59 -0
  77. package/scripts/tests/test_tracked_secrets.py +175 -0
  78. package/scripts/tests/test_tracked_tree.py +28 -0
  79. package/_shared/advisor/reference/sol-rung.md +0 -39
  80. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  81. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  82. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
  83. package/hooks/git-hooks/post_commit.py +0 -241
  84. 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 == ""