Files
ai-exchange/tests/unit/test_api_server.py
T
2026-05-06 16:03:40 +08:00

514 lines
21 KiB
Python

import contextlib
import io
import json
import os
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from sqlalchemy.exc import OperationalError
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from src.api import route_request
from scripts import run_execution, run_execution_settlement, run_offline_checks, run_pipeline
class ApiServerRoutingTests(unittest.TestCase):
def test_routes_health(self) -> None:
status, payload = route_request(
"/health",
{},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [],
"backtests": lambda **kwargs: [],
},
)
self.assertEqual(status, 200)
self.assertEqual(payload, {"ok": True})
def test_routes_signals_query(self) -> None:
status, payload = route_request(
"/signals",
{"instrument_id": ["1"], "limit": ["2"]},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [{"id": 1}, {"id": 2}],
"backtests": lambda **kwargs: [],
},
)
self.assertEqual(status, 200)
self.assertEqual(payload["items"], [{"id": 1}, {"id": 2}])
def test_routes_backtests_without_instrument_filter(self) -> None:
status, payload = route_request(
"/backtests",
{"limit": ["1"]},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [],
"backtests": lambda **kwargs: [{"id": 9}],
"executions": lambda **kwargs: [],
},
)
self.assertEqual(status, 200)
self.assertEqual(payload["items"], [{"id": 9}])
def test_routes_executions_query(self) -> None:
status, payload = route_request(
"/executions",
{"instrument_id": ["1"], "limit": ["2"]},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [],
"backtests": lambda **kwargs: [],
"executions": lambda **kwargs: [{"execution_id": 1, "audit_status": "ok"}],
},
)
self.assertEqual(status, 200)
self.assertEqual(payload["items"], [{"execution_id": 1, "audit_status": "ok"}])
def test_routes_execution_tickets_query(self) -> None:
status, payload = route_request(
"/execution-tickets",
{"signal_id": ["1"], "limit": ["2"]},
dependencies={
"execution_tickets": lambda **kwargs: [{"id": 1, "status": "awaiting_confirmation"}],
},
)
self.assertEqual(status, 200)
self.assertEqual(payload["items"], [{"id": 1, "status": "awaiting_confirmation"}])
def test_routes_health(self) -> None:
status, payload = route_request(
"/health",
{},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [],
"backtests": lambda **kwargs: [],
"executions": lambda **kwargs: [],
},
)
self.assertEqual(status, 200)
self.assertEqual(payload, {"ok": True})
def test_routes_signals_query(self) -> None:
status, payload = route_request(
"/signals",
{"instrument_id": ["1"], "limit": ["2"]},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [{"id": 1}, {"id": 2}],
"backtests": lambda **kwargs: [],
"executions": lambda **kwargs: [],
},
)
self.assertEqual(status, 200)
self.assertEqual(payload["items"], [{"id": 1}, {"id": 2}])
def test_rejects_missing_required_query_param(self) -> None:
with self.assertRaises(ValueError):
route_request(
"/signals",
{},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [],
"backtests": lambda **kwargs: [],
"executions": lambda **kwargs: [],
},
)
def test_returns_not_found_for_unknown_route(self) -> None:
status, payload = route_request(
"/unknown",
{},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [],
"backtests": lambda **kwargs: [],
"executions": lambda **kwargs: [],
},
)
self.assertEqual(status, 404)
self.assertEqual(payload["error"], "not_found")
class ScriptFallbackTests(unittest.TestCase):
def test_rejects_missing_required_query_param(self) -> None:
with self.assertRaises(ValueError):
route_request(
"/signals",
{},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [],
"backtests": lambda **kwargs: [],
},
)
def test_returns_not_found_for_unknown_route(self) -> None:
status, payload = route_request(
"/unknown",
{},
dependencies={
"health": lambda: {"ok": True},
"candles": lambda **kwargs: [],
"sessions": lambda **kwargs: [],
"signals": lambda **kwargs: [],
"backtests": lambda **kwargs: [],
},
)
self.assertEqual(status, 404)
self.assertEqual(payload["error"], "not_found")
class ScriptFallbackTests(unittest.TestCase):
def test_run_execution_reports_database_unavailable(self) -> None:
output = io.StringIO()
with patch("scripts.run_execution.ExecutionService.prepare_execution", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
exit_code = run_execution.main()
self.assertEqual(exit_code, 2)
self.assertIn("Database unavailable for execution sample: OperationalError", output.getvalue())
def test_run_execution_reports_database_unavailable_json(self) -> None:
output = io.StringIO()
with patch("scripts.run_execution.ExecutionService.prepare_execution", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
exit_code = run_execution.main(env={"EXECUTION_OUTPUT_JSON": "1"})
payload = json.loads(output.getvalue())
self.assertEqual(exit_code, 2)
self.assertFalse(payload["passed"])
self.assertEqual(payload["error_type"], "OperationalError")
self.assertEqual(payload["execution_mode"], "paper")
def test_run_execution_builds_account_risk_state_from_env(self) -> None:
state = run_execution.build_account_risk_state_from_env(
{
"EXECUTION_KILL_SWITCH": "true",
"EXECUTION_DAILY_PNL_PCT": "-0.01",
"EXECUTION_CONSECUTIVE_LOSSES": "2",
"EXECUTION_OPEN_POSITIONS": "1",
"EXECUTION_SAME_SIDE_OPEN_POSITIONS": "1",
"EXECUTION_MAX_DAILY_LOSS_PCT": "0.015",
"EXECUTION_MAX_CONSECUTIVE_LOSSES": "4",
"EXECUTION_MAX_OPEN_POSITIONS": "2",
"EXECUTION_MAX_SAME_SIDE_POSITIONS": "2",
}
)
self.assertTrue(state.kill_switch)
self.assertEqual(state.daily_pnl_pct, -0.01)
self.assertEqual(state.consecutive_losses, 2)
self.assertEqual(state.max_daily_loss_pct, 0.015)
self.assertEqual(state.max_consecutive_losses, 4)
def test_run_execution_empty_env_ignores_ambient_risk_state(self) -> None:
with patch.dict(
os.environ,
{
"EXECUTION_KILL_SWITCH": "true",
"EXECUTION_CONSECUTIVE_LOSSES": "9",
"EXECUTION_OPEN_POSITIONS": "7",
},
clear=False,
):
state = run_execution.build_account_risk_state_from_env({})
self.assertFalse(state.kill_switch)
self.assertEqual(state.consecutive_losses, 0)
self.assertEqual(state.open_positions, 0)
def test_run_execution_settlement_reports_database_unavailable(self) -> None:
output = io.StringIO()
with patch("scripts.run_execution_settlement.ExecutionService.settle_latest_execution_for_signal", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
exit_code = run_execution_settlement.main()
self.assertEqual(exit_code, 2)
self.assertIn("Database unavailable for execution settlement sample: OperationalError", output.getvalue())
def test_run_execution_settlement_reports_database_unavailable_json(self) -> None:
output = io.StringIO()
with patch("scripts.run_execution_settlement.ExecutionService.settle_latest_execution_for_signal", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
exit_code = run_execution_settlement.main(env={"EXECUTION_SETTLEMENT_OUTPUT_JSON": "1"})
payload = json.loads(output.getvalue())
self.assertEqual(exit_code, 2)
self.assertFalse(payload["passed"])
self.assertEqual(payload["error_type"], "OperationalError")
self.assertEqual(payload["execution_mode"], "paper")
def test_run_pipeline_reports_database_unavailable(self) -> None:
output = io.StringIO()
with patch("scripts.run_pipeline.run_closed_loop", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
exit_code = run_pipeline.main()
self.assertEqual(exit_code, 2)
self.assertIn("Database unavailable for pipeline sample: OperationalError", output.getvalue())
def test_run_pipeline_reports_database_unavailable_json(self) -> None:
output = io.StringIO()
with patch("scripts.run_pipeline.run_closed_loop", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
exit_code = run_pipeline.main(env={"PIPELINE_OUTPUT_JSON": "1"})
payload = json.loads(output.getvalue())
self.assertEqual(exit_code, 2)
self.assertFalse(payload["passed"])
self.assertEqual(payload["error_type"], "OperationalError")
def test_run_pipeline_passes_backtest_cost_config(self) -> None:
captured = {}
def fake_run_closed_loop(**kwargs):
captured.update(kwargs)
return {"ok": True}
output = io.StringIO()
env = {
"BACKTEST_FEE_RATE": "0.0005",
"BACKTEST_SLIPPAGE_RATE": "0.0007",
"BACKTEST_FIXED_COST_R": "0.05",
}
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
run_pipeline.main()
cost_config = captured["backtest_cost_config"]
self.assertEqual(cost_config.fee_rate, 0.0005)
self.assertEqual(cost_config.slippage_rate, 0.0007)
self.assertEqual(cost_config.fixed_cost_r, 0.05)
def test_run_pipeline_passes_backtest_quality_gate_config(self) -> None:
captured = {}
def fake_run_closed_loop(**kwargs):
captured.update(kwargs)
return {"ok": True, "backtest_quality_gate": {"passed": True}}
output = io.StringIO()
env = {
"BACKTEST_MIN_TOTAL_SIGNALS": "50",
"BACKTEST_MIN_EXECUTED_SIGNALS": "20",
"BACKTEST_MIN_EXPECTANCY_R": "0.1",
"BACKTEST_MIN_PROFIT_FACTOR_R": "1.5",
"BACKTEST_MAX_DRAWDOWN_R": "6.0",
"BACKTEST_MAX_CONSECUTIVE_LOSSES": "3",
}
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
run_pipeline.main()
quality_gate = captured["backtest_quality_gate"]
self.assertEqual(quality_gate.min_total_signals, 50)
self.assertEqual(quality_gate.min_executed_signals, 20)
self.assertEqual(quality_gate.min_expectancy_r, 0.1)
self.assertEqual(quality_gate.min_profit_factor_r, 1.5)
self.assertEqual(quality_gate.max_drawdown_r, 6.0)
self.assertEqual(quality_gate.max_consecutive_losses, 3)
def test_run_pipeline_passes_backtest_replay_mode_and_config(self) -> None:
captured = {}
def fake_run_closed_loop(**kwargs):
captured.update(kwargs)
return {"ok": True, "backtest_quality_gate": {"passed": True}}
output = io.StringIO()
env = {
"BACKTEST_MODE": "replay_no_lookahead",
"BACKTEST_REPLAY_LEFT_BARS": "1",
"BACKTEST_REPLAY_RIGHT_BARS": "3",
}
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
run_pipeline.main()
self.assertEqual(captured["backtest_mode"], "replay_no_lookahead")
self.assertEqual(captured["backtest_replay_config"].left_bars, 1)
self.assertEqual(captured["backtest_replay_config"].right_bars, 3)
def test_run_pipeline_passes_signal_evidence_mode(self) -> None:
captured = {}
def fake_run_closed_loop(**kwargs):
captured.update(kwargs)
return {"ok": True, "backtest_quality_gate": {"passed": True}}
output = io.StringIO()
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, {"SIGNAL_EVIDENCE_MODE": "strict"}, clear=False), contextlib.redirect_stdout(output):
run_pipeline.main()
self.assertIn("sweep_id", captured["signal_required_evidence_keys"])
self.assertIn("liquidity_target_id", captured["signal_required_evidence_keys"])
def test_run_pipeline_passes_execution_account_state(self) -> None:
captured = {}
def fake_run_closed_loop(**kwargs):
captured.update(kwargs)
return {"ok": True}
output = io.StringIO()
env = {
"EXECUTION_DAILY_PNL_PCT": "-0.01",
"EXECUTION_CONSECUTIVE_LOSSES": "2",
}
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
run_pipeline.main()
account_state = captured["execution_account_state"]
self.assertEqual(account_state.daily_pnl_pct, -0.01)
self.assertEqual(account_state.consecutive_losses, 2)
def test_run_pipeline_blocks_when_required_quality_gate_fails(self) -> None:
output = io.StringIO()
result = {"backtest_quality_gate": {"passed": False, "reasons": ["sample_size_too_small"]}}
with patch("scripts.run_pipeline.run_closed_loop", return_value=result), patch.dict(os.environ, {"BACKTEST_REQUIRE_QUALITY_GATE": "1"}, clear=False), contextlib.redirect_stdout(output):
exit_code = run_pipeline.main()
self.assertEqual(exit_code, 2)
self.assertIn('"backtest_quality_gate"', output.getvalue())
class OfflineChecksScriptTests(unittest.TestCase):
def test_build_offline_check_commands_adds_replay_file_when_configured(self) -> None:
commands = run_offline_checks.build_check_commands(env={"OFFLINE_REPLAY_FILE": "sample.jsonl"})
self.assertEqual(commands[-1][0], "replay_file")
self.assertEqual(commands[-1][1][-1], "sample.jsonl")
def test_run_offline_checks_returns_zero_when_all_checks_pass(self) -> None:
calls = []
def fake_runner(command, **kwargs):
calls.append(command)
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="")
output = io.StringIO()
commands = [("unit_tests", ["python", "-m", "unittest"]), ("dashboard_js_syntax", ["node", "--check", "src/web/app.js"])]
with contextlib.redirect_stdout(output):
exit_code = run_offline_checks.run_checks(commands=commands, runner=fake_runner, node_available=True, env={})
self.assertEqual(exit_code, 0)
self.assertEqual(len(calls), 2)
self.assertIn("Offline check passed: dashboard_js_syntax", output.getvalue())
def test_run_offline_checks_can_include_optional_replay_file(self) -> None:
calls = []
def fake_runner(command, **kwargs):
calls.append(command)
return SimpleNamespace(returncode=0, stdout="", stderr="")
output = io.StringIO()
with contextlib.redirect_stdout(output):
exit_code = run_offline_checks.run_checks(runner=fake_runner, node_available=True, env={"OFFLINE_REPLAY_FILE": "sample.csv"})
self.assertEqual(exit_code, 0)
self.assertEqual(calls[-1][-1], "sample.csv")
self.assertIn("Offline check passed: replay_file", output.getvalue())
def test_run_offline_checks_stops_on_failure(self) -> None:
calls = []
def fake_runner(command, **kwargs):
calls.append(command)
return SimpleNamespace(returncode=7, stdout="", stderr="failed\n")
output = io.StringIO()
with contextlib.redirect_stdout(output):
exit_code = run_offline_checks.run_checks(commands=[("unit_tests", ["python", "-m", "unittest"])], runner=fake_runner, env={})
self.assertEqual(exit_code, 7)
self.assertEqual(len(calls), 1)
self.assertIn("Offline check failed: unit_tests exit_code=7", output.getvalue())
def test_run_offline_checks_skips_node_when_unavailable(self) -> None:
calls = []
def fake_runner(command, **kwargs):
calls.append(command)
return SimpleNamespace(returncode=0, stdout="", stderr="")
output = io.StringIO()
with contextlib.redirect_stdout(output):
exit_code = run_offline_checks.run_checks(commands=[("dashboard_js_syntax", ["node", "--check", "src/web/app.js"])], runner=fake_runner, node_available=False, env={})
self.assertEqual(exit_code, 0)
self.assertEqual(calls, [])
self.assertIn("Offline check skipped: dashboard_js_syntax", output.getvalue())
def test_run_offline_checks_can_emit_json_summary(self) -> None:
def fake_runner(command, **kwargs):
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="")
output = io.StringIO()
with contextlib.redirect_stdout(output):
exit_code = run_offline_checks.run_checks(
commands=[("unit_tests", ["python", "-m", "unittest"])],
runner=fake_runner,
env={"OFFLINE_CHECKS_OUTPUT_JSON": "1"},
)
summary = json.loads(output.getvalue())
self.assertEqual(exit_code, 0)
self.assertTrue(summary["passed"])
self.assertEqual(summary["checks"][0]["name"], "unit_tests")
self.assertEqual(summary["checks"][0]["status"], "passed")
def test_run_offline_checks_passes_merged_env_to_subprocesses(self) -> None:
seen_env = {}
def fake_runner(command, **kwargs):
seen_env.update(kwargs["env"])
return SimpleNamespace(returncode=0, stdout="", stderr="")
with contextlib.redirect_stdout(io.StringIO()):
exit_code = run_offline_checks.run_checks(
commands=[("unit_tests", ["python", "-m", "unittest"])],
runner=fake_runner,
env={"CUSTOM_OFFLINE_FLAG": "1"},
)
self.assertEqual(exit_code, 0)
self.assertEqual(seen_env["CUSTOM_OFFLINE_FLAG"], "1")
self.assertIn("PATH", seen_env)
if __name__ == "__main__":
unittest.main()