gabriel / muse public

test_merge_base_supercharge.py file-level

at sha256:2 · View file ↗ · Intel ↗

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