Spaces:
Sleeping
Sleeping
| """Regression test for the /api/futures/analyze 422: | |
| "Trade plan validation failed" -> PlanValidationError: | |
| "expires_at must be a valid UTC timestamp" | |
| Root cause: trade_cycle._empty_plan() (used for every NO_TRADE / rejected | |
| analysis) set expires_at=None, while plans_repo.validate_plan() requires | |
| expires_at on every persisted plan -- including NO_TRADE ones -- before its | |
| NO_TRADE early return. Any rejected analysis therefore failed plan | |
| persistence with a 422. | |
| """ | |
| from __future__ import annotations | |
| import time | |
| import pytest | |
| from trading.domain.plans_repo import PlanValidationError, validate_plan | |
| from trading.trade_cycle import _empty_plan, _iso | |
| def _base_no_trade_plan(**overrides): | |
| plan = _empty_plan( | |
| symbol="BTCUSDT", | |
| decision="NO_TRADE", | |
| reasons=["Order book unavailable"], | |
| warnings=[], | |
| components={}, | |
| external_context={}, | |
| created_at=time.time(), | |
| risk_profile="moderate", | |
| ) | |
| # _empty_plan() doesn't set the market-context fields that | |
| # _attach_context_metadata() normally adds; fill in the rest of | |
| # REQUIRED_PLAN_FIELDS so we're testing expires_at in isolation. | |
| plan.setdefault("futuresVerified", False) | |
| plan.setdefault("trading_readiness", "blocked") | |
| plan.update(overrides) | |
| return plan | |
| def test_no_trade_plan_no_longer_ships_expires_at_none(): | |
| """This is the exact shape that used to 422 in production.""" | |
| plan = _base_no_trade_plan() | |
| assert plan["expires_at"] is not None | |
| validate_plan(plan) # must not raise | |
| def test_no_trade_plan_expiry_is_canonical_utc_and_after_creation(): | |
| plan = _base_no_trade_plan() | |
| assert plan["expires_at"].endswith("Z") | |
| parsed = time.strptime(plan["expires_at"], "%Y-%m-%dT%H:%M:%SZ") | |
| created = time.strptime(plan["created_at"], "%Y-%m-%dT%H:%M:%SZ") | |
| assert parsed >= created | |
| def test_missing_or_malformed_expires_at_still_rejected(expires_at): | |
| """The fix must not silently accept genuinely invalid input.""" | |
| plan = _base_no_trade_plan(expires_at=expires_at) | |
| with pytest.raises(PlanValidationError, match="expires_at"): | |
| validate_plan(plan) | |
| def test_general_iso8601_variants_are_accepted(expires_at): | |
| plan = _base_no_trade_plan(expires_at=expires_at) | |
| validate_plan(plan) # must not raise for any of these | |
| def test_iso_helper_output_matches_required_canonical_format(): | |
| ts = _iso(1900000000.0) | |
| assert ts == "2030-03-17T17:46:40Z" | |