gabriel / muse public
test_plumbing_merge_base.py python
169 lines 5.9 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d 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 tests.cli_test_helper import CliRunner
16
17 cli = None # argparse migration — CliRunner ignores this arg
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