"""Tests for domain models — pure data structures with no framework dependencies.""" from datetime import datetime, timezone from domain.models import ( RuleDocument, RuleSearchResult, Conversation, ChatMessage, LLMResponse, ChatResult, ) class TestRuleDocument: """RuleDocument holds rule content with metadata for the knowledge base.""" def test_create_with_required_fields(self): doc = RuleDocument( rule_id="5.2.1(b)", title="Stolen Base Attempts", section="Baserunning", content="When a runner attempts to steal...", source_file="data/rules/baserunning.md", ) assert doc.rule_id == "5.2.1(b)" assert doc.title == "Stolen Base Attempts" assert doc.section == "Baserunning" assert doc.parent_rule is None assert doc.page_ref is None def test_optional_fields(self): doc = RuleDocument( rule_id="5.2", title="Baserunning Overview", section="Baserunning", content="Overview content", source_file="rules.md", parent_rule="5", page_ref="32", ) assert doc.parent_rule == "5" assert doc.page_ref == "32" def test_metadata_dict_for_vector_store(self): """to_metadata() returns a flat dict suitable for ChromaDB/vector store metadata.""" doc = RuleDocument( rule_id="5.2.1(b)", title="Stolen Base Attempts", section="Baserunning", content="content", source_file="rules.md", parent_rule="5.2", page_ref="32", ) meta = doc.to_metadata() assert meta == { "rule_id": "5.2.1(b)", "title": "Stolen Base Attempts", "section": "Baserunning", "parent_rule": "5.2", "page_ref": "32", "source_file": "rules.md", } def test_metadata_dict_empty_optionals(self): """Optional fields should be empty strings in metadata (not None) for vector stores.""" doc = RuleDocument( rule_id="1.0", title="General", section="General", content="c", source_file="f.md", ) meta = doc.to_metadata() assert meta["parent_rule"] == "" assert meta["page_ref"] == "" class TestRuleSearchResult: """RuleSearchResult is what comes back from a semantic search.""" def test_create(self): result = RuleSearchResult( rule_id="5.2.1(b)", title="Stolen Base Attempts", content="When a runner attempts...", section="Baserunning", similarity=0.85, ) assert result.similarity == 0.85 def test_similarity_bounds(self): """Similarity must be between 0.0 and 1.0.""" import pytest with pytest.raises(ValueError): RuleSearchResult( rule_id="x", title="t", content="c", section="s", similarity=-0.1 ) with pytest.raises(ValueError): RuleSearchResult( rule_id="x", title="t", content="c", section="s", similarity=1.1 ) class TestConversation: """Conversation tracks a chat session between a user and the bot.""" def test_create_with_defaults(self): conv = Conversation( id="conv-123", user_id="user-456", channel_id="chan-789", ) assert conv.id == "conv-123" assert isinstance(conv.created_at, datetime) assert isinstance(conv.last_activity, datetime) def test_explicit_timestamps(self): ts = datetime(2026, 1, 1, tzinfo=timezone.utc) conv = Conversation( id="c", user_id="u", channel_id="ch", created_at=ts, last_activity=ts, ) assert conv.created_at == ts class TestChatMessage: """ChatMessage is a single message in a conversation.""" def test_user_message(self): msg = ChatMessage( id="msg-1", conversation_id="conv-1", content="What is the steal rule?", is_user=True, ) assert msg.is_user is True assert msg.parent_id is None def test_assistant_message_with_parent(self): msg = ChatMessage( id="msg-2", conversation_id="conv-1", content="According to Rule 5.2.1(b)...", is_user=False, parent_id="msg-1", ) assert msg.parent_id == "msg-1" class TestLLMResponse: """LLMResponse is the structured output from the LLM port.""" def test_create(self): resp = LLMResponse( answer="Based on Rule 5.2.1(b), runners can steal...", cited_rules=["5.2.1(b)"], confidence=0.9, needs_human=False, ) assert resp.answer.startswith("Based on") assert resp.confidence == 0.9 def test_defaults(self): resp = LLMResponse(answer="text") assert resp.cited_rules == [] assert resp.confidence == 0.5 assert resp.needs_human is False class TestChatResult: """ChatResult is the final result returned by ChatService to inbound adapters.""" def test_create(self): result = ChatResult( response="answer text", conversation_id="conv-1", message_id="msg-2", parent_message_id="msg-1", cited_rules=["5.2.1(b)"], confidence=0.85, needs_human=False, ) assert result.response == "answer text" assert result.parent_message_id == "msg-1" def test_optional_parent(self): result = ChatResult( response="r", conversation_id="c", message_id="m", cited_rules=[], confidence=0.5, needs_human=False, ) assert result.parent_message_id is None