Add production condition retrieval smoke test

This commit is contained in:
2026-08-11 14:58:28 +07:00
parent 59e6ad2d0d
commit 7ebbe1f309
38 changed files with 3752 additions and 121 deletions
+104 -1
View File
@@ -1,10 +1,11 @@
from fastapi.testclient import TestClient
from adapters.prometheus import PrometheusMetrics
from adapters.postgres import FeedbackTraceNotFound
from config import Settings
from main import create_app
from rag.agent import AgentReply
from rag.answer import Citation, GroundedAnswerService
from rag.answer import DISCLAIMER, Citation, GroundedAnswerService
from rag.metrics import TRACE_WRITE_FAILED, InMemoryMetrics
from rag.models import EvidenceDecision, QueryIntent, RetrievalResult, SubjectScope
@@ -20,11 +21,51 @@ class FixedRouting:
class MemoryTraceWriter:
def __init__(self):
self.rows = []
self.feedback = []
def save(self, **fields):
self.rows.append(fields)
return "trace-1"
def save_feedback(self, **fields):
self.feedback.append(fields)
return "feedback-1"
def test_feedback_is_linked_to_the_answer_trace():
traces = MemoryTraceWriter()
app = create_app(settings=Settings(), trace_writer=traces)
response = TestClient(app).post("/v1/rag/feedback", json={
"trace_id": "8f33dd3e-9000-4384-ae69-33da2629e29b",
"rating": "not_helpful",
"comment": " Thiếu lưu ý suy thận. ",
"conversation_id": "case-1",
})
assert response.status_code == 200
assert response.json() == {"feedback_id": "feedback-1", "status": "saved"}
assert traces.feedback == [{
"trace_id": "8f33dd3e-9000-4384-ae69-33da2629e29b",
"rating": "not_helpful",
"comment": "Thiếu lưu ý suy thận.",
"conversation_id": "case-1",
}]
def test_feedback_rejects_an_unpersisted_trace():
class MissingTraceWriter(MemoryTraceWriter):
def save_feedback(self, **fields):
raise FeedbackTraceNotFound(fields["trace_id"])
app = create_app(settings=Settings(), trace_writer=MissingTraceWriter())
response = TestClient(app).post("/v1/rag/feedback", json={
"trace_id": "8f33dd3e-9000-4384-ae69-33da2629e29b",
"rating": "helpful",
})
assert response.status_code == 404
assert response.json() == {"detail": "trace_not_found"}
def test_health_and_fail_closed_rag_response_are_traced():
traces = MemoryTraceWriter()
@@ -46,6 +87,68 @@ def test_health_and_fail_closed_rag_response_are_traced():
assert traces.rows[0]["reason"] == "drug_not_resolved"
def test_every_query_response_carries_the_disclaimer_including_an_abstain():
"""The API-visible half of the guardrail.
`docs/architecture.md` specifies the disclaimer at several layers and the
web banner was the only one present, so any consumer other than that one
UI received medical content with nothing attached. An abstain is the case
worth pinning: it is easy to treat as "not really an answer", and it is
still the system responding to a clinical question.
"""
app = create_app(
settings=Settings(),
answer_service=GroundedAnswerService(FixedRouting()),
trace_writer=MemoryTraceWriter(),
)
response = TestClient(app).post("/v1/rag/query", json={
"query": "Liều thuốc?",
"subject_scope": "human",
"intent": "fact_lookup",
})
body = response.json()
assert body["decision"] == "abstain"
assert body["disclaimer"] == DISCLAIMER
assert "không thay thế chỉ định" in body["disclaimer"]
def _metrics_app(**settings_kwargs):
return create_app(
settings=Settings(**settings_kwargs),
answer_service=GroundedAnswerService(FixedRouting()),
trace_writer=MemoryTraceWriter(),
)
def test_metrics_stays_open_when_no_token_is_configured():
"""The default must not break the existing Compose scrape or local runs —
the endpoint is not internet-reachable in that topology."""
response = TestClient(_metrics_app()).get("/metrics")
assert response.status_code in (200, 404) # 404 only when no exporter
def test_metrics_requires_the_token_once_one_is_configured():
client = TestClient(_metrics_app(metrics_token="s3cret"))
assert client.get("/metrics").status_code == 401
assert client.get(
"/metrics", headers={"Authorization": "Bearer wrong"}
).status_code == 401
# A correct token gets past the guard; whether an exporter is attached is
# a separate concern, so 404 is an acceptable non-401 here.
assert client.get(
"/metrics", headers={"Authorization": "Bearer s3cret"}
).status_code in (200, 404)
def test_metrics_token_is_not_accepted_from_a_query_string():
"""Secrets in URLs end up in access logs and referrers, so only the
Authorization header is honoured."""
client = TestClient(_metrics_app(metrics_token="s3cret"))
assert client.get("/metrics?token=s3cret").status_code == 401
def test_query_requires_structured_scope_and_intent():
app = create_app(
settings=Settings(),