gabriel / muse public
test_core_transport.py python
351 lines 13.3 KB
dec4604a feat(mwp): replace JSON+base64 wire protocol with MWP binary msgpack Gabriel Cardona <gabriel@tellurstori.com> 13h ago
1 """Tests for muse.core.transport — HttpTransport and response parsers."""
2
3 from __future__ import annotations
4
5 import json
6 import unittest.mock
7 import urllib.error
8 import urllib.request
9 from io import BytesIO
10
11 import msgpack
12 import pytest
13
14 from muse.core.pack import PackBundle, RemoteInfo
15 from muse.core.transport import (
16 HttpTransport,
17 TransportError,
18 _parse_bundle,
19 _parse_push_result,
20 _parse_remote_info,
21 )
22
23
24 # ---------------------------------------------------------------------------
25 # Helpers
26 # ---------------------------------------------------------------------------
27
28
29 def _mock_response(body: bytes, status: int = 200, content_type: str = "application/x-msgpack") -> unittest.mock.MagicMock:
30 """Return a mock urllib response context manager."""
31 resp = unittest.mock.MagicMock()
32 resp.read.return_value = body
33 resp.headers = {"Content-Type": content_type}
34 resp.__enter__ = lambda s: s
35 resp.__exit__ = unittest.mock.MagicMock(return_value=False)
36 return resp
37
38
39 def _mp(data: dict[str, str | int | float | bool | bytes | None | list[str | dict[str, str | None | bool]] | dict[str, str]]) -> bytes:
40 """Encode data as msgpack."""
41 return msgpack.packb(data, use_bin_type=True)
42
43
44 def _http_error(code: int, body: bytes = b"") -> urllib.error.HTTPError:
45 return urllib.error.HTTPError(
46 url="https://example.com",
47 code=code,
48 msg=str(code),
49 hdrs=None,
50 fp=BytesIO(body),
51 )
52
53
54 # ---------------------------------------------------------------------------
55 # _parse_remote_info
56 # ---------------------------------------------------------------------------
57
58
59 class TestParseRemoteInfo:
60 def test_valid_response(self) -> None:
61 raw = _mp(
62 {
63 "repo_id": "r123",
64 "domain": "midi",
65 "default_branch": "main",
66 "branch_heads": {"main": "abc123", "dev": "def456"},
67 }
68 )
69 info = _parse_remote_info(raw)
70 assert info["repo_id"] == "r123"
71 assert info["domain"] == "midi"
72 assert info["default_branch"] == "main"
73 assert info["branch_heads"] == {"main": "abc123", "dev": "def456"}
74
75 def test_invalid_msgpack_raises_transport_error(self) -> None:
76 with pytest.raises(TransportError):
77 _parse_remote_info(b"\xff\xff\xff\xff\xff invalid")
78
79 def test_non_dict_response_returns_defaults(self) -> None:
80 raw = _mp([1, 2, 3])
81 info = _parse_remote_info(raw)
82 assert info["repo_id"] == ""
83 assert info["branch_heads"] == {}
84
85 def test_missing_fields_get_defaults(self) -> None:
86 raw = _mp({"repo_id": "x"})
87 info = _parse_remote_info(raw)
88 assert info["repo_id"] == "x"
89 assert info["domain"] == "midi"
90 assert info["default_branch"] == "main"
91 assert info["branch_heads"] == {}
92
93 def test_non_string_branch_heads_excluded(self) -> None:
94 raw = _mp({"branch_heads": {"main": "abc", "bad": 123}})
95 info = _parse_remote_info(raw)
96 assert "main" in info["branch_heads"]
97 assert "bad" not in info["branch_heads"]
98
99
100 # ---------------------------------------------------------------------------
101 # _parse_bundle
102 # ---------------------------------------------------------------------------
103
104
105 class TestParseBundle:
106 def test_empty_msgpack_object_returns_empty_bundle(self) -> None:
107 bundle = _parse_bundle(_mp({}))
108 assert bundle == {}
109
110 def test_non_dict_returns_empty_bundle(self) -> None:
111 bundle = _parse_bundle(_mp([]))
112 assert bundle == {}
113
114 def test_commits_extracted(self) -> None:
115 raw = _mp(
116 {
117 "commits": [
118 {
119 "commit_id": "c1",
120 "repo_id": "r1",
121 "branch": "main",
122 "snapshot_id": "1" * 64,
123 "message": "test",
124 "committed_at": "2026-01-01T00:00:00+00:00",
125 "parent_commit_id": None,
126 "parent2_commit_id": None,
127 "author": "bob",
128 "metadata": {},
129 }
130 ]
131 }
132 )
133 bundle = _parse_bundle(raw)
134 commits = bundle.get("commits") or []
135 assert len(commits) == 1
136 assert commits[0]["commit_id"] == "c1"
137
138 def test_objects_extracted(self) -> None:
139 raw = _mp(
140 {
141 "objects": [
142 {
143 "object_id": "abc123",
144 "content": b"hello",
145 }
146 ]
147 }
148 )
149 bundle = _parse_bundle(raw)
150 objs = bundle.get("objects") or []
151 assert len(objs) == 1
152 assert objs[0]["object_id"] == "abc123"
153 assert objs[0]["content"] == b"hello"
154
155 def test_object_missing_content_excluded(self) -> None:
156 raw = _mp({"objects": [{"object_id": "abc"}]})
157 bundle = _parse_bundle(raw)
158 assert (bundle.get("objects") or []) == []
159
160 def test_branch_heads_extracted(self) -> None:
161 raw = _mp({"branch_heads": {"main": "abc123"}})
162 bundle = _parse_bundle(raw)
163 assert bundle.get("branch_heads") == {"main": "abc123"}
164
165
166 # ---------------------------------------------------------------------------
167 # _parse_push_result
168 # ---------------------------------------------------------------------------
169
170
171 class TestParsePushResult:
172 def test_success_response(self) -> None:
173 raw = _mp({"ok": True, "message": "pushed", "branch_heads": {"main": "abc"}})
174 result = _parse_push_result(raw)
175 assert result["ok"] is True
176 assert result["message"] == "pushed"
177 assert result["branch_heads"] == {"main": "abc"}
178
179 def test_failure_response(self) -> None:
180 raw = _mp({"ok": False, "message": "rejected", "branch_heads": {}})
181 result = _parse_push_result(raw)
182 assert result["ok"] is False
183 assert result["message"] == "rejected"
184
185 def test_non_msgpack_raises_transport_error(self) -> None:
186 with pytest.raises(TransportError):
187 _parse_push_result(b"\xff\xff invalid msgpack")
188
189 def test_missing_ok_defaults_false(self) -> None:
190 raw = _mp({"message": "hm", "branch_heads": {}})
191 result = _parse_push_result(raw)
192 assert result["ok"] is False
193
194
195 # ---------------------------------------------------------------------------
196 # HttpTransport — mocked urlopen
197 # ---------------------------------------------------------------------------
198
199
200 class TestHttpTransportFetchRemoteInfo:
201 def test_calls_correct_endpoint(self) -> None:
202 body = _mp(
203 {
204 "repo_id": "r1",
205 "domain": "midi",
206 "default_branch": "main",
207 "branch_heads": {"main": "abc"},
208 }
209 )
210 mock_resp = _mock_response(body)
211 with unittest.mock.patch("muse.core.transport._open_url", return_value=mock_resp) as m:
212 transport = HttpTransport()
213 info = transport.fetch_remote_info("https://hub.example.com/repos/r1", None)
214 req = m.call_args[0][0]
215 assert req.full_url == "https://hub.example.com/repos/r1/refs"
216 assert info["repo_id"] == "r1"
217
218 def test_bearer_token_sent(self) -> None:
219 body = _mp(
220 {"repo_id": "r1", "domain": "midi", "default_branch": "main", "branch_heads": {}}
221 )
222 mock_resp = _mock_response(body)
223 with unittest.mock.patch("muse.core.transport._open_url", return_value=mock_resp) as m:
224 HttpTransport().fetch_remote_info("https://hub.example.com/repos/r1", "my-token")
225 req = m.call_args[0][0]
226 assert req.get_header("Authorization") == "Bearer my-token"
227
228 def test_no_token_no_auth_header(self) -> None:
229 body = _mp(
230 {"repo_id": "r1", "domain": "midi", "default_branch": "main", "branch_heads": {}}
231 )
232 mock_resp = _mock_response(body)
233 with unittest.mock.patch("muse.core.transport._open_url", return_value=mock_resp) as m:
234 HttpTransport().fetch_remote_info("https://hub.example.com/repos/r1", None)
235 req = m.call_args[0][0]
236 assert req.get_header("Authorization") is None
237
238 def test_http_401_raises_transport_error(self) -> None:
239 with unittest.mock.patch(
240 "muse.core.transport._open_url", side_effect=_http_error(401, b"Unauthorized")
241 ):
242 with pytest.raises(TransportError) as exc_info:
243 HttpTransport().fetch_remote_info("https://hub.example.com/repos/r1", None)
244 assert exc_info.value.status_code == 401
245
246 def test_http_404_raises_transport_error(self) -> None:
247 with unittest.mock.patch(
248 "muse.core.transport._open_url", side_effect=_http_error(404)
249 ):
250 with pytest.raises(TransportError) as exc_info:
251 HttpTransport().fetch_remote_info("https://hub.example.com/repos/r1", None)
252 assert exc_info.value.status_code == 404
253
254 def test_http_500_raises_transport_error(self) -> None:
255 with unittest.mock.patch(
256 "muse.core.transport._open_url", side_effect=_http_error(500, b"Internal Error")
257 ):
258 with pytest.raises(TransportError) as exc_info:
259 HttpTransport().fetch_remote_info("https://hub.example.com/repos/r1", None)
260 assert exc_info.value.status_code == 500
261
262 def test_url_error_raises_transport_error_with_code_0(self) -> None:
263 with unittest.mock.patch(
264 "muse.core.transport._open_url",
265 side_effect=urllib.error.URLError("Name or service not known"),
266 ):
267 with pytest.raises(TransportError) as exc_info:
268 HttpTransport().fetch_remote_info("https://bad.host/r", None)
269 assert exc_info.value.status_code == 0
270
271 def test_trailing_slash_stripped_from_url(self) -> None:
272 body = _mp(
273 {"repo_id": "r", "domain": "midi", "default_branch": "main", "branch_heads": {}}
274 )
275 mock_resp = _mock_response(body)
276 with unittest.mock.patch("muse.core.transport._open_url", return_value=mock_resp) as m:
277 HttpTransport().fetch_remote_info("https://hub.example.com/repos/r1/", None)
278 req = m.call_args[0][0]
279 assert req.full_url == "https://hub.example.com/repos/r1/refs"
280
281
282 class TestHttpTransportFetchPack:
283 def test_posts_to_fetch_endpoint(self) -> None:
284 bundle_body = _mp(
285 {
286 "commits": [],
287 "snapshots": [],
288 "objects": [],
289 "branch_heads": {"main": "abc"},
290 }
291 )
292 mock_resp = _mock_response(bundle_body)
293 with unittest.mock.patch("muse.core.transport._open_url", return_value=mock_resp) as m:
294 transport = HttpTransport()
295 bundle = transport.fetch_pack(
296 "https://hub.example.com/repos/r1",
297 "tok",
298 want=["abc"],
299 have=["def"],
300 )
301 req = m.call_args[0][0]
302 assert req.full_url == "https://hub.example.com/repos/r1/fetch"
303 sent = msgpack.unpackb(req.data, raw=False)
304 assert sent["want"] == ["abc"]
305 assert sent["have"] == ["def"]
306 assert bundle.get("branch_heads") == {"main": "abc"}
307
308 def test_http_409_raises_transport_error(self) -> None:
309 with unittest.mock.patch(
310 "muse.core.transport._open_url", side_effect=_http_error(409)
311 ):
312 with pytest.raises(TransportError) as exc_info:
313 HttpTransport().fetch_pack("https://hub.example.com/r", None, [], [])
314 assert exc_info.value.status_code == 409
315
316
317 class TestHttpTransportPushPack:
318 def test_posts_to_push_endpoint(self) -> None:
319 push_body = _mp({"ok": True, "message": "ok", "branch_heads": {"main": "new"}})
320 mock_resp = _mock_response(push_body)
321 bundle: PackBundle = {"commits": [], "snapshots": [], "objects": []}
322 with unittest.mock.patch("muse.core.transport._open_url", return_value=mock_resp) as m:
323 result = HttpTransport().push_pack(
324 "https://hub.example.com/repos/r1", "tok", bundle, "main", False
325 )
326 req = m.call_args[0][0]
327 assert req.full_url == "https://hub.example.com/repos/r1/push"
328 sent = msgpack.unpackb(req.data, raw=False)
329 assert sent["branch"] == "main"
330 assert sent["force"] is False
331 assert result["ok"] is True
332
333 def test_force_flag_sent(self) -> None:
334 push_body = _mp({"ok": True, "message": "", "branch_heads": {}})
335 mock_resp = _mock_response(push_body)
336 bundle: PackBundle = {}
337 with unittest.mock.patch("muse.core.transport._open_url", return_value=mock_resp) as m:
338 HttpTransport().push_pack("https://hub.example.com/r", None, bundle, "main", True)
339 req = m.call_args[0][0]
340 sent = msgpack.unpackb(req.data, raw=False)
341 assert sent["force"] is True
342
343 def test_push_rejected_raises_transport_error(self) -> None:
344 with unittest.mock.patch(
345 "muse.core.transport._open_url", side_effect=_http_error(409, b"non-fast-forward")
346 ):
347 with pytest.raises(TransportError) as exc_info:
348 HttpTransport().push_pack(
349 "https://hub.example.com/r", None, {}, "main", False
350 )
351 assert exc_info.value.status_code == 409