c85c0068a2
- Replace all datetime.utcnow() with datetime.now(tz=timezone.utc) across 8 files - Fix 12 failing tests to match current implementation behavior - Fix pytest_plugins in non-top-level conftest (moved to root conftest.py) - Auto-fix 189 lint issues (import sorting, unused imports) - Add CI/CD pipeline infrastructure (ARC, ArgoCD, Kargo manifests) - Add values-beta.yaml and values-paper.yaml for staged deployments - Update GitHub Actions workflow to use self-hosted-gremlin runners - Add integration-test job to CI pipeline Result: 1596 passed, 0 failed, 0 warnings
191 lines
6.7 KiB
Python
191 lines
6.7 KiB
Python
"""Shared pytest fixtures for integration tests.
|
|
|
|
Provides HTTP clients (httpx.AsyncClient) for each service, base URL
|
|
fixtures driven by environment variables, and a seed_ids dict that
|
|
re-exports every deterministic UUID from seed_sandbox.py.
|
|
|
|
When the ``profiler`` fixture is active (provided by conftest_profiling.py),
|
|
each HTTP client is wrapped in :class:`ProfiledAsyncClient` so that every
|
|
request is automatically timed and recorded.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from tests.integration.profiler import EndpointProfiler
|
|
from tests.integration.seed_sandbox import (
|
|
SEED_AGENT_IDS,
|
|
SEED_BROKER_ACCOUNT_ID,
|
|
SEED_COMPANY_IDS,
|
|
SEED_DOCUMENT_IDS,
|
|
SEED_GLOBAL_EVENT_IDS,
|
|
SEED_ORDER_IDS,
|
|
SEED_PORTFOLIO_SNAPSHOT_ID,
|
|
SEED_POSITION_IDS,
|
|
SEED_RECOMMENDATION_IDS,
|
|
SEED_RISK_CONFIG_ID,
|
|
SEED_TRADING_DECISION_ID,
|
|
SEED_TREND_IDS,
|
|
SEED_VARIANT_IDS,
|
|
)
|
|
|
|
# Profiling plugin loaded via root conftest.py (pytest_plugins must be top-level)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ProfiledAsyncClient — transparent timing wrapper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ProfiledAsyncClient:
|
|
"""Wraps :class:`httpx.AsyncClient` to record per-request timing.
|
|
|
|
Every HTTP method call (get, post, put, patch, delete, head, options)
|
|
is automatically timed via :meth:`EndpointProfiler.track` using the
|
|
pattern ``"METHOD /path"``.
|
|
|
|
Attribute access for anything not explicitly wrapped is forwarded to
|
|
the underlying client so tests can still use ``client.base_url``,
|
|
``client.headers``, etc.
|
|
"""
|
|
|
|
def __init__(self, client: httpx.AsyncClient, profiler: EndpointProfiler) -> None:
|
|
self._client = client
|
|
self._profiler = profiler
|
|
|
|
# -- Proxied HTTP methods ------------------------------------------------
|
|
|
|
async def get(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
async with self._profiler.track(f"GET {url}"):
|
|
return await self._client.get(url, **kwargs)
|
|
|
|
async def post(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
async with self._profiler.track(f"POST {url}"):
|
|
return await self._client.post(url, **kwargs)
|
|
|
|
async def put(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
async with self._profiler.track(f"PUT {url}"):
|
|
return await self._client.put(url, **kwargs)
|
|
|
|
async def patch(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
async with self._profiler.track(f"PATCH {url}"):
|
|
return await self._client.patch(url, **kwargs)
|
|
|
|
async def delete(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
async with self._profiler.track(f"DELETE {url}"):
|
|
return await self._client.delete(url, **kwargs)
|
|
|
|
async def head(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
async with self._profiler.track(f"HEAD {url}"):
|
|
return await self._client.head(url, **kwargs)
|
|
|
|
async def options(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
async with self._profiler.track(f"OPTIONS {url}"):
|
|
return await self._client.options(url, **kwargs)
|
|
|
|
# -- Transparent attribute forwarding ------------------------------------
|
|
|
|
def __getattr__(self, name: str) -> Any:
|
|
return getattr(self._client, name)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# URL fixtures — read from env vars set by the runner Job (runner.yaml)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def query_api_url() -> str:
|
|
"""Base URL for the Query API service."""
|
|
return os.environ.get("QUERY_API_URL", "http://localhost:8000")
|
|
|
|
|
|
@pytest.fixture
|
|
def registry_api_url() -> str:
|
|
"""Base URL for the Symbol Registry service."""
|
|
return os.environ.get("REGISTRY_API_URL", "http://localhost:8001")
|
|
|
|
|
|
@pytest.fixture
|
|
def risk_api_url() -> str:
|
|
"""Base URL for the Risk Engine service."""
|
|
return os.environ.get("RISK_API_URL", "http://localhost:8002")
|
|
|
|
|
|
@pytest.fixture
|
|
def trading_api_url() -> str:
|
|
"""Base URL for the Trading Engine service."""
|
|
return os.environ.get("TRADING_API_URL", "http://localhost:8003")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Async HTTP client fixtures — one per service, 30 s timeout
|
|
# Wrapped with ProfiledAsyncClient for automatic timing collection.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
async def query_client(
|
|
query_api_url: str, profiler: EndpointProfiler,
|
|
) -> ProfiledAsyncClient:
|
|
"""Profiled async HTTP client pointed at the Query API."""
|
|
async with httpx.AsyncClient(base_url=query_api_url, timeout=30.0) as client:
|
|
yield ProfiledAsyncClient(client, profiler)
|
|
|
|
|
|
@pytest.fixture
|
|
async def registry_client(
|
|
registry_api_url: str, profiler: EndpointProfiler,
|
|
) -> ProfiledAsyncClient:
|
|
"""Profiled async HTTP client pointed at the Symbol Registry."""
|
|
async with httpx.AsyncClient(base_url=registry_api_url, timeout=30.0) as client:
|
|
yield ProfiledAsyncClient(client, profiler)
|
|
|
|
|
|
@pytest.fixture
|
|
async def risk_client(
|
|
risk_api_url: str, profiler: EndpointProfiler,
|
|
) -> ProfiledAsyncClient:
|
|
"""Profiled async HTTP client pointed at the Risk Engine."""
|
|
async with httpx.AsyncClient(base_url=risk_api_url, timeout=30.0) as client:
|
|
yield ProfiledAsyncClient(client, profiler)
|
|
|
|
|
|
@pytest.fixture
|
|
async def trading_client(
|
|
trading_api_url: str, profiler: EndpointProfiler,
|
|
) -> ProfiledAsyncClient:
|
|
"""Profiled async HTTP client pointed at the Trading Engine."""
|
|
async with httpx.AsyncClient(base_url=trading_api_url, timeout=30.0) as client:
|
|
yield ProfiledAsyncClient(client, profiler)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Seed ID lookup — single dict with all deterministic IDs from seed_sandbox
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def seed_ids() -> dict:
|
|
"""All deterministic seed IDs for assertion in integration tests."""
|
|
return {
|
|
"companies": SEED_COMPANY_IDS,
|
|
"documents": SEED_DOCUMENT_IDS,
|
|
"trends": SEED_TREND_IDS,
|
|
"recommendations": SEED_RECOMMENDATION_IDS,
|
|
"orders": SEED_ORDER_IDS,
|
|
"positions": SEED_POSITION_IDS,
|
|
"global_events": SEED_GLOBAL_EVENT_IDS,
|
|
"agents": SEED_AGENT_IDS,
|
|
"variants": SEED_VARIANT_IDS,
|
|
"broker_account_id": SEED_BROKER_ACCOUNT_ID,
|
|
"trading_decision_id": SEED_TRADING_DECISION_ID,
|
|
"portfolio_snapshot_id": SEED_PORTFOLIO_SNAPSHOT_ID,
|
|
"risk_config_id": SEED_RISK_CONFIG_ID,
|
|
}
|