"""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)