gabriel / muse public
test_merge_base_supercharge.py python
416 lines 15.2 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Supercharge tests for ``muse merge-base``.
2
3 Coverage tiers
4 --------------
5 - JSON envelope: exit_code and duration_ms present on found and not-found outcomes
6 - Error payload: errors go to stdout as JSON in --json mode, no dual stderr prose
7 - Data integrity: symmetry (A,B)==(B,A); merge-commit as input; deep DAG
8 - TypedDicts: _MergeBaseFoundJson and _MergeBaseErrorJson with required annotations
9 - Docstring: module docstring covers exit_code and duration_ms
10 - No-prose pollution: no emoji / prose leaks into JSON stdout
11 - Stress: 100-commit linear chain resolves correctly
12 """
13 from __future__ import annotations
14 from collections.abc import Mapping
15
16 import datetime
17 import json
18 import pathlib
19 from typing import get_type_hints
20
21 from muse.core._types import fake_id
22 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
23 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
24 from tests.cli_test_helper import CliRunner
25
26 runner = CliRunner()
27
28 _DT = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
29
30
31 # ---------------------------------------------------------------------------
32 # Helpers
33 # ---------------------------------------------------------------------------
34
35
36 def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path:
37 muse = tmp_path / ".muse"
38 for sub in ("objects", "commits", "snapshots", "refs/heads"):
39 (muse / sub).mkdir(parents=True)
40 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
41 (muse / "repo.json").write_text(
42 json.dumps({"repo_id": "supercharge-test", "domain": "code"}),
43 encoding="utf-8",
44 )
45 return tmp_path
46
47
48 def _env(root: pathlib.Path) -> Mapping[str, str]:
49 return {"MUSE_REPO_ROOT": str(root)}
50
51
52 def _snap(root: pathlib.Path) -> str:
53 sid = compute_snapshot_id({})
54 write_snapshot(root, SnapshotRecord(snapshot_id=sid, manifest={}, created_at=_DT))
55 return sid
56
57
58 def _commit(
59 root: pathlib.Path,
60 msg: str,
61 *,
62 parent: str | None = None,
63 parent2: str | None = None,
64 branch: str = "main",
65 ) -> str:
66 sid = _snap(root)
67 parent_ids = [p for p in [parent, parent2] if p is not None]
68 cid = compute_commit_id(
69 repo_id="supercharge-test",
70 parent_ids=parent_ids,
71 snapshot_id=sid,
72 message=msg,
73 committed_at_iso=_DT.isoformat(),
74 )
75 write_commit(root, CommitRecord(
76 commit_id=cid,
77 repo_id="supercharge-test",
78 created_on_branch=branch,
79 snapshot_id=sid,
80 message=msg,
81 committed_at=_DT,
82 parent_commit_id=parent,
83 parent2_commit_id=parent2,
84 ))
85 return cid
86
87
88 def _set_branch(root: pathlib.Path, branch: str, cid: str) -> None:
89 ref = root / ".muse" / "refs" / "heads" / branch
90 ref.parent.mkdir(parents=True, exist_ok=True)
91 ref.write_text(cid, encoding="utf-8")
92 (root / ".muse" / "HEAD").write_text(f"ref: refs/heads/{branch}", encoding="utf-8")
93
94
95 def _mb(root: pathlib.Path, *args: str):
96 from muse.cli.app import main as cli
97 return runner.invoke(cli, ["merge-base", *args], env=_env(root))
98
99
100 def _mbj(root: pathlib.Path, *args: str):
101 """Like _mb but always passes --json."""
102 return _mb(root, "--json", *args)
103
104
105 def _diverged_repo(root: pathlib.Path) -> tuple[str, str, str]:
106 """Create base → left and base → right. Returns (base, left, right)."""
107 base = _commit(root, "base")
108 left = _commit(root, "left", parent=base, branch="left")
109 right = _commit(root, "right", parent=base, branch="right")
110 _set_branch(root, "left", left)
111 _set_branch(root, "right", right)
112 return base, left, right
113
114
115 def _unrelated_repo(root: pathlib.Path) -> tuple[str, str]:
116 """Two commits with no shared history."""
117 c1 = _commit(root, "unrelated-c1")
118 c2 = _commit(root, "unrelated-c2")
119 return c1, c2
120
121
122 # ---------------------------------------------------------------------------
123 # JSON envelope — exit_code
124 # ---------------------------------------------------------------------------
125
126
127 class TestJsonEnvelopeExitCode:
128 """exit_code is present and correct on all merge-base outcomes."""
129
130 def test_found_has_exit_code(self, tmp_path: pathlib.Path) -> None:
131 root = _init_repo(tmp_path)
132 base, left, right = _diverged_repo(root)
133 r = _mbj(root, left, right)
134 assert r.exit_code == 0
135 d = json.loads(r.output)
136 assert "exit_code" in d, "exit_code missing when merge base is found"
137 assert d["exit_code"] == 0
138
139 def test_not_found_has_exit_code(self, tmp_path: pathlib.Path) -> None:
140 root = _init_repo(tmp_path)
141 c1, c2 = _unrelated_repo(root)
142 r = _mbj(root, c1, c2)
143 assert r.exit_code == 0
144 d = json.loads(r.output)
145 assert "exit_code" in d, "exit_code missing when no common ancestor"
146 assert d["exit_code"] == 0
147
148 def test_same_commit_has_exit_code(self, tmp_path: pathlib.Path) -> None:
149 root = _init_repo(tmp_path)
150 cid = _commit(root, "solo")
151 r = _mbj(root, cid, cid)
152 d = json.loads(r.output)
153 assert "exit_code" in d
154 assert d["exit_code"] == 0
155
156
157 # ---------------------------------------------------------------------------
158 # JSON envelope — duration_ms
159 # ---------------------------------------------------------------------------
160
161
162 class TestJsonEnvelopeDurationMs:
163 """duration_ms is present and non-negative on all merge-base outcomes."""
164
165 def test_found_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
166 root = _init_repo(tmp_path)
167 base, left, right = _diverged_repo(root)
168 r = _mbj(root, left, right)
169 d = json.loads(r.output)
170 assert "duration_ms" in d, "duration_ms missing when merge base is found"
171 assert isinstance(d["duration_ms"], float)
172 assert d["duration_ms"] >= 0.0
173
174 def test_not_found_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
175 root = _init_repo(tmp_path)
176 c1, c2 = _unrelated_repo(root)
177 r = _mbj(root, c1, c2)
178 d = json.loads(r.output)
179 assert "duration_ms" in d, "duration_ms missing when no common ancestor"
180 assert isinstance(d["duration_ms"], float)
181
182 def test_same_commit_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
183 root = _init_repo(tmp_path)
184 cid = _commit(root, "dur-solo")
185 r = _mbj(root, cid, cid)
186 d = json.loads(r.output)
187 assert "duration_ms" in d
188 assert isinstance(d["duration_ms"], float)
189
190
191 # ---------------------------------------------------------------------------
192 # Error payload — errors route to stdout as JSON in --json mode
193 # ---------------------------------------------------------------------------
194
195
196 class TestErrorPayload:
197 """In --json mode, all errors appear on stdout as JSON — no stderr prose."""
198
199 def test_bad_ref_error_is_json_on_stdout(self, tmp_path: pathlib.Path) -> None:
200 root = _init_repo(tmp_path)
201 r = _mbj(root, "no-such-branch", "also-missing")
202 assert r.exit_code != 0
203 # Error JSON must go to stdout: stderr should be empty.
204 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
205 d = json.loads(r.output)
206 assert d["status"] == "error"
207
208 def test_bad_ref_error_has_status_error(self, tmp_path: pathlib.Path) -> None:
209 root = _init_repo(tmp_path)
210 r = _mbj(root, "ghost", "phantom")
211 d = json.loads(r.output)
212 assert d["status"] == "error"
213
214 def test_bad_ref_error_has_exit_code(self, tmp_path: pathlib.Path) -> None:
215 root = _init_repo(tmp_path)
216 r = _mbj(root, "ghost", "phantom")
217 d = json.loads(r.output)
218 assert "exit_code" in d
219 assert d["exit_code"] != 0
220
221 def test_bad_ref_error_has_error_field(self, tmp_path: pathlib.Path) -> None:
222 root = _init_repo(tmp_path)
223 r = _mbj(root, "ghost", "phantom")
224 d = json.loads(r.output)
225 assert "error" in d
226 assert d["error"] # non-empty message
227
228 def test_no_duplicate_stderr_prose(self, tmp_path: pathlib.Path) -> None:
229 """In --json mode, errors must not also print ❌ prose to stderr."""
230 root = _init_repo(tmp_path)
231 r = _mbj(root, "no-such", "branch")
232 assert "❌" not in r.stderr
233
234 def test_second_bad_ref_error_is_json_on_stdout(self, tmp_path: pathlib.Path) -> None:
235 """Error on second ref (commit_b) also goes to stdout, not stderr."""
236 root = _init_repo(tmp_path)
237 cid = _commit(root, "real-commit")
238 r = _mbj(root, cid, "nonexistent")
239 assert r.exit_code != 0
240 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
241 d = json.loads(r.output)
242 assert d["status"] == "error"
243
244
245 # ---------------------------------------------------------------------------
246 # Data integrity
247 # ---------------------------------------------------------------------------
248
249
250 class TestDataIntegrity:
251 """Correctness properties that must hold across all inputs."""
252
253 def test_symmetry(self, tmp_path: pathlib.Path) -> None:
254 """merge-base(A, B) == merge-base(B, A)."""
255 root = _init_repo(tmp_path)
256 base, left, right = _diverged_repo(root)
257 r_ab = json.loads(_mbj(root, left, right).output)
258 r_ba = json.loads(_mbj(root, right, left).output)
259 assert r_ab["merge_base"] == r_ba["merge_base"]
260
261 def test_merge_commit_as_input(self, tmp_path: pathlib.Path) -> None:
262 """A merge commit (two parents) is handled correctly as input."""
263 root = _init_repo(tmp_path)
264 base, left, right = _diverged_repo(root)
265 # Simulate a merge commit that has both left and right as parents
266 merge_commit = _commit(root, "merge", parent=left, parent2=right, branch="main")
267 _set_branch(root, "main", merge_commit)
268 # merge-base of the merge commit with right should be right (right is ancestor of merge)
269 r = _mbj(root, merge_commit, right)
270 assert r.exit_code == 0
271 d = json.loads(r.output)
272 assert d["merge_base"] == right
273
274 def test_found_merge_base_is_correct(self, tmp_path: pathlib.Path) -> None:
275 """merge_base field equals the actual LCA commit ID."""
276 root = _init_repo(tmp_path)
277 base, left, right = _diverged_repo(root)
278 d = json.loads(_mbj(root, left, right).output)
279 assert d["merge_base"] == base
280
281 def test_commit_a_and_b_echoed_correctly(self, tmp_path: pathlib.Path) -> None:
282 """commit_a and commit_b in the response match what was requested."""
283 root = _init_repo(tmp_path)
284 base, left, right = _diverged_repo(root)
285 d = json.loads(_mbj(root, left, right).output)
286 assert d["commit_a"] == left
287 assert d["commit_b"] == right
288
289 def test_not_found_merge_base_is_null(self, tmp_path: pathlib.Path) -> None:
290 """merge_base is null (not absent) when no common ancestor."""
291 root = _init_repo(tmp_path)
292 c1, c2 = _unrelated_repo(root)
293 d = json.loads(_mbj(root, c1, c2).output)
294 assert "merge_base" in d
295 assert d["merge_base"] is None
296
297
298 # ---------------------------------------------------------------------------
299 # No-prose pollution
300 # ---------------------------------------------------------------------------
301
302
303 class TestNoProsePollution:
304 """JSON stdout must be valid, parseable JSON on all non-error paths."""
305
306 def test_found_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
307 root = _init_repo(tmp_path)
308 base, left, right = _diverged_repo(root)
309 r = _mbj(root, left, right)
310 json.loads(r.output) # must not raise
311
312 def test_not_found_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
313 root = _init_repo(tmp_path)
314 c1, c2 = _unrelated_repo(root)
315 json.loads(_mbj(root, c1, c2).output) # must not raise
316
317 def test_no_emoji_in_found_json(self, tmp_path: pathlib.Path) -> None:
318 root = _init_repo(tmp_path)
319 base, left, right = _diverged_repo(root)
320 r = _mbj(root, left, right)
321 assert "✅" not in r.output
322 assert "❌" not in r.output
323
324
325 # ---------------------------------------------------------------------------
326 # TypedDicts
327 # ---------------------------------------------------------------------------
328
329
330 class TestTypedDicts:
331 def test_merge_base_found_json_typeddict_exists(self) -> None:
332 from muse.cli.commands.merge_base import _MergeBaseFoundJson
333 assert _MergeBaseFoundJson is not None
334
335 def test_merge_base_error_json_typeddict_exists(self) -> None:
336 from muse.cli.commands.merge_base import _MergeBaseErrorJson
337 assert _MergeBaseErrorJson is not None
338
339 def test_merge_base_found_json_has_exit_code_annotation(self) -> None:
340 from muse.cli.commands.merge_base import _MergeBaseFoundJson
341 hints = get_type_hints(_MergeBaseFoundJson)
342 assert "exit_code" in hints
343
344 def test_merge_base_found_json_has_duration_ms_annotation(self) -> None:
345 from muse.cli.commands.merge_base import _MergeBaseFoundJson
346 hints = get_type_hints(_MergeBaseFoundJson)
347 assert "duration_ms" in hints
348
349 def test_merge_base_error_json_has_required_fields(self) -> None:
350 from muse.cli.commands.merge_base import _MergeBaseErrorJson
351 hints = get_type_hints(_MergeBaseErrorJson)
352 for field in ("status", "error", "exit_code"):
353 assert field in hints, f"Missing annotation: {field!r}"
354
355
356 # ---------------------------------------------------------------------------
357 # Docstring coverage
358 # ---------------------------------------------------------------------------
359
360
361 class TestDocstring:
362 def _doc(self) -> str:
363 import muse.cli.commands.merge_base as mod
364 return mod.__doc__ or ""
365
366 def test_docstring_documents_exit_code(self) -> None:
367 assert "exit_code" in self._doc()
368
369 def test_docstring_documents_duration_ms(self) -> None:
370 assert "duration_ms" in self._doc()
371
372
373 # ---------------------------------------------------------------------------
374 # Stress
375 # ---------------------------------------------------------------------------
376
377
378 class TestStress:
379 def test_100_commit_linear_chain(self, tmp_path: pathlib.Path) -> None:
380 """100-commit chain: merge-base of tip and midpoint is the midpoint."""
381 root = _init_repo(tmp_path)
382 commits: list[str] = []
383 for i in range(100):
384 parent = commits[-1] if commits else None
385 cid = _commit(root, f"chain-{i:03d}", parent=parent)
386 commits.append(cid)
387 mid = commits[49]
388 tip = commits[-1]
389 r = _mbj(root, tip, mid)
390 assert r.exit_code == 0
391 d = json.loads(r.output)
392 assert d["merge_base"] == mid
393 assert "duration_ms" in d
394 assert "exit_code" in d
395
396
397 class TestRegisterFlags:
398 def _parse(self, *args):
399 import argparse
400 from muse.cli.commands.merge_base import register
401 p = argparse.ArgumentParser()
402 subs = p.add_subparsers()
403 register(subs)
404 return p.parse_args(["merge-base", fake_id("a"), fake_id("b"), *args])
405
406 def test_json_short_flag(self):
407 args = self._parse("-j")
408 assert args.json_out is True
409
410 def test_json_long_flag(self):
411 args = self._parse("--json")
412 assert args.json_out is True
413
414 def test_default_no_json(self):
415 args = self._parse()
416 assert args.json_out is False
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago