gabriel / muse public
test_cli_clone.py python
309 lines 11.0 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 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
21 runner = CliRunner()
22
23
24 # ---------------------------------------------------------------------------
25 # Helpers
26 # ---------------------------------------------------------------------------
27
28
29
30 def _make_remote_info(
31 domain: str = "midi",
32 default_branch: str = "main",
33 branch_heads: Manifest | None = None,
34 repo_id: str = "remote-repo-id",
35 ) -> RemoteInfo:
36 return RemoteInfo(
37 repo_id=repo_id,
38 domain=domain,
39 default_branch=default_branch,
40 branch_heads={"main": long_id("c" * 64)} if branch_heads is None else branch_heads,
41 )
42
43
44 def _make_bundle(branch: str = "main", message: str = "initial") -> MPackBundle:
45 """Build a MPackBundle whose commit/snapshot IDs are content-addressed.
46
47 Uses ``compute_commit_id`` and ``compute_snapshot_id`` so that
48 ``read_commit``/``read_snapshot`` pass ``_verify_commit_id``/
49 ``_verify_snapshot_id`` on the receiving end.
50 """
51 content = b"cloned content"
52 oid = blob_id(content)
53 committed_at = "2026-01-01T00:00:00+00:00"
54 snap_id = compute_snapshot_id({"hello.txt": oid})
55 cid = compute_commit_id(
56 repo_id="remote-repo-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 "created_on_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 mock.fetch_stream.return_value = {
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 return mock
117
118
119 # ---------------------------------------------------------------------------
120 # Tests
121 # ---------------------------------------------------------------------------
122
123
124 class TestClone:
125 def test_clone_creates_muse_dir(
126 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
127 ) -> None:
128 monkeypatch.chdir(tmp_path)
129 info = _make_remote_info()
130 bundle = _make_bundle()
131 mock = _mock_transport(info, bundle)
132
133 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
134 result = runner.invoke(
135 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
136 )
137
138 assert result.exit_code == 0, result.output
139 assert (tmp_path / "my-repo" / ".muse").is_dir()
140
141 def test_clone_sets_origin_remote(
142 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
143 ) -> None:
144 monkeypatch.chdir(tmp_path)
145 info = _make_remote_info()
146 bundle = _make_bundle()
147 mock = _mock_transport(info, bundle)
148
149 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
150 runner.invoke(
151 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
152 )
153
154 origin = get_remote("origin", tmp_path / "my-repo")
155 assert origin == "https://hub.example.com/repos/r1"
156
157 def test_clone_sets_upstream_tracking(
158 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
159 ) -> None:
160 monkeypatch.chdir(tmp_path)
161 info = _make_remote_info()
162 bundle = _make_bundle()
163 mock = _mock_transport(info, bundle)
164
165 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
166 runner.invoke(
167 cli, ["clone", "https://hub.example.com/repos/r1", "my-repo"]
168 )
169
170 upstream = get_upstream("main", tmp_path / "my-repo")
171 assert upstream == "origin"
172
173 def test_clone_writes_commits_and_snapshots(
174 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
175 ) -> None:
176 monkeypatch.chdir(tmp_path)
177 bundle = _make_bundle()
178 cid = bundle["commits"][0]["commit_id"]
179 info = _make_remote_info(branch_heads={"main": cid})
180 mock = _mock_transport(info, bundle)
181
182 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
183 runner.invoke(
184 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
185 )
186
187 dest = tmp_path / "dest"
188 cid = bundle["commits"][0]["commit_id"]
189 snap_id = bundle["snapshots"][0]["snapshot_id"]
190 assert isinstance(cid, str) and isinstance(snap_id, str)
191 assert read_commit(dest, cid) is not None
192 assert read_snapshot(dest, snap_id) is not None
193
194 def test_clone_propagates_domain(
195 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
196 ) -> None:
197 monkeypatch.chdir(tmp_path)
198 info = _make_remote_info(domain="code")
199 bundle = _make_bundle()
200 mock = _mock_transport(info, bundle)
201
202 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
203 runner.invoke(
204 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
205 )
206
207 dest = tmp_path / "dest"
208 repo_meta = json.loads((dest / ".muse" / "repo.json").read_text())
209 assert repo_meta["domain"] == "code"
210
211 def test_clone_uses_remote_repo_id(
212 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
213 ) -> None:
214 monkeypatch.chdir(tmp_path)
215 info = _make_remote_info(repo_id="the-real-repo-id")
216 bundle = _make_bundle()
217 mock = _mock_transport(info, bundle)
218
219 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
220 runner.invoke(
221 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
222 )
223
224 dest = tmp_path / "dest"
225 repo_meta = json.loads((dest / ".muse" / "repo.json").read_text())
226 assert repo_meta["repo_id"] == "the-real-repo-id"
227
228 def test_clone_infers_directory_name_from_url(
229 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
230 ) -> None:
231 monkeypatch.chdir(tmp_path)
232 info = _make_remote_info()
233 bundle = _make_bundle()
234 mock = _mock_transport(info, bundle)
235
236 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
237 runner.invoke(cli, ["clone", "https://hub.example.com/repos/my-project"])
238
239 assert (tmp_path / "my-project" / ".muse").is_dir()
240
241 def test_clone_transport_error_fails_cleanly(
242 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
243 ) -> None:
244 monkeypatch.chdir(tmp_path)
245 mock = unittest.mock.MagicMock()
246 mock.fetch_remote_info.side_effect = TransportError("connection refused", 0)
247
248 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
249 result = runner.invoke(
250 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
251 )
252
253 assert result.exit_code != 0
254 assert "Cannot reach remote" in result.output
255
256 def test_clone_existing_repo_fails(
257 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
258 ) -> None:
259 monkeypatch.chdir(tmp_path)
260 # Pre-create a .muse directory.
261 (tmp_path / "dest" / ".muse").mkdir(parents=True)
262
263 mock = unittest.mock.MagicMock()
264 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
265 result = runner.invoke(
266 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
267 )
268
269 assert result.exit_code != 0
270 assert "already a Muse repository" in result.output
271
272 def test_clone_empty_repo_fails(
273 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
274 ) -> None:
275 monkeypatch.chdir(tmp_path)
276 info = _make_remote_info(branch_heads={}) # no branches
277 mock = unittest.mock.MagicMock()
278 mock.fetch_remote_info.return_value = info
279
280 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
281 result = runner.invoke(
282 cli, ["clone", "https://hub.example.com/repos/r1", "dest"]
283 )
284
285 assert result.exit_code != 0
286 assert "empty" in result.output.lower() or "no branches" in result.output.lower()
287
288 def test_clone_branch_flag_selects_branch(
289 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
290 ) -> None:
291 monkeypatch.chdir(tmp_path)
292 info = _make_remote_info(
293 default_branch="main",
294 branch_heads={"main": "c" * 64, "dev": "d" * 64},
295 )
296 bundle = _make_bundle(branch="dev", message="initial on dev")
297 mock = _mock_transport(info, bundle)
298
299 with unittest.mock.patch("muse.cli.commands.clone.make_transport", return_value=mock):
300 # Options must precede positional args in add_typer groups.
301 result = runner.invoke(
302 cli,
303 ["clone", "--branch", "dev", "https://hub.example.com/repos/r1", "dest"],
304 )
305
306 assert result.exit_code == 0, result.output
307 dest = tmp_path / "dest"
308 head_ref = (dest / ".muse" / "HEAD").read_text().strip()
309 assert "dev" in head_ref
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago