Spaces:
Sleeping
Sleeping
| """ | |
| Tests for engine/environment.py β Core environment logic. | |
| """ | |
| import pytest | |
| import sys | |
| import os | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from models import Action, ActionType | |
| from engine.environment import CloudFinOpsEnvironment | |
| from collections import namedtuple | |
| # Dummy reward for backwards compatibility in tests | |
| DummyReward = namedtuple("DummyReward", ["value", "message", "is_valid"]) | |
| class _TestEnvWrapper: | |
| """Wraps the fully compliant OpenEnv to behave like our old local tuple version strictly for testing.""" | |
| def __init__(self): | |
| self._env = CloudFinOpsEnvironment() | |
| def __getattr__(self, name): | |
| return getattr(self._env, name) | |
| def step(self, action): | |
| obs = self._env.step(action) | |
| # Re-pack the Observation properties back into the 4-part tuple the tests expect | |
| reward_obj = DummyReward(obs.reward, obs.message, True) | |
| return obs, reward_obj, obs.done, getattr(obs, "metadata", {}) | |
| def env(): | |
| return _TestEnvWrapper() | |
| def easy_env(env): | |
| env.reset(task_id="easy_orphan_cleanup") | |
| return env | |
| def medium_env(env): | |
| env.reset(task_id="medium_rightsize") | |
| return env | |
| def hard_env(env): | |
| env.reset(task_id="hard_dependency_migration") | |
| return env | |
| # βββ Reset Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestReset: | |
| def test_reset_returns_observation(self, env): | |
| obs = env.reset(task_id="easy_orphan_cleanup") | |
| assert obs is not None | |
| assert obs.step_number == 0 | |
| assert len(obs.resources) == 10 | |
| def test_reset_medium(self, env): | |
| obs = env.reset(task_id="medium_rightsize") | |
| assert len(obs.resources) == 20 | |
| assert obs.budget_target == 3800.0 | |
| def test_reset_hard(self, env): | |
| obs = env.reset(task_id="hard_dependency_migration") | |
| assert len(obs.resources) == 35 | |
| assert obs.maintenance_window == "02:00-06:00 UTC" | |
| def test_metrics_hidden_after_reset(self, easy_env): | |
| obs = easy_env.reset(task_id="easy_orphan_cleanup") | |
| for r in obs.resources: | |
| assert r.metrics is None, f"Metrics should be hidden for {r.resource_id}" | |
| def test_reset_invalid_task(self, env): | |
| with pytest.raises(ValueError): | |
| env.reset(task_id="nonexistent") | |
| def test_reset_all_tasks(self, env, task_id): | |
| obs = env.reset(task_id=task_id) | |
| assert obs.task_description != "" | |
| assert obs.total_monthly_cost > 0 | |
| # βββ Query Metrics Tests βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestQueryMetrics: | |
| def test_query_reveals_metrics(self, easy_env): | |
| action = Action(action_type=ActionType.QUERY_METRICS, resource_id="vol-0a1b2c3d4e5f60001") | |
| obs, reward, done, info = easy_env.step(action) | |
| # Find the resource in observation | |
| vol = next(r for r in obs.resources if r.resource_id == "vol-0a1b2c3d4e5f60001") | |
| assert vol.metrics is not None | |
| assert vol.metrics.cpu_avg_7d == 0.0 | |
| def test_query_metrics_reward(self, easy_env): | |
| action = Action(action_type=ActionType.QUERY_METRICS, resource_id="vol-0a1b2c3d4e5f60001") | |
| obs, reward, done, info = easy_env.step(action) | |
| assert reward.value < 0 # small negative cost | |
| assert done is False | |
| def test_query_invalid_resource(self, easy_env): | |
| action = Action(action_type=ActionType.QUERY_METRICS, resource_id="nonexistent") | |
| obs, reward, done, info = easy_env.step(action) | |
| assert "not found" in reward.message.lower() | |
| # βββ Delete Tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestDelete: | |
| def test_delete_orphaned_resource(self, easy_env): | |
| """Deleting an orphaned/wasteful resource should give positive reward.""" | |
| action = Action(action_type=ActionType.DELETE, resource_id="vol-0a1b2c3d4e5f60001") | |
| obs, reward, done, info = easy_env.step(action) | |
| assert reward.value > 0 # positive (savings - step_cost) | |
| assert info["cost_saved"] > 0 | |
| # Resource should be removed from observation | |
| ids = [r.resource_id for r in obs.resources] | |
| assert "vol-0a1b2c3d4e5f60001" not in ids | |
| def test_delete_production_resource_penalty(self, easy_env): | |
| """Deleting a production resource should give catastrophic penalty.""" | |
| action = Action(action_type=ActionType.DELETE, resource_id="i-0a1b2c3d4e5f60007") | |
| obs, reward, done, info = easy_env.step(action) | |
| assert reward.value < -0.5 | |
| assert len(info["safety_violations"]) > 0 | |
| assert "production" in reward.message.lower() or "critical" in reward.message.lower() | |
| def test_delete_already_deleted(self, easy_env): | |
| """Deleting the same resource twice should fail gracefully.""" | |
| action = Action(action_type=ActionType.DELETE, resource_id="vol-0a1b2c3d4e5f60001") | |
| easy_env.step(action) | |
| obs, reward, done, info = easy_env.step(action) | |
| assert "already" in reward.message.lower() or "deleted" in reward.message.lower() | |
| # βββ Resize Tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestResize: | |
| def test_resize_over_provisioned(self, medium_env): | |
| """Resizing an over-provisioned resource should give positive reward.""" | |
| action = Action( | |
| action_type=ActionType.RESIZE, | |
| resource_id="i-0b2c3d4e5f6a70004", | |
| new_size="t3.small", | |
| ) | |
| obs, reward, done, info = medium_env.step(action) | |
| assert reward.value > 0 | |
| assert info["cost_saved"] > 0 | |
| def test_resize_invalid_target(self, medium_env): | |
| """Resizing to an invalid target should fail.""" | |
| action = Action( | |
| action_type=ActionType.RESIZE, | |
| resource_id="i-0b2c3d4e5f6a70004", | |
| new_size="p4d.24xlarge", # not a valid resize target | |
| ) | |
| obs, reward, done, info = medium_env.step(action) | |
| assert "invalid" in reward.message.lower() or "valid" in reward.message.lower() | |
| def test_resize_no_size_specified(self, medium_env): | |
| """Resize without new_size should fail gracefully.""" | |
| action = Action(action_type=ActionType.RESIZE, resource_id="i-0b2c3d4e5f6a70004") | |
| obs, reward, done, info = medium_env.step(action) | |
| assert "no new_size" in reward.message.lower() or "not specified" in reward.message.lower() | |
| # βββ Stop Tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestStop: | |
| def test_stop_instance(self, easy_env): | |
| """Stopping a non-critical instance should work.""" | |
| # The stopped dev instance | |
| action = Action(action_type=ActionType.STOP, resource_id="i-0a1b2c3d4e5f60006") | |
| obs, reward, done, info = easy_env.step(action) | |
| # Already stopped β should report that | |
| assert "already stopped" in reward.message.lower() | |
| def test_stop_production_penalty(self, easy_env): | |
| """Stopping a production instance should give penalty.""" | |
| action = Action(action_type=ActionType.STOP, resource_id="i-0a1b2c3d4e5f60007") | |
| obs, reward, done, info = easy_env.step(action) | |
| assert reward.value < 0 | |
| assert len(info["safety_violations"]) > 0 | |
| # βββ Commit Tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestCommit: | |
| def test_commit_ends_episode(self, easy_env): | |
| action = Action(action_type=ActionType.COMMIT_CHANGES) | |
| obs, reward, done, info = easy_env.step(action) | |
| assert done is True | |
| def test_commit_with_savings(self, easy_env): | |
| """Delete some resources, then commit and check score.""" | |
| # Delete a few wasteful resources | |
| easy_env.step(Action(action_type=ActionType.DELETE, resource_id="vol-0a1b2c3d4e5f60001")) | |
| easy_env.step(Action(action_type=ActionType.DELETE, resource_id="vol-0a1b2c3d4e5f60002")) | |
| # Now commit | |
| obs, reward, done, info = easy_env.step(Action(action_type=ActionType.COMMIT_CHANGES)) | |
| assert done is True | |
| assert info["cost_saved"] > 0 | |
| def test_step_after_done_raises(self, easy_env): | |
| easy_env.step(Action(action_type=ActionType.COMMIT_CHANGES)) | |
| with pytest.raises(RuntimeError, match="done"): | |
| easy_env.step(Action(action_type=ActionType.COMMIT_CHANGES)) | |
| # βββ Check Dependencies Tests ββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestCheckDeps: | |
| def test_check_deps_returns_info(self, hard_env): | |
| action = Action(action_type=ActionType.CHECK_DEPS, resource_id="i-0c3d4e5f6a7b80012") | |
| obs, reward, done, info = hard_env.step(action) | |
| assert "kafka" in obs.message.lower() or "cluster" in obs.message.lower() or "depend" in obs.message.lower() | |
| def test_check_deps_cost(self, hard_env): | |
| action = Action(action_type=ActionType.CHECK_DEPS, resource_id="i-0c3d4e5f6a7b80012") | |
| obs, reward, done, info = hard_env.step(action) | |
| assert reward.value < 0 # small cost | |
| # βββ List Resources Tests ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestListResources: | |
| def test_list_resources(self, easy_env): | |
| action = Action(action_type=ActionType.LIST_RESOURCES) | |
| obs, reward, done, info = easy_env.step(action) | |
| assert len(obs.resources) == 10 | |
| assert reward.value < 0 # small cost | |
| # βββ State Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestState: | |
| def test_state_returns_full_info(self, easy_env): | |
| state = easy_env.state() | |
| assert state.task_id == "easy_orphan_cleanup" | |
| assert state.optimal_savings > 0 | |
| assert len(state.critical_resources) > 0 | |
| assert state.done is False | |
| def test_state_not_initialized_raises(self): | |
| env = CloudFinOpsEnvironment() | |
| with pytest.raises(RuntimeError, match="not initialized"): | |
| env.state() | |
| # βββ Oracle Solution Tests ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestOracleSolution: | |
| def test_easy_oracle_solution(self, env): | |
| """Running the oracle solution should achieve near-perfect score.""" | |
| from data.generator import load_solution | |
| solution = load_solution("easy_orphan_cleanup") | |
| env.reset(task_id="easy_orphan_cleanup") | |
| for step_data in solution["optimal_action_sequence"]: | |
| action = Action( | |
| action_type=step_data["action_type"], | |
| resource_id=step_data.get("resource_id"), | |
| new_size=step_data.get("new_size"), | |
| reason=step_data.get("reason"), | |
| ) | |
| obs, reward, done, info = env.step(action) | |
| assert done is True | |
| assert info["cost_saved"] > 60 # Should save ~$63/mo | |
| assert len(info["safety_violations"]) == 0 | |
| score = env.get_final_score() | |
| assert score > 0.8, f"Expected score > 0.8, got {score}" | |
| def test_medium_oracle_solution(self, env): | |
| """Running the medium oracle solution should achieve good score.""" | |
| from data.generator import load_solution | |
| solution = load_solution("medium_rightsize") | |
| env.reset(task_id="medium_rightsize") | |
| for step_data in solution["optimal_action_sequence"]: | |
| action = Action( | |
| action_type=step_data["action_type"], | |
| resource_id=step_data.get("resource_id"), | |
| new_size=step_data.get("new_size"), | |
| reason=step_data.get("reason"), | |
| ) | |
| obs, reward, done, info = env.step(action) | |
| assert done is True | |
| assert info["cost_saved"] > 900 # Should save ~$1037/mo | |
| assert len(info["safety_violations"]) == 0 | |
| def test_hard_oracle_solution(self, env): | |
| """Running the hard oracle solution should achieve good score with no violations.""" | |
| from data.generator import load_solution | |
| solution = load_solution("hard_dependency_migration") | |
| env.reset(task_id="hard_dependency_migration") | |
| for step_data in solution["optimal_action_sequence"]: | |
| action = Action( | |
| action_type=step_data["action_type"], | |
| resource_id=step_data.get("resource_id"), | |
| new_size=step_data.get("new_size"), | |
| reason=step_data.get("reason"), | |
| ) | |
| obs, reward, done, info = env.step(action) | |
| assert done is True | |
| assert info["cost_saved"] > 2400 # ~$2513 optimal | |
| assert len(info["safety_violations"]) == 0, f"Oracle should have 0 violations, got: {info['safety_violations']}" | |
| score = env.get_final_score() | |
| assert score > 0.8, f"Expected score > 0.8, got {score}" | |
| # βββ Maintenance Eligible Tests βββββββββββββββββββββββββββββββββββββββββββ | |
| class TestMaintenanceEligible: | |
| def test_resize_critical_with_maintenance_eligible(self, hard_env): | |
| """Resizing a critical resource WITH maintenance_eligible should NOT penalize.""" | |
| action = Action( | |
| action_type=ActionType.RESIZE, | |
| resource_id="rds-0c3d4e5f6a7b80015", # critical + maintenance_eligible | |
| new_size="db.r5.large", | |
| ) | |
| obs, reward, done, info = hard_env.step(action) | |
| assert reward.value > 0 # should succeed | |
| assert len(info["safety_violations"]) == 0 | |