test_response_shapes.py•13.2 kB
"""
Unit tests for response shape consistency.
Validates that all response objects follow consistent structure
and that error/success responses maintain same shape.
"""
import pytest
from pydantic import ValidationError
from mcp_debug_tool.schemas import (
BreakpointResponse,
ContinueResponse,
EndSessionResponse,
ExecutionError,
FrameInfo,
SessionStateResponse,
SessionStatus,
StartSessionResponse,
)
class TestResponseShapes:
"""Tests for response object shapes and consistency."""
def test_start_session_response_shape(self):
"""Test StartSessionResponse has required fields."""
response = StartSessionResponse(sessionId="test-session-123")
# Verify required fields
assert hasattr(response, "sessionId")
assert response.sessionId == "test-session-123"
assert isinstance(response.sessionId, str)
def test_breakpoint_response_shape(self):
"""Test BreakpointResponse has all expected fields."""
response = BreakpointResponse(
hit=True,
frameInfo=FrameInfo(file="test.py", line=1),
locals={"x": {"type": "int", "repr": "1", "isTruncated": False}},
completed=False,
error=None,
)
# Verify all fields exist
assert hasattr(response, "hit")
assert hasattr(response, "frameInfo")
assert hasattr(response, "locals")
assert hasattr(response, "completed")
assert hasattr(response, "error")
# Verify types
assert isinstance(response.hit, bool)
assert response.frameInfo is None or isinstance(response.frameInfo, FrameInfo)
assert response.locals is None or isinstance(response.locals, dict)
assert isinstance(response.completed, bool)
assert response.error is None or isinstance(response.error, ExecutionError)
def test_breakpoint_response_with_error(self):
"""Test BreakpointResponse with error."""
error = ExecutionError(
type="ValueError",
message="Invalid input",
traceback=None,
)
response = BreakpointResponse(
hit=False,
frameInfo=None,
locals=None,
completed=True,
error=error,
)
assert response.hit is False
assert response.error is not None
assert response.error.type == "ValueError"
assert response.error.message == "Invalid input"
def test_breakpoint_response_minimal(self):
"""Test BreakpointResponse with minimal fields."""
response = BreakpointResponse(hit=False, completed=False)
# Required fields only
assert response.hit is False
assert response.completed is False
# Optional fields default to None
assert response.frameInfo is None
assert response.locals is None
assert response.error is None
def test_continue_response_shape(self):
"""Test ContinueResponse has all expected fields."""
response = ContinueResponse(
hit=True,
completed=False,
frameInfo=FrameInfo(file="test.py", line=5),
locals={"y": {"type": "int", "repr": "2", "isTruncated": False}},
error=None,
)
# Verify all fields
assert hasattr(response, "hit")
assert hasattr(response, "completed")
assert hasattr(response, "frameInfo")
assert hasattr(response, "locals")
assert hasattr(response, "error")
def test_continue_response_with_error(self):
"""Test ContinueResponse with error."""
error = ExecutionError(
type="TimeoutError",
message="Timeout",
traceback=None,
)
response = ContinueResponse(
hit=False,
completed=False,
error=error,
)
assert response.error is not None
assert response.error.type == "TimeoutError"
def test_execution_error_shape(self):
"""Test ExecutionError has required fields."""
error = ExecutionError(
type="RuntimeError",
message="Something went wrong",
traceback="File ...",
)
assert hasattr(error, "type")
assert hasattr(error, "message")
assert hasattr(error, "traceback")
assert isinstance(error.type, str)
assert isinstance(error.message, str)
assert error.traceback is None or isinstance(error.traceback, str)
def test_execution_error_minimal(self):
"""Test ExecutionError with minimal fields."""
error = ExecutionError(
type="Error",
message="Error message",
)
assert error.type == "Error"
assert error.message == "Error message"
assert error.traceback is None
def test_frame_info_shape(self):
"""Test FrameInfo has required fields."""
frame = FrameInfo(file="script.py", line=10)
assert hasattr(frame, "file")
assert hasattr(frame, "line")
assert isinstance(frame.file, str)
assert isinstance(frame.line, int)
def test_session_state_response_shape(self):
"""Test SessionStateResponse has expected fields."""
response = SessionStateResponse(
status=SessionStatus.PAUSED,
lastBreakpoint=None,
timings=None,
)
assert hasattr(response, "status")
assert hasattr(response, "lastBreakpoint")
assert hasattr(response, "timings")
assert isinstance(response.status, SessionStatus)
def test_end_session_response_shape(self):
"""Test EndSessionResponse has required fields."""
response = EndSessionResponse(ended=True)
assert hasattr(response, "ended")
assert response.ended is True
def test_responses_are_serializable(self):
"""Test that responses can be serialized to dict."""
response = BreakpointResponse(
hit=True,
frameInfo=FrameInfo(file="test.py", line=1),
locals={"x": {"type": "int", "repr": "1", "isTruncated": False}},
completed=False,
error=None,
)
# Should be able to convert to dict (for JSON serialization)
data = response.model_dump()
assert isinstance(data, dict)
assert "hit" in data
assert "frameInfo" in data
assert "locals" in data
def test_error_response_is_serializable(self):
"""Test that error responses can be serialized."""
error = ExecutionError(
type="ValueError",
message="Bad value",
traceback=None,
)
data = error.model_dump()
assert isinstance(data, dict)
assert data["type"] == "ValueError"
assert data["message"] == "Bad value"
class TestResponseConsistency:
"""Tests for consistency across different response types."""
def test_success_and_error_responses_have_same_structure(self):
"""Test that success and error responses have compatible structure."""
# Success response
success = BreakpointResponse(
hit=True,
frameInfo=FrameInfo(file="test.py", line=1),
locals={},
completed=False,
error=None,
)
# Error response
error = BreakpointResponse(
hit=False,
frameInfo=None,
locals=None,
completed=False,
error=ExecutionError(
type="Error",
message="Error",
),
)
# Both have same fields
success_fields = set(success.model_fields.keys())
error_fields = set(error.model_fields.keys())
assert success_fields == error_fields
def test_breakpoint_and_continue_responses_have_same_fields(self):
"""Test that breakpoint and continue responses have same structure."""
bp_response = BreakpointResponse(hit=True)
cont_response = ContinueResponse(hit=True, completed=False)
# Extract field names
bp_fields = set(bp_response.model_fields.keys())
cont_fields = set(cont_response.model_fields.keys())
# Should have mostly same fields (continue has completed)
common_fields = {"hit", "frameInfo", "locals", "error"}
assert common_fields.issubset(bp_fields)
assert common_fields.issubset(cont_fields)
def test_error_field_is_optional_everywhere(self):
"""Test that error field is optional in all responses."""
# Breakpoint without error
bp1 = BreakpointResponse(hit=True)
assert bp1.error is None
# Breakpoint with error
bp2 = BreakpointResponse(
hit=False,
error=ExecutionError(type="Error", message="msg"),
)
assert bp2.error is not None
# Continue without error
cont1 = ContinueResponse(hit=True, completed=False)
assert cont1.error is None
# Continue with error
cont2 = ContinueResponse(
hit=False,
completed=False,
error=ExecutionError(type="Error", message="msg"),
)
assert cont2.error is not None
def test_frame_info_field_is_optional(self):
"""Test that frameInfo is optional."""
# With frame info
response1 = BreakpointResponse(
hit=True,
frameInfo=FrameInfo(file="test.py", line=1),
)
assert response1.frameInfo is not None
# Without frame info
response2 = BreakpointResponse(hit=False)
assert response2.frameInfo is None
def test_locals_field_is_optional(self):
"""Test that locals field is optional."""
# With locals
response1 = BreakpointResponse(
hit=True,
locals={"x": {"type": "int", "repr": "1", "isTruncated": False}},
)
assert response1.locals is not None
# Without locals
response2 = BreakpointResponse(hit=False)
assert response2.locals is None
class TestResponseFieldTypes:
"""Tests for response field types."""
def test_hit_field_is_boolean(self):
"""Test that hit field is boolean."""
response1 = BreakpointResponse(hit=True)
assert isinstance(response1.hit, bool)
response2 = BreakpointResponse(hit=False)
assert isinstance(response2.hit, bool)
def test_completed_field_is_boolean(self):
"""Test that completed field is boolean."""
response1 = BreakpointResponse(hit=True, completed=True)
assert isinstance(response1.completed, bool)
response2 = BreakpointResponse(hit=False, completed=False)
assert isinstance(response2.completed, bool)
def test_locals_field_is_dict(self):
"""Test that locals field is dict."""
response = BreakpointResponse(
hit=True,
locals={"x": {"type": "int", "repr": "1", "isTruncated": False}},
)
assert isinstance(response.locals, dict)
def test_session_id_is_string(self):
"""Test that sessionId is string."""
response = StartSessionResponse(sessionId="abc123")
assert isinstance(response.sessionId, str)
def test_error_type_and_message_are_strings(self):
"""Test that error fields are strings."""
error = ExecutionError(
type="ValueError",
message="Invalid value",
)
assert isinstance(error.type, str)
assert isinstance(error.message, str)
def test_frame_file_and_line(self):
"""Test that frame file is string and line is int."""
frame = FrameInfo(file="test.py", line=42)
assert isinstance(frame.file, str)
assert isinstance(frame.line, int)
class TestResponseValidation:
"""Tests for response validation."""
def test_response_requires_hit_field(self):
"""Test that BreakpointResponse requires hit field."""
# Missing hit field should fail with ValidationError
with pytest.raises(ValidationError):
BreakpointResponse()
def test_response_requires_completed_for_continue(self):
"""Test that ContinueResponse requires certain fields."""
# Continue requires at least hit and completed
with pytest.raises(ValidationError):
ContinueResponse()
def test_error_requires_type_and_message(self):
"""Test that ExecutionError requires type and message."""
# Missing type should fail
with pytest.raises(ValidationError):
ExecutionError(message="msg")
# Missing message should fail
with pytest.raises(ValidationError):
ExecutionError(type="Error")
def test_frame_info_requires_file_and_line(self):
"""Test that FrameInfo requires file and line."""
# Missing file should fail
with pytest.raises(ValidationError):
FrameInfo(line=1)
# Missing line should fail
with pytest.raises(ValidationError):
FrameInfo(file="test.py")