- 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)
59 lines
1.9 KiB
Python
59 lines
1.9 KiB
Python
# tests/test_chat_api.py
|
|
"""Tests for POST /api/chat — RAG chat (BSL, BYOK gate)."""
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from app.services.retriever import RetrievedChunk
|
|
|
|
|
|
def test_chat_returns_402_without_ollama(client, monkeypatch):
|
|
monkeypatch.delenv("PAGEPIPER_OLLAMA_URL", raising=False)
|
|
resp = client.post("/api/chat", json={"message": "How does Fireball work?", "history": []})
|
|
assert resp.status_code == 402
|
|
body = resp.json()
|
|
assert "detail" in body
|
|
assert "Ollama" in body["detail"]["message"]
|
|
|
|
|
|
def test_chat_returns_answer_with_mocked_ollama(client, test_db, monkeypatch):
|
|
monkeypatch.setenv("PAGEPIPER_OLLAMA_URL", "http://localhost:11434")
|
|
|
|
conn = sqlite3.connect(test_db)
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO documents(id, title, file_path, status) VALUES ('b1','PHB','phb.pdf','ready')"
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO page_chunks(doc_id, page_number, text, source, word_count) "
|
|
"VALUES ('b1',15,'Fireball deals 8d6 fire damage.','text_layer',6)"
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
mock_llm = MagicMock()
|
|
mock_llm.complete.return_value = "Fireball deals 8d6 fire damage [p.15]."
|
|
|
|
mock_chunks = [
|
|
RetrievedChunk(
|
|
chunk_id="c1",
|
|
doc_id="b1",
|
|
page_number=15,
|
|
text="Fireball deals 8d6 fire damage.",
|
|
bm25_score=1.0,
|
|
vector_score=None,
|
|
)
|
|
]
|
|
|
|
with patch("app.api.chat.Retriever.hybrid_search", return_value=mock_chunks):
|
|
with patch("app.api.chat._get_llm_router", return_value=mock_llm):
|
|
resp = client.post(
|
|
"/api/chat",
|
|
json={"message": "How does Fireball work?", "history": [], "doc_ids": ["b1"]},
|
|
)
|
|
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert "answer" in body
|
|
assert "citations" in body
|
|
assert "Fireball" in body["answer"]
|