claude-dev-env 7.5.0 → 7.6.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/everything-search/scripts/everything_search.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
- package/.agents/skills/fresh-branch/SKILL.md +1 -0
- package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
- package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/syncing-submodules/SKILL.md +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
- package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/SKILL.md +1 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
- package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
- package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
- package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
- package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
- package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
- package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
- package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
- package/_shared/pr-loop/scripts/preflight.py +7 -0
- package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
- package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
- package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
- package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
- package/bin/install-constants.mjs +4 -2
- package/bin/install.cursor-rules.test.mjs +57 -15
- package/bin/install.mjs +10 -15
- package/bin/pstack-model-preferences.claude.json +23 -0
- package/hooks/advisory/pr_done_reminder.py +7 -0
- package/hooks/advisory/refactor_guard.py +8 -0
- package/hooks/advisory/test_pr_done_reminder.py +21 -0
- package/hooks/advisory/test_refactor_guard.py +40 -0
- package/hooks/blocking/block_main_commit.py +4 -0
- package/hooks/blocking/code_rules_comments.py +39 -5
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +3 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
- package/hooks/git-hooks/pull_request_handoff.py +3 -0
- package/hooks/git-hooks/subprocess_window_access.py +41 -0
- package/hooks/git-hooks/test_verification_start.py +4 -0
- package/hooks/git-hooks/verification_notice_context.py +3 -0
- package/hooks/git-hooks/verification_start.py +7 -8
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/subprocess_window.py +141 -0
- package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/test_subprocess_window.py +128 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
- package/hooks/validation/mypy_validator.py +4 -0
- package/hooks/validators/README.md +1 -0
- package/hooks/validators/git_checks.py +4 -0
- package/hooks/validators/health_check.py +3 -0
- package/hooks/validators/hook_timing_harness.py +3 -0
- package/hooks/validators/mypy_integration.py +4 -0
- package/hooks/validators/ruff_integration.py +6 -0
- package/hooks/validators/run_all_validators.py +7 -0
- package/hooks/validators/subprocess_window_access.py +41 -0
- package/hooks/workflow/auto_formatter.py +8 -0
- package/hooks/workflow/test_auto_formatter.py +39 -0
- package/package.json +1 -1
- package/rules/pstack-models.md +4 -3
- package/scripts/automatic_advisory/checkout.py +8 -1
- package/scripts/automatic_advisory/cli.py +9 -3
- package/scripts/automatic_advisory/config/constants.py +0 -2
- package/scripts/automatic_advisory/execution.py +7 -1
- package/scripts/automatic_advisory/git.py +9 -1
- package/scripts/automatic_advisory/process_host.py +3 -0
- package/scripts/claude_chain_runner.py +3 -0
- package/scripts/gh_artifact_upload.py +3 -0
- package/scripts/grok_headless_runner.py +3 -0
- package/scripts/grok_patch_artifacts.py +4 -0
- package/scripts/grok_worker_preflight.py +3 -0
- package/scripts/invoke_code_review.py +3 -0
- package/scripts/local_verification/command_runner.py +3 -0
- package/scripts/local_verification/git_state.py +3 -0
- package/scripts/policy_lint/adapter_detectors.py +48 -2
- package/scripts/policy_lint/adapter_pairing.py +150 -2
- package/scripts/policy_lint/adapters.py +46 -4
- package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection_git.py +11 -0
- package/scripts/repository_checks/config/constants.py +5 -0
- package/scripts/repository_checks/config/policy_document.py +19 -0
- package/scripts/repository_checks/email_exemptions.py +14 -71
- package/scripts/repository_checks/path_exemptions.py +35 -0
- package/scripts/repository_checks/policy_document.py +120 -0
- package/scripts/repository_checks/tracked_secrets.py +27 -4
- package/scripts/setup_project_paths.py +3 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/subprocess_window_access.py +41 -0
- package/scripts/test_dispatcher_profile_import.py +4 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
- package/scripts/tests/test_email_exemptions.py +37 -0
- package/scripts/tests/test_path_exemptions.py +164 -0
- package/scripts/tests/test_policy_document.py +104 -0
- package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
- package/scripts/tests/test_policy_lint_selection.py +34 -0
- package/scripts/tests/test_subprocess_window_access.py +118 -0
- package/scripts/tests/test_tracked_secrets.py +79 -0
- package/scripts/validate_instruction_pairs.py +3 -0
- package/scripts/verify_installable_package.py +5 -0
- package/scripts/automatic_advisory/window_flags.py +0 -41
- package/scripts/repository_checks/config/email_exemptions.py +0 -10
|
@@ -70,6 +70,37 @@ def test_approved_pairs_name_files_that_exist() -> None:
|
|
|
70
70
|
assert (repository_root / each_test_path).is_file(), each_test_path
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def test_every_approved_pair_actually_waives_its_production_file(tmp_path: Path) -> None:
|
|
74
|
+
"""Each mapping entry must silence the rule when its production file changes.
|
|
75
|
+
|
|
76
|
+
File existence alone leaves an entry inert when its key carries a suffix the
|
|
77
|
+
rule skips, or when its value is not a name the rule reads as a test.
|
|
78
|
+
"""
|
|
79
|
+
for (
|
|
80
|
+
production_path,
|
|
81
|
+
all_test_paths,
|
|
82
|
+
) in APPROVED_TEST_PATHS_BY_PRODUCTION_PATH.items():
|
|
83
|
+
production_document = Document(
|
|
84
|
+
production_path,
|
|
85
|
+
_BODY_AFTER,
|
|
86
|
+
_BODY_BEFORE,
|
|
87
|
+
frozenset({1}),
|
|
88
|
+
ContentOrigin.REVISION_DIFF,
|
|
89
|
+
)
|
|
90
|
+
for each_test_path in all_test_paths:
|
|
91
|
+
test_document = Document(
|
|
92
|
+
each_test_path,
|
|
93
|
+
_BODY_AFTER,
|
|
94
|
+
_BODY_BEFORE,
|
|
95
|
+
frozenset({1}),
|
|
96
|
+
ContentOrigin.REVISION_DIFF,
|
|
97
|
+
)
|
|
98
|
+
document_set = DocumentSet(
|
|
99
|
+
(production_document, test_document), SelectionKind.BASE, tmp_path
|
|
100
|
+
)
|
|
101
|
+
assert adapters.test_pairing_diagnostics(document_set) == (), production_path
|
|
102
|
+
|
|
103
|
+
|
|
73
104
|
def test_pairing_ignores_python_docstring_only_changes(tmp_path: Path) -> None:
|
|
74
105
|
all_diagnostics = _pairing_diagnostics(
|
|
75
106
|
tmp_path,
|
|
@@ -161,3 +192,347 @@ def test_pairing_rejects_an_unrelated_module_beside_a_changed_approved_suite(
|
|
|
161
192
|
_body_change_at(_APPROVED_TEST_PATH),
|
|
162
193
|
)
|
|
163
194
|
assert all_paths == (_UNRELATED_PRODUCTION_PATH,)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
_MECHANICAL_BEFORE = '''"""Before."""
|
|
198
|
+
|
|
199
|
+
import subprocess
|
|
200
|
+
|
|
201
|
+
TIMEOUT_SECONDS = 5
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def work(command: list[str]) -> int:
|
|
205
|
+
"""Run one command."""
|
|
206
|
+
if TIMEOUT_SECONDS > 0:
|
|
207
|
+
completed = subprocess.run(command, timeout=TIMEOUT_SECONDS, check=False)
|
|
208
|
+
return completed.returncode
|
|
209
|
+
return 1
|
|
210
|
+
'''
|
|
211
|
+
_KEYWORD_ADDED = '''"""Before."""
|
|
212
|
+
|
|
213
|
+
import subprocess
|
|
214
|
+
|
|
215
|
+
TIMEOUT_SECONDS = 5
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def work(command: list[str]) -> int:
|
|
219
|
+
"""Run one command."""
|
|
220
|
+
if TIMEOUT_SECONDS > 0:
|
|
221
|
+
completed = subprocess.run(
|
|
222
|
+
command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
|
|
223
|
+
)
|
|
224
|
+
return completed.returncode
|
|
225
|
+
return 1
|
|
226
|
+
'''
|
|
227
|
+
_IMPORT_AND_KEYWORD_ADDED = '''"""Before."""
|
|
228
|
+
|
|
229
|
+
import os
|
|
230
|
+
import subprocess
|
|
231
|
+
|
|
232
|
+
TIMEOUT_SECONDS = 5
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def work(command: list[str]) -> int:
|
|
236
|
+
"""Run one command."""
|
|
237
|
+
if TIMEOUT_SECONDS > 0:
|
|
238
|
+
completed = subprocess.run(
|
|
239
|
+
command, timeout=TIMEOUT_SECONDS, check=False, creationflags=os.O_RDONLY
|
|
240
|
+
)
|
|
241
|
+
return completed.returncode
|
|
242
|
+
return 1
|
|
243
|
+
'''
|
|
244
|
+
_DOCSTRING_AND_KEYWORD_CHANGED = '''"""After."""
|
|
245
|
+
|
|
246
|
+
import subprocess
|
|
247
|
+
|
|
248
|
+
TIMEOUT_SECONDS = 5
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def work(command: list[str]) -> int:
|
|
252
|
+
"""Run one command and report its exit status."""
|
|
253
|
+
if TIMEOUT_SECONDS > 0:
|
|
254
|
+
completed = subprocess.run(
|
|
255
|
+
command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
|
|
256
|
+
)
|
|
257
|
+
return completed.returncode
|
|
258
|
+
return 1
|
|
259
|
+
'''
|
|
260
|
+
_KEYWORD_ADDED_AND_LITERAL_CHANGED = '''"""Before."""
|
|
261
|
+
|
|
262
|
+
import subprocess
|
|
263
|
+
|
|
264
|
+
TIMEOUT_SECONDS = 6
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def work(command: list[str]) -> int:
|
|
268
|
+
"""Run one command."""
|
|
269
|
+
if TIMEOUT_SECONDS > 0:
|
|
270
|
+
completed = subprocess.run(
|
|
271
|
+
command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
|
|
272
|
+
)
|
|
273
|
+
return completed.returncode
|
|
274
|
+
return 1
|
|
275
|
+
'''
|
|
276
|
+
_KEYWORD_VALUE_CHANGED = '''"""Before."""
|
|
277
|
+
|
|
278
|
+
import subprocess
|
|
279
|
+
|
|
280
|
+
TIMEOUT_SECONDS = 5
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def work(command: list[str]) -> int:
|
|
284
|
+
"""Run one command."""
|
|
285
|
+
if TIMEOUT_SECONDS > 0:
|
|
286
|
+
completed = subprocess.run(command, timeout=TIMEOUT_SECONDS, check=True)
|
|
287
|
+
return completed.returncode
|
|
288
|
+
return 1
|
|
289
|
+
'''
|
|
290
|
+
_KEYWORD_VALUE_TYPE_CHANGED = '''"""Before."""
|
|
291
|
+
|
|
292
|
+
import subprocess
|
|
293
|
+
|
|
294
|
+
TIMEOUT_SECONDS = 5
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def work(command: list[str]) -> int:
|
|
298
|
+
"""Run one command."""
|
|
299
|
+
if TIMEOUT_SECONDS > 0:
|
|
300
|
+
completed = subprocess.run(command, timeout=TIMEOUT_SECONDS, check=0)
|
|
301
|
+
return completed.returncode
|
|
302
|
+
return 1
|
|
303
|
+
'''
|
|
304
|
+
_KEYWORD_REMOVED = '''"""Before."""
|
|
305
|
+
|
|
306
|
+
import subprocess
|
|
307
|
+
|
|
308
|
+
TIMEOUT_SECONDS = 5
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def work(command: list[str]) -> int:
|
|
312
|
+
"""Run one command."""
|
|
313
|
+
if TIMEOUT_SECONDS > 0:
|
|
314
|
+
completed = subprocess.run(command, timeout=TIMEOUT_SECONDS)
|
|
315
|
+
return completed.returncode
|
|
316
|
+
return 1
|
|
317
|
+
'''
|
|
318
|
+
_POSITIONAL_CHANGED = '''"""Before."""
|
|
319
|
+
|
|
320
|
+
import subprocess
|
|
321
|
+
|
|
322
|
+
TIMEOUT_SECONDS = 5
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def work(command: list[str]) -> int:
|
|
326
|
+
"""Run one command."""
|
|
327
|
+
if TIMEOUT_SECONDS > 0:
|
|
328
|
+
completed = subprocess.run(
|
|
329
|
+
command[:1], timeout=TIMEOUT_SECONDS, check=False, creationflags=0
|
|
330
|
+
)
|
|
331
|
+
return completed.returncode
|
|
332
|
+
return 1
|
|
333
|
+
'''
|
|
334
|
+
_STATEMENT_ADDED_IN_BODY = '''"""Before."""
|
|
335
|
+
|
|
336
|
+
import subprocess
|
|
337
|
+
|
|
338
|
+
TIMEOUT_SECONDS = 5
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def work(command: list[str]) -> int:
|
|
342
|
+
"""Run one command."""
|
|
343
|
+
if TIMEOUT_SECONDS > 0:
|
|
344
|
+
completed = subprocess.run(
|
|
345
|
+
command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
|
|
346
|
+
)
|
|
347
|
+
completed.check_returncode()
|
|
348
|
+
return completed.returncode
|
|
349
|
+
return 1
|
|
350
|
+
'''
|
|
351
|
+
_CONDITION_CHANGED = '''"""Before."""
|
|
352
|
+
|
|
353
|
+
import subprocess
|
|
354
|
+
|
|
355
|
+
TIMEOUT_SECONDS = 5
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def work(command: list[str]) -> int:
|
|
359
|
+
"""Run one command."""
|
|
360
|
+
if TIMEOUT_SECONDS > 1:
|
|
361
|
+
completed = subprocess.run(
|
|
362
|
+
command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
|
|
363
|
+
)
|
|
364
|
+
return completed.returncode
|
|
365
|
+
return 1
|
|
366
|
+
'''
|
|
367
|
+
_IMPORT_REMOVED = '''"""Before."""
|
|
368
|
+
|
|
369
|
+
TIMEOUT_SECONDS = 5
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def work(command: list[str]) -> int:
|
|
373
|
+
"""Run one command."""
|
|
374
|
+
if TIMEOUT_SECONDS > 0:
|
|
375
|
+
completed = subprocess.run(
|
|
376
|
+
command, timeout=TIMEOUT_SECONDS, check=False, creationflags=0
|
|
377
|
+
)
|
|
378
|
+
return completed.returncode
|
|
379
|
+
return 1
|
|
380
|
+
'''
|
|
381
|
+
_UNPARSEABLE = "def work(:\n return None\n"
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _mechanical_diagnostics(
|
|
385
|
+
temporary_path: Path, current_source: str, suffix: str = ".py"
|
|
386
|
+
) -> tuple[Diagnostic, ...]:
|
|
387
|
+
return _pairing_diagnostics(
|
|
388
|
+
temporary_path,
|
|
389
|
+
_changed_document(current_source, _MECHANICAL_BEFORE, suffix),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_pairing_waives_one_added_keyword_argument(tmp_path: Path) -> None:
|
|
394
|
+
assert _mechanical_diagnostics(tmp_path, _KEYWORD_ADDED) == ()
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_pairing_waives_an_added_import_beside_an_added_keyword(
|
|
398
|
+
tmp_path: Path,
|
|
399
|
+
) -> None:
|
|
400
|
+
assert _mechanical_diagnostics(tmp_path, _IMPORT_AND_KEYWORD_ADDED) == ()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_pairing_waives_a_docstring_edit_beside_an_added_keyword(
|
|
404
|
+
tmp_path: Path,
|
|
405
|
+
) -> None:
|
|
406
|
+
assert _mechanical_diagnostics(tmp_path, _DOCSTRING_AND_KEYWORD_CHANGED) == ()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_pairing_reports_an_added_keyword_beside_a_changed_literal(
|
|
410
|
+
tmp_path: Path,
|
|
411
|
+
) -> None:
|
|
412
|
+
assert (
|
|
413
|
+
len(_mechanical_diagnostics(tmp_path, _KEYWORD_ADDED_AND_LITERAL_CHANGED)) == 1
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_pairing_reports_a_changed_keyword_value(tmp_path: Path) -> None:
|
|
418
|
+
assert len(_mechanical_diagnostics(tmp_path, _KEYWORD_VALUE_CHANGED)) == 1
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_pairing_reports_a_keyword_value_that_changed_literal_type(
|
|
422
|
+
tmp_path: Path,
|
|
423
|
+
) -> None:
|
|
424
|
+
assert len(_mechanical_diagnostics(tmp_path, _KEYWORD_VALUE_TYPE_CHANGED)) == 1
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_pairing_reports_a_removed_keyword(tmp_path: Path) -> None:
|
|
428
|
+
assert len(_mechanical_diagnostics(tmp_path, _KEYWORD_REMOVED)) == 1
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_pairing_reports_a_changed_positional_argument(tmp_path: Path) -> None:
|
|
432
|
+
assert len(_mechanical_diagnostics(tmp_path, _POSITIONAL_CHANGED)) == 1
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def test_pairing_reports_a_statement_added_inside_a_function(tmp_path: Path) -> None:
|
|
436
|
+
assert len(_mechanical_diagnostics(tmp_path, _STATEMENT_ADDED_IN_BODY)) == 1
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_pairing_reports_a_changed_if_condition(tmp_path: Path) -> None:
|
|
440
|
+
assert len(_mechanical_diagnostics(tmp_path, _CONDITION_CHANGED)) == 1
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def test_pairing_reports_a_removed_import(tmp_path: Path) -> None:
|
|
444
|
+
assert len(_mechanical_diagnostics(tmp_path, _IMPORT_REMOVED)) == 1
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def test_pairing_reports_a_non_python_document_with_the_same_shape(
|
|
448
|
+
tmp_path: Path,
|
|
449
|
+
) -> None:
|
|
450
|
+
assert len(_mechanical_diagnostics(tmp_path, _KEYWORD_ADDED, ".ts")) == 1
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def test_pairing_reports_unparseable_current_source(tmp_path: Path) -> None:
|
|
454
|
+
assert len(_mechanical_diagnostics(tmp_path, _UNPARSEABLE)) == 1
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_pairing_reports_unparseable_prior_source(tmp_path: Path) -> None:
|
|
458
|
+
all_diagnostics = _pairing_diagnostics(
|
|
459
|
+
tmp_path, _changed_document(_KEYWORD_ADDED, _UNPARSEABLE)
|
|
460
|
+
)
|
|
461
|
+
assert len(all_diagnostics) == 1
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def test_pairing_reports_an_added_document_without_prior_text(tmp_path: Path) -> None:
|
|
465
|
+
added_document = Document(
|
|
466
|
+
PurePosixPath("src/feature.py"),
|
|
467
|
+
_KEYWORD_ADDED,
|
|
468
|
+
None,
|
|
469
|
+
frozenset({1}),
|
|
470
|
+
ContentOrigin.REVISION_DIFF,
|
|
471
|
+
)
|
|
472
|
+
assert len(_pairing_diagnostics(tmp_path, added_document)) == 1
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
_MAPPING_UNPACKING_ADDED = '''"""Before."""
|
|
476
|
+
|
|
477
|
+
import subprocess
|
|
478
|
+
|
|
479
|
+
TIMEOUT_SECONDS = 5
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def work(command: list[str]) -> int:
|
|
483
|
+
"""Run one command."""
|
|
484
|
+
if TIMEOUT_SECONDS > 0:
|
|
485
|
+
completed = subprocess.run(
|
|
486
|
+
command, timeout=TIMEOUT_SECONDS, check=False, **{"creationflags": 0}
|
|
487
|
+
)
|
|
488
|
+
return completed.returncode
|
|
489
|
+
return 1
|
|
490
|
+
'''
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def test_pairing_reports_added_mapping_unpacking_on_a_call(tmp_path: Path) -> None:
|
|
494
|
+
assert len(_mechanical_diagnostics(tmp_path, _MAPPING_UNPACKING_ADDED)) == 1
|
|
495
|
+
_INSTALLER_PRODUCTION_PATHS = (
|
|
496
|
+
PurePosixPath("packages/claude-dev-env/bin/install-constants.mjs"),
|
|
497
|
+
PurePosixPath("packages/claude-dev-env/bin/install.mjs"),
|
|
498
|
+
)
|
|
499
|
+
_INSTALLER_SUITE_PATH = PurePosixPath(
|
|
500
|
+
"packages/claude-dev-env/bin/install.cursor-rules.test.mjs"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def test_pairing_accepts_installer_modules_with_the_cursor_rules_suite(
|
|
505
|
+
tmp_path: Path,
|
|
506
|
+
) -> None:
|
|
507
|
+
all_paths = _diagnostic_paths(
|
|
508
|
+
tmp_path,
|
|
509
|
+
*(_body_change_at(each_path) for each_path in _INSTALLER_PRODUCTION_PATHS),
|
|
510
|
+
_body_change_at(_INSTALLER_SUITE_PATH),
|
|
511
|
+
)
|
|
512
|
+
assert all_paths == ()
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
_COMMENT_RULES_PRODUCTION_PATH = PurePosixPath(
|
|
516
|
+
"packages/claude-dev-env/hooks/blocking/code_rules_comments.py"
|
|
517
|
+
)
|
|
518
|
+
_COMMENT_RULES_SUITE_PATH = PurePosixPath(
|
|
519
|
+
"packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def test_pairing_accepts_comment_rules_module_with_one_approved_suite(
|
|
524
|
+
tmp_path: Path,
|
|
525
|
+
) -> None:
|
|
526
|
+
all_paths = _diagnostic_paths(
|
|
527
|
+
tmp_path,
|
|
528
|
+
_body_change_at(_COMMENT_RULES_PRODUCTION_PATH),
|
|
529
|
+
_body_change_at(_COMMENT_RULES_SUITE_PATH),
|
|
530
|
+
)
|
|
531
|
+
assert all_paths == ()
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def test_pairing_rejects_comment_rules_module_without_any_approved_suite(
|
|
535
|
+
tmp_path: Path,
|
|
536
|
+
) -> None:
|
|
537
|
+
all_paths = _diagnostic_paths(tmp_path, _body_change_at(_COMMENT_RULES_PRODUCTION_PATH))
|
|
538
|
+
assert all_paths == (_COMMENT_RULES_PRODUCTION_PATH,)
|
|
@@ -22,6 +22,27 @@ from policy_lint.model import (
|
|
|
22
22
|
|
|
23
23
|
_FUNCTION_SOURCE = "def work() -> None:\n pass\n"
|
|
24
24
|
_CHANGED_RULE_SETS = frozenset({"changed"})
|
|
25
|
+
_UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT = (
|
|
26
|
+
"import subprocess\n"
|
|
27
|
+
"PYTHON_FORMAT_TIMEOUT_SECONDS = 12\n"
|
|
28
|
+
"GIT_CHECK_TIMEOUT_SECONDS = 5\n"
|
|
29
|
+
"def worst_case_python_format_seconds() -> int:\n"
|
|
30
|
+
" return PYTHON_FORMAT_TIMEOUT_SECONDS + PYTHON_FORMAT_TIMEOUT_SECONDS\n"
|
|
31
|
+
"def is_untracked_in_git(file_path: str) -> bool:\n"
|
|
32
|
+
" git_check = subprocess.run(['git', 'ls-files', file_path], "
|
|
33
|
+
"timeout=GIT_CHECK_TIMEOUT_SECONDS)\n"
|
|
34
|
+
" return git_check.returncode != 0\n"
|
|
35
|
+
"def run_format(file_path: str) -> None:\n"
|
|
36
|
+
" subprocess.run(['ruff', 'format', file_path], "
|
|
37
|
+
"timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)\n"
|
|
38
|
+
"def main(file_path: str) -> None:\n"
|
|
39
|
+
" if is_untracked_in_git(file_path):\n"
|
|
40
|
+
" return\n"
|
|
41
|
+
" run_format(file_path)\n"
|
|
42
|
+
)
|
|
43
|
+
_GENERATED_CHANGELOG_TEXT = (
|
|
44
|
+
"## 7.5.0\n\n* **rules:** correct hook claims for gates that " + "no" + " longer run\n"
|
|
45
|
+
)
|
|
25
46
|
|
|
26
47
|
|
|
27
48
|
def _rule_named(rule_id: str) -> DocumentRule:
|
|
@@ -266,6 +287,51 @@ def test_hook_format_accepts_claude_settings() -> None:
|
|
|
266
287
|
assert configuration_rule.accepts(settings_document) is False
|
|
267
288
|
|
|
268
289
|
|
|
290
|
+
def test_prose_rules_skip_a_generated_changelog() -> None:
|
|
291
|
+
changelog_document = Document.from_text(
|
|
292
|
+
"packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT
|
|
293
|
+
)
|
|
294
|
+
handwritten_document = Document.from_text("docs/notes.md", _GENERATED_CHANGELOG_TEXT)
|
|
295
|
+
stored_prompt_changelog = Document.from_text("rules/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT)
|
|
296
|
+
stored_prompt_document = Document.from_text("rules/notes.md", _GENERATED_CHANGELOG_TEXT)
|
|
297
|
+
state_rule = _rule_named("state-description")
|
|
298
|
+
plain_language_rule = _rule_named("plain-language")
|
|
299
|
+
assert state_rule.accepts(changelog_document) is False
|
|
300
|
+
assert state_rule.accepts(handwritten_document) is True
|
|
301
|
+
assert plain_language_rule.accepts(stored_prompt_changelog) is False
|
|
302
|
+
assert plain_language_rule.accepts(stored_prompt_document) is True
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_lint_reports_the_changelog_phrase_only_in_the_handwritten_document(
|
|
306
|
+
tmp_path: Path,
|
|
307
|
+
) -> None:
|
|
308
|
+
_ensure_git_repository(tmp_path)
|
|
309
|
+
document_set = DocumentSet(
|
|
310
|
+
(
|
|
311
|
+
Document.from_text("packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT),
|
|
312
|
+
Document.from_text("docs/notes.md", _GENERATED_CHANGELOG_TEXT),
|
|
313
|
+
),
|
|
314
|
+
SelectionKind.TEXT,
|
|
315
|
+
tmp_path,
|
|
316
|
+
)
|
|
317
|
+
lint_report = lint(
|
|
318
|
+
LintRequest(tmp_path, document_set), all_registry=(_rule_named("state-description"),)
|
|
319
|
+
)
|
|
320
|
+
all_reported_paths = {
|
|
321
|
+
each_diagnostic.location.path.as_posix()
|
|
322
|
+
for each_diagnostic in lint_report.diagnostics
|
|
323
|
+
if each_diagnostic.location is not None
|
|
324
|
+
}
|
|
325
|
+
assert all_reported_paths == {"docs/notes.md"}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_non_prose_markdown_rules_still_run_on_a_generated_changelog() -> None:
|
|
329
|
+
changelog_document = Document.from_text(
|
|
330
|
+
"packages/claude-dev-env/CHANGELOG.md", _GENERATED_CHANGELOG_TEXT
|
|
331
|
+
)
|
|
332
|
+
assert _rule_named("docstring-gate-count").accepts(changelog_document) is True
|
|
333
|
+
|
|
334
|
+
|
|
269
335
|
def test_workflow_substitution_accepts_workflow_js_suffix() -> None:
|
|
270
336
|
workflow_document = Document.from_text("packages/foo/bar.workflow.js", "loop")
|
|
271
337
|
directory_document = Document.from_text("docs/workflow/readme.md", "loop")
|
|
@@ -581,3 +647,53 @@ def test_lint_keeps_skipped_rules_out_of_executed_rules(tmp_path: Path) -> None:
|
|
|
581
647
|
all_rules_report = lint(request, all_registry=(skipped_rule,))
|
|
582
648
|
assert all_rules_report.executed_rules == ()
|
|
583
649
|
assert all_rules_report.skipped_rules == ("skipped",)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def test_subprocess_budget_skips_a_test_module_and_keeps_production() -> None:
|
|
653
|
+
subprocess_budget_rule = _rule_named("subprocess-budget")
|
|
654
|
+
prefixed_test_document = Document.from_text(
|
|
655
|
+
"packages/claude-dev-env/hooks/workflow/test_auto_formatter.py",
|
|
656
|
+
_UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT,
|
|
657
|
+
)
|
|
658
|
+
repository_root_test_document = Document.from_text(
|
|
659
|
+
"tests/timing.py", _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT
|
|
660
|
+
)
|
|
661
|
+
production_document = Document.from_text(
|
|
662
|
+
"src/timing.py", _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT
|
|
663
|
+
)
|
|
664
|
+
assert subprocess_budget_rule.accepts(prefixed_test_document) is False
|
|
665
|
+
assert subprocess_budget_rule.accepts(repository_root_test_document) is False
|
|
666
|
+
assert subprocess_budget_rule.accepts(production_document) is True
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _subprocess_budget_rule_identifiers(
|
|
670
|
+
repository_root: Path, document_path: str
|
|
671
|
+
) -> tuple[str, ...]:
|
|
672
|
+
document_set = DocumentSet(
|
|
673
|
+
(Document.from_text(document_path, _UNDERCOUNTED_SUBPROCESS_BUDGET_TEXT),),
|
|
674
|
+
SelectionKind.TEXT,
|
|
675
|
+
repository_root,
|
|
676
|
+
)
|
|
677
|
+
lint_report = lint(
|
|
678
|
+
LintRequest(repository_root, document_set),
|
|
679
|
+
all_registry=(_rule_named("subprocess-budget"),),
|
|
680
|
+
)
|
|
681
|
+
return tuple(
|
|
682
|
+
each_diagnostic.rule_id for each_diagnostic in lint_report.diagnostics
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def test_lint_reports_an_undercounted_budget_only_in_the_production_module(
|
|
687
|
+
tmp_path: Path,
|
|
688
|
+
) -> None:
|
|
689
|
+
_ensure_git_repository(tmp_path)
|
|
690
|
+
assert _subprocess_budget_rule_identifiers(tmp_path, "src/timing.py") == (
|
|
691
|
+
"subprocess-budget",
|
|
692
|
+
)
|
|
693
|
+
assert (
|
|
694
|
+
_subprocess_budget_rule_identifiers(
|
|
695
|
+
tmp_path, "packages/claude-dev-env/hooks/workflow/test_auto_formatter.py"
|
|
696
|
+
)
|
|
697
|
+
== ()
|
|
698
|
+
)
|
|
699
|
+
assert _subprocess_budget_rule_identifiers(tmp_path, "tests/timing.py") == ()
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import subprocess
|
|
7
|
+
import sys
|
|
7
8
|
from pathlib import Path, PurePosixPath
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
@@ -19,6 +20,9 @@ from policy_lint.model import (
|
|
|
19
20
|
)
|
|
20
21
|
from policy_lint.selection import SelectionRunFatal, select_documents
|
|
21
22
|
|
|
23
|
+
WINDOWS_NO_WINDOW_FLAG = 0x08000000
|
|
24
|
+
EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
|
|
25
|
+
|
|
22
26
|
NON_UTF8_PNG_BYTES = bytes(
|
|
23
27
|
(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0xFF, 0xFE, 0xFF)
|
|
24
28
|
)
|
|
@@ -501,3 +505,33 @@ def test_files_missing_path_should_still_raise(tmp_path: Path) -> None:
|
|
|
501
505
|
|
|
502
506
|
with pytest.raises(SelectionRunFatal, match="does not exist"):
|
|
503
507
|
select_documents(LintRequest.files(repository_root, [Path("missing.py")]))
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _record_git_creation_flags(monkeypatch: pytest.MonkeyPatch) -> dict[str, object]:
|
|
511
|
+
all_recorded_keyword_arguments: dict[str, object] = {}
|
|
512
|
+
|
|
513
|
+
def _record(*args: object, **kwargs: object) -> subprocess.CompletedProcess[bytes]:
|
|
514
|
+
del args
|
|
515
|
+
all_recorded_keyword_arguments.update(kwargs)
|
|
516
|
+
return subprocess.CompletedProcess([], 0, stdout=b"abc123\n", stderr=b"")
|
|
517
|
+
|
|
518
|
+
monkeypatch.setattr(selection_git.subprocess, "run", _record)
|
|
519
|
+
return all_recorded_keyword_arguments
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def test_git_bytes_for_should_start_without_a_console_window(
|
|
523
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
524
|
+
) -> None:
|
|
525
|
+
all_recorded_keyword_arguments = _record_git_creation_flags(monkeypatch)
|
|
526
|
+
|
|
527
|
+
assert selection_git.git_bytes_for(tmp_path, ("status",)) == b"abc123\n"
|
|
528
|
+
assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_head_revision_should_start_without_a_console_window(
|
|
532
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
533
|
+
) -> None:
|
|
534
|
+
all_recorded_keyword_arguments = _record_git_creation_flags(monkeypatch)
|
|
535
|
+
|
|
536
|
+
assert selection_git.head_revision(tmp_path) == "abc123"
|
|
537
|
+
assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Prove every deployed subprocess-window shim reaches the packaged helpers.
|
|
2
|
+
|
|
3
|
+
Each shim sits beside the scripts of one deployed directory. A deployed
|
|
4
|
+
profile carries no installed Python distribution, and the repository runs its
|
|
5
|
+
scripts under ``python -S -E``, which hides installed packages from the child.
|
|
6
|
+
So each check below drives a real ``-S -E`` child, imports the shim by the name
|
|
7
|
+
its callers use, and reads back where the helper came from.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
_PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
20
|
+
_SHIM_MODULE_NAME = "subprocess_window_access"
|
|
21
|
+
_SHIM_FILE_NAME = f"{_SHIM_MODULE_NAME}.py"
|
|
22
|
+
_PACKAGED_HELPER_PATH = (
|
|
23
|
+
_PACKAGE_ROOT / "hooks" / "hooks_constants" / "subprocess_window.py"
|
|
24
|
+
)
|
|
25
|
+
_ALL_EXPORTED_HELPER_NAMES = (
|
|
26
|
+
"HiddenWindowStartupInfo",
|
|
27
|
+
"detached_hidden_window_creation_flags",
|
|
28
|
+
"hidden_window_creation_flags",
|
|
29
|
+
"inherited_stream_startup_info",
|
|
30
|
+
)
|
|
31
|
+
_ALL_SHIM_DIRECTORY_PATHS = (
|
|
32
|
+
"scripts",
|
|
33
|
+
"_shared/pr-loop/scripts",
|
|
34
|
+
"_shared/process-tree/scripts",
|
|
35
|
+
"hooks/git-hooks",
|
|
36
|
+
"hooks/validators",
|
|
37
|
+
".agents/skills/fresh-branch/scripts",
|
|
38
|
+
".agents/skills/syncing-submodules/scripts",
|
|
39
|
+
".agents/skills/test-runner/scripts",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_IMPORT_PROBE_SOURCE = "\n".join(
|
|
43
|
+
(
|
|
44
|
+
"import json",
|
|
45
|
+
"import sys",
|
|
46
|
+
"shim_directory = sys.argv[1]",
|
|
47
|
+
"sys.path.insert(0, shim_directory)",
|
|
48
|
+
"head_before_import = sys.path[0]",
|
|
49
|
+
f"import {_SHIM_MODULE_NAME} as shim",
|
|
50
|
+
"print(json.dumps({",
|
|
51
|
+
" 'helper_path': sys.modules[",
|
|
52
|
+
" shim.hidden_window_creation_flags.__module__",
|
|
53
|
+
" ].__file__,",
|
|
54
|
+
" 'head_after_import': sys.path[0],",
|
|
55
|
+
" 'head_before_import': head_before_import,",
|
|
56
|
+
" 'exported_names': sorted(shim.__all__),",
|
|
57
|
+
"}))",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _import_probe_results(shim_directory: Path) -> dict[str, object]:
|
|
63
|
+
"""Import one shim in a ``-S -E`` child and read back what it resolved."""
|
|
64
|
+
completed_process = subprocess.run(
|
|
65
|
+
[
|
|
66
|
+
sys.executable,
|
|
67
|
+
"-S",
|
|
68
|
+
"-E",
|
|
69
|
+
"-c",
|
|
70
|
+
_IMPORT_PROBE_SOURCE,
|
|
71
|
+
str(shim_directory),
|
|
72
|
+
],
|
|
73
|
+
cwd=str(_PACKAGE_ROOT),
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
check=False,
|
|
77
|
+
)
|
|
78
|
+
assert completed_process.returncode == 0, completed_process.stderr
|
|
79
|
+
return json.loads(completed_process.stdout)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
|
|
83
|
+
def test_every_deployed_directory_carries_a_shim(shim_directory_path: str) -> None:
|
|
84
|
+
"""Each directory whose scripts import the shim also ships one."""
|
|
85
|
+
assert (_PACKAGE_ROOT / shim_directory_path / _SHIM_FILE_NAME).is_file()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
|
|
89
|
+
def test_shim_resolves_the_packaged_helper_without_an_installed_package(
|
|
90
|
+
shim_directory_path: str,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""A ``-S -E`` child reaches the packaged helper through the shim alone."""
|
|
93
|
+
all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
|
|
94
|
+
|
|
95
|
+
assert Path(str(all_probe_results["helper_path"])) == _PACKAGED_HELPER_PATH
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
|
|
99
|
+
def test_shim_leaves_the_head_of_the_search_path_alone(
|
|
100
|
+
shim_directory_path: str,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""A caller that prepended its own roots keeps them ahead of the hooks tree."""
|
|
103
|
+
all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
|
|
104
|
+
|
|
105
|
+
assert (
|
|
106
|
+
all_probe_results["head_after_import"]
|
|
107
|
+
== (all_probe_results["head_before_import"])
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.parametrize("shim_directory_path", _ALL_SHIM_DIRECTORY_PATHS)
|
|
112
|
+
def test_shim_re_exports_every_hidden_window_helper(
|
|
113
|
+
shim_directory_path: str,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Every caller finds the name it needs on the shim it imports."""
|
|
116
|
+
all_probe_results = _import_probe_results(_PACKAGE_ROOT / shim_directory_path)
|
|
117
|
+
|
|
118
|
+
assert all_probe_results["exported_names"] == sorted(_ALL_EXPORTED_HELPER_NAMES)
|