claude-dev-env 7.0.0 → 7.2.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 +213 -0
  6. package/.agents/skills/session-continuity/SKILL.md +125 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +447 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +373 -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 +88 -0
  20. package/bin/install-session-continuity.test.mjs +55 -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
@@ -1,537 +0,0 @@
1
- """Behavior tests for the session_edit_stage_gate PreToolUse hook.
2
-
3
- Each test builds a real git repository in a temporary directory, records a
4
- session-edit set in a redirected temp directory, and runs the hook script as a
5
- subprocess with a PreToolUse JSON payload on stdin — the exact production
6
- invocation path. Environment overrides point ``tempfile.gettempdir`` at the
7
- per-test temp directory (so the hook reads the test's session-edit file) and
8
- point the home directory at a per-test directory (so the block log stays out of
9
- the real home).
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import json
15
- import os
16
- import subprocess
17
- import sys
18
- from pathlib import Path
19
-
20
- _HOOK_DIR = Path(__file__).resolve().parent
21
- _HOOKS_TREE = _HOOK_DIR.parent
22
- for each_path in (str(_HOOKS_TREE),):
23
- if each_path not in sys.path:
24
- sys.path.insert(0, each_path)
25
-
26
- from hooks_constants.session_edit_stage_gate_constants import ( # noqa: E402
27
- ALL_EDITED_FILE_PATHS_KEY,
28
- SESSION_EDIT_FILE_PREFIX,
29
- SESSION_EDIT_FILE_SUFFIX,
30
- )
31
-
32
- HOOK_PATH = _HOOK_DIR / "session_edit_stage_gate.py"
33
- _SESSION_ID = "gate-session-1"
34
-
35
-
36
- def _clean_git_environment() -> dict[str, str]:
37
- return {
38
- each_key: each_value
39
- for each_key, each_value in os.environ.items()
40
- if not each_key.startswith("GIT_")
41
- }
42
-
43
-
44
- def run_git(repository_root: Path, *git_arguments: str) -> None:
45
- subprocess.run(
46
- ["git", "-C", str(repository_root), *git_arguments],
47
- check=True,
48
- capture_output=True,
49
- env=_clean_git_environment(),
50
- )
51
-
52
-
53
- def initialize_repository(repository_root: Path) -> None:
54
- run_git(repository_root, "init")
55
- run_git(repository_root, "config", "user.email", "tests@example.com")
56
- run_git(repository_root, "config", "user.name", "Gate Tests")
57
-
58
-
59
- def prepare_repository_with_unstaged_edit(repository_root: Path) -> Path:
60
- initialize_repository(repository_root)
61
- tracked_file = repository_root / "widget.py"
62
- tracked_file.write_text("x = 1\n", encoding="utf-8")
63
- run_git(repository_root, "add", "widget.py")
64
- run_git(repository_root, "commit", "-m", "add widget")
65
- tracked_file.write_text("x = 2\n", encoding="utf-8")
66
- return tracked_file
67
-
68
-
69
- def write_session_edits(temp_directory: Path, session_id: str, absolute_paths: list[Path]) -> None:
70
- edit_file = temp_directory / f"{SESSION_EDIT_FILE_PREFIX}{session_id}{SESSION_EDIT_FILE_SUFFIX}"
71
- payload = {ALL_EDITED_FILE_PATHS_KEY: [str(each_path) for each_path in absolute_paths]}
72
- edit_file.write_text(json.dumps(payload), encoding="utf-8")
73
-
74
-
75
- def run_hook(
76
- command: str,
77
- session_id: str,
78
- cwd: Path,
79
- temp_directory: Path,
80
- home_directory: Path,
81
- ) -> subprocess.CompletedProcess[str]:
82
- environment = _clean_git_environment()
83
- environment["TMP"] = str(temp_directory)
84
- environment["TEMP"] = str(temp_directory)
85
- environment["TMPDIR"] = str(temp_directory)
86
- environment["USERPROFILE"] = str(home_directory)
87
- environment["HOME"] = str(home_directory)
88
- payload = json.dumps({"session_id": session_id, "tool_input": {"command": command}})
89
- return subprocess.run(
90
- [sys.executable, str(HOOK_PATH)],
91
- check=False,
92
- input=payload,
93
- capture_output=True,
94
- text=True,
95
- cwd=str(cwd),
96
- env=environment,
97
- timeout=60,
98
- )
99
-
100
-
101
- def _make_directories(tmp_path: Path) -> tuple[Path, Path, Path]:
102
- repository_root = tmp_path / "repo"
103
- repository_root.mkdir()
104
- temp_directory = tmp_path / "tmp"
105
- temp_directory.mkdir()
106
- home_directory = tmp_path / "home"
107
- home_directory.mkdir()
108
- return repository_root, temp_directory, home_directory
109
-
110
-
111
- def parse_denial(hook_stdout: str) -> dict:
112
- return json.loads(hook_stdout)["hookSpecificOutput"]
113
-
114
-
115
- def test_denies_commit_dropping_session_edited_unstaged_file(tmp_path: Path) -> None:
116
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
117
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
118
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
119
- completed_hook = run_hook(
120
- "git commit -m update", _SESSION_ID, repository_root, temp_directory, home_directory
121
- )
122
- assert completed_hook.returncode == 0
123
- denial = parse_denial(completed_hook.stdout)
124
- assert denial["permissionDecision"] == "deny"
125
- assert "widget.py" in denial["permissionDecisionReason"]
126
-
127
-
128
- def test_deny_writes_hook_block_log(tmp_path: Path) -> None:
129
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
130
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
131
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
132
- run_hook("git commit -m update", _SESSION_ID, repository_root, temp_directory, home_directory)
133
- logs_directory = home_directory / ".claude" / "logs"
134
- all_log_files = list(logs_directory.glob("*.log"))
135
- assert all_log_files
136
- all_log_text = "".join(each_file.read_text(encoding="utf-8") for each_file in all_log_files)
137
- assert "session_edit_stage_gate.py" in all_log_text
138
-
139
-
140
- def test_allows_commit_when_session_edited_file_is_staged(tmp_path: Path) -> None:
141
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
142
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
143
- run_git(repository_root, "add", "widget.py")
144
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
145
- completed_hook = run_hook(
146
- "git commit -m update", _SESSION_ID, repository_root, temp_directory, home_directory
147
- )
148
- assert completed_hook.returncode == 0
149
- assert completed_hook.stdout.strip() == ""
150
-
151
-
152
- def test_allows_commit_all_flag(tmp_path: Path) -> None:
153
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
154
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
155
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
156
- completed_hook = run_hook(
157
- "git commit -a -m update", _SESSION_ID, repository_root, temp_directory, home_directory
158
- )
159
- assert completed_hook.returncode == 0
160
- assert completed_hook.stdout.strip() == ""
161
-
162
-
163
- def test_allows_non_commit_command_containing_git_commit_substring(tmp_path: Path) -> None:
164
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
165
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
166
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
167
- completed_hook = run_hook(
168
- 'grep -rn "git commit" .',
169
- _SESSION_ID,
170
- repository_root,
171
- temp_directory,
172
- home_directory,
173
- )
174
- assert completed_hook.returncode == 0
175
- assert completed_hook.stdout.strip() == ""
176
-
177
-
178
- def test_allows_all_flag_commit_after_bare_commit_token_segment(tmp_path: Path) -> None:
179
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
180
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
181
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
182
- completed_hook = run_hook(
183
- "echo commit && git commit -a -m update",
184
- _SESSION_ID,
185
- repository_root,
186
- temp_directory,
187
- home_directory,
188
- )
189
- assert completed_hook.returncode == 0
190
- assert completed_hook.stdout.strip() == ""
191
-
192
-
193
- def test_allows_non_git_command_with_git_and_commit_as_arguments(tmp_path: Path) -> None:
194
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
195
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
196
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
197
- completed_hook = run_hook(
198
- "echo git commit",
199
- _SESSION_ID,
200
- repository_root,
201
- temp_directory,
202
- home_directory,
203
- )
204
- assert completed_hook.returncode == 0
205
- assert completed_hook.stdout.strip() == ""
206
-
207
-
208
- def test_denies_commit_with_leading_environment_assignment(tmp_path: Path) -> None:
209
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
210
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
211
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
212
- completed_hook = run_hook(
213
- "GIT_AUTHOR_NAME=x git commit -m update",
214
- _SESSION_ID,
215
- repository_root,
216
- temp_directory,
217
- home_directory,
218
- )
219
- assert completed_hook.returncode == 0
220
- denial = parse_denial(completed_hook.stdout)
221
- assert denial["permissionDecision"] == "deny"
222
- assert "widget.py" in denial["permissionDecisionReason"]
223
-
224
-
225
- def test_denies_commit_with_no_space_message_containing_letter_a(tmp_path: Path) -> None:
226
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
227
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
228
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
229
- completed_hook = run_hook(
230
- 'git commit -m"add feature"',
231
- _SESSION_ID,
232
- repository_root,
233
- temp_directory,
234
- home_directory,
235
- )
236
- assert completed_hook.returncode == 0
237
- denial = parse_denial(completed_hook.stdout)
238
- assert denial["permissionDecision"] == "deny"
239
- assert "widget.py" in denial["permissionDecisionReason"]
240
-
241
-
242
- def test_denies_commit_with_no_space_message_option_form(tmp_path: Path) -> None:
243
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
244
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
245
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
246
- completed_hook = run_hook(
247
- "git commit -mupdate", _SESSION_ID, repository_root, temp_directory, home_directory
248
- )
249
- assert completed_hook.returncode == 0
250
- denial = parse_denial(completed_hook.stdout)
251
- assert denial["permissionDecision"] == "deny"
252
- assert "widget.py" in denial["permissionDecisionReason"]
253
-
254
-
255
- def test_allows_clustered_all_and_message_flags(tmp_path: Path) -> None:
256
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
257
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
258
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
259
- completed_hook = run_hook(
260
- "git commit -am update", _SESSION_ID, repository_root, temp_directory, home_directory
261
- )
262
- assert completed_hook.returncode == 0
263
- assert completed_hook.stdout.strip() == ""
264
-
265
-
266
- def test_allows_commit_when_add_stages_in_same_command(tmp_path: Path) -> None:
267
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
268
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
269
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
270
- completed_hook = run_hook(
271
- "git add widget.py && git commit -m update",
272
- _SESSION_ID,
273
- repository_root,
274
- temp_directory,
275
- home_directory,
276
- )
277
- assert completed_hook.returncode == 0
278
- assert completed_hook.stdout.strip() == ""
279
-
280
-
281
- def test_allows_commit_when_git_dash_c_add_stages_in_same_command(tmp_path: Path) -> None:
282
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
283
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
284
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
285
- completed_hook = run_hook(
286
- f'git -C "{repository_root}" add widget.py && git commit -m update',
287
- _SESSION_ID,
288
- repository_root,
289
- temp_directory,
290
- home_directory,
291
- )
292
- assert completed_hook.returncode == 0
293
- assert completed_hook.stdout.strip() == ""
294
-
295
-
296
- def test_denies_commit_when_specific_add_leaves_session_edit_unstaged(tmp_path: Path) -> None:
297
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
298
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
299
- readme_file = repository_root / "README.md"
300
- readme_file.write_text("readme\n", encoding="utf-8")
301
- run_git(repository_root, "add", "README.md")
302
- run_git(repository_root, "commit", "-m", "add readme")
303
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
304
- completed_hook = run_hook(
305
- "git add README.md && git commit -m update",
306
- _SESSION_ID,
307
- repository_root,
308
- temp_directory,
309
- home_directory,
310
- )
311
- assert completed_hook.returncode == 0
312
- denial = parse_denial(completed_hook.stdout)
313
- assert denial["permissionDecision"] == "deny"
314
- assert "widget.py" in denial["permissionDecisionReason"]
315
-
316
-
317
- def test_allows_commit_when_stage_all_add_precedes(tmp_path: Path) -> None:
318
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
319
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
320
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
321
- completed_hook = run_hook(
322
- "git add -A && git commit -m update",
323
- _SESSION_ID,
324
- repository_root,
325
- temp_directory,
326
- home_directory,
327
- )
328
- assert completed_hook.returncode == 0
329
- assert completed_hook.stdout.strip() == ""
330
-
331
-
332
- def test_allows_commit_when_dash_named_path_staged_after_end_of_options(tmp_path: Path) -> None:
333
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
334
- initialize_repository(repository_root)
335
- tracked_file = repository_root / "-weird.py"
336
- tracked_file.write_text("x = 1\n", encoding="utf-8")
337
- run_git(repository_root, "add", "--", "-weird.py")
338
- run_git(repository_root, "commit", "-m", "add weird")
339
- tracked_file.write_text("x = 2\n", encoding="utf-8")
340
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
341
- completed_hook = run_hook(
342
- "git add -- -weird.py && git commit -m update",
343
- _SESSION_ID,
344
- repository_root,
345
- temp_directory,
346
- home_directory,
347
- )
348
- assert completed_hook.returncode == 0
349
- assert completed_hook.stdout.strip() == ""
350
-
351
-
352
- def test_denies_when_dash_a_filename_staged_after_end_of_options(tmp_path: Path) -> None:
353
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
354
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
355
- dash_a_file = repository_root / "-A"
356
- dash_a_file.write_text("a\n", encoding="utf-8")
357
- run_git(repository_root, "add", "--", "-A")
358
- run_git(repository_root, "commit", "-m", "add dash-a file")
359
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
360
- completed_hook = run_hook(
361
- "git add -- -A && git commit -m update",
362
- _SESSION_ID,
363
- repository_root,
364
- temp_directory,
365
- home_directory,
366
- )
367
- assert completed_hook.returncode == 0
368
- denial = parse_denial(completed_hook.stdout)
369
- assert denial["permissionDecision"] == "deny"
370
- assert "widget.py" in denial["permissionDecisionReason"]
371
-
372
-
373
- def test_denies_commit_when_add_runs_after_commit_segment(tmp_path: Path) -> None:
374
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
375
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
376
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
377
- completed_hook = run_hook(
378
- "git commit -m update ; git add widget.py",
379
- _SESSION_ID,
380
- repository_root,
381
- temp_directory,
382
- home_directory,
383
- )
384
- assert completed_hook.returncode == 0
385
- denial = parse_denial(completed_hook.stdout)
386
- assert denial["permissionDecision"] == "deny"
387
- assert "widget.py" in denial["permissionDecisionReason"]
388
-
389
-
390
- def test_allows_pathspec_commit(tmp_path: Path) -> None:
391
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
392
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
393
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
394
- completed_hook = run_hook(
395
- "git commit -m update widget.py",
396
- _SESSION_ID,
397
- repository_root,
398
- temp_directory,
399
- home_directory,
400
- )
401
- assert completed_hook.returncode == 0
402
- assert completed_hook.stdout.strip() == ""
403
-
404
-
405
- def test_allows_partial_commit_marker(tmp_path: Path) -> None:
406
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
407
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
408
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
409
- completed_hook = run_hook(
410
- "git commit -m update # partial-commit",
411
- _SESSION_ID,
412
- repository_root,
413
- temp_directory,
414
- home_directory,
415
- )
416
- assert completed_hook.returncode == 0
417
- assert completed_hook.stdout.strip() == ""
418
-
419
-
420
- def test_denies_when_marker_not_trailing(tmp_path: Path) -> None:
421
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
422
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
423
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
424
- completed_hook = run_hook(
425
- "echo # partial-commit && git commit -m update",
426
- _SESSION_ID,
427
- repository_root,
428
- temp_directory,
429
- home_directory,
430
- )
431
- assert completed_hook.returncode == 0
432
- denial = parse_denial(completed_hook.stdout)
433
- assert denial["permissionDecision"] == "deny"
434
- assert "widget.py" in denial["permissionDecisionReason"]
435
-
436
-
437
- def test_denies_when_marker_only_inside_commit_message(tmp_path: Path) -> None:
438
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
439
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
440
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
441
- completed_hook = run_hook(
442
- 'git commit -m "fix # partial-commit substring handling"',
443
- _SESSION_ID,
444
- repository_root,
445
- temp_directory,
446
- home_directory,
447
- )
448
- assert completed_hook.returncode == 0
449
- denial = parse_denial(completed_hook.stdout)
450
- assert denial["permissionDecision"] == "deny"
451
- assert "widget.py" in denial["permissionDecisionReason"]
452
-
453
-
454
- def test_denies_commit_dropping_non_ascii_named_file(tmp_path: Path) -> None:
455
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
456
- initialize_repository(repository_root)
457
- tracked_file = repository_root / "café.py"
458
- tracked_file.write_text("x = 1\n", encoding="utf-8")
459
- run_git(repository_root, "add", "café.py")
460
- run_git(repository_root, "commit", "-m", "add cafe")
461
- tracked_file.write_text("x = 2\n", encoding="utf-8")
462
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
463
- completed_hook = run_hook(
464
- "git commit -m update", _SESSION_ID, repository_root, temp_directory, home_directory
465
- )
466
- assert completed_hook.returncode == 0
467
- denial = parse_denial(completed_hook.stdout)
468
- assert denial["permissionDecision"] == "deny"
469
- assert "café.py" in denial["permissionDecisionReason"]
470
-
471
-
472
- def test_allows_amend_still_gates(tmp_path: Path) -> None:
473
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
474
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
475
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
476
- completed_hook = run_hook(
477
- "git commit --amend -m update",
478
- _SESSION_ID,
479
- repository_root,
480
- temp_directory,
481
- home_directory,
482
- )
483
- assert completed_hook.returncode == 0
484
- denial = parse_denial(completed_hook.stdout)
485
- assert denial["permissionDecision"] == "deny"
486
-
487
-
488
- def test_allows_when_no_tracker_file(tmp_path: Path) -> None:
489
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
490
- prepare_repository_with_unstaged_edit(repository_root)
491
- completed_hook = run_hook(
492
- "git commit -m update", _SESSION_ID, repository_root, temp_directory, home_directory
493
- )
494
- assert completed_hook.returncode == 0
495
- assert completed_hook.stdout.strip() == ""
496
-
497
-
498
- def test_allows_non_commit_command(tmp_path: Path) -> None:
499
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
500
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
501
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
502
- completed_hook = run_hook(
503
- "git status", _SESSION_ID, repository_root, temp_directory, home_directory
504
- )
505
- assert completed_hook.returncode == 0
506
- assert completed_hook.stdout.strip() == ""
507
-
508
-
509
- def test_allows_when_unstaged_file_not_session_edited(tmp_path: Path) -> None:
510
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
511
- prepare_repository_with_unstaged_edit(repository_root)
512
- unrelated_path = repository_root / "unrelated.py"
513
- write_session_edits(temp_directory, _SESSION_ID, [unrelated_path.resolve()])
514
- completed_hook = run_hook(
515
- "git commit -m update", _SESSION_ID, repository_root, temp_directory, home_directory
516
- )
517
- assert completed_hook.returncode == 0
518
- assert completed_hook.stdout.strip() == ""
519
-
520
-
521
- def test_git_dash_c_form_resolves_repository(tmp_path: Path) -> None:
522
- repository_root, temp_directory, home_directory = _make_directories(tmp_path)
523
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
524
- write_session_edits(temp_directory, _SESSION_ID, [tracked_file.resolve()])
525
- elsewhere = tmp_path / "elsewhere"
526
- elsewhere.mkdir()
527
- completed_hook = run_hook(
528
- f'git -C "{repository_root}" commit -m update',
529
- _SESSION_ID,
530
- elsewhere,
531
- temp_directory,
532
- home_directory,
533
- )
534
- assert completed_hook.returncode == 0
535
- denial = parse_denial(completed_hook.stdout)
536
- assert denial["permissionDecision"] == "deny"
537
- assert "widget.py" in denial["permissionDecisionReason"]
@@ -1,64 +0,0 @@
1
- """Production-path staging tests for the session-edit stage gate."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from pathlib import Path
7
-
8
- _TEST_DIRECTORY = Path(__file__).resolve().parent
9
- if str(_TEST_DIRECTORY) not in sys.path:
10
- sys.path.insert(0, str(_TEST_DIRECTORY))
11
-
12
- from test_session_edit_stage_gate_support import ( # noqa: E402
13
- make_test_directories,
14
- parse_dispatcher_decision,
15
- prepare_repository_with_unstaged_edit,
16
- run_bash_dispatcher,
17
- run_tracker_event,
18
- read_tracker_paths,
19
- )
20
-
21
- SESSION_ID = "staging-production-session"
22
-
23
-
24
- def test_dispatcher_denies_tracker_recorded_unstaged_edit(tmp_path: Path) -> None:
25
- """The Bash dispatcher denies a commit for a tracker-recorded edit."""
26
- repository_root, temp_directory, home_directory = make_test_directories(tmp_path)
27
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
28
- tracker_run = run_tracker_event(
29
- tracked_file, SESSION_ID, repository_root, temp_directory, home_directory
30
- )
31
- assert tracker_run.returncode == 0
32
- assert read_tracker_paths(temp_directory, SESSION_ID) == [str(tracked_file.resolve())]
33
-
34
- dispatcher_run = run_bash_dispatcher(
35
- "git commit -m update",
36
- SESSION_ID,
37
- repository_root,
38
- temp_directory,
39
- home_directory,
40
- )
41
- assert dispatcher_run.returncode == 0
42
- decision, reason = parse_dispatcher_decision(dispatcher_run.stdout)
43
- assert decision == "deny"
44
- assert "widget.py" in reason
45
-
46
-
47
- def test_dispatcher_allows_specific_staging_segment(tmp_path: Path) -> None:
48
- """The Bash dispatcher accepts a commit preceded by a specific git add."""
49
- repository_root, temp_directory, home_directory = make_test_directories(tmp_path)
50
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
51
- tracker_run = run_tracker_event(
52
- tracked_file, SESSION_ID, repository_root, temp_directory, home_directory
53
- )
54
- assert tracker_run.returncode == 0
55
-
56
- dispatcher_run = run_bash_dispatcher(
57
- "git add widget.py && git commit -m update",
58
- SESSION_ID,
59
- repository_root,
60
- temp_directory,
61
- home_directory,
62
- )
63
- assert dispatcher_run.returncode == 0
64
- assert parse_dispatcher_decision(dispatcher_run.stdout) == ("", "")