File size: 9,512 Bytes
fcf8749
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""
Unit tests for Final Resolution Agent.
Tests swap logic and fairness validation.
"""

import pytest
from uuid import UUID
from app.services.final_resolution import FinalResolutionAgent
from app.schemas.agent_schemas import (
    AllocationItem,
    DriverLiaisonDecision,
    FairnessMetrics,
    RoutePlanResult,
)

# Fixed UUIDs for deterministic tests
D1_UUID = UUID("11111111-1111-1111-1111-111111111111")
D2_UUID = UUID("22222222-2222-2222-2222-222222222222")
R1_UUID = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
R2_UUID = UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")


class TestFinalResolutionSwaps:
    """Tests for swap acceptance and rejection logic."""
    
    def setup_method(self):
        """Set up test fixtures."""
        self.agent = FinalResolutionAgent(metric_epsilon=0.05)
    
    def test_swap_applied_improves_fairness(self):
        """Swap should be applied when it improves or maintains fairness."""
        # Create a proposal where driver-1 has high effort, driver-2 has low
        proposal = RoutePlanResult(
            allocation=[
                AllocationItem(driver_id=D1_UUID, route_id=R1_UUID, effort=80.0),
                AllocationItem(driver_id=D2_UUID, route_id=R2_UUID, effort=40.0),
            ],
            total_effort=120.0,
            avg_effort=60.0,
            per_driver_effort={str(D1_UUID): 80.0, str(D2_UUID): 40.0},
            proposal_number=1,
        )
        
        decisions = [
            DriverLiaisonDecision(
                driver_id=str(D1_UUID),
                decision="COUNTER",
                preferred_route_id=str(R2_UUID),  # Wants the easier route
                reason="Too heavy",
            ),
            DriverLiaisonDecision(
                driver_id=str(D2_UUID),
                decision="ACCEPT",
                reason="Within comfort",
            ),
        ]
        
        # Effort matrix: d1 can do r2 for 45, d2 can do r1 for 75
        effort_matrix = [
            [80.0, 45.0],  # d1's efforts for r1, r2
            [75.0, 40.0],  # d2's efforts for r1, r2
        ]
        
        current_metrics = FairnessMetrics(
            avg_effort=60.0,
            std_dev=20.0,
            max_gap=40.0,
            gini_index=0.167,
            min_effort=40.0,
            max_effort=80.0,
        )
        
        result = self.agent.resolve_counters(
            approved_proposal=proposal,
            decisions=decisions,
            effort_matrix=effort_matrix,
            driver_ids=[str(D1_UUID), str(D2_UUID)],
            route_ids=[str(R1_UUID), str(R2_UUID)],
            current_metrics=current_metrics,
        )
        
        # Swap should be applied: d1->r2 (45), d2->r1 (75)
        assert len(result.swaps_applied) == 1
        assert result.swaps_applied[0].driver_a == str(D1_UUID)
        assert result.per_driver_effort[str(D1_UUID)] == 45.0
        assert result.per_driver_effort[str(D2_UUID)] == 75.0
    
    def test_swap_rejected_worsens_fairness(self):
        """Swap should be rejected when it significantly worsens fairness."""
        proposal = RoutePlanResult(
            allocation=[
                AllocationItem(driver_id=D1_UUID, route_id=R1_UUID, effort=55.0),
                AllocationItem(driver_id=D2_UUID, route_id=R2_UUID, effort=45.0),
            ],
            total_effort=100.0,
            avg_effort=50.0,
            per_driver_effort={str(D1_UUID): 55.0, str(D2_UUID): 45.0},
            proposal_number=1,
        )
        
        decisions = [
            DriverLiaisonDecision(
                driver_id=str(D1_UUID),
                decision="COUNTER",
                preferred_route_id=str(R2_UUID),
                reason="Want easier",
            ),
        ]
        
        # Effort matrix: swap would make things much worse for d2
        effort_matrix = [
            [55.0, 40.0],  # d1: r1=55, r2=40
            [90.0, 45.0],  # d2: r1=90, r2=45 - huge penalty for d2
        ]
        
        current_metrics = FairnessMetrics(
            avg_effort=50.0,
            std_dev=5.0,
            max_gap=10.0,
            gini_index=0.05,
            min_effort=45.0,
            max_effort=55.0,
        )
        
        result = self.agent.resolve_counters(
            approved_proposal=proposal,
            decisions=decisions,
            effort_matrix=effort_matrix,
            driver_ids=[str(D1_UUID), str(D2_UUID)],
            route_ids=[str(R1_UUID), str(R2_UUID)],
            current_metrics=current_metrics,
        )
        
        # Swap should be rejected: d2 would go from 45 to 90 (100% increase)
        assert len(result.swaps_applied) == 0
        assert str(D1_UUID) in result.unfulfilled_counters
    
    def test_no_swaps_when_no_counters(self):
        """No swaps should occur when there are no COUNTER decisions."""
        proposal = RoutePlanResult(
            allocation=[
                AllocationItem(driver_id=D1_UUID, route_id=R1_UUID, effort=50.0),
                AllocationItem(driver_id=D2_UUID, route_id=R2_UUID, effort=50.0),
            ],
            total_effort=100.0,
            avg_effort=50.0,
            per_driver_effort={str(D1_UUID): 50.0, str(D2_UUID): 50.0},
            proposal_number=1,
        )
        
        decisions = [
            DriverLiaisonDecision(driver_id=str(D1_UUID), decision="ACCEPT", reason="OK"),
            DriverLiaisonDecision(driver_id=str(D2_UUID), decision="ACCEPT", reason="OK"),
        ]
        
        effort_matrix = [[50.0, 60.0], [60.0, 50.0]]
        
        current_metrics = FairnessMetrics(
            avg_effort=50.0,
            std_dev=0.0,
            max_gap=0.0,
            gini_index=0.0,
            min_effort=50.0,
            max_effort=50.0,
        )
        
        result = self.agent.resolve_counters(
            approved_proposal=proposal,
            decisions=decisions,
            effort_matrix=effort_matrix,
            driver_ids=[str(D1_UUID), str(D2_UUID)],
            route_ids=[str(R1_UUID), str(R2_UUID)],
            current_metrics=current_metrics,
        )
        
        assert len(result.swaps_applied) == 0
        assert len(result.unfulfilled_counters) == 0
    
    def test_unfulfilled_counter_recorded(self):
        """Unfulfilled counters should be recorded in result."""
        proposal = RoutePlanResult(
            allocation=[
                AllocationItem(driver_id=D1_UUID, route_id=R1_UUID, effort=70.0),
                AllocationItem(driver_id=D2_UUID, route_id=R2_UUID, effort=50.0),
            ],
            total_effort=120.0,
            avg_effort=60.0,
            per_driver_effort={str(D1_UUID): 70.0, str(D2_UUID): 50.0},
            proposal_number=1,
        )
        
        decisions = [
            DriverLiaisonDecision(
                driver_id=str(D1_UUID),
                decision="COUNTER",
                preferred_route_id="non-existent-route",  # Non-existent route
                reason="Want different route",
            ),
        ]
        
        effort_matrix = [[70.0, 55.0], [65.0, 50.0]]
        
        current_metrics = FairnessMetrics(
            avg_effort=60.0,
            std_dev=10.0,
            max_gap=20.0,
            gini_index=0.083,
            min_effort=50.0,
            max_effort=70.0,
        )
        
        result = self.agent.resolve_counters(
            approved_proposal=proposal,
            decisions=decisions,
            effort_matrix=effort_matrix,
            driver_ids=[str(D1_UUID), str(D2_UUID)],
            route_ids=[str(R1_UUID), str(R2_UUID)],
            current_metrics=current_metrics,
        )
        
        # Counter for non-existent route should be unfulfilled
        assert str(D1_UUID) in result.unfulfilled_counters


class TestFinalResolutionMetrics:
    """Tests for metric computation."""
    
    def setup_method(self):
        """Set up test fixtures."""
        self.agent = FinalResolutionAgent()
    
    def test_compute_metrics(self):
        """Test metric computation."""
        efforts = [40.0, 50.0, 60.0]
        
        metrics = self.agent._compute_metrics(efforts)
        
        assert metrics["avg_effort"] == 50.0
        assert metrics["min_effort"] == 40.0
        assert metrics["max_effort"] == 60.0
        assert metrics["max_gap"] == 20.0
        assert metrics["gini_index"] > 0
    
    def test_compute_gini_perfect_equality(self):
        """Gini should be 0 for perfect equality."""
        values = [50.0, 50.0, 50.0]
        gini = self.agent._compute_gini(values)
        assert gini == 0.0
    
    def test_snapshot_generation(self):
        """Test input/output snapshot generation."""
        metrics = FairnessMetrics(
            avg_effort=50.0,
            std_dev=10.0,
            max_gap=20.0,
            gini_index=0.1,
            min_effort=40.0,
            max_effort=60.0,
        )
        
        input_snap = self.agent.get_input_snapshot(3, metrics, 50.0)
        assert input_snap["num_counters"] == 3
        assert input_snap["original_gini"] == 0.1
        
        from app.schemas.agent_schemas import FinalResolutionResult
        result = FinalResolutionResult(
            allocation=[],
            per_driver_effort={},
            metrics={"gini_index": 0.08},
            swaps_applied=[],
            unfulfilled_counters=[],
        )
        
        output_snap = self.agent.get_output_snapshot(result)
        assert output_snap["num_swaps_applied"] == 0