# tests/test_environment.py """Tests for AquaGuardEnvironment — reset, step, state cycle.""" import sys import os import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from models import AquaGuardAction, AquaGuardObservation, AquaGuardState from server.aquaguard_environment import AquaGuardEnvironment class TestReset: """Tests for the reset() method.""" def test_reset_returns_observation(self): """reset() must return an AquaGuardObservation.""" env = AquaGuardEnvironment() obs = env.reset(task="baseline", seed=42) assert isinstance(obs, AquaGuardObservation) def test_reset_step_number_is_zero(self): """Initial observation should have step_number=0.""" env = AquaGuardEnvironment() obs = env.reset() assert obs.step_number == 0 def test_reset_reward_is_none(self): """Initial reward should be None (not yet computed).""" env = AquaGuardEnvironment() obs = env.reset() assert obs.reward is None def test_reset_done_is_false(self): """Episode should not be done at start.""" env = AquaGuardEnvironment() obs = env.reset() assert obs.done is False def test_reset_has_three_zones(self): """Observation must have 3 zones.""" env = AquaGuardEnvironment() obs = env.reset() assert len(obs.zones) == 3 assert "zone_a" in obs.zones assert "zone_b" in obs.zones assert "zone_c" in obs.zones def test_reset_has_six_crops(self): """Observation must have 6 crops.""" env = AquaGuardEnvironment() obs = env.reset() assert len(obs.crops) == 6 def test_reset_has_scenario_description(self): """Observation must have a non-empty scenario description.""" env = AquaGuardEnvironment() obs = env.reset() assert len(obs.scenario_description) > 50 def test_reset_season_valid(self): """Season must be one of kharif/rabi/zaid.""" env = AquaGuardEnvironment() obs = env.reset() assert obs.season in ["kharif", "rabi", "zaid"] def test_reset_reproducible_with_seed(self): """Same seed should produce same initial state.""" env1 = AquaGuardEnvironment() env2 = AquaGuardEnvironment() obs1 = env1.reset(task="baseline", seed=42) obs2 = env2.reset(task="baseline", seed=42) assert abs(obs1.shared_aquifer_level_m - obs2.shared_aquifer_level_m) < 0.01 def test_reset_all_tasks(self): """All 5 tasks should reset without errors.""" env = AquaGuardEnvironment() for task in ["baseline", "crisis", "policy_shift", "climate_shock", "multi_district"]: obs = env.reset(task=task, seed=42) assert obs is not None assert obs.task_name == task def test_crisis_task_starts_with_deep_aquifer(self): """Crisis task should start with deeper aquifer than baseline.""" env = AquaGuardEnvironment() obs_baseline = env.reset(task="baseline", seed=42) gw_baseline = obs_baseline.shared_aquifer_level_m obs_crisis = env.reset(task="crisis", seed=42) gw_crisis = obs_crisis.shared_aquifer_level_m assert gw_crisis > gw_baseline class TestStep: """Tests for the step() method.""" def test_step_returns_observation(self): """step() must return AquaGuardObservation.""" env = AquaGuardEnvironment() env.reset(task="baseline", seed=42) obs = env.step(AquaGuardAction()) assert isinstance(obs, AquaGuardObservation) def test_step_increments_step_number(self): """step_number should increment after each step.""" env = AquaGuardEnvironment() env.reset() for i in range(1, 4): obs = env.step(AquaGuardAction()) assert obs.step_number == i def test_step_returns_reward(self): """reward must be numeric and in [-10, 10].""" env = AquaGuardEnvironment() env.reset() obs = env.step(AquaGuardAction()) assert obs.reward is not None assert isinstance(obs.reward, (int, float)) assert -10.0 <= obs.reward <= 10.0 def test_episode_terminates_at_max_steps(self): """Episode must terminate at max_steps.""" env = AquaGuardEnvironment() obs = env.reset(task="baseline") max_steps = env._max_steps for _ in range(max_steps): if obs.done: break obs = env.step(AquaGuardAction()) assert obs.done is True def test_step_before_reset_raises(self): """step() before reset() should raise RuntimeError.""" env = AquaGuardEnvironment() with pytest.raises(RuntimeError): env.step(AquaGuardAction()) def test_multiple_resets(self): """Environment can be reset multiple times.""" env = AquaGuardEnvironment() for seed in range(3): obs = env.reset(task="baseline", seed=seed) obs = env.step(AquaGuardAction()) assert obs.step_number == 1 def test_conservative_policy_better_gw(self): """Conservative water policy should maintain better GW than wasteful one.""" # Conservative action env1 = AquaGuardEnvironment() env1.reset(task="baseline", seed=42) conservative = AquaGuardAction( water_quotas={"zone_a": 500, "zone_b": 450, "zone_c": 400}, extraction_limits={"zone_a": 10.0, "zone_b": 8.0, "zone_c": 6.0}, crop_allocation={"rice": 0.15, "wheat": 0.20, "millet": 0.30, "pulses": 0.20, "oilseeds": 0.10, "vegetables": 0.05}, ) env1.step(conservative) gw_conservative = env1._zone_states["zone_a"]["gw_depth_m"] # Wasteful action env2 = AquaGuardEnvironment() env2.reset(task="baseline", seed=42) wasteful = AquaGuardAction( water_quotas={"zone_a": 1800, "zone_b": 1800, "zone_c": 1800}, extraction_limits={"zone_a": 55.0, "zone_b": 55.0, "zone_c": 55.0}, crop_allocation={"rice": 0.50, "wheat": 0.30, "millet": 0.05, "pulses": 0.05, "oilseeds": 0.05, "vegetables": 0.05}, ) env2.step(wasteful) gw_wasteful = env2._zone_states["zone_a"]["gw_depth_m"] assert gw_conservative < gw_wasteful class TestState: """Tests for the state property.""" def test_state_returns_aquaguard_state(self): """state property must return AquaGuardState.""" env = AquaGuardEnvironment() env.reset() state = env.state assert isinstance(state, AquaGuardState) def test_state_has_episode_id(self): """state must have a non-None episode_id after reset.""" env = AquaGuardEnvironment() env.reset() state = env.state assert state.episode_id is not None def test_state_step_count_increments(self): """step_count must increment after each step.""" env = AquaGuardEnvironment() env.reset() for i in range(3): env.step(AquaGuardAction()) assert env.state.step_count == i + 1 def test_state_task_name_correct(self): """state.task_name must match the task passed to reset.""" env = AquaGuardEnvironment() for task in ["baseline", "crisis", "policy_shift"]: env.reset(task=task) assert env.state.task_name == task def test_state_cumulative_reward_accumulates(self): """cumulative_reward must increase with steps.""" env = AquaGuardEnvironment() env.reset(task="baseline", seed=42) for _ in range(5): env.step(AquaGuardAction()) state = env.state # cumulative reward can be negative, but step_count should be 5 assert state.step_count == 5