claude-dev-env 8.10.0 → 8.11.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 (77) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +85 -3
  2. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +34 -0
  3. package/_shared/pr-loop/scripts/fix_pr_test_proof.py +132 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/fix_pr_test_proof_constants.py +43 -0
  5. package/_shared/pr-loop/scripts/test_fix_pr_test_proof.py +126 -0
  6. package/bin/codex-skill-load-block.mjs +45 -0
  7. package/bin/codex-skill-load-block.test.mjs +72 -0
  8. package/bin/install.agents-home.test.mjs +76 -1
  9. package/bin/install.mjs +53 -31
  10. package/bin/install.test.mjs +37 -0
  11. package/bin/install.transaction.test.mjs +29 -7
  12. package/bin/seed-codex-pstack-models.mjs +27 -0
  13. package/bin/seed-codex-pstack-models.test.mjs +72 -0
  14. package/docs/CODE_RULES.md +5 -1
  15. package/hooks/blocking/code_rules_enforcer.py +1 -3
  16. package/hooks/blocking/code_rules_magic_values.py +1 -3
  17. package/hooks/blocking/code_rules_paired_test.py +5 -6
  18. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -1
  19. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -46
  20. package/hooks/blocking/test_pre_tool_use_dispatcher.py +2 -40
  21. package/hooks/followup_ledger.py +8 -1
  22. package/hooks/git-hooks/test_installed_policy_lint.py +4 -2
  23. package/hooks/hooks.json +15 -0
  24. package/hooks/hooks_constants/followup_ledger_constants.py +4 -0
  25. package/hooks/hooks_constants/messages.py +0 -1
  26. package/hooks/hooks_constants/nested_project_hooks_constants.py +37 -0
  27. package/hooks/hooks_constants/test_messages.py +1 -7
  28. package/hooks/lifecycle/nested_project_hooks.py +395 -0
  29. package/hooks/lifecycle/test_nested_project_hooks.py +251 -0
  30. package/hooks/test_followup_ledger.py +18 -0
  31. package/package.json +2 -1
  32. package/pstack-codex-models.md +37 -0
  33. package/rules/bdd.md +1 -1
  34. package/rules/build-needs-a-user.md +53 -0
  35. package/rules/code-standards.md +1 -1
  36. package/rules/flag-non-breaking-findings.md +7 -0
  37. package/rules/paired-test-coverage.md +4 -2
  38. package/rules/testing.md +5 -1
  39. package/scripts/cde_lint.py +47 -1
  40. package/scripts/policy_lint/adapter_callers.py +124 -0
  41. package/scripts/policy_lint/adapter_pairing.py +4 -2
  42. package/scripts/policy_lint/adapter_support.py +4 -3
  43. package/scripts/policy_lint/adapters.py +15 -0
  44. package/scripts/policy_lint/check_catalog.py +24 -0
  45. package/scripts/policy_lint/config/approved_test_pairs.py +3 -2
  46. package/scripts/policy_lint/config/check_catalog_constants.py +13 -0
  47. package/scripts/policy_lint/config/constants.py +18 -0
  48. package/scripts/policy_lint/engine.py +9 -3
  49. package/scripts/policy_lint/model.py +6 -2
  50. package/scripts/policy_lint/registry.py +6 -0
  51. package/scripts/policy_lint/selection.py +2 -0
  52. package/scripts/tests/test_cde_lint.py +50 -0
  53. package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
  54. package/scripts/tests/test_policy_lint_rules_callers.py +126 -0
  55. package/scripts/tests/test_policy_lint_rules_check_catalog.py +25 -1
  56. package/scripts/tests/test_policy_lint_rules_pairing.py +31 -2
  57. package/scripts/tests/test_policy_lint_selection.py +35 -0
  58. package/hooks/blocking/tdd_enforcer.py +0 -195
  59. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +0 -142
  60. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +0 -1
  61. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +0 -49
  62. package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +0 -298
  63. package/hooks/blocking/tdd_enforcer_parts/decisions.py +0 -96
  64. package/hooks/blocking/tdd_enforcer_parts/freshness.py +0 -95
  65. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +0 -132
  66. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +0 -119
  67. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +0 -31
  68. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +0 -256
  69. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +0 -46
  70. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +0 -28
  71. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +0 -111
  72. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +0 -36
  73. package/hooks/blocking/test_tdd_enforcer.py +0 -1506
  74. package/hooks/blocking/test_tdd_enforcer_restore.py +0 -108
  75. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +0 -105
  76. package/scripts/codex_compat_watcher.py +0 -502
  77. package/scripts/tests/test_codex_compat_watcher.py +0 -599
@@ -1,256 +0,0 @@
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
- """
9
-
10
- import json
11
- import os
12
- import time
13
- from pathlib import Path
14
-
15
- from tdd_enforcer_parts import content_hash_store
16
-
17
-
18
- def _write_fresh_test_file(directory: Path, content: str = "def test_fulfill(): pass\n") -> Path:
19
- candidate = directory / "test_orders.py"
20
- candidate.write_text(content)
21
- return candidate
22
-
23
-
24
- def test_state_file_path_differs_by_repository_root(tmp_path: Path) -> None:
25
- first_root_path = content_hash_store._state_file_path("session-a", str(tmp_path / "one"))
26
- second_root_path = content_hash_store._state_file_path("session-a", str(tmp_path / "two"))
27
- assert first_root_path != second_root_path
28
-
29
-
30
- def test_state_file_path_is_stable_for_the_same_inputs(tmp_path: Path) -> None:
31
- first_lookup = content_hash_store._state_file_path("session-a", str(tmp_path))
32
- second_lookup = content_hash_store._state_file_path("session-a", str(tmp_path))
33
- assert first_lookup == second_lookup
34
-
35
-
36
- def test_first_sight_allows_and_stores_a_fresh_dirty_test(tmp_path: Path) -> None:
37
- candidate = _write_fresh_test_file(tmp_path)
38
-
39
- is_allowed = content_hash_store.has_recorded_or_fresh_test(
40
- [candidate], "session-1", str(tmp_path), 600
41
- )
42
-
43
- assert is_allowed is True
44
- state_file = content_hash_store._state_file_path("session-1", str(tmp_path))
45
- state = json.loads(state_file.read_text())
46
- assert content_hash_store._state_key_for(candidate) in state
47
-
48
-
49
- def test_first_sight_denies_when_test_evidence_is_missing(tmp_path: Path) -> None:
50
- candidate = _write_fresh_test_file(tmp_path, content="orders = 'not a test'\n")
51
-
52
- is_allowed = content_hash_store.has_recorded_or_fresh_test(
53
- [candidate], "session-2", str(tmp_path), 600
54
- )
55
-
56
- assert is_allowed is False
57
-
58
-
59
- def test_first_sight_denies_when_test_file_is_stale(tmp_path: Path) -> None:
60
- candidate = _write_fresh_test_file(tmp_path)
61
- stale_timestamp = time.time() - 700
62
- os.utime(candidate, (stale_timestamp, stale_timestamp))
63
-
64
- is_allowed = content_hash_store.has_recorded_or_fresh_test(
65
- [candidate], "session-3", str(tmp_path), 600
66
- )
67
-
68
- assert is_allowed is False
69
-
70
-
71
- def test_second_sight_denies_a_touch_that_leaves_content_unchanged(tmp_path: Path) -> None:
72
- candidate = _write_fresh_test_file(tmp_path)
73
- session_id, repository_root = "session-4", str(tmp_path)
74
- assert content_hash_store.has_recorded_or_fresh_test(
75
- [candidate], session_id, repository_root, 600
76
- )
77
- state_file = content_hash_store._state_file_path(session_id, repository_root)
78
- state = json.loads(state_file.read_text())
79
- candidate_key = content_hash_store._state_key_for(candidate)
80
- state[candidate_key][content_hash_store.STORED_CHANGED_AT_KEY] -= 700
81
- state_file.write_text(json.dumps(state))
82
- candidate.touch()
83
-
84
- is_allowed = content_hash_store.has_recorded_or_fresh_test(
85
- [candidate], session_id, repository_root, 600
86
- )
87
-
88
- assert is_allowed is False
89
-
90
-
91
- def test_second_sight_allows_when_content_actually_changed_after_the_window(
92
- tmp_path: Path,
93
- ) -> None:
94
- candidate = _write_fresh_test_file(tmp_path)
95
- session_id, repository_root = "session-5", str(tmp_path)
96
- assert content_hash_store.has_recorded_or_fresh_test(
97
- [candidate], session_id, repository_root, 600
98
- )
99
- state_file = content_hash_store._state_file_path(session_id, repository_root)
100
- state = json.loads(state_file.read_text())
101
- candidate_key = content_hash_store._state_key_for(candidate)
102
- state[candidate_key][content_hash_store.STORED_CHANGED_AT_KEY] -= 700
103
- state_file.write_text(json.dumps(state))
104
- candidate.write_text("def test_fulfill(): assert True\n")
105
-
106
- is_allowed = content_hash_store.has_recorded_or_fresh_test(
107
- [candidate], session_id, repository_root, 600
108
- )
109
-
110
- assert is_allowed is True
111
-
112
-
113
- def test_second_sight_allows_a_repeat_write_within_the_window_untouched(
114
- tmp_path: Path,
115
- ) -> None:
116
- candidate = _write_fresh_test_file(tmp_path)
117
- session_id, repository_root = "session-6", str(tmp_path)
118
- assert content_hash_store.has_recorded_or_fresh_test(
119
- [candidate], session_id, repository_root, 600
120
- )
121
-
122
- is_allowed = content_hash_store.has_recorded_or_fresh_test(
123
- [candidate], session_id, repository_root, 600
124
- )
125
-
126
- assert is_allowed is True
127
-
128
-
129
- def test_any_candidate_satisfying_the_gate_allows_the_write(tmp_path: Path) -> None:
130
- stale_candidate = _write_fresh_test_file(tmp_path)
131
- stale_timestamp = time.time() - 700
132
- os.utime(stale_candidate, (stale_timestamp, stale_timestamp))
133
- fresh_candidate = tmp_path / "orders_test.py"
134
- fresh_candidate.write_text("def test_fulfill(): pass\n")
135
-
136
- is_allowed = content_hash_store.has_recorded_or_fresh_test(
137
- [stale_candidate, fresh_candidate], "session-7", str(tmp_path), 600
138
- )
139
-
140
- assert is_allowed is True
141
-
142
-
143
- def test_denies_when_no_candidate_file_exists(tmp_path: Path) -> None:
144
- missing_candidate = tmp_path / "test_missing.py"
145
-
146
- is_allowed = content_hash_store.has_recorded_or_fresh_test(
147
- [missing_candidate], "session-8", str(tmp_path), 600
148
- )
149
-
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
@@ -1,46 +0,0 @@
1
- """Behavioral tests for the decisions parts module."""
2
-
3
- import json
4
- from pathlib import Path
5
-
6
- import pytest
7
- from tdd_enforcer_parts import decisions
8
-
9
-
10
- def test_build_deny_reason_names_candidates_and_production_path() -> None:
11
- reason = decisions.build_deny_reason(Path("pkg/orders.py"), [Path("pkg/test_orders.py")])
12
- assert "orders.py" in reason
13
- assert "test_orders.py" in reason
14
- assert "propose" in reason.lower() or "enhancement" in reason.lower()
15
-
16
-
17
- def test_build_deny_reason_points_exemption_guidance_at_entry_hook() -> None:
18
- reason = decisions.build_deny_reason(Path("pkg/orders.py"), [])
19
- assert "tdd_enforcer.py" in reason
20
- assert "decisions.py" not in reason
21
-
22
-
23
- def test_build_deny_reason_names_content_not_only_modification_time() -> None:
24
- reason = decisions.build_deny_reason(Path("pkg/orders.py"), [Path("pkg/test_orders.py")])
25
- assert "content" in reason.lower()
26
-
27
-
28
- def test_build_deny_reason_covers_first_sight_and_stale_hash_and_names_the_fix() -> None:
29
- reason = decisions.build_deny_reason(Path("pkg/orders.py"), [Path("pkg/test_orders.py")]).lower()
30
- assert "fresh" in reason
31
- assert "change" in reason
32
- assert "touch" in reason
33
-
34
-
35
- def test_emit_allow_writes_allow_decision(capsys: pytest.CaptureFixture[str]) -> None:
36
- decisions.emit_allow()
37
- parsed = json.loads(capsys.readouterr().out)
38
- assert parsed["hookSpecificOutput"]["permissionDecision"] == "allow"
39
-
40
-
41
- def test_emit_deny_writes_deny_and_system_message(capsys: pytest.CaptureFixture[str]) -> None:
42
- decisions.emit_deny("blocked because no fresh test")
43
- parsed = json.loads(capsys.readouterr().out)
44
- assert parsed["hookSpecificOutput"]["permissionDecision"] == "deny"
45
- assert parsed["suppressOutput"] is True
46
- assert isinstance(parsed["systemMessage"], str)
@@ -1,28 +0,0 @@
1
- """Behavioral tests for the freshness parts module."""
2
-
3
- import os
4
- import time
5
- from pathlib import Path
6
-
7
- from tdd_enforcer_parts import freshness
8
-
9
-
10
- def test_has_fresh_test_true_for_recent_test_file(tmp_path: Path) -> None:
11
- test_file = tmp_path / "test_orders.py"
12
- test_file.write_text("def test_x(): pass\n")
13
- assert freshness.has_fresh_test([test_file], freshness._freshness_seconds()) is True
14
-
15
-
16
- def test_has_fresh_test_false_for_stale_test_file(tmp_path: Path) -> None:
17
- test_file = tmp_path / "test_orders.py"
18
- test_file.write_text("def test_x(): pass\n")
19
- window = freshness._freshness_seconds()
20
- stale_timestamp = time.time() - window - 60
21
- os.utime(test_file, (stale_timestamp, stale_timestamp))
22
- assert freshness.has_fresh_test([test_file], window) is False
23
-
24
-
25
- def test_has_fresh_test_false_when_no_test_evidence(tmp_path: Path) -> None:
26
- test_file = tmp_path / "test_orders.py"
27
- test_file.write_text("x = 1\n")
28
- assert freshness.has_fresh_test([test_file], freshness._freshness_seconds()) is False
@@ -1,111 +0,0 @@
1
- """Behavioral tests for the git_tracking parts module."""
2
-
3
- import os
4
- import subprocess
5
- from pathlib import Path
6
-
7
- from tdd_enforcer_parts import git_tracking
8
-
9
-
10
- def _git(repository_root: Path, *arguments: str) -> None:
11
- subprocess.run(
12
- ["git", *arguments],
13
- cwd=str(repository_root),
14
- check=True,
15
- capture_output=True,
16
- env={**os.environ, "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_SYSTEM": os.devnull},
17
- )
18
-
19
-
20
- def _init_repository(repository_root: Path) -> None:
21
- _git(repository_root, "init", "-q")
22
- _git(repository_root, "config", "user.email", "test@example.com")
23
- _git(repository_root, "config", "user.name", "Test")
24
- _git(repository_root, "config", "commit.gpgsign", "false")
25
-
26
-
27
- def test_absent_but_tracked_true_after_committed_file_removed(tmp_path: Path) -> None:
28
- _init_repository(tmp_path)
29
- tracked = tmp_path / "service.py"
30
- tracked.write_text("def serve(): return 1\n")
31
- _git(tmp_path, "add", "-A")
32
- _git(tmp_path, "commit", "-q", "-m", "init")
33
- tracked.unlink()
34
- assert git_tracking.is_absent_but_tracked(tracked) is True
35
-
36
-
37
- def test_absent_but_tracked_false_for_untracked_absent_file(tmp_path: Path) -> None:
38
- _init_repository(tmp_path)
39
- assert git_tracking.is_absent_but_tracked(tmp_path / "ghost.py") is False
40
-
41
-
42
- def test_absent_but_tracked_false_when_file_present(tmp_path: Path) -> None:
43
- _init_repository(tmp_path)
44
- present = tmp_path / "present.py"
45
- present.write_text("def here(): return 1\n")
46
- _git(tmp_path, "add", "-A")
47
- _git(tmp_path, "commit", "-q", "-m", "init")
48
- assert git_tracking.is_absent_but_tracked(present) is False
49
-
50
-
51
- def test_has_uncommitted_change_from_head_true_for_untracked_file(tmp_path: Path) -> None:
52
- _init_repository(tmp_path)
53
- untracked = tmp_path / "test_orders.py"
54
- untracked.write_text("def test_fulfill(): pass\n")
55
- assert git_tracking.has_uncommitted_change_from_head(untracked) is True
56
-
57
-
58
- def test_has_uncommitted_change_from_head_true_for_tracked_modified_file(tmp_path: Path) -> None:
59
- _init_repository(tmp_path)
60
- tracked = tmp_path / "test_orders.py"
61
- tracked.write_text("def test_fulfill(): pass\n")
62
- _git(tmp_path, "add", "-A")
63
- _git(tmp_path, "commit", "-q", "-m", "init")
64
- tracked.write_text("def test_fulfill(): assert True\n")
65
- assert git_tracking.has_uncommitted_change_from_head(tracked) is True
66
-
67
-
68
- def test_has_uncommitted_change_from_head_false_for_tracked_clean_file(tmp_path: Path) -> None:
69
- _init_repository(tmp_path)
70
- tracked = tmp_path / "test_orders.py"
71
- tracked.write_text("def test_fulfill(): pass\n")
72
- _git(tmp_path, "add", "-A")
73
- _git(tmp_path, "commit", "-q", "-m", "init")
74
- tracked.touch()
75
- assert git_tracking.has_uncommitted_change_from_head(tracked) is False
76
-
77
-
78
- def test_has_uncommitted_change_from_head_true_outside_a_git_repository(tmp_path: Path) -> None:
79
- stray = tmp_path / "test_orders.py"
80
- stray.write_text("def test_fulfill(): pass\n")
81
- assert git_tracking.has_uncommitted_change_from_head(stray) is True
82
-
83
-
84
- def test_has_uncommitted_change_from_head_true_for_staged_new_file_never_committed(
85
- tmp_path: Path,
86
- ) -> None:
87
- """A brand-new file only ``git add``-ed carries content HEAD never had.
88
-
89
- Regression guard: comparing the working tree to the index (rather than to
90
- HEAD) reads a freshly staged file as unchanged, since staging makes the
91
- index match the working tree exactly. That misses every staged-but-never-
92
- committed file, which is exactly how a RED-step test file is staged.
93
- """
94
- _init_repository(tmp_path)
95
- _git(tmp_path, "commit", "--allow-empty", "-q", "-m", "baseline")
96
- staged_new_test = tmp_path / "test_orders.py"
97
- staged_new_test.write_text("def test_fulfill(): pass\n")
98
- _git(tmp_path, "add", "-A")
99
- assert git_tracking.has_uncommitted_change_from_head(staged_new_test) is True
100
-
101
-
102
- def test_has_uncommitted_change_from_head_true_for_staged_modification(tmp_path: Path) -> None:
103
- """A tracked test's staged edit still differs from HEAD, not only the index."""
104
- _init_repository(tmp_path)
105
- tracked = tmp_path / "test_orders.py"
106
- tracked.write_text("def test_fulfill(): pass\n")
107
- _git(tmp_path, "add", "-A")
108
- _git(tmp_path, "commit", "-q", "-m", "init")
109
- tracked.write_text("def test_fulfill(): assert True\n")
110
- _git(tmp_path, "add", "-A")
111
- assert git_tracking.has_uncommitted_change_from_head(tracked) is True
@@ -1,36 +0,0 @@
1
- """Behavioral tests for the path_classification parts module."""
2
-
3
- from tdd_enforcer_parts import path_classification
4
-
5
-
6
- def test_production_extensions_includes_python_and_typescript() -> None:
7
- extensions = path_classification.production_extensions()
8
- assert ".py" in extensions
9
- assert ".tsx" in extensions
10
-
11
-
12
- def test_skip_extensions_includes_markdown_and_json() -> None:
13
- extensions = path_classification.skip_extensions()
14
- assert ".md" in extensions
15
- assert ".json" in extensions
16
-
17
-
18
- def test_is_inside_dotclaude_segment_matches_exact_segment_only() -> None:
19
- assert path_classification._is_inside_dotclaude_segment("/home/user/.claude/agent.py") is True
20
- assert path_classification._is_inside_dotclaude_segment("/src/my.claude.helpers.py") is False
21
-
22
-
23
- def test_matches_any_skip_pattern_flags_test_file_names() -> None:
24
- assert (
25
- path_classification._matches_any_skip_pattern("test_orders.py", "pkg/test_orders.py")
26
- is True
27
- )
28
- assert path_classification._matches_any_skip_pattern("orders.py", "pkg/orders.py") is False
29
-
30
-
31
- def test_extract_written_content_reads_write_and_multiedit() -> None:
32
- assert path_classification._extract_written_content("Write", {"content": "x"}) == "x"
33
- joined = path_classification._extract_written_content(
34
- "MultiEdit", {"edits": [{"new_string": "a"}, {"new_string": "b"}]}
35
- )
36
- assert joined == "a\nb"