Files
stonks-oracle/tests/integration/test_query_api_extended.py
Celes Renata 898f89926d feat: beta API integration test suite — 85 new tests across 6 modules
Extends integration test coverage from 108 to 193 tests for the beta gate.

New test modules:
- test_query_api_extended.py (33 tests): documents, evidence, macro/competitive, ops/admin, agents, analytics
- test_registry_write_paths.py (16 tests): write paths, validation, duplicates, competitor/exposure CRUD
- test_risk_approval_lifecycle.py (8 tests): evaluation edge cases, full approval lifecycle
- test_trading_extended.py (12 tests): config round-trips, decision filtering, override validation
- test_cross_service_roundtrip.py (4 tests): cross-service data consistency
- test_error_handling.py (12 tests): 404s, 422s, empty states, health checks

Seed script extended with watchlists, approvals, lockouts, notifications,
ingestion runs, saved queries, and daily risk snapshots.
2026-04-20 02:34:19 +00:00

408 lines
16 KiB
Python

"""Extended integration tests for the Query API — documents, evidence, macro,
competitive, operational, admin, agents, and analytics endpoints.
Validates endpoints not covered by test_query_api.py 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
# ---------------------------------------------------------------------------
# Task 2: Documents and Evidence
# ---------------------------------------------------------------------------
class TestQueryAPIDocumentFiltering:
"""Endpoints: /api/documents with ticker and document_type filters."""
async def test_filter_documents_by_ticker(self, query_client, seed_ids):
"""GET /api/documents?ticker=AAPL — only AAPL documents."""
resp = await query_client.get("/api/documents", params={"ticker": "AAPL"})
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 1
# All returned docs should have an id field
for doc in data:
assert "id" in doc
async def test_filter_documents_by_doc_type(self, query_client, seed_ids):
"""GET /api/documents?document_type=filing — only filing documents."""
resp = await query_client.get(
"/api/documents", params={"document_type": "filing"}
)
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 1
for doc in data:
assert doc["document_type"] == "filing"
class TestQueryAPIDocumentDetail:
"""Endpoint: /api/documents/{id} — 404 for non-existent UUID."""
async def test_document_not_found(self, query_client):
"""GET /api/documents/{id} — 404 for non-existent UUID."""
fake_id = "00000000-0000-4000-ffff-000000000099"
resp = await query_client.get(f"/api/documents/{fake_id}")
assert resp.status_code == 404
class TestQueryAPIRecommendationEvidence:
"""Endpoint: /api/recommendations/{id}/evidence — evidence drill-down."""
async def test_recommendation_evidence_drilldown(self, query_client, seed_ids):
"""GET /api/recommendations/{id}/evidence — evidence drill-down."""
rec_id = seed_ids["recommendations"]["REC_01"]
resp = await query_client.get(f"/api/recommendations/{rec_id}/evidence")
assert resp.status_code == 200
data = resp.json()
assert "recommendation" in data
assert "evidence" in data
class TestQueryAPITrendEvidence:
"""Endpoint: /api/trends/{id}/evidence — trend evidence drill-down."""
async def test_trend_evidence_drilldown(self, query_client, seed_ids):
"""GET /api/trends/{id}/evidence — trend evidence drill-down."""
trend_id = seed_ids["trends"]["TREND_01"]
resp = await query_client.get(f"/api/trends/{trend_id}/evidence")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, dict)
assert "trend" in data or "id" in data
class TestQueryAPITrendProjection:
"""Endpoint: /api/trends/{id}/projection — trend projection."""
async def test_trend_projection(self, query_client, seed_ids):
"""GET /api/trends/{id}/projection — trend with projection."""
trend_id = seed_ids["trends"]["TREND_01"]
resp = await query_client.get(f"/api/trends/{trend_id}/projection")
assert resp.status_code == 200
data = resp.json()
assert "projected_direction" in data
assert "projected_strength" in data
assert "projected_confidence" in data
assert "macro_contribution_pct" in data
async def test_trend_projection_not_found(self, query_client):
"""GET /api/trends/{id}/projection — no projection returns 404 for non-existent trend."""
fake_id = "00000000-0000-4000-ffff-000000000099"
resp = await query_client.get(f"/api/trends/{fake_id}/projection")
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# Task 3: Macro and Competitive Layer
# ---------------------------------------------------------------------------
class TestQueryAPIMacro:
"""Endpoints: /api/admin/macro/status, /api/macro/events, /api/macro/impacts."""
async def test_macro_status(self, query_client):
"""GET /api/admin/macro/status — macro layer toggle status."""
resp = await query_client.get("/api/admin/macro/status")
assert resp.status_code == 200
data = resp.json()
assert "macro_enabled" in data
async def test_list_macro_events(self, query_client, seed_ids):
"""GET /api/macro/events — at least 2 seeded events."""
resp = await query_client.get("/api/macro/events")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert len(data) >= 2
for evt in data:
assert "id" in evt
assert "severity" in evt
assert "summary" in evt
async def test_get_macro_event_detail(self, query_client, seed_ids):
"""GET /api/macro/events/{id} — event detail with impacts."""
event_id = seed_ids["global_events"]["EVT_01"]
resp = await query_client.get(f"/api/macro/events/{event_id}")
assert resp.status_code == 200
data = resp.json()
assert data["id"] == event_id
assert "impacts" in data
async def test_macro_impacts_by_ticker(self, query_client):
"""GET /api/macro/impacts/AAPL — at least 1 impact record."""
resp = await query_client.get("/api/macro/impacts/AAPL")
assert resp.status_code == 200
data = resp.json()
assert "impacts" in data
assert isinstance(data["impacts"], list)
assert len(data["impacts"]) >= 1
for impact in data["impacts"]:
assert "macro_impact_score" in impact
assert "impact_direction" in impact
class TestQueryAPICompetitive:
"""Endpoints: /api/admin/competitive/status, /api/patterns/{ticker}/competitive-signals, /api/patterns/{ticker}."""
async def test_competitive_status(self, query_client):
"""GET /api/admin/competitive/status — competitive layer toggle."""
resp = await query_client.get("/api/admin/competitive/status")
assert resp.status_code == 200
data = resp.json()
assert "competitive_enabled" in data
async def test_competitive_signals(self, query_client):
"""GET /api/patterns/AAPL/competitive-signals — competitive signals."""
resp = await query_client.get("/api/patterns/AAPL/competitive-signals")
assert resp.status_code == 200
data = resp.json()
assert "competitive_signals" in data
assert isinstance(data["competitive_signals"], list)
async def test_patterns_for_ticker(self, query_client):
"""GET /api/patterns/AAPL — patterns data."""
resp = await query_client.get("/api/patterns/AAPL")
assert resp.status_code == 200
data = resp.json()
assert "ticker" in data
assert "patterns" in data
# ---------------------------------------------------------------------------
# Task 4: Operational and Admin Endpoints
# ---------------------------------------------------------------------------
class TestQueryAPIOpsExtended:
"""Endpoints: /api/ops/pipeline/health, /api/ops/ingestion/summary, /api/ops/sources/coverage-gaps."""
async def test_pipeline_health_fields(self, query_client):
"""GET /api/ops/pipeline/health — verify all required fields."""
resp = await query_client.get("/api/ops/pipeline/health")
assert resp.status_code == 200
data = resp.json()
assert "document_stages" in data
assert "parsing" in data
assert "extraction" in data
assert "aggregation" in data
assert "queue_depths" in data
async def test_ingestion_summary_fields(self, query_client):
"""GET /api/ops/ingestion/summary — verify total_runs and by_source_type."""
resp = await query_client.get("/api/ops/ingestion/summary")
assert resp.status_code == 200
data = resp.json()
assert "total_runs" in data
assert "by_source_type" in data
async def test_coverage_gaps_fields(self, query_client):
"""GET /api/ops/sources/coverage-gaps — verify fields."""
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
class TestQueryAPISourceToggle:
"""Endpoint: PUT /api/admin/sources/{id}/toggle."""
async def test_toggle_source(self, query_client):
"""PUT /api/admin/sources/{id}/toggle?active=false — toggle source off."""
source_id = "00000000-0000-4000-b000-000000000001" # SOURCE_AAPL
resp = await query_client.put(
f"/api/admin/sources/{source_id}/toggle", params={"active": "false"}
)
assert resp.status_code == 200
# Toggle back on
resp2 = await query_client.put(
f"/api/admin/sources/{source_id}/toggle", params={"active": "true"}
)
assert resp2.status_code == 200
class TestQueryAPITradingAdmin:
"""Endpoints: /api/admin/trading/config, approvals, lockouts."""
async def test_trading_config(self, query_client):
"""GET /api/admin/trading/config — trading config."""
resp = await query_client.get("/api/admin/trading/config")
assert resp.status_code == 200
data = resp.json()
assert "trading_mode" in data or "config" in data or "name" in data
async def test_pending_approvals(self, query_client, seed_ids):
"""GET /api/admin/trading/approvals — pending approvals list."""
resp = await query_client.get("/api/admin/trading/approvals")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
# Should have at least the seeded pending approval
assert len(data) >= 1
async def test_active_lockouts(self, query_client, seed_ids):
"""GET /api/admin/trading/lockouts — active lockouts list."""
resp = await query_client.get("/api/admin/trading/lockouts")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert len(data) >= 1
async def test_lockout_create_and_delete(self, query_client):
"""POST then DELETE /api/admin/trading/lockouts — lifecycle."""
# Create
body = {
"ticker": "TSLA",
"lockout_type": "manual",
"reason": "Integration test lockout",
"duration_minutes": 1440,
}
resp = await query_client.post("/api/admin/trading/lockouts", json=body)
assert resp.status_code == 200
data = resp.json()
lockout_id = data["id"]
# Delete
resp2 = await query_client.delete(
f"/api/admin/trading/lockouts/{lockout_id}"
)
assert resp2.status_code == 200
# ---------------------------------------------------------------------------
# Task 5: Agents and Analytics
# ---------------------------------------------------------------------------
class TestQueryAPIAgentsExtended:
"""Endpoints: /api/agents CRUD, variants, performance."""
async def test_agent_detail(self, query_client, seed_ids):
"""GET /api/agents/{id} — agent detail with system_prompt, temperature, max_tokens."""
agent_id = seed_ids["agents"]["extractor"]
resp = await query_client.get(f"/api/agents/{agent_id}")
assert resp.status_code == 200
data = resp.json()
assert "system_prompt" in data
assert "temperature" in data
assert "max_tokens" in data
async def test_create_agent(self, query_client):
"""POST /api/agents — create agent with slug generation."""
body = {
"name": "Integration Test Agent",
"purpose": "Testing agent creation",
"model_provider": "ollama",
"model_name": "qwen3.5:9b",
"system_prompt": "You are a test agent.",
"prompt_version": "test-v1",
"schema_version": "1.0.0",
}
resp = await query_client.post("/api/agents", json=body)
assert resp.status_code == 201
data = resp.json()
assert "id" in data
assert "slug" in data
assert data["name"] == "Integration Test Agent"
async def test_update_agent(self, query_client, seed_ids):
"""PUT /api/agents/{id} — update agent."""
agent_id = seed_ids["agents"]["thesis"]
body = {"purpose": "Updated purpose for integration test"}
resp = await query_client.put(f"/api/agents/{agent_id}", json=body)
assert resp.status_code == 200
data = resp.json()
assert data["purpose"] == "Updated purpose for integration test"
async def test_create_variant(self, query_client, seed_ids):
"""POST /api/agents/{id}/variants — create variant."""
agent_id = seed_ids["agents"]["extractor"]
body = {
"variant_name": "Test Variant",
"description": "Integration test variant",
"model_provider": "ollama",
"model_name": "qwen3.5:9b",
"system_prompt": "Test variant prompt",
"prompt_version": "test-variant-v1",
}
resp = await query_client.post(f"/api/agents/{agent_id}/variants", json=body)
assert resp.status_code == 201
data = resp.json()
assert "id" in data
assert data["variant_name"] == "Test Variant"
async def test_activate_variant(self, query_client, seed_ids):
"""POST /api/agents/{id}/variants/{vid}/activate — activate variant."""
agent_id = seed_ids["agents"]["extractor"]
variant_id = seed_ids["variants"]["extractor"]
resp = await query_client.post(
f"/api/agents/{agent_id}/variants/{variant_id}/activate"
)
assert resp.status_code == 200
async def test_agent_performance(self, query_client, seed_ids):
"""GET /api/agents/{id}/performance — agent performance metrics."""
agent_id = seed_ids["agents"]["extractor"]
resp = await query_client.get(f"/api/agents/{agent_id}/performance")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, dict)
async def test_variant_performance(self, query_client, seed_ids):
"""GET /api/agents/{id}/variants/{vid}/performance — variant performance."""
agent_id = seed_ids["agents"]["extractor"]
variant_id = seed_ids["variants"]["extractor"]
resp = await query_client.get(
f"/api/agents/{agent_id}/variants/{variant_id}/performance"
)
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, dict)
class TestQueryAPIAnalytics:
"""Endpoints: /api/analytics/pg-schema, saved-queries, pg-query."""
async def test_pg_schema(self, query_client):
"""GET /api/analytics/pg-schema — PG schema info."""
resp = await query_client.get("/api/analytics/pg-schema")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, (list, dict))
async def test_list_saved_queries(self, query_client, seed_ids):
"""GET /api/analytics/saved-queries — at least 1 from seed."""
resp = await query_client.get("/api/analytics/saved-queries")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert len(data) >= 1
async def test_saved_query_create_and_delete(self, query_client):
"""POST then DELETE /api/analytics/saved-queries — lifecycle."""
body = {
"name": "IntTest Query",
"description": "Integration test saved query",
"sql_text": "SELECT 1 AS test",
}
resp = await query_client.post("/api/analytics/saved-queries", json=body)
assert resp.status_code == 201
data = resp.json()
query_id = data["id"]
# Delete
resp2 = await query_client.delete(f"/api/analytics/saved-queries/{query_id}")
assert resp2.status_code == 200
async def test_pg_query(self, query_client):
"""POST /api/analytics/pg-query — valid read-only SQL."""
body = {"sql": "SELECT ticker, legal_name FROM companies LIMIT 5"}
resp = await query_client.post("/api/analytics/pg-query", json=body)
assert resp.status_code == 200
data = resp.json()
assert "columns" in data or "rows" in data or isinstance(data, list)