unknown1321's picture
Clean deploy to HF Space
e9ce6e9
Raw
History Blame Contribute Delete
7.36 kB
"""
tests/test_reset.py
===================
Phase 10 β€” 10 tests covering UnifiedFintechEnv.reset() contract per CLAUDE.md.
"""
import pytest
from unified_gateway import AEPOAction, AEPOObservation, UnifiedFintechEnv
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
def make_action(**kwargs) -> AEPOAction:
"""Safe default action."""
defaults = dict(risk_decision=0, crypto_verify=0, infra_routing=0,
db_retry_policy=0, settlement_policy=0, app_priority=2)
defaults.update(kwargs)
return AEPOAction(**defaults)
# ---------------------------------------------------------------------------
# Test 1 β€” reset("easy") returns valid AEPOObservation
# ---------------------------------------------------------------------------
def test_reset_easy_returns_valid_observation() -> None:
"""reset("easy") must return an AEPOObservation with all fields in valid ranges."""
env = UnifiedFintechEnv()
obs, _ = env.reset(options={"task": "easy"})
assert isinstance(obs, AEPOObservation)
norm = obs.normalized()
for key, val in norm.items():
assert 0.0 <= val <= 1.0, f"{key}={val} out of [0,1]"
# ---------------------------------------------------------------------------
# Test 2 β€” reset("medium") returns valid AEPOObservation
# ---------------------------------------------------------------------------
def test_reset_medium_returns_valid_observation() -> None:
"""reset("medium") must return an AEPOObservation with all fields in valid ranges."""
env = UnifiedFintechEnv()
obs, _ = env.reset(options={"task": "medium"})
assert isinstance(obs, AEPOObservation)
norm = obs.normalized()
for key, val in norm.items():
assert 0.0 <= val <= 1.0, f"{key}={val} out of [0,1]"
# ---------------------------------------------------------------------------
# Test 3 β€” reset("hard") returns valid AEPOObservation
# ---------------------------------------------------------------------------
def test_reset_hard_returns_valid_observation() -> None:
"""reset("hard") must return an AEPOObservation with all fields in valid ranges."""
env = UnifiedFintechEnv()
obs, _ = env.reset(options={"task": "hard"})
assert isinstance(obs, AEPOObservation)
norm = obs.normalized()
for key, val in norm.items():
assert 0.0 <= val <= 1.0, f"{key}={val} out of [0,1]"
# ---------------------------------------------------------------------------
# Test 4 β€” reset("easy") initialises phase to "normal"
# ---------------------------------------------------------------------------
def test_reset_easy_initialises_phase_normal() -> None:
"""easy task must start in normal phase."""
env = UnifiedFintechEnv()
env.reset(options={"task": "easy"})
_, _, _, info = env.step(make_action())
assert info["phase"] == "normal", f"Expected 'normal', got {info['phase']}"
# ---------------------------------------------------------------------------
# Test 5 β€” reset("hard") initialises phase to "normal" (first phase)
# ---------------------------------------------------------------------------
def test_reset_hard_initialises_phase_normal() -> None:
"""hard task must ALSO start in normal phase (first phase of 4-phase sequence)."""
env = UnifiedFintechEnv()
env.reset(options={"task": "hard"})
_, _, _, info = env.step(make_action())
assert info["phase"] == "normal", (
f"hard task should start in normal phase, got {info['phase']}"
)
# ---------------------------------------------------------------------------
# Test 6 β€” reset() with invalid task name raises ValueError
# ---------------------------------------------------------------------------
def test_reset_invalid_task_raises() -> None:
"""reset() with an unrecognised task name must raise ValueError."""
env = UnifiedFintechEnv()
with pytest.raises((ValueError, KeyError, Exception)):
env.reset(options={"task": "impossible_task"})
# ---------------------------------------------------------------------------
# Test 7 β€” reset() clears _throttle_relief_queue accumulator
# ---------------------------------------------------------------------------
def test_reset_clears_throttle_relief_queue() -> None:
"""
BOUNDARY RULE: _throttle_relief_queue must be cleared on reset().
Otherwise lag relief from a previous episode bleeds into the first steps
of the next episode.
"""
env = UnifiedFintechEnv()
env.reset(options={"task": "easy"})
# Issue a throttle action to queue relief items
env.step(AEPOAction(risk_decision=0, crypto_verify=1, infra_routing=1,
db_retry_policy=0, settlement_policy=0, app_priority=2))
# Reset should clear the queue
env.reset(options={"task": "easy"})
assert len(env._throttle_relief_queue) == 0, (
"_throttle_relief_queue must be empty after reset()"
)
# ---------------------------------------------------------------------------
# Test 8 β€” reset() sets step_in_episode to 0
# ---------------------------------------------------------------------------
def test_reset_sets_step_to_zero() -> None:
"""current_step (step_in_episode counter) must be 0 immediately after reset."""
env = UnifiedFintechEnv()
env.reset(options={"task": "easy"})
# Advance a few steps then reset
for _ in range(5):
env.step(make_action())
env.reset(options={"task": "easy"})
assert env.current_step == 0, f"Expected 0 after reset, got {env.current_step}"
# ---------------------------------------------------------------------------
# Test 9 β€” two reset() calls produce deterministic obs with the same seed
# ---------------------------------------------------------------------------
def test_reset_deterministic_with_same_seed() -> None:
"""reset(seed=X) must produce the same initial observation when called twice."""
env = UnifiedFintechEnv()
obs1, _ = env.reset(seed=42, options={"task": "hard"})
obs2, _ = env.reset(seed=42, options={"task": "hard"})
assert obs1.model_dump() == obs2.model_dump(), (
"Same seed must yield identical initial observations"
)
# ---------------------------------------------------------------------------
# Test 10 β€” curriculum_level resets to 0 on env INIT, NOT on episode reset
# ---------------------------------------------------------------------------
def test_curriculum_level_not_reset_on_episode_reset() -> None:
"""
curriculum_level must NOT be reset on episode reset() β€” it persists
across episodes within the same env instance. Only __init__ sets it to 0.
"""
env = UnifiedFintechEnv()
env.reset(options={"task": "easy"})
# Inject 5 winning episodes to advance curriculum
for _ in range(5):
env._episode_step_rewards = [0.80] * env.max_steps
env._close_episode()
env._episode_step_rewards = []
level_before = env._curriculum_level
# Episode reset should NOT wipe curriculum_level
env.reset(options={"task": "easy"})
assert env._curriculum_level == level_before, (
f"curriculum_level changed from {level_before} to {env._curriculum_level} on reset β€” must not regress"
)