SimpleChatbot / hermes_overlay /tests /test_expires_at_normalization.py
Amin
fix(futures): stop 422 on NO_TRADE plans, harden expires_at parsing
15b2eec
Raw
History Blame Contribute Delete
3.02 kB
"""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
@pytest.mark.parametrize(
"expires_at",
[
None,
"",
"not-a-timestamp",
"2026-13-40T99:99:99Z",
],
)
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)
@pytest.mark.parametrize(
"expires_at",
[
"2099-01-01T00:00:00Z", # canonical form this service emits
"2099-01-01T00:00:00+00:00", # explicit UTC offset
"2099-01-01T00:00:00.123456Z", # fractional seconds
"2099-01-01T05:30:00+05:30", # non-UTC offset, must normalize
"2099-01-01T00:00:00", # naive -- treated as UTC
],
)
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"