"""Phase-0 gate: the output contract validates and rejects garbage.""" from __future__ import annotations from datetime import datetime, timezone import pytest from pydantic import ValidationError from agents.schemas import AgentOutput, AgentStatus, Direction, Pick, RunManifest def _pick(**kw) -> Pick: base = dict(ticker="nvda", conviction=0.8, thesis="momentum", requested_weight_pct=0.05) base.update(kw) return Pick(**base) def test_pick_valid_and_ticker_uppercased(): p = _pick() assert p.ticker == "NVDA" assert p.direction == Direction.LONG def test_conviction_out_of_range_rejected(): with pytest.raises(ValidationError): _pick(conviction=1.5) def test_weight_must_be_positive_fraction(): with pytest.raises(ValidationError): _pick(requested_weight_pct=0.0) with pytest.raises(ValidationError): _pick(requested_weight_pct=1.5) def test_stop_take_pct_must_be_fraction(): with pytest.raises(ValidationError): _pick(stop_loss_pct=7.0) # 700% — clearly a mistake assert _pick(stop_loss_pct=0.07).stop_loss_pct == 0.07 def test_long_absolute_levels_ordering_enforced(): with pytest.raises(ValidationError): _pick(entry_type="limit", limit_price=100.0, stop_price=105.0) # stop above entry with pytest.raises(ValidationError): _pick(entry_type="limit", limit_price=100.0, take_price=95.0) # take below entry ok = _pick(entry_type="limit", limit_price=100.0, stop_price=93.0, take_price=120.0) assert ok.limit_price == 100.0 def test_agent_output_roundtrip(): out = AgentOutput( agent_name="Nova", persona_id="nova", started_at=datetime.now(timezone.utc), status=AgentStatus.COMPLETE, picks=[_pick(), _pick(ticker="amd", conviction=0.6)], ) restored = AgentOutput.model_validate_json(out.model_dump_json()) assert restored.agent_name == "Nova" assert [p.ticker for p in restored.picks] == ["NVDA", "AMD"] def test_run_manifest_minimal(): m = RunManifest( run_id="r1", trade_date="2026-07-01", model="gpt-oss-20b", started_at=datetime.now(timezone.utc), ) assert m.status == "running" assert m.agents == [] def test_option_pick_rejects_fabricated_occ_symbol(): import pytest as _pytest from agents.schemas import Pick bad = dict( ticker="JPM", conviction=0.8, thesis="t", requested_weight_pct=0.05, instrument="option", option_right="call", ) # missing underlying root (a real LLM fabrication seen in testing) with _pytest.raises(Exception): Pick(**bad, contract_symbol="20270119C0035000") # 6-digit strike instead of 8 (another real fabrication) with _pytest.raises(Exception): Pick(**bad, contract_symbol="PFE260729C002500") # valid OCC symbol passes and is normalized to uppercase p = Pick(**bad, contract_symbol="jpm270115c00300000") assert p.contract_symbol == "JPM270115C00300000" def test_option_direction_normalization(): import pytest as _pytest from agents.schemas import Direction, Pick base = dict( ticker="JNJ", conviction=0.7, thesis="bearish", requested_weight_pct=0.05, instrument="option", contract_symbol="JNJ260715P00155000", ) # short + put -> normalized to a long put (same economic view, buy-to-open) p = Pick(**base, option_right="put", direction="short") assert p.direction == Direction.LONG # short + call has no long-only equivalent -> rejected with _pytest.raises(Exception): Pick(**{**base, "contract_symbol": "JNJ260715C00155000"}, option_right="call", direction="short")