| """ |
| Unit tests for FairnessManagerAgent. |
| Tests Gini calculation, metric computation, and ACCEPT/REOPTIMIZE decision logic. |
| """ |
|
|
| import pytest |
|
|
| from app.services.fairness_manager_agent import FairnessManagerAgent |
| from app.schemas.agent_schemas import ( |
| AllocationItem, |
| FairnessThresholds, |
| RoutePlanResult, |
| ) |
| from uuid import uuid4 |
|
|
|
|
| def create_mock_plan_result(efforts: list[float]) -> RoutePlanResult: |
| """Create a mock RoutePlanResult from effort values.""" |
| allocation = [] |
| per_driver_effort = {} |
| |
| for i, effort in enumerate(efforts): |
| driver_id = uuid4() |
| route_id = uuid4() |
| allocation.append(AllocationItem( |
| driver_id=driver_id, |
| route_id=route_id, |
| effort=effort, |
| )) |
| per_driver_effort[str(driver_id)] = effort |
| |
| return RoutePlanResult( |
| allocation=allocation, |
| total_effort=sum(efforts), |
| avg_effort=sum(efforts) / len(efforts) if efforts else 0.0, |
| per_driver_effort=per_driver_effort, |
| proposal_number=1, |
| ) |
|
|
|
|
| class TestFairnessManagerAgent: |
| """Test suite for FairnessManagerAgent.""" |
| |
| def test_gini_perfect_equality(self): |
| """Test Gini index for perfectly equal distribution.""" |
| agent = FairnessManagerAgent() |
| |
| |
| plan = create_mock_plan_result([50.0, 50.0, 50.0, 50.0]) |
| result = agent.check(plan) |
| |
| assert result.metrics.gini_index == 0.0, "Perfect equality should have Gini = 0" |
| |
| def test_gini_inequality(self): |
| """Test Gini index for unequal distribution.""" |
| agent = FairnessManagerAgent() |
| |
| |
| plan = create_mock_plan_result([10.0, 20.0, 30.0, 100.0]) |
| result = agent.check(plan) |
| |
| assert result.metrics.gini_index > 0.0, "Unequal distribution should have Gini > 0" |
| assert result.metrics.gini_index <= 1.0, "Gini should not exceed 1" |
| |
| def test_std_dev_calculation(self): |
| """Test standard deviation calculation.""" |
| agent = FairnessManagerAgent() |
| |
| |
| plan = create_mock_plan_result([60.0, 60.0, 60.0]) |
| result = agent.check(plan) |
| |
| assert result.metrics.std_dev == 0.0, "Equal values should have std_dev = 0" |
| |
| |
| plan2 = create_mock_plan_result([50.0, 60.0, 70.0]) |
| result2 = agent.check(plan2) |
| |
| assert result2.metrics.std_dev > 0.0, "Varied values should have std_dev > 0" |
| |
| def test_max_gap_calculation(self): |
| """Test max gap (max - min) calculation.""" |
| agent = FairnessManagerAgent() |
| |
| plan = create_mock_plan_result([30.0, 50.0, 80.0]) |
| result = agent.check(plan) |
| |
| expected_gap = 80.0 - 30.0 |
| assert result.metrics.max_gap == expected_gap |
| assert result.metrics.min_effort == 30.0 |
| assert result.metrics.max_effort == 80.0 |
| |
| def test_accept_when_within_thresholds(self): |
| """Test ACCEPT status when all thresholds met.""" |
| agent = FairnessManagerAgent( |
| thresholds=FairnessThresholds( |
| gini_threshold=0.5, |
| stddev_threshold=50.0, |
| max_gap_threshold=50.0, |
| ) |
| ) |
| |
| |
| plan = create_mock_plan_result([55.0, 60.0, 65.0]) |
| result = agent.check(plan) |
| |
| assert result.status == "ACCEPT" |
| assert result.recommendations is None |
| |
| def test_reoptimize_when_gini_exceeds_threshold(self): |
| """Test REOPTIMIZE status when Gini exceeds threshold.""" |
| agent = FairnessManagerAgent( |
| thresholds=FairnessThresholds( |
| gini_threshold=0.1, |
| stddev_threshold=100.0, |
| max_gap_threshold=100.0, |
| ) |
| ) |
| |
| |
| plan = create_mock_plan_result([10.0, 50.0, 90.0]) |
| result = agent.check(plan) |
| |
| assert result.status == "REOPTIMIZE" |
| assert result.recommendations is not None |
| |
| def test_reoptimize_when_stddev_exceeds_threshold(self): |
| """Test REOPTIMIZE status when std_dev exceeds threshold.""" |
| agent = FairnessManagerAgent( |
| thresholds=FairnessThresholds( |
| gini_threshold=1.0, |
| stddev_threshold=5.0, |
| max_gap_threshold=100.0, |
| ) |
| ) |
| |
| plan = create_mock_plan_result([30.0, 50.0, 70.0]) |
| result = agent.check(plan) |
| |
| assert result.status == "REOPTIMIZE" |
| |
| def test_reoptimize_when_max_gap_exceeds_threshold(self): |
| """Test REOPTIMIZE status when max_gap exceeds threshold.""" |
| agent = FairnessManagerAgent( |
| thresholds=FairnessThresholds( |
| gini_threshold=1.0, |
| stddev_threshold=100.0, |
| max_gap_threshold=10.0, |
| ) |
| ) |
| |
| plan = create_mock_plan_result([40.0, 50.0, 80.0]) |
| result = agent.check(plan) |
| |
| assert result.status == "REOPTIMIZE" |
| |
| def test_recommendations_identify_high_effort_drivers(self): |
| """Test that recommendations identify high-effort drivers.""" |
| agent = FairnessManagerAgent( |
| thresholds=FairnessThresholds( |
| gini_threshold=0.1, |
| stddev_threshold=5.0, |
| max_gap_threshold=10.0, |
| ) |
| ) |
| |
| plan = create_mock_plan_result([50.0, 50.0, 50.0, 100.0]) |
| result = agent.check(plan) |
| |
| assert result.status == "REOPTIMIZE" |
| assert result.recommendations is not None |
| assert len(result.recommendations.high_effort_driver_ids) > 0 |
| assert result.recommendations.penalty_factor >= 1.0 |
| |
| def test_outlier_count(self): |
| """Test outlier counting (drivers above avg + 2*std_dev).""" |
| agent = FairnessManagerAgent() |
| |
| |
| |
| plan = create_mock_plan_result([10.0] * 8 + [100.0]) |
| result = agent.check(plan) |
| |
| assert result.metrics.outlier_count >= 1 |
| |
| def test_empty_plan(self): |
| """Test handling of empty plan.""" |
| agent = FairnessManagerAgent() |
| |
| plan = create_mock_plan_result([]) |
| result = agent.check(plan) |
| |
| assert result.status == "ACCEPT" |
| assert result.metrics.gini_index == 0.0 |
| assert result.metrics.std_dev == 0.0 |
| |
| def test_single_driver(self): |
| """Test handling of single driver.""" |
| agent = FairnessManagerAgent() |
| |
| plan = create_mock_plan_result([75.0]) |
| result = agent.check(plan) |
| |
| assert result.status == "ACCEPT" |
| assert result.metrics.gini_index == 0.0 |
| assert result.metrics.std_dev == 0.0 |
| assert result.metrics.max_gap == 0.0 |
| |
| def test_thresholds_included_in_result(self): |
| """Test that used thresholds are included in result.""" |
| thresholds = FairnessThresholds( |
| gini_threshold=0.25, |
| stddev_threshold=20.0, |
| max_gap_threshold=30.0, |
| ) |
| agent = FairnessManagerAgent(thresholds=thresholds) |
| |
| plan = create_mock_plan_result([50.0, 60.0]) |
| result = agent.check(plan) |
| |
| assert result.thresholds_used["gini_threshold"] == 0.25 |
| assert result.thresholds_used["stddev_threshold"] == 20.0 |
| assert result.thresholds_used["max_gap_threshold"] == 30.0 |
| |
| def test_snapshot_generation(self): |
| """Test input/output snapshot generation.""" |
| agent = FairnessManagerAgent() |
| |
| plan = create_mock_plan_result([40.0, 50.0, 60.0]) |
| |
| input_snapshot = agent.get_input_snapshot(plan) |
| assert "proposal_number" in input_snapshot |
| assert "num_drivers" in input_snapshot |
| |
| result = agent.check(plan) |
| output_snapshot = agent.get_output_snapshot(result) |
| assert "status" in output_snapshot |
| assert "gini_index" in output_snapshot |
|
|