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