cgcardona / muse public
test_cli_clone.py python
283 lines 9.8 KB
99c4ff79 feat: add remote sync commands (remote, clone, fetch, pull, push, ls-remote) Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 """Tests for muse clone CLI command."""
2
3 from __future__ import annotations
4
5 import base64
6 import hashlib
7 import json
8 import pathlib
9 import unittest.mock
10
11 import pytest
12 from typer.testing import CliRunner
13
14 from muse.cli.app import cli
15 from muse.cli.config import get_remote, get_upstream
16 from muse.core.pack import ObjectPayload, PackBundle, RemoteInfo
17 from muse.core.store import read_commit, read_snapshot
18 from muse.core.transport import TransportError
19
20 runner = CliRunner()
21
22
23 # ---------------------------------------------------------------------------
24 # Helpers
25 # ---------------------------------------------------------------------------
26
27
28 def _sha(content: bytes) -> str:
29 return hashlib.sha256(content).hexdigest()
30
31
32 def _make_remote_info(
33 domain: str = "midi",
34 default_branch: str = "main",
35 branch_heads: dict[str, str] | None = None,
36 repo_id: str = "remote-repo-id",
37 ) -> RemoteInfo:
38 return RemoteInfo(
39 repo_id=repo_id,
40 domain=domain,
41 default_branch=default_branch,
42 branch_heads={"main": "c1"} if branch_heads is None else branch_heads,
43 )
44
45
46 def _make_bundle(commit_id: str = "c1", branch: str = "main") -> PackBundle:
47 content = b"cloned content"
48 oid = _sha(content)
49 return PackBundle(
50 commits=[
51 {
52 "commit_id": commit_id,
53 "repo_id": "remote-repo-id",
54 "branch": branch,
55 "snapshot_id": "s1",
56 "message": "initial",
57 "committed_at": "2026-01-01T00:00:00+00:00",
58 "parent_commit_id": None,
59 "parent2_commit_id": None,
60 "author": "alice",
61 "metadata": {},
62 "structured_delta": None,
63 "sem_ver_bump": "none",
64 "breaking_changes": [],
65 "agent_id": "",
66 "model_id": "",
67 "toolchain_id": "",
68 "prompt_hash": "",
69 "signature": "",
70 "signer_key_id": "",
71 "format_version": 5,
72 "reviewed_by": [],
73 "test_runs": 0,
74 }
75 ],
76 snapshots=[
77 {
78 "snapshot_id": "s1",
79 "manifest": {"hello.txt": oid},
80 "created_at": "2026-01-01T00:00:00+00:00",
81 }
82 ],
83 objects=[
84 ObjectPayload(object_id=oid, content_b64=base64.b64encode(content).decode())
85 ],
86 branch_heads={"main": commit_id},
87 )
88
89
90 def _mock_transport(info: RemoteInfo, bundle: PackBundle) -> unittest.mock.MagicMock:
91 mock = unittest.mock.MagicMock()
92 mock.fetch_remote_info.return_value = info
93 mock.fetch_pack.return_value = bundle
94 return mock
95
96
97 # ---------------------------------------------------------------------------
98 # Tests
99 # ---------------------------------------------------------------------------
100
101
102 class TestClone:
103 def test_clone_creates_muse_dir(
104 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
105 ) -> None:
106 monkeypatch.chdir(tmp_path)
107 info = _make_remote_info()
108 bundle = _make_bundle()
109 mock = _mock_transport(info, bundle)
110
111 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
112 result = runner.invoke(
113 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
114 )
115
116 assert result.exit_code == 0, result.output
117 assert (tmp_path / "my-repo" / ".muse").is_dir()
118
119 def test_clone_sets_origin_remote(
120 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
121 ) -> None:
122 monkeypatch.chdir(tmp_path)
123 info = _make_remote_info()
124 bundle = _make_bundle()
125 mock = _mock_transport(info, bundle)
126
127 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
128 runner.invoke(
129 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
130 )
131
132 origin = get_remote("origin", tmp_path / "my-repo")
133 assert origin == "https://hub.example.com/repos/r1"
134
135 def test_clone_sets_upstream_tracking(
136 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
137 ) -> None:
138 monkeypatch.chdir(tmp_path)
139 info = _make_remote_info()
140 bundle = _make_bundle()
141 mock = _mock_transport(info, bundle)
142
143 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
144 runner.invoke(
145 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
146 )
147
148 upstream = get_upstream("main", tmp_path / "my-repo")
149 assert upstream == "origin"
150
151 def test_clone_writes_commits_and_snapshots(
152 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
153 ) -> None:
154 monkeypatch.chdir(tmp_path)
155 info = _make_remote_info()
156 bundle = _make_bundle()
157 mock = _mock_transport(info, bundle)
158
159 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
160 runner.invoke(
161 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
162 )
163
164 dest = tmp_path / "dest"
165 assert read_commit(dest, "c1") is not None
166 assert read_snapshot(dest, "s1") is not None
167
168 def test_clone_propagates_domain(
169 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
170 ) -> None:
171 monkeypatch.chdir(tmp_path)
172 info = _make_remote_info(domain="code")
173 bundle = _make_bundle()
174 mock = _mock_transport(info, bundle)
175
176 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
177 runner.invoke(
178 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
179 )
180
181 dest = tmp_path / "dest"
182 repo_meta = json.loads((dest / ".muse" / "repo.json").read_text())
183 assert repo_meta["domain"] == "code"
184
185 def test_clone_uses_remote_repo_id(
186 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
187 ) -> None:
188 monkeypatch.chdir(tmp_path)
189 info = _make_remote_info(repo_id="the-real-repo-id")
190 bundle = _make_bundle()
191 mock = _mock_transport(info, bundle)
192
193 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
194 runner.invoke(
195 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
196 )
197
198 dest = tmp_path / "dest"
199 repo_meta = json.loads((dest / ".muse" / "repo.json").read_text())
200 assert repo_meta["repo_id"] == "the-real-repo-id"
201
202 def test_clone_infers_directory_name_from_url(
203 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
204 ) -> None:
205 monkeypatch.chdir(tmp_path)
206 info = _make_remote_info()
207 bundle = _make_bundle()
208 mock = _mock_transport(info, bundle)
209
210 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
211 runner.invoke(cli, ["clone", "https://hub.example.com/repos/my-project"])
212
213 assert (tmp_path / "my-project" / ".muse").is_dir()
214
215 def test_clone_transport_error_fails_cleanly(
216 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
217 ) -> None:
218 monkeypatch.chdir(tmp_path)
219 mock = unittest.mock.MagicMock()
220 mock.fetch_remote_info.side_effect = TransportError("connection refused", 0)
221
222 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
223 result = runner.invoke(
224 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
225 )
226
227 assert result.exit_code != 0
228 assert "Cannot reach remote" in result.output
229
230 def test_clone_existing_repo_fails(
231 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
232 ) -> None:
233 monkeypatch.chdir(tmp_path)
234 # Pre-create a .muse directory.
235 (tmp_path / "dest" / ".muse").mkdir(parents=True)
236
237 mock = unittest.mock.MagicMock()
238 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
239 result = runner.invoke(
240 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
241 )
242
243 assert result.exit_code != 0
244 assert "already a Muse repository" in result.output
245
246 def test_clone_empty_repo_fails(
247 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
248 ) -> None:
249 monkeypatch.chdir(tmp_path)
250 info = _make_remote_info(branch_heads={}) # no branches
251 mock = unittest.mock.MagicMock()
252 mock.fetch_remote_info.return_value = info
253
254 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
255 result = runner.invoke(
256 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
257 )
258
259 assert result.exit_code != 0
260 assert "empty" in result.output.lower() or "no branches" in result.output.lower()
261
262 def test_clone_branch_flag_selects_branch(
263 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
264 ) -> None:
265 monkeypatch.chdir(tmp_path)
266 info = _make_remote_info(
267 default_branch="main",
268 branch_heads={"main": "c1", "dev": "c2"},
269 )
270 bundle = _make_bundle(commit_id="c2", branch="dev")
271 mock = _mock_transport(info, bundle)
272
273 with unittest.mock.patch("muse.cli.commands.clone.HttpTransport", return_value=mock):
274 # Options must precede positional args in add_typer groups.
275 result = runner.invoke(
276 cli,
277 ["clone", "--branch", "dev", "https://hub.example.com/repos/r1", "dest"],
278 )
279
280 assert result.exit_code == 0, result.output
281 dest = tmp_path / "dest"
282 head_ref = (dest / ".muse" / "HEAD").read_text().strip()
283 assert "dev" in head_ref