"""Retrieval-only chat integration; synthetic evidence, never a generation call.""" import copy import pytest from fastapi.testclient import TestClient from sqlalchemy import func, select, text from sqlalchemy.orm import Session from test_catalog import document, source from test_retrieval import add_item from test_runtime import server from app.models.rag import RagContext from app.models.retrieval import RetrievalRun from app.services import retrieval def test_overlong_chat_question_is_rejected_at_the_http_boundary(client): with TestClient( client.app, base_url="https://testserver", raise_server_exceptions=False ) as public: response = public.post("/api/chat", json={"question": "q" * 501}) assert response.status_code == 422, response.text @pytest.mark.parametrize( "payload", [ {}, {"question": ""}, {"question": " \t\n "}, {"question": None}, {"question": 123}, {"question": "q" * 2000}, {"question": "q" * 2001}, {"question": "alpha", "model": "not-accepted"}, {"question": "alpha", "messages": []}, ], ) def test_invalid_chat_input_creates_no_retrieval_or_context(client, migrated_database, payload): response = client.post("/api/chat", json=payload) assert response.status_code == 422, response.text assert all("input" not in error for error in response.json()["detail"]) with Session(migrated_database) as db: assert db.scalar(select(func.count()).select_from(RetrievalRun)) == 0 assert db.scalar(select(func.count()).select_from(RagContext)) == 0 def test_chat_query_boundary_normalization_and_public_history_permissions(client): response = client.post("/api/chat", json={"question": "q" * 500}) assert response.status_code == 200, response.text assert response.json()["status"] == "no_evidence" client.cookies.clear() public = client.post("/api/chat", json={"question": " alpha "}) assert public.status_code == 200 assert public.json()["status"] == "no_evidence" and public.json()["sources"] == [] assert client.get(f"/api/retrieval/runs/{public.json()['retrieval_run_id']}").status_code == 401 def test_chat_sources_match_audited_snapshot_and_history_stays_immutable(client, migrated_database): approved = add_item(client, title="verified alpha", content="alpha synthetic evidence") add_item(client, status="pending", title="pending alpha", content="alpha unreviewed") add_item(client, status="rejected", title="rejected alpha", content="alpha rejected") deleted = add_item(client, title="deleted alpha", content="alpha deleted") assert ( client.delete( f"/api/knowledge/{deleted['id']}?expected_version={deleted['version']}" ).status_code == 204 ) result = client.post("/api/chat", json={"question": " alpha "}).json() assert result["status"] == "retrieval_only" and "δΈη”Ÿζˆ" in result["answer"] history = client.get(f"/api/retrieval/runs/{result['retrieval_run_id']}").json() assert history["query"] == "alpha" and history["mode"] == "keyword" assert [h["knowledge_id"] for h in history["results"]] == [approved["id"]] assert result["sources"] == [ {"title": h["title"], "source_url": h["source_url"]} for h in history["results"] ] saved = copy.deepcopy(history) assert ( client.patch( f"/api/knowledge/{approved['id']}", json={"expected_version": approved["version"], "content": "revised alpha"}, ).status_code == 200 ) assert client.post("/api/chat", json={"question": "alpha"}).json()["status"] == "no_evidence" assert client.get(f"/api/retrieval/runs/{history['run_id']}").json() == saved assert ( client.post( "/api/rag/contexts", json={"retrieval_run_id": history["run_id"], "completion": {"model": "contract-model"}}, ).status_code == 422 ) with Session(migrated_database) as db: assert db.scalar(select(func.count()).select_from(RagContext)) == 0 @pytest.mark.parametrize("change", ["source", "document"]) def test_chat_cannot_reuse_evidence_after_catalog_revision(client, change): src = source(client) doc = document(client, src["id"]) add_item(client, document_id=doc["id"], content="alpha synthetic catalog evidence") assert client.post("/api/chat", json={"question": "alpha"}).json()["status"] == "retrieval_only" path, version, fields = ( (f"sources/{src['id']}", src["version"], {"source_url": "https://example.com/new"}) if change == "source" else (f"documents/{doc['id']}", doc["version"], {"content": "revised synthetic document"}) ) assert ( client.patch("/api/" + path, json={"expected_version": version, **fields}).status_code == 200 ) result = client.post("/api/chat", json={"question": "alpha"}).json() assert result["status"] == "no_evidence" and result["sources"] == [] def test_chat_capacity_failure_is_not_a_successful_empty_answer( client, migrated_database, monkeypatch ): add_item(client, content="alpha synthetic capacity evidence") monkeypatch.setattr(retrieval, "MAX_ITEMS", 0) response = client.post("/api/chat", json={"question": "alpha"}) assert response.status_code == 503 and "sources" not in response.json() with Session(migrated_database) as db: assert db.scalar(select(func.count()).select_from(RetrievalRun)) == 0 def test_chat_failed_history_insert_does_not_leave_partial_rows(client, migrated_database): with migrated_database.begin() as db: db.execute( text("ALTER TABLE retrieval_runs ADD CONSTRAINT reject_chat_run CHECK(false) NOT VALID") ) assert client.post("/api/chat", json={"question": "alpha"}).status_code == 409 with Session(migrated_database) as db: assert db.scalar(select(func.count()).select_from(RetrievalRun)) == 0 def test_chat_retrieval_history_survives_real_http_restart(migrated_database): with server(migrated_database) as client: assert client.post("/api/chat", json={"question": "q" * 501}).status_code == 422 item = add_item(client, content="alpha real HTTP synthetic evidence") response = client.post("/api/chat", json={"question": " alpha "}) assert response.status_code == 200 result = response.json() history = client.get(f"/api/retrieval/runs/{result['retrieval_run_id']}").json() assert history["results"][0]["knowledge_id"] == item["id"] assert history["query"] == "alpha" with server(migrated_database) as client: assert client.get(f"/api/retrieval/runs/{result['retrieval_run_id']}").json() == history exported = client.post( "/api/retrieval/exports", json={"run_ids": [result["retrieval_run_id"]], "include_contexts": True}, ) assert exported.status_code == 200 client.cookies.clear() assert ( client.post("/api/chat", json={"question": "alpha"}).json()["status"] == "retrieval_only" )