"""Tests for gRPC framing strip on the server side. The server receives a stream of gRPC length-prefix frames. Each message is one MWP frame. The server strips the 5-byte gRPC header, then feeds the inner bytes to iter_wire_frames exactly as before. Test plan: A. iter_wire_frames_grpc strips gRPC prefix and yields MWP frames correctly B. Empty stream yields nothing C. Multiple gRPC-wrapped MWP frames all decoded in order D. gRPC frame with wrong compression flag raises WireFrameError E. Truncated gRPC header raises WireFrameError F. Truncated gRPC message body raises WireFrameError G. Roundtrip: client grpc_frame(MuseWireFrameWriter.wrap(...)) → server iter """ from __future__ import annotations import struct import asyncio import msgpack import pytest # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _pack(obj: object) -> bytes: return msgpack.packb(obj, use_bin_type=True) def _grpc_wrap(mwp_frame: bytes) -> bytes: """Encode one MWP frame in gRPC length-prefix format.""" return b"\x00" + struct.pack(">I", len(mwp_frame)) + mwp_frame def _make_mwp_frame(ft: str, payload: bytes) -> bytes: from muse.core.mpack import MuseWireFrameWriter return MuseWireFrameWriter().wrap(frame_type=ft, payload=payload) async def _collect(body: bytes) -> list[tuple[dict, bytes]]: """Run iter_wire_frames_grpc over a byte blob, collect all (envelope, payload) pairs.""" from musehub.services.musehub_wire import iter_wire_frames_grpc async def _iter(): yield body results = [] async for envelope, payload in iter_wire_frames_grpc(_iter()): results.append((envelope, payload)) return results def _run(coro): return asyncio.run(coro) # --------------------------------------------------------------------------- # A. Single frame roundtrip # --------------------------------------------------------------------------- def test_single_frame_roundtrip(): payload = _pack({"t": "E"}) mwp = _make_mwp_frame("E", payload) stream = _grpc_wrap(mwp) results = _run(_collect(stream)) assert len(results) == 1 envelope, decoded_payload = results[0] assert envelope["ft"] == "E" assert decoded_payload == payload # --------------------------------------------------------------------------- # B. Empty stream # --------------------------------------------------------------------------- def test_empty_stream_yields_nothing(): results = _run(_collect(b"")) assert results == [] # --------------------------------------------------------------------------- # C. Multiple frames in order # --------------------------------------------------------------------------- def test_multiple_frames_decoded_in_order(): frames = [ _make_mwp_frame("H", _pack({"t": "H", "repo": "r", "branch": "b", "have": [], "local_head": None, "force": False, "object_count": 0})), _make_mwp_frame("E", _pack({"t": "E"})), ] stream = b"".join(_grpc_wrap(f) for f in frames) results = _run(_collect(stream)) assert len(results) == 2 assert results[0][0]["ft"] == "H" assert results[1][0]["ft"] == "E" # --------------------------------------------------------------------------- # D. Wrong compression flag raises WireFrameError # --------------------------------------------------------------------------- def test_wrong_compression_flag_raises(): from muse.core.mpack import WireFrameError from musehub.services.musehub_wire import iter_wire_frames_grpc mwp = _make_mwp_frame("E", _pack({"t": "E"})) bad_stream = b"\x01" + struct.pack(">I", len(mwp)) + mwp # flag=1 = compressed async def _run_bad(): async def _iter(): yield bad_stream async for _ in iter_wire_frames_grpc(_iter()): pass with pytest.raises(WireFrameError, match="compress"): _run(_run_bad()) # --------------------------------------------------------------------------- # E. Truncated gRPC header raises WireFrameError # --------------------------------------------------------------------------- def test_truncated_grpc_header_raises(): from muse.core.mpack import WireFrameError from musehub.services.musehub_wire import iter_wire_frames_grpc async def _run_bad(): async def _iter(): yield b"\x00\x00\x00" # only 3 bytes — header needs 5 async for _ in iter_wire_frames_grpc(_iter()): pass with pytest.raises(WireFrameError): _run(_run_bad()) # --------------------------------------------------------------------------- # F. Truncated gRPC message body raises WireFrameError # --------------------------------------------------------------------------- def test_truncated_grpc_body_raises(): from muse.core.mpack import WireFrameError from musehub.services.musehub_wire import iter_wire_frames_grpc async def _run_bad(): async def _iter(): # says 100 bytes but only provides 10 yield b"\x00" + struct.pack(">I", 100) + b"x" * 10 async for _ in iter_wire_frames_grpc(_iter()): pass with pytest.raises(WireFrameError): _run(_run_bad()) # --------------------------------------------------------------------------- # G. Full roundtrip: client grpc_frame(MuseWireFrameWriter.wrap) → server iter # --------------------------------------------------------------------------- def test_full_client_server_roundtrip(): from muse.core.mpack import grpc_frame, MuseWireFrameWriter fw = MuseWireFrameWriter() payload = _pack({"t": "E"}) mwp = fw.wrap(frame_type="E", payload=payload) stream = grpc_frame(mwp) results = _run(_collect(stream)) assert len(results) == 1 envelope, decoded_payload = results[0] assert envelope["ft"] == "E" assert decoded_payload == payload