gabriel / muse public
test_cli_clone.py python
311 lines 11.1 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 days ago
1 """Tests for muse clone CLI command."""
2
3 from __future__ import annotations
4
5 import base64
6 import json
7 import pathlib
8 import unittest.mock
9
10 import pytest
11 from tests.cli_test_helper import CliRunner
12
13 cli = None # argparse migration — CliRunner ignores this arg
14 from muse.cli.config import get_remote, get_upstream
15 from muse.core.pack import ObjectPayload, MPackBundle, RemoteInfo
16 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
17 from muse.core.store import read_commit, read_snapshot
18 from muse.core.transport import TransportError
19 from muse.core.types import Manifest, blob_id, long_id
20 from muse.core.paths import head_path, muse_dir, repo_json_path
21
22 runner = CliRunner()
23
24
25 # ---------------------------------------------------------------------------
26 # Helpers
27 # ---------------------------------------------------------------------------
28
29
30
31 def _make_remote_info(
32 domain: str = "midi",
33 default_branch: str = "main",
34 branch_heads: Manifest | None = None,
35 repo_id: str = "remote-repo-id",
36 ) -> RemoteInfo:
37 return RemoteInfo(
38 repo_id=repo_id,
39 domain=domain,
40 default_branch=default_branch,
41 branch_heads={"main": long_id("c" * 64)} if branch_heads is None else branch_heads,
42 )
43
44
45 def _make_bundle(branch: str = "main", message: str = "initial") -> MPackBundle:
46 """Build a MPackBundle whose commit/snapshot IDs are content-addressed.
47
48 Uses ``compute_commit_id`` and ``compute_snapshot_id`` so that
49 ``read_commit``/``read_snapshot`` pass ``_verify_commit_id``/
50 ``_verify_snapshot_id`` on the receiving end.
51 """
52 content = b"cloned content"
53 oid = blob_id(content)
54 committed_at = "2026-01-01T00:00:00+00:00"
55 snap_id = compute_snapshot_id({"hello.txt": oid})
56 cid = compute_commit_id(
57 parent_ids=[],
58 snapshot_id=snap_id,
59 message=message,
60 committed_at_iso=committed_at,
61 author="alice",
62 )
63 return MPackBundle(
64 commits=[
65 {
66 "commit_id": cid,
67 "repo_id": "remote-repo-id",
68 "branch": branch,
69 "snapshot_id": snap_id,
70 "message": message,
71 "committed_at": committed_at,
72 "parent_commit_id": None,
73 "parent2_commit_id": None,
74 "author": "alice",
75 "metadata": {},
76 "structured_delta": None,
77 "sem_ver_bump": "none",
78 "breaking_changes": [],
79 "agent_id": "",
80 "model_id": "",
81 "toolchain_id": "",
82 "prompt_hash": "",
83 "signature": "",
84 "signer_key_id": "",
85 "reviewed_by": [],
86 "test_runs": 0,
87 }
88 ],
89 snapshots=[
90 {
91 "snapshot_id": snap_id,
92 "manifest": {"hello.txt": oid},
93 "created_at": committed_at,
94 }
95 ],
96 objects=[
97 ObjectPayload(object_id=oid, content=content)
98 ],
99 branch_heads={branch: cid},
100 )
101
102
103 def _mock_transport(info: RemoteInfo, bundle: MPackBundle) -> unittest.mock.MagicMock:
104 mock = unittest.mock.MagicMock()
105 mock.fetch_remote_info.return_value = info
106 mock.fetch_pack.return_value = bundle
107 _fetch_result = {
108 "repo_id": info["repo_id"],
109 "domain": info["domain"],
110 "default_branch": info["default_branch"],
111 "branch_heads": info["branch_heads"],
112 "commits": bundle["commits"],
113 "snapshots": bundle["snapshots"],
114 "objects_received": len(bundle["objects"]),
115 }
116 mock.fetch_stream.return_value = _fetch_result
117 mock.fetch_presign_or_stream.return_value = _fetch_result
118 return mock
119
120
121 # ---------------------------------------------------------------------------
122 # Tests
123 # ---------------------------------------------------------------------------
124
125
126 class TestClone:
127 def test_clone_creates_muse_dir(
128 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
129 ) -> None:
130 monkeypatch.chdir(tmp_path)
131 info = _make_remote_info()
132 bundle = _make_bundle()
133 mock = _mock_transport(info, bundle)
134
135 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
136 result = runner.invoke(
137 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
138 )
139
140 assert result.exit_code == 0, result.output
141 assert (tmp_path / "my-repo" / ".muse").is_dir()
142
143 def test_clone_sets_origin_remote(
144 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
145 ) -> None:
146 monkeypatch.chdir(tmp_path)
147 info = _make_remote_info()
148 bundle = _make_bundle()
149 mock = _mock_transport(info, bundle)
150
151 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
152 runner.invoke(
153 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
154 )
155
156 origin = get_remote("origin", tmp_path / "my-repo")
157 assert origin == "https://hub.example.com/repos/r1"
158
159 def test_clone_sets_upstream_tracking(
160 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
161 ) -> None:
162 monkeypatch.chdir(tmp_path)
163 info = _make_remote_info()
164 bundle = _make_bundle()
165 mock = _mock_transport(info, bundle)
166
167 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
168 runner.invoke(
169 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
170 )
171
172 upstream = get_upstream("main", tmp_path / "my-repo")
173 assert upstream == "origin"
174
175 def test_clone_writes_commits_and_snapshots(
176 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
177 ) -> None:
178 monkeypatch.chdir(tmp_path)
179 bundle = _make_bundle()
180 cid = bundle["commits"][0]["commit_id"]
181 info = _make_remote_info(branch_heads={"main": cid})
182 mock = _mock_transport(info, bundle)
183
184 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
185 runner.invoke(
186 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
187 )
188
189 dest = tmp_path / "dest"
190 cid = bundle["commits"][0]["commit_id"]
191 snap_id = bundle["snapshots"][0]["snapshot_id"]
192 assert isinstance(cid, str) and isinstance(snap_id, str)
193 assert read_commit(dest, cid) is not None
194 assert read_snapshot(dest, snap_id) is not None
195
196 def test_clone_propagates_domain(
197 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
198 ) -> None:
199 monkeypatch.chdir(tmp_path)
200 info = _make_remote_info(domain="code")
201 bundle = _make_bundle()
202 mock = _mock_transport(info, bundle)
203
204 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
205 runner.invoke(
206 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
207 )
208
209 dest = tmp_path / "dest"
210 repo_meta = json.loads((repo_json_path(dest)).read_text())
211 assert repo_meta["domain"] == "code"
212
213 def test_clone_uses_remote_repo_id(
214 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
215 ) -> None:
216 monkeypatch.chdir(tmp_path)
217 info = _make_remote_info(repo_id="the-real-repo-id")
218 bundle = _make_bundle()
219 mock = _mock_transport(info, bundle)
220
221 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
222 runner.invoke(
223 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
224 )
225
226 dest = tmp_path / "dest"
227 repo_meta = json.loads((repo_json_path(dest)).read_text())
228 assert repo_meta["repo_id"] == "the-real-repo-id"
229
230 def test_clone_infers_directory_name_from_url(
231 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
232 ) -> None:
233 monkeypatch.chdir(tmp_path)
234 info = _make_remote_info()
235 bundle = _make_bundle()
236 mock = _mock_transport(info, bundle)
237
238 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
239 runner.invoke(cli, ["clone", "https://hub.example.com/repos/my-project"])
240
241 assert (tmp_path / "my-project" / ".muse").is_dir()
242
243 def test_clone_transport_error_fails_cleanly(
244 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
245 ) -> None:
246 monkeypatch.chdir(tmp_path)
247 mock = unittest.mock.MagicMock()
248 mock.fetch_remote_info.side_effect = TransportError("connection refused", 0)
249
250 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
251 result = runner.invoke(
252 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
253 )
254
255 assert result.exit_code != 0
256 assert "Cannot reach remote" in result.stderr
257
258 def test_clone_existing_repo_fails(
259 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
260 ) -> None:
261 monkeypatch.chdir(tmp_path)
262 # Pre-create a .muse directory.
263 muse_dir(tmp_path / "dest").mkdir(parents=True)
264
265 mock = unittest.mock.MagicMock()
266 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
267 result = runner.invoke(
268 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
269 )
270
271 assert result.exit_code != 0
272 assert "already a Muse repository" in result.stderr
273
274 def test_clone_empty_repo_fails(
275 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
276 ) -> None:
277 monkeypatch.chdir(tmp_path)
278 info = _make_remote_info(branch_heads={}) # no branches
279 mock = unittest.mock.MagicMock()
280 mock.fetch_remote_info.return_value = info
281
282 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
283 result = runner.invoke(
284 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
285 )
286
287 assert result.exit_code != 0
288 assert "empty" in result.stderr.lower() or "no branches" in result.stderr.lower()
289
290 def test_clone_branch_flag_selects_branch(
291 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
292 ) -> None:
293 monkeypatch.chdir(tmp_path)
294 info = _make_remote_info(
295 default_branch="main",
296 branch_heads={"main": long_id("c" * 64), "dev": long_id("d" * 64)},
297 )
298 bundle = _make_bundle(branch="dev", message="initial on dev")
299 mock = _mock_transport(info, bundle)
300
301 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
302 # Options must precede positional args in add_typer groups.
303 result = runner.invoke(
304 cli,
305 ["clone", "--branch", "dev", "https://hub.example.com/repos/r1", "dest"],
306 )
307
308 assert result.exit_code == 0, result.output
309 dest = tmp_path / "dest"
310 head_ref = (head_path(dest)).read_text().strip()
311 assert "dev" in head_ref
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago