1use std::collections::HashMap;
15
16#[inline]
22fn xorshift64(state: &mut u64) -> u64 {
23 let mut x = *state;
24 x ^= x << 13;
25 x ^= x >> 7;
26 x ^= x << 17;
27 *state = x;
28 x
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub struct RouteHandlerId(pub String);
38
39impl RouteHandlerId {
40 pub fn new(id: impl Into<String>) -> Self {
42 Self(id.into())
43 }
44
45 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49}
50
51impl std::fmt::Display for RouteHandlerId {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.write_str(&self.0)
54 }
55}
56
57#[derive(Debug, Clone)]
61pub struct RouteDefinition {
62 pub id: RouteHandlerId,
64 pub name: String,
66 pub embedding: Vec<f64>,
68 pub metadata: HashMap<String, String>,
70 pub max_queries_per_second: f64,
72 pub priority: u8,
74}
75
76impl RouteDefinition {
77 pub fn new(id: impl Into<String>, name: impl Into<String>, embedding: Vec<f64>) -> Self {
79 Self {
80 id: RouteHandlerId::new(id),
81 name: name.into(),
82 embedding,
83 metadata: HashMap::new(),
84 max_queries_per_second: f64::MAX,
85 priority: 128,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
94pub struct V2RoutingDecision {
95 pub query_text: String,
97 pub query_embedding: Vec<f64>,
99 pub matched_route: RouteHandlerId,
101 pub similarity: f64,
104 pub fallback_used: bool,
106 pub latency_hint_ms: u32,
108}
109
110#[derive(Debug, Clone)]
114pub enum FallbackStrategy {
115 UseDefault { default_id: RouteHandlerId },
117 RoundRobin,
119 LeastLoaded,
121 Random { seed: u64 },
123}
124
125#[derive(Debug, Clone)]
129pub struct RouterV2Config {
130 pub similarity_threshold: f64,
132 pub max_routes: usize,
134 pub fallback: FallbackStrategy,
136 pub enable_load_balancing: bool,
138}
139
140impl Default for RouterV2Config {
141 fn default() -> Self {
142 Self {
143 similarity_threshold: 0.7,
144 max_routes: 100,
145 fallback: FallbackStrategy::RoundRobin,
146 enable_load_balancing: true,
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
155pub struct RouteStats {
156 pub handler_id: String,
158 pub total_routed: u64,
160 pub avg_similarity: f64,
162 pub last_routed_at: u64,
164}
165
166impl RouteStats {
167 fn new(handler_id: impl Into<String>) -> Self {
168 Self {
169 handler_id: handler_id.into(),
170 total_routed: 0,
171 avg_similarity: 0.0,
172 last_routed_at: 0,
173 }
174 }
175
176 fn record_direct(&mut self, similarity: f64, now: u64) {
178 self.total_routed += 1;
180 let n = self.total_routed as f64;
181 self.avg_similarity += (similarity - self.avg_similarity) / n;
182 self.last_routed_at = now;
183 }
184
185 fn record_fallback(&mut self, now: u64) {
187 self.total_routed += 1;
188 self.last_routed_at = now;
189 }
190}
191
192#[derive(Debug, Clone)]
196pub struct RouterV2Stats {
197 pub route_count: usize,
199 pub total_queries: u64,
201 pub fallback_count: u64,
203 pub fallback_rate: f64,
205 pub avg_similarity: f64,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum RouterV2Error {
214 RouteAlreadyExists(String),
216 RouteNotFound(String),
218 MaxRoutesReached,
220 EmptyRoutes,
222}
223
224impl std::fmt::Display for RouterV2Error {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 Self::RouteAlreadyExists(id) => write!(f, "route already exists: {id}"),
228 Self::RouteNotFound(id) => write!(f, "route not found: {id}"),
229 Self::MaxRoutesReached => write!(f, "maximum route count reached"),
230 Self::EmptyRoutes => write!(f, "no routes registered"),
231 }
232 }
233}
234
235impl std::error::Error for RouterV2Error {}
236
237pub struct SemanticRouterV2 {
253 pub config: RouterV2Config,
255 pub routes: Vec<RouteDefinition>,
257 pub route_stats: HashMap<String, RouteStats>,
259 pub rr_index: usize,
261 pub total_queries: u64,
263 pub fallback_count: u64,
265 pub rng_state: u64,
267}
268
269impl SemanticRouterV2 {
270 pub fn new(config: RouterV2Config) -> Self {
274 Self {
275 config,
276 routes: Vec::new(),
277 route_stats: HashMap::new(),
278 rr_index: 0,
279 total_queries: 0,
280 fallback_count: 0,
281 rng_state: 0xCAFEBABE_12345678_u64,
282 }
283 }
284
285 pub fn add_route(&mut self, route: RouteDefinition) -> Result<(), RouterV2Error> {
292 if self.routes.len() >= self.config.max_routes {
293 return Err(RouterV2Error::MaxRoutesReached);
294 }
295 if self.routes.iter().any(|r| r.id == route.id) {
296 return Err(RouterV2Error::RouteAlreadyExists(route.id.0.clone()));
297 }
298 let id_str = route.id.0.clone();
299 self.routes.push(route);
300 self.route_stats
301 .entry(id_str.clone())
302 .or_insert_with(|| RouteStats::new(id_str));
303 Ok(())
304 }
305
306 pub fn remove_route(&mut self, id: &RouteHandlerId) -> bool {
310 let before = self.routes.len();
311 self.routes.retain(|r| r.id != *id);
312 let removed = self.routes.len() < before;
313 if removed {
314 self.route_stats.remove(&id.0);
315 if !self.routes.is_empty() {
317 self.rr_index %= self.routes.len();
318 } else {
319 self.rr_index = 0;
320 }
321 }
322 removed
323 }
324
325 pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
329 let min_len = a.len().min(b.len());
330 if min_len == 0 {
331 return 0.0;
332 }
333 let mut dot = 0.0_f64;
334 let mut mag_a = 0.0_f64;
335 let mut mag_b = 0.0_f64;
336 for i in 0..min_len {
337 dot += a[i] * b[i];
338 mag_a += a[i] * a[i];
339 mag_b += b[i] * b[i];
340 }
341 let denom = mag_a.sqrt() * mag_b.sqrt();
342 if denom < f64::EPSILON {
343 0.0
344 } else {
345 (dot / denom).clamp(-1.0, 1.0)
346 }
347 }
348
349 pub fn best_match(&self, query_embedding: &[f64]) -> Option<(usize, f64)> {
355 if self.routes.is_empty() {
356 return None;
357 }
358 let mut best_idx = 0usize;
359 let mut best_sim = f64::NEG_INFINITY;
360 for (i, route) in self.routes.iter().enumerate() {
361 let sim = Self::cosine_similarity(query_embedding, &route.embedding);
362 if sim > best_sim {
363 best_sim = sim;
364 best_idx = i;
365 }
366 }
367 if best_sim >= self.config.similarity_threshold {
368 Some((best_idx, best_sim))
369 } else {
370 None
371 }
372 }
373
374 pub fn route(
380 &mut self,
381 query_text: String,
382 query_embedding: Vec<f64>,
383 now: u64,
384 ) -> V2RoutingDecision {
385 self.total_queries += 1;
386
387 if let Some((idx, similarity)) = self.best_match(&query_embedding) {
389 let matched_id = self.routes[idx].id.clone();
390 self.update_stats_direct(&matched_id, similarity, now);
391 return V2RoutingDecision {
392 query_text,
393 query_embedding,
394 matched_route: matched_id,
395 similarity,
396 fallback_used: false,
397 latency_hint_ms: 0,
398 };
399 }
400
401 self.fallback_count += 1;
403 let fallback_id = self.apply_fallback(now);
404 V2RoutingDecision {
405 query_text,
406 query_embedding,
407 matched_route: fallback_id,
408 similarity: 0.0,
409 fallback_used: true,
410 latency_hint_ms: 0,
411 }
412 }
413
414 pub fn route_batch(
418 &mut self,
419 queries: Vec<(String, Vec<f64>)>,
420 now: u64,
421 ) -> Vec<V2RoutingDecision> {
422 queries
423 .into_iter()
424 .map(|(text, emb)| self.route(text, emb, now))
425 .collect()
426 }
427
428 pub fn top_routes(&self, k: usize) -> Vec<&RouteStats> {
430 let mut stats: Vec<&RouteStats> = self.route_stats.values().collect();
431 stats.sort_by_key(|s| std::cmp::Reverse(s.total_routed));
432 stats.truncate(k);
433 stats
434 }
435
436 pub fn route_by_id(&self, id: &RouteHandlerId) -> Option<&RouteDefinition> {
438 self.routes.iter().find(|r| r.id == *id)
439 }
440
441 pub fn update_route_embedding(&mut self, id: &RouteHandlerId, new_embedding: Vec<f64>) -> bool {
445 if let Some(route) = self.routes.iter_mut().find(|r| r.id == *id) {
446 route.embedding = new_embedding;
447 true
448 } else {
449 false
450 }
451 }
452
453 pub fn router_stats(&self) -> RouterV2Stats {
455 let route_count = self.routes.len();
456 let fallback_rate = if self.total_queries == 0 {
457 0.0
458 } else {
459 self.fallback_count as f64 / self.total_queries as f64
460 };
461
462 let (total_sim, total_direct) =
464 self.route_stats
465 .values()
466 .fold((0.0_f64, 0u64), |(acc_sim, acc_n), s| {
467 (
470 acc_sim + s.avg_similarity * s.total_routed as f64,
471 acc_n + s.total_routed,
472 )
473 });
474 let avg_similarity = if total_direct == 0 {
475 0.0
476 } else {
477 total_sim / total_direct as f64
478 };
479
480 RouterV2Stats {
481 route_count,
482 total_queries: self.total_queries,
483 fallback_count: self.fallback_count,
484 fallback_rate,
485 avg_similarity,
486 }
487 }
488
489 fn update_stats_direct(&mut self, id: &RouteHandlerId, similarity: f64, now: u64) {
492 let stats = self
493 .route_stats
494 .entry(id.0.clone())
495 .or_insert_with(|| RouteStats::new(id.0.clone()));
496 stats.record_direct(similarity, now);
497 }
498
499 fn update_stats_fallback(&mut self, id: &RouteHandlerId, now: u64) {
500 let stats = self
501 .route_stats
502 .entry(id.0.clone())
503 .or_insert_with(|| RouteStats::new(id.0.clone()));
504 stats.record_fallback(now);
505 }
506
507 fn apply_fallback(&mut self, now: u64) -> RouteHandlerId {
513 if self.routes.is_empty() {
514 return RouteHandlerId::new("__no_routes__");
516 }
517
518 let selected_id = match self.config.fallback.clone() {
519 FallbackStrategy::UseDefault { default_id } => {
520 if self.routes.iter().any(|r| r.id == default_id) {
522 default_id
523 } else {
524 self.round_robin_id()
525 }
526 }
527 FallbackStrategy::RoundRobin => self.round_robin_id(),
528 FallbackStrategy::LeastLoaded => self.least_loaded_id(),
529 FallbackStrategy::Random { .. } => self.random_id(),
530 };
531
532 self.update_stats_fallback(&selected_id.clone(), now);
533 selected_id
534 }
535
536 fn round_robin_id(&mut self) -> RouteHandlerId {
537 let idx = self.rr_index % self.routes.len();
538 self.rr_index = idx.wrapping_add(1);
539 self.routes[idx].id.clone()
540 }
541
542 fn least_loaded_id(&self) -> RouteHandlerId {
543 self.routes
545 .iter()
546 .min_by_key(|r| self.route_stats.get(&r.id.0).map_or(0, |s| s.total_routed))
547 .map(|r| r.id.clone())
548 .unwrap_or_else(|| self.routes[0].id.clone())
549 }
550
551 fn random_id(&mut self) -> RouteHandlerId {
552 let val = xorshift64(&mut self.rng_state);
553 let idx = (val as usize) % self.routes.len();
554 self.routes[idx].id.clone()
555 }
556}
557
558#[cfg(test)]
563mod tests {
564 use super::{
565 FallbackStrategy, RouteDefinition, RouteHandlerId, RouterV2Config, RouterV2Error,
566 SemanticRouterV2,
567 };
568 use std::collections::HashMap;
569
570 fn make_router() -> SemanticRouterV2 {
573 SemanticRouterV2::new(RouterV2Config::default())
574 }
575
576 fn unit_route(id: &str, dim: usize, val: f64) -> RouteDefinition {
577 RouteDefinition::new(id, id, vec![val; dim])
578 }
579
580 fn orthogonal_route(id: &str, dim: usize, hot_index: usize) -> RouteDefinition {
581 let mut emb = vec![0.0f64; dim];
582 if hot_index < dim {
583 emb[hot_index] = 1.0;
584 }
585 RouteDefinition::new(id, id, emb)
586 }
587
588 #[test]
591 fn test_new_router_has_correct_defaults() {
592 let router = make_router();
593 assert_eq!(router.total_queries, 0);
594 assert_eq!(router.fallback_count, 0);
595 assert!(router.routes.is_empty());
596 assert_eq!(router.rng_state, 0xCAFEBABE_12345678_u64);
597 }
598
599 #[test]
600 fn test_new_router_with_custom_config() {
601 let cfg = RouterV2Config {
602 similarity_threshold: 0.9,
603 max_routes: 5,
604 fallback: FallbackStrategy::RoundRobin,
605 enable_load_balancing: false,
606 };
607 let router = SemanticRouterV2::new(cfg);
608 assert_eq!(router.config.similarity_threshold, 0.9);
609 assert_eq!(router.config.max_routes, 5);
610 assert!(!router.config.enable_load_balancing);
611 }
612
613 #[test]
616 fn test_add_route_success() {
617 let mut router = make_router();
618 let route = unit_route("r1", 4, 1.0);
619 assert!(router.add_route(route).is_ok());
620 assert_eq!(router.routes.len(), 1);
621 }
622
623 #[test]
624 fn test_add_duplicate_route_returns_error() {
625 let mut router = make_router();
626 router.add_route(unit_route("r1", 4, 1.0)).ok();
627 let err = router
628 .add_route(unit_route("r1", 4, 0.5))
629 .expect_err("should fail");
630 assert_eq!(err, RouterV2Error::RouteAlreadyExists("r1".into()));
631 }
632
633 #[test]
634 fn test_max_routes_reached() {
635 let cfg = RouterV2Config {
636 max_routes: 2,
637 ..RouterV2Config::default()
638 };
639 let mut router = SemanticRouterV2::new(cfg);
640 router.add_route(unit_route("r1", 4, 1.0)).ok();
641 router.add_route(unit_route("r2", 4, 0.5)).ok();
642 let err = router
643 .add_route(unit_route("r3", 4, 0.25))
644 .expect_err("should fail");
645 assert_eq!(err, RouterV2Error::MaxRoutesReached);
646 }
647
648 #[test]
651 fn test_remove_existing_route() {
652 let mut router = make_router();
653 router.add_route(unit_route("r1", 4, 1.0)).ok();
654 let removed = router.remove_route(&RouteHandlerId::new("r1"));
655 assert!(removed);
656 assert!(router.routes.is_empty());
657 }
658
659 #[test]
660 fn test_remove_nonexistent_route_returns_false() {
661 let mut router = make_router();
662 let removed = router.remove_route(&RouteHandlerId::new("ghost"));
663 assert!(!removed);
664 }
665
666 #[test]
667 fn test_remove_cleans_up_stats() {
668 let mut router = make_router();
669 router.add_route(unit_route("r1", 4, 1.0)).ok();
670 router.remove_route(&RouteHandlerId::new("r1"));
671 assert!(!router.route_stats.contains_key("r1"));
672 }
673
674 #[test]
677 fn test_cosine_similarity_identical_vectors() {
678 let v = vec![1.0, 2.0, 3.0];
679 let sim = SemanticRouterV2::cosine_similarity(&v, &v);
680 assert!((sim - 1.0).abs() < 1e-9);
681 }
682
683 #[test]
684 fn test_cosine_similarity_orthogonal_vectors() {
685 let a = vec![1.0, 0.0, 0.0];
686 let b = vec![0.0, 1.0, 0.0];
687 let sim = SemanticRouterV2::cosine_similarity(&a, &b);
688 assert!(sim.abs() < 1e-9);
689 }
690
691 #[test]
692 fn test_cosine_similarity_opposite_vectors() {
693 let a = vec![1.0, 0.0];
694 let b = vec![-1.0, 0.0];
695 let sim = SemanticRouterV2::cosine_similarity(&a, &b);
696 assert!((sim - (-1.0)).abs() < 1e-9);
697 }
698
699 #[test]
700 fn test_cosine_similarity_zero_vector() {
701 let a = vec![0.0, 0.0, 0.0];
702 let b = vec![1.0, 2.0, 3.0];
703 let sim = SemanticRouterV2::cosine_similarity(&a, &b);
704 assert_eq!(sim, 0.0);
705 }
706
707 #[test]
708 fn test_cosine_similarity_empty_slices() {
709 let sim = SemanticRouterV2::cosine_similarity(&[], &[]);
710 assert_eq!(sim, 0.0);
711 }
712
713 #[test]
714 fn test_cosine_similarity_mismatched_lengths() {
715 let a = vec![1.0, 0.0, 0.0];
716 let b = vec![1.0, 0.0];
717 let sim = SemanticRouterV2::cosine_similarity(&a, &b);
719 assert!((sim - 1.0).abs() < 1e-9);
720 }
721
722 #[test]
723 fn test_cosine_similarity_known_value() {
724 let a = vec![3.0, 4.0];
725 let b = vec![4.0, 3.0];
726 let sim = SemanticRouterV2::cosine_similarity(&a, &b);
728 assert!((sim - 0.96).abs() < 1e-9);
729 }
730
731 #[test]
734 fn test_best_match_empty_router() {
735 let router = make_router();
736 assert!(router.best_match(&[1.0, 0.0]).is_none());
737 }
738
739 #[test]
740 fn test_best_match_above_threshold() {
741 let mut router = make_router();
742 router.add_route(orthogonal_route("r1", 3, 0)).ok(); let result = router.best_match(&[1.0, 0.0, 0.0]);
744 assert!(result.is_some());
745 let (idx, sim) = result.expect("expected match");
746 assert_eq!(idx, 0);
747 assert!((sim - 1.0).abs() < 1e-9);
748 }
749
750 #[test]
751 fn test_best_match_below_threshold() {
752 let mut router = SemanticRouterV2::new(RouterV2Config {
753 similarity_threshold: 0.99,
754 ..RouterV2Config::default()
755 });
756 router.add_route(orthogonal_route("r1", 3, 0)).ok();
757 let query = vec![0.5_f64, 0.5, 0.0];
759 assert!(router.best_match(&query).is_none());
760 }
761
762 #[test]
763 fn test_best_match_picks_highest_similarity() {
764 let mut router = make_router();
765 router.add_route(orthogonal_route("r1", 3, 0)).ok(); router.add_route(orthogonal_route("r2", 3, 1)).ok(); let result = router.best_match(&[0.1, 0.99, 0.0]);
769 let (idx, _) = result.expect("expected match");
770 assert_eq!(idx, 1);
771 }
772
773 #[test]
776 fn test_route_direct_match() {
777 let mut router = make_router();
778 router.add_route(orthogonal_route("r1", 3, 0)).ok();
779 let decision = router.route("query".into(), vec![1.0, 0.0, 0.0], 1000);
780 assert!(!decision.fallback_used);
781 assert_eq!(decision.matched_route, RouteHandlerId::new("r1"));
782 assert!((decision.similarity - 1.0).abs() < 1e-9);
783 }
784
785 #[test]
786 fn test_route_fallback_when_no_match() {
787 let mut router = SemanticRouterV2::new(RouterV2Config {
788 similarity_threshold: 0.99,
789 ..RouterV2Config::default()
790 });
791 router.add_route(orthogonal_route("r1", 3, 0)).ok();
792 let decision = router.route("query".into(), vec![0.0, 1.0, 0.0], 1000);
793 assert!(decision.fallback_used);
794 assert_eq!(decision.similarity, 0.0);
795 }
796
797 #[test]
798 fn test_route_increments_total_queries() {
799 let mut router = make_router();
800 router.add_route(unit_route("r1", 3, 1.0)).ok();
801 router.route("q".into(), vec![1.0, 1.0, 1.0], 0);
802 router.route("q".into(), vec![1.0, 1.0, 1.0], 1);
803 assert_eq!(router.total_queries, 2);
804 }
805
806 #[test]
807 fn test_route_increments_fallback_count() {
808 let mut router = SemanticRouterV2::new(RouterV2Config {
809 similarity_threshold: 0.99,
810 ..RouterV2Config::default()
811 });
812 router.add_route(orthogonal_route("r1", 3, 0)).ok();
813 router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
814 assert_eq!(router.fallback_count, 1);
815 }
816
817 #[test]
818 fn test_route_updates_stats() {
819 let mut router = make_router();
820 router.add_route(orthogonal_route("r1", 3, 0)).ok();
821 router.route("q".into(), vec![1.0, 0.0, 0.0], 42);
822 let stats = router.route_stats.get("r1").expect("stats should exist");
823 assert_eq!(stats.total_routed, 1);
824 assert_eq!(stats.last_routed_at, 42);
825 }
826
827 #[test]
830 fn test_fallback_round_robin_cycles() {
831 let mut router = SemanticRouterV2::new(RouterV2Config {
832 similarity_threshold: 2.0, fallback: FallbackStrategy::RoundRobin,
834 ..RouterV2Config::default()
835 });
836 router.add_route(unit_route("r1", 3, 1.0)).ok();
837 router.add_route(unit_route("r2", 3, 0.0)).ok();
838 let d1 = router.route("q".into(), vec![0.5, 0.5, 0.5], 0);
839 let d2 = router.route("q".into(), vec![0.5, 0.5, 0.5], 0);
840 let d3 = router.route("q".into(), vec![0.5, 0.5, 0.5], 0);
841 assert_eq!(d1.matched_route.0, "r1");
843 assert_eq!(d2.matched_route.0, "r2");
844 assert_eq!(d3.matched_route.0, "r1");
845 }
846
847 #[test]
848 fn test_fallback_use_default_valid_id() {
849 let default_id = RouteHandlerId::new("default_handler");
850 let mut router = SemanticRouterV2::new(RouterV2Config {
851 similarity_threshold: 2.0,
852 fallback: FallbackStrategy::UseDefault {
853 default_id: default_id.clone(),
854 },
855 ..RouterV2Config::default()
856 });
857 router.add_route(unit_route("r1", 3, 1.0)).ok();
858 router.add_route(unit_route("default_handler", 3, 0.0)).ok();
859 let decision = router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
860 assert_eq!(decision.matched_route, default_id);
861 }
862
863 #[test]
864 fn test_fallback_use_default_invalid_falls_to_rr() {
865 let mut router = SemanticRouterV2::new(RouterV2Config {
866 similarity_threshold: 2.0,
867 fallback: FallbackStrategy::UseDefault {
868 default_id: RouteHandlerId::new("ghost"),
869 },
870 ..RouterV2Config::default()
871 });
872 router.add_route(unit_route("r1", 3, 1.0)).ok();
873 let decision = router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
875 assert!(decision.fallback_used);
876 }
877
878 #[test]
879 fn test_fallback_least_loaded_picks_min() {
880 let mut router = SemanticRouterV2::new(RouterV2Config {
881 similarity_threshold: 0.9,
882 fallback: FallbackStrategy::LeastLoaded,
883 ..RouterV2Config::default()
884 });
885 router.add_route(orthogonal_route("r1", 3, 0)).ok(); router.add_route(orthogonal_route("r2", 3, 1)).ok(); router.route("q".into(), vec![1.0, 0.0, 0.0], 0);
889 router.route("q".into(), vec![1.0, 0.0, 0.0], 0);
890 let decision = router.route("q".into(), vec![0.0, 0.0, 1.0], 0);
892 assert!(decision.fallback_used);
893 assert_eq!(decision.matched_route.0, "r2");
894 }
895
896 #[test]
897 fn test_fallback_random_is_deterministic_with_seed() {
898 let cfg = RouterV2Config {
900 similarity_threshold: 2.0,
901 fallback: FallbackStrategy::Random { seed: 0 }, ..RouterV2Config::default()
903 };
904 let mut r1 = SemanticRouterV2::new(cfg.clone());
905 r1.rng_state = 42;
906 let mut r2 = SemanticRouterV2::new(cfg);
907 r2.rng_state = 42;
908 r1.add_route(unit_route("a", 3, 1.0)).ok();
909 r1.add_route(unit_route("b", 3, 0.0)).ok();
910 r2.add_route(unit_route("a", 3, 1.0)).ok();
911 r2.add_route(unit_route("b", 3, 0.0)).ok();
912 let d1 = r1.route("q".into(), vec![0.0; 3], 0);
913 let d2 = r2.route("q".into(), vec![0.0; 3], 0);
914 assert_eq!(d1.matched_route, d2.matched_route);
915 }
916
917 #[test]
920 fn test_route_batch_returns_correct_count() {
921 let mut router = make_router();
922 router.add_route(unit_route("r1", 3, 1.0)).ok();
923 let queries = vec![
924 ("q1".to_string(), vec![1.0, 1.0, 1.0]),
925 ("q2".to_string(), vec![1.0, 1.0, 1.0]),
926 ("q3".to_string(), vec![1.0, 1.0, 1.0]),
927 ];
928 let decisions = router.route_batch(queries, 0);
929 assert_eq!(decisions.len(), 3);
930 }
931
932 #[test]
933 fn test_route_batch_updates_total_queries() {
934 let mut router = make_router();
935 router.add_route(unit_route("r1", 3, 1.0)).ok();
936 let queries: Vec<_> = (0..5).map(|i| (format!("q{i}"), vec![1.0; 3])).collect();
937 router.route_batch(queries, 0);
938 assert_eq!(router.total_queries, 5);
939 }
940
941 #[test]
944 fn test_top_routes_ordering() {
945 let mut router = make_router();
946 router.add_route(orthogonal_route("r1", 3, 0)).ok();
947 router.add_route(orthogonal_route("r2", 3, 1)).ok();
948 for _ in 0..3 {
950 router.route("q".into(), vec![1.0, 0.0, 0.0], 0);
951 }
952 router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
953 let top = router.top_routes(2);
954 assert_eq!(top[0].handler_id, "r1");
955 assert_eq!(top[1].handler_id, "r2");
956 }
957
958 #[test]
959 fn test_top_routes_k_larger_than_registered() {
960 let mut router = make_router();
961 router.add_route(unit_route("r1", 3, 1.0)).ok();
962 router.route("q".into(), vec![1.0; 3], 0);
963 let top = router.top_routes(100);
964 assert_eq!(top.len(), 1);
965 }
966
967 #[test]
970 fn test_route_by_id_found() {
971 let mut router = make_router();
972 router.add_route(unit_route("r1", 3, 1.0)).ok();
973 let found = router.route_by_id(&RouteHandlerId::new("r1"));
974 assert!(found.is_some());
975 assert_eq!(found.expect("should find").name, "r1");
976 }
977
978 #[test]
979 fn test_route_by_id_not_found() {
980 let router = make_router();
981 assert!(router.route_by_id(&RouteHandlerId::new("ghost")).is_none());
982 }
983
984 #[test]
987 fn test_update_route_embedding_success() {
988 let mut router = make_router();
989 router.add_route(orthogonal_route("r1", 3, 0)).ok(); let updated =
991 router.update_route_embedding(&RouteHandlerId::new("r1"), vec![0.0, 1.0, 0.0]);
992 assert!(updated);
993 let route = router
994 .route_by_id(&RouteHandlerId::new("r1"))
995 .expect("must exist");
996 assert_eq!(route.embedding, vec![0.0, 1.0, 0.0]);
997 }
998
999 #[test]
1000 fn test_update_route_embedding_not_found() {
1001 let mut router = make_router();
1002 let updated = router.update_route_embedding(&RouteHandlerId::new("ghost"), vec![1.0]);
1003 assert!(!updated);
1004 }
1005
1006 #[test]
1009 fn test_router_stats_initial() {
1010 let router = make_router();
1011 let stats = router.router_stats();
1012 assert_eq!(stats.route_count, 0);
1013 assert_eq!(stats.total_queries, 0);
1014 assert_eq!(stats.fallback_count, 0);
1015 assert_eq!(stats.fallback_rate, 0.0);
1016 }
1017
1018 #[test]
1019 fn test_router_stats_fallback_rate() {
1020 let mut router = SemanticRouterV2::new(RouterV2Config {
1021 similarity_threshold: 0.99,
1022 ..RouterV2Config::default()
1023 });
1024 router.add_route(orthogonal_route("r1", 3, 0)).ok();
1025 router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
1027 router.route("q".into(), vec![0.0, 0.0, 1.0], 0);
1028 let stats = router.router_stats();
1029 assert_eq!(stats.total_queries, 2);
1030 assert_eq!(stats.fallback_count, 2);
1031 assert!((stats.fallback_rate - 1.0).abs() < 1e-9);
1032 }
1033
1034 #[test]
1037 fn test_route_handler_id_display() {
1038 let id = RouteHandlerId::new("handler_42");
1039 assert_eq!(id.to_string(), "handler_42");
1040 }
1041
1042 #[test]
1043 fn test_route_handler_id_as_str() {
1044 let id = RouteHandlerId::new("x");
1045 assert_eq!(id.as_str(), "x");
1046 }
1047
1048 #[test]
1049 fn test_route_handler_id_equality() {
1050 let a = RouteHandlerId::new("abc");
1051 let b = RouteHandlerId::new("abc");
1052 let c = RouteHandlerId::new("xyz");
1053 assert_eq!(a, b);
1054 assert_ne!(a, c);
1055 }
1056
1057 #[test]
1060 fn test_route_definition_with_metadata() {
1061 let mut route = RouteDefinition::new("r1", "Route 1", vec![1.0, 0.0]);
1062 route.metadata.insert("owner".into(), "team-a".into());
1063 route.priority = 200;
1064 route.max_queries_per_second = 50.0;
1065 assert_eq!(
1066 route.metadata.get("owner").map(|s| s.as_str()),
1067 Some("team-a")
1068 );
1069 assert_eq!(route.priority, 200);
1070 }
1071
1072 #[test]
1075 fn test_xorshift64_non_zero() {
1076 use super::xorshift64;
1077 let mut state = 1u64;
1078 let v = xorshift64(&mut state);
1079 assert_ne!(v, 0);
1080 assert_ne!(state, 1); }
1082
1083 #[test]
1084 fn test_xorshift64_deterministic() {
1085 use super::xorshift64;
1086 let mut s1 = 12345u64;
1087 let mut s2 = 12345u64;
1088 let v1 = xorshift64(&mut s1);
1089 let v2 = xorshift64(&mut s2);
1090 assert_eq!(v1, v2);
1091 }
1092
1093 #[test]
1096 fn test_error_display_route_already_exists() {
1097 let e = RouterV2Error::RouteAlreadyExists("foo".into());
1098 assert!(e.to_string().contains("foo"));
1099 }
1100
1101 #[test]
1102 fn test_error_display_route_not_found() {
1103 let e = RouterV2Error::RouteNotFound("bar".into());
1104 assert!(e.to_string().contains("bar"));
1105 }
1106
1107 #[test]
1108 fn test_error_display_max_routes_reached() {
1109 let e = RouterV2Error::MaxRoutesReached;
1110 assert!(!e.to_string().is_empty());
1111 }
1112
1113 #[test]
1114 fn test_error_display_empty_routes() {
1115 let e = RouterV2Error::EmptyRoutes;
1116 assert!(!e.to_string().is_empty());
1117 }
1118
1119 #[test]
1122 fn test_avg_similarity_welford_convergence() {
1123 let mut router = make_router();
1124 router.add_route(orthogonal_route("r1", 2, 0)).ok(); for t in 0..10u64 {
1127 router.route("q".into(), vec![1.0, 0.0], t);
1128 }
1129 let stats = router.route_stats.get("r1").expect("stats");
1130 assert!((stats.avg_similarity - 1.0).abs() < 1e-9);
1131 assert_eq!(stats.total_routed, 10);
1132 }
1133
1134 #[test]
1135 fn test_remove_and_readd_route() {
1136 let mut router = make_router();
1137 router.add_route(unit_route("r1", 3, 1.0)).ok();
1138 router.remove_route(&RouteHandlerId::new("r1"));
1139 assert!(router.add_route(unit_route("r1", 3, 1.0)).is_ok());
1141 }
1142
1143 #[test]
1144 fn test_route_decision_preserves_query_text() {
1145 let mut router = make_router();
1146 router.add_route(unit_route("r1", 3, 1.0)).ok();
1147 let decision = router.route("hello world".into(), vec![1.0; 3], 0);
1148 assert_eq!(decision.query_text, "hello world");
1149 }
1150
1151 #[test]
1152 fn test_router_with_many_routes() {
1153 let mut router = make_router();
1154 for i in 0..50usize {
1155 let mut emb = vec![0.0f64; 64];
1156 emb[i % 64] = 1.0;
1157 router
1158 .add_route(RouteDefinition::new(
1159 format!("r{i}"),
1160 format!("route {i}"),
1161 emb,
1162 ))
1163 .ok();
1164 }
1165 assert_eq!(router.routes.len(), 50);
1166 let stats = router.router_stats();
1167 assert_eq!(stats.route_count, 50);
1168 }
1169
1170 #[test]
1171 fn test_batch_and_individual_route_agree() {
1172 let mut router_a = make_router();
1173 let mut router_b = make_router();
1174 router_a.add_route(orthogonal_route("r1", 3, 0)).ok();
1175 router_b.add_route(orthogonal_route("r1", 3, 0)).ok();
1176 let queries = vec![
1177 ("q1".into(), vec![1.0, 0.0, 0.0]),
1178 ("q2".into(), vec![1.0, 0.0, 0.0]),
1179 ];
1180 let batch_results = router_a.route_batch(queries, 0);
1181 let ind1 = router_b.route("q1".into(), vec![1.0, 0.0, 0.0], 0);
1182 let ind2 = router_b.route("q2".into(), vec![1.0, 0.0, 0.0], 0);
1183 assert_eq!(batch_results[0].matched_route, ind1.matched_route);
1184 assert_eq!(batch_results[1].matched_route, ind2.matched_route);
1185 }
1186
1187 #[test]
1188 fn test_route_stats_last_routed_at_updated() {
1189 let mut router = make_router();
1190 router.add_route(orthogonal_route("r1", 2, 0)).ok();
1191 router.route("q".into(), vec![1.0, 0.0], 1000);
1192 let stats = router.route_stats.get("r1").expect("stats");
1193 assert_eq!(stats.last_routed_at, 1000);
1194 }
1195
1196 #[test]
1197 fn test_metadata_stored_in_route_definition() {
1198 let mut route = RouteDefinition::new("r1", "Route 1", vec![1.0, 0.0]);
1199 route.metadata = HashMap::from([
1200 ("region".into(), "us-east".into()),
1201 ("tier".into(), "premium".into()),
1202 ]);
1203 let mut router = make_router();
1204 router.add_route(route).ok();
1205 let found = router
1206 .route_by_id(&RouteHandlerId::new("r1"))
1207 .expect("found");
1208 assert_eq!(
1209 found.metadata.get("region").map(|s| s.as_str()),
1210 Some("us-east")
1211 );
1212 }
1213}