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.
137 lines
5.0 KiB
Python
137 lines
5.0 KiB
Python
"""Integration tests for Risk Engine — evaluation edge cases and approval lifecycle.
|
|
|
|
Validates evaluation with minimal/extreme orders, custom config overrides,
|
|
and the full approval lifecycle (list → detail → review → expire) against
|
|
the live sandbox with deterministic seed data.
|
|
|
|
Uses the ``risk_client`` and ``seed_ids`` fixtures from conftest.py.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1 Evaluation Edge Cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRiskEvaluationEdgeCases:
|
|
"""Edge-case scenarios for POST /evaluate."""
|
|
|
|
async def test_minimal_order_evaluation(self, risk_client):
|
|
"""POST /evaluate — minimal order with only ticker."""
|
|
payload = {"order": {"ticker": "AAPL"}}
|
|
resp = await risk_client.post("/evaluate", json=payload)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "evaluation_id" in data
|
|
assert "eligible" in data
|
|
assert "rejection_reasons" in data
|
|
|
|
async def test_order_exceeding_position_cap(self, risk_client):
|
|
"""POST /evaluate — order exceeding position cap returns eligible: false."""
|
|
payload = {
|
|
"order": {
|
|
"ticker": "AAPL",
|
|
"action": "buy",
|
|
"quantity": 100000,
|
|
"estimated_value": 18550000.00,
|
|
"confidence": 0.5,
|
|
"sector": "Technology",
|
|
},
|
|
}
|
|
resp = await risk_client.post("/evaluate", json=payload)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["eligible"] is False
|
|
assert len(data["rejection_reasons"]) >= 1
|
|
|
|
async def test_evaluation_with_custom_config(self, risk_client):
|
|
"""POST /evaluate — custom config override."""
|
|
payload = {
|
|
"order": {
|
|
"ticker": "MSFT",
|
|
"action": "buy",
|
|
"quantity": 5,
|
|
"estimated_value": 2050.00,
|
|
"confidence": 0.8,
|
|
},
|
|
"config": {
|
|
"max_portfolio_heat": 0.50,
|
|
"max_single_position_pct": 0.10,
|
|
"max_sector_concentration": 0.50,
|
|
"daily_loss_limit_pct": 0.05,
|
|
},
|
|
}
|
|
resp = await risk_client.post("/evaluate", json=payload)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "evaluation_id" in data
|
|
assert "eligible" in data
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2 Approval Lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRiskApprovalLifecycle:
|
|
"""Full approval lifecycle: list → detail → review → expire."""
|
|
|
|
async def test_pending_approvals_list(self, risk_client, seed_ids):
|
|
"""GET /approvals/pending — list pending approvals from seed."""
|
|
resp = await risk_client.get("/approvals/pending")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) >= 1
|
|
|
|
async def test_approval_detail(self, risk_client, seed_ids):
|
|
"""GET /approvals/{id} — seeded pending approval detail."""
|
|
approval_id = seed_ids["approvals"]["PENDING"]
|
|
resp = await risk_client.get(f"/approvals/{approval_id}")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["ticker"] == "AAPL"
|
|
assert data["side"] == "buy"
|
|
assert "status" in data
|
|
assert "expires_at" in data
|
|
|
|
async def test_approval_review(self, risk_client, seed_ids):
|
|
"""POST /approvals/{id}/review — approve the seeded pending approval."""
|
|
approval_id = seed_ids["approvals"]["PENDING"]
|
|
payload = {
|
|
"approved": True,
|
|
"reviewed_by": "test-operator",
|
|
"review_note": "Integration test approval",
|
|
}
|
|
resp = await risk_client.post(
|
|
f"/approvals/{approval_id}/review", json=payload,
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] == "approved"
|
|
|
|
async def test_review_nonexistent_approval(self, risk_client):
|
|
"""POST /approvals/{id}/review — 404 for non-existent approval."""
|
|
fake_id = "00000000-0000-4000-ffff-000000000099"
|
|
payload = {
|
|
"approved": True,
|
|
"reviewed_by": "test-operator",
|
|
"review_note": "Should fail",
|
|
}
|
|
resp = await risk_client.post(
|
|
f"/approvals/{fake_id}/review", json=payload,
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
async def test_approval_expiry(self, risk_client):
|
|
"""POST /approvals/expire — expire stale approvals."""
|
|
resp = await risk_client.post("/approvals/expire")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "expired" in data
|
|
assert isinstance(data["expired"], int)
|