262 lines
9.6 KiB
Python
262 lines
9.6 KiB
Python
import importlib
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from starlette.websockets import WebSocketDisconnect
|
|
|
|
|
|
def _load_main_module(monkeypatch):
|
|
backend_dir = Path(__file__).resolve().parents[1]
|
|
monkeypatch.syspath_prepend(str(backend_dir))
|
|
sys.modules.pop("main", None)
|
|
return importlib.import_module("main")
|
|
|
|
|
|
def test_config_check_smoke(monkeypatch):
|
|
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
monkeypatch.delenv("MINIMAX_API_KEY", raising=False)
|
|
|
|
main = _load_main_module(monkeypatch)
|
|
|
|
with TestClient(main.app) as client:
|
|
response = client.get("/api/config/check")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"configured": False}
|
|
|
|
|
|
def test_analysis_task_routes_smoke(monkeypatch):
|
|
monkeypatch.delenv("DASHBOARD_API_KEY", raising=False)
|
|
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
|
|
main = _load_main_module(monkeypatch)
|
|
|
|
seeded_task = {
|
|
"contract_version": "v1alpha1",
|
|
"task_id": "task-smoke",
|
|
"request_id": "req-task-smoke",
|
|
"executor_type": "legacy_subprocess",
|
|
"result_ref": None,
|
|
"ticker": "AAPL",
|
|
"date": "2026-04-11",
|
|
"status": "running",
|
|
"progress": 10,
|
|
"current_stage": "analysts",
|
|
"created_at": "2026-04-11T10:00:00",
|
|
"elapsed_seconds": 1,
|
|
"stages": [],
|
|
"result": None,
|
|
"error": None,
|
|
"degradation_summary": None,
|
|
"data_quality_summary": None,
|
|
"compat": {},
|
|
}
|
|
|
|
with TestClient(main.app) as client:
|
|
main.app.state.task_results["task-smoke"] = seeded_task
|
|
|
|
health_response = client.get("/health")
|
|
tasks_response = client.get("/api/analysis/tasks")
|
|
status_response = client.get("/api/analysis/status/task-smoke")
|
|
|
|
assert health_response.status_code == 200
|
|
assert health_response.json() == {"status": "ok"}
|
|
assert tasks_response.status_code == 200
|
|
assert tasks_response.json()["total"] >= 1
|
|
assert any(task["task_id"] == "task-smoke" for task in tasks_response.json()["tasks"])
|
|
assert status_response.status_code == 200
|
|
assert status_response.json()["task_id"] == "task-smoke"
|
|
assert status_response.json()["contract_version"] == "v1alpha1"
|
|
assert status_response.json()["request_id"] == "req-task-smoke"
|
|
assert status_response.json()["result"] is None
|
|
|
|
|
|
def test_analysis_start_route_uses_analysis_service(monkeypatch):
|
|
monkeypatch.delenv("DASHBOARD_API_KEY", raising=False)
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
main = _load_main_module(monkeypatch)
|
|
created: dict[str, object] = {}
|
|
|
|
class DummyTask:
|
|
def cancel(self):
|
|
return None
|
|
|
|
def fake_create_task(coro):
|
|
created["scheduled_coro"] = coro.cr_code.co_name
|
|
coro.close()
|
|
task = DummyTask()
|
|
created["task"] = task
|
|
return task
|
|
|
|
monkeypatch.setattr(main.asyncio, "create_task", fake_create_task)
|
|
|
|
with TestClient(main.app) as client:
|
|
response = client.post(
|
|
"/api/analysis/start",
|
|
json={"ticker": "AAPL", "date": "2026-04-11"},
|
|
headers={"api-key": "test-key"},
|
|
)
|
|
|
|
payload = response.json()
|
|
task_id = payload["task_id"]
|
|
|
|
assert response.status_code == 200
|
|
assert payload["ticker"] == "AAPL"
|
|
assert payload["date"] == "2026-04-11"
|
|
assert payload["status"] == "running"
|
|
assert created["scheduled_coro"] == "_run_analysis"
|
|
assert main.app.state.analysis_tasks[task_id] is created["task"]
|
|
assert main.app.state.task_results[task_id]["current_stage"] == "analysts"
|
|
assert main.app.state.task_results[task_id]["status"] == "running"
|
|
assert main.app.state.task_results[task_id]["request_id"]
|
|
assert main.app.state.task_results[task_id]["executor_type"] == "legacy_subprocess"
|
|
assert main.app.state.task_results[task_id]["result_ref"] is None
|
|
assert main.app.state.task_results[task_id]["compat"] == {}
|
|
|
|
|
|
def test_portfolio_analyze_route_uses_analysis_service_smoke(monkeypatch):
|
|
monkeypatch.delenv("DASHBOARD_API_KEY", raising=False)
|
|
monkeypatch.setenv("TRADINGAGENTS_USE_APPLICATION_SERVICES", "1")
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "service-key")
|
|
|
|
main = _load_main_module(monkeypatch)
|
|
captured: dict[str, object] = {}
|
|
|
|
async def fake_start_portfolio_analysis(*, task_id, date, request_context, broadcast_progress):
|
|
captured["task_id"] = task_id
|
|
captured["date"] = date
|
|
captured["request_context"] = request_context
|
|
captured["broadcast_progress"] = broadcast_progress
|
|
return {"task_id": task_id, "status": "running", "total": 3}
|
|
|
|
with TestClient(main.app) as client:
|
|
monkeypatch.setattr(main.app.state.analysis_service, "start_portfolio_analysis", fake_start_portfolio_analysis)
|
|
response = client.post("/api/portfolio/analyze", headers={"api-key": "service-key"})
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "running"
|
|
assert str(captured["task_id"]).startswith("port_")
|
|
assert isinstance(captured["date"], str)
|
|
assert captured["request_context"].api_key == "service-key"
|
|
assert callable(captured["broadcast_progress"])
|
|
|
|
|
|
def test_analysis_websocket_progress_is_contract_first(monkeypatch):
|
|
monkeypatch.delenv("DASHBOARD_API_KEY", raising=False)
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
main = _load_main_module(monkeypatch)
|
|
|
|
with TestClient(main.app) as client:
|
|
main.app.state.task_results["task-ws"] = {
|
|
"contract_version": "v1alpha1",
|
|
"task_id": "task-ws",
|
|
"request_id": "req-task-ws",
|
|
"executor_type": "legacy_subprocess",
|
|
"result_ref": None,
|
|
"ticker": "AAPL",
|
|
"date": "2026-04-11",
|
|
"status": "running",
|
|
"progress": 50,
|
|
"current_stage": "research",
|
|
"created_at": "2026-04-11T10:00:00",
|
|
"elapsed_seconds": 3,
|
|
"stages": [],
|
|
"result": None,
|
|
"error": None,
|
|
"degradation_summary": None,
|
|
"data_quality_summary": None,
|
|
"compat": {"decision": "HOLD"},
|
|
}
|
|
with client.websocket_connect("/ws/analysis/task-ws?api_key=test-key") as websocket:
|
|
message = websocket.receive_json()
|
|
|
|
assert message["type"] == "progress"
|
|
assert message["contract_version"] == "v1alpha1"
|
|
assert message["task_id"] == "task-ws"
|
|
assert message["request_id"] == "req-task-ws"
|
|
assert message["compat"]["decision"] == "HOLD"
|
|
assert "decision" not in message
|
|
|
|
|
|
def test_orchestrator_websocket_smoke_is_contract_first(monkeypatch):
|
|
monkeypatch.delenv("DASHBOARD_API_KEY", raising=False)
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
main = _load_main_module(monkeypatch)
|
|
|
|
import orchestrator.config as config_module
|
|
import orchestrator.live_mode as live_mode_module
|
|
import orchestrator.orchestrator as orchestrator_module
|
|
|
|
class DummyConfig:
|
|
def __init__(self, *args, **kwargs):
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
|
|
class DummyOrchestrator:
|
|
def __init__(self, config):
|
|
self.config = config
|
|
|
|
class DummyLiveMode:
|
|
def __init__(self, orchestrator):
|
|
self.orchestrator = orchestrator
|
|
|
|
async def run_once(self, tickers, date=None):
|
|
assert tickers == ["AAPL"]
|
|
assert date == "2026-04-11"
|
|
return [
|
|
{
|
|
"contract_version": "v1alpha1",
|
|
"ticker": "AAPL",
|
|
"date": "2026-04-11",
|
|
"status": "degraded_success",
|
|
"result": {
|
|
"direction": 1,
|
|
"confidence": 0.55,
|
|
"quant_direction": None,
|
|
"llm_direction": 1,
|
|
"timestamp": "2026-04-11T12:00:00+00:00",
|
|
},
|
|
"error": None,
|
|
"degradation": {
|
|
"degraded": True,
|
|
"reason_codes": ["quant_signal_failed"],
|
|
"source_diagnostics": {"quant": {"reason_code": "quant_signal_failed"}},
|
|
},
|
|
"data_quality": {"state": "partial_data", "source": "quant"},
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(config_module, "OrchestratorConfig", DummyConfig)
|
|
monkeypatch.setattr(orchestrator_module, "TradingOrchestrator", DummyOrchestrator)
|
|
monkeypatch.setattr(live_mode_module, "LiveMode", DummyLiveMode)
|
|
|
|
with TestClient(main.app) as client:
|
|
with client.websocket_connect("/ws/orchestrator?api_key=test-key") as websocket:
|
|
websocket.send_json({"tickers": ["AAPL"], "date": "2026-04-11"})
|
|
message = websocket.receive_json()
|
|
|
|
assert message["contract_version"] == "v1alpha1"
|
|
assert message["signals"][0]["contract_version"] == "v1alpha1"
|
|
assert message["signals"][0]["status"] == "degraded_success"
|
|
assert message["signals"][0]["degradation"]["reason_codes"] == ["quant_signal_failed"]
|
|
assert message["signals"][0]["data_quality"]["state"] == "partial_data"
|
|
|
|
|
|
def test_orchestrator_websocket_rejects_unauthorized(monkeypatch):
|
|
monkeypatch.delenv("DASHBOARD_API_KEY", raising=False)
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
main = _load_main_module(monkeypatch)
|
|
|
|
with TestClient(main.app) as client:
|
|
with pytest.raises(WebSocketDisconnect) as exc_info:
|
|
with client.websocket_connect("/ws/orchestrator"):
|
|
pass
|
|
|
|
assert exc_info.value.code == 4401
|