Files
ai-exchange/tests/unit/test_launch_readiness_service.py
T
2026-05-11 23:37:47 +08:00

157 lines
6.5 KiB
Python

import os
import unittest
from unittest.mock import patch
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from src.services.readiness import LaunchReadinessService
class LaunchReadinessServiceTests(unittest.TestCase):
def test_build_summary_reports_assisted_not_ready_without_real_trade_sample(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
broker_read_model_service=_BrokerReadService(),
weekly_review_service=_WeeklyReviewService(),
trust_service=_TrustService(),
live_readiness_service=_LiveReadinessService(),
)
with (
patch("src.services.readiness.launch_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()),
patch("src.services.readiness.launch_readiness_service.settings.launch_require_real_trade_sample", True),
):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertTrue(payload["analysis_only_ready"])
self.assertTrue(payload["assisted_trading_ready"])
self.assertTrue(payload["operator_soft_rollout_ready"])
self.assertEqual(payload["operator_soft_rollout_mode"], "execution_assist")
self.assertTrue(payload["real_trade_sample_gate_required"])
self.assertEqual(payload["real_trade_sample_gate_status"], "waiting_for_real_fill_sample")
self.assertIn("wait_for_and_verify_real_fill_sample", payload["next_actions"])
self.assertFalse(payload["real_trade_loop_ready"])
self.assertIn("real_trade_sample_verified", payload["warning_reasons"])
self.assertIn("real_trade_sample_verified", payload["external_waiting_items"])
self.assertFalse(payload["real_live_ready"])
def test_build_summary_defers_real_trade_sample_gate_for_analysis_decision_phase(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
broker_read_model_service=_BrokerReadService(),
weekly_review_service=_WeeklyReviewService(),
trust_service=_TrustService(),
live_readiness_service=_LiveReadinessService(),
)
with (
patch("src.services.readiness.launch_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()),
patch("src.services.readiness.launch_readiness_service.settings.launch_require_real_trade_sample", False),
):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertTrue(payload["analysis_only_ready"])
self.assertFalse(payload["real_trade_sample_gate_required"])
self.assertEqual(payload["real_trade_sample_gate_status"], "deferred_analysis_decision_only")
self.assertEqual(payload["real_trade_sample_gate_phase"], "deferred_analysis_decision_only")
self.assertNotIn("real_trade_sample_verified", payload["warning_reasons"])
self.assertNotIn("real_trade_sample_verified", payload["external_waiting_items"])
self.assertNotIn("wait_for_and_verify_real_fill_sample", payload["next_actions"])
def test_build_summary_exposes_restricted_soft_rollout_when_only_blacklist_models_exist(self) -> None:
service = LaunchReadinessService(
market_state_service=_MarketStateService(),
broker_read_model_service=_BrokerReadService(),
weekly_review_service=_WeeklyReviewService(),
trust_service=_BlacklistOnlyTrustService(),
live_readiness_service=_LiveReadinessService(),
)
with patch("src.services.readiness.launch_readiness_service.get_trading_runtime_service", return_value=_RuntimeService()):
payload = service.build_summary(instrument_id=1, timeframe="1m")
self.assertTrue(payload["analysis_only_ready"])
self.assertFalse(payload["decision_support_ready"])
self.assertTrue(payload["restricted_decision_support_ready"])
self.assertTrue(payload["operator_soft_rollout_ready"])
self.assertEqual(payload["operator_soft_rollout_mode"], "restricted_decision_support")
self.assertIn("start_restricted_decision_support_soft_rollout", payload["next_actions"])
self.assertIn("execution_assist_remains_disabled", payload["operator_soft_rollout_boundary_notes"])
self.assertNotIn("trust_model_available", payload["warning_reasons"])
class _MarketState:
freshness = type("Freshness", (), {"code": "market_data_fresh"})()
stream_state = type("Stream", (), {"code": "market_stream_connected"})()
data_gap_state = type("Gap", (), {"code": "market_data_gap_clear"})()
news_window = type("News", (), {"provider_name": "cached", "freshness_code": "news_provider_cached", "error": None})()
def to_dict(self):
return {
"freshness": {"code": "market_data_fresh"},
"stream_state": {"code": "market_stream_connected"},
"data_gap_state": {"code": "market_data_gap_clear"},
}
class _MarketStateService:
def build_market_state(self, **kwargs):
return _MarketState()
class _BrokerState:
status = "broker_state_ok"
def to_dict(self):
return {"status": "broker_state_ok"}
class _BrokerReadService:
def fetch_snapshot(self, **kwargs):
return _BrokerState()
class _WeeklyReviewService:
def build_summary(self, **kwargs):
return {"real_trade_count": 0}
class _TrustService:
def build_summary(self, **kwargs):
return {
"whitelist": ["OSOK"],
"observe_list": [],
"blacklist": [],
"analysis_only_list": ["OSOK"],
"restricted_decision_support_list": ["OSOK"],
"decision_support_list": ["OSOK"],
"execution_candidate_list": ["OSOK"],
}
class _BlacklistOnlyTrustService:
def build_summary(self, **kwargs):
return {
"whitelist": [],
"observe_list": [],
"blacklist": ["SWEEP_MSS_FVG"],
"analysis_only_list": ["SWEEP_MSS_FVG"],
"restricted_decision_support_list": ["SWEEP_MSS_FVG"],
"decision_support_list": [],
"execution_candidate_list": [],
}
class _LiveReadinessService:
def build_summary(self, **kwargs):
return {"ready_for_real_live": False, "blocking_reasons": ["adapter_real_live_not_ready"]}
class _RuntimeService:
def status(self):
return {"scheduler_state": "running", "current_blockers": []}
if __name__ == "__main__":
unittest.main()