Testing¶
Pytest suite for Eagle-RAG. Tests live in tests/; configuration in pyproject.toml [tool.pytest.ini_options].
task be:test # uv run pytest
uv run pytest -k mcp # subset
uv run pytest -v tests/test_api_admin_health.py
Pytest configuration¶
pytest-asyncioruns async tests without manual markers in most cases.- Dev deps:
uv sync --group dev.
Testcontainers¶
Eagle-RAG does not use testcontainers in this repository. There is no testcontainers dependency in pyproject.toml and no container-based integration harness in CI.
Integration with real Postgres, Milvus, Redis, or MinIO is done manually via task up or ad-hoc scripts — not in the default pytest run.
Testing strategy overview¶
quadrantChart
title Test pyramid (Eagle-RAG)
x-axis Low fidelity --> High fidelity
y-axis Fast --> Slow
quadrant-1 Manual Docker E2E
quadrant-2 Integration (sparse)
quadrant-3 Unit (majority)
quadrant-4 Telemetry / MCP contract
Unit tests: [0.25, 0.75]
API contract tests: [0.45, 0.65]
Mocked retrieval: [0.35, 0.7]
Manual stack E2E: [0.9, 0.2]
| Layer | What runs | External deps |
|---|---|---|
| Unit | Pure logic, adapters with mocks | None |
| API contract | FastAPI TestClient / async client | Mocked stores |
| Component | Router engine, retrievers with MagicMock |
No Milvus |
| Integration (manual) | Ingest + query against Docker stack | Real services |
| E2E (manual) | Frontend + API + workers | Full compose |
Default pytest target: unit + API contract only.
Shared fixtures — conftest.py¶
tests/conftest.py defines autouse fixtures:
_reset_telemetry_state¶
Runs before and after every test.
Resets:
eagle_rag.telemetry._configuredlogging_setup._configured,_enabled,_ai_logger_factorycontext._enabled, contextvars dicttracing._tracer,_tracing_enabled- structlog
contextvars - stdlib logger
eagle_ai_telemetryhandlers
Why: Telemetry configures global state once; without reset, order-dependent failures occur in test_telemetry_* and tracing tests. OpenTelemetry TracerProvider is process-global — tests only reset the module _tracer reference so trace_span no-ops until reconfigured.
_kb_registered¶
Patches:
patch("eagle_rag.kb.registry.kb_exists_sync", return_value=True)
patch("eagle_rag.kb.registry.get_pdf_ratio_sync", return_value=None)
Why: Ingest and query tests should not require a live Postgres knowledge_bases row.
Test file map¶
| File | Focus | Style |
|---|---|---|
test_api_admin_health.py |
/health, /admin/* probes |
Async client, mocked backends |
test_api_query_sessions_documents_tasks.py |
Query, sessions, documents | API contract |
test_api_ingest_queue_metrics.py |
Ingest API, metrics | API + mocks |
test_api_kb_attachments_notifications_users.py |
KB CRUD, attachments | API contract |
test_router_generation.py |
EagleRouterQueryEngine, VLM mock |
Unit + mock retrievers |
test_retrievers.py |
Retriever behaviour | Mock Milvus |
test_ingest_smoke.py |
Router dispatch smoke | Mocks |
test_ingest_assets.py |
Asset paths | Unit |
test_ingest_url_validation.py |
URL prefetch rules | Unit |
test_knowhere_sections.py |
Section tree parsing | Unit / fixture files |
test_knowhere_visual_chunks.py |
Visual chunk dispatch | Mock |
test_attachments_parser.py |
Attachment lazy parse | Unit |
test_milvus_structure_fetch.py |
Document structure API | Mock Milvus |
test_mcp_server.py (via test_mcp_*) |
MCP tools, auth, cache, HTTP | Mixed |
test_mcp_metrics.py |
Prometheus with_metrics |
Unit |
test_mcp_http_transport.py |
Streamable HTTP | Async |
test_mcp_resilience.py |
Circuit breaker, retry | Unit |
test_mcp_config.py |
MCP settings | Unit |
test_mcp_auth.py |
Token auth | Unit |
test_mcp_cache.py |
Tool result cache | Unit |
test_telemetry_logging.py |
loguru / structlog setup | Unit, tmp paths |
test_telemetry_tracing.py |
trace_span, middleware |
Unit |
test_telemetry_hotspots.py |
Span coverage on hot paths | Unit / smoke |
Unit vs integration classification¶
Unit tests¶
- No network; no Docker.
unittest.mock.patch,MagicMock,pytest.fixturefor small data.- Examples: URL validator, routing heuristics, metrics status inference (
_infer_status), dead-letter payload shape, schema validation.
API contract tests¶
- Use FastAPI
TestClientorhttpx.AsyncClientagainstapp. - External services patched at import boundary (e.g.
MilvusClient,asyncpg, Redis). - Verify status codes,
response_modelshape, error handling.
Integration tests (informal)¶
Not a separate pytest marker today. These require running services:
| Scenario | How to run |
|---|---|
| Full ingest → Milvus | task up, upload via API, inspect /admin/milvus |
| Knowhere parse | task knowhere:up, real document |
| PixelRAG visual | worker-pixelrag with GPU/CPU torch |
| Query streaming | curl -N on /query/stream |
Mark future automated integration tests with @pytest.mark.integration if introduced — skip by default in CI.
Mocking patterns¶
Retrievers¶
test_router_generation.py injects mocks:
mock_text = MagicMock()
mock_text.retrieve.return_value = [node1, node2]
engine = EagleRouterQueryEngine(text_retriever=mock_text, visual_retriever=mock_visual)
VLM / DashScope¶
Patch dashscope.MultiModalConversation.call or inject MagicMock with complete / stream iterator.
Milvus¶
Patch eagle_rag.index.milvus_text_store / milvus_visual_store module functions or MilvusClient at call site.
Knowhere¶
Patch knowhere SDK or HTTP client in knowhere_adapter tests — no live :5005 in pytest.
Celery¶
Tasks tested synchronously by calling the underlying function or using celery_app.conf.task_always_eager = True only when explicitly set in a test (not global today).
Async tests¶
asyncio_mode = "auto" — async def tests run in an event loop without @pytest.mark.asyncio in most pytest-asyncio versions.
Admin health tests use async HTTP against lifespan-managed app.
Writing new tests¶
- Place file as
tests/test_<domain>_<feature>.py. - Prefer one behaviour per test function name:
test_query_scope_filter_persists_to_session. - Use autouse telemetry reset — do not call
configure_telemetrywithout tmp log paths unless you cleanup. - Patch at the lowest stable boundary (the module under test imports from).
- No real API keys — use env patches or empty keys with mocked upstream.
Example skeleton:
from unittest.mock import patch
import pytest
@pytest.mark.asyncio
async def test_my_endpoint(client):
with patch("eagle_rag.some.module.external_call", return_value={"ok": True}):
resp = await client.get("/my-path")
assert resp.status_code == 200
Coverage gaps (intentional)¶
| Area | Why not fully automated |
|---|---|
| Milvus ANN quality | Requires vectors + tuning |
| Knowhere job polling | Long-running HTTP integration |
| Chrome PixelRAG render | Heavy deps in CI |
| OpenTelemetry OTLP export | Manual collector verification |
Telemetry test notes¶
test_telemetry_logging.pyuses temporary log directories.- Reset fixture clears loguru handlers — if adding tests that configure telemetry, use
tmp_pathforop_log_file/ai_log_file. - TracerProvider cannot be unset globally; tests assert on span behaviour when
configure_tracingruns in-test.
MCP metrics tests¶
test_mcp_metrics.py validates:
with_metricsincrementsmcp_tool_calls_total{status}- Circuit state gauge updates
- Cache hit overrides status to
cache_hit
Uses prometheus_client registry in-process (no scrape server required).
Running subsets¶
uv run pytest tests/test_telemetry_tracing.py -v
uv run pytest -k "scope_filter"
uv run pytest --tb=short -q
CI recommendation for maintainers¶
A minimal CI job should run:
uv sync --group dev
uv run ruff check
uv run ruff format --check
uv run mypy eagle_rag
uv run pytest
Frontend:
Manual verification checklist (release)¶
After large ingest or retrieval changes:
task be:testgreen.task up—task healthok.- Upload sample PDF (text + scanned).
POST /queryand/query/stream.GET /admin/celery— queues drain.- Check
logs/ai_telemetry.jsonlforquery_completed.