pagepiper/tests/test_synthesizer.py
pyr0ball 0e493ab560 feat(api): add retriever, synthesizer, and chat endpoint (BSL — BYOK gate)
- 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)
2026-05-04 17:47:10 -07:00

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")