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.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
"""Integration tests for Trading Engine — extended coverage.
|
||||
|
||||
Validates configuration round-trips, pause/resume lifecycle, metrics
|
||||
consistency, notification config, decision filtering, and override
|
||||
order validation against the live sandbox.
|
||||
|
||||
Uses the ``trading_client`` fixture from conftest.py.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1 Config Round-Trip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTradingConfigRoundTrip:
|
||||
"""PUT config → GET status — verify risk_tier reflected."""
|
||||
|
||||
async def test_config_round_trip(self, trading_client):
|
||||
"""PUT config → GET status — verify risk_tier reflected."""
|
||||
# Set to aggressive
|
||||
resp = await trading_client.put(
|
||||
"/api/trading/config", json={"risk_tier": "aggressive"}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
# Verify in status
|
||||
status_resp = await trading_client.get("/api/trading/status")
|
||||
assert status_resp.status_code == 200
|
||||
assert status_resp.json()["risk_tier"] == "aggressive"
|
||||
# Restore to moderate
|
||||
await trading_client.put(
|
||||
"/api/trading/config", json={"risk_tier": "moderate"}
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2 Pause/Resume Round-Trip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTradingPauseResumeRoundTrip:
|
||||
"""POST pause → GET status → POST resume → GET status."""
|
||||
|
||||
async def test_pause_resume_round_trip(self, trading_client):
|
||||
"""POST pause → GET status → POST resume → GET status."""
|
||||
# Pause
|
||||
resp = await trading_client.post("/api/trading/pause")
|
||||
assert resp.status_code == 200
|
||||
# Verify paused
|
||||
status_resp = await trading_client.get("/api/trading/status")
|
||||
assert status_resp.status_code == 200
|
||||
assert status_resp.json()["paused"] is True
|
||||
# Resume
|
||||
resp2 = await trading_client.post("/api/trading/resume")
|
||||
assert resp2.status_code == 200
|
||||
# Verify resumed
|
||||
status_resp2 = await trading_client.get("/api/trading/status")
|
||||
assert status_resp2.status_code == 200
|
||||
assert status_resp2.json()["paused"] is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3 Metrics Consistency
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTradingMetricsConsistency:
|
||||
"""GET /api/trading/metrics — total ≈ active + reserve + unrealized."""
|
||||
|
||||
async def test_metrics_consistency(self, trading_client):
|
||||
"""GET /api/trading/metrics — total ≈ active + reserve + unrealized."""
|
||||
resp = await trading_client.get("/api/trading/metrics")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
total = data["total_portfolio_value"]
|
||||
active = data["active_pool"]
|
||||
reserve = data["reserve_pool"]
|
||||
unrealized = data["unrealized_pnl"]
|
||||
# Allow tolerance for rounding
|
||||
assert abs(total - (active + reserve + unrealized)) < 1.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4 Metrics History
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTradingMetricsHistoryExtended:
|
||||
"""GET /api/trading/metrics/history — returns portfolio snapshots."""
|
||||
|
||||
async def test_metrics_history_snapshots(self, trading_client):
|
||||
"""GET /api/trading/metrics/history — returns portfolio snapshots."""
|
||||
resp = await trading_client.get("/api/trading/metrics/history")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
if len(data) >= 1:
|
||||
snap = data[0]
|
||||
assert "portfolio_value" in snap
|
||||
assert "snapshot_date" in snap
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5 Notification Config Round-Trip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTradingNotificationRoundTrip:
|
||||
"""PUT → GET notification config round-trip."""
|
||||
|
||||
async def test_notification_config_round_trip(self, trading_client):
|
||||
"""PUT → GET notification config round-trip."""
|
||||
# Update phone number (which drives sms_enabled)
|
||||
resp = await trading_client.put(
|
||||
"/api/trading/notifications/config",
|
||||
json={"phone_number": "+15551234567"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
# Verify GET returns config structure
|
||||
get_resp = await trading_client.get("/api/trading/notifications/config")
|
||||
assert get_resp.status_code == 200
|
||||
data = get_resp.json()
|
||||
assert "sms_enabled" in data
|
||||
assert "email_enabled" in data
|
||||
assert isinstance(data["sms_enabled"], bool)
|
||||
assert isinstance(data["email_enabled"], bool)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6 Notification History
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTradingNotificationHistory:
|
||||
"""GET /api/trading/notifications/history — returns list."""
|
||||
|
||||
async def test_notification_history(self, trading_client):
|
||||
"""GET /api/trading/notifications/history — returns list."""
|
||||
resp = await trading_client.get("/api/trading/notifications/history")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7 Decision Filtering
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTradingDecisionFiltering:
|
||||
"""GET /api/trading/decisions with various filters."""
|
||||
|
||||
async def test_decisions_filter_by_ticker(self, trading_client):
|
||||
"""GET /api/trading/decisions?ticker=AAPL — only AAPL decisions."""
|
||||
resp = await trading_client.get(
|
||||
"/api/trading/decisions", params={"ticker": "AAPL"}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
for d in data:
|
||||
assert d["ticker"] == "AAPL"
|
||||
|
||||
async def test_decisions_filter_by_limit(self, trading_client):
|
||||
"""GET /api/trading/decisions?limit=1 — at most 1 decision."""
|
||||
resp = await trading_client.get(
|
||||
"/api/trading/decisions", params={"limit": "1"}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) <= 1
|
||||
|
||||
async def test_decisions_filter_by_decision_type(self, trading_client):
|
||||
"""GET /api/trading/decisions?decision=execute — only execute decisions."""
|
||||
resp = await trading_client.get(
|
||||
"/api/trading/decisions", params={"decision": "execute"}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
for d in data:
|
||||
assert d["decision"] == "execute"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8 Override Order Validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTradingOverrideValidation:
|
||||
"""POST /api/trading/override/order — validation edge cases."""
|
||||
|
||||
async def test_override_invalid_ticker(self, trading_client):
|
||||
"""POST /api/trading/override/order — invalid ticker returns 422."""
|
||||
payload = {
|
||||
"ticker": "AAPL123", # contains digits — should be rejected
|
||||
"side": "buy",
|
||||
"quantity": 1.0,
|
||||
"order_type": "market",
|
||||
}
|
||||
resp = await trading_client.post(
|
||||
"/api/trading/override/order", json=payload
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
async def test_override_zero_quantity(self, trading_client):
|
||||
"""POST /api/trading/override/order — zero quantity returns 422."""
|
||||
payload = {
|
||||
"ticker": "AAPL",
|
||||
"side": "buy",
|
||||
"quantity": 0,
|
||||
"order_type": "market",
|
||||
}
|
||||
resp = await trading_client.post(
|
||||
"/api/trading/override/order", json=payload
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
async def test_override_valid_order(self, trading_client):
|
||||
"""POST /api/trading/override/order — valid order returns 202 or structured error."""
|
||||
payload = {
|
||||
"ticker": "AAPL",
|
||||
"side": "buy",
|
||||
"quantity": 1.0,
|
||||
"order_type": "market",
|
||||
}
|
||||
resp = await trading_client.post(
|
||||
"/api/trading/override/order", json=payload
|
||||
)
|
||||
assert resp.status_code in (200, 202, 400, 422, 503)
|
||||
data = resp.json()
|
||||
if resp.status_code == 202:
|
||||
assert "job_id" in data
|
||||
assert data["status"] == "queued"
|
||||
assert data["ticker"] == "AAPL"
|
||||
assert data["side"] == "buy"
|
||||
assert data["quantity"] == 1.0
|
||||
else:
|
||||
assert isinstance(data, dict)
|
||||
Reference in New Issue
Block a user