1use super::traits::*;
7use std::time::Duration;
8
9#[derive(Debug, Clone)]
15pub struct SelectionCriteria {
16 pub capability: Capability,
18 pub min_health: HealthState,
20 pub max_latency: Option<Duration>,
22 pub min_privacy: PrivacyLevel,
24 pub preferred_regions: Vec<RegionId>,
26 pub excluded_nodes: Vec<NodeId>,
28}
29
30impl Default for SelectionCriteria {
31 fn default() -> Self {
32 Self {
33 capability: Capability::Generate,
34 min_health: HealthState::Degraded,
35 max_latency: None,
36 min_privacy: PrivacyLevel::Public,
37 preferred_regions: vec![],
38 excluded_nodes: vec![],
39 }
40 }
41}
42
43pub struct LatencyPolicy {
52 pub weight: f64,
54 pub max_latency: Duration,
56}
57
58impl Default for LatencyPolicy {
59 fn default() -> Self {
60 Self {
61 weight: 1.0,
62 max_latency: Duration::from_secs(5),
63 }
64 }
65}
66
67impl RoutingPolicyTrait for LatencyPolicy {
68 fn score(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> f64 {
69 let latency_ms = candidate.target.estimated_latency.as_millis() as f64;
70 let max_ms = self.max_latency.as_millis() as f64;
71
72 if latency_ms >= max_ms {
73 return 0.0;
74 }
75
76 let score = 1.0 - (latency_ms / max_ms);
78 score * self.weight
79 }
80
81 fn is_eligible(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> bool {
82 candidate.target.estimated_latency <= self.max_latency
83 }
84
85 fn name(&self) -> &'static str {
86 "latency"
87 }
88}
89
90pub struct LocalityPolicy {
95 pub weight: f64,
97 pub same_region_boost: f64,
99 pub cross_region_penalty: f64,
101}
102
103impl Default for LocalityPolicy {
104 fn default() -> Self {
105 Self {
106 weight: 1.0,
107 same_region_boost: 0.3,
108 cross_region_penalty: 0.1,
109 }
110 }
111}
112
113impl RoutingPolicyTrait for LocalityPolicy {
114 fn score(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> f64 {
115 let base_score = 0.5;
117
118 let score = base_score + candidate.scores.locality_score * self.same_region_boost;
120 score * self.weight
121 }
122
123 fn is_eligible(&self, _candidate: &RouteCandidate, _request: &InferenceRequest) -> bool {
124 true }
126
127 fn name(&self) -> &'static str {
128 "locality"
129 }
130}
131
132#[derive(Default)]
136pub struct PrivacyPolicy {
137 pub region_privacy: std::collections::HashMap<RegionId, PrivacyLevel>,
139}
140
141impl PrivacyPolicy {
142 #[must_use]
144 pub fn with_region(mut self, region: RegionId, level: PrivacyLevel) -> Self {
145 self.region_privacy.insert(region, level);
146 self
147 }
148}
149
150impl RoutingPolicyTrait for PrivacyPolicy {
151 fn score(&self, _candidate: &RouteCandidate, _request: &InferenceRequest) -> f64 {
152 1.0 }
154
155 fn is_eligible(&self, candidate: &RouteCandidate, request: &InferenceRequest) -> bool {
156 let region_level = self
157 .region_privacy
158 .get(&candidate.target.region_id)
159 .copied()
160 .unwrap_or(PrivacyLevel::Internal);
162
163 region_level >= request.qos.privacy
165 }
166
167 fn name(&self) -> &'static str {
168 "privacy"
169 }
170}
171
172pub struct CostPolicy {
176 pub weight: f64,
178 pub region_costs: std::collections::HashMap<RegionId, f64>,
180}
181
182impl Default for CostPolicy {
183 fn default() -> Self {
184 Self {
185 weight: 1.0,
186 region_costs: std::collections::HashMap::new(),
187 }
188 }
189}
190
191impl CostPolicy {
192 #[must_use]
193 pub fn with_region_cost(mut self, region: RegionId, cost: f64) -> Self {
194 self.region_costs.insert(region, cost.clamp(0.0, 1.0));
195 self
196 }
197}
198
199impl RoutingPolicyTrait for CostPolicy {
200 fn score(&self, candidate: &RouteCandidate, request: &InferenceRequest) -> f64 {
201 let region_cost = self
202 .region_costs
203 .get(&candidate.target.region_id)
204 .copied()
205 .unwrap_or(0.5);
206
207 let cost_tolerance = request.qos.cost_tolerance as f64 / 100.0;
208
209 let score = if cost_tolerance > 0.5 {
212 candidate.scores.throughput_score
214 } else {
215 1.0 - region_cost
217 };
218
219 score * self.weight
220 }
221
222 fn is_eligible(&self, _candidate: &RouteCandidate, _request: &InferenceRequest) -> bool {
223 true
224 }
225
226 fn name(&self) -> &'static str {
227 "cost"
228 }
229}
230
231pub struct HealthPolicy {
235 pub weight: f64,
237 pub healthy_score: f64,
239 pub degraded_score: f64,
241}
242
243impl Default for HealthPolicy {
244 fn default() -> Self {
245 Self {
246 weight: 2.0, healthy_score: 1.0,
248 degraded_score: 0.3,
249 }
250 }
251}
252
253impl RoutingPolicyTrait for HealthPolicy {
254 fn score(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> f64 {
255 candidate.scores.health_score * self.weight
256 }
257
258 fn is_eligible(&self, candidate: &RouteCandidate, _request: &InferenceRequest) -> bool {
259 candidate.scores.health_score > 0.0
261 }
262
263 fn name(&self) -> &'static str {
264 "health"
265 }
266}
267
268pub struct CompositePolicy {
274 policies: Vec<Box<dyn RoutingPolicyTrait>>,
275}
276
277impl CompositePolicy {
278 pub fn new() -> Self {
279 Self { policies: vec![] }
280 }
281
282 #[must_use]
283 pub fn with_policy(mut self, policy: impl RoutingPolicyTrait + 'static) -> Self {
284 self.policies.push(Box::new(policy));
285 self
286 }
287
288 pub fn enterprise_default() -> Self {
290 Self::new()
291 .with_policy(HealthPolicy::default())
292 .with_policy(LatencyPolicy::default())
293 .with_policy(PrivacyPolicy::default())
294 .with_policy(LocalityPolicy::default())
295 .with_policy(CostPolicy::default())
296 }
297}
298
299impl Default for CompositePolicy {
300 fn default() -> Self {
301 Self::enterprise_default()
302 }
303}
304
305impl RoutingPolicyTrait for CompositePolicy {
306 fn score(&self, candidate: &RouteCandidate, request: &InferenceRequest) -> f64 {
307 if self.policies.is_empty() {
308 return 1.0;
309 }
310
311 let total: f64 = self
312 .policies
313 .iter()
314 .map(|p| p.score(candidate, request))
315 .sum();
316
317 total / self.policies.len() as f64
318 }
319
320 fn is_eligible(&self, candidate: &RouteCandidate, request: &InferenceRequest) -> bool {
321 self.policies
323 .iter()
324 .all(|p| p.is_eligible(candidate, request))
325 }
326
327 fn name(&self) -> &'static str {
328 "composite"
329 }
330}
331
332pub struct RoutingPolicy {
338 #[allow(dead_code)]
339 inner: Box<dyn RoutingPolicyTrait>,
340}
341
342impl RoutingPolicy {
343 pub fn latency() -> Self {
344 Self {
345 inner: Box::new(LatencyPolicy::default()),
346 }
347 }
348
349 pub fn locality() -> Self {
350 Self {
351 inner: Box::new(LocalityPolicy::default()),
352 }
353 }
354
355 pub fn privacy() -> Self {
356 Self {
357 inner: Box::new(PrivacyPolicy::default()),
358 }
359 }
360
361 pub fn cost() -> Self {
362 Self {
363 inner: Box::new(CostPolicy::default()),
364 }
365 }
366
367 pub fn health() -> Self {
368 Self {
369 inner: Box::new(HealthPolicy::default()),
370 }
371 }
372
373 pub fn enterprise() -> Self {
374 Self {
375 inner: Box::new(CompositePolicy::enterprise_default()),
376 }
377 }
378}
379
380#[cfg(test)]
385mod tests {
386 use super::*;
387
388 fn mock_candidate(latency_ms: u64, health_score: f64) -> RouteCandidate {
389 RouteCandidate {
390 target: RouteTarget {
391 node_id: NodeId("node1".to_string()),
392 region_id: RegionId("us-west".to_string()),
393 endpoint: "http://node1:8080".to_string(),
394 estimated_latency: Duration::from_millis(latency_ms),
395 score: 0.0,
396 },
397 scores: RouteScores {
398 latency_score: 1.0 - (latency_ms as f64 / 5000.0),
399 throughput_score: 0.8,
400 cost_score: 0.5,
401 locality_score: 0.7,
402 health_score,
403 total: 0.0,
404 },
405 eligible: true,
406 rejection_reason: None,
407 }
408 }
409
410 fn mock_request() -> InferenceRequest {
411 InferenceRequest {
412 capability: Capability::Generate,
413 input: vec![],
414 qos: QoSRequirements::default(),
415 request_id: "req-1".to_string(),
416 tenant_id: None,
417 }
418 }
419
420 #[test]
421 fn test_latency_policy_scoring() {
422 let policy = LatencyPolicy::default();
423 let request = mock_request();
424
425 let fast = mock_candidate(100, 1.0);
427 let slow = mock_candidate(4000, 1.0);
428
429 let fast_score = policy.score(&fast, &request);
430 let slow_score = policy.score(&slow, &request);
431
432 assert!(fast_score > slow_score);
433 assert!(fast_score > 0.9); }
435
436 #[test]
437 fn test_latency_policy_eligibility() {
438 let policy = LatencyPolicy {
439 max_latency: Duration::from_secs(2),
440 ..Default::default()
441 };
442 let request = mock_request();
443
444 let fast = mock_candidate(1000, 1.0);
445 let slow = mock_candidate(3000, 1.0);
446
447 assert!(policy.is_eligible(&fast, &request));
448 assert!(!policy.is_eligible(&slow, &request));
449 }
450
451 #[test]
452 fn test_health_policy_scoring() {
453 let policy = HealthPolicy::default();
454 let request = mock_request();
455
456 let healthy = mock_candidate(100, 1.0);
457 let degraded = mock_candidate(100, 0.3);
458
459 let healthy_score = policy.score(&healthy, &request);
460 let degraded_score = policy.score(°raded, &request);
461
462 assert!(healthy_score > degraded_score);
463 }
464
465 #[test]
466 fn test_composite_policy() {
467 let policy = CompositePolicy::enterprise_default();
468 let request = mock_request();
469
470 let good = mock_candidate(100, 1.0);
471 let bad = mock_candidate(4000, 0.2);
472
473 let good_score = policy.score(&good, &request);
474 let bad_score = policy.score(&bad, &request);
475
476 assert!(good_score > bad_score);
477 }
478
479 #[test]
480 fn test_privacy_policy_eligibility() {
481 let policy = PrivacyPolicy::default()
482 .with_region(RegionId("eu-west".to_string()), PrivacyLevel::Confidential)
483 .with_region(RegionId("us-east".to_string()), PrivacyLevel::Public);
484
485 let mut request = mock_request();
486 request.qos.privacy = PrivacyLevel::Confidential;
487
488 let eu_candidate = RouteCandidate {
489 target: RouteTarget {
490 node_id: NodeId("node-eu".to_string()),
491 region_id: RegionId("eu-west".to_string()),
492 endpoint: "http://eu:8080".to_string(),
493 estimated_latency: Duration::from_millis(100),
494 score: 0.0,
495 },
496 scores: RouteScores::default(),
497 eligible: true,
498 rejection_reason: None,
499 };
500
501 let us_candidate = RouteCandidate {
502 target: RouteTarget {
503 node_id: NodeId("node-us".to_string()),
504 region_id: RegionId("us-east".to_string()),
505 endpoint: "http://us:8080".to_string(),
506 estimated_latency: Duration::from_millis(50),
507 score: 0.0,
508 },
509 scores: RouteScores::default(),
510 eligible: true,
511 rejection_reason: None,
512 };
513
514 assert!(policy.is_eligible(&eu_candidate, &request));
516 assert!(!policy.is_eligible(&us_candidate, &request));
518 }
519
520 #[test]
525 fn test_selection_criteria_default() {
526 let criteria = SelectionCriteria::default();
527 assert!(matches!(criteria.capability, Capability::Generate));
528 assert_eq!(criteria.min_health, HealthState::Degraded);
529 assert!(criteria.max_latency.is_none());
530 assert_eq!(criteria.min_privacy, PrivacyLevel::Public);
531 assert!(criteria.preferred_regions.is_empty());
532 assert!(criteria.excluded_nodes.is_empty());
533 }
534
535 #[test]
536 fn test_selection_criteria_custom() {
537 let criteria = SelectionCriteria {
538 capability: Capability::Transcribe,
539 min_health: HealthState::Healthy,
540 max_latency: Some(Duration::from_secs(2)),
541 min_privacy: PrivacyLevel::Confidential,
542 preferred_regions: vec![RegionId("eu-west".to_string())],
543 excluded_nodes: vec![NodeId("bad-node".to_string())],
544 };
545 assert!(matches!(criteria.capability, Capability::Transcribe));
546 assert_eq!(criteria.max_latency, Some(Duration::from_secs(2)));
547 assert_eq!(criteria.preferred_regions.len(), 1);
548 assert_eq!(criteria.excluded_nodes.len(), 1);
549 }
550
551 #[test]
556 fn test_latency_policy_at_max() {
557 let policy = LatencyPolicy::default(); let request = mock_request();
559
560 let at_max = mock_candidate(5000, 1.0);
562 let score = policy.score(&at_max, &request);
563 assert_eq!(score, 0.0);
564 }
565
566 #[test]
567 fn test_latency_policy_beyond_max() {
568 let policy = LatencyPolicy::default();
569 let request = mock_request();
570
571 let beyond = mock_candidate(6000, 1.0);
572 let score = policy.score(&beyond, &request);
573 assert_eq!(score, 0.0);
574 }
575
576 #[test]
577 fn test_latency_policy_zero_latency() {
578 let policy = LatencyPolicy::default();
579 let request = mock_request();
580
581 let zero = mock_candidate(0, 1.0);
582 let score = policy.score(&zero, &request);
583 assert!((score - 1.0).abs() < f64::EPSILON);
584 }
585
586 #[test]
587 fn test_latency_policy_name() {
588 let policy = LatencyPolicy::default();
589 assert_eq!(policy.name(), "latency");
590 }
591
592 #[test]
593 fn test_latency_policy_custom_weight() {
594 let policy = LatencyPolicy {
595 weight: 2.0,
596 max_latency: Duration::from_secs(5),
597 };
598 let request = mock_request();
599 let candidate = mock_candidate(0, 1.0);
600
601 let score = policy.score(&candidate, &request);
602 assert!((score - 2.0).abs() < f64::EPSILON);
603 }
604
605 #[test]
606 fn test_latency_policy_eligibility_at_boundary() {
607 let policy = LatencyPolicy {
608 max_latency: Duration::from_millis(1000),
609 ..Default::default()
610 };
611 let request = mock_request();
612
613 let at_boundary = mock_candidate(1000, 1.0);
615 assert!(policy.is_eligible(&at_boundary, &request));
616
617 let over = mock_candidate(1001, 1.0);
619 assert!(!policy.is_eligible(&over, &request));
620 }
621
622 #[test]
627 fn test_locality_policy_default() {
628 let policy = LocalityPolicy::default();
629 assert_eq!(policy.weight, 1.0);
630 assert_eq!(policy.same_region_boost, 0.3);
631 assert_eq!(policy.cross_region_penalty, 0.1);
632 }
633
634 #[test]
635 fn test_locality_policy_scoring() {
636 let policy = LocalityPolicy::default();
637 let request = mock_request();
638
639 let high_locality = RouteCandidate {
640 target: RouteTarget {
641 node_id: NodeId("n1".to_string()),
642 region_id: RegionId("us-west".to_string()),
643 endpoint: String::new(),
644 estimated_latency: Duration::from_millis(50),
645 score: 0.0,
646 },
647 scores: RouteScores {
648 latency_score: 0.9,
649 throughput_score: 0.8,
650 cost_score: 0.5,
651 locality_score: 1.0,
652 health_score: 1.0,
653 total: 0.0,
654 },
655 eligible: true,
656 rejection_reason: None,
657 };
658
659 let low_locality = RouteCandidate {
660 target: RouteTarget {
661 node_id: NodeId("n2".to_string()),
662 region_id: RegionId("ap-south".to_string()),
663 endpoint: String::new(),
664 estimated_latency: Duration::from_millis(200),
665 score: 0.0,
666 },
667 scores: RouteScores {
668 latency_score: 0.5,
669 throughput_score: 0.3,
670 cost_score: 0.5,
671 locality_score: 0.0,
672 health_score: 1.0,
673 total: 0.0,
674 },
675 eligible: true,
676 rejection_reason: None,
677 };
678
679 let high_score = policy.score(&high_locality, &request);
680 let low_score = policy.score(&low_locality, &request);
681 assert!(high_score > low_score);
682 }
683
684 #[test]
685 fn test_locality_policy_always_eligible() {
686 let policy = LocalityPolicy::default();
687 let request = mock_request();
688 let candidate = mock_candidate(100, 1.0);
689 assert!(policy.is_eligible(&candidate, &request));
690 }
691
692 #[test]
693 fn test_locality_policy_name() {
694 let policy = LocalityPolicy::default();
695 assert_eq!(policy.name(), "locality");
696 }
697
698 #[test]
703 fn test_privacy_policy_name() {
704 let policy = PrivacyPolicy::default();
705 assert_eq!(policy.name(), "privacy");
706 }
707
708 #[test]
709 fn test_privacy_policy_score_always_one() {
710 let policy = PrivacyPolicy::default();
711 let request = mock_request();
712 let candidate = mock_candidate(100, 1.0);
713 let score = policy.score(&candidate, &request);
714 assert!((score - 1.0).abs() < f64::EPSILON);
715 }
716
717 #[test]
718 fn test_privacy_policy_unknown_region_defaults_internal() {
719 let policy = PrivacyPolicy::default(); let mut request = mock_request();
722 request.qos.privacy = PrivacyLevel::Internal;
723
724 let candidate = mock_candidate(100, 1.0);
725 assert!(policy.is_eligible(&candidate, &request));
727 }
728
729 #[test]
730 fn test_privacy_policy_unknown_region_fails_confidential() {
731 let policy = PrivacyPolicy::default();
732
733 let mut request = mock_request();
734 request.qos.privacy = PrivacyLevel::Confidential;
735
736 let candidate = mock_candidate(100, 1.0);
737 assert!(!policy.is_eligible(&candidate, &request));
739 }
740
741 #[test]
742 fn test_privacy_policy_restricted_region() {
743 let policy = PrivacyPolicy::default()
744 .with_region(RegionId("us-west".to_string()), PrivacyLevel::Restricted);
745
746 let mut request = mock_request();
747 request.qos.privacy = PrivacyLevel::Restricted;
748
749 let candidate = mock_candidate(100, 1.0);
750 assert!(policy.is_eligible(&candidate, &request));
752 }
753
754 #[test]
755 fn test_privacy_policy_public_request_any_region() {
756 let policy = PrivacyPolicy::default()
757 .with_region(RegionId("us-west".to_string()), PrivacyLevel::Public);
758
759 let mut request = mock_request();
760 request.qos.privacy = PrivacyLevel::Public;
761
762 let candidate = mock_candidate(100, 1.0);
763 assert!(policy.is_eligible(&candidate, &request));
764 }
765
766 #[test]
771 fn test_cost_policy_default() {
772 let policy = CostPolicy::default();
773 assert_eq!(policy.weight, 1.0);
774 assert!(policy.region_costs.is_empty());
775 }
776
777 #[test]
778 fn test_cost_policy_with_region_cost() {
779 let policy = CostPolicy::default()
780 .with_region_cost(RegionId("us-west".to_string()), 0.3)
781 .with_region_cost(RegionId("eu-west".to_string()), 0.7);
782
783 assert_eq!(policy.region_costs.len(), 2);
784 }
785
786 #[test]
787 fn test_cost_policy_clamps_cost() {
788 let policy = CostPolicy::default()
789 .with_region_cost(RegionId("r1".to_string()), 1.5)
790 .with_region_cost(RegionId("r2".to_string()), -0.5);
791
792 assert_eq!(
793 *policy
794 .region_costs
795 .get(&RegionId("r1".to_string()))
796 .expect("r1"),
797 1.0
798 );
799 assert_eq!(
800 *policy
801 .region_costs
802 .get(&RegionId("r2".to_string()))
803 .expect("r2"),
804 0.0
805 );
806 }
807
808 #[test]
809 fn test_cost_policy_high_tolerance_prefers_throughput() {
810 let policy = CostPolicy::default().with_region_cost(RegionId("us-west".to_string()), 0.9);
811
812 let mut request = mock_request();
813 request.qos.cost_tolerance = 80; let candidate = mock_candidate(100, 1.0); let score = policy.score(&candidate, &request);
817
818 assert!((score - 0.8).abs() < f64::EPSILON);
820 }
821
822 #[test]
823 fn test_cost_policy_low_tolerance_prefers_cheap() {
824 let policy = CostPolicy::default().with_region_cost(RegionId("us-west".to_string()), 0.8);
825
826 let mut request = mock_request();
827 request.qos.cost_tolerance = 20; let candidate = mock_candidate(100, 1.0);
830 let score = policy.score(&candidate, &request);
831
832 assert!((score - 0.2).abs() < f64::EPSILON);
834 }
835
836 #[test]
837 fn test_cost_policy_unknown_region_defaults() {
838 let policy = CostPolicy::default(); let mut request = mock_request();
841 request.qos.cost_tolerance = 20; let candidate = mock_candidate(100, 1.0);
844 let score = policy.score(&candidate, &request);
845
846 assert!((score - 0.5).abs() < f64::EPSILON);
848 }
849
850 #[test]
851 fn test_cost_policy_always_eligible() {
852 let policy = CostPolicy::default();
853 let request = mock_request();
854 let candidate = mock_candidate(100, 1.0);
855 assert!(policy.is_eligible(&candidate, &request));
856 }
857
858 #[test]
859 fn test_cost_policy_name() {
860 let policy = CostPolicy::default();
861 assert_eq!(policy.name(), "cost");
862 }
863
864 #[test]
865 fn test_cost_policy_tolerance_boundary_50() {
866 let policy = CostPolicy::default().with_region_cost(RegionId("us-west".to_string()), 0.3);
868
869 let mut request = mock_request();
870 request.qos.cost_tolerance = 50;
871
872 let candidate = mock_candidate(100, 1.0);
873 let score = policy.score(&candidate, &request);
874
875 assert!((score - 0.7).abs() < f64::EPSILON);
877 }
878
879 #[test]
884 fn test_health_policy_default() {
885 let policy = HealthPolicy::default();
886 assert_eq!(policy.weight, 2.0);
887 assert_eq!(policy.healthy_score, 1.0);
888 assert_eq!(policy.degraded_score, 0.3);
889 }
890
891 #[test]
892 fn test_health_policy_name() {
893 let policy = HealthPolicy::default();
894 assert_eq!(policy.name(), "health");
895 }
896
897 #[test]
898 fn test_health_policy_eligibility_zero_health() {
899 let policy = HealthPolicy::default();
900 let request = mock_request();
901
902 let dead = mock_candidate(100, 0.0);
903 assert!(!policy.is_eligible(&dead, &request));
904 }
905
906 #[test]
907 fn test_health_policy_eligibility_positive_health() {
908 let policy = HealthPolicy::default();
909 let request = mock_request();
910
911 let alive = mock_candidate(100, 0.01);
912 assert!(policy.is_eligible(&alive, &request));
913 }
914
915 #[test]
916 fn test_health_policy_score_scales_with_weight() {
917 let policy = HealthPolicy {
918 weight: 3.0,
919 ..Default::default()
920 };
921 let request = mock_request();
922
923 let healthy = mock_candidate(100, 1.0);
924 let score = policy.score(&healthy, &request);
925 assert!((score - 3.0).abs() < f64::EPSILON);
926 }
927
928 #[test]
933 fn test_composite_policy_new_empty() {
934 let policy = CompositePolicy::new();
935 let request = mock_request();
936 let candidate = mock_candidate(100, 1.0);
937
938 let score = policy.score(&candidate, &request);
940 assert!((score - 1.0).abs() < f64::EPSILON);
941 }
942
943 #[test]
944 fn test_composite_policy_default_is_enterprise() {
945 let policy = CompositePolicy::default();
946 assert_eq!(policy.name(), "composite");
947 }
948
949 #[test]
950 fn test_composite_policy_name() {
951 let policy = CompositePolicy::new();
952 assert_eq!(policy.name(), "composite");
953 }
954
955 #[test]
956 fn test_composite_policy_eligibility_all_pass() {
957 let policy = CompositePolicy::new()
958 .with_policy(HealthPolicy::default())
959 .with_policy(LatencyPolicy::default());
960 let request = mock_request();
961
962 let good = mock_candidate(100, 1.0);
963 assert!(policy.is_eligible(&good, &request));
964 }
965
966 #[test]
967 fn test_composite_policy_eligibility_one_fails() {
968 let policy = CompositePolicy::new()
970 .with_policy(HealthPolicy::default())
971 .with_policy(LatencyPolicy::default());
972 let request = mock_request();
973
974 let dead = mock_candidate(100, 0.0);
975 assert!(!policy.is_eligible(&dead, &request));
976 }
977
978 #[test]
979 fn test_composite_policy_eligibility_empty_passes() {
980 let policy = CompositePolicy::new();
981 let request = mock_request();
982 let candidate = mock_candidate(100, 1.0);
983 assert!(policy.is_eligible(&candidate, &request));
984 }
985
986 #[test]
987 fn test_composite_policy_score_averages() {
988 let policy = CompositePolicy::new()
990 .with_policy(HealthPolicy {
991 weight: 1.0,
992 ..Default::default()
993 })
994 .with_policy(LatencyPolicy::default());
995 let request = mock_request();
996 let candidate = mock_candidate(0, 1.0);
997
998 let score = policy.score(&candidate, &request);
1001 assert!((score - 1.0).abs() < f64::EPSILON);
1002 }
1003
1004 #[test]
1009 fn test_routing_policy_latency() {
1010 let _policy = RoutingPolicy::latency();
1011 }
1012
1013 #[test]
1014 fn test_routing_policy_locality() {
1015 let _policy = RoutingPolicy::locality();
1016 }
1017
1018 #[test]
1019 fn test_routing_policy_privacy() {
1020 let _policy = RoutingPolicy::privacy();
1021 }
1022
1023 #[test]
1024 fn test_routing_policy_cost() {
1025 let _policy = RoutingPolicy::cost();
1026 }
1027
1028 #[test]
1029 fn test_routing_policy_health() {
1030 let _policy = RoutingPolicy::health();
1031 }
1032
1033 #[test]
1034 fn test_routing_policy_enterprise() {
1035 let _policy = RoutingPolicy::enterprise();
1036 }
1037
1038 #[test]
1043 fn test_route_scores_default() {
1044 let scores = RouteScores::default();
1045 assert_eq!(scores.latency_score, 0.5);
1046 assert_eq!(scores.throughput_score, 0.5);
1047 assert_eq!(scores.cost_score, 0.5);
1048 assert_eq!(scores.locality_score, 0.5);
1049 assert_eq!(scores.health_score, 1.0);
1050 assert_eq!(scores.total, 0.5);
1051 }
1052
1053 fn mock_candidate_in_region(
1058 region: &str,
1059 latency_ms: u64,
1060 health_score: f64,
1061 ) -> RouteCandidate {
1062 RouteCandidate {
1063 target: RouteTarget {
1064 node_id: NodeId("node1".to_string()),
1065 region_id: RegionId(region.to_string()),
1066 endpoint: "http://node1:8080".to_string(),
1067 estimated_latency: Duration::from_millis(latency_ms),
1068 score: 0.0,
1069 },
1070 scores: RouteScores {
1071 latency_score: 1.0 - (latency_ms as f64 / 5000.0),
1072 throughput_score: 0.8,
1073 cost_score: 0.5,
1074 locality_score: 0.7,
1075 health_score,
1076 total: 0.0,
1077 },
1078 eligible: true,
1079 rejection_reason: None,
1080 }
1081 }
1082
1083 #[test]
1084 fn test_privacy_policy_with_specific_region_candidate() {
1085 let policy = PrivacyPolicy::default()
1086 .with_region(RegionId("eu-west".to_string()), PrivacyLevel::Restricted)
1087 .with_region(RegionId("us-west".to_string()), PrivacyLevel::Internal);
1088
1089 let mut request = mock_request();
1090 request.qos.privacy = PrivacyLevel::Restricted;
1091
1092 let eu = mock_candidate_in_region("eu-west", 100, 1.0);
1093 let us = mock_candidate_in_region("us-west", 50, 1.0);
1094
1095 assert!(policy.is_eligible(&eu, &request));
1096 assert!(!policy.is_eligible(&us, &request));
1097 }
1098}
1099
1100impl Default for RouteScores {
1101 fn default() -> Self {
1102 Self {
1103 latency_score: 0.5,
1104 throughput_score: 0.5,
1105 cost_score: 0.5,
1106 locality_score: 0.5,
1107 health_score: 1.0,
1108 total: 0.5,
1109 }
1110 }
1111}