Files
ai-exchange/tests/unit/test_decision_packet_service.py
T
2026-05-12 08:11:16 +08:00

269 lines
11 KiB
Python

from datetime import datetime, timezone
import os
import unittest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from unittest.mock import patch
from src.services.decision.decision_packet_service import (
_select_candidate,
build_alert_candidates,
build_decision_packet_from_rows,
DecisionPacketService,
)
class StubVisualSnapshotRepository:
def __init__(self):
self.snapshot_calls = []
self.review_calls = []
def insert_snapshot(self, **kwargs):
self.snapshot_calls.append(kwargs)
return 333
def insert_visual_review(self, **kwargs):
self.review_calls.append(kwargs)
return 444
class DecisionPacketServiceTests(unittest.TestCase):
def test_build_packet_recommends_confirm_ticket_when_risk_passes(self) -> None:
packet = build_decision_packet_from_rows(
instrument_id=1,
observed_ts=datetime(2026, 5, 8, 1, 0, tzinfo=timezone.utc),
plan={
"id": 10,
"status": "active",
"narrative": "active:bullish",
"primary_draw": {"source": "liquidity_pool", "price_high": 101},
"bias": "bullish",
"invalidations": [{"code": "range_low_lost"}],
},
candidate={
"id": 20,
"trading_plan_id": 10,
"model_code": "OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE",
"status": "executable",
"entry_low": 100,
"entry_high": 101,
"stop_loss": 99,
"tp1": 102,
"tp2": 104,
"quality_score": 0.8,
"confirmation_state": {"present_conditions": ["sweep", "mss", "fvg"], "reason_codes": ["confirmed"]},
"missing_confirmations": [],
"management_rules": [{"rule": "tp1_partial"}],
"entry": {"target_plan": {"tp1": {"source": "irl"}, "tp2": {"source": "erl"}}},
},
signal={"id": 30, "entry_type": "limit", "target_plan": {"tp1": {"source": "irl"}, "tp2": {"source": "erl"}}},
ticket={"id": 40, "status": "awaiting_confirmation"},
risk_result={"allowed": True, "status": "allowed", "reasons": []},
)
self.assertEqual(packet.status, "actionable")
self.assertEqual(packet.action_recommendation, "confirm_ticket")
self.assertEqual(packet.targets["tp1"]["source"], "irl")
self.assertIn("risk_governor_passed", packet.why_allowed)
def test_build_packet_explains_missing_confirmations(self) -> None:
packet = build_decision_packet_from_rows(
instrument_id=1,
observed_ts=datetime(2026, 5, 8, 1, 0, tzinfo=timezone.utc),
plan={"id": 10, "status": "active", "bias": "bearish"},
candidate={
"id": 20,
"trading_plan_id": 10,
"status": "blocked",
"missing_confirmations": ["mss_confirmed"],
"confirmation_state": {"rejected_conditions": ["fvg_missing"]},
},
signal={},
ticket={},
risk_result={"allowed": None, "status": "signal_missing", "reasons": ["signal_missing"]},
)
self.assertEqual(packet.status, "blocked")
self.assertEqual(packet.action_recommendation, "wait_for_confirmations")
self.assertIn("mss_confirmed", packet.why_blocked)
self.assertIn("fvg_missing", packet.why_blocked)
self.assertIn("signal_missing", packet.why_blocked)
def test_alert_candidates_are_stable_for_actionable_packet(self) -> None:
packet = build_decision_packet_from_rows(
instrument_id=1,
observed_ts=datetime(2026, 5, 8, 1, 0, tzinfo=timezone.utc),
plan={"id": 10, "status": "active"},
candidate={"id": 20, "trading_plan_id": 10, "status": "executable", "missing_confirmations": []},
signal={"id": 30},
ticket={"id": 40, "status": "awaiting_confirmation"},
risk_result={"allowed": True, "status": "allowed", "reasons": []},
)
alerts = build_alert_candidates(packet)
self.assertEqual(alerts[0]["alert_type"], "candidate_actionable")
self.assertIn("confirm_ticket", alerts[0]["alert_key"])
def test_blacklist_model_becomes_observe_only_packet(self) -> None:
packet = build_decision_packet_from_rows(
instrument_id=1,
observed_ts=datetime(2026, 5, 8, 1, 0, tzinfo=timezone.utc),
plan={"id": 10, "status": "active", "bias": "bullish"},
candidate={"id": 20, "trading_plan_id": 10, "model_code": "SWEEP_MSS_FVG", "status": "executable"},
signal={"id": 30},
ticket={},
risk_result={"allowed": True, "status": "allowed", "reasons": []},
trust_policy={
"classification": "blacklist",
"analysis_only_allowed": True,
"decision_support_allowed": False,
"execution_allowed": False,
"operator_mode": "observe_only",
"blocking_reasons": ["trust_model_execution_disabled"],
},
)
self.assertEqual(packet.action_recommendation, "observe_only")
self.assertEqual(packet.operator_mode, "observe_only")
self.assertIn("trust_model_execution_disabled", packet.why_blocked)
def test_select_candidate_prefers_active_plan_scope(self) -> None:
selected = _select_candidate(
[
{"id": 1, "trading_plan_id": 9, "model_code": "OTHER"},
{"id": 2, "trading_plan_id": 10, "model_code": "OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE"},
],
plan_id=10,
model_code="OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE",
)
self.assertEqual(selected["id"], 2)
def test_build_persist_and_alert_attaches_visual_watch_reservation(self) -> None:
packet = build_decision_packet_from_rows(
instrument_id=1,
observed_ts=datetime(2026, 5, 8, 1, 0, tzinfo=timezone.utc),
plan={"id": 10, "status": "active"},
candidate={
"id": 20,
"trading_plan_id": 10,
"status": "executable",
"missing_confirmations": [],
"model_code": "OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE",
"evidence": {"symbol": "BTC-USDT"},
"trigger_timeframe": "5m",
},
signal={"id": 30, "trigger_timeframe": "5m"},
ticket={"id": 40, "status": "awaiting_confirmation"},
risk_result={"allowed": True, "status": "allowed", "reasons": []},
trust_policy={
"classification": "allowlist",
"analysis_only_allowed": True,
"restricted_decision_support_allowed": True,
"decision_support_allowed": True,
"execution_allowed": True,
"operator_mode": "execution_assist",
"blocking_reasons": [],
},
)
class StubDecisionRepository:
def __init__(self):
self.alert_payloads = []
def insert_decision_packet(self, **kwargs):
self.packet_payload = kwargs["payload"]
return 901
def insert_decision_alert(self, **kwargs):
self.alert_payloads.append(kwargs)
return 902
class StubVisualWatchService:
def __init__(self):
self.requests = []
def reserve_watch_request(self, request):
self.requests.append(dict(request))
return {
"status": "reserved",
"watch_run_id": 777,
"trigger_type": request["trigger_type"],
"reserved_ts": "2026-05-08T01:00:00+00:00",
}
repository = StubDecisionRepository()
visual_watch_service = StubVisualWatchService()
service = DecisionPacketService(decision_repository=repository)
with patch.object(service, "build_packet", return_value=packet), patch(
"src.services.decision.decision_packet_service.VisualWatchService",
return_value=visual_watch_service,
):
result = service.build_persist_and_alert(instrument_id=1)
self.assertEqual(result["id"], 901)
self.assertEqual(len(result["alerts"]), 1)
alert_payload = result["alerts"][0]["payload"]
self.assertEqual(alert_payload["visual_watch_reservation"]["watch_run_id"], 777)
self.assertEqual(alert_payload["visual_trigger_policy"]["status"], "triggered")
self.assertEqual(alert_payload["visual_trigger_roi"]["expected_review_value"], "high")
self.assertEqual(visual_watch_service.requests[0]["decision_packet_id"], 901)
self.assertEqual(visual_watch_service.requests[0]["signal_id"], 30)
self.assertEqual(visual_watch_service.requests[0]["visual_trigger_policy"]["primary_trigger_type"], "candidate_actionable_watch")
self.assertEqual(repository.alert_payloads[0]["payload"]["visual_watch_reservation"]["status"], "reserved")
def test_record_snapshot_returns_snapshot_payload(self) -> None:
from src.services.visual_watch.visual_watch_service import VisualWatchService
repository = StubVisualSnapshotRepository()
service = VisualWatchService(repository=repository)
result = service.record_snapshot(
{
"watch_run_id": 44,
"image_path": "captures/btc-1.png",
"source_name": "capture_bot",
"captured_ts": "2026-05-10T12:30:00+00:00",
"status": "captured",
"payload": {"screen": "main"},
}
)
self.assertEqual(result["snapshot_id"], 333)
self.assertEqual(result["watch_run_id"], 44)
self.assertEqual(result["image_path"], "captures/btc-1.png")
self.assertEqual(repository.snapshot_calls[0]["source_name"], "capture_bot")
self.assertEqual(repository.snapshot_calls[0]["status"], "captured")
def test_record_visual_review_returns_review_payload(self) -> None:
from src.services.visual_watch.visual_watch_service import VisualWatchService
repository = StubVisualSnapshotRepository()
service = VisualWatchService(repository=repository)
result = service.record_visual_review(
{
"watch_run_id": 44,
"snapshot_id": 333,
"visual_state": "aligned",
"visual_confidence": 0.91,
"visual_notes": "structure intact",
"ui_anomalies": ["minor_overlay"],
"reviewed_ts": "2026-05-10T12:35:00+00:00",
"payload": {"operator": "vision_bot"},
}
)
self.assertEqual(result["review_id"], 444)
self.assertEqual(result["watch_run_id"], 44)
self.assertEqual(result["snapshot_id"], 333)
self.assertEqual(result["visual_state"], "aligned")
self.assertEqual(repository.review_calls[0]["visual_notes"], "structure intact")
self.assertEqual(repository.review_calls[0]["ui_anomalies"], ["minor_overlay"])
if __name__ == "__main__":
unittest.main()