c85c0068a2
- Replace all datetime.utcnow() with datetime.now(tz=timezone.utc) across 8 files - Fix 12 failing tests to match current implementation behavior - Fix pytest_plugins in non-top-level conftest (moved to root conftest.py) - Auto-fix 189 lint issues (import sorting, unused imports) - Add CI/CD pipeline infrastructure (ARC, ArgoCD, Kargo manifests) - Add values-beta.yaml and values-paper.yaml for staged deployments - Update GitHub Actions workflow to use self-hosted-gremlin runners - Add integration-test job to CI pipeline Result: 1596 passed, 0 failed, 0 warnings
289 lines
11 KiB
Python
289 lines
11 KiB
Python
"""Integration tests for the Query API — all 17 frontend-facing endpoints.
|
||
|
||
Validates every GET endpoint the frontend calls against the live sandbox
|
||
with deterministic seed data. Uses the ``query_client`` and ``seed_ids``
|
||
fixtures from conftest.py.
|
||
"""
|
||
|
||
import pytest
|
||
|
||
pytestmark = pytest.mark.asyncio
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1–3 Companies
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestQueryAPICompanies:
|
||
"""Endpoints: /api/companies, /api/companies/{id}, /api/companies/{id}/sources."""
|
||
|
||
async def test_list_companies(self, query_client, seed_ids):
|
||
"""GET /api/companies — expect at least 5 seeded companies."""
|
||
resp = await query_client.get("/api/companies")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert len(data) >= 5
|
||
tickers = {c["ticker"] for c in data}
|
||
assert {"AAPL", "MSFT", "JPM", "JNJ", "XOM"} <= tickers
|
||
# Every company row must have core fields
|
||
for c in data:
|
||
assert "id" in c
|
||
assert "legal_name" in c
|
||
assert "sector" in c
|
||
|
||
async def test_get_company(self, query_client, seed_ids):
|
||
"""GET /api/companies/{id} — detail for AAPL."""
|
||
company_id = seed_ids["companies"]["AAPL"]
|
||
resp = await query_client.get(f"/api/companies/{company_id}")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["ticker"] == "AAPL"
|
||
assert data["legal_name"] == "Apple Inc"
|
||
assert "aliases" in data
|
||
assert "active_source_count" in data
|
||
|
||
async def test_list_company_sources(self, query_client, seed_ids):
|
||
"""GET /api/companies/{id}/sources — AAPL has at least 1 source."""
|
||
company_id = seed_ids["companies"]["AAPL"]
|
||
resp = await query_client.get(f"/api/companies/{company_id}/sources")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert isinstance(data, list)
|
||
assert len(data) >= 1
|
||
assert "source_type" in data[0]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4–5 Documents
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestQueryAPIDocuments:
|
||
"""Endpoints: /api/documents, /api/documents/{id}."""
|
||
|
||
async def test_list_documents(self, query_client, seed_ids):
|
||
"""GET /api/documents — expect at least 10 seeded documents."""
|
||
resp = await query_client.get("/api/documents")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert len(data) >= 10
|
||
for doc in data:
|
||
assert "id" in doc
|
||
assert "document_type" in doc
|
||
assert "title" in doc
|
||
|
||
async def test_get_document(self, query_client, seed_ids):
|
||
"""GET /api/documents/{id} — detail with intelligence."""
|
||
doc_id = seed_ids["documents"]["DOC_01"]
|
||
resp = await query_client.get(f"/api/documents/{doc_id}")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["id"] == doc_id
|
||
assert "title" in data
|
||
assert "document_type" in data
|
||
# Intelligence extraction should be present for seeded docs
|
||
assert "intelligence" in data
|
||
assert "company_mentions" in data
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 6–7 Trends
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestQueryAPITrends:
|
||
"""Endpoints: /api/trends, /api/trends/{id}."""
|
||
|
||
async def test_list_trends(self, query_client, seed_ids):
|
||
"""GET /api/trends — expect at least 5 seeded trend windows."""
|
||
resp = await query_client.get("/api/trends")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert len(data) >= 5
|
||
for t in data:
|
||
assert "id" in t
|
||
assert "trend_direction" in t
|
||
assert "confidence" in t
|
||
|
||
async def test_get_trend(self, query_client, seed_ids):
|
||
"""GET /api/trends/{id} — detail for first seeded trend."""
|
||
trend_id = seed_ids["trends"]["TREND_01"]
|
||
resp = await query_client.get(f"/api/trends/{trend_id}")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["id"] == trend_id
|
||
assert data["trend_direction"] in ("bullish", "bearish", "mixed")
|
||
assert 0 <= data["confidence"] <= 1
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 8–9 Recommendations
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestQueryAPIRecommendations:
|
||
"""Endpoints: /api/recommendations, /api/recommendations/{id}."""
|
||
|
||
async def test_list_recommendations(self, query_client, seed_ids):
|
||
"""GET /api/recommendations — expect at least 5 seeded recs."""
|
||
resp = await query_client.get("/api/recommendations", params={"latest": "false"})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert len(data) >= 5
|
||
for r in data:
|
||
assert "id" in r
|
||
assert "ticker" in r
|
||
assert "action" in r
|
||
assert "confidence" in r
|
||
|
||
async def test_get_recommendation(self, query_client, seed_ids):
|
||
"""GET /api/recommendations/{id} — detail with evidence."""
|
||
rec_id = seed_ids["recommendations"]["REC_01"]
|
||
resp = await query_client.get(f"/api/recommendations/{rec_id}")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["id"] == rec_id
|
||
assert "ticker" in data
|
||
assert "thesis" in data
|
||
assert "evidence" in data
|
||
assert "risk_evaluation" in data
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 10–11 Orders
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestQueryAPIOrders:
|
||
"""Endpoints: /api/orders, /api/orders/{id}."""
|
||
|
||
async def test_list_orders(self, query_client, seed_ids):
|
||
"""GET /api/orders — expect at least 3 seeded orders."""
|
||
resp = await query_client.get("/api/orders")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert len(data) >= 3
|
||
statuses = {o["status"] for o in data}
|
||
# Seed has filled, pending, cancelled
|
||
assert len(statuses) >= 2
|
||
for o in data:
|
||
assert "id" in o
|
||
assert "ticker" in o
|
||
assert "side" in o
|
||
|
||
async def test_get_order(self, query_client, seed_ids):
|
||
"""GET /api/orders/{id} — detail with events and audit trail."""
|
||
order_id = seed_ids["orders"]["ORDER_01"]
|
||
resp = await query_client.get(f"/api/orders/{order_id}")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["id"] == order_id
|
||
assert "ticker" in data
|
||
assert "events" in data
|
||
assert isinstance(data["events"], list)
|
||
assert "audit_trail" in data
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 12 Positions
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestQueryAPIPositions:
|
||
"""Endpoint: /api/positions."""
|
||
|
||
async def test_list_positions(self, query_client, seed_ids):
|
||
"""GET /api/positions — expect at least 2 seeded positions."""
|
||
resp = await query_client.get("/api/positions")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert len(data) >= 2
|
||
for p in data:
|
||
assert "id" in p
|
||
assert "ticker" in p
|
||
assert "quantity" in p
|
||
assert "unrealized_pnl" in p
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 13 Pipeline Health
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestQueryAPIOps:
|
||
"""Endpoints: /api/ops/pipeline/health, /api/ops/ingestion/summary, /api/ops/sources/coverage-gaps."""
|
||
|
||
async def test_pipeline_health(self, query_client, seed_ids):
|
||
"""GET /api/ops/pipeline/health — returns structured health data."""
|
||
resp = await query_client.get("/api/ops/pipeline/health")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert "hours" in data
|
||
assert "document_stages" in data
|
||
assert "parsing" in data
|
||
assert "extraction" in data
|
||
assert "aggregation" in data
|
||
assert "queue_depths" in data
|
||
|
||
# -----------------------------------------------------------------------
|
||
# 14 Ingestion Summary
|
||
# -----------------------------------------------------------------------
|
||
|
||
async def test_ingestion_summary(self, query_client, seed_ids):
|
||
"""GET /api/ops/ingestion/summary — returns ingestion stats."""
|
||
resp = await query_client.get("/api/ops/ingestion/summary")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert "hours" in data
|
||
assert "total_runs" in data
|
||
assert "by_source_type" in data
|
||
assert isinstance(data["by_source_type"], list)
|
||
|
||
# -----------------------------------------------------------------------
|
||
# 15 Coverage Gaps
|
||
# -----------------------------------------------------------------------
|
||
|
||
async def test_coverage_gaps(self, query_client, seed_ids):
|
||
"""GET /api/ops/sources/coverage-gaps — returns gap analysis."""
|
||
resp = await query_client.get("/api/ops/sources/coverage-gaps")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert "missing_source_types" in data
|
||
assert "stale_sources" in data
|
||
assert isinstance(data["missing_source_types"], list)
|
||
assert isinstance(data["stale_sources"], list)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 16–17 Agents & Variants
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestQueryAPIAgents:
|
||
"""Endpoints: /api/agents, /api/agents/{id}/variants."""
|
||
|
||
async def test_list_agents(self, query_client, seed_ids):
|
||
"""GET /api/agents — expect at least 3 seeded agents."""
|
||
resp = await query_client.get("/api/agents")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert len(data) >= 3
|
||
for a in data:
|
||
assert "id" in a
|
||
assert "name" in a
|
||
assert "slug" in a
|
||
|
||
async def test_list_agent_variants(self, query_client, seed_ids):
|
||
"""GET /api/agents/{id}/variants — variants for the extractor agent."""
|
||
agent_id = seed_ids["agents"]["extractor"]
|
||
resp = await query_client.get(f"/api/agents/{agent_id}/variants")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert isinstance(data, list)
|
||
assert len(data) >= 1
|
||
for v in data:
|
||
assert "id" in v
|
||
assert "variant_name" in v
|
||
assert v["agent_id"] == agent_id
|