claude-dev-env 2.35.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -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/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/README.md +35 -1
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +368 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +376 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +388 -0
- package/scripts/policy_lint/registry.py +171 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- 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/select_pstack_models.mjs +304 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +398 -0
- 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_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
- package/scripts/tests/test_policy_lint_selection.py +373 -0
- package/scripts/tests/test_pytest_testpaths.py +66 -0
- package/scripts/tests/test_reporting.py +54 -0
- package/scripts/tests/test_repository_policy.py +71 -0
- package/scripts/tests/test_runner.py +59 -0
- package/scripts/tests/test_tracked_secrets.py +175 -0
- package/scripts/tests/test_tracked_tree.py +28 -0
- package/_shared/advisor/reference/sol-rung.md +0 -39
- package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Document-selection boundary tests for files, staged, base, and repository sources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from policy_lint import selection as selection_module
|
|
11
|
+
from policy_lint import selection_git
|
|
12
|
+
from policy_lint.config import constants
|
|
13
|
+
from policy_lint.model import (
|
|
14
|
+
ContentOrigin,
|
|
15
|
+
Document,
|
|
16
|
+
DocumentSet,
|
|
17
|
+
LintRequest,
|
|
18
|
+
SelectionKind,
|
|
19
|
+
)
|
|
20
|
+
from policy_lint.selection import SelectionRunFatal, select_documents
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _git_environment() -> dict[str, str]:
|
|
24
|
+
return {
|
|
25
|
+
each_name: each_environment_text
|
|
26
|
+
for each_name, each_environment_text in os.environ.items()
|
|
27
|
+
if not each_name.upper().startswith("GIT_")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _run_git(repository_root: Path, *all_arguments: str) -> None:
|
|
32
|
+
completed = subprocess.run(
|
|
33
|
+
[constants.GIT_EXECUTABLE, *all_arguments],
|
|
34
|
+
cwd=repository_root,
|
|
35
|
+
check=False,
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
env=_git_environment(),
|
|
39
|
+
)
|
|
40
|
+
assert completed.returncode == 0, completed.stderr
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _initialize_repository(repository_root: Path) -> Path:
|
|
44
|
+
_run_git(repository_root, "init", "--quiet", "-b", "main")
|
|
45
|
+
_run_git(repository_root, "config", "user.name", "Selection Tests")
|
|
46
|
+
_run_git(repository_root, "config", "user.email", "selection@example.invalid")
|
|
47
|
+
_run_git(repository_root, "config", "commit.gpgsign", "false")
|
|
48
|
+
_run_git(repository_root, "config", "core.autocrlf", "false")
|
|
49
|
+
return repository_root
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _write_text(repository_root: Path, relative_path: str, text: str) -> None:
|
|
53
|
+
file_path = repository_root / relative_path
|
|
54
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
file_path.write_bytes(text.encode(constants.UTF8_ENCODING))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _commit_text(repository_root: Path, relative_path: str, text: str) -> None:
|
|
59
|
+
_write_text(repository_root, relative_path, text)
|
|
60
|
+
_run_git(repository_root, "add", "--", relative_path)
|
|
61
|
+
_run_git(repository_root, "commit", "--quiet", "-m", "selection fixture")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _document_for(document_set: DocumentSet, relative_path: str) -> Document:
|
|
65
|
+
for each_document in document_set.documents:
|
|
66
|
+
if each_document.path.as_posix() == relative_path:
|
|
67
|
+
return each_document
|
|
68
|
+
raise AssertionError(f"missing document: {relative_path}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_files_source_should_read_current_worktree_bytes(tmp_path: Path) -> None:
|
|
72
|
+
repository_root = _initialize_repository(tmp_path)
|
|
73
|
+
_commit_text(repository_root, "file.py", "committed\n")
|
|
74
|
+
_write_text(repository_root, "file.py", "worktree\n")
|
|
75
|
+
|
|
76
|
+
document_set = select_documents(
|
|
77
|
+
LintRequest.files(repository_root, [Path("file.py")])
|
|
78
|
+
)
|
|
79
|
+
selected_document = _document_for(document_set, "file.py")
|
|
80
|
+
|
|
81
|
+
assert document_set.selection == SelectionKind.FILES
|
|
82
|
+
assert selected_document.text == "worktree\n"
|
|
83
|
+
assert selected_document.origin == ContentOrigin.WORKTREE
|
|
84
|
+
assert selected_document.prior_text is None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_staged_modified_file_should_read_index_bytes_not_worktree(
|
|
88
|
+
tmp_path: Path,
|
|
89
|
+
) -> None:
|
|
90
|
+
repository_root = _initialize_repository(tmp_path)
|
|
91
|
+
_commit_text(repository_root, "file.py", "committed\n")
|
|
92
|
+
_write_text(repository_root, "file.py", "staged\n")
|
|
93
|
+
_run_git(repository_root, "add", "--", "file.py")
|
|
94
|
+
_write_text(repository_root, "file.py", "worktree\n")
|
|
95
|
+
|
|
96
|
+
document_set = select_documents(LintRequest.staged(repository_root))
|
|
97
|
+
selected_document = _document_for(document_set, "file.py")
|
|
98
|
+
|
|
99
|
+
assert document_set.selection == SelectionKind.STAGED
|
|
100
|
+
assert selected_document.text == "staged\n"
|
|
101
|
+
assert selected_document.prior_text == "committed\n"
|
|
102
|
+
assert selected_document.origin == ContentOrigin.INDEX
|
|
103
|
+
assert selected_document.prior_path is None
|
|
104
|
+
assert document_set.deleted_paths == ()
|
|
105
|
+
assert 1 in (selected_document.changed_lines or frozenset())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_staged_added_file_should_have_no_prior_text(tmp_path: Path) -> None:
|
|
109
|
+
repository_root = _initialize_repository(tmp_path)
|
|
110
|
+
_commit_text(repository_root, "keep.py", "keep\n")
|
|
111
|
+
_write_text(repository_root, "new.py", "added\n")
|
|
112
|
+
_run_git(repository_root, "add", "--", "new.py")
|
|
113
|
+
|
|
114
|
+
document_set = select_documents(LintRequest.staged(repository_root))
|
|
115
|
+
selected_document = _document_for(document_set, "new.py")
|
|
116
|
+
|
|
117
|
+
assert selected_document.text == "added\n"
|
|
118
|
+
assert selected_document.prior_text is None
|
|
119
|
+
assert document_set.deleted_paths == ()
|
|
120
|
+
assert selected_document.changed_lines == frozenset({1})
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_staged_deleted_file_should_record_a_true_delete_only(tmp_path: Path) -> None:
|
|
124
|
+
repository_root = _initialize_repository(tmp_path)
|
|
125
|
+
_commit_text(repository_root, "gone.py", "body\n")
|
|
126
|
+
_run_git(repository_root, "rm", "--quiet", "--", "gone.py")
|
|
127
|
+
|
|
128
|
+
document_set = select_documents(LintRequest.staged(repository_root))
|
|
129
|
+
|
|
130
|
+
assert document_set.documents == ()
|
|
131
|
+
assert document_set.deleted_paths == (PurePosixPath("gone.py"),)
|
|
132
|
+
assert document_set.renamed_paths == ()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_staged_renamed_file_should_keep_prior_path_metadata(tmp_path: Path) -> None:
|
|
136
|
+
repository_root = _initialize_repository(tmp_path)
|
|
137
|
+
_commit_text(repository_root, "old.py", "body\n")
|
|
138
|
+
_run_git(repository_root, "mv", "--", "old.py", "new.py")
|
|
139
|
+
|
|
140
|
+
document_set = select_documents(LintRequest.staged(repository_root))
|
|
141
|
+
selected_document = _document_for(document_set, "new.py")
|
|
142
|
+
|
|
143
|
+
assert selected_document.text == "body\n"
|
|
144
|
+
assert selected_document.prior_text == "body\n"
|
|
145
|
+
assert selected_document.prior_path == PurePosixPath("old.py")
|
|
146
|
+
assert document_set.renamed_paths == (
|
|
147
|
+
(PurePosixPath("old.py"), PurePosixPath("new.py")),
|
|
148
|
+
)
|
|
149
|
+
assert document_set.deleted_paths == ()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_staged_copied_file_should_not_record_a_rename(
|
|
153
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
154
|
+
) -> None:
|
|
155
|
+
repository_root = _initialize_repository(tmp_path)
|
|
156
|
+
_commit_text(repository_root, "source.py", "copied-body\n")
|
|
157
|
+
_write_text(repository_root, "copy.py", "copied-body\n")
|
|
158
|
+
_run_git(repository_root, "add", "--", "copy.py")
|
|
159
|
+
original_git_bytes_for = selection_module.git_bytes_for
|
|
160
|
+
|
|
161
|
+
def git_bytes_for_with_copy(
|
|
162
|
+
git_repository_root: Path, all_arguments: tuple[str, ...]
|
|
163
|
+
) -> bytes:
|
|
164
|
+
if constants.GIT_NAME_STATUS_FLAG in all_arguments:
|
|
165
|
+
return b"C100\0source.py\0copy.py\0"
|
|
166
|
+
return original_git_bytes_for(git_repository_root, all_arguments)
|
|
167
|
+
|
|
168
|
+
monkeypatch.setattr(selection_module, "git_bytes_for", git_bytes_for_with_copy)
|
|
169
|
+
document_set = select_documents(LintRequest.staged(repository_root))
|
|
170
|
+
selected_document = _document_for(document_set, "copy.py")
|
|
171
|
+
|
|
172
|
+
assert selected_document.text == "copied-body\n"
|
|
173
|
+
assert selected_document.prior_path is None
|
|
174
|
+
assert selected_document.prior_text is None
|
|
175
|
+
assert document_set.renamed_paths == ()
|
|
176
|
+
assert document_set.deleted_paths == ()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_staged_missing_non_delete_blob_should_raise(
|
|
180
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
181
|
+
) -> None:
|
|
182
|
+
repository_root = _initialize_repository(tmp_path)
|
|
183
|
+
_commit_text(repository_root, "file.py", "committed\n")
|
|
184
|
+
_write_text(repository_root, "file.py", "staged\n")
|
|
185
|
+
_run_git(repository_root, "add", "--", "file.py")
|
|
186
|
+
original_read_blob = selection_module.read_blob
|
|
187
|
+
|
|
188
|
+
def missing_index_blob(
|
|
189
|
+
git_repository_root: Path, revision: str, relative_path: str
|
|
190
|
+
) -> str | None:
|
|
191
|
+
if revision == constants.GIT_INDEX_REFERENCE_PREFIX:
|
|
192
|
+
return None
|
|
193
|
+
return original_read_blob(git_repository_root, revision, relative_path)
|
|
194
|
+
|
|
195
|
+
monkeypatch.setattr(selection_module, "read_blob", missing_index_blob)
|
|
196
|
+
with pytest.raises(SelectionRunFatal, match="unavailable"):
|
|
197
|
+
select_documents(LintRequest.staged(repository_root))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_git_blob_unexpected_failure_should_raise(
|
|
201
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
202
|
+
) -> None:
|
|
203
|
+
def failed_git(*_all_arguments: object, **_all_keywords: object) -> subprocess.CompletedProcess[bytes]:
|
|
204
|
+
return subprocess.CompletedProcess(
|
|
205
|
+
args=[constants.GIT_EXECUTABLE],
|
|
206
|
+
returncode=1,
|
|
207
|
+
stdout=b"",
|
|
208
|
+
stderr=b"fatal: Not a valid object name HEAD:file.py",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
monkeypatch.setattr(selection_git.subprocess, "run", failed_git)
|
|
212
|
+
with pytest.raises(selection_git.GitSelectionError, match="Not a valid object"):
|
|
213
|
+
selection_git.read_blob(tmp_path, "HEAD", "file.py")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_unborn_repository_head_should_return_none(tmp_path: Path) -> None:
|
|
217
|
+
repository_root = _initialize_repository(tmp_path)
|
|
218
|
+
assert selection_git.head_revision(repository_root) is None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_unexpected_head_failure_should_raise(
|
|
222
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
223
|
+
) -> None:
|
|
224
|
+
def failed_git(
|
|
225
|
+
*_all_arguments: object, **_all_keywords: object
|
|
226
|
+
) -> subprocess.CompletedProcess[bytes]:
|
|
227
|
+
return subprocess.CompletedProcess(
|
|
228
|
+
args=[constants.GIT_EXECUTABLE],
|
|
229
|
+
returncode=1,
|
|
230
|
+
stdout=b"",
|
|
231
|
+
stderr=b"fatal: broken HEAD state",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
monkeypatch.setattr(selection_git.subprocess, "run", failed_git)
|
|
235
|
+
with pytest.raises(selection_git.GitSelectionError, match="broken HEAD"):
|
|
236
|
+
selection_git.head_revision(tmp_path)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_base_source_should_compare_merge_base_and_read_worktree_bytes(
|
|
240
|
+
tmp_path: Path,
|
|
241
|
+
) -> None:
|
|
242
|
+
repository_root = _initialize_repository(tmp_path)
|
|
243
|
+
_commit_text(repository_root, "file.py", "base-text\n")
|
|
244
|
+
_run_git(repository_root, "checkout", "--quiet", "-b", "feature")
|
|
245
|
+
_commit_text(repository_root, "file.py", "feature-committed\n")
|
|
246
|
+
_write_text(repository_root, "file.py", "feature-worktree\n")
|
|
247
|
+
|
|
248
|
+
document_set = select_documents(LintRequest.base(repository_root, "main"))
|
|
249
|
+
selected_document = _document_for(document_set, "file.py")
|
|
250
|
+
|
|
251
|
+
assert document_set.selection == SelectionKind.BASE
|
|
252
|
+
assert selected_document.text == "feature-worktree\n"
|
|
253
|
+
assert selected_document.prior_text == "base-text\n"
|
|
254
|
+
assert selected_document.origin == ContentOrigin.REVISION_DIFF
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_repository_source_should_read_tracked_worktree_files_only(
|
|
258
|
+
tmp_path: Path,
|
|
259
|
+
) -> None:
|
|
260
|
+
repository_root = _initialize_repository(tmp_path)
|
|
261
|
+
_commit_text(repository_root, "tracked.py", "tracked\n")
|
|
262
|
+
_write_text(repository_root, "untracked.py", "nope\n")
|
|
263
|
+
|
|
264
|
+
document_set = select_documents(LintRequest.repository(repository_root))
|
|
265
|
+
all_paths = {
|
|
266
|
+
each_document.path.as_posix() for each_document in document_set.documents
|
|
267
|
+
}
|
|
268
|
+
selected_document = _document_for(document_set, "tracked.py")
|
|
269
|
+
|
|
270
|
+
assert document_set.selection == SelectionKind.REPOSITORY
|
|
271
|
+
assert all_paths == {"tracked.py"}
|
|
272
|
+
assert selected_document.text == "tracked\n"
|
|
273
|
+
assert selected_document.origin == ContentOrigin.WORKTREE
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_prebuilt_document_set_should_keep_repository_relative_paths(
|
|
277
|
+
tmp_path: Path,
|
|
278
|
+
) -> None:
|
|
279
|
+
repository_root = _initialize_repository(tmp_path)
|
|
280
|
+
document_set = DocumentSet(
|
|
281
|
+
(
|
|
282
|
+
Document(
|
|
283
|
+
PurePosixPath("ok.py"),
|
|
284
|
+
"text\n",
|
|
285
|
+
None,
|
|
286
|
+
None,
|
|
287
|
+
ContentOrigin.EDITOR,
|
|
288
|
+
),
|
|
289
|
+
),
|
|
290
|
+
SelectionKind.TEXT,
|
|
291
|
+
repository_root,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
selected_document_set = select_documents(LintRequest(repository_root, document_set))
|
|
295
|
+
|
|
296
|
+
assert selected_document_set.documents[0].path == PurePosixPath("ok.py")
|
|
297
|
+
assert selected_document_set.selection == SelectionKind.TEXT
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_files_path_outside_repository_should_raise(tmp_path: Path) -> None:
|
|
301
|
+
repository_root = _initialize_repository(tmp_path)
|
|
302
|
+
outside_path = repository_root.parent / "escape.py"
|
|
303
|
+
with pytest.raises(SelectionRunFatal, match="outside"):
|
|
304
|
+
select_documents(LintRequest.files(repository_root, [outside_path]))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_missing_git_should_raise_a_selection_error(
|
|
308
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
309
|
+
) -> None:
|
|
310
|
+
def missing_git(_repository_root: Path, _all_arguments: tuple[str, ...]) -> bytes:
|
|
311
|
+
raise FileNotFoundError("git")
|
|
312
|
+
|
|
313
|
+
monkeypatch.setattr(selection_module, "git_bytes_for", missing_git)
|
|
314
|
+
with pytest.raises(SelectionRunFatal, match="git"):
|
|
315
|
+
select_documents(LintRequest.files(tmp_path, [Path("file.py")]))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_prebuilt_prior_path_outside_repository_should_raise(tmp_path: Path) -> None:
|
|
319
|
+
repository_root = _initialize_repository(tmp_path)
|
|
320
|
+
document_set = DocumentSet(
|
|
321
|
+
(
|
|
322
|
+
Document(
|
|
323
|
+
PurePosixPath("ok.py"),
|
|
324
|
+
"text\n",
|
|
325
|
+
None,
|
|
326
|
+
None,
|
|
327
|
+
ContentOrigin.EDITOR,
|
|
328
|
+
PurePosixPath("../escape"),
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
SelectionKind.TEXT,
|
|
332
|
+
repository_root,
|
|
333
|
+
)
|
|
334
|
+
with pytest.raises(SelectionRunFatal, match="outside"):
|
|
335
|
+
select_documents(LintRequest(repository_root, document_set))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_prebuilt_deleted_path_outside_repository_should_raise(tmp_path: Path) -> None:
|
|
339
|
+
repository_root = _initialize_repository(tmp_path)
|
|
340
|
+
document_set = DocumentSet(
|
|
341
|
+
(),
|
|
342
|
+
SelectionKind.STAGED,
|
|
343
|
+
repository_root,
|
|
344
|
+
deleted_paths=(PurePosixPath("../escape"),),
|
|
345
|
+
)
|
|
346
|
+
with pytest.raises(SelectionRunFatal, match="outside"):
|
|
347
|
+
select_documents(LintRequest(repository_root, document_set))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_prebuilt_renamed_path_outside_repository_should_raise(tmp_path: Path) -> None:
|
|
351
|
+
repository_root = _initialize_repository(tmp_path)
|
|
352
|
+
document_set = DocumentSet(
|
|
353
|
+
(),
|
|
354
|
+
SelectionKind.STAGED,
|
|
355
|
+
repository_root,
|
|
356
|
+
renamed_paths=((PurePosixPath("../escape"), PurePosixPath("ok.py")),),
|
|
357
|
+
)
|
|
358
|
+
with pytest.raises(SelectionRunFatal, match="outside"):
|
|
359
|
+
select_documents(LintRequest(repository_root, document_set))
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_tracked_worktree_document_preserves_git_path_spelling(
|
|
363
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
364
|
+
) -> None:
|
|
365
|
+
monkeypatch.setattr(
|
|
366
|
+
selection_module,
|
|
367
|
+
"_read_worktree_text",
|
|
368
|
+
lambda _file_path: "body\n",
|
|
369
|
+
)
|
|
370
|
+
selected_document = selection_module._tracked_worktree_document(
|
|
371
|
+
tmp_path, "MiXeD.py"
|
|
372
|
+
)
|
|
373
|
+
assert selected_document.path == PurePosixPath("MiXeD.py")
|
|
@@ -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")
|