188 lines
7.9 KiB
Python
188 lines
7.9 KiB
Python
import unittest
|
|
|
|
from src.services.visual_watch import (
|
|
VISUAL_TRIGGER_CANDIDATE_ACTIONABLE_WATCH,
|
|
VISUAL_TRIGGER_MARKET_ANOMALY_REVIEW,
|
|
)
|
|
from src.services.visual_watch.visual_review_service import VisualReviewService
|
|
|
|
|
|
class StubVisualSnapshotRepository:
|
|
def __init__(self) -> None:
|
|
self.watch_runs = {}
|
|
self.snapshots = {}
|
|
self.latest_snapshots_by_candidate = {}
|
|
self.snapshot_calls = []
|
|
self.review_calls = []
|
|
self.completed_calls = []
|
|
self.pending_watch_runs = []
|
|
|
|
def fetch_watch_run_by_id(self, watch_run_id: int):
|
|
return self.watch_runs.get(watch_run_id)
|
|
|
|
def fetch_snapshot_by_id(self, snapshot_id: int):
|
|
return self.snapshots.get(snapshot_id)
|
|
|
|
def fetch_latest_snapshot_for_candidate(self, candidate_id: int):
|
|
return self.latest_snapshots_by_candidate.get(candidate_id)
|
|
|
|
def insert_snapshot(self, **kwargs):
|
|
self.snapshot_calls.append(kwargs)
|
|
snapshot_id = len(self.snapshot_calls) + 332
|
|
self.snapshots[snapshot_id] = {
|
|
"id": snapshot_id,
|
|
"watch_run_id": kwargs["watch_run_id"],
|
|
"source_name": kwargs["source_name"],
|
|
"image_path": kwargs["image_path"],
|
|
"captured_ts": kwargs["captured_ts"],
|
|
"status": kwargs["status"],
|
|
"payload": kwargs["payload"],
|
|
}
|
|
return snapshot_id
|
|
|
|
def insert_visual_review(self, **kwargs):
|
|
self.review_calls.append(kwargs)
|
|
return len(self.review_calls) + 443
|
|
|
|
def mark_visual_watch_run_completed(self, **kwargs):
|
|
self.completed_calls.append(kwargs)
|
|
|
|
def fetch_pending_watch_runs(self, **kwargs):
|
|
return list(self.pending_watch_runs)
|
|
|
|
|
|
class StubVisualSnapshotCaptureService:
|
|
def __init__(self) -> None:
|
|
self.calls = []
|
|
|
|
def capture_watch_run_snapshot(self, *, watch_run: dict, source_name: str | None = None, output_dir=None):
|
|
self.calls.append({"watch_run": watch_run, "source_name": source_name, "output_dir": output_dir})
|
|
return {
|
|
"status": "captured",
|
|
"watch_run_id": watch_run["id"],
|
|
"instrument_id": watch_run["instrument_id"],
|
|
"source_name": source_name or "auto_visual_watch_capture",
|
|
"image_path": f"captures/watch-{watch_run['id']}.html",
|
|
"artifact_path": f"captures/watch-{watch_run['id']}.json",
|
|
"captured_ts": "2026-05-11T02:00:00+00:00",
|
|
"capture_mode": "review_packet_html",
|
|
"payload": {
|
|
"capture_mode": "review_packet_html",
|
|
"captured_ts": "2026-05-11T02:00:00+00:00",
|
|
"source_name": source_name or "auto_visual_watch_capture",
|
|
"image_path": f"captures/watch-{watch_run['id']}.html",
|
|
"artifact_path": f"captures/watch-{watch_run['id']}.json",
|
|
"watch_run": {"id": watch_run["id"]},
|
|
"review_packet_status": "captured",
|
|
"review_packet_summary": {"present": True},
|
|
"review_packet": {"decision_packet": {"id": watch_run["decision_packet_id"]}},
|
|
},
|
|
}
|
|
|
|
|
|
class VisualReviewServiceTests(unittest.TestCase):
|
|
def test_review_watch_run_with_image_path_creates_snapshot_and_review(self) -> None:
|
|
repository = StubVisualSnapshotRepository()
|
|
repository.watch_runs[11] = {
|
|
"id": 11,
|
|
"trigger_type": VISUAL_TRIGGER_CANDIDATE_ACTIONABLE_WATCH,
|
|
"source_name": "reserved_for_visual_watch",
|
|
"instrument_id": 1,
|
|
"candidate_id": 22,
|
|
"signal_id": 33,
|
|
"decision_packet_id": 44,
|
|
"symbol": "BTC-USDT",
|
|
"timeframe": "1m",
|
|
"status": "reserved",
|
|
"payload": {
|
|
"trigger_type": VISUAL_TRIGGER_CANDIDATE_ACTIONABLE_WATCH,
|
|
"visual_trigger_policy": {"status": "triggered", "reason": "high_value_actionable_candidate"},
|
|
"visual_trigger_reason": "high_value_actionable_candidate",
|
|
},
|
|
}
|
|
service = VisualReviewService(repository=repository)
|
|
|
|
result = service.review_watch_run(
|
|
watch_run_id=11,
|
|
image_path="captures/btc.png",
|
|
source_name="capture_bot",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "reviewed")
|
|
self.assertEqual(result["watch_run_id"], 11)
|
|
self.assertEqual(result["visual_state"], "confirm")
|
|
self.assertEqual(result["snapshot_id"], 333)
|
|
self.assertEqual(repository.snapshot_calls[0]["image_path"], "captures/btc.png")
|
|
self.assertEqual(repository.review_calls[0]["visual_state"], "confirm")
|
|
self.assertEqual(repository.review_calls[0]["payload"]["payload"]["trigger_policy"]["status"], "triggered")
|
|
self.assertEqual(repository.completed_calls[0]["status"], "reviewed")
|
|
|
|
def test_review_pending_watch_runs_uses_existing_snapshot(self) -> None:
|
|
repository = StubVisualSnapshotRepository()
|
|
repository.watch_runs[21] = {
|
|
"id": 21,
|
|
"trigger_type": VISUAL_TRIGGER_MARKET_ANOMALY_REVIEW,
|
|
"source_name": "reserved_for_visual_watch",
|
|
"instrument_id": 1,
|
|
"candidate_id": 55,
|
|
"signal_id": 66,
|
|
"decision_packet_id": 77,
|
|
"symbol": "ETH-USDT",
|
|
"timeframe": "5m",
|
|
"status": "reserved",
|
|
"payload": {"trigger_type": VISUAL_TRIGGER_MARKET_ANOMALY_REVIEW},
|
|
}
|
|
repository.latest_snapshots_by_candidate[55] = {
|
|
"id": 444,
|
|
"watch_run_id": 99,
|
|
"source_name": "manual_capture",
|
|
"image_path": "captures/eth.png",
|
|
"captured_ts": "2026-05-11T01:00:00+00:00",
|
|
"status": "captured",
|
|
"payload": {},
|
|
}
|
|
repository.pending_watch_runs = [repository.watch_runs[21]]
|
|
service = VisualReviewService(repository=repository)
|
|
|
|
result = service.review_pending_watch_runs(limit=1, instrument_id=1)
|
|
|
|
self.assertEqual(result["status"], "completed")
|
|
self.assertEqual(result["attempted"], 1)
|
|
self.assertEqual(result["reviewed"], 1)
|
|
self.assertEqual(result["items"][0]["visual_state"], "mismatch")
|
|
self.assertEqual(result["items"][0]["snapshot_id"], 444)
|
|
self.assertEqual(result["trigger_type_counts"][VISUAL_TRIGGER_MARKET_ANOMALY_REVIEW], 1)
|
|
self.assertEqual(result["review_state_counts"]["mismatch"], 1)
|
|
self.assertEqual(repository.completed_calls[0]["payload"]["review_status"], "reviewed")
|
|
|
|
def test_review_watch_run_auto_captures_when_no_snapshot_exists(self) -> None:
|
|
repository = StubVisualSnapshotRepository()
|
|
repository.watch_runs[31] = {
|
|
"id": 31,
|
|
"trigger_type": VISUAL_TRIGGER_CANDIDATE_ACTIONABLE_WATCH,
|
|
"source_name": "reserved_for_visual_watch",
|
|
"instrument_id": 1,
|
|
"candidate_id": None,
|
|
"signal_id": 88,
|
|
"decision_packet_id": 99,
|
|
"symbol": "SOL-USDT",
|
|
"timeframe": "1m",
|
|
"status": "reserved",
|
|
"payload": {"trigger_type": VISUAL_TRIGGER_CANDIDATE_ACTIONABLE_WATCH},
|
|
}
|
|
capture_service = StubVisualSnapshotCaptureService()
|
|
service = VisualReviewService(repository=repository, snapshot_capture_service=capture_service)
|
|
|
|
result = service.review_watch_run(watch_run_id=31)
|
|
|
|
self.assertEqual(result["status"], "reviewed")
|
|
self.assertEqual(result["snapshot_image_path"], "captures/watch-31.html")
|
|
self.assertEqual(result["snapshot_artifact_path"], "captures/watch-31.json")
|
|
self.assertEqual(capture_service.calls[0]["source_name"], "auto_visual_watch_capture")
|
|
self.assertEqual(repository.snapshot_calls[0]["source_name"], "auto_visual_watch_capture")
|
|
self.assertEqual(repository.snapshot_calls[0]["payload"]["payload"]["capture_mode"], "review_packet_html")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|