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,147 @@
1
+ """Tracked-secret checks for committed trees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+ from types import ModuleType
9
+
10
+ from policy_lint.config import constants as policy_constants
11
+
12
+ from repository_checks.config import constants as repository_constants
13
+ from repository_checks.hook_modules import load_hooks_module
14
+ from repository_checks.models import RepositoryFinding
15
+
16
+
17
+ def collect_tracked_secret_findings(
18
+ repository_root: Path, all_tracked_paths: Sequence[str]
19
+ ) -> list[RepositoryFinding]:
20
+ """Return non-exempt secrets found in tracked UTF-8 files.
21
+
22
+ Args:
23
+ repository_root: Git repository root.
24
+ all_tracked_paths: Repository-relative tracked paths.
25
+
26
+ Returns:
27
+ Findings with redacted previews for non-exempt secrets.
28
+ """
29
+ scanner = load_hooks_module(repository_constants.PII_SCANNER_MODULE_NAME)
30
+ exemption = load_hooks_module(repository_constants.REPOSITORY_EXEMPTION_MODULE_NAME)
31
+ all_allowlisted_literals = exemption.repository_allowlisted_values(repository_root)
32
+ all_findings: list[RepositoryFinding] = []
33
+ for each_relative_path in all_tracked_paths:
34
+ all_findings.extend(
35
+ _find_secrets_for_path(
36
+ repository_root,
37
+ each_relative_path,
38
+ scanner,
39
+ all_allowlisted_literals,
40
+ )
41
+ )
42
+ return all_findings
43
+
44
+
45
+ def _find_secrets_for_path(
46
+ repository_root: Path,
47
+ relative_path: str,
48
+ scanner: ModuleType,
49
+ all_allowlisted_literals: frozenset[str],
50
+ ) -> list[RepositoryFinding]:
51
+ posix_relative_path = relative_path.replace(
52
+ repository_constants.WINDOWS_PATH_SEPARATOR,
53
+ policy_constants.PATH_SEPARATOR,
54
+ )
55
+ if scanner.is_path_exempt_from_pii_scan(posix_relative_path):
56
+ return []
57
+ maybe_content = _read_utf8_text(
58
+ repository_root,
59
+ repository_root / relative_path,
60
+ )
61
+ if maybe_content is None:
62
+ return []
63
+ return _find_secret_matches(
64
+ posix_relative_path,
65
+ maybe_content,
66
+ scanner,
67
+ all_allowlisted_literals,
68
+ )
69
+
70
+
71
+ def _find_secret_matches(
72
+ posix_relative_path: str,
73
+ content: str,
74
+ scanner: ModuleType,
75
+ all_allowlisted_literals: frozenset[str],
76
+ ) -> list[RepositoryFinding]:
77
+ return [
78
+ _build_finding(
79
+ posix_relative_path,
80
+ category=each_match.category,
81
+ preview=each_match.preview,
82
+ )
83
+ for each_match in scanner.scan_text_for_pii(content)
84
+ if _should_report_match(
85
+ posix_relative_path,
86
+ each_match.category,
87
+ each_match.matched_text,
88
+ all_allowlisted_literals,
89
+ )
90
+ ]
91
+
92
+
93
+ def _read_utf8_text(repository_root: Path, absolute_path: Path) -> str | None:
94
+ if not absolute_path.is_file():
95
+ return None
96
+ _require_path_inside_repository(repository_root, absolute_path)
97
+ try:
98
+ return absolute_path.read_text(encoding=policy_constants.UTF8_ENCODING)
99
+ except UnicodeDecodeError:
100
+ return None
101
+
102
+
103
+ def _require_path_inside_repository(repository_root: Path, absolute_path: Path) -> None:
104
+ resolved_repository_root = repository_root.resolve(strict=True)
105
+ resolved_absolute_path = absolute_path.resolve(strict=True)
106
+ try:
107
+ resolved_absolute_path.relative_to(resolved_repository_root)
108
+ except ValueError as e:
109
+ raise OSError(absolute_path) from e
110
+
111
+
112
+ def _build_finding(
113
+ posix_relative_path: str, category: str, preview: str
114
+ ) -> RepositoryFinding:
115
+ return RepositoryFinding(
116
+ repository_constants.CHECK_ID_TRACKED_PERSONAL_DATA,
117
+ posix_relative_path,
118
+ repository_constants.TRACKED_MATCH_MESSAGE_TEMPLATE.format(
119
+ category=category,
120
+ preview=preview,
121
+ ),
122
+ )
123
+
124
+
125
+ def _should_report_match(
126
+ posix_relative_path: str,
127
+ category: str,
128
+ matched_text: str,
129
+ all_allowlisted_literals: frozenset[str],
130
+ ) -> bool:
131
+ exact_exemption_identity = (
132
+ posix_relative_path,
133
+ category,
134
+ _secret_digest(matched_text),
135
+ )
136
+ if (
137
+ exact_exemption_identity
138
+ in repository_constants.ALL_TRACKED_SECRET_EXACT_EXEMPTIONS
139
+ ):
140
+ return False
141
+ return matched_text not in all_allowlisted_literals
142
+
143
+
144
+ def _secret_digest(matched_text: str) -> str:
145
+ return hashlib.sha256(
146
+ matched_text.encode(policy_constants.UTF8_ENCODING)
147
+ ).hexdigest()
@@ -0,0 +1,28 @@
1
+ """List tracked worktree paths for the committed-tree checker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from policy_lint.selection_git import git_bytes_for, split_nul_tokens
8
+
9
+ from repository_checks.config.constants import ALL_GIT_LS_FILES_ARGUMENTS
10
+
11
+
12
+ def tracked_relative_paths(repository_root: Path) -> tuple[str, ...]:
13
+ """Return tracked paths as repository-relative posix strings.
14
+
15
+ ::
16
+
17
+ git ls-files -z -> ("notes/CLAUDE.md", "src/app.py")
18
+ ok: paths are relative
19
+ flag: an absolute Windows path
20
+
21
+ Args:
22
+ repository_root: Git repository root.
23
+
24
+ Returns:
25
+ Tracked relative paths in Git order.
26
+ """
27
+ raw_paths = git_bytes_for(repository_root, ALL_GIT_LS_FILES_ARGUMENTS)
28
+ return split_nul_tokens(raw_paths)
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """Public command for the committed-tree repository checker."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ from collections.abc import Sequence
9
+ from pathlib import Path
10
+ from typing import Never, TextIO
11
+
12
+ from policy_lint.config.constants import ALL_GIT_ROOT_ARGUMENTS, UTF8_ENCODING
13
+ from policy_lint.selection_git import GitSelectionError, git_bytes_for
14
+ from repository_checks.config.constants import (
15
+ ALL_CHECK_IDS,
16
+ REPOSITORY_ROOT_FLAG,
17
+ USAGE_EXIT_CODE,
18
+ )
19
+ from repository_checks.reporting import render_report
20
+ from repository_checks.runner import run_repository_checks
21
+
22
+ __all__ = ["ALL_CHECK_IDS", "main"]
23
+
24
+
25
+ class _RepositoryPolicyUsageError(ValueError):
26
+ """Raised when command arguments cannot form a repository scan."""
27
+
28
+
29
+ class _RepositoryPolicyArgumentParser(argparse.ArgumentParser):
30
+ def error(self, message: str) -> Never:
31
+ """Raise a usage error for the caller.
32
+
33
+ Args:
34
+ message: Parser error text.
35
+
36
+ Raises:
37
+ _RepositoryPolicyUsageError: Always.
38
+ """
39
+ raise _RepositoryPolicyUsageError(message)
40
+
41
+
42
+ def _discover_repository_root(starting_directory: Path) -> Path:
43
+ """Find the Git root that contains the command's current directory.
44
+
45
+ Args:
46
+ starting_directory: Directory where the command started.
47
+
48
+ Returns:
49
+ The resolved Git repository root.
50
+
51
+ Raises:
52
+ _RepositoryPolicyUsageError: If the directory is not inside a Git repository.
53
+ """
54
+ try:
55
+ root_bytes = git_bytes_for(starting_directory.resolve(), ALL_GIT_ROOT_ARGUMENTS)
56
+ except (GitSelectionError, OSError) as error:
57
+ raise _RepositoryPolicyUsageError(
58
+ "Current directory is not inside a Git repository"
59
+ ) from error
60
+ root_text = root_bytes.decode(UTF8_ENCODING).strip()
61
+ if not root_text:
62
+ raise _RepositoryPolicyUsageError("Git returned an empty repository root")
63
+ return Path(root_text).resolve()
64
+
65
+
66
+ def _build_parser() -> argparse.ArgumentParser:
67
+ parser = _RepositoryPolicyArgumentParser(prog="repository_policy")
68
+ parser.add_argument(
69
+ REPOSITORY_ROOT_FLAG,
70
+ dest="repository_root",
71
+ default=None,
72
+ help="Git repository root to scan.",
73
+ )
74
+ return parser
75
+
76
+
77
+ def main(
78
+ all_arguments: Sequence[str],
79
+ *,
80
+ repository_root: Path,
81
+ stdout: TextIO = sys.stdout,
82
+ stderr: TextIO = sys.stderr,
83
+ ) -> int:
84
+ """Run committed-tree repository checks.
85
+
86
+ Args:
87
+ all_arguments: Command arguments.
88
+ repository_root: Repository root used when the flag is omitted.
89
+ stdout: Output stream.
90
+ stderr: Error stream.
91
+
92
+ Returns:
93
+ Zero when the tree is clean, one for findings, two for usage errors,
94
+ and three when a check fails closed.
95
+ """
96
+ try:
97
+ namespace = _build_parser().parse_args(list(all_arguments))
98
+ except _RepositoryPolicyUsageError as usage_error:
99
+ stderr.write(f"{usage_error}\n")
100
+ return USAGE_EXIT_CODE
101
+ selected_root = repository_root
102
+ if namespace.repository_root is not None:
103
+ selected_root = Path(namespace.repository_root).resolve()
104
+ report = run_repository_checks(selected_root)
105
+ stdout.write(render_report(report))
106
+ return report.exit_code
107
+
108
+
109
+ if __name__ == "__main__":
110
+ try:
111
+ shell_directory = Path.cwd()
112
+ parsed_namespace = _build_parser().parse_args(sys.argv[1:])
113
+ if parsed_namespace.repository_root is not None:
114
+ shell_repository_root = Path(parsed_namespace.repository_root).resolve()
115
+ else:
116
+ shell_repository_root = _discover_repository_root(shell_directory)
117
+ except _RepositoryPolicyUsageError as usage_error:
118
+ sys.stderr.write(f"{usage_error}\n")
119
+ raise SystemExit(USAGE_EXIT_CODE) from usage_error
120
+ raise SystemExit(main(sys.argv[1:], repository_root=shell_repository_root))
@@ -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
- assert json.loads(json_stdout)["diagnostics"] == []
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
+ )