from __future__ import annotations import pytest from pydantic import ValidationError from yuxi.gateway.protocol.errors import ErrorCode, ErrorDetail from yuxi.gateway.protocol.frames import EventFrame, RequestFrame, ResponseFrame class TestRequestFrame: def test_minimal_construction(self): frame = RequestFrame(id="req-001", method="chat.send") assert frame.type == "req" assert frame.id == "req-001" assert frame.method == "chat.send" assert frame.params == {} def test_with_params(self): frame = RequestFrame(id="req-002", method="chat.send", params={"content": "hello"}) assert frame.params == {"content": "hello"} def test_id_is_required(self): with pytest.raises(ValidationError): RequestFrame(method="chat.send") def test_method_is_required(self): with pytest.raises(ValidationError): RequestFrame(id="req-001") def test_json_serialization(self): frame = RequestFrame(id="req-001", method="chat.send", params={"key": "val"}) data = frame.model_dump() assert data == {"type": "req", "id": "req-001", "method": "chat.send", "params": {"key": "val"}} def test_json_deserialization(self): data = {"type": "req", "id": "req-001", "method": "chat.send", "params": {"key": "val"}} frame = RequestFrame.model_validate(data) assert frame.id == "req-001" assert frame.method == "chat.send" def test_params_default_empty_dict(self): frame = RequestFrame(id="r1", method="ping") assert frame.params == {} assert isinstance(frame.params, dict) def test_params_not_shared(self): frame1 = RequestFrame(id="r1", method="ping") frame2 = RequestFrame(id="r2", method="pong") frame1.params["key"] = "val" assert frame2.params == {} def test_id_accepts_uuid_string(self): frame = RequestFrame(id="550e8400-e29b-41d4-a716-446655440000", method="chat.send") assert frame.id == "550e8400-e29b-41d4-a716-446655440000" def test_params_accepts_nested_dicts(self): frame = RequestFrame(id="r1", method="config.update", params={"nested": {"key": "value"}}) assert frame.params["nested"]["key"] == "value" class TestResponseFrame: def test_success_response(self): frame = ResponseFrame(id="req-001", ok=True, payload={"result": "ok"}) assert frame.type == "res" assert frame.id == "req-001" assert frame.ok is True assert frame.payload == {"result": "ok"} assert frame.error is None def test_error_response(self): error = ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message="something went wrong") frame = ResponseFrame(id="req-001", ok=False, error=error) assert frame.ok is False assert frame.error.code == ErrorCode.INTERNAL_ERROR assert frame.error.message == "something went wrong" def test_id_is_required(self): with pytest.raises(ValidationError): ResponseFrame(ok=True) def test_ok_is_required(self): with pytest.raises(ValidationError): ResponseFrame(id="req-001") def test_payload_default_none(self): frame = ResponseFrame(id="r1", ok=True) assert frame.payload is None def test_error_default_none(self): frame = ResponseFrame(id="r1", ok=True) assert frame.error is None def test_json_serialization_success(self): frame = ResponseFrame(id="req-001", ok=True, payload="hello") data = frame.model_dump() assert data == {"type": "res", "id": "req-001", "ok": True, "payload": "hello", "error": None} def test_json_serialization_error(self): error = ErrorDetail(code=ErrorCode.RATE_LIMITED, message="too fast", retryable=True, retry_after_ms=1000) frame = ResponseFrame(id="req-001", ok=False, error=error) data = frame.model_dump() assert data["ok"] is False assert data["error"]["code"] == -32000 assert data["error"]["retryable"] is True assert data["error"]["retry_after_ms"] == 1000 def test_deserialization_with_error(self): data = { "type": "res", "id": "r1", "ok": False, "error": {"code": -32601, "message": "not found"}, "payload": None, } frame = ResponseFrame.model_validate(data) assert frame.error.code == ErrorCode.METHOD_NOT_FOUND def test_payload_accepts_list(self): frame = ResponseFrame(id="r1", ok=True, payload=[1, 2, 3]) assert frame.payload == [1, 2, 3] def test_payload_accepts_scalar(self): frame = ResponseFrame(id="r1", ok=True, payload=42) assert frame.payload == 42 class TestEventFrame: def test_minimal_construction(self): frame = EventFrame(event="message.received") assert frame.type == "event" assert frame.event == "message.received" assert frame.payload is None assert frame.seq == 0 assert frame.state_version == 0 def test_with_payload_and_seq(self): frame = EventFrame(event="message.received", payload={"text": "hi"}, seq=5, state_version=3) assert frame.payload == {"text": "hi"} assert frame.seq == 5 assert frame.state_version == 3 def test_event_is_required(self): with pytest.raises(ValidationError): EventFrame() def test_default_seq_zero(self): frame = EventFrame(event="ping") assert frame.seq == 0 def test_default_state_version_zero(self): frame = EventFrame(event="ping") assert frame.state_version == 0 def test_json_serialization(self): frame = EventFrame(event="message.received", payload={"text": "hi"}, seq=1) data = frame.model_dump() expected = { "type": "event", "event": "message.received", "payload": {"text": "hi"}, "seq": 1, "state_version": 0, } assert data == expected def test_deserialization(self): data = {"type": "event", "event": "typing", "payload": None, "seq": 0, "state_version": 0} frame = EventFrame.model_validate(data) assert frame.event == "typing" def test_event_accepts_dotted_name(self): frame = EventFrame(event="bot.added") assert frame.event == "bot.added" def test_seq_must_be_int(self): with pytest.raises(ValidationError): EventFrame(event="test", seq="invalid") def test_state_version_must_be_int(self): with pytest.raises(ValidationError): EventFrame(event="test", state_version="invalid")