898f89926d
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.
245 lines
9.0 KiB
Python
245 lines
9.0 KiB
Python
"""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)
|