# #region Test.Schemas.Agent [C:2] [TYPE Module] [SEMANTICS test,schema,agent] # @BRIEF Tests for schemas/agent.py — ConversationItem, ConversationListResponse, ToolCall, # AttachmentMeta, MessageItem, HistoryResponse, DeleteResponse, ServiceTokenRequest, # ServiceTokenResponse, SaveConversationRequest. # @RELATION BINDS_TO -> [Schemas.Agent] # @TEST_EDGE: missing_field -> optional fields default correctly # @TEST_EDGE: invalid_type -> Pydantic validation rejects bad types # @TEST_EDGE: serialization -> round-trip via model_dump/model_validate from datetime import datetime, timezone import pytest from pydantic import ValidationError from src.schemas.agent import ( AttachmentMeta, ConversationItem, ConversationListResponse, DeleteResponse, HistoryResponse, MessageItem, SaveConversationRequest, ServiceTokenRequest, ServiceTokenResponse, ToolCall, ) class TestConversationItem: """ConversationItem — required fields and defaults.""" def test_minimal(self): ts = datetime.now(timezone.utc) item = ConversationItem(id="c1", title="Chat", updated_at=ts, message_count=5) assert item.id == "c1" assert item.title == "Chat" assert item.message_count == 5 def test_serialize_roundtrip(self): ts = datetime.now(timezone.utc) item = ConversationItem(id="c1", title="Chat", updated_at=ts, message_count=5) data = item.model_dump() restored = ConversationItem.model_validate(data) assert restored.id == item.id assert restored.title == item.title def test_missing_required_raises(self): with pytest.raises(ValidationError): ConversationItem() class TestConversationListResponse: """ConversationListResponse — items list and optional counters.""" def test_defaults(self): resp = ConversationListResponse(items=[]) assert resp.items == [] assert resp.has_next is False assert resp.active_total == 0 assert resp.archived_total == 0 def test_with_items(self): ts = datetime.now(timezone.utc) items = [ConversationItem(id="c1", title="A", updated_at=ts, message_count=1)] resp = ConversationListResponse(items=items, has_next=True, active_total=10, archived_total=2) assert len(resp.items) == 1 assert resp.has_next is True assert resp.active_total == 10 def test_serialize_roundtrip(self): ts = datetime.now(timezone.utc) items = [ConversationItem(id="c1", title="A", updated_at=ts, message_count=1)] resp = ConversationListResponse(items=items, has_next=True) data = resp.model_dump() restored = ConversationListResponse.model_validate(data) assert restored.has_next is True assert len(restored.items) == 1 class TestToolCall: """ToolCall — tool name, input/output/error, status defaults.""" def test_defaults(self): tc = ToolCall(tool="test_tool") assert tc.tool == "test_tool" assert tc.input == {} assert tc.output is None assert tc.error is None assert tc.status == "executing" def test_with_all_fields(self): tc = ToolCall( tool="query", input={"q": "1"}, output={"result": "ok"}, error=None, status="completed", ) assert tc.tool == "query" assert tc.output == {"result": "ok"} def test_serialize_roundtrip(self): tc = ToolCall(tool="calc", input={"expr": "2+2"}, status="completed") data = tc.model_dump() restored = ToolCall.model_validate(data) assert restored.tool == "calc" assert restored.input == {"expr": "2+2"} class TestAttachmentMeta: """AttachmentMeta — name, type, size, optional preview_url.""" def test_minimal(self): am = AttachmentMeta(name="file.pdf", type="pdf", size=1024) assert am.name == "file.pdf" assert am.type == "pdf" assert am.size == 1024 assert am.preview_url is None def test_with_preview(self): am = AttachmentMeta(name="img.png", type="png", size=2048, preview_url="/preview/img.png") assert am.preview_url == "/preview/img.png" def test_serialize_roundtrip(self): am = AttachmentMeta(name="data.csv", type="csv", size=512) data = am.model_dump() restored = AttachmentMeta.model_validate(data) assert restored.name == "data.csv" class TestMessageItem: """MessageItem — all fields including nested ToolCall and AttachmentMeta.""" def test_minimal(self): ts = datetime.now(timezone.utc) msg = MessageItem(id="m1", conversation_id="c1", role="user", created_at=ts) assert msg.text is None assert msg.state is None assert msg.tool_calls is None assert msg.attachments is None def test_with_nested_objects(self): ts = datetime.now(timezone.utc) tc = ToolCall(tool="search", status="completed") am = AttachmentMeta(name="doc.txt", type="txt", size=100) msg = MessageItem( id="m1", conversation_id="c1", role="assistant", text="Here are results", state="done", tool_calls=[tc], attachments=[am], created_at=ts, ) assert len(msg.tool_calls) == 1 assert len(msg.attachments) == 1 assert msg.tool_calls[0].tool == "search" def test_serialize_roundtrip(self): ts = datetime.now(timezone.utc) msg = MessageItem( id="m1", conversation_id="c1", role="user", text="hello", created_at=ts, ) data = msg.model_dump() restored = MessageItem.model_validate(data) assert restored.text == "hello" class TestHistoryResponse: """HistoryResponse — items list and pagination fields.""" def test_defaults(self): resp = HistoryResponse(items=[]) assert resp.items == [] assert resp.has_next is False assert resp.conversation_id is None def test_with_conversation_id(self): resp = HistoryResponse(items=[], conversation_id="c1") assert resp.conversation_id == "c1" def test_serialize_roundtrip(self): ts = datetime.now(timezone.utc) items = [MessageItem(id="m1", conversation_id="c1", role="user", created_at=ts)] resp = HistoryResponse(items=items, has_next=True) data = resp.model_dump() restored = HistoryResponse.model_validate(data) assert restored.has_next is True class TestDeleteResponse: """DeleteResponse — just a deleted flag defaulting True.""" def test_default(self): resp = DeleteResponse() assert resp.deleted is True def test_false(self): resp = DeleteResponse(deleted=False) assert resp.deleted is False def test_serialize_roundtrip(self): resp = DeleteResponse() data = resp.model_dump() restored = DeleteResponse.model_validate(data) assert restored.deleted is True class TestServiceTokenRequest: """ServiceTokenRequest — just service_secret.""" def test_required(self): req = ServiceTokenRequest(service_secret="s3cr3t") assert req.service_secret == "s3cr3t" def test_missing_raises(self): with pytest.raises(ValidationError): ServiceTokenRequest() def test_serialize_roundtrip(self): req = ServiceTokenRequest(service_secret="key") data = req.model_dump() restored = ServiceTokenRequest.model_validate(data) assert restored.service_secret == "key" class TestServiceTokenResponse: """ServiceTokenResponse — token response with defaults.""" def test_defaults(self): resp = ServiceTokenResponse(access_token="jwt") assert resp.access_token == "jwt" assert resp.token_type == "bearer" assert resp.expires_in == 86400 assert resp.role == "agent" def test_custom_values(self): resp = ServiceTokenResponse(access_token="tok", token_type="Bearer", expires_in=3600, role="admin") assert resp.expires_in == 3600 assert resp.role == "admin" def test_serialize_roundtrip(self): resp = ServiceTokenResponse(access_token="jwt-token") data = resp.model_dump() restored = ServiceTokenResponse.model_validate(data) assert restored.access_token == "jwt-token" class TestSaveConversationRequest: """SaveConversationRequest — conversation save payload.""" def test_defaults(self): req = SaveConversationRequest(conversation_id="c1") assert req.title == "" assert req.user_id == "admin" assert req.messages == [] def test_with_messages(self): req = SaveConversationRequest(conversation_id="c1", title="Chat", user_id="u1", messages=[{"role": "user", "text": "hi"}]) assert req.title == "Chat" assert len(req.messages) == 1 def test_missing_required_raises(self): with pytest.raises(ValidationError): SaveConversationRequest() def test_serialize_roundtrip(self): req = SaveConversationRequest(conversation_id="c1", title="Test", messages=[{"role": "user", "text": "hello"}]) data = req.model_dump() restored = SaveConversationRequest.model_validate(data) assert restored.title == "Test" assert restored.messages == [{"role": "user", "text": "hello"}] # #endregion Test.Schemas.Agent