claude-dev-env 2.31.1 → 2.32.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 (46) hide show
  1. package/bin/install.test.mjs +11 -3
  2. package/docs/CODE_RULES.md +1 -1
  3. package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
  4. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
  5. package/hooks/blocking/code_rules_duplicate_body.py +13 -7
  6. package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
  7. package/hooks/blocking/shell_substitution_blocker.py +175 -8
  8. package/hooks/blocking/stop_dispatcher.py +6 -9
  9. package/hooks/blocking/tdd_enforcer.py +3 -1
  10. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
  11. package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
  12. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
  13. package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
  14. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +8 -1
  15. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +7 -2
  16. package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
  17. package/hooks/blocking/test_state_description_blocker.py +56 -71
  18. package/hooks/blocking/test_tdd_enforcer.py +67 -1
  19. package/hooks/hooks.json +2 -2
  20. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
  21. package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
  22. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
  23. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
  24. package/hooks/hooks_constants/state_file_lock.py +76 -0
  25. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
  26. package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
  27. package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
  28. package/hooks/observability/session_file_edit_tracker.py +2 -55
  29. package/hooks/observability/test_failure_recorder.py +219 -0
  30. package/hooks/observability/test_test_failure_recorder.py +173 -0
  31. package/hooks/validators/config/directory_exemption_constants.py +0 -9
  32. package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
  33. package/hooks/validators/mypy_integration.py +10 -36
  34. package/hooks/validators/project_roots.py +58 -0
  35. package/hooks/validators/run_all_validators.py +87 -33
  36. package/hooks/validators/system_temporary_roots.py +1 -1
  37. package/hooks/validators/test_run_all_validators.py +22 -0
  38. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
  39. package/hooks/validators/test_run_all_validators_reporting.py +66 -0
  40. package/package.json +1 -1
  41. package/rules/shell-invocation.md +1 -1
  42. package/rules/testing.md +4 -0
  43. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +9 -5
  44. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
  45. package/scripts/shared_tree_paths.py +59 -25
  46. package/scripts/test_shared_tree_paths.py +30 -0
@@ -30,6 +30,7 @@ import time
30
30
  from pathlib import Path
31
31
 
32
32
  from atomic_file_writer import write_text_atomically
33
+ from hooks_constants.state_file_lock import hold_state_file_lock
33
34
  from hooks_constants.session_edit_stage_gate_constants import (
34
35
  SESSION_ID_UNSAFE_CHARACTERS_PATTERN,
35
36
  STATE_FILE_ATOMIC_WRITE_SUFFIX,
@@ -45,6 +46,10 @@ from tdd_enforcer_parts.config.tdd_enforcer_constants import (
45
46
  REPOSITORY_ROOT_DIGEST_LENGTH,
46
47
  STORED_CHANGED_AT_KEY,
47
48
  STORED_CONTENT_HASH_KEY,
49
+ STORED_FAILED_AT_KEY,
50
+ STORED_FAILURE_COMMAND_KEY,
51
+ STORED_FAILURE_EXIT_STATUS_KEY,
52
+ STORED_FAILURE_HASH_KEY,
48
53
  )
49
54
 
50
55
 
@@ -96,6 +101,26 @@ def _recorded_entry(current_hash: str, current_time: float) -> dict[str, object]
96
101
  return {STORED_CONTENT_HASH_KEY: current_hash, STORED_CHANGED_AT_KEY: current_time}
97
102
 
98
103
 
104
+ def _has_matching_recorded_failure(
105
+ stored_entry: object, current_hash: str, current_time: float, freshness_seconds: int
106
+ ) -> bool:
107
+ """Return whether a recorded test failure still evidences the candidate's content.
108
+
109
+ A failure counts as evidence only while the candidate's content is
110
+ exactly what failed and the record has not aged past the freshness
111
+ window -- editing the test again, or letting the record go stale, drops
112
+ it as evidence and falls back to the ordinary content-hash tracking.
113
+ """
114
+ if not isinstance(stored_entry, dict):
115
+ return False
116
+ if stored_entry.get(STORED_FAILURE_HASH_KEY) != current_hash:
117
+ return False
118
+ failed_at = stored_entry.get(STORED_FAILED_AT_KEY)
119
+ if not isinstance(failed_at, (int, float)):
120
+ return False
121
+ return current_time - failed_at <= freshness_seconds
122
+
123
+
99
124
  def _is_qualifying_first_sight(
100
125
  candidate_path: Path, candidate_text: str, current_time: float, freshness_seconds: int
101
126
  ) -> bool:
@@ -107,6 +132,34 @@ def _is_qualifying_first_sight(
107
132
  return git_tracking.has_uncommitted_change_from_head(candidate_path)
108
133
 
109
134
 
135
+ def _first_sight_decision(
136
+ candidate_path: Path,
137
+ candidate_text: str,
138
+ current_time: float,
139
+ freshness_seconds: int,
140
+ current_hash: str,
141
+ ) -> tuple[bool, dict[str, object] | None]:
142
+ if not _is_qualifying_first_sight(
143
+ candidate_path, candidate_text, current_time, freshness_seconds
144
+ ):
145
+ return False, None
146
+ return True, _recorded_entry(current_hash, current_time)
147
+
148
+
149
+ def _content_hash_decision(
150
+ all_stored_entry_fields: dict[str, object],
151
+ current_hash: str,
152
+ current_time: float,
153
+ freshness_seconds: int,
154
+ ) -> tuple[bool, dict[str, object] | None]:
155
+ if current_hash != all_stored_entry_fields.get(STORED_CONTENT_HASH_KEY):
156
+ return True, _recorded_entry(current_hash, current_time)
157
+ stored_changed_at = all_stored_entry_fields.get(STORED_CHANGED_AT_KEY)
158
+ if not isinstance(stored_changed_at, (int, float)):
159
+ return False, None
160
+ return current_time - stored_changed_at <= freshness_seconds, None
161
+
162
+
110
163
  def _evaluate_candidate(
111
164
  candidate_path: Path,
112
165
  stored_entry: object,
@@ -117,18 +170,15 @@ def _evaluate_candidate(
117
170
  if candidate_text is None:
118
171
  return False, None
119
172
  current_hash = _content_hash(candidate_text)
120
- if not isinstance(stored_entry, dict):
121
- if _is_qualifying_first_sight(
122
- candidate_path, candidate_text, current_time, freshness_seconds
123
- ):
124
- return True, _recorded_entry(current_hash, current_time)
125
- return False, None
126
- if current_hash != stored_entry.get(STORED_CONTENT_HASH_KEY):
127
- return True, _recorded_entry(current_hash, current_time)
128
- stored_changed_at = stored_entry.get(STORED_CHANGED_AT_KEY)
129
- if not isinstance(stored_changed_at, (int, float)):
130
- return False, None
131
- return current_time - stored_changed_at <= freshness_seconds, None
173
+ if _has_matching_recorded_failure(
174
+ stored_entry, current_hash, current_time, freshness_seconds
175
+ ):
176
+ return True, None
177
+ if not isinstance(stored_entry, dict) or STORED_CONTENT_HASH_KEY not in stored_entry:
178
+ return _first_sight_decision(
179
+ candidate_path, candidate_text, current_time, freshness_seconds, current_hash
180
+ )
181
+ return _content_hash_decision(stored_entry, current_hash, current_time, freshness_seconds)
132
182
 
133
183
 
134
184
  def _apply_candidate_decisions(
@@ -142,7 +192,10 @@ def _apply_candidate_decisions(
142
192
  for each_candidate in all_candidates:
143
193
  candidate_key = _state_key_for(each_candidate)
144
194
  candidate_allowed, updated_entry = _evaluate_candidate(
145
- each_candidate, all_stored_hashes.get(candidate_key), current_time, freshness_seconds
195
+ each_candidate,
196
+ all_stored_hashes.get(candidate_key),
197
+ current_time,
198
+ freshness_seconds,
146
199
  )
147
200
  if candidate_allowed:
148
201
  is_allowed = True
@@ -162,19 +215,84 @@ def has_recorded_or_fresh_test(
162
215
 
163
216
  Args:
164
217
  all_candidates: Candidate test paths for the production file.
165
- session_id: Raw ``session_id`` from the hook payload.
166
- repository_root: Raw ``cwd`` from the hook payload.
218
+ session_id: Raw session_id from the hook payload.
219
+ repository_root: Raw cwd from the hook payload.
167
220
  freshness_seconds: Maximum age, in seconds, a stored hash may have.
168
221
 
169
222
  Returns:
170
- True when at least one candidate is a fresh, dirty first sighting, a
171
- recorded hash that now differs, or a recorded hash still in window.
223
+ True when at least one candidate carries a matching recorded test
224
+ failure, is a fresh dirty first sighting, has a recorded hash that
225
+ now differs, or has a recorded hash still in window.
172
226
  """
173
227
  state_file = _state_file_path(session_id, repository_root)
174
- all_stored_hashes = _load_state(state_file)
175
- is_allowed, state_changed = _apply_candidate_decisions(
176
- all_candidates, all_stored_hashes, time.time(), freshness_seconds
177
- )
178
- if state_changed:
179
- _atomic_write_state(state_file, all_stored_hashes)
228
+ with hold_state_file_lock(state_file):
229
+ all_stored_hashes = _load_state(state_file)
230
+ is_allowed, state_changed = _apply_candidate_decisions(
231
+ all_candidates, all_stored_hashes, time.time(), freshness_seconds
232
+ )
233
+ if state_changed:
234
+ _atomic_write_state(state_file, all_stored_hashes)
180
235
  return is_allowed
236
+
237
+
238
+ def _merged_failure_entry(
239
+ existing_entry: object,
240
+ candidate_text: str,
241
+ command: str,
242
+ exit_status: int,
243
+ current_time: float,
244
+ ) -> dict[str, object]:
245
+ merged_entry = dict(existing_entry) if isinstance(existing_entry, dict) else {}
246
+ merged_entry[STORED_FAILURE_HASH_KEY] = _content_hash(candidate_text)
247
+ merged_entry[STORED_FAILED_AT_KEY] = current_time
248
+ merged_entry[STORED_FAILURE_COMMAND_KEY] = command
249
+ merged_entry[STORED_FAILURE_EXIT_STATUS_KEY] = exit_status
250
+ return merged_entry
251
+
252
+
253
+ def _record_one_test_file_failure(
254
+ test_file_path: Path,
255
+ all_stored_hashes: dict[str, object],
256
+ command: str,
257
+ exit_status: int,
258
+ current_time: float,
259
+ ) -> bool:
260
+ candidate_text = freshness._read_candidate_text(test_file_path)
261
+ if candidate_text is None:
262
+ return False
263
+ candidate_key = _state_key_for(test_file_path)
264
+ all_stored_hashes[candidate_key] = _merged_failure_entry(
265
+ all_stored_hashes.get(candidate_key),
266
+ candidate_text,
267
+ command,
268
+ exit_status,
269
+ current_time,
270
+ )
271
+ return True
272
+
273
+
274
+ def record_test_command_failure(
275
+ all_test_file_paths: list[Path],
276
+ command: str,
277
+ exit_status: int,
278
+ session_id: str,
279
+ repository_root: str,
280
+ ) -> None:
281
+ """Record a failing test command as gate evidence, one entry per test file.
282
+
283
+ Keyed exactly as the gate looks candidates up, so a failure satisfies the
284
+ gate only for the file it names, never a sibling the command also ran.
285
+ Existing content-hash fields on that entry are preserved, not overwritten.
286
+ """
287
+ state_file = _state_file_path(session_id, repository_root)
288
+ current_time = time.time()
289
+ with hold_state_file_lock(state_file):
290
+ all_stored_hashes = _load_state(state_file)
291
+ all_recorded = [
292
+ _record_one_test_file_failure(
293
+ each_path, all_stored_hashes, command, exit_status, current_time
294
+ )
295
+ for each_path in all_test_file_paths
296
+ ]
297
+ if any(all_recorded):
298
+ _atomic_write_state(state_file, all_stored_hashes)
@@ -1,4 +1,11 @@
1
- """Behavioral tests for the content_hash_store parts module."""
1
+ """Behavioral tests for the content_hash_store parts module.
2
+
3
+ Covers the recorded-test-failure evidence path: a genuinely failing test
4
+ command outranks a stale mtime, never satisfies a candidate it did not
5
+ name, expires after the freshness window, and applies only while the
6
+ candidate's content still matches what actually failed. Also covers the
7
+ private decision helpers by name, so a rename keeps its own paired test.
8
+ """
2
9
 
3
10
  import json
4
11
  import os
@@ -141,3 +148,109 @@ def test_denies_when_no_candidate_file_exists(tmp_path: Path) -> None:
141
148
  )
142
149
 
143
150
  assert is_allowed is False
151
+
152
+
153
+ def test_recorded_failure_allows_a_stale_candidate(tmp_path: Path) -> None:
154
+ """RED-evidence: a genuinely recorded failure outranks a stale mtime."""
155
+ candidate = _write_fresh_test_file(tmp_path)
156
+ stale_timestamp = time.time() - 700
157
+ os.utime(candidate, (stale_timestamp, stale_timestamp))
158
+ session_id, repository_root = "session-failure-1", str(tmp_path)
159
+ content_hash_store.record_test_command_failure(
160
+ [candidate], "pytest test_orders.py", 1, session_id, repository_root
161
+ )
162
+
163
+ is_allowed = content_hash_store.has_recorded_or_fresh_test(
164
+ [candidate], session_id, repository_root, 600
165
+ )
166
+
167
+ assert is_allowed is True
168
+
169
+
170
+ def test_recorded_failure_stores_the_command_and_exit_status(tmp_path: Path) -> None:
171
+ candidate = _write_fresh_test_file(tmp_path)
172
+ session_id, repository_root = "session-failure-2", str(tmp_path)
173
+
174
+ content_hash_store.record_test_command_failure(
175
+ [candidate], "pytest test_orders.py -q", 1, session_id, repository_root
176
+ )
177
+
178
+ state_file = content_hash_store._state_file_path(session_id, repository_root)
179
+ state = json.loads(state_file.read_text())
180
+ entry = state[content_hash_store._state_key_for(candidate)]
181
+ assert entry[content_hash_store.STORED_FAILURE_COMMAND_KEY] == "pytest test_orders.py -q"
182
+ assert entry[content_hash_store.STORED_FAILURE_EXIT_STATUS_KEY] == 1
183
+
184
+
185
+ def test_recorded_failure_does_not_satisfy_a_different_candidate(tmp_path: Path) -> None:
186
+ """Anti-bypass: a failure recorded for one test file must not unlock a sibling."""
187
+ failing_candidate = tmp_path / "test_bar.py"
188
+ failing_candidate.write_text("def test_bar(): pass\n")
189
+ unrelated_candidate = tmp_path / "test_foo.py"
190
+ unrelated_candidate.write_text("def test_foo(): pass\n")
191
+ stale_timestamp = time.time() - 700
192
+ os.utime(unrelated_candidate, (stale_timestamp, stale_timestamp))
193
+ session_id, repository_root = "session-failure-3", str(tmp_path)
194
+ content_hash_store.record_test_command_failure(
195
+ [failing_candidate], "pytest test_bar.py", 1, session_id, repository_root
196
+ )
197
+
198
+ is_allowed = content_hash_store.has_recorded_or_fresh_test(
199
+ [unrelated_candidate], session_id, repository_root, 600
200
+ )
201
+
202
+ assert is_allowed is False
203
+
204
+
205
+ def test_recorded_failure_expires_after_the_freshness_window(tmp_path: Path) -> None:
206
+ candidate = _write_fresh_test_file(tmp_path)
207
+ stale_timestamp = time.time() - 700
208
+ os.utime(candidate, (stale_timestamp, stale_timestamp))
209
+ session_id, repository_root = "session-failure-4", str(tmp_path)
210
+ state_file = content_hash_store._state_file_path(session_id, repository_root)
211
+ candidate_key = content_hash_store._state_key_for(candidate)
212
+ stale_failed_at = time.time() - 700
213
+ state_file.write_text(
214
+ json.dumps(
215
+ {
216
+ candidate_key: {
217
+ content_hash_store.STORED_FAILURE_HASH_KEY: content_hash_store._content_hash(
218
+ candidate.read_text()
219
+ ),
220
+ content_hash_store.STORED_FAILED_AT_KEY: stale_failed_at,
221
+ }
222
+ }
223
+ )
224
+ )
225
+
226
+ is_allowed = content_hash_store.has_recorded_or_fresh_test(
227
+ [candidate], session_id, repository_root, 600
228
+ )
229
+
230
+ assert is_allowed is False
231
+
232
+
233
+ def test_recorded_failure_does_not_apply_once_content_no_longer_matches(
234
+ tmp_path: Path,
235
+ ) -> None:
236
+ """Anti-bypass: editing the test after a recorded RED clears that RED as evidence.
237
+
238
+ A bare failure-only entry (no prior content-hash tracking) must not fall
239
+ through to the unconditional hash-differs allow either -- it still needs a
240
+ real qualifying first sight (fresh mtime plus a real git-visible edit).
241
+ """
242
+ candidate = tmp_path / "test_orders.py"
243
+ candidate.write_text("def test_fulfill(): pass\n")
244
+ session_id, repository_root = "session-failure-5", str(tmp_path)
245
+ content_hash_store.record_test_command_failure(
246
+ [candidate], "pytest test_orders.py", 1, session_id, repository_root
247
+ )
248
+ candidate.write_text("def test_fulfill(): assert True\n")
249
+ stale_timestamp = time.time() - 700
250
+ os.utime(candidate, (stale_timestamp, stale_timestamp))
251
+
252
+ is_allowed = content_hash_store.has_recorded_or_fresh_test(
253
+ [candidate], session_id, repository_root, 600
254
+ )
255
+
256
+ assert is_allowed is False
@@ -0,0 +1,112 @@
1
+ """Behavior tests for the Bash PostToolUse dispatcher.
2
+
3
+ One interpreter start runs every hosted PostToolUse hook a Bash call fires.
4
+ The unit tests pin selection, call order, and crash isolation with a fake
5
+ runner; the end-to-end test drives the real hosted hooks through a real
6
+ payload and confirms the recorder's own real side effect lands.
7
+ ``hooks/conftest.py`` already puts the hooks and blocking directories on
8
+ sys.path for test collection, so this file needs no bootstrap of its own.
9
+ """
10
+
11
+ import io
12
+ import json
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ from bash_post_call_dispatcher import dispatch, main, select_applicable_entries
19
+ from hooks_constants.bash_pre_tool_use_dispatcher_constants import BASH_TOOL_NAME
20
+ from hooks_constants.hosted_hook_runner import HostedHookRun
21
+ from tdd_enforcer_parts import content_hash_store
22
+
23
+
24
+ def test_select_applicable_entries_returns_both_hosted_hooks_for_bash() -> None:
25
+ all_entries = select_applicable_entries(BASH_TOOL_NAME)
26
+ all_script_paths = [each_entry.script_relative_path for each_entry in all_entries]
27
+ assert all_script_paths == [
28
+ "blocking/gh_pr_author_restore.py",
29
+ "observability/test_failure_recorder.py",
30
+ ]
31
+
32
+
33
+ def test_select_applicable_entries_returns_none_for_a_write_tool() -> None:
34
+ assert select_applicable_entries("Write") == []
35
+
36
+
37
+ def test_dispatch_runs_every_hosted_hook_in_roster_order(
38
+ monkeypatch: pytest.MonkeyPatch,
39
+ ) -> None:
40
+ all_call_paths: list[str] = []
41
+
42
+ def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
43
+ del payload_text
44
+ all_call_paths.append(Path(script_path).name)
45
+ return HostedHookRun(captured_stdout="", did_crash=False)
46
+
47
+ monkeypatch.setattr("bash_post_call_dispatcher.run_hook_capturing_output", _fake_run_hook)
48
+
49
+ dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
50
+
51
+ assert all_call_paths == ["gh_pr_author_restore.py", "test_failure_recorder.py"]
52
+
53
+
54
+ def test_dispatch_runs_the_later_hook_after_an_earlier_one_crashes(
55
+ monkeypatch: pytest.MonkeyPatch,
56
+ ) -> None:
57
+ all_call_paths: list[str] = []
58
+
59
+ def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
60
+ del payload_text
61
+ script_name = Path(script_path).name
62
+ all_call_paths.append(script_name)
63
+ if script_name == "gh_pr_author_restore.py":
64
+ return HostedHookRun(captured_stdout="", did_crash=True)
65
+ return HostedHookRun(captured_stdout="", did_crash=False)
66
+
67
+ monkeypatch.setattr("bash_post_call_dispatcher.run_hook_capturing_output", _fake_run_hook)
68
+
69
+ dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
70
+
71
+ assert all_call_paths == ["gh_pr_author_restore.py", "test_failure_recorder.py"]
72
+
73
+
74
+ def test_main_exits_zero_and_writes_nothing_to_stdout(
75
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
76
+ ) -> None:
77
+ payload = json.dumps({"tool_name": "Write", "tool_input": {}})
78
+ monkeypatch.setattr(sys, "stdin", io.StringIO(payload))
79
+
80
+ with pytest.raises(SystemExit) as raised_exit:
81
+ main()
82
+
83
+ assert int(raised_exit.value.code or 0) == 0
84
+ assert capsys.readouterr().out == ""
85
+
86
+
87
+ def test_end_to_end_real_hosted_hooks_record_a_real_pytest_failure(tmp_path: Path) -> None:
88
+ """Drives the real, unfaked roster: both scripts run in one process."""
89
+ test_file = tmp_path / "test_orders.py"
90
+ test_file.write_text("def test_fulfill(): assert False\n")
91
+ session_id = "post-dispatcher-session"
92
+ failing_output = (
93
+ "Error: Exit code 1\n"
94
+ "FAILED test_orders.py::test_fulfill - AssertionError\n"
95
+ "1 failed in 0.02s"
96
+ )
97
+ payload = json.dumps(
98
+ {
99
+ "session_id": session_id,
100
+ "cwd": str(tmp_path),
101
+ "tool_name": "Bash",
102
+ "tool_input": {"command": "pytest test_orders.py"},
103
+ "tool_response": failing_output,
104
+ }
105
+ )
106
+
107
+ dispatch(payload, BASH_TOOL_NAME)
108
+
109
+ state_file = content_hash_store._state_file_path(session_id, str(tmp_path))
110
+ state = json.loads(state_file.read_text())
111
+ entry = state[content_hash_store._state_key_for(test_file)]
112
+ assert entry[content_hash_store.STORED_FAILURE_EXIT_STATUS_KEY] == 1
@@ -648,8 +648,11 @@ def test_should_read_no_function_when_the_file_holds_no_qualifying_helper() -> N
648
648
  pins the verdict such a file gets.
649
649
  """
650
650
  all_module_functions = ast.parse(NO_INLINE_DUPLICATE_SOURCE).body
651
+ dump_by_statement: dict[ast.stmt, str] = {}
651
652
  all_windows = [
652
- _duplicate_body_module._helper_match_window_dumps(each_function)
653
+ _duplicate_body_module._helper_match_window_dumps(
654
+ each_function, dump_by_statement
655
+ )
653
656
  for each_function in all_module_functions
654
657
  if isinstance(each_function, ast.FunctionDef | ast.AsyncFunctionDef)
655
658
  ]
@@ -657,6 +660,10 @@ def test_should_read_no_function_when_the_file_holds_no_qualifying_helper() -> N
657
660
  "this source must hold no qualifying helper for the skip to be exercised, "
658
661
  f"got windows: {all_windows}"
659
662
  )
663
+ assert dump_by_statement == {}, (
664
+ "a source with no qualifying helper must take no statement dumps, "
665
+ f"got {len(dump_by_statement)} memo entries"
666
+ )
660
667
  issues = check_same_file_inline_duplicate_body(
661
668
  NO_INLINE_DUPLICATE_SOURCE, "module.py"
662
669
  )
@@ -46,8 +46,13 @@ import state_description_blocker # noqa: E402, I001
46
46
  _STATE_DESCRIPTION_SCRIPT = str(_BLOCKING_DIR / "state_description_blocker.py")
47
47
  _DISPATCHER_SCRIPT = str(_BLOCKING_DIR / "pre_tool_use_dispatcher.py")
48
48
 
49
- _REPOSITORY_ROOT = str(Path(__file__).resolve().parents[4])
50
- _MARKDOWN_PATH = str(Path(_REPOSITORY_ROOT) / "docs" / "native_equivalence_probe.md")
49
+ from code_rules_enforcer_test_support import ( # noqa: E402
50
+ repository_file_path,
51
+ repository_root,
52
+ )
53
+
54
+ _REPOSITORY_ROOT = str(repository_root())
55
+ _MARKDOWN_PATH = repository_file_path("docs/native_equivalence_probe.md")
51
56
  _STATE_DESCRIPTION_ALLOW_CONTENT = "# Guide\n\nThe API uses port 8080.\n"
52
57
  _STATE_DESCRIPTION_DENY_CONTENT = "# Guide\n\nPreviously the system used port 8080.\n"
53
58
 
@@ -191,3 +191,108 @@ def test_denies_nested_command_substitution_without_arithmetic() -> None:
191
191
  }
192
192
  response = json.loads(_run_hook(payload).stdout)
193
193
  assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
194
+
195
+
196
+ def test_allows_backtick_inside_single_quoted_heredoc_body() -> None:
197
+ payload = {
198
+ "tool_name": "Bash",
199
+ "tool_input": {
200
+ "command": (
201
+ "cat > notes.md <<'EOF'\n"
202
+ "reStructuredText markup: ``inert literal``\n"
203
+ "EOF"
204
+ )
205
+ },
206
+ }
207
+ assert _run_hook(payload).stdout == ""
208
+
209
+
210
+ def test_allows_backtick_inside_tab_stripped_quoted_heredoc_body() -> None:
211
+ """A ``<<-`` opener closes on a tab-indented terminator, and stays inert.
212
+
213
+ The tab-strip rule decides which line ends the body. Getting it wrong
214
+ swallows the rest of the command or ends the body early, so the branch is
215
+ pinned here as well as in the shared pipeline that owns the rule.
216
+ """
217
+ payload = {
218
+ "tool_name": "Bash",
219
+ "tool_input": {
220
+ "command": (
221
+ "cat > notes.md <<-'EOF'\n"
222
+ "reStructuredText markup: ``inert literal``\n"
223
+ "\tEOF\n"
224
+ "echo done"
225
+ )
226
+ },
227
+ }
228
+ assert _run_hook(payload).stdout == ""
229
+
230
+
231
+ def test_denies_backtick_inside_bare_heredoc_body() -> None:
232
+ payload = {
233
+ "tool_name": "Bash",
234
+ "tool_input": {"command": "cat > notes.md <<EOF\nnow: `date`\nEOF"},
235
+ }
236
+ response = json.loads(_run_hook(payload).stdout)
237
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
238
+
239
+
240
+ def test_denies_command_substitution_inside_bare_heredoc_body() -> None:
241
+ payload = {
242
+ "tool_name": "Bash",
243
+ "tool_input": {
244
+ "command": "cat > notes.md <<EOF\nhead: $(git rev-parse HEAD)\nEOF"
245
+ },
246
+ }
247
+ response = json.loads(_run_hook(payload).stdout)
248
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
249
+
250
+
251
+ def test_denies_live_substitution_outside_a_quoted_heredoc() -> None:
252
+ payload = {
253
+ "tool_name": "Bash",
254
+ "tool_input": {
255
+ "command": (
256
+ "cat > notes.md <<'EOF'\n"
257
+ "inert ``literal``\n"
258
+ "EOF\n"
259
+ 'echo "$(git rev-parse HEAD)"'
260
+ )
261
+ },
262
+ }
263
+ response = json.loads(_run_hook(payload).stdout)
264
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
265
+
266
+
267
+ def test_allows_backslash_escaped_heredoc_delimiter_body() -> None:
268
+ payload = {
269
+ "tool_name": "Bash",
270
+ "tool_input": {
271
+ "command": "cat > notes.md <<\\EOF\ninert `text`\nEOF"
272
+ },
273
+ }
274
+ assert _run_hook(payload).stdout == ""
275
+
276
+
277
+ def test_denies_a_substitution_below_an_opener_inside_a_quoted_string() -> None:
278
+ """An opener spelled inside a quoted string opens no heredoc.
279
+
280
+ Bash runs the second line here. Reading the first line as an opener and
281
+ dropping everything below it hides a live substitution from the scan.
282
+ """
283
+ payload = {
284
+ "tool_name": "Bash",
285
+ "tool_input": {"command": 'echo "<<\'EOF\'"\necho $(whoami)'},
286
+ }
287
+ response = json.loads(_run_hook(payload).stdout)
288
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
289
+
290
+
291
+ def test_denies_that_bypass_even_when_a_terminator_line_follows() -> None:
292
+ """A terminator line below the substitution does not make the strip safe."""
293
+ payload = {
294
+ "tool_name": "Bash",
295
+ "tool_input": {"command": 'echo "<<\'EOF\'"\necho $(whoami)\nEOF'},
296
+ }
297
+ response = json.loads(_run_hook(payload).stdout)
298
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"