test_wire_push_plain.py
python
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