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