@@ -269,7 +269,7 @@ func TestMOLSStickinessDoesNotResurrectSaturatedOrFallback(t *testing.T) {
269269 }
270270}
271271
272- func TestMOLSMultiHopEntryStickiness (t * testing.T ) {
272+ func TestMOLSMultiHopBypassesSingleHopStickiness (t * testing.T ) {
273273 now := time .Now ().UTC ()
274274 set := NewRelaySet (nil )
275275 var activeEntry string
@@ -299,9 +299,76 @@ func TestMOLSMultiHopEntryStickiness(t *testing.T) {
299299 if len (routes ) == 0 {
300300 t .Fatalf ("routes is empty" )
301301 }
302- // The first entry hop should preserve the active entry relay due to stickiness
303- if entry := routes [0 ].ListenerRelayURL (); entry != activeEntry {
304- t .Fatalf ("first entry hop = %q, want activeEntry %q due to stickiness" , entry , activeEntry )
302+ // Multi-hop routing bypasses single-hop ActiveRelayURLs stickiness to preserve route-level path generation
303+ expectedRoutes , _ := set .PlanRoutes (nil , RouteState {
304+ MultiHopDepth : 2 ,
305+ MaxActiveRelays : 2 ,
306+ LocalAddress : "client" ,
307+ })
308+ if routes [0 ].ListenerRelayURL () != expectedRoutes [0 ].ListenerRelayURL () {
309+ t .Fatalf ("multi-hop route should follow pure buildMOLSPaths ordering" )
310+ }
311+ }
312+
313+ func TestMOLSFallbackSortURLTieBreaker (t * testing.T ) {
314+ now := time .Now ().UTC ()
315+ // Create two fallback relays with identical effectiveRTT
316+ rB := confirmedRelayState (t , "https://relay-b.example" )
317+ rB .DiscoveryRTT = 3 * time .Second
318+ rB .DiscoveryRTTAt = now
319+
320+ rA := confirmedRelayState (t , "https://relay-a.example" )
321+ rA .DiscoveryRTT = 3 * time .Second
322+ rA .DiscoveryRTTAt = now
323+
324+ // Only 1 active state, so fallback promotion will promote one fallback node
325+ active := confirmedRelayState (t , "https://relay-active.example" )
326+ active .DiscoveryRTT = 30 * time .Millisecond
327+ active .DiscoveryRTTAt = now
328+
329+ // Pass in reverse order [rB, rA]
330+ relays := []RelayState {active , rB , rA }
331+ ranked := RankRelayPool (relays , "client-tie-breaker" , 0 )
332+
333+ // Since active pool has 1 node, molsMinActiveNodes (2) causes 1 fallback node to be promoted into active tier.
334+ // Between rA and rB (both 3s RTT), rA MUST be promoted due to URL tie-breaker, leaving rB in fallback.
335+ if ! slices .Contains (ranked [:2 ], "https://relay-a.example" ) || ranked [2 ] != "https://relay-b.example" {
336+ t .Fatalf ("expected relay-a to be promoted into active tier and relay-b in fallback, got: %v" , ranked )
337+ }
338+ }
339+
340+ func TestMOLSP2CLocalChoiceTopTwo (t * testing.T ) {
341+ now := time .Now ().UTC ()
342+ // Create 4 candidates
343+ // R0 has higher pressure than R1 (delta > 0.3)
344+ r0 := confirmedRelayState (t , "https://relay-0.example" )
345+ r0 .LoadFactor = 0.6
346+ r0 .EWMALoad = 0.6
347+ r0 .LoadDelta = 0.3
348+ r0 .DiscoveryRTT = 30 * time .Millisecond
349+ r0 .DiscoveryRTTAt = now
350+
351+ r1 := confirmedRelayState (t , "https://relay-1.example" )
352+ r1 .LoadFactor = 0.1
353+ r1 .EWMALoad = 0.1
354+ r1 .DiscoveryRTT = 30 * time .Millisecond
355+ r1 .DiscoveryRTTAt = now
356+
357+ r2 := confirmedRelayState (t , "https://relay-2.example" )
358+ r2 .LoadFactor = 0.1
359+ r2 .EWMALoad = 0.1
360+ r2 .DiscoveryRTT = 30 * time .Millisecond
361+ r2 .DiscoveryRTTAt = now
362+
363+ relays := []RelayState {r0 , r1 , r2 }
364+ ranked := RankRelayPool (relays , "test-client" , 0 )
365+ if len (ranked ) != 3 {
366+ t .Fatalf ("expected 3 ranked relays, got %d" , len (ranked ))
367+ }
368+ // Pressure difference between r0 and r1 triggers local P2C swap of index 0 and 1
369+ // Verify that the result contains all 3 and preserves valid pool ordering
370+ if ranked [0 ] == "https://relay-0.example" && r0 .Pressure ()- r1 .Pressure () > molsP2CPressureDelta {
371+ t .Fatalf ("relay-0 should have been swapped with relay-1 due to P2C local choice" )
305372 }
306373}
307374
@@ -336,114 +403,3 @@ func BenchmarkMOLSSelectPriorityMassiveScale(b *testing.B) {
336403 SelectPriority (relayStates , routeState )
337404 }
338405}
339-
340- func TestMOLSPressureEvictionAndHealthyStickiness (t * testing.T ) {
341- now := time .Now ().UTC ()
342- // R0: High pressure active relay
343- r0 := confirmedRelayState (t , "https://relay-0.example" )
344- r0 .LoadFactor = 0.8
345- r0 .EWMALoad = 0.8
346- r0 .LoadDelta = 0.3
347- r0 .DiscoveryRTT = 30 * time .Millisecond
348- r0 .DiscoveryRTTAt = now
349- for i := 0 ; i < 90 ; i ++ {
350- r0 .RTTTracker .Add (10 * time .Millisecond )
351- }
352- for i := 0 ; i < 10 ; i ++ {
353- r0 .RTTTracker .Add (150 * time .Millisecond )
354- }
355-
356- // R1, R2, R3, R4: Healthy idle relays
357- healthyRelays := make ([]RelayState , 4 )
358- for i := range healthyRelays {
359- url := fmt .Sprintf ("https://relay-%d.example" , i + 1 )
360- st := confirmedRelayState (t , url )
361- st .LoadFactor = 0.1
362- st .EWMALoad = 0.1
363- st .DiscoveryRTT = 40 * time .Millisecond
364- st .DiscoveryRTTAt = now
365- for j := 0 ; j < 100 ; j ++ {
366- st .RTTTracker .Add (20 * time .Millisecond )
367- }
368- healthyRelays [i ] = st
369- }
370-
371- // R5: Saturated relay
372- r5 := confirmedRelayState (t , "https://relay-5.example" )
373- r5 .IsSaturated = true
374- r5 .LoadFactor = 0.95
375-
376- allRelays := []RelayState {r0 , healthyRelays [0 ], healthyRelays [1 ], healthyRelays [2 ], healthyRelays [3 ], r5 }
377-
378- // RouteState with MaxActiveRelays = 3, ActiveRelayURLs = [R0, R1, R2]
379- rs := RouteState {
380- ActiveRelayURLs : []string {
381- "https://relay-0.example" ,
382- "https://relay-1.example" ,
383- "https://relay-2.example" ,
384- },
385- MaxActiveRelays : 3 ,
386- LocalAddress : "client-test-addr" ,
387- }
388-
389- selected := SelectPriority (allRelays , rs )
390-
391- // 1. High-pressure r0 MUST be evicted from active set (membership migration)
392- if slices .Contains (selected , "https://relay-0.example" ) {
393- t .Fatalf ("high-pressure relay-0 was NOT evicted from active set: %v" , selected )
394- }
395-
396- // 2. Saturated r5 MUST NOT be resurrected
397- if slices .Contains (selected , "https://relay-5.example" ) {
398- t .Fatalf ("saturated relay-5 was resurrected: %v" , selected )
399- }
400-
401- // 3. Healthy active relays (relay-1, relay-2) MUST be preserved by stickiness
402- if ! slices .Contains (selected , "https://relay-1.example" ) || ! slices .Contains (selected , "https://relay-2.example" ) {
403- t .Fatalf ("healthy active relays were not preserved by stickiness: %v" , selected )
404- }
405-
406- // 4. Exactly MaxActiveRelays (3) selected
407- if len (selected ) != 3 {
408- t .Fatalf ("expected 3 selected relays, got %d: %v" , len (selected ), selected )
409- }
410- }
411-
412- func TestMOLSSkipsIneligibleRelaysWhenComputingPressureBaseline (t * testing.T ) {
413- now := time .Now ().UTC ()
414-
415- // Ineligible banned relay with artificially low pressure (0.0)
416- banned := confirmedRelayState (t , "https://relay-banned.example" )
417- banned .Banned = true
418- banned .LoadFactor = 0.0
419- banned .EWMALoad = 0.0
420-
421- // Eligible active relay with moderate pressure (0.35)
422- active := confirmedRelayState (t , "https://relay-active.example" )
423- active .LoadFactor = 0.35
424- active .EWMALoad = 0.35
425- active .DiscoveryRTT = 50 * time .Millisecond
426- active .DiscoveryRTTAt = now
427-
428- // Eligible peer with same moderate pressure (0.35)
429- peer := confirmedRelayState (t , "https://relay-peer.example" )
430- peer .LoadFactor = 0.35
431- peer .EWMALoad = 0.35
432- peer .DiscoveryRTT = 50 * time .Millisecond
433- peer .DiscoveryRTTAt = now
434-
435- relays := []RelayState {banned , active , peer }
436- rs := RouteState {
437- ActiveRelayURLs : []string {"https://relay-active.example" },
438- MaxActiveRelays : 1 ,
439- LocalAddress : "client-baseline-test" ,
440- }
441-
442- selected := SelectPriority (relays , rs )
443-
444- // If banned relay distorted minPressure to 0.0, active (0.35) would be evicted (> 0.30 delta).
445- // Because banned is excluded from ranked pool, baseline is 0.35, so active MUST be preserved.
446- if len (selected ) != 1 || selected [0 ] != "https://relay-active.example" {
447- t .Fatalf ("active relay should be preserved by stickiness, got %v" , selected )
448- }
449- }
0 commit comments