claude-dev-env 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +78 -0
- package/bin/install-session-continuity.test.mjs +42 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -1,418 +1,62 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
3
|
from pathlib import Path
|
|
8
4
|
|
|
9
5
|
import pre_commit
|
|
10
6
|
import pytest
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
encoding="utf-8",
|
|
19
|
-
)
|
|
20
|
-
return target_path
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@pytest.fixture()
|
|
24
|
-
def fake_gate_script_blocking(tmp_path: Path) -> Path:
|
|
25
|
-
return make_gate_script_returning(1, tmp_path / "fake_gate_blocking.py")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@pytest.fixture()
|
|
29
|
-
def fake_gate_script_passing(tmp_path: Path) -> Path:
|
|
30
|
-
return make_gate_script_returning(0, tmp_path / "fake_gate_passing.py")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_main_exits_zero_when_gate_script_missing(
|
|
34
|
-
tmp_path: Path,
|
|
35
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
36
|
-
) -> None:
|
|
37
|
-
monkeypatch.setenv(
|
|
38
|
-
"CODE_RULES_GATE_PATH",
|
|
39
|
-
str(tmp_path / "does_not_exist.py"),
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
exit_code = pre_commit.main()
|
|
43
|
-
|
|
44
|
-
assert exit_code == 0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_main_propagates_blocking_exit_code_from_gate(
|
|
48
|
-
fake_gate_script_blocking: Path,
|
|
49
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
50
|
-
) -> None:
|
|
51
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(fake_gate_script_blocking))
|
|
52
|
-
|
|
53
|
-
exit_code = pre_commit.main()
|
|
54
|
-
|
|
55
|
-
assert exit_code == 1
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def test_main_propagates_passing_exit_code_from_gate(
|
|
59
|
-
fake_gate_script_passing: Path,
|
|
60
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
61
|
-
) -> None:
|
|
62
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(fake_gate_script_passing))
|
|
63
|
-
|
|
64
|
-
exit_code = pre_commit.main()
|
|
65
|
-
|
|
66
|
-
assert exit_code == 0
|
|
7
|
+
from test_native_hook_support import (
|
|
8
|
+
configure_native_repository,
|
|
9
|
+
install_native_hook,
|
|
10
|
+
run_native_git_action,
|
|
11
|
+
run_native_hook,
|
|
12
|
+
)
|
|
13
|
+
from test_native_hook_support import run_git as run_shared_git
|
|
67
14
|
|
|
68
15
|
|
|
69
|
-
def
|
|
70
|
-
tmp_path: Path,
|
|
16
|
+
def test_main_always_returns_zero_when_notice_reports_failure(
|
|
71
17
|
monkeypatch: pytest.MonkeyPatch,
|
|
72
18
|
) -> None:
|
|
73
|
-
|
|
74
|
-
recording_gate_script_path = tmp_path / "recording_gate.py"
|
|
75
|
-
recording_gate_script_path.write_text(
|
|
76
|
-
"import sys, pathlib\n"
|
|
77
|
-
f'pathlib.Path(r"{recorded_arguments_path}").write_text('
|
|
78
|
-
"'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
|
|
79
|
-
"sys.exit(0)\n",
|
|
80
|
-
encoding="utf-8",
|
|
81
|
-
)
|
|
82
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
19
|
+
monkeypatch.setattr(pre_commit, "verification_notice_main", lambda *args, **kwargs: 1)
|
|
83
20
|
|
|
84
21
|
exit_code = pre_commit.main()
|
|
85
22
|
|
|
86
23
|
assert exit_code == 0
|
|
87
|
-
assert recorded_arguments_path.exists(), (
|
|
88
|
-
f"recording gate did not write to {recorded_arguments_path}"
|
|
89
|
-
)
|
|
90
|
-
recorded_arguments = recorded_arguments_path.read_text(
|
|
91
|
-
encoding="utf-8"
|
|
92
|
-
).splitlines()
|
|
93
|
-
assert recorded_arguments == ["--immediate"]
|
|
94
24
|
|
|
95
25
|
|
|
96
|
-
def
|
|
97
|
-
tmp_path: Path,
|
|
26
|
+
def test_main_requests_notice_without_starting_verification(
|
|
98
27
|
monkeypatch: pytest.MonkeyPatch,
|
|
99
28
|
) -> None:
|
|
100
|
-
|
|
101
|
-
existing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
102
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(existing_gate_path))
|
|
103
|
-
|
|
104
|
-
original_run = __import__("subprocess").run
|
|
105
|
-
|
|
106
|
-
def raising_run(*args: object, **kwargs: object) -> object:
|
|
107
|
-
raise OSError("no such file")
|
|
29
|
+
all_notice_arguments: list[list[str]] = []
|
|
108
30
|
|
|
109
|
-
|
|
31
|
+
def record_notice_arguments(all_arguments: list[str], **keyword_arguments: object) -> int:
|
|
32
|
+
all_notice_arguments.append(all_arguments)
|
|
33
|
+
return 0
|
|
110
34
|
|
|
111
|
-
|
|
35
|
+
monkeypatch.setattr(pre_commit, "verification_notice_main", record_notice_arguments)
|
|
112
36
|
|
|
113
|
-
assert
|
|
37
|
+
assert pre_commit.main() == 0
|
|
38
|
+
assert len(all_notice_arguments) == 1
|
|
39
|
+
assert "--notice-only" in all_notice_arguments[0]
|
|
114
40
|
|
|
115
41
|
|
|
116
|
-
def
|
|
117
|
-
tmp_path
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"CODE_RULES_GATE_PATH",
|
|
123
|
-
str(tmp_path / "does_not_exist.py"),
|
|
42
|
+
def test_native_git_commit_allows_missing_setup_and_prints_advisory(tmp_path: Path) -> None:
|
|
43
|
+
repository_path = tmp_path / "repo with spaces Ω"
|
|
44
|
+
repository_path.mkdir()
|
|
45
|
+
configure_native_repository(
|
|
46
|
+
repository_path,
|
|
47
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
124
48
|
)
|
|
49
|
+
hook_directory = tmp_path / "hooks"
|
|
50
|
+
install_native_hook(repository_path, hook_directory, "pre-commit", "pre_commit")
|
|
51
|
+
run_shared_git(repository_path, "add", "README.md")
|
|
125
52
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
assert exit_code == 0
|
|
129
|
-
captured = capsys.readouterr()
|
|
130
|
-
assert "gate script not found" in captured.err
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_invoke_gate_returns_infrastructure_failure_when_strict_resolve_raises(
|
|
134
|
-
tmp_path: Path,
|
|
135
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
136
|
-
) -> None:
|
|
137
|
-
missing_gate_path = tmp_path / "missing_gate.py"
|
|
138
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(missing_gate_path))
|
|
139
|
-
missing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
140
|
-
|
|
141
|
-
original_resolve = Path.resolve
|
|
142
|
-
|
|
143
|
-
def raising_resolve(self: Path, strict: bool = False) -> Path:
|
|
144
|
-
if strict and self == missing_gate_path.resolve():
|
|
145
|
-
raise FileNotFoundError("not found")
|
|
146
|
-
return original_resolve(self, strict=strict)
|
|
147
|
-
|
|
148
|
-
monkeypatch.setattr(Path, "resolve", raising_resolve)
|
|
149
|
-
|
|
150
|
-
exit_code = pre_commit.invoke_gate(missing_gate_path)
|
|
151
|
-
|
|
152
|
-
assert exit_code == 2
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def test_invoke_gate_uses_resolved_path(
|
|
156
|
-
tmp_path: Path,
|
|
157
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
158
|
-
) -> None:
|
|
159
|
-
real_gate_dir = tmp_path / "real"
|
|
160
|
-
real_gate_dir.mkdir()
|
|
161
|
-
real_gate_path = real_gate_dir / "gate.py"
|
|
162
|
-
recorded_path_file = tmp_path / "recorded_path.txt"
|
|
163
|
-
real_gate_path.write_text(
|
|
164
|
-
"import sys, pathlib\n"
|
|
165
|
-
f'pathlib.Path(r"{recorded_path_file}").write_text(sys.argv[0], encoding="utf-8")\n'
|
|
166
|
-
"sys.exit(0)\n",
|
|
167
|
-
encoding="utf-8",
|
|
168
|
-
)
|
|
169
|
-
symlink_gate_path = tmp_path / "link_gate.py"
|
|
170
|
-
symlink_gate_path.symlink_to(real_gate_path)
|
|
171
|
-
resolved_path = symlink_gate_path.resolve()
|
|
172
|
-
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(symlink_gate_path))
|
|
173
|
-
|
|
174
|
-
exit_code = pre_commit.main()
|
|
175
|
-
|
|
176
|
-
assert exit_code == 0
|
|
177
|
-
executed_path = recorded_path_file.read_text(encoding="utf-8")
|
|
178
|
-
assert executed_path == str(resolved_path)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
RMTREE_FIXTURE_KEYWORD = "ignore_errors"
|
|
182
|
-
ALL_RULE_FIXTURES = (
|
|
183
|
-
(
|
|
184
|
-
"code-rules",
|
|
185
|
-
"src/worker.py",
|
|
186
|
-
"from typing import Any\n\ndef worker() -> Any:\n return None\n",
|
|
187
|
-
),
|
|
188
|
-
(
|
|
189
|
-
"test-pairing",
|
|
190
|
-
"src/feature.py",
|
|
191
|
-
"def work() -> None:\n pass\n",
|
|
192
|
-
),
|
|
193
|
-
(
|
|
194
|
-
"rmtree-safety",
|
|
195
|
-
"src/cleanup.py",
|
|
196
|
-
f"import shutil\nshutil.rmtree('fixture', {RMTREE_FIXTURE_KEYWORD}=True)\n",
|
|
197
|
-
),
|
|
198
|
-
(
|
|
199
|
-
"state-description",
|
|
200
|
-
"docs/config.md",
|
|
201
|
-
"# Config\n\nPreviously set via env var.\n",
|
|
202
|
-
),
|
|
203
|
-
(
|
|
204
|
-
"subprocess-budget",
|
|
205
|
-
"src/timing.py",
|
|
206
|
-
"import subprocess\n"
|
|
207
|
-
"PYTHON_FORMAT_TIMEOUT_SECONDS = 12\n"
|
|
208
|
-
"GIT_CHECK_TIMEOUT_SECONDS = 5\n"
|
|
209
|
-
"def worst_case_python_format_seconds() -> int:\n"
|
|
210
|
-
" fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS\n"
|
|
211
|
-
" format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS\n"
|
|
212
|
-
" return fix_phase_seconds + format_phase_seconds\n"
|
|
213
|
-
"def is_untracked_in_git(file_path: str) -> bool:\n"
|
|
214
|
-
" git_check = subprocess.run(['git', 'ls-files', file_path], "
|
|
215
|
-
"timeout=GIT_CHECK_TIMEOUT_SECONDS)\n"
|
|
216
|
-
" return git_check.returncode != 0\n"
|
|
217
|
-
"def run_format(file_path: str) -> None:\n"
|
|
218
|
-
" subprocess.run(['ruff', 'format', file_path], "
|
|
219
|
-
"timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)\n"
|
|
220
|
-
"def main(file_path: str) -> None:\n"
|
|
221
|
-
" if is_untracked_in_git(file_path):\n"
|
|
222
|
-
" return\n"
|
|
223
|
-
" run_format(file_path)\n",
|
|
224
|
-
),
|
|
225
|
-
(
|
|
226
|
-
"hook-prose-consistency",
|
|
227
|
-
"hooks/hooks_constants/probe_constants.py",
|
|
228
|
-
'CORRECTIVE_MESSAGE = "appears as a path or output-key segment"\n',
|
|
229
|
-
),
|
|
230
|
-
(
|
|
231
|
-
"workflow-substitution",
|
|
232
|
-
"scripts/sample.workflow.js",
|
|
233
|
-
"For EACH candidate i, build a bible dir cand_i per the contract.\n"
|
|
234
|
-
" & ${PY} compose.py --out ${args.work_dir}\\\\cand_i\\\\sample.png "
|
|
235
|
-
"--glow <candidate glow_hex>\n"
|
|
236
|
-
'Return: {key: "cand_i", name, sample_png}\n',
|
|
237
|
-
),
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def _git(repository_root: Path, *all_arguments: str) -> str:
|
|
242
|
-
completed = subprocess.run(
|
|
243
|
-
["git", *all_arguments],
|
|
244
|
-
cwd=repository_root,
|
|
245
|
-
check=True,
|
|
246
|
-
capture_output=True,
|
|
247
|
-
text=True,
|
|
248
|
-
timeout=30,
|
|
249
|
-
)
|
|
250
|
-
return completed.stdout.strip()
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
@pytest.fixture()
|
|
254
|
-
def repository_root(
|
|
255
|
-
tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch,
|
|
256
|
-
) -> Path:
|
|
257
|
-
"""Create a repository with private Git configuration and no inherited hooks."""
|
|
258
|
-
tmp_path = tmp_path_factory.mktemp("policy-fixture")
|
|
259
|
-
home_directory = tmp_path / "home"
|
|
260
|
-
home_directory.mkdir()
|
|
261
|
-
monkeypatch.setenv("HOME", str(home_directory))
|
|
262
|
-
monkeypatch.setenv("USERPROFILE", str(home_directory))
|
|
263
|
-
monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull)
|
|
264
|
-
monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
|
|
265
|
-
monkeypatch.delenv("GIT_INDEX_FILE", raising=False)
|
|
266
|
-
monkeypatch.delenv("GIT_DIR", raising=False)
|
|
267
|
-
monkeypatch.delenv("GIT_WORK_TREE", raising=False)
|
|
268
|
-
root = tmp_path / "repository"
|
|
269
|
-
root.mkdir()
|
|
270
|
-
_git(root, "init")
|
|
271
|
-
_git(root, "config", "core.hooksPath", str(tmp_path / "disabled-fixture-hooks"))
|
|
272
|
-
_git(root, "config", "user.name", "Fixture")
|
|
273
|
-
_git(root, "config", "user.email", "fixture@example.com")
|
|
274
|
-
_git(root, "commit", "--allow-empty", "-m", "fixture base")
|
|
275
|
-
monkeypatch.chdir(root)
|
|
276
|
-
return root
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def _stage(repository_root: Path, relative_path: str, content: str) -> Path:
|
|
280
|
-
file_path = repository_root / relative_path
|
|
281
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
282
|
-
file_path.write_text(content, encoding="utf-8")
|
|
283
|
-
_git(repository_root, "add", "--", relative_path)
|
|
284
|
-
return file_path
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def _lint_report(repository_root: Path) -> tuple[int, dict[str, object]]:
|
|
288
|
-
completed = subprocess.run(
|
|
289
|
-
[
|
|
290
|
-
sys.executable,
|
|
291
|
-
str(pre_commit.resolve_policy_lint_script_path()),
|
|
292
|
-
"--staged",
|
|
293
|
-
"--format",
|
|
294
|
-
"json",
|
|
295
|
-
],
|
|
296
|
-
cwd=repository_root,
|
|
297
|
-
check=False,
|
|
298
|
-
capture_output=True,
|
|
299
|
-
text=True,
|
|
300
|
-
timeout=POLICY_LINT_TIMEOUT_SECONDS,
|
|
301
|
-
)
|
|
302
|
-
assert completed.stderr == "", completed.stderr
|
|
303
|
-
return completed.returncode, json.loads(completed.stdout)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
@pytest.mark.parametrize(("rule_id", "relative_path", "content"), ALL_RULE_FIXTURES)
|
|
307
|
-
def test_real_staged_linter_catches_each_replaced_rule(
|
|
308
|
-
repository_root: Path,
|
|
309
|
-
rule_id: str,
|
|
310
|
-
relative_path: str,
|
|
311
|
-
content: str,
|
|
312
|
-
) -> None:
|
|
313
|
-
_stage(repository_root, relative_path, content)
|
|
314
|
-
exit_code, report = _lint_report(repository_root)
|
|
315
|
-
assert exit_code == 1, report
|
|
316
|
-
assert report["failed_rules"] == [], report
|
|
317
|
-
all_diagnostics = report["diagnostics"]
|
|
318
|
-
assert isinstance(all_diagnostics, list)
|
|
319
|
-
assert rule_id in {each["rule_id"] for each in all_diagnostics}, report
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def _stage_valid_pair(repository_root: Path, relative_path: str) -> None:
|
|
323
|
-
source_path = Path(relative_path)
|
|
324
|
-
clean_content = {
|
|
325
|
-
".py": (
|
|
326
|
-
"def is_ready() -> bool:\n"
|
|
327
|
-
' """Report readiness.\n\n'
|
|
328
|
-
" Returns:\n True for the fixture.\n"
|
|
329
|
-
' """\n'
|
|
330
|
-
" return True\n"
|
|
331
|
-
),
|
|
332
|
-
".js": "export const isEnabled = true;\n",
|
|
333
|
-
".md": "# Config\n\nThe API uses port 8080.\n",
|
|
334
|
-
}[source_path.suffix]
|
|
335
|
-
_stage(repository_root, relative_path, clean_content)
|
|
336
|
-
paired_name = (
|
|
337
|
-
f"test_{source_path.stem}.py"
|
|
338
|
-
if source_path.suffix == ".py"
|
|
339
|
-
else f"{source_path.stem}.test{source_path.suffix}"
|
|
340
|
-
)
|
|
341
|
-
paired_content = (
|
|
342
|
-
f"from {source_path.stem} import is_ready\n\n"
|
|
343
|
-
"def test_is_ready() -> None:\n assert is_ready()\n"
|
|
344
|
-
if source_path.suffix == ".py"
|
|
345
|
-
else ""
|
|
346
|
-
)
|
|
347
|
-
_stage(repository_root, (source_path.parent / paired_name).as_posix(), paired_content)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
@pytest.mark.parametrize(("rule_id", "relative_path", "content"), ALL_RULE_FIXTURES)
|
|
351
|
-
def test_valid_paired_content_passes_the_real_local_commit_linter(
|
|
352
|
-
repository_root: Path,
|
|
353
|
-
rule_id: str,
|
|
354
|
-
relative_path: str,
|
|
355
|
-
content: str,
|
|
356
|
-
) -> None:
|
|
357
|
-
_stage_valid_pair(repository_root, relative_path)
|
|
358
|
-
assert pre_commit.run_staged_policy_lint() == 0
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def test_unstaged_fix_does_not_hide_staged_violation(repository_root: Path) -> None:
|
|
362
|
-
target = _stage(repository_root, "docs/config.md", "Previously set via env var.\n")
|
|
363
|
-
target.write_text("The API uses port 8080.\n", encoding="utf-8")
|
|
364
|
-
assert pre_commit.run_staged_policy_lint() == 1
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def test_alternate_git_index_is_checked(
|
|
368
|
-
repository_root: Path,
|
|
369
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
370
|
-
) -> None:
|
|
371
|
-
alternate_index = repository_root / ".git" / "alternate-index"
|
|
372
|
-
monkeypatch.setenv("GIT_INDEX_FILE", str(alternate_index))
|
|
373
|
-
_git(repository_root, "read-tree", "HEAD")
|
|
374
|
-
_stage(repository_root, "docs/config.md", "Previously set via env var.\n")
|
|
375
|
-
assert pre_commit.run_staged_policy_lint() == 1
|
|
376
|
-
monkeypatch.delenv("GIT_INDEX_FILE")
|
|
377
|
-
assert pre_commit.run_staged_policy_lint() == 0
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def test_missing_linter_does_not_allow_commit(
|
|
381
|
-
tmp_path: Path,
|
|
382
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
383
|
-
) -> None:
|
|
384
|
-
monkeypatch.setattr(
|
|
385
|
-
pre_commit, "resolve_policy_lint_script_path", lambda: tmp_path / "missing.py"
|
|
386
|
-
)
|
|
387
|
-
assert pre_commit.main() == 2
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
@pytest.mark.parametrize("exit_code", (1, 2, 3))
|
|
391
|
-
def test_linter_failure_reaches_git_before_legacy_gate(
|
|
392
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
393
|
-
exit_code: int,
|
|
394
|
-
) -> None:
|
|
395
|
-
monkeypatch.setattr(pre_commit, "run_staged_policy_lint", lambda: exit_code)
|
|
396
|
-
all_gate_calls: list[object] = []
|
|
397
|
-
monkeypatch.setattr(
|
|
398
|
-
pre_commit,
|
|
399
|
-
"resolve_gate_script_path",
|
|
400
|
-
lambda: all_gate_calls.append("unexpected gate"),
|
|
401
|
-
)
|
|
402
|
-
assert pre_commit.main() == exit_code
|
|
403
|
-
assert all_gate_calls == []
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
def test_timeout_is_an_infrastructure_failure(
|
|
407
|
-
tmp_path: Path,
|
|
408
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
409
|
-
) -> None:
|
|
410
|
-
script_path = tmp_path / "lint.py"
|
|
411
|
-
script_path.write_text("", encoding="utf-8")
|
|
412
|
-
monkeypatch.setattr(pre_commit, "resolve_policy_lint_script_path", lambda: script_path)
|
|
413
|
-
|
|
414
|
-
def timed_out(*all_arguments: object, **all_options: object) -> object:
|
|
415
|
-
raise subprocess.TimeoutExpired("lint fixture", POLICY_LINT_TIMEOUT_SECONDS)
|
|
53
|
+
completed_commit = run_native_git_action(repository_path, "commit", "-m", "initial")
|
|
416
54
|
|
|
417
|
-
|
|
418
|
-
|
|
55
|
+
current_head = run_shared_git(repository_path, "rev-parse", "HEAD").stdout.strip()
|
|
56
|
+
completed_hook = run_native_hook(repository_path, "pre_commit.py")
|
|
57
|
+
combined_output = completed_hook.stdout + completed_hook.stderr
|
|
58
|
+
assert completed_commit.returncode == 0
|
|
59
|
+
assert completed_hook.returncode == 0
|
|
60
|
+
assert "LOCAL VERIFICATION ADVISORY" in combined_output
|
|
61
|
+
assert f"Current SHA: {current_head}" in combined_output
|
|
62
|
+
assert "State: pending" in combined_output
|