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