gabriel / muse public
test_transport_push_mpack_unpack.py python
164 lines 6.8 KB
Raw
sha256:79ffe87f5fe2ec146e35f05521218bbf54dffdb0440c07f970bad05f16efb89f chore: merge main — carry all urllib/typing/test fixes from dev Sonnet 4.6 minor ⚠ breaking 19 days ago
1 """TDD — HttpTransport.push_mpack_unpack: client-side Step 3 of the mpack push protocol.
2
3 MU-1 Happy path: POST returns job_id, head, branch, objects_in_mpack, commits_in_mpack.
4 MU-2 POST body encodes all five unpack fields correctly.
5 MU-3 POST is sent to /{owner}/{slug}/push/unpack-mpack.
6 MU-4 Non-2xx response raises TransportError.
7 MU-5 Response missing job_id raises TransportError.
8 MU-6 MSign Authorization header is present (unlike step 2).
9 """
10 from __future__ import annotations
11
12 from unittest.mock import MagicMock, patch
13
14 import msgpack
15 import pytest
16
17 from muse.core.transport import HttpTransport, TransportError
18 from muse.core.types import blob_id, fake_id
19
20 _Headers = dict[str, str]
21 _URL = "https://staging.musehub.ai/gabriel/muse"
22 _MPACK_KEY = blob_id(b"mpack bytes")
23 _HEAD = fake_id("tip-commit")
24 _JOB_ID = fake_id("job-id")
25
26
27 def _unpack_response(
28 job_id: str = _JOB_ID,
29 head: str = _HEAD,
30 branch: str = "main",
31 objects_in_mpack: int = 5,
32 commits_in_mpack: int = 2,
33 ) -> bytes:
34 return msgpack.packb({
35 "job_id": job_id,
36 "head": head,
37 "branch": branch,
38 "objects_in_mpack": objects_in_mpack,
39 "commits_in_mpack": commits_in_mpack,
40 }, use_bin_type=True)
41
42
43 class _Resp:
44 def __init__(self, body: bytes, status: int = 200) -> None:
45 self.status_code = status
46 self.content = body
47
48
49 def _mock_client(resp: _Resp) -> MagicMock:
50 mock = MagicMock()
51 mock.__enter__ = MagicMock(return_value=mock)
52 mock.__exit__ = MagicMock(return_value=False)
53 mock.post = MagicMock(return_value=resp)
54 return mock
55
56
57 # ── MU-1 ──────────────────────────────────────────────────────────────────────
58
59 def test_mu1_happy_path_returns_all_fields() -> None:
60 """200 response → all result fields present and correctly parsed."""
61 mock_client = _mock_client(_Resp(_unpack_response()))
62 transport = HttpTransport()
63
64 with patch("muse.core.transport._httpx_mod.Client", return_value=mock_client):
65 result = transport.push_mpack_unpack(
66 _URL, None, _MPACK_KEY, branch="main", head=_HEAD,
67 commits_count=2, objects_count=5,
68 )
69
70 assert result["job_id"] == _JOB_ID
71 assert result["head"] == _HEAD
72 assert result["branch"] == "main"
73 assert result["objects_in_mpack"] == 5
74 assert result["commits_in_mpack"] == 2
75
76
77 # ── MU-2 ──────────────────────────────────────────────────────────────────────
78
79 def test_mu2_post_body_encodes_all_fields() -> None:
80 """POST body must contain all five unpack fields with correct values."""
81 mock_client = _mock_client(_Resp(_unpack_response()))
82 transport = HttpTransport()
83
84 with patch("muse.core.transport._httpx_mod.Client", return_value=mock_client):
85 transport.push_mpack_unpack(
86 _URL, None, _MPACK_KEY, branch="dev", head=_HEAD,
87 commits_count=3, objects_count=10,
88 )
89
90 call_kwargs = mock_client.post.call_args
91 sent_body: bytes = call_kwargs[1].get("content") or call_kwargs[0][1]
92 payload = msgpack.unpackb(sent_body, raw=False)
93
94 assert payload["mpack_key"] == _MPACK_KEY
95 assert payload["branch"] == "dev"
96 assert payload["head"] == _HEAD
97 assert payload["commits_count"] == 3
98 assert payload["objects_count"] == 10
99
100
101 # ── MU-3 ──────────────────────────────────────────────────────────────────────
102
103 def test_mu3_posts_to_correct_endpoint() -> None:
104 """POST must go to /{owner}/{slug}/push/unpack-mpack."""
105 mock_client = _mock_client(_Resp(_unpack_response()))
106 transport = HttpTransport()
107
108 with patch("muse.core.transport._httpx_mod.Client", return_value=mock_client):
109 transport.push_mpack_unpack(_URL, None, _MPACK_KEY, head=_HEAD)
110
111 posted_url: str = mock_client.post.call_args[0][0]
112 assert posted_url.endswith("/push/unpack-mpack")
113
114
115 # ── MU-4 ──────────────────────────────────────────────────────────────────────
116
117 def test_mu4_non_200_raises_transport_error() -> None:
118 """4xx/5xx from server raises TransportError."""
119 mock_client = _mock_client(_Resp(b"integrity failure", status=422))
120 transport = HttpTransport()
121
122 with patch("muse.core.transport._httpx_mod.Client", return_value=mock_client):
123 with pytest.raises(TransportError):
124 transport.push_mpack_unpack(_URL, None, _MPACK_KEY, head=_HEAD)
125
126
127 # ── MU-5 ──────────────────────────────────────────────────────────────────────
128
129 def test_mu5_missing_job_id_raises_transport_error() -> None:
130 """Response without job_id raises TransportError."""
131 bad_resp = msgpack.packb({"head": _HEAD, "branch": "main"}, use_bin_type=True)
132 mock_client = _mock_client(_Resp(bad_resp))
133 transport = HttpTransport()
134
135 with patch("muse.core.transport._httpx_mod.Client", return_value=mock_client):
136 with pytest.raises(TransportError, match="job_id"):
137 transport.push_mpack_unpack(_URL, None, _MPACK_KEY, head=_HEAD)
138
139
140 # ── MU-6 ──────────────────────────────────────────────────────────────────────
141
142 def test_mu6_authorization_header_present() -> None:
143 """Unlike step 2, step 3 is a MuseHub API call — MSign auth header required."""
144 from muse.core.transport import SigningIdentity
145 from unittest.mock import MagicMock as MM
146
147 mock_key = MM()
148 mock_key.sign = MM(return_value=b"\x00" * 64)
149 mock_key.public_key = MM(return_value=MM(
150 public_bytes=MM(return_value=b"\x01" * 32)
151 ))
152 signing = SigningIdentity(handle="gabriel", private_key=mock_key)
153
154 mock_client = _mock_client(_Resp(_unpack_response()))
155
156 with patch("muse.core.transport._httpx_mod.Client", return_value=mock_client):
157 with patch("muse.core.msign.build_msign_header", return_value="MSign fake") as mock_sign:
158 transport = HttpTransport()
159 transport.push_mpack_unpack(_URL, signing, _MPACK_KEY, head=_HEAD)
160
161 mock_sign.assert_called_once()
162 call_kwargs = mock_client.post.call_args
163 headers: _Headers = call_kwargs[1].get("headers") or {}
164 assert any(k.lower() == "authorization" for k in headers)
File History 3 commits
sha256:79ffe87f5fe2ec146e35f05521218bbf54dffdb0440c07f970bad05f16efb89f chore: merge main — carry all urllib/typing/test fixes from dev Sonnet 4.6 minor 19 days ago
sha256:0bea7600d1eee83e87950be49933b1006fa9dc2c71e7c4ee748d324f61138156 chore: bump version to 0.2.0rc11; fix typing audit violatio… Sonnet 4.6 minor 19 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago