ForcePilot/backend/test/unit/gateway/protocol/test_frames.py

179 lines
6.5 KiB
Python
Raw Normal View History

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")