gabriel / muse public
test_merge_tree_supercharge.py python
427 lines 16.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-tree``.
2
3 Coverage tiers
4 --------------
5 - JSON envelope: exit_code and duration_ms present on clean and conflict outcomes
6 - Error payload: errors go to stdout as JSON in --json mode, no dual stderr prose
7 - Data integrity: sha256: OID prefix preserved in merged_manifest; --base with
8 sha256:-prefixed commit ID accepted
9 - TypedDicts: _MergeTreeJson and _MergeTreeErrorJson with required annotations
10 - Docstring: module docstring covers exit_code and duration_ms
11 - No-prose pollution: JSON stdout is valid on all non-error paths
12 - Stress: 100-file manifest, 40% conflict rate — correct counts, correct exit code
13 """
14 from __future__ import annotations
15 from collections.abc import Mapping
16
17 import argparse
18 import datetime
19 import json
20 import pathlib
21 from typing import get_type_hints
22
23 from muse.core.object_store import write_object
24 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
25 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
26 from tests.cli_test_helper import CliRunner, InvokeResult
27 from muse.core.types import blob_id
28 from muse.core.paths import ref_path, muse_dir
29
30 runner = CliRunner()
31
32 _REPO_ID = "mt-supercharge"
33 _DT = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
34
35
36 # ---------------------------------------------------------------------------
37 # Helpers
38 # ---------------------------------------------------------------------------
39
40
41
42
43 def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path:
44 dot_muse = muse_dir(tmp_path)
45 for sub in ("objects", "commits", "snapshots", "refs/heads"):
46 (dot_muse / sub).mkdir(parents=True)
47 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
48 (dot_muse / "repo.json").write_text(
49 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
50 )
51 return tmp_path
52
53
54 def _env(root: pathlib.Path) -> Mapping[str, str]:
55 return {"MUSE_REPO_ROOT": str(root)}
56
57
58 def _write_obj(root: pathlib.Path, content: bytes) -> str:
59 oid = blob_id(content)
60 write_object(root, oid, content)
61 return oid
62
63
64 def _make_commit(
65 root: pathlib.Path,
66 manifest: Mapping[str, str],
67 branch: str,
68 parent: str | None = None,
69 msg: str = "test",
70 ) -> str:
71 snap_id = compute_snapshot_id(manifest)
72 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest, created_at=_DT))
73 parent_ids = [parent] if parent else []
74 cid = compute_commit_id( parent_ids=parent_ids,
75 snapshot_id=snap_id,
76 message=msg,
77 committed_at_iso=_DT.isoformat(),
78 )
79 write_commit(root, CommitRecord(
80 commit_id=cid, repo_id=_REPO_ID, branch=branch,
81 snapshot_id=snap_id, message=msg, committed_at=_DT,
82 parent_commit_id=parent,
83 ))
84 ref = ref_path(root, branch)
85 ref.parent.mkdir(parents=True, exist_ok=True)
86 ref.write_text(cid, encoding="utf-8")
87 return cid
88
89
90 def _mt(root: pathlib.Path, *args: str) -> InvokeResult:
91 from muse.cli.app import main as cli
92 return runner.invoke(cli, ["merge-tree", *args], env=_env(root))
93
94
95 def _diverged(root: pathlib.Path) -> tuple[str, str, str]:
96 """base → branch-a (adds a.py) and base → branch-b (adds b.py). No conflict."""
97 base_oid = _write_obj(root, b"base")
98 base_cid = _make_commit(root, {"base.py": base_oid}, "main")
99 a_oid = _write_obj(root, b"a content")
100 a_cid = _make_commit(root, {"base.py": base_oid, "a.py": a_oid}, "branch-a", parent=base_cid)
101 b_oid = _write_obj(root, b"b content")
102 b_cid = _make_commit(root, {"base.py": base_oid, "b.py": b_oid}, "branch-b", parent=base_cid)
103 return base_cid, a_cid, b_cid
104
105
106 def _conflicted(root: pathlib.Path) -> tuple[str, str, str]:
107 """base → branch-a and branch-b both modify shared.py differently."""
108 v1 = _write_obj(root, b"v1")
109 base_cid = _make_commit(root, {"shared.py": v1}, "main")
110 va = _write_obj(root, b"version-a")
111 a_cid = _make_commit(root, {"shared.py": va}, "branch-a", parent=base_cid)
112 vb = _write_obj(root, b"version-b")
113 b_cid = _make_commit(root, {"shared.py": vb}, "branch-b", parent=base_cid)
114 return base_cid, a_cid, b_cid
115
116
117 # ---------------------------------------------------------------------------
118 # JSON envelope — exit_code
119 # ---------------------------------------------------------------------------
120
121
122 class TestJsonEnvelopeExitCode:
123 def test_clean_merge_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
124 root = _init_repo(tmp_path)
125 _diverged(root)
126 r = _mt(root, "branch-a", "branch-b", "--json")
127 assert r.exit_code == 0
128 d = json.loads(r.output)
129 assert "exit_code" in d, "exit_code missing from clean merge envelope"
130 assert d["exit_code"] == 0
131
132 def test_conflict_merge_has_exit_code_nonzero(self, tmp_path: pathlib.Path) -> None:
133 root = _init_repo(tmp_path)
134 _conflicted(root)
135 r = _mt(root, "branch-a", "branch-b", "--json")
136 assert r.exit_code != 0
137 d = json.loads(r.output)
138 assert "exit_code" in d, "exit_code missing from conflict envelope"
139 assert d["exit_code"] != 0
140
141
142 # ---------------------------------------------------------------------------
143 # JSON envelope — duration_ms
144 # ---------------------------------------------------------------------------
145
146
147 class TestJsonEnvelopeDurationMs:
148 def test_clean_merge_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
149 root = _init_repo(tmp_path)
150 _diverged(root)
151 r = _mt(root, "branch-a", "branch-b", "--json")
152 d = json.loads(r.output)
153 assert "duration_ms" in d, "duration_ms missing from clean merge envelope"
154 assert isinstance(d["duration_ms"], float)
155 assert d["duration_ms"] >= 0.0
156
157 def test_conflict_merge_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
158 root = _init_repo(tmp_path)
159 _conflicted(root)
160 r = _mt(root, "branch-a", "branch-b", "--json")
161 d = json.loads(r.output)
162 assert "duration_ms" in d, "duration_ms missing from conflict envelope"
163 assert isinstance(d["duration_ms"], float)
164
165 def test_write_objects_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
166 root = _init_repo(tmp_path)
167 _diverged(root)
168 r = _mt(root, "branch-a", "branch-b", "--write-objects", "--json")
169 d = json.loads(r.output)
170 assert "duration_ms" in d
171
172
173 # ---------------------------------------------------------------------------
174 # Error payload — errors route to stdout as JSON in --json mode
175 # ---------------------------------------------------------------------------
176
177
178 class TestErrorPayload:
179 def test_bad_branch1_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
180 root = _init_repo(tmp_path)
181 r = _mt(root, "no-such", "also-no", "--json")
182 assert r.exit_code != 0
183 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
184 d = json.loads(r.output)
185 assert d["status"] == "error"
186
187 def test_bad_branch2_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
188 root = _init_repo(tmp_path)
189 base_oid = _write_obj(root, b"x")
190 _make_commit(root, {"x.py": base_oid}, "main")
191 r = _mt(root, "main", "nonexistent", "--json")
192 assert r.exit_code != 0
193 assert not r.stderr.strip(), f"unexpected stderr: {r.stderr!r}"
194 d = json.loads(r.output)
195 assert d["status"] == "error"
196
197 def test_no_common_ancestor_error_goes_to_stdout(self, tmp_path: pathlib.Path) -> None:
198 root = _init_repo(tmp_path)
199 oid = _write_obj(root, b"x")
200 _make_commit(root, {"x.py": oid}, "orphan-a")
201 oid2 = _write_obj(root, b"y")
202 _make_commit(root, {"y.py": oid2}, "orphan-b")
203 r = _mt(root, "orphan-a", "orphan-b", "--json")
204 assert r.exit_code != 0
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_error_payload_has_status_error(self, tmp_path: pathlib.Path) -> None:
210 root = _init_repo(tmp_path)
211 r = _mt(root, "ghost", "phantom", "--json")
212 d = json.loads(r.output)
213 assert d["status"] == "error"
214
215 def test_error_payload_has_exit_code(self, tmp_path: pathlib.Path) -> None:
216 root = _init_repo(tmp_path)
217 r = _mt(root, "ghost", "phantom", "--json")
218 d = json.loads(r.output)
219 assert "exit_code" in d
220 assert d["exit_code"] != 0
221
222 def test_error_payload_has_error_field(self, tmp_path: pathlib.Path) -> None:
223 root = _init_repo(tmp_path)
224 r = _mt(root, "ghost", "phantom", "--json")
225 d = json.loads(r.output)
226 assert "error" in d
227 assert d["error"]
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 = _mt(root, "ghost", "phantom", "--json")
233 assert "❌" not in r.stderr
234
235
236 # ---------------------------------------------------------------------------
237 # Data integrity
238 # ---------------------------------------------------------------------------
239
240
241 class TestDataIntegrity:
242 def test_merged_manifest_oids_are_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
243 """All non-null object IDs in merged_manifest must carry sha256: prefix."""
244 root = _init_repo(tmp_path)
245 _diverged(root)
246 r = _mt(root, "branch-a", "branch-b", "--json")
247 d = json.loads(r.output)
248 for path, oid in d["merged_manifest"].items():
249 if oid is not None:
250 assert oid.startswith("sha256:"), (
251 f"OID for '{path}' missing sha256: prefix: {oid!r}"
252 )
253
254 def test_base_with_sha256_prefixed_commit_id(self, tmp_path: pathlib.Path) -> None:
255 """--base accepts sha256:-prefixed commit IDs (not just branch names)."""
256 root = _init_repo(tmp_path)
257 base_cid, a_cid, b_cid = _diverged(root)
258 r = _mt(root, "branch-a", "branch-b", "--base", base_cid, "--json")
259 assert r.exit_code == 0
260 d = json.loads(r.output)
261 assert d["base"] == base_cid
262
263 def test_branch_ids_echoed_in_response(self, tmp_path: pathlib.Path) -> None:
264 root = _init_repo(tmp_path)
265 base_cid, a_cid, b_cid = _diverged(root)
266 r = _mt(root, "branch-a", "branch-b", "--json")
267 d = json.loads(r.output)
268 assert d["branch1"] == a_cid
269 assert d["branch2"] == b_cid
270
271 def test_conflict_paths_have_null_oid(self, tmp_path: pathlib.Path) -> None:
272 root = _init_repo(tmp_path)
273 _conflicted(root)
274 r = _mt(root, "branch-a", "branch-b", "--json")
275 d = json.loads(r.output)
276 for path in d["conflicts"]:
277 assert d["merged_manifest"][path] is None
278
279 def test_snapshot_id_is_sha256_prefixed(self, tmp_path: pathlib.Path) -> None:
280 """--write-objects snapshot_id must carry sha256: prefix."""
281 root = _init_repo(tmp_path)
282 _diverged(root)
283 r = _mt(root, "branch-a", "branch-b", "--write-objects", "--json")
284 d = json.loads(r.output)
285 assert "snapshot_id" in d
286 assert d["snapshot_id"].startswith("sha256:"), (
287 f"snapshot_id missing sha256: prefix: {d['snapshot_id']!r}"
288 )
289
290
291 # ---------------------------------------------------------------------------
292 # No-prose pollution
293 # ---------------------------------------------------------------------------
294
295
296 class TestNoProsePollution:
297 def test_clean_merge_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
298 root = _init_repo(tmp_path)
299 _diverged(root)
300 r = _mt(root, "branch-a", "branch-b", "--json")
301 json.loads(r.output) # must not raise
302
303 def test_conflict_merge_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
304 root = _init_repo(tmp_path)
305 _conflicted(root)
306 json.loads(_mt(root, "branch-a", "branch-b", "--json").output)
307
308 def test_no_emoji_in_clean_json(self, tmp_path: pathlib.Path) -> None:
309 root = _init_repo(tmp_path)
310 _diverged(root)
311 r = _mt(root, "branch-a", "branch-b", "--json")
312 assert "✅" not in r.output
313 assert "❌" not in r.output
314
315
316 # ---------------------------------------------------------------------------
317 # TypedDicts
318 # ---------------------------------------------------------------------------
319
320
321 class TestTypedDicts:
322 def test_merge_tree_json_typeddict_exists(self) -> None:
323 from muse.cli.commands.merge_tree import _MergeTreeJson
324 assert _MergeTreeJson is not None
325
326 def test_merge_tree_error_json_typeddict_exists(self) -> None:
327 from muse.cli.commands.merge_tree import _MergeTreeErrorJson
328 assert _MergeTreeErrorJson is not None
329
330 def test_merge_tree_json_has_exit_code_annotation(self) -> None:
331 from muse.cli.commands.merge_tree import _MergeTreeJson
332 hints = get_type_hints(_MergeTreeJson)
333 assert "exit_code" in hints
334
335 def test_merge_tree_json_has_duration_ms_annotation(self) -> None:
336 from muse.cli.commands.merge_tree import _MergeTreeJson
337 hints = get_type_hints(_MergeTreeJson)
338 assert "duration_ms" in hints
339
340 def test_merge_tree_error_json_has_required_fields(self) -> None:
341 from muse.cli.commands.merge_tree import _MergeTreeErrorJson
342 hints = get_type_hints(_MergeTreeErrorJson)
343 for field in ("status", "error", "exit_code"):
344 assert field in hints, f"Missing annotation: {field!r}"
345
346
347 # ---------------------------------------------------------------------------
348 # Docstring coverage
349 # ---------------------------------------------------------------------------
350
351
352 class TestDocstring:
353 def _doc(self) -> str:
354 import muse.cli.commands.merge_tree as mod
355 return mod.__doc__ or ""
356
357 def test_docstring_documents_exit_code(self) -> None:
358 assert "exit_code" in self._doc()
359
360 def test_docstring_documents_duration_ms(self) -> None:
361 assert "duration_ms" in self._doc()
362
363
364 # ---------------------------------------------------------------------------
365 # Stress
366 # ---------------------------------------------------------------------------
367
368
369 class TestStress:
370 def test_100_files_40_pct_conflicts(self, tmp_path: pathlib.Path) -> None:
371 root = _init_repo(tmp_path)
372 n = 100
373 conflict_n = 40
374
375 base_manifest = {f"f{i:03d}.py": _write_obj(root, f"base-{i}".encode()) for i in range(n)}
376 base_cid = _make_commit(root, base_manifest, "main", msg="base")
377
378 a_manifest = dict(base_manifest)
379 for i in range(conflict_n):
380 a_manifest[f"f{i:03d}.py"] = _write_obj(root, f"a-{i}".encode())
381 _make_commit(root, a_manifest, "stress-a", parent=base_cid)
382
383 b_manifest = dict(base_manifest)
384 for i in range(conflict_n):
385 b_manifest[f"f{i:03d}.py"] = _write_obj(root, f"b-{i}".encode())
386 _make_commit(root, b_manifest, "stress-b", parent=base_cid)
387
388 r = _mt(root, "stress-a", "stress-b", "--json")
389 assert r.exit_code != 0
390 d = json.loads(r.output)
391 assert len(d["conflicts"]) == conflict_n
392 assert d["exit_code"] != 0
393 assert "duration_ms" in d
394
395
396 # ---------------------------------------------------------------------------
397 # TestRegisterFlags — argparse-level verification
398 # ---------------------------------------------------------------------------
399
400
401 class TestRegisterFlags:
402 """Verify that register() wires --json / -j correctly."""
403
404 def _make_parser(self) -> "argparse.ArgumentParser":
405 import argparse
406 from muse.cli.commands.merge_tree import register
407 ap = argparse.ArgumentParser()
408 subs = ap.add_subparsers()
409 register(subs)
410 return ap
411
412 def test_json_flag_long(self) -> None:
413 ns = self._make_parser().parse_args(["merge-tree", "feat/x", "dev", "--json"])
414 assert ns.json_out is True
415
416 def test_j_alias(self) -> None:
417 ns = self._make_parser().parse_args(["merge-tree", "feat/x", "dev", "-j"])
418 assert ns.json_out is True
419
420 def test_default_is_text(self) -> None:
421 ns = self._make_parser().parse_args(["merge-tree", "feat/x", "dev"])
422 assert ns.json_out is False
423
424 def test_dest_is_json_out(self) -> None:
425 ns = self._make_parser().parse_args(["merge-tree", "feat/x", "dev", "-j"])
426 assert hasattr(ns, "json_out")
427 assert not hasattr(ns, "fmt")
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago