""" Verify the /query and /query/stream endpoint response types. Ensures: - /query → application/json (no streaming, backward-compatible) - /query/stream → application/x-ndjson """ import sys from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient _ENV_VARS_TO_ISOLATE = ( "LLM_BINDING", "EMBEDDING_BINDING", "LLM_BINDING_HOST", "LLM_BINDING_API_KEY", "LLM_MODEL", "EMBEDDING_BINDING_HOST", "EMBEDDING_BINDING_API_KEY", "EMBEDDING_MODEL", "LIGHTRAG_API_PREFIX", "LIGHTRAG_KV_STORAGE", "LIGHTRAG_VECTOR_STORAGE", "LIGHTRAG_GRAPH_STORAGE", "LIGHTRAG_DOC_STATUS_STORAGE", ) @pytest.fixture(autouse=True) def _isolate_env(monkeypatch): for var in _ENV_VARS_TO_ISOLATE: monkeypatch.delenv(var, raising=False) monkeypatch.setenv("LLM_BINDING", "ollama") monkeypatch.setenv("EMBEDDING_BINDING", "ollama") def _build_client(): original_argv = sys.argv.copy() try: sys.argv = ["lightrag-server"] from lightrag.api.config import parse_args from lightrag.api.lightrag_server import create_app args = parse_args() with patch("lightrag.api.lightrag_server.LightRAG") as mock_rag: mock_rag.return_value = MagicMock() return TestClient(create_app(args)) finally: sys.argv = original_argv class TestQueryRouteJsonOnly: """The /query endpoint must stay JSON-only to preserve backward compatibility.""" def test_openapi_spec_declares_json_response(self): client = _build_client() response = client.get("/openapi.json") assert response.status_code == 200 spec = response.json() paths = spec.get("paths", {}) query_path = paths.get("/query", {}) assert query_path, "/query must be in OpenAPI paths" post_op = query_path.get("post", {}) responses = post_op.get("responses", {}) ok_resp = responses.get("200", {}) content = ok_resp.get("content", {}) # The /query endpoint must declare application/json — NOT ndjson assert "application/json" in content, ( "/query must declare application/json in OpenAPI spec" ) assert "application/x-ndjson" not in content, ( "/query must NOT declare application/x-ndjson — streaming belongs to /query/stream" ) def test_query_route_exists_and_accepts_post(self): client = _build_client() # A minimal POST to /query should reach the route (it'll 422 or 500 # since we don't have a real LLM, but it should NOT 404/405) response = client.post("/query", json={"query": "test", "mode": "mix"}) assert response.status_code not in ( 404, 405, ), "/query route must exist and accept POST" class TestQueryStreamRoute: """The /query/stream endpoint must serve application/x-ndjson.""" def test_openapi_spec_declares_ndjson_response(self): client = _build_client() response = client.get("/openapi.json") assert response.status_code == 200 spec = response.json() paths = spec.get("paths", {}) stream_path = paths.get("/query/stream", {}) assert stream_path, "/query/stream must be in OpenAPI paths" post_op = stream_path.get("post", {}) responses = post_op.get("responses", {}) ok_resp = responses.get("200", {}) content = ok_resp.get("content", {}) # The /query/stream endpoint must declare application/x-ndjson assert "application/x-ndjson" in content, ( "/query/stream must declare application/x-ndjson in OpenAPI spec" ) def test_stream_route_exists_and_accepts_post(self): client = _build_client() response = client.post("/query/stream", json={"query": "test", "mode": "mix"}) assert response.status_code not in ( 404, 405, ), "/query/stream route must exist and accept POST" class TestQueryStreamResponseContentType: """When the mock LLM returns a non-streaming result, /query/stream must still set the correct Content-Type header.""" def test_stream_response_has_ndjson_content_type(self): """Even without a real LLM, the streaming response must carry the correct media type header.""" original_argv = sys.argv.copy() try: sys.argv = ["lightrag-server"] from lightrag.api.config import parse_args from lightrag.api.lightrag_server import create_app args = parse_args() mock_rag = MagicMock() mock_result = { "llm_response": { "is_streaming": False, "content": "test response", }, "data": {"references": []}, } # Return a coroutine mock_rag.aquery_llm = MagicMock() async def _fake_aquery(*a, **kw): return mock_result mock_rag.aquery_llm.side_effect = _fake_aquery with patch("lightrag.api.lightrag_server.LightRAG", return_value=mock_rag): app = create_app(args) client = TestClient(app) response = client.post( "/query/stream", json={ "query": "test", "mode": "mix", "include_references": True, }, ) content_type = response.headers.get("content-type", "") assert "application/x-ndjson" in content_type, ( f"/query/stream must return application/x-ndjson, got: {content_type}" ) finally: sys.argv = original_argv