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.
Files changed (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +189 -0
  6. package/.agents/skills/session-continuity/SKILL.md +114 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +375 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +78 -0
  20. package/bin/install-session-continuity.test.mjs +42 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
@@ -10,8 +10,7 @@ import sys
10
10
  from pathlib import Path
11
11
 
12
12
  import pytest
13
- from test_pre_commit import ALL_RULE_FIXTURES, _git, _stage
14
- from test_pre_commit import repository_root as repository_root
13
+ from test_native_hook_support import run_git
15
14
 
16
15
  ALL_RETIRED_HOOK_PATHS = (
17
16
  "blocking/code_rules_enforcer.py",
@@ -21,8 +20,53 @@ ALL_RETIRED_HOOK_PATHS = (
21
20
  "blocking/subprocess_budget_completeness.py",
22
21
  "blocking/hook_prose_detector_consistency.py",
23
22
  "blocking/workflow_substitution_slot_blocker.py",
23
+ "validation/hook_format_validator.py",
24
+ "blocking/open_questions_in_plans_blocker.py",
25
+ "blocking/docstring_rule_gate_count_blocker.py",
26
+ "blocking/plain_language_blocker.py",
27
+ "lifecycle/config_change_guard.py",
24
28
  )
25
29
  PACKAGE_ROOT = Path(__file__).resolve().parents[2]
30
+ ALL_RULE_FIXTURES = (
31
+ ("code-rules", "src/worker.py", "from typing import Any\n\ndef worker() -> Any:\n return None\n"),
32
+ ("test-pairing", "src/feature.py", "def work() -> None:\n pass\n"),
33
+ ("state-description", "docs/config.md", "# Config\n\nPreviously set via env var.\n"),
34
+ ("subprocess-budget", "src/timing.py", "import subprocess\nPYTHON_FORMAT_TIMEOUT_SECONDS = 12\nGIT_CHECK_TIMEOUT_SECONDS = 5\ndef worst_case_python_format_seconds() -> int:\n fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS\n format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS\n return fix_phase_seconds + format_phase_seconds\ndef is_untracked_in_git(file_path: str) -> bool:\n git_check = subprocess.run(['git', 'ls-files', file_path], timeout=GIT_CHECK_TIMEOUT_SECONDS)\n return git_check.returncode != 0\ndef run_format(file_path: str) -> None:\n subprocess.run(['ruff', 'format', file_path], timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)\ndef main(file_path: str) -> None:\n if is_untracked_in_git(file_path):\n return\n run_format(file_path)\n"),
35
+ ("hook-prose-consistency", "hooks/hooks_constants/probe_constants.py", 'CORRECTIVE_MESSAGE = "appears as a path or output-key segment"\n'),
36
+ ("workflow-substitution", "scripts/sample.workflow.js", "For EACH candidate i, build a bible dir cand_i per the contract.\n & ${PY} compose.py --out ${args.work_dir}\\\\cand_i\\\\sample.png --glow <candidate glow_hex>\nReturn: {key: \"cand_i\", name, sample_png}\n"),
37
+ )
38
+
39
+
40
+ def _git(repository_path: Path, *arguments: str) -> str:
41
+ return run_git(repository_path, *arguments).stdout.strip()
42
+
43
+
44
+ def _stage(repository_path: Path, relative_path: str, content: str) -> Path:
45
+ file_path = repository_path / relative_path
46
+ file_path.parent.mkdir(parents=True, exist_ok=True)
47
+ file_path.write_text(content, encoding="utf-8")
48
+ _git(repository_path, "add", "--", relative_path)
49
+ return file_path
50
+
51
+
52
+ @pytest.fixture()
53
+ def repository_root(tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch) -> Path:
54
+ temporary_root = tmp_path_factory.mktemp("policy-fixture")
55
+ home_directory = temporary_root / "home"
56
+ home_directory.mkdir()
57
+ monkeypatch.setenv("HOME", str(home_directory))
58
+ monkeypatch.setenv("USERPROFILE", str(home_directory))
59
+ monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull)
60
+ monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
61
+ repository_path = temporary_root / "repository"
62
+ repository_path.mkdir()
63
+ _git(repository_path, "init")
64
+ _git(repository_path, "config", "core.hooksPath", str(temporary_root / "disabled-fixture-hooks"))
65
+ _git(repository_path, "config", "user.name", "Fixture")
66
+ _git(repository_path, "config", "user.email", "fixture@example.com")
67
+ _git(repository_path, "commit", "--allow-empty", "-m", "fixture base")
68
+ monkeypatch.chdir(repository_path)
69
+ return repository_path
26
70
 
27
71
 
28
72
  def _legacy_settings_bytes(managed_root: Path, foreign_command: str) -> bytes:
@@ -115,7 +159,7 @@ def managed_installation(tmp_path_factory: pytest.TempPathFactory) -> Path:
115
159
 
116
160
 
117
161
  @pytest.mark.parametrize(("rule_id", "relative_path", "content"), ALL_RULE_FIXTURES)
118
- def test_installed_native_commit_catches_each_retired_file_rule(
162
+ def test_installed_linter_reports_each_retired_file_rule_before_soft_commit(
119
163
  managed_installation: Path,
120
164
  repository_root: Path,
121
165
  monkeypatch: pytest.MonkeyPatch,
@@ -128,13 +172,18 @@ def test_installed_native_commit_catches_each_retired_file_rule(
128
172
  monkeypatch.setenv("CODE_RULES_GATE_PATH", str(repository_root / "missing-old-gate.py"))
129
173
  before = _git(repository_root, "rev-parse", "HEAD")
130
174
  _stage(repository_root, relative_path, content)
175
+ lint_completed = subprocess.run(
176
+ [sys.executable, str(managed_installation / "scripts/cde_lint.py"), "--staged", "--format", "json"],
177
+ cwd=repository_root, capture_output=True, text=True, check=False, timeout=240,
178
+ )
179
+ assert lint_completed.returncode != 0
180
+ assert rule_id in lint_completed.stdout + lint_completed.stderr
131
181
  completed = subprocess.run(
132
182
  ["git", "commit", "-m", "invalid fixture"],
133
183
  cwd=repository_root, capture_output=True, text=True, check=False, timeout=240,
134
184
  )
135
- assert completed.returncode != 0
136
- assert rule_id in completed.stdout + completed.stderr
137
- assert _git(repository_root, "rev-parse", "HEAD") == before
185
+ assert completed.returncode == 0
186
+ assert _git(repository_root, "rev-parse", "HEAD") != before
138
187
 
139
188
 
140
189
  def test_installed_linter_and_native_commit_allow_valid_changes(
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ def run_git(
9
+ repository_path: Path,
10
+ *arguments: str,
11
+ ) -> subprocess.CompletedProcess[str]:
12
+ return subprocess.run(
13
+ ["git", *arguments],
14
+ cwd=repository_path,
15
+ check=True,
16
+ capture_output=True,
17
+ text=True,
18
+ encoding="utf-8",
19
+ )
20
+
21
+
22
+ def run_native_git_action(
23
+ repository_path: Path,
24
+ *arguments: str,
25
+ ) -> subprocess.CompletedProcess[str]:
26
+ return subprocess.run(
27
+ ["git", *arguments],
28
+ cwd=repository_path,
29
+ check=False,
30
+ capture_output=True,
31
+ text=True,
32
+ encoding="utf-8",
33
+ )
34
+
35
+
36
+ def run_native_hook(
37
+ repository_path: Path,
38
+ hook_script_name: str,
39
+ ) -> subprocess.CompletedProcess[str]:
40
+ hook_script_path = Path(__file__).resolve().parent / hook_script_name
41
+ return subprocess.run(
42
+ [sys.executable, str(hook_script_path)],
43
+ cwd=repository_path,
44
+ check=False,
45
+ capture_output=True,
46
+ text=True,
47
+ encoding="utf-8",
48
+ )
49
+
50
+
51
+ def read_git_head(repository_path: Path, reference_name: str) -> str:
52
+ return run_git(repository_path, "rev-parse", reference_name).stdout.strip()
53
+
54
+
55
+ def create_bare_repository(temporary_directory: Path) -> Path:
56
+ bare_repository_path = temporary_directory / "remote.git"
57
+ subprocess.run(
58
+ ["git", "init", "--bare", "--quiet", str(bare_repository_path)],
59
+ cwd=temporary_directory,
60
+ check=True,
61
+ capture_output=True,
62
+ text=True,
63
+ encoding="utf-8",
64
+ )
65
+ return bare_repository_path
66
+
67
+
68
+ def configure_remote_url_rewrite(
69
+ repository_path: Path,
70
+ bare_repository_path: Path,
71
+ remote_url: str,
72
+ ) -> None:
73
+ run_git(
74
+ repository_path,
75
+ "config",
76
+ f"url.{bare_repository_path.as_uri()}.insteadOf",
77
+ remote_url,
78
+ )
79
+
80
+
81
+ def configure_native_repository(repository_path: Path, remote_url: str) -> None:
82
+ run_git(repository_path, "init", "--quiet")
83
+ run_git(repository_path, "config", "user.email", "test@example.invalid")
84
+ run_git(repository_path, "config", "user.name", "Verification Test")
85
+ run_git(repository_path, "remote", "add", "origin", remote_url)
86
+ (repository_path / "README.md").write_text("check\n", encoding="utf-8")
87
+
88
+
89
+ def create_native_repository(
90
+ temporary_directory: Path,
91
+ repository_name: str,
92
+ remote_url: str,
93
+ initial_branch: str | None = None,
94
+ ) -> Path:
95
+ repository_path = temporary_directory / repository_name
96
+ repository_path.mkdir()
97
+ all_init_arguments = ["init", "--quiet"]
98
+ if initial_branch:
99
+ all_init_arguments.insert(1, f"--initial-branch={initial_branch}")
100
+ run_git(repository_path, *all_init_arguments)
101
+ run_git(repository_path, "config", "user.email", "test@example.invalid")
102
+ run_git(repository_path, "config", "user.name", "Verification Test")
103
+ run_git(repository_path, "remote", "add", "origin", remote_url)
104
+ (repository_path / "README.md").write_text("check\n", encoding="utf-8")
105
+ return repository_path
106
+
107
+
108
+ def install_native_hook(
109
+ repository_path: Path,
110
+ hook_directory: Path,
111
+ hook_name: str,
112
+ module_name: str,
113
+ ) -> None:
114
+ source_directory = Path(__file__).resolve().parent
115
+ hook_directory.mkdir()
116
+ shim_text = (
117
+ "#!/usr/bin/env python3\n"
118
+ "import sys\n"
119
+ "from pathlib import Path\n"
120
+ f"hook_source_directory = Path({str(source_directory)!r})\n"
121
+ "if str(hook_source_directory) not in sys.path:\n"
122
+ " sys.path.insert(0, str(hook_source_directory))\n"
123
+ f"import {module_name}\n"
124
+ f"sys.exit({module_name}.main())\n"
125
+ )
126
+ hook_path = hook_directory / hook_name
127
+ hook_path.write_text(shim_text, encoding="utf-8")
128
+ hook_path.chmod(0o755)
129
+ run_git(repository_path, "config", "core.hooksPath", str(hook_directory))
@@ -10,8 +10,31 @@ from pathlib import Path
10
10
 
11
11
  import pre_commit
12
12
  import pytest
13
- from test_pre_commit import _git, _stage
14
- from test_pre_commit import repository_root as repository_root
13
+ from test_native_hook_support import run_git
14
+
15
+
16
+ def _git(repository_path: Path, *arguments: str) -> str:
17
+ return run_git(repository_path, *arguments).stdout.strip()
18
+
19
+
20
+ def _stage(repository_path: Path, relative_path: str, content: str) -> Path:
21
+ file_path = repository_path / relative_path
22
+ file_path.parent.mkdir(parents=True, exist_ok=True)
23
+ file_path.write_text(content, encoding="utf-8")
24
+ _git(repository_path, "add", "--", relative_path)
25
+ return file_path
26
+
27
+
28
+ @pytest.fixture()
29
+ def repository_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
30
+ repository_path = tmp_path / "repository"
31
+ repository_path.mkdir()
32
+ _git(repository_path, "init")
33
+ _git(repository_path, "config", "user.name", "Fixture")
34
+ _git(repository_path, "config", "user.email", "fixture@example.com")
35
+ _git(repository_path, "commit", "--allow-empty", "-m", "fixture base")
36
+ monkeypatch.chdir(repository_path)
37
+ return repository_path
15
38
 
16
39
 
17
40
  @pytest.fixture()
@@ -66,7 +89,7 @@ def _assert_write_proceeds(repository_root: Path, relative_path: str, content: s
66
89
  assert edit_output.get("hookSpecificOutput", {}).get("permissionDecision") != "deny"
67
90
 
68
91
 
69
- def test_retired_write_check_allows_edit_but_local_commit_catches_violation(
92
+ def test_retired_write_check_allows_edit_but_explicit_lint_reports_violation(
70
93
  installed_native_hook: Path,
71
94
  ) -> None:
72
95
  relative_path = "docs/config.md"
@@ -74,6 +97,17 @@ def test_retired_write_check_allows_edit_but_local_commit_catches_violation(
74
97
  _assert_write_proceeds(installed_native_hook, relative_path, content)
75
98
  before = _git(installed_native_hook, "rev-parse", "HEAD")
76
99
  _stage(installed_native_hook, relative_path, content)
100
+ package_root = Path(pre_commit.__file__).resolve().parents[2]
101
+ lint_result = subprocess.run(
102
+ [sys.executable, str(package_root / "scripts/cde_lint.py"), "--staged", "--format", "json"],
103
+ cwd=installed_native_hook,
104
+ capture_output=True,
105
+ text=True,
106
+ check=False,
107
+ timeout=240,
108
+ )
109
+ assert lint_result.returncode != 0
110
+ assert "state-description" in lint_result.stdout + lint_result.stderr
77
111
  commit_result = subprocess.run(
78
112
  ["git", "commit", "-m", "invalid fixture"],
79
113
  cwd=installed_native_hook,
@@ -82,6 +116,5 @@ def test_retired_write_check_allows_edit_but_local_commit_catches_violation(
82
116
  check=False,
83
117
  timeout=240,
84
118
  )
85
- assert commit_result.returncode != 0
86
- assert "state-description" in commit_result.stdout + commit_result.stderr
87
- assert _git(installed_native_hook, "rev-parse", "HEAD") == before
119
+ assert commit_result.returncode == 0
120
+ assert _git(installed_native_hook, "rev-parse", "HEAD") != before
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import post_commit
6
+ import pytest
7
+ from test_native_hook_support import (
8
+ create_native_repository,
9
+ install_native_hook,
10
+ read_git_head,
11
+ run_git,
12
+ run_native_git_action,
13
+ )
14
+
15
+
16
+ def test_main_always_returns_zero_when_notice_reports_failure(
17
+ monkeypatch: pytest.MonkeyPatch,
18
+ ) -> None:
19
+ monkeypatch.setattr(post_commit, "verification_notice_main", lambda *args, **kwargs: 1)
20
+
21
+ exit_code = post_commit.main()
22
+
23
+ assert exit_code == 0
24
+
25
+
26
+ def test_native_git_commit_notice_reports_the_new_head(tmp_path: Path) -> None:
27
+ repository_path = create_native_repository(
28
+ tmp_path,
29
+ "repo",
30
+ "https://github.com/JonEcho/python-automation.git",
31
+ )
32
+ hook_directory = tmp_path / "hooks"
33
+ install_native_hook(repository_path, hook_directory, "post-commit", "post_commit")
34
+ run_git(repository_path, "add", "README.md")
35
+
36
+ completed_commit = run_native_git_action(repository_path, "commit", "-m", "initial")
37
+
38
+ current_head = read_git_head(repository_path, "HEAD")
39
+ combined_output = completed_commit.stdout + completed_commit.stderr
40
+ assert completed_commit.returncode == 0
41
+ assert "Event: commit" in combined_output
42
+ assert f"Current SHA: {current_head}" in combined_output
43
+ assert "State: pending" in combined_output