| """ |
| tests/test_reset.py |
| =================== |
| Phase 10 β 10 tests covering UnifiedFintechEnv.reset() contract per CLAUDE.md. |
| """ |
| import pytest |
|
|
| from unified_gateway import AEPOAction, AEPOObservation, UnifiedFintechEnv |
|
|
|
|
| |
| |
| |
|
|
| 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) |
|
|
|
|
| |
| |
| |
|
|
| 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]" |
|
|
|
|
| |
| |
| |
|
|
| 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]" |
|
|
|
|
| |
| |
| |
|
|
| 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]" |
|
|
|
|
| |
| |
| |
|
|
| 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']}" |
|
|
|
|
| |
| |
| |
|
|
| 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']}" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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"}) |
|
|
|
|
| |
| |
| |
|
|
| 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"}) |
|
|
| |
| env.step(AEPOAction(risk_decision=0, crypto_verify=1, infra_routing=1, |
| db_retry_policy=0, settlement_policy=0, app_priority=2)) |
|
|
| |
| env.reset(options={"task": "easy"}) |
| assert len(env._throttle_relief_queue) == 0, ( |
| "_throttle_relief_queue must be empty after reset()" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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"}) |
|
|
| |
| 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}" |
|
|
|
|
| |
| |
| |
|
|
| 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" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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"}) |
|
|
| |
| 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 |
| |
| 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" |
| ) |
|
|