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,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
|
-
|
|
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
|
+
)
|