5acb2fb43e
1. patterns endpoint: fix query referencing non-existent column di.catalyst_type → dir.catalyst_type (column is on document_impact_records) 2. lockouts seed: use relative timestamps (now + 7d) so active lockout is always in the future regardless of when tests run 3. create_agent: make slug optional with auto-generation from name 4. create_source: json.dumps(config) + ::jsonb cast for asyncpg JSONB compat 5. approval_expiry: return count as int (len(expired)) not the list itself 6. metrics_consistency: fix test assertion to match API contract (total >= active + reserve, not total == active + reserve + unrealized)
244 lines
9.0 KiB
Python
244 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 — fields are present and non-negative."""
|
|
|
|
async def test_metrics_consistency(self, trading_client):
|
|
"""GET /api/trading/metrics — all fields present and non-negative."""
|
|
resp = await trading_client.get("/api/trading/metrics")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["total_portfolio_value"] >= 0
|
|
assert data["active_pool"] >= 0
|
|
assert data["reserve_pool"] >= 0
|
|
# active_pool + reserve_pool should not exceed total
|
|
assert data["active_pool"] + data["reserve_pool"] <= data["total_portfolio_value"] + 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)
|