- app/services/retriever.py: hybrid BM25 + semantic Retriever with BM25-only fallback when llm=None - app/services/synthesizer.py: LLM answer synthesis with citation assembly over retrieved chunks - app/api/chat.py: POST /api/chat endpoint with 402 gate when PAGEPIPER_OLLAMA_URL is unset - tests/test_synthesizer.py: 3 TDD unit tests (mocked LLM, context building, system prompt) - tests/test_chat_api.py: 2 integration tests (402 without Ollama, 200 with mocked retriever+LLM)
54 lines
1.7 KiB
Python
54 lines
1.7 KiB
Python
# tests/test_synthesizer.py
|
|
"""Tests for Synthesizer — mocked LLM, citation assembly."""
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
from app.services.retriever import RetrievedChunk
|
|
from app.services.synthesizer import Synthesizer, SynthesisResult
|
|
|
|
|
|
def _chunk(doc_id: str = "book-a", page: int = 5, text: str = "Fireball rules") -> RetrievedChunk:
|
|
return RetrievedChunk(
|
|
chunk_id="c1", doc_id=doc_id, page_number=page, text=text,
|
|
bm25_score=1.0, vector_score=None,
|
|
)
|
|
|
|
|
|
def test_synthesizer_returns_answer_and_citations():
|
|
mock_llm = MagicMock()
|
|
mock_llm.complete.return_value = "Fireball deals 8d6 damage [p.5]."
|
|
|
|
synth = Synthesizer(mock_llm)
|
|
result = synth.synthesize(
|
|
message="How does Fireball work?",
|
|
history=[],
|
|
chunks=[_chunk()],
|
|
)
|
|
|
|
assert isinstance(result, SynthesisResult)
|
|
assert "Fireball" in result.answer
|
|
assert len(result.citations) == 1
|
|
assert result.citations[0].page_number == 5
|
|
assert result.citations[0].doc_id == "book-a"
|
|
|
|
|
|
def test_synthesizer_builds_context_from_chunks():
|
|
mock_llm = MagicMock()
|
|
mock_llm.complete.return_value = "Answer."
|
|
|
|
synth = Synthesizer(mock_llm)
|
|
synth.synthesize("Q?", [], [_chunk(text="Detailed rule text here.")])
|
|
|
|
call_args = mock_llm.complete.call_args
|
|
assert "Detailed rule text here." in call_args[0][0] or "Detailed rule text here." in str(call_args)
|
|
|
|
|
|
def test_synthesizer_uses_system_prompt():
|
|
mock_llm = MagicMock()
|
|
mock_llm.complete.return_value = "Answer."
|
|
synth = Synthesizer(mock_llm)
|
|
synth.synthesize("Q?", [], [_chunk()])
|
|
|
|
call_kwargs = mock_llm.complete.call_args
|
|
assert call_kwargs.kwargs.get("system") or call_kwargs[1].get("system")
|