-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_empty_source_guard.py
More file actions
209 lines (177 loc) · 6.92 KB
/
Copy pathtest_empty_source_guard.py
File metadata and controls
209 lines (177 loc) · 6.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""Regression tests: refuse to overwrite templates from empty source.
2026-07-05 incident: a bulk regen run against a wrong project root
resolved every feature's globs to zero files, computed
``source_hash`` of the empty string
(e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855),
and rewrote 36 existing templates with drastically degraded content.
The generation pipeline must fail loudly (before any write) when
empty-resolved source would overwrite existing output. Scaffolding a
brand-new feature (nothing on disk yet) from empty source remains
allowed — see ``test_feature_glob_with_zero_matches`` in
``test_generator.py``.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from attune_author import EMPTY_SOURCE_SHA256, EmptySourceError
from attune_author.generator import generate_feature_templates, prepare_polish_phase
from attune_author.manifest import Feature
@pytest.fixture
def feature() -> Feature:
return Feature(
name="auth",
description="Authentication and authorization",
files=["src/auth/**"],
tags=["security"],
)
GOOD_TEMPLATE = (
"---\n"
"type: concept\n"
"name: auth-concept\n"
"feature: auth\n"
"depth: concept\n"
"generated_at: 2026-06-23T00:00:00+00:00\n"
"source_hash: 544951b28662066a703ef7be552af08e83ef52a5186e5ad71ad216119352938b\n"
"status: generated\n"
"---\n"
"\n"
"# Real polished content that must not be clobbered\n"
)
def _seed_existing_template(help_dir: Path) -> Path:
"""Put a healthy generated template on disk for feature 'auth'."""
tdir = help_dir / "templates" / "auth"
tdir.mkdir(parents=True)
concept = tdir / "concept.md"
concept.write_text(GOOD_TEMPLATE, encoding="utf-8")
return concept
class TestEmptySourceOverwriteGuard:
"""Empty resolved source + existing output on disk → hard refuse."""
def test_zero_matched_files_refuses_and_preserves_content(
self, feature: Feature, help_dir: Path, tmp_path: Path
) -> None:
concept = _seed_existing_template(help_dir)
empty_root = tmp_path / "emptyroot"
empty_root.mkdir()
with pytest.raises(EmptySourceError, match="no files matched"):
prepare_polish_phase(
feature=feature,
help_dir=help_dir,
project_root=empty_root,
overwrite=True,
)
# The guard fires before any write — existing content intact.
assert concept.read_text(encoding="utf-8") == GOOD_TEMPLATE
def test_all_empty_matched_files_refuses(
self, feature: Feature, help_dir: Path, tmp_path: Path
) -> None:
# Non-.py so the legacy byte-concatenation path is exercised;
# zero bytes total → the empty-string SHA.
_seed_existing_template(help_dir)
root = tmp_path / "zerobyte"
(root / "src" / "auth").mkdir(parents=True)
(root / "src" / "auth" / "data.txt").write_bytes(b"")
with pytest.raises(EmptySourceError, match="empty/unreadable"):
prepare_polish_phase(
feature=feature,
help_dir=help_dir,
project_root=root,
overwrite=True,
)
def test_generate_feature_templates_propagates(
self, feature: Feature, help_dir: Path, tmp_path: Path
) -> None:
_seed_existing_template(help_dir)
empty_root = tmp_path / "emptyroot"
empty_root.mkdir()
with pytest.raises(EmptySourceError):
generate_feature_templates(
feature=feature,
help_dir=help_dir,
project_root=empty_root,
overwrite=True,
)
def test_scaffolding_new_feature_from_empty_source_allowed(
self, feature: Feature, help_dir: Path, tmp_path: Path
) -> None:
# No templates on disk → bootstrap scaffold stays permitted.
empty_root = tmp_path / "emptyroot"
empty_root.mkdir()
prep = prepare_polish_phase(
feature=feature,
help_dir=help_dir,
project_root=empty_root,
overwrite=True,
)
assert prep.matched_files == []
assert prep.pending
def test_healthy_source_still_generates(
self, feature: Feature, help_dir: Path, project_root: Path
) -> None:
_seed_existing_template(help_dir)
prep = prepare_polish_phase(
feature=feature,
help_dir=help_dir,
project_root=project_root,
overwrite=True,
)
assert prep.matched_files
assert prep.source_hash != EMPTY_SOURCE_SHA256
assert prep.pending
def test_unrecognized_depth_name_is_skipped_not_raised(
self, feature: Feature, help_dir: Path, tmp_path: Path
) -> None:
# A depth string outside _ALL_TEMPLATE_NAMES must not crash the
# at-risk scan — it's silently skipped, not treated as a file
# to check for existing output.
empty_root = tmp_path / "emptyroot"
empty_root.mkdir()
prep = prepare_polish_phase(
feature=feature,
help_dir=help_dir,
project_root=empty_root,
depths=["not-a-real-depth"],
overwrite=True,
)
assert prep.matched_files == []
def test_project_doc_depth_at_risk_raises(
self, feature: Feature, help_dir: Path, tmp_path: Path
) -> None:
# Project-doc kinds (how-to/tutorial/cli-reference/architecture)
# resolve their at-risk path via _project_doc_output_path, not
# template_dir — must be checked too, not just .help/ templates.
empty_root = tmp_path / "emptyroot"
empty_root.mkdir()
how_to = empty_root / "docs" / "how-to" / f"{feature.name}.md"
how_to.parent.mkdir(parents=True)
how_to.write_text("# Existing how-to doc\n", encoding="utf-8")
with pytest.raises(EmptySourceError, match="how-to"):
prepare_polish_phase(
feature=feature,
help_dir=help_dir,
project_root=empty_root,
depths=["how-to"],
overwrite=True,
)
class TestEmptySourceCLI:
"""The CLI exits non-zero with a clear message — no traceback."""
def test_generate_against_empty_root_exits_1(
self, help_dir: Path, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
from attune_author.cli import main
_seed_existing_template(help_dir)
empty_root = tmp_path / "emptyroot"
empty_root.mkdir()
exit_code = main(
[
"generate",
"auth",
"--help-dir",
str(help_dir),
"--project-root",
str(empty_root),
"--overwrite",
]
)
assert exit_code == 1
err = capsys.readouterr().err
assert "Refusing to regenerate" in err