Files
Celes Renata 5acb2fb43e fix: resolve 6 integration test failures
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)
2026-04-20 04:30:13 +00:00

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)