CloudFinOpsEnv / tests /test_environment.py
jacklachan's picture
Upload folder using huggingface_hub
daace4d verified
Raw
History Blame Contribute Delete
15.2 kB
"""
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", {})
@pytest.fixture
def env():
return _TestEnvWrapper()
@pytest.fixture
def easy_env(env):
env.reset(task_id="easy_orphan_cleanup")
return env
@pytest.fixture
def medium_env(env):
env.reset(task_id="medium_rightsize")
return env
@pytest.fixture
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")
@pytest.mark.parametrize("task_id", ["easy_orphan_cleanup", "medium_rightsize", "hard_dependency_migration"])
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