legco_ai_assistant/backend/app/test/test_phase5_highlight_model...

376 lines
13 KiB
Python

"""Tests for Phase 5.4 Highlight Pydantic models.
Validates ChunkHighlightTarget, HighlightBatchRequest, RelevantSentence,
ChunkHighlights, HighlightBatchResult, and HighlightBatchResponse models.
Ensures correct validation, defaults, and serialization.
"""
import pytest
from pydantic import ValidationError
class TestChunkHighlightTarget:
"""Tests for ChunkHighlightTarget model."""
def test_valid_creation(self):
"""Should create a valid ChunkHighlightTarget with all fields."""
from app.models.highlight import ChunkHighlightTarget
target = ChunkHighlightTarget(
document_id="doc-123",
chunk_index=5,
sub_question_text="What is the main topic?",
sub_question_index=0,
)
assert target.document_id == "doc-123"
assert target.chunk_index == 5
assert target.sub_question_text == "What is the main topic?"
assert target.sub_question_index == 0
def test_missing_document_id_rejected(self):
"""Missing document_id should raise ValidationError."""
from app.models.highlight import ChunkHighlightTarget
with pytest.raises(ValidationError):
ChunkHighlightTarget(
chunk_index=0,
sub_question_text="test",
sub_question_index=0,
)
def test_missing_chunk_index_rejected(self):
"""Missing chunk_index should raise ValidationError."""
from app.models.highlight import ChunkHighlightTarget
with pytest.raises(ValidationError):
ChunkHighlightTarget(
document_id="doc-123",
sub_question_text="test",
sub_question_index=0,
)
def test_negative_chunk_index_accepted(self):
"""Negative chunk_index should be accepted (no gt constraint)."""
from app.models.highlight import ChunkHighlightTarget
target = ChunkHighlightTarget(
document_id="doc-123",
chunk_index=-1,
sub_question_text="test",
sub_question_index=0,
)
assert target.chunk_index == -1
class TestHighlightBatchRequest:
"""Tests for HighlightBatchRequest model."""
def test_valid_with_multiple_targets(self):
"""Should accept a list of ChunkHighlightTarget objects."""
from app.models.highlight import ChunkHighlightTarget, HighlightBatchRequest
request = HighlightBatchRequest(
targets=[
ChunkHighlightTarget(
document_id="doc-123",
chunk_index=0,
sub_question_text="Q1",
sub_question_index=0,
),
ChunkHighlightTarget(
document_id="doc-456",
chunk_index=1,
sub_question_text="Q2",
sub_question_index=1,
),
]
)
assert len(request.targets) == 2
assert request.targets[0].document_id == "doc-123"
assert request.targets[1].document_id == "doc-456"
def test_empty_targets_accepted(self):
"""Empty targets list should be accepted."""
from app.models.highlight import HighlightBatchRequest
request = HighlightBatchRequest(targets=[])
assert request.targets == []
def test_missing_targets_rejected(self):
"""Missing targets field should raise ValidationError."""
from app.models.highlight import HighlightBatchRequest
with pytest.raises(ValidationError):
HighlightBatchRequest() # type: ignore
def test_invalid_target_type_rejected(self):
"""Non-ChunkHighlightTarget items should raise ValidationError."""
from app.models.highlight import HighlightBatchRequest
with pytest.raises(ValidationError):
HighlightBatchRequest(targets=["not a target"]) # type: ignore
class TestRelevantSentence:
"""Tests for RelevantSentence model."""
def test_valid_creation(self):
"""Should create a valid RelevantSentence with description fields."""
from app.models.highlight import RelevantSentence
rs = RelevantSentence(
sentence_index=3,
reason="Directly answers the sub-question",
)
assert rs.sentence_index == 3
assert rs.reason == "Directly answers the sub-question"
def test_reason_max_length_enforced(self):
"""Reason exceeding max_length=80 should raise ValidationError."""
from app.models.highlight import RelevantSentence
with pytest.raises(ValidationError, match="reason"):
RelevantSentence(
sentence_index=0,
reason="x" * 81,
)
def test_reason_at_max_length_accepted(self):
"""Reason exactly at max_length=80 should be accepted."""
from app.models.highlight import RelevantSentence
rs = RelevantSentence(
sentence_index=0,
reason="x" * 80,
)
assert len(rs.reason) == 80
def test_missing_sentence_index_rejected(self):
"""Missing sentence_index should raise ValidationError."""
from app.models.highlight import RelevantSentence
with pytest.raises(ValidationError):
RelevantSentence(reason="test")
def test_missing_reason_rejected(self):
"""Missing reason should raise ValidationError."""
from app.models.highlight import RelevantSentence
with pytest.raises(ValidationError):
RelevantSentence(sentence_index=0)
class TestChunkHighlights:
"""Tests for ChunkHighlights model."""
def test_valid_with_sentences(self):
"""Should create ChunkHighlights with relevant_sentences."""
from app.models.highlight import ChunkHighlights, RelevantSentence
ch = ChunkHighlights(
document_id="doc-123",
chunk_index=0,
relevant_sentences=[
RelevantSentence(sentence_index=1, reason="Key point"),
RelevantSentence(sentence_index=3, reason="Supports answer"),
],
)
assert ch.document_id == "doc-123"
assert ch.chunk_index == 0
assert len(ch.relevant_sentences) == 2
assert ch.relevant_sentences[0].sentence_index == 1
def test_default_empty_sentences(self):
"""Default relevant_sentences should be an empty list."""
from app.models.highlight import ChunkHighlights
ch = ChunkHighlights(
document_id="doc-123",
chunk_index=0,
)
assert ch.relevant_sentences == []
def test_explicit_empty_sentences(self):
"""Explicitly passing empty list should work."""
from app.models.highlight import ChunkHighlights
ch = ChunkHighlights(
document_id="doc-123",
chunk_index=0,
relevant_sentences=[],
)
assert ch.relevant_sentences == []
class TestHighlightBatchResult:
"""Tests for HighlightBatchResult model."""
def test_valid_with_results(self):
"""Should create HighlightBatchResult with ChunkHighlights list."""
from app.models.highlight import (
ChunkHighlights,
HighlightBatchResult,
RelevantSentence,
)
result = HighlightBatchResult(
results=[
ChunkHighlights(
document_id="doc-123",
chunk_index=0,
relevant_sentences=[
RelevantSentence(sentence_index=0, reason="First")
],
),
]
)
assert len(result.results) == 1
assert result.results[0].document_id == "doc-123"
def test_empty_results_accepted(self):
"""Empty results list should be accepted."""
from app.models.highlight import HighlightBatchResult
result = HighlightBatchResult(results=[])
assert result.results == []
def test_missing_results_rejected(self):
"""Missing results field should raise ValidationError."""
from app.models.highlight import HighlightBatchResult
with pytest.raises(ValidationError):
HighlightBatchResult() # type: ignore
class TestHighlightBatchResponse:
"""Tests for HighlightBatchResponse model."""
def test_status_completed(self):
"""Should accept 'completed' status."""
from app.models.highlight import HighlightBatchResponse
resp = HighlightBatchResponse(status="completed")
assert resp.status == "completed"
assert resp.cached_count == 0
assert resp.errors == []
def test_status_partial(self):
"""Should accept 'partial' status."""
from app.models.highlight import HighlightBatchResponse
resp = HighlightBatchResponse(status="partial", cached_count=2)
assert resp.status == "partial"
assert resp.cached_count == 2
def test_status_failed(self):
"""Should accept 'failed' status with errors."""
from app.models.highlight import HighlightBatchResponse
resp = HighlightBatchResponse(
status="failed",
errors=["document not found", "chunk out of range"],
)
assert resp.status == "failed"
assert len(resp.errors) == 2
def test_invalid_status_rejected(self):
"""Status not in Literal should raise ValidationError."""
from app.models.highlight import HighlightBatchResponse
with pytest.raises(ValidationError):
HighlightBatchResponse(status="unknown") # type: ignore
def test_default_cached_count(self):
"""Default cached_count should be 0."""
from app.models.highlight import HighlightBatchResponse
resp = HighlightBatchResponse(status="completed")
assert resp.cached_count == 0
def test_default_errors(self):
"""Default errors should be an empty list."""
from app.models.highlight import HighlightBatchResponse
resp = HighlightBatchResponse(status="completed")
assert resp.errors == []
class TestSerialization:
"""Tests for model_dump() serialization."""
def test_chunk_highlight_target_dump(self):
"""model_dump() should produce expected dict for ChunkHighlightTarget."""
from app.models.highlight import ChunkHighlightTarget
target = ChunkHighlightTarget(
document_id="doc-123",
chunk_index=5,
sub_question_text="What is the main topic?",
sub_question_index=0,
)
data = target.model_dump()
assert data == {
"document_id": "doc-123",
"chunk_index": 5,
"sub_question_text": "What is the main topic?",
"sub_question_index": 0,
}
def test_highlight_batch_request_dump(self):
"""model_dump() should produce expected nested dict."""
from app.models.highlight import ChunkHighlightTarget, HighlightBatchRequest
request = HighlightBatchRequest(
targets=[
ChunkHighlightTarget(
document_id="doc-123",
chunk_index=0,
sub_question_text="Q1",
sub_question_index=0,
),
]
)
data = request.model_dump()
assert data == {
"targets": [
{
"document_id": "doc-123",
"chunk_index": 0,
"sub_question_text": "Q1",
"sub_question_index": 0,
},
]
}
def test_chunk_highlights_dump(self):
"""model_dump() should include default empty list for relevant_sentences."""
from app.models.highlight import ChunkHighlights
ch = ChunkHighlights(document_id="doc-123", chunk_index=0)
data = ch.model_dump()
assert data == {
"document_id": "doc-123",
"chunk_index": 0,
"relevant_sentences": [],
}
def test_highlight_batch_response_dump(self):
"""model_dump() should produce expected dict with defaults."""
from app.models.highlight import HighlightBatchResponse
resp = HighlightBatchResponse(status="partial", cached_count=3)
data = resp.model_dump()
assert data == {
"status": "partial",
"cached_count": 3,
"errors": [],
}
def test_relevant_sentence_reason_max_length(self):
"""model_dump() should preserve reason at max length."""
from app.models.highlight import RelevantSentence
rs = RelevantSentence(sentence_index=0, reason="x" * 80)
data = rs.model_dump()
assert data["reason"] == "x" * 80