gabriel / muse public
test_cli_remote.py python
227 lines 9.8 KB
Raw
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 7 days ago
1 """Tests for muse remote — add, remove, rename, list, get-url, set-url."""
2
3 from __future__ import annotations
4
5 import json
6 import os
7 import pathlib
8
9 import pytest
10 from tests.cli_test_helper import CliRunner
11
12 from muse._version import __version__
13 cli = None # argparse migration — CliRunner ignores this arg
14 from muse.cli.config import get_remote, list_remotes
15 from muse.core.paths import muse_dir, remotes_dir
16
17
18 # ---------------------------------------------------------------------------
19 # Fixture — initialised repo
20 # ---------------------------------------------------------------------------
21
22
23 @pytest.fixture
24 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
25 """Minimal .muse/ repo; cwd and MUSE_REPO_ROOT set to tmp_path."""
26 dot_muse = muse_dir(tmp_path)
27 (dot_muse / "refs" / "heads").mkdir(parents=True)
28 (dot_muse / "objects").mkdir()
29 (dot_muse / "commits").mkdir()
30 (dot_muse / "snapshots").mkdir()
31 (dot_muse / "repo.json").write_text(
32 json.dumps({"repo_id": "test-repo", "schema_version": __version__, "domain": "midi"})
33 )
34 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
35 (dot_muse / "refs" / "heads" / "main").write_text("")
36 (dot_muse / "config.toml").write_text("")
37 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
38 monkeypatch.chdir(tmp_path)
39 return tmp_path
40
41
42 runner = CliRunner()
43
44
45 # ---------------------------------------------------------------------------
46 # remote add
47 # ---------------------------------------------------------------------------
48
49
50 class TestRemoteAdd:
51 def test_add_new_remote(self, repo: pathlib.Path) -> None:
52 result = runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
53 assert result.exit_code == 0
54 assert "origin" in result.stderr
55 assert get_remote("origin", repo) == "https://hub.muse.io/repos/r1"
56
57 def test_add_duplicate_remote_fails(self, repo: pathlib.Path) -> None:
58 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
59 result = runner.invoke(cli, ["remote", "add", "origin", "https://other.com/r"])
60 assert result.exit_code != 0
61 assert "already exists" in result.stderr
62
63 def test_add_multiple_remotes(self, repo: pathlib.Path) -> None:
64 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
65 runner.invoke(cli, ["remote", "add", "upstream", "https://hub.muse.io/repos/r2"])
66 remotes = {r["name"] for r in list_remotes(repo)}
67 assert remotes == {"origin", "upstream"}
68
69
70 # ---------------------------------------------------------------------------
71 # remote remove
72 # ---------------------------------------------------------------------------
73
74
75 class TestRemoteRemove:
76 def test_remove_existing_remote(self, repo: pathlib.Path) -> None:
77 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
78 result = runner.invoke(cli, ["remote", "remove", "origin"])
79 assert result.exit_code == 0
80 assert get_remote("origin", repo) is None
81
82 def test_remove_nonexistent_remote_fails(self, repo: pathlib.Path) -> None:
83 result = runner.invoke(cli, ["remote", "remove", "ghost"])
84 assert result.exit_code != 0
85 assert "does not exist" in result.stderr
86
87 def test_remove_cleans_tracking_refs(self, repo: pathlib.Path) -> None:
88 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
89 refs_dir = remotes_dir(repo) / "origin"
90 refs_dir.mkdir(parents=True)
91 (refs_dir / "main").write_text("abc123")
92 runner.invoke(cli, ["remote", "remove", "origin"])
93 assert not refs_dir.exists()
94
95
96 # ---------------------------------------------------------------------------
97 # remote rename
98 # ---------------------------------------------------------------------------
99
100
101 class TestRemoteRename:
102 def test_rename_existing_remote(self, repo: pathlib.Path) -> None:
103 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
104 result = runner.invoke(cli, ["remote", "rename", "origin", "upstream"])
105 assert result.exit_code == 0
106 assert get_remote("upstream", repo) == "https://hub.muse.io/repos/r1"
107 assert get_remote("origin", repo) is None
108
109 def test_rename_nonexistent_fails(self, repo: pathlib.Path) -> None:
110 result = runner.invoke(cli, ["remote", "rename", "ghost", "new"])
111 assert result.exit_code != 0
112 assert "does not exist" in result.stderr
113
114 def test_rename_to_existing_name_fails(self, repo: pathlib.Path) -> None:
115 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
116 runner.invoke(cli, ["remote", "add", "upstream", "https://hub.muse.io/repos/r2"])
117 result = runner.invoke(cli, ["remote", "rename", "origin", "upstream"])
118 assert result.exit_code != 0
119 assert "already exists" in result.stderr
120
121
122 # ---------------------------------------------------------------------------
123 # muse remote (implied list)
124 # ---------------------------------------------------------------------------
125
126
127 class TestRemoteList:
128 def test_list_empty(self, repo: pathlib.Path) -> None:
129 result = runner.invoke(cli, ["remote"])
130 assert result.exit_code == 0
131 assert "No remotes" in result.stderr
132
133 def test_list_shows_names(self, repo: pathlib.Path) -> None:
134 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
135 runner.invoke(cli, ["remote", "add", "upstream", "https://hub.muse.io/repos/r2"])
136 result = runner.invoke(cli, ["remote"])
137 assert result.exit_code == 0
138 assert "origin" in result.output
139 assert "upstream" in result.output
140
141 def test_list_verbose_shows_url(self, repo: pathlib.Path) -> None:
142 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
143 result = runner.invoke(cli, ["remote", "-v"])
144 assert result.exit_code == 0
145 assert "https://hub.muse.io/repos/r1" in result.output
146
147 def test_list_verbose_shows_fetch_and_push(self, repo: pathlib.Path) -> None:
148 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
149 result = runner.invoke(cli, ["remote", "-v"])
150 assert result.exit_code == 0
151 assert "(fetch)" in result.output
152 assert "(push)" in result.output
153 lines = [l for l in result.output.splitlines() if "hub.muse.io" in l]
154 assert len(lines) == 2, "verbose should print one fetch line and one push line"
155
156
157 # ---------------------------------------------------------------------------
158 # remote get-url
159 # ---------------------------------------------------------------------------
160
161
162 class TestRemoteGetUrl:
163 def test_get_url_existing(self, repo: pathlib.Path) -> None:
164 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
165 result = runner.invoke(cli, ["remote", "get-url", "origin"])
166 assert result.exit_code == 0
167 assert "https://hub.muse.io/repos/r1" in result.output
168
169 def test_get_url_nonexistent_fails(self, repo: pathlib.Path) -> None:
170 result = runner.invoke(cli, ["remote", "get-url", "ghost"])
171 assert result.exit_code != 0
172 assert "does not exist" in result.stderr
173
174
175 # ---------------------------------------------------------------------------
176 # remote set-url
177 # ---------------------------------------------------------------------------
178
179
180 class TestRemoteSetUrl:
181 def test_set_url_updates_existing(self, repo: pathlib.Path) -> None:
182 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/repos/r1"])
183 result = runner.invoke(
184 cli, ["remote", "set-url", "origin", "https://hub.muse.io/repos/r2"]
185 )
186 assert result.exit_code == 0
187 assert get_remote("origin", repo) == "https://hub.muse.io/repos/r2"
188
189 def test_set_url_nonexistent_fails(self, repo: pathlib.Path) -> None:
190 result = runner.invoke(cli, ["remote", "set-url", "ghost", "https://example.com"])
191 assert result.exit_code != 0
192 assert "does not exist" in result.stderr
193
194
195 # ---------------------------------------------------------------------------
196 # remote --json shape — empty optional fields must be null, not ""
197 # ---------------------------------------------------------------------------
198
199
200 class TestRemoteJsonShape:
201 def test_json_list_empty_tracking_is_null(self, repo: pathlib.Path) -> None:
202 """tracking must be null (not '') when no upstream is configured."""
203 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/r"])
204 result = runner.invoke(cli, ["remote", "--json"])
205 assert result.exit_code == 0
206 data = json.loads(result.output)
207 entry = data["remotes"][0]
208 assert entry["tracking"] is None, f"expected null, got {entry['tracking']!r}"
209
210 def test_json_list_empty_head_is_null(self, repo: pathlib.Path) -> None:
211 """head must be null (not '') when no remote HEAD is known."""
212 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/r"])
213 result = runner.invoke(cli, ["remote", "--json"])
214 assert result.exit_code == 0
215 data = json.loads(result.output)
216 entry = data["remotes"][0]
217 assert entry["head"] is None, f"expected null, got {entry['head']!r}"
218
219 def test_json_list_multiple_remotes_all_null_when_no_upstream(self, repo: pathlib.Path) -> None:
220 """All remotes without upstream must have null tracking and head."""
221 runner.invoke(cli, ["remote", "add", "origin", "https://hub.muse.io/r1"])
222 runner.invoke(cli, ["remote", "add", "upstream", "https://hub.muse.io/r2"])
223 result = runner.invoke(cli, ["remote", "--json"])
224 data = json.loads(result.output)
225 for entry in data["remotes"]:
226 assert entry["tracking"] is None
227 assert entry["head"] is None
File History 1 commit
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 7 days ago