gabriel / musehub public
test_wire_push_plain.py python
219 lines 7.4 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 144 days ago
1 """TDD — server reads raw MWP frames and returns a plain final response.
2
3 Rules:
4
5 S7 push/stream returns Content-Type: application/x-muse-wire.
6
7 S8 A valid push with raw MWP frames yields ok=True.
8 Full round-trip: client sends raw MWP, server parses, returns result.
9
10 S9 Auth is checked before any frames are consumed.
11 """
12 from __future__ import annotations
13
14 import inspect
15 from collections.abc import AsyncIterator
16 from datetime import datetime, timezone
17 from unittest.mock import AsyncMock, MagicMock
18
19 import msgpack
20 import pytest
21 from httpx import AsyncClient
22 from sqlalchemy.ext.asyncio import AsyncSession
23
24 from muse.core.types import blob_id
25 from musehub.types.json_types import JSONObject, JSONValue, StrDict
26 from musehub.models.wire import (
27 SFRAME_COMMIT_PACK,
28 SFRAME_END,
29 SFRAME_HEADER,
30 SFRAME_RESULT,
31 )
32 from muse.core.mpack import WIRE_CONTENT_TYPE, MuseWireFrameWriter
33 from tests.factories import create_repo
34
35 _fw = MuseWireFrameWriter()
36
37
38 def _last_frame(raw: bytes) -> JSONObject:
39 unpacker = msgpack.Unpacker(raw=False)
40 unpacker.feed(raw)
41 last: JSONObject = {}
42 for frame in unpacker:
43 last = frame
44 return last
45
46
47 # ---------------------------------------------------------------------------
48 # Helpers — raw MWP (no gRPC prefix)
49 # ---------------------------------------------------------------------------
50
51 def _pack(obj: JSONValue) -> bytes:
52 return msgpack.packb(obj, use_bin_type=True)
53
54
55 def _wrap_raw(ft: str, data: JSONValue) -> bytes:
56 """Wrap a logical frame in a raw MWP envelope — NO gRPC prefix."""
57 return _fw.wrap(frame_type=ft, payload=_pack(data))
58
59
60 def _sha256_oid(data: bytes) -> str:
61 return blob_id(data)
62
63
64 def _utc() -> str:
65 return datetime.now(tz=timezone.utc).isoformat()
66
67
68 def _header_frame(n_objects: int = 0, n_commits: int = 1) -> bytes:
69 return _wrap_raw(SFRAME_HEADER, {
70 "t": SFRAME_HEADER,
71 "branch": "main",
72 "force": False,
73 "have": [],
74 "head": _sha256_oid(b"head"),
75 "n_objects": n_objects,
76 "n_commits": n_commits,
77 })
78
79
80 def _commit_pack_frame(commits: list[dict], snapshots: list[dict]) -> bytes:
81 return _wrap_raw(SFRAME_COMMIT_PACK, {
82 "t": SFRAME_COMMIT_PACK,
83 "commits": commits,
84 "snapshots": snapshots,
85 })
86
87
88 def _end_frame(n_objects: int = 0, n_commits: int = 0) -> bytes:
89 return _wrap_raw(SFRAME_END, {"t": SFRAME_END, "n_objects": n_objects, "n_commits": n_commits})
90
91
92 def _make_commit(snapshot_id: str | None = None) -> JSONObject:
93 snap = snapshot_id or _sha256_oid(b"default-snap")
94 return {
95 "commit_id": _sha256_oid(f"commit-{_utc()}".encode()),
96 "parent_ids": [],
97 "snapshot_id": snap,
98 "branch": "main",
99 "message": "test commit",
100 "author": "gabriel",
101 "committed_at": _utc(),
102 "signature": "",
103 "signer_key_id": "",
104 "agent_id": "",
105 "model_id": "",
106 "metadata": {},
107 }
108
109
110 def _make_snapshot(snap_id: str) -> JSONObject:
111 return {"snapshot_id": snap_id, "manifest": {}}
112
113
114 def _stub_r2_backend(monkeypatch: pytest.MonkeyPatch) -> None:
115 backend = MagicMock()
116 backend.store_object = AsyncMock(return_value=None)
117 backend.object_exists = AsyncMock(return_value=False)
118 backend.get_object = AsyncMock(return_value=None)
119 monkeypatch.setattr("musehub.services.musehub_wire.get_backend", lambda: backend)
120
121
122 # ---------------------------------------------------------------------------
123 # S7 — push/stream response Content-Type is application/x-muse-wire
124 # ---------------------------------------------------------------------------
125
126 def test_s7_push_route_response_content_type() -> None:
127 """push/stream response Content-Type must be application/x-muse-wire."""
128 from musehub.api.routes import wire
129
130 source = inspect.getsource(wire.push_stream)
131 assert "WIRE_CONTENT_TYPE" in source or "application/x-muse-wire" in source, (
132 "push/stream route must set Content-Type: application/x-muse-wire. "
133 "Replace GRPC_CONTENT_TYPE with WIRE_CONTENT_TYPE."
134 )
135 assert "GRPC_CONTENT_TYPE" not in source, (
136 "push/stream route still uses GRPC_CONTENT_TYPE. "
137 "Replace with WIRE_CONTENT_TYPE."
138 )
139
140
141 # ---------------------------------------------------------------------------
142 # S8 — valid push with raw MWP frames yields ok=True
143 # ---------------------------------------------------------------------------
144
145 @pytest.mark.asyncio
146 async def test_s8_raw_mwp_push_succeeds(
147 client: AsyncClient,
148 db_session: AsyncSession,
149 auth_headers: StrDict,
150 monkeypatch: pytest.MonkeyPatch,
151 ) -> None:
152 """A push body with raw MWP frames (no gRPC prefix) must yield ok=True.
153
154 This is the core end-to-end test: client builds raw MWP frames,
155 sends them as a plain HTTPS POST, server reads and stores them,
156 returns a plain msgpack result body.
157 """
158 _stub_r2_backend(monkeypatch)
159 repo = await create_repo(db_session, owner="testuser", name="s8-plain-push")
160
161 snap_id = _sha256_oid(b"s8-snap")
162 commit = _make_commit(snapshot_id=snap_id)
163 snap = _make_snapshot(snap_id)
164 body = _header_frame() + _commit_pack_frame([commit], [snap]) + _end_frame(n_commits=1)
165
166 # Confirm body does NOT start with gRPC prefix
167 assert body[:1] != b"\x00", "Test body should be raw MWP, not gRPC-prefixed"
168 assert body[:4] == b"muse", "Test body should start with MWP magic"
169
170 resp = await client.post(
171 f"/{repo.owner}/{repo.slug}/push/stream",
172 content=body,
173 headers={**auth_headers, "Content-Type": WIRE_CONTENT_TYPE},
174 )
175
176 assert resp.status_code == 200, f"Expected 200, got {resp.status_code}: {resp.text[:200]}"
177 assert resp.headers.get("content-type", "").startswith(WIRE_CONTENT_TYPE), (
178 f"Expected Content-Type {WIRE_CONTENT_TYPE}, got {resp.headers.get('content-type')}"
179 )
180
181 # Response is a plain msgpack dict, not a stream of gRPC frames
182 result = _last_frame(resp.content)
183 assert isinstance(result, dict), f"Expected dict response, got {type(result)}"
184 assert result.get("ok") is True, f"Expected ok=True, got: {result}"
185 assert "stored_commits" in result, f"Missing stored_commits in result: {result}"
186
187
188 # ---------------------------------------------------------------------------
189 # S9 — auth is still checked before stream consumption (unchanged)
190 # ---------------------------------------------------------------------------
191
192 @pytest.mark.asyncio
193 async def test_s9_auth_still_checked_before_stream(
194 client: AsyncClient,
195 db_session: AsyncSession,
196 monkeypatch: pytest.MonkeyPatch,
197 ) -> None:
198 """Unauthenticated push must be rejected without consuming the request body."""
199 _stub_r2_backend(monkeypatch)
200 repo = await create_repo(db_session, owner="testuser", name="s9-auth-plain")
201
202 wire_was_called = []
203
204 async def _spy(session: AsyncSession, repo_id: str, body_iter: AsyncIterator[bytes], pusher_id: str | None) -> None:
205 wire_was_called.append(True)
206
207 monkeypatch.setattr("musehub.api.routes.wire.wire_push_stream", _spy)
208
209 body = _header_frame() + _end_frame()
210 resp = await client.post(
211 f"/{repo.owner}/{repo.slug}/push/stream",
212 content=body,
213 headers={"Content-Type": WIRE_CONTENT_TYPE},
214 )
215
216 assert resp.status_code in (401, 403), (
217 f"Unauthenticated push should return 401/403, got {resp.status_code}"
218 )
219 assert not wire_was_called, "wire_push_stream called despite missing auth"
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ 144 days ago