gabriel / muse public
test_plumbing_merge_base.py python
169 lines 5.8 KB
e88283c9 feat(plumbing): add 7 new interrogation commands to complete plumbing layer Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 """Tests for ``muse plumbing merge-base``.
2
3 Verifies commit-ID resolution, branch-name resolution, HEAD resolution,
4 text-format output, and error handling for unresolvable refs.
5 """
6
7 from __future__ import annotations
8
9 import datetime
10 import hashlib
11 import json
12 import pathlib
13
14 import pytest
15 from typer.testing import CliRunner
16
17 from muse.cli.app import cli
18 from muse.core.errors import ExitCode
19 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
20
21 runner = CliRunner()
22
23
24 # ---------------------------------------------------------------------------
25 # Helpers
26 # ---------------------------------------------------------------------------
27
28
29 def _sha(tag: str) -> str:
30 return hashlib.sha256(tag.encode()).hexdigest()
31
32
33 def _init_repo(path: pathlib.Path, domain: str = "midi") -> pathlib.Path:
34 muse = path / ".muse"
35 (muse / "commits").mkdir(parents=True)
36 (muse / "snapshots").mkdir(parents=True)
37 (muse / "objects").mkdir(parents=True)
38 (muse / "refs" / "heads").mkdir(parents=True)
39 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
40 (muse / "repo.json").write_text(
41 json.dumps({"repo_id": "test-repo", "domain": domain}), encoding="utf-8"
42 )
43 return path
44
45
46 def _env(repo: pathlib.Path) -> dict[str, str]:
47 return {"MUSE_REPO_ROOT": str(repo)}
48
49
50 def _snap(repo: pathlib.Path) -> str:
51 sid = _sha("snap")
52 write_snapshot(
53 repo,
54 SnapshotRecord(
55 snapshot_id=sid,
56 manifest={},
57 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
58 ),
59 )
60 return sid
61
62
63 def _commit(
64 repo: pathlib.Path,
65 tag: str,
66 snap_id: str,
67 branch: str = "main",
68 parent: str | None = None,
69 ) -> str:
70 cid = _sha(tag)
71 write_commit(
72 repo,
73 CommitRecord(
74 commit_id=cid,
75 repo_id="test-repo",
76 branch=branch,
77 snapshot_id=snap_id,
78 message=tag,
79 committed_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
80 author="tester",
81 parent_commit_id=parent,
82 ),
83 )
84 return cid
85
86
87 def _set_branch(repo: pathlib.Path, branch: str, commit_id: str) -> None:
88 ref = repo / ".muse" / "refs" / "heads" / branch
89 ref.parent.mkdir(parents=True, exist_ok=True)
90 ref.write_text(commit_id, encoding="utf-8")
91
92
93 def _linear_dag(repo: pathlib.Path) -> tuple[str, str, str]:
94 """Build A → B (main) and A → C (feat). Returns (A, B, C)."""
95 sid = _snap(repo)
96 cid_a = _commit(repo, "base", sid)
97 cid_b = _commit(repo, "main-tip", sid, branch="main", parent=cid_a)
98 cid_c = _commit(repo, "feat-tip", sid, branch="feat", parent=cid_a)
99 _set_branch(repo, "main", cid_b)
100 _set_branch(repo, "feat", cid_c)
101 (repo / ".muse" / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
102 return cid_a, cid_b, cid_c
103
104
105 # ---------------------------------------------------------------------------
106 # Tests
107 # ---------------------------------------------------------------------------
108
109
110 class TestMergeBase:
111 def test_finds_common_ancestor_by_commit_id(self, tmp_path: pathlib.Path) -> None:
112 repo = _init_repo(tmp_path)
113 cid_a, cid_b, cid_c = _linear_dag(repo)
114 result = runner.invoke(cli, ["plumbing", "merge-base", cid_b, cid_c], env=_env(repo))
115 assert result.exit_code == 0, result.output
116 data = json.loads(result.stdout)
117 assert data["merge_base"] == cid_a
118 assert data["commit_a"] == cid_b
119 assert data["commit_b"] == cid_c
120
121 def test_branch_names_resolve_to_correct_base(self, tmp_path: pathlib.Path) -> None:
122 repo = _init_repo(tmp_path)
123 cid_a, _b, _c = _linear_dag(repo)
124 result = runner.invoke(cli, ["plumbing", "merge-base", "main", "feat"], env=_env(repo))
125 assert result.exit_code == 0, result.output
126 assert json.loads(result.stdout)["merge_base"] == cid_a
127
128 def test_head_resolves_to_current_branch(self, tmp_path: pathlib.Path) -> None:
129 repo = _init_repo(tmp_path)
130 cid_a, _b, _c = _linear_dag(repo)
131 result = runner.invoke(cli, ["plumbing", "merge-base", "HEAD", "feat"], env=_env(repo))
132 assert result.exit_code == 0, result.output
133 assert json.loads(result.stdout)["merge_base"] == cid_a
134
135 def test_same_commit_returns_itself(self, tmp_path: pathlib.Path) -> None:
136 repo = _init_repo(tmp_path)
137 sid = _snap(repo)
138 cid = _commit(repo, "solo", sid)
139 _set_branch(repo, "main", cid)
140 result = runner.invoke(cli, ["plumbing", "merge-base", cid, cid], env=_env(repo))
141 assert result.exit_code == 0, result.output
142 assert json.loads(result.stdout)["merge_base"] == cid
143
144 def test_text_format_emits_bare_commit_id(self, tmp_path: pathlib.Path) -> None:
145 repo = _init_repo(tmp_path)
146 cid_a, cid_b, cid_c = _linear_dag(repo)
147 result = runner.invoke(
148 cli, ["plumbing", "merge-base", "--format", "text", cid_b, cid_c], env=_env(repo)
149 )
150 assert result.exit_code == 0, result.output
151 assert cid_a in result.stdout
152
153 def test_unresolvable_ref_a_exits_user_error(self, tmp_path: pathlib.Path) -> None:
154 repo = _init_repo(tmp_path)
155 result = runner.invoke(
156 cli, ["plumbing", "merge-base", "no-such-branch", "also-missing"], env=_env(repo)
157 )
158 assert result.exit_code == ExitCode.USER_ERROR
159 assert "error" in json.loads(result.stdout)
160
161 def test_bad_format_flag_exits_user_error(self, tmp_path: pathlib.Path) -> None:
162 repo = _init_repo(tmp_path)
163 sid = _snap(repo)
164 cid = _commit(repo, "c", sid)
165 _set_branch(repo, "main", cid)
166 result = runner.invoke(
167 cli, ["plumbing", "merge-base", "--format", "yaml", cid, cid], env=_env(repo)
168 )
169 assert result.exit_code == ExitCode.USER_ERROR