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.
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +85 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +34 -0
- package/_shared/pr-loop/scripts/fix_pr_test_proof.py +132 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/fix_pr_test_proof_constants.py +43 -0
- package/_shared/pr-loop/scripts/test_fix_pr_test_proof.py +126 -0
- package/bin/codex-skill-load-block.mjs +45 -0
- package/bin/codex-skill-load-block.test.mjs +72 -0
- package/bin/install.agents-home.test.mjs +76 -1
- package/bin/install.mjs +53 -31
- package/bin/install.test.mjs +37 -0
- package/bin/install.transaction.test.mjs +29 -7
- package/bin/seed-codex-pstack-models.mjs +27 -0
- package/bin/seed-codex-pstack-models.test.mjs +72 -0
- package/docs/CODE_RULES.md +5 -1
- package/hooks/blocking/code_rules_enforcer.py +1 -3
- package/hooks/blocking/code_rules_magic_values.py +1 -3
- package/hooks/blocking/code_rules_paired_test.py +5 -6
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -46
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +2 -40
- package/hooks/followup_ledger.py +8 -1
- package/hooks/git-hooks/test_installed_policy_lint.py +4 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/followup_ledger_constants.py +4 -0
- package/hooks/hooks_constants/messages.py +0 -1
- package/hooks/hooks_constants/nested_project_hooks_constants.py +37 -0
- package/hooks/hooks_constants/test_messages.py +1 -7
- package/hooks/lifecycle/nested_project_hooks.py +395 -0
- package/hooks/lifecycle/test_nested_project_hooks.py +251 -0
- package/hooks/test_followup_ledger.py +18 -0
- package/package.json +2 -1
- package/pstack-codex-models.md +37 -0
- package/rules/bdd.md +1 -1
- package/rules/build-needs-a-user.md +53 -0
- package/rules/code-standards.md +1 -1
- package/rules/flag-non-breaking-findings.md +7 -0
- package/rules/paired-test-coverage.md +4 -2
- package/rules/testing.md +5 -1
- package/scripts/cde_lint.py +47 -1
- package/scripts/policy_lint/adapter_callers.py +124 -0
- package/scripts/policy_lint/adapter_pairing.py +4 -2
- package/scripts/policy_lint/adapter_support.py +4 -3
- package/scripts/policy_lint/adapters.py +15 -0
- package/scripts/policy_lint/check_catalog.py +24 -0
- package/scripts/policy_lint/config/approved_test_pairs.py +3 -2
- package/scripts/policy_lint/config/check_catalog_constants.py +13 -0
- package/scripts/policy_lint/config/constants.py +18 -0
- package/scripts/policy_lint/engine.py +9 -3
- package/scripts/policy_lint/model.py +6 -2
- package/scripts/policy_lint/registry.py +6 -0
- package/scripts/policy_lint/selection.py +2 -0
- package/scripts/tests/test_cde_lint.py +50 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
- package/scripts/tests/test_policy_lint_rules_callers.py +126 -0
- package/scripts/tests/test_policy_lint_rules_check_catalog.py +25 -1
- package/scripts/tests/test_policy_lint_rules_pairing.py +31 -2
- package/scripts/tests/test_policy_lint_selection.py +35 -0
- package/hooks/blocking/tdd_enforcer.py +0 -195
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +0 -142
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +0 -1
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +0 -49
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +0 -298
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +0 -96
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +0 -95
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +0 -132
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +0 -119
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +0 -31
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +0 -256
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +0 -46
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +0 -28
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +0 -111
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +0 -36
- package/hooks/blocking/test_tdd_enforcer.py +0 -1506
- package/hooks/blocking/test_tdd_enforcer_restore.py +0 -108
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +0 -105
- package/scripts/codex_compat_watcher.py +0 -502
- 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"
|