Add read-only production runtime audit

This commit is contained in:
2026-08-17 11:17:40 +07:00
parent 057d4ed9dc
commit a1de4715a4
106 changed files with 6869 additions and 1782 deletions
+62 -5
View File
@@ -5,7 +5,7 @@ from fastapi.testclient import TestClient
from adapters.prometheus import PrometheusMetrics
from adapters.postgres import FeedbackTraceNotFound, RetrievalTrace
from config import Settings
from main import create_app
from main import _route_label, create_app
from rag.agent import AgentReply
from rag.answer import DISCLAIMER, Citation, GroundedAnswerService
from rag.metrics import TRACE_WRITE_FAILED, InMemoryMetrics
@@ -150,6 +150,18 @@ def test_history_for_unknown_conversation_is_empty_not_an_error():
assert response.json() == {"items": []}
def test_history_rejects_an_oversized_conversation_id_before_querying_storage():
traces = FakeHistoryTraceWriter({})
app = create_app(settings=Settings(), trace_writer=traces)
response = TestClient(app).get(
"/v1/rag/history", params={"conversation_id": "x" * 129}
)
assert response.status_code == 422
assert traces.calls == []
def test_health_and_fail_closed_rag_response_are_traced():
traces = MemoryTraceWriter()
app = create_app(
@@ -204,6 +216,19 @@ def _metrics_app(**settings_kwargs):
)
def test_all_public_rag_endpoints_have_bounded_request_metric_labels():
paths = (
"/v1/rag/query",
"/v1/rag/suggest",
"/v1/rag/feedback",
"/v1/rag/history",
"/v1/rag/sections",
"/v1/rag/section-text",
)
assert {_route_label(path) for path in paths} == set(paths)
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."""
@@ -252,10 +277,15 @@ class FakeAgent:
def __init__(self, reply: AgentReply) -> None:
self._reply = reply
self.calls: list[tuple[str, str | None]] = []
self.calls: list[tuple[str, str | None, str]] = []
def handle(self, turn: str, conversation_id: str | None = None) -> AgentReply:
self.calls.append((turn, conversation_id))
def handle(
self,
turn: str,
conversation_id: str | None = None,
response_mode: str = "ai",
) -> AgentReply:
self.calls.append((turn, conversation_id, response_mode))
return self._reply
def complete(self, prefix: str, k: int = 8) -> list[str]:
@@ -291,7 +321,34 @@ def test_query_routes_through_the_agent_when_one_is_configured():
assert body["answer"] == "Liều 500 mg [1]."
assert body["resolved_drug_id"] == "metformin"
assert len(body["citations"]) == 1
assert agent.calls == [("Liều metformin?", "c1")]
assert agent.calls == [("Liều metformin?", "c1", "ai")]
def test_query_forwards_monograph_response_mode_to_agent():
agent = FakeAgent(AgentReply(
decision="clarify",
reason="select_drug_sections",
clarification="Chọn mục cần xem.",
drugs=("metformin",),
turn_type="drug_overview",
))
app = create_app(
settings=Settings(),
answer_service=GroundedAnswerService(FixedRouting()),
conversational=agent,
trace_writer=MemoryTraceWriter(),
)
response = TestClient(app).post("/v1/rag/query", json={
"query": "Metformin",
"subject_scope": "human",
"intent": "fact_lookup",
"response_mode": "monograph",
})
assert response.status_code == 200
assert response.json()["reason"] == "select_drug_sections"
assert agent.calls == [("Metformin", None, "monograph")]
def test_query_agent_clarification_is_surfaced_as_the_answer():