"""Tests for the efficiency bonus reward function.""" from __future__ import annotations import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from graders.reward import compute_efficiency_bonus, RewardBreakdown, STEP_BUDGETS from graders.health import HealthReport from graders.security import SecurityReport # --------------------------------------------------------------------------- # compute_efficiency_bonus unit tests # --------------------------------------------------------------------------- class TestEfficiencyBonus: def test_full_bonus_at_zero_steps(self): bonus = compute_efficiency_bonus(steps_taken=0, difficulty="easy") assert bonus == 0.15 def test_zero_bonus_at_budget(self): bonus = compute_efficiency_bonus(steps_taken=10, difficulty="easy") assert bonus == 0.0 def test_half_bonus_at_half_budget(self): bonus = compute_efficiency_bonus(steps_taken=5, difficulty="easy") assert abs(bonus - 0.075) < 0.001 def test_over_budget_clamps_to_zero(self): bonus = compute_efficiency_bonus(steps_taken=50, difficulty="easy") assert bonus == 0.0 def test_all_difficulty_budgets_exist(self): for diff in ("easy", "medium", "hard"): assert diff in STEP_BUDGETS bonus = compute_efficiency_bonus(steps_taken=0, difficulty=diff) assert bonus == 0.15 def test_unknown_difficulty_falls_back(self): bonus = compute_efficiency_bonus(steps_taken=0, difficulty="unknown") assert bonus == 0.15 # falls back to medium budget # --------------------------------------------------------------------------- # RewardBreakdown integration # --------------------------------------------------------------------------- def _make_security(finding_count: int) -> SecurityReport: return SecurityReport(finding_count=finding_count, findings=[]) def _make_health(score: float) -> HealthReport: return HealthReport( score=score, passed=1, failures=0, errors=0, total=1, exit_code=0, stdout="", stderr="" ) class TestRewardBreakdown: def test_perfect_score_with_efficiency(self): reward = RewardBreakdown.from_reports( security=_make_security(0), health=_make_health(1.0), initial_leaks=5, steps_taken=0, difficulty="easy", ) # base = 1.0 * 1.0 = 1.0, efficiency = 0.15 → clamped to 1.0 assert reward.total_reward == 1.0 assert reward.efficiency_bonus == 0.15 def test_zero_security_gets_zero_reward(self): reward = RewardBreakdown.from_reports( security=_make_security(5), health=_make_health(1.0), initial_leaks=5, steps_taken=0, difficulty="easy", ) # No secrets fixed = base is 0 = no reward (efficiency can't give free points) assert reward.total_reward == 0.0 def test_reward_clamped_to_1(self): reward = RewardBreakdown.from_reports( security=_make_security(0), health=_make_health(1.0), initial_leaks=2, steps_taken=0, difficulty="hard", ) assert reward.total_reward <= 1.0 def test_reward_stays_positive(self): reward = RewardBreakdown.from_reports( security=_make_security(10), health=_make_health(0.0), initial_leaks=10, steps_taken=30, difficulty="hard", ) assert reward.total_reward >= 0.0 def test_efficiency_bonus_field_exposed(self): reward = RewardBreakdown.from_reports( security=_make_security(3), health=_make_health(0.8), initial_leaks=5, steps_taken=5, difficulty="medium", ) assert hasattr(reward, "efficiency_bonus") assert 0.0 <= reward.efficiency_bonus <= 0.15