"""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 == []