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.
Files changed (99) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/autoconverge/SKILL.md +19 -15
  3. package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
  4. package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
  5. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
  6. package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
  7. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
  8. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
  9. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
  10. package/.agents/skills/team-advisor/SKILL.md +11 -11
  11. package/_shared/advisor/advisor-protocol.md +51 -51
  12. package/_shared/advisor/reference/advisor-block.md +9 -9
  13. package/_shared/advisor/reference/astra-rung.md +39 -0
  14. package/_shared/advisor/reference/identity.md +4 -4
  15. package/_shared/advisor/reference/lifecycle.md +6 -6
  16. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  17. package/_shared/advisor/reference/third-party-bind.md +11 -11
  18. package/_shared/advisor/reference/warm-up.md +13 -13
  19. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  20. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  21. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  22. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  23. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  24. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  25. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  26. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  27. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  28. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  29. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  30. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  31. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  32. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  33. package/bin/cde.mjs +175 -0
  34. package/bin/cde.test.mjs +222 -0
  35. package/bin/install-constants.mjs +4 -0
  36. package/bin/install.cursor-rules.test.mjs +233 -2
  37. package/bin/install.mjs +49 -3
  38. package/bin/pstack-model-preferences.codex.json +23 -0
  39. package/bin/select-pstack-models.test.mjs +378 -0
  40. package/docs/references/team-advisor-skill.md +3 -3
  41. package/hooks/hooks_constants/tool_names.py +20 -0
  42. package/hooks/validators/README.md +35 -1
  43. package/hooks/validators/fast_save_validators.py +63 -89
  44. package/hooks/validators/run_all_validators.py +16 -8
  45. package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
  46. package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
  47. package/package.json +3 -2
  48. package/rules/pstack-models.md +119 -0
  49. package/scripts/cde_lint.py +258 -0
  50. package/scripts/check.ps1 +18 -6
  51. package/scripts/policy_lint/__init__.py +41 -0
  52. package/scripts/policy_lint/adapter_configuration.py +91 -0
  53. package/scripts/policy_lint/adapter_detectors.py +368 -0
  54. package/scripts/policy_lint/adapter_pairing.py +145 -0
  55. package/scripts/policy_lint/adapter_support.py +97 -0
  56. package/scripts/policy_lint/adapters.py +376 -0
  57. package/scripts/policy_lint/config/__init__.py +1 -0
  58. package/scripts/policy_lint/config/constants.py +77 -0
  59. package/scripts/policy_lint/engine.py +224 -0
  60. package/scripts/policy_lint/model.py +388 -0
  61. package/scripts/policy_lint/registry.py +171 -0
  62. package/scripts/policy_lint/render.py +91 -0
  63. package/scripts/policy_lint/selection.py +372 -0
  64. package/scripts/policy_lint/selection_git.py +147 -0
  65. package/scripts/repository_checks/__init__.py +1 -0
  66. package/scripts/repository_checks/claude_md.py +108 -0
  67. package/scripts/repository_checks/config/__init__.py +1 -0
  68. package/scripts/repository_checks/config/constants.py +150 -0
  69. package/scripts/repository_checks/env_var_documentation.py +243 -0
  70. package/scripts/repository_checks/hook_modules.py +34 -0
  71. package/scripts/repository_checks/models.py +37 -0
  72. package/scripts/repository_checks/package_inventory.py +118 -0
  73. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  74. package/scripts/repository_checks/reporting.py +39 -0
  75. package/scripts/repository_checks/runner.py +93 -0
  76. package/scripts/repository_checks/tracked_secrets.py +147 -0
  77. package/scripts/repository_checks/tracked_tree.py +28 -0
  78. package/scripts/repository_policy.py +120 -0
  79. package/scripts/select_pstack_models.mjs +304 -0
  80. package/scripts/tests/repository_policy_test_support.py +89 -0
  81. package/scripts/tests/test_cde_lint.py +398 -0
  82. package/scripts/tests/test_claude_md.py +68 -0
  83. package/scripts/tests/test_env_var_documentation.py +133 -0
  84. package/scripts/tests/test_hook_modules.py +17 -0
  85. package/scripts/tests/test_models.py +26 -0
  86. package/scripts/tests/test_package_inventory.py +96 -0
  87. package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
  88. package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
  89. package/scripts/tests/test_policy_lint_selection.py +373 -0
  90. package/scripts/tests/test_pytest_testpaths.py +66 -0
  91. package/scripts/tests/test_reporting.py +54 -0
  92. package/scripts/tests/test_repository_policy.py +71 -0
  93. package/scripts/tests/test_runner.py +59 -0
  94. package/scripts/tests/test_tracked_secrets.py +175 -0
  95. package/scripts/tests/test_tracked_tree.py +28 -0
  96. package/_shared/advisor/reference/sol-rung.md +0 -39
  97. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  98. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  99. 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")