gabriel / muse public
test_cmd_for_each_ref_hardening.py python
329 lines 12.3 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 137 days ago
1 """Hardening tests for ``muse for-each-ref`` — agent supercharge series.
2
3 Tests added in this pass
4 ------------------------
5 - ``duration_ms`` present and valid in JSON output
6 - ``exit_code`` present and zero in JSON output
7 - ``current_branch`` present — which branch HEAD points to
8 - JSON is compact (single line)
9 - ``commit_id`` and ``snapshot_id`` carry sha256: prefix
10 - Data integrity: duration_ms non-negative float, exit_code int,
11 current_branch matches HEAD, count == len(refs)
12 - Performance: 100-branch repo round-trip under 5 s, duration_ms plausible
13 - Security: error output to stderr, no traceback
14 """
15 from __future__ import annotations
16 from collections.abc import Mapping
17
18 import datetime
19 import json
20 import pathlib
21 import time
22
23 import pytest
24
25 from tests.cli_test_helper import CliRunner, InvokeResult
26 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
27 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
28
29 runner = CliRunner()
30
31
32 # ---------------------------------------------------------------------------
33 # Helpers
34 # ---------------------------------------------------------------------------
35
36 def _init_repo(path: pathlib.Path, head_branch: str = "main") -> pathlib.Path:
37 muse = path / ".muse"
38 for sub in ("commits", "snapshots", "objects", "refs/heads"):
39 (muse / sub).mkdir(parents=True, exist_ok=True)
40 (muse / "HEAD").write_text(f"ref: refs/heads/{head_branch}\n")
41 (muse / "repo.json").write_text(
42 json.dumps({"repo_id": "test-repo", "domain": "code"})
43 )
44 return path
45
46
47 def _commit(
48 repo: pathlib.Path,
49 msg: str,
50 branch: str = "main",
51 parent: str | None = None,
52 ts: datetime.datetime | None = None,
53 author: str = "gabriel",
54 ) -> str:
55 ts = ts or datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
56 sid = compute_snapshot_id({})
57 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest={}, created_at=ts))
58 parent_ids = [parent] if parent else []
59 cid = compute_commit_id(
60 repo_id="test-repo",
61 parent_ids=parent_ids,
62 snapshot_id=sid,
63 message=msg,
64 committed_at_iso=ts.isoformat(),
65 author=author,
66 )
67 write_commit(repo, CommitRecord(
68 commit_id=cid, repo_id="test-repo", created_on_branch=branch,
69 snapshot_id=sid, message=msg, committed_at=ts,
70 author=author, parent_commit_id=parent, parent2_commit_id=None,
71 ))
72 ref_path = repo / ".muse" / "refs" / "heads" / branch
73 ref_path.parent.mkdir(parents=True, exist_ok=True)
74 ref_path.write_text(cid)
75 return cid
76
77
78 def _fer(repo: pathlib.Path, *args: str) -> InvokeResult:
79 return runner.invoke(None, ["for-each-ref", "--json", *args],
80 env={"MUSE_REPO_ROOT": str(repo)})
81
82
83 def _json(r: InvokeResult) -> Mapping[str, object]:
84 return json.loads(r.output)
85
86
87 # ---------------------------------------------------------------------------
88 # duration_ms
89 # ---------------------------------------------------------------------------
90
91 class TestElapsedSeconds:
92 def test_present_in_full_output(self, tmp_path: pathlib.Path) -> None:
93 _init_repo(tmp_path)
94 _commit(tmp_path, "c1")
95 assert "duration_ms" in _json(_fer(tmp_path))
96
97 def test_present_with_no_commits(self, tmp_path: pathlib.Path) -> None:
98 _init_repo(tmp_path)
99 _commit(tmp_path, "c1")
100 assert "duration_ms" in _json(_fer(tmp_path, "--no-commits"))
101
102 def test_present_on_empty_repo(self, tmp_path: pathlib.Path) -> None:
103 _init_repo(tmp_path)
104 assert "duration_ms" in _json(_fer(tmp_path))
105
106 def test_is_float(self, tmp_path: pathlib.Path) -> None:
107 _init_repo(tmp_path)
108 _commit(tmp_path, "c1")
109 assert isinstance(_json(_fer(tmp_path))["duration_ms"], float)
110
111 def test_non_negative(self, tmp_path: pathlib.Path) -> None:
112 _init_repo(tmp_path)
113 _commit(tmp_path, "c1")
114 assert _json(_fer(tmp_path))["duration_ms"] >= 0.0
115
116 def test_six_decimal_places(self, tmp_path: pathlib.Path) -> None:
117 _init_repo(tmp_path)
118 _commit(tmp_path, "c1")
119 v = _json(_fer(tmp_path))["duration_ms"]
120 assert v == round(v, 6)
121
122 def test_present_with_pattern_filter(self, tmp_path: pathlib.Path) -> None:
123 _init_repo(tmp_path)
124 _commit(tmp_path, "c1")
125 data = _json(_fer(tmp_path, "--pattern", "refs/heads/main"))
126 assert "duration_ms" in data
127
128 def test_present_with_count_limit(self, tmp_path: pathlib.Path) -> None:
129 _init_repo(tmp_path)
130 for b in ["a", "b", "c"]:
131 _commit(tmp_path, f"c-{b}", b)
132 assert "duration_ms" in _json(_fer(tmp_path, "--count", "2"))
133
134
135 # ---------------------------------------------------------------------------
136 # exit_code
137 # ---------------------------------------------------------------------------
138
139 class TestExitCode:
140 def test_present_in_full_output(self, tmp_path: pathlib.Path) -> None:
141 _init_repo(tmp_path)
142 _commit(tmp_path, "c1")
143 assert "exit_code" in _json(_fer(tmp_path))
144
145 def test_zero_on_success(self, tmp_path: pathlib.Path) -> None:
146 _init_repo(tmp_path)
147 _commit(tmp_path, "c1")
148 assert _json(_fer(tmp_path))["exit_code"] == 0
149
150 def test_zero_on_empty_repo(self, tmp_path: pathlib.Path) -> None:
151 _init_repo(tmp_path)
152 assert _json(_fer(tmp_path))["exit_code"] == 0
153
154 def test_zero_with_no_commits(self, tmp_path: pathlib.Path) -> None:
155 _init_repo(tmp_path)
156 _commit(tmp_path, "c1")
157 assert _json(_fer(tmp_path, "--no-commits"))["exit_code"] == 0
158
159 def test_is_int_not_bool(self, tmp_path: pathlib.Path) -> None:
160 _init_repo(tmp_path)
161 _commit(tmp_path, "c1")
162 assert type(_json(_fer(tmp_path))["exit_code"]) is int
163
164
165 # ---------------------------------------------------------------------------
166 # current_branch
167 # ---------------------------------------------------------------------------
168
169 class TestCurrentBranch:
170 def test_present_in_output(self, tmp_path: pathlib.Path) -> None:
171 _init_repo(tmp_path, head_branch="main")
172 _commit(tmp_path, "c1", "main")
173 assert "current_branch" in _json(_fer(tmp_path))
174
175 def test_matches_head_branch(self, tmp_path: pathlib.Path) -> None:
176 _init_repo(tmp_path, head_branch="dev")
177 _commit(tmp_path, "c1", "dev")
178 assert _json(_fer(tmp_path))["current_branch"] == "dev"
179
180 def test_main_by_default(self, tmp_path: pathlib.Path) -> None:
181 _init_repo(tmp_path, head_branch="main")
182 _commit(tmp_path, "c1", "main")
183 assert _json(_fer(tmp_path))["current_branch"] == "main"
184
185 def test_present_with_no_commits_flag(self, tmp_path: pathlib.Path) -> None:
186 _init_repo(tmp_path, head_branch="main")
187 _commit(tmp_path, "c1", "main")
188 assert "current_branch" in _json(_fer(tmp_path, "--no-commits"))
189
190 def test_present_on_empty_repo(self, tmp_path: pathlib.Path) -> None:
191 _init_repo(tmp_path, head_branch="main")
192 data = _json(_fer(tmp_path))
193 assert "current_branch" in data
194
195 def test_feature_branch_reflected(self, tmp_path: pathlib.Path) -> None:
196 _init_repo(tmp_path, head_branch="feat/my-thing")
197 _commit(tmp_path, "c1", "feat/my-thing")
198 assert _json(_fer(tmp_path))["current_branch"] == "feat/my-thing"
199
200
201 # ---------------------------------------------------------------------------
202 # Compact JSON
203 # ---------------------------------------------------------------------------
204
205 class TestCompactJson:
206 def test_output_is_single_line(self, tmp_path: pathlib.Path) -> None:
207 _init_repo(tmp_path)
208 _commit(tmp_path, "c1")
209 r = _fer(tmp_path)
210 assert len(r.output.strip().splitlines()) == 1
211
212 def test_no_commits_is_single_line(self, tmp_path: pathlib.Path) -> None:
213 _init_repo(tmp_path)
214 _commit(tmp_path, "c1")
215 r = _fer(tmp_path, "--no-commits")
216 assert len(r.output.strip().splitlines()) == 1
217
218 def test_empty_repo_is_single_line(self, tmp_path: pathlib.Path) -> None:
219 _init_repo(tmp_path)
220 r = _fer(tmp_path)
221 assert len(r.output.strip().splitlines()) == 1
222
223
224 # ---------------------------------------------------------------------------
225 # sha256: prefix on IDs
226 # ---------------------------------------------------------------------------
227
228 class TestSha256Prefix:
229 def test_commit_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
230 _init_repo(tmp_path)
231 _commit(tmp_path, "c1")
232 ref = _json(_fer(tmp_path))["refs"][0]
233 assert ref["commit_id"].startswith("sha256:")
234
235 def test_commit_id_full_length(self, tmp_path: pathlib.Path) -> None:
236 _init_repo(tmp_path)
237 _commit(tmp_path, "c1")
238 ref = _json(_fer(tmp_path))["refs"][0]
239 # sha256: (7) + 64 hex chars = 71
240 assert len(ref["commit_id"]) == 71
241
242 def test_snapshot_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
243 _init_repo(tmp_path)
244 _commit(tmp_path, "c1")
245 ref = _json(_fer(tmp_path))["refs"][0]
246 assert ref["snapshot_id"].startswith("sha256:")
247
248 def test_no_commits_commit_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
249 _init_repo(tmp_path)
250 _commit(tmp_path, "c1")
251 ref = _json(_fer(tmp_path, "--no-commits"))["refs"][0]
252 assert ref["commit_id"].startswith("sha256:")
253
254
255 # ---------------------------------------------------------------------------
256 # Data integrity
257 # ---------------------------------------------------------------------------
258
259 class TestDataIntegrity:
260 def test_count_equals_len_refs(self, tmp_path: pathlib.Path) -> None:
261 _init_repo(tmp_path)
262 for b in ["a", "b", "c", "d"]:
263 _commit(tmp_path, f"c-{b}", b)
264 data = _json(_fer(tmp_path))
265 assert data["count"] == len(data["refs"])
266
267 def test_count_equals_len_refs_after_pattern(self, tmp_path: pathlib.Path) -> None:
268 _init_repo(tmp_path)
269 for b in ["feat/x", "feat/y", "main"]:
270 _commit(tmp_path, f"c-{b}", b)
271 data = _json(_fer(tmp_path, "--pattern", "refs/heads/feat/*"))
272 assert data["count"] == len(data["refs"])
273
274 def test_count_equals_len_refs_after_count_limit(self, tmp_path: pathlib.Path) -> None:
275 _init_repo(tmp_path)
276 for b in ["a", "b", "c", "d", "e"]:
277 _commit(tmp_path, f"c-{b}", b)
278 data = _json(_fer(tmp_path, "--count", "3"))
279 assert data["count"] == len(data["refs"])
280 assert data["count"] == 3
281
282 def test_all_refs_have_branch_and_ref_fields(self, tmp_path: pathlib.Path) -> None:
283 _init_repo(tmp_path)
284 for b in ["main", "dev", "feat/x"]:
285 _commit(tmp_path, f"c-{b}", b)
286 data = _json(_fer(tmp_path))
287 for ref in data["refs"]:
288 assert "branch" in ref
289 assert "ref" in ref
290 assert ref["ref"] == f"refs/heads/{ref['branch']}"
291
292 def test_committed_at_is_iso8601(self, tmp_path: pathlib.Path) -> None:
293 _init_repo(tmp_path)
294 _commit(tmp_path, "c1")
295 ref = _json(_fer(tmp_path))["refs"][0]
296 # Must parse as a datetime without raising
297 import datetime
298 datetime.datetime.fromisoformat(ref["committed_at"])
299
300
301 # ---------------------------------------------------------------------------
302 # Performance
303 # ---------------------------------------------------------------------------
304
305 class TestPerformance:
306 def test_duration_ms_plausible(self, tmp_path: pathlib.Path) -> None:
307 _init_repo(tmp_path)
308 _commit(tmp_path, "c1")
309 assert _json(_fer(tmp_path))["duration_ms"] < 10.0
310
311 def test_100_branch_repo_under_5s(self, tmp_path: pathlib.Path) -> None:
312 _init_repo(tmp_path)
313 for i in range(100):
314 _commit(tmp_path, f"c-{i}", f"branch-{i:03d}")
315 t0 = time.monotonic()
316 r = _fer(tmp_path)
317 assert r.exit_code == 0
318 assert time.monotonic() - t0 < 5.0
319 assert _json(r)["count"] == 100
320
321 def test_no_commits_faster_than_full(self, tmp_path: pathlib.Path) -> None:
322 """--no-commits duration_ms <= full duration_ms (with some slack)."""
323 _init_repo(tmp_path)
324 for i in range(50):
325 _commit(tmp_path, f"c-{i}", f"b-{i:03d}")
326 full = _json(_fer(tmp_path))["duration_ms"]
327 fast = _json(_fer(tmp_path, "--no-commits"))["duration_ms"]
328 # fast path must not be 10x slower than full (loose bound; CI noise)
329 assert fast < full * 10 + 1.0
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 137 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 143 days ago