"""Tests for scripts.vision_schema.M2DExtraction.""" from __future__ import annotations import json import sys from datetime import datetime, timezone from pathlib import Path import pytest from pydantic import ValidationError sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from scripts.vision_schema import ( # noqa: E402 M2DExtraction, parse_extraction, parse_extraction_dict, ) def _buy_payload(**overrides) -> dict: base = { "screenshot_file": "dia-1min-example.png", "data": "2026-05-13", "ora_utc": "14:23", "instrument": "DIA", "directie": "Buy", "tf_mare": "5min", "tf_mic": "1min", "calitate": "Clară", "entry": 400.0, "sl": 399.0, "tp0": 400.5, "tp1": 401.0, "tp2": 402.0, "risc_pct": 0.25, "outcome_path": "TP0→TP1", "max_reached": "TP1", "be_moved": True, "confidence": "high", "ambiguities": [], "note": "", } base.update(overrides) return base def _sell_payload(**overrides) -> dict: base = { "screenshot_file": "dia-sell.png", "data": "2026-05-13", "ora_utc": "15:00", "instrument": "US30", "directie": "Sell", "tf_mare": "15min", "tf_mic": "3min", "calitate": "Mai mare ca impuls", "entry": 400.0, "sl": 401.0, "tp0": 399.5, "tp1": 399.0, "tp2": 398.0, "risc_pct": 0.3, "outcome_path": "TP0→TP2", "max_reached": "TP2", "be_moved": False, "confidence": "medium", "ambiguities": ["entry overlap with wick"], "note": "nothing", } base.update(overrides) return base def test_happy_path_buy(): m = parse_extraction_dict(_buy_payload()) assert m.directie == "Buy" assert m.entry == 400.0 def test_happy_path_sell(): m = parse_extraction_dict(_sell_payload()) assert m.directie == "Sell" assert m.sl > m.entry > m.tp0 > m.tp1 > m.tp2 def test_parse_extraction_from_json_str(): payload = _buy_payload() m = parse_extraction(json.dumps(payload)) assert isinstance(m, M2DExtraction) @pytest.mark.parametrize( "field,bad_value", [ ("directie", "Long"), ("instrument", "SPY"), ("tf_mare", "30min"), ("tf_mic", "2min"), ("calitate", "Bună"), ("outcome_path", "BE"), ("max_reached", "BE"), ("confidence", "very-high"), ], ) def test_each_literal_rejection(field, bad_value): payload = _buy_payload(**{field: bad_value}) with pytest.raises(ValidationError): parse_extraction_dict(payload) def test_entry_equals_sl(): with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(entry=399.0, sl=399.0)) def test_buy_tp_inverted(): # tp1 < tp0 violates ordering with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(tp0=401.0, tp1=400.5, tp2=402.0)) def test_buy_sl_above_entry_rejected(): with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(sl=400.5)) def test_sell_order_correct(): m = parse_extraction_dict(_sell_payload()) assert m.sl > m.entry assert m.entry > m.tp0 assert m.tp0 > m.tp1 > m.tp2 def test_sell_order_inverted_rejected(): # using Buy-ordering values with directie=Sell with pytest.raises(ValidationError): parse_extraction_dict( _sell_payload(sl=399.0, entry=400.0, tp0=400.5, tp1=401.0, tp2=402.0) ) def test_data_in_future(): with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(data="2099-01-01")) def test_data_today_ok(): today = datetime.now(timezone.utc).date().isoformat() m = parse_extraction_dict(_buy_payload(data=today)) assert m.data == today def test_outcome_path_sl_max_reached_inconsistent(): with pytest.raises(ValidationError): parse_extraction_dict( _buy_payload(outcome_path="SL", max_reached="TP1") ) def test_outcome_path_sl_max_reached_sl_first_ok(): m = parse_extraction_dict( _buy_payload(outcome_path="SL", max_reached="SL_first") ) assert m.outcome_path == "SL" def test_outcome_path_tp0_max_reached_sl_first_rejected(): with pytest.raises(ValidationError): parse_extraction_dict( _buy_payload(outcome_path="TP0→TP1", max_reached="SL_first") ) def test_outcome_path_pending_any_max_reached_ok(): m = parse_extraction_dict( _buy_payload(outcome_path="pending", max_reached="SL_first") ) assert m.outcome_path == "pending" def test_extra_field_forbidden(): payload = _buy_payload() payload["unexpected_field"] = "x" with pytest.raises(ValidationError): parse_extraction_dict(payload) def test_data_bad_format(): with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(data="2026/05/13")) def test_data_bad_format_short(): with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(data="26-05-13")) def test_ora_utc_bad_format(): with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(ora_utc="14:23:00")) def test_ora_utc_bad_format_no_colon(): with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(ora_utc="1423")) def test_ora_utc_invalid_hour(): with pytest.raises(ValidationError): parse_extraction_dict(_buy_payload(ora_utc="25:00")) def test_ambiguities_default_empty(): payload = _buy_payload() del payload["ambiguities"] m = parse_extraction_dict(payload) assert m.ambiguities == [] def test_note_default_empty(): payload = _buy_payload() del payload["note"] m = parse_extraction_dict(payload) assert m.note == ""