1use std::collections::HashMap;
12
13use serde::{Deserialize, Serialize};
14
15#[non_exhaustive]
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23#[serde(rename_all = "snake_case")]
24pub enum TierSelectionStrategy {
25 PreferenceOrder,
27 RoundRobin,
29 LowestCost,
31 Random,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct RoutingConfig {
43 #[serde(default = "default_routing_mode")]
45 pub mode: String,
46
47 #[serde(default)]
50 pub tiers: Vec<ModelTierConfig>,
51
52 #[serde(default, alias = "selectionStrategy")]
54 pub selection_strategy: Option<TierSelectionStrategy>,
55
56 #[serde(default, alias = "fallbackModel")]
59 pub fallback_model: Option<String>,
60
61 #[serde(default)]
63 pub permissions: PermissionsConfig,
64
65 #[serde(default)]
67 pub escalation: EscalationConfig,
68
69 #[serde(default, alias = "costBudgets")]
71 pub cost_budgets: CostBudgetConfig,
72
73 #[serde(default, alias = "rateLimiting")]
75 pub rate_limiting: RateLimitConfig,
76}
77
78fn default_routing_mode() -> String {
79 "static".into()
80}
81
82impl Default for RoutingConfig {
83 fn default() -> Self {
84 Self {
85 mode: default_routing_mode(),
86 tiers: Vec::new(),
87 selection_strategy: None,
88 fallback_model: None,
89 permissions: PermissionsConfig::default(),
90 escalation: EscalationConfig::default(),
91 cost_budgets: CostBudgetConfig::default(),
92 rate_limiting: RateLimitConfig::default(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ModelTierConfig {
106 pub name: String,
108
109 #[serde(default)]
112 pub models: Vec<String>,
113
114 #[serde(default = "default_complexity_range", alias = "complexityRange")]
116 pub complexity_range: [f32; 2],
117
118 #[serde(default, alias = "costPer1kTokens")]
120 pub cost_per_1k_tokens: f64,
121
122 #[serde(default = "default_tier_max_context", alias = "maxContextTokens")]
124 pub max_context_tokens: usize,
125}
126
127fn default_complexity_range() -> [f32; 2] {
128 [0.0, 1.0]
129}
130
131fn default_tier_max_context() -> usize {
132 8192
133}
134
135impl Default for ModelTierConfig {
136 fn default() -> Self {
137 Self {
138 name: String::new(),
139 models: Vec::new(),
140 complexity_range: default_complexity_range(),
141 cost_per_1k_tokens: 0.0,
142 max_context_tokens: default_tier_max_context(),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
155pub struct PermissionsConfig {
156 #[serde(default)]
158 pub zero_trust: PermissionLevelConfig,
159
160 #[serde(default)]
162 pub user: PermissionLevelConfig,
163
164 #[serde(default)]
166 pub admin: PermissionLevelConfig,
167
168 #[serde(default)]
170 pub users: HashMap<String, PermissionLevelConfig>,
171
172 #[serde(default)]
174 pub channels: HashMap<String, PermissionLevelConfig>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, Default)]
187pub struct PermissionLevelConfig {
188 #[serde(default)]
190 pub level: Option<u8>,
191
192 #[serde(default, alias = "maxTier")]
194 pub max_tier: Option<String>,
195
196 #[serde(default, alias = "modelAccess")]
198 pub model_access: Option<Vec<String>>,
199
200 #[serde(default, alias = "modelDenylist")]
202 pub model_denylist: Option<Vec<String>>,
203
204 #[serde(default, alias = "toolAccess")]
206 pub tool_access: Option<Vec<String>>,
207
208 #[serde(default, alias = "toolDenylist")]
210 pub tool_denylist: Option<Vec<String>>,
211
212 #[serde(default, alias = "maxContextTokens")]
214 pub max_context_tokens: Option<usize>,
215
216 #[serde(default, alias = "maxOutputTokens")]
218 pub max_output_tokens: Option<usize>,
219
220 #[serde(default, alias = "rateLimit")]
222 pub rate_limit: Option<u32>,
223
224 #[serde(default, alias = "streamingAllowed")]
226 pub streaming_allowed: Option<bool>,
227
228 #[serde(default, alias = "escalationAllowed")]
230 pub escalation_allowed: Option<bool>,
231
232 #[serde(default, alias = "escalationThreshold")]
234 pub escalation_threshold: Option<f32>,
235
236 #[serde(default, alias = "modelOverride")]
238 pub model_override: Option<bool>,
239
240 #[serde(default, alias = "costBudgetDailyUsd")]
242 pub cost_budget_daily_usd: Option<f64>,
243
244 #[serde(default, alias = "costBudgetMonthlyUsd")]
246 pub cost_budget_monthly_usd: Option<f64>,
247
248 #[serde(default, alias = "customPermissions")]
250 pub custom_permissions: Option<HashMap<String, serde_json::Value>>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct UserPermissions {
263 #[serde(default)]
265 pub level: u8,
266
267 #[serde(default, alias = "maxTier")]
269 pub max_tier: String,
270
271 #[serde(default, alias = "modelAccess")]
273 pub model_access: Vec<String>,
274
275 #[serde(default, alias = "modelDenylist")]
277 pub model_denylist: Vec<String>,
278
279 #[serde(default, alias = "toolAccess")]
281 pub tool_access: Vec<String>,
282
283 #[serde(default, alias = "toolDenylist")]
285 pub tool_denylist: Vec<String>,
286
287 #[serde(default = "default_max_context_tokens", alias = "maxContextTokens")]
289 pub max_context_tokens: usize,
290
291 #[serde(default = "default_max_output_tokens", alias = "maxOutputTokens")]
293 pub max_output_tokens: usize,
294
295 #[serde(default = "default_rate_limit", alias = "rateLimit")]
297 pub rate_limit: u32,
298
299 #[serde(default, alias = "streamingAllowed")]
301 pub streaming_allowed: bool,
302
303 #[serde(default, alias = "escalationAllowed")]
305 pub escalation_allowed: bool,
306
307 #[serde(default = "default_escalation_threshold", alias = "escalationThreshold")]
309 pub escalation_threshold: f32,
310
311 #[serde(default, alias = "modelOverride")]
313 pub model_override: bool,
314
315 #[serde(default = "default_cost_budget_daily_usd", alias = "costBudgetDailyUsd")]
318 pub cost_budget_daily_usd: f64,
319
320 #[serde(default = "default_cost_budget_monthly_usd", alias = "costBudgetMonthlyUsd")]
323 pub cost_budget_monthly_usd: f64,
324
325 #[serde(default, alias = "customPermissions")]
327 pub custom_permissions: HashMap<String, serde_json::Value>,
328}
329
330fn default_cost_budget_daily_usd() -> f64 {
331 0.10
332}
333
334fn default_cost_budget_monthly_usd() -> f64 {
335 2.00
336}
337
338fn default_max_context_tokens() -> usize {
339 4096
340}
341
342fn default_max_output_tokens() -> usize {
343 1024
344}
345
346fn default_rate_limit() -> u32 {
347 10
348}
349
350fn default_escalation_threshold() -> f32 {
351 1.0
352}
353
354impl Default for UserPermissions {
355 fn default() -> Self {
359 Self {
360 level: 0,
361 max_tier: "free".into(),
362 model_access: Vec::new(),
363 model_denylist: Vec::new(),
364 tool_access: Vec::new(),
365 tool_denylist: Vec::new(),
366 max_context_tokens: default_max_context_tokens(),
367 max_output_tokens: default_max_output_tokens(),
368 rate_limit: default_rate_limit(),
369 streaming_allowed: false,
370 escalation_allowed: false,
371 escalation_threshold: default_escalation_threshold(),
372 model_override: false,
373 cost_budget_daily_usd: default_cost_budget_daily_usd(),
374 cost_budget_monthly_usd: default_cost_budget_monthly_usd(),
375 custom_permissions: HashMap::new(),
376 }
377 }
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct AuthContext {
393 #[serde(default, alias = "senderId")]
396 pub sender_id: String,
397
398 #[serde(default)]
400 pub channel: String,
401
402 #[serde(default)]
404 pub permissions: UserPermissions,
405}
406
407impl Default for AuthContext {
408 fn default() -> Self {
411 Self {
412 sender_id: String::new(),
413 channel: String::new(),
414 permissions: UserPermissions::default(),
415 }
416 }
417}
418
419impl AuthContext {
420 pub fn cli_default() -> Self {
424 Self {
425 sender_id: "local".into(),
426 channel: "cli".into(),
427 permissions: UserPermissions {
428 level: 2,
429 max_tier: "elite".into(),
430 tool_access: vec!["*".into()],
431 max_context_tokens: 200_000,
432 max_output_tokens: 16_384,
433 rate_limit: 0,
434 streaming_allowed: true,
435 escalation_allowed: true,
436 escalation_threshold: 0.0,
437 model_override: true,
438 cost_budget_daily_usd: 0.0,
439 cost_budget_monthly_usd: 0.0,
440 ..UserPermissions::default()
441 },
442 }
443 }
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct EscalationConfig {
451 #[serde(default)]
453 pub enabled: bool,
454
455 #[serde(default = "default_global_escalation_threshold")]
457 pub threshold: f32,
458
459 #[serde(default = "default_max_escalation_tiers", alias = "maxEscalationTiers")]
461 pub max_escalation_tiers: u32,
462}
463
464fn default_global_escalation_threshold() -> f32 {
465 0.6
466}
467
468fn default_max_escalation_tiers() -> u32 {
469 1
470}
471
472impl Default for EscalationConfig {
473 fn default() -> Self {
474 Self {
475 enabled: false,
476 threshold: default_global_escalation_threshold(),
477 max_escalation_tiers: default_max_escalation_tiers(),
478 }
479 }
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct CostBudgetConfig {
489 #[serde(default, alias = "globalDailyLimitUsd")]
491 pub global_daily_limit_usd: f64,
492
493 #[serde(default, alias = "globalMonthlyLimitUsd")]
495 pub global_monthly_limit_usd: f64,
496
497 #[serde(default, alias = "trackingPersistence")]
499 pub tracking_persistence: bool,
500
501 #[serde(default, alias = "resetHourUtc")]
503 pub reset_hour_utc: u8,
504}
505
506impl Default for CostBudgetConfig {
507 fn default() -> Self {
508 Self {
509 global_daily_limit_usd: 0.0,
510 global_monthly_limit_usd: 0.0,
511 tracking_persistence: false,
512 reset_hour_utc: 0,
513 }
514 }
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct RateLimitConfig {
526 #[serde(default = "default_window_seconds", alias = "windowSeconds")]
528 pub window_seconds: u32,
529
530 #[serde(default = "default_rate_limit_strategy")]
532 pub strategy: String,
533
534 #[serde(default, alias = "globalRateLimitRpm")]
537 pub global_rate_limit_rpm: u32,
538}
539
540fn default_window_seconds() -> u32 {
541 60
542}
543
544fn default_rate_limit_strategy() -> String {
545 "sliding_window".into()
546}
547
548impl Default for RateLimitConfig {
549 fn default() -> Self {
550 Self {
551 window_seconds: default_window_seconds(),
552 strategy: default_rate_limit_strategy(),
553 global_rate_limit_rpm: 0,
554 }
555 }
556}
557
558#[cfg(test)]
561mod tests {
562 use super::*;
563
564 const TIERED_FIXTURE_PATH: &str = concat!(
565 env!("CARGO_MANIFEST_DIR"),
566 "/../../tests/fixtures/config_tiered.json"
567 );
568
569 fn load_tiered_fixture() -> crate::config::Config {
570 let content = std::fs::read_to_string(TIERED_FIXTURE_PATH)
571 .expect("config_tiered.json fixture should exist");
572 serde_json::from_str(&content).expect("tiered fixture should deserialize")
573 }
574
575 #[test]
576 fn routing_config_defaults() {
577 let cfg = RoutingConfig::default();
578 assert_eq!(cfg.mode, "static");
579 assert!(cfg.tiers.is_empty());
580 assert!(cfg.selection_strategy.is_none());
581 assert!(cfg.fallback_model.is_none());
582 assert!(!cfg.escalation.enabled);
583 }
584
585 #[test]
586 fn model_tier_config_defaults() {
587 let cfg = ModelTierConfig::default();
588 assert!(cfg.name.is_empty());
589 assert!(cfg.models.is_empty());
590 assert_eq!(cfg.complexity_range, [0.0, 1.0]);
591 assert_eq!(cfg.cost_per_1k_tokens, 0.0);
592 assert_eq!(cfg.max_context_tokens, 8192);
593 }
594
595 #[test]
596 fn permission_level_config_defaults() {
597 let cfg = PermissionLevelConfig::default();
598 assert!(cfg.level.is_none());
599 assert!(cfg.max_tier.is_none());
600 assert!(cfg.tool_access.is_none());
601 assert!(cfg.rate_limit.is_none());
602 assert!(cfg.streaming_allowed.is_none());
603 }
604
605 #[test]
606 fn user_permissions_defaults() {
607 let perms = UserPermissions::default();
608 assert_eq!(perms.level, 0);
609 assert_eq!(perms.max_tier, "free");
610 assert!(perms.tool_access.is_empty());
611 assert_eq!(perms.max_context_tokens, 4096);
612 assert_eq!(perms.max_output_tokens, 1024);
613 assert_eq!(perms.rate_limit, 10);
614 assert!(!perms.streaming_allowed);
615 assert!(!perms.escalation_allowed);
616 assert!((perms.escalation_threshold - 1.0).abs() < f32::EPSILON);
617 assert!(!perms.model_override);
618 assert!((perms.cost_budget_daily_usd - 0.10).abs() < f64::EPSILON);
619 assert!((perms.cost_budget_monthly_usd - 2.00).abs() < f64::EPSILON);
620 assert!(perms.custom_permissions.is_empty());
621 }
622
623 #[test]
624 fn auth_context_defaults() {
625 let ctx = AuthContext::default();
626 assert!(ctx.sender_id.is_empty());
627 assert!(ctx.channel.is_empty());
628 assert_eq!(ctx.permissions.level, 0);
629 assert!((ctx.permissions.cost_budget_daily_usd - 0.10).abs() < f64::EPSILON);
630 assert!((ctx.permissions.cost_budget_monthly_usd - 2.00).abs() < f64::EPSILON);
631 }
632
633 #[test]
634 fn auth_context_cli_default() {
635 let ctx = AuthContext::cli_default();
636 assert_eq!(ctx.sender_id, "local");
637 assert_eq!(ctx.channel, "cli");
638 assert_eq!(ctx.permissions.level, 2);
639 assert_eq!(ctx.permissions.max_tier, "elite");
640 assert_eq!(ctx.permissions.tool_access, vec!["*"]);
641 assert_eq!(ctx.permissions.max_context_tokens, 200_000);
642 assert_eq!(ctx.permissions.max_output_tokens, 16_384);
643 assert_eq!(ctx.permissions.rate_limit, 0);
644 assert!(ctx.permissions.streaming_allowed);
645 assert!(ctx.permissions.escalation_allowed);
646 assert!((ctx.permissions.escalation_threshold - 0.0).abs() < f32::EPSILON);
647 assert!(ctx.permissions.model_override);
648 assert_eq!(ctx.permissions.cost_budget_daily_usd, 0.0);
649 assert_eq!(ctx.permissions.cost_budget_monthly_usd, 0.0);
650 }
651
652 #[test]
653 fn tier_selection_strategy_serde() {
654 let json = serde_json::to_string(&TierSelectionStrategy::PreferenceOrder).unwrap();
655 assert_eq!(json, "\"preference_order\"");
656
657 let json = serde_json::to_string(&TierSelectionStrategy::RoundRobin).unwrap();
658 assert_eq!(json, "\"round_robin\"");
659
660 let json = serde_json::to_string(&TierSelectionStrategy::LowestCost).unwrap();
661 assert_eq!(json, "\"lowest_cost\"");
662
663 let json = serde_json::to_string(&TierSelectionStrategy::Random).unwrap();
664 assert_eq!(json, "\"random\"");
665
666 let strategy: TierSelectionStrategy =
667 serde_json::from_str("\"preference_order\"").unwrap();
668 assert_eq!(strategy, TierSelectionStrategy::PreferenceOrder);
669
670 let strategy: TierSelectionStrategy =
671 serde_json::from_str("\"round_robin\"").unwrap();
672 assert_eq!(strategy, TierSelectionStrategy::RoundRobin);
673
674 let result = serde_json::from_str::<TierSelectionStrategy>("\"invalid_strategy\"");
675 assert!(result.is_err());
676 }
677
678 #[test]
679 fn escalation_config_defaults() {
680 let cfg = EscalationConfig::default();
681 assert!(!cfg.enabled);
682 assert!((cfg.threshold - 0.6).abs() < f32::EPSILON);
683 assert_eq!(cfg.max_escalation_tiers, 1);
684 }
685
686 #[test]
687 fn cost_budget_config_defaults() {
688 let cfg = CostBudgetConfig::default();
689 assert_eq!(cfg.global_daily_limit_usd, 0.0);
690 assert_eq!(cfg.global_monthly_limit_usd, 0.0);
691 assert!(!cfg.tracking_persistence);
692 assert_eq!(cfg.reset_hour_utc, 0);
693 }
694
695 #[test]
696 fn rate_limit_config_defaults() {
697 let cfg = RateLimitConfig::default();
698 assert_eq!(cfg.window_seconds, 60);
699 assert_eq!(cfg.strategy, "sliding_window");
700 assert_eq!(cfg.global_rate_limit_rpm, 0);
701 }
702
703 #[test]
704 fn deserialize_full_tiered_config() {
705 let cfg = load_tiered_fixture();
706 let routing = &cfg.routing;
707
708 assert_eq!(routing.mode, "tiered");
709
710 assert_eq!(routing.tiers.len(), 4);
711 assert_eq!(routing.tiers[0].name, "free");
712 assert_eq!(routing.tiers[0].models.len(), 2);
713 assert_eq!(routing.tiers[0].complexity_range, [0.0, 0.3]);
714 assert_eq!(routing.tiers[0].cost_per_1k_tokens, 0.0);
715 assert_eq!(routing.tiers[0].max_context_tokens, 8192);
716
717 assert_eq!(routing.tiers[1].name, "standard");
718 assert_eq!(routing.tiers[2].name, "premium");
719 assert_eq!(routing.tiers[3].name, "elite");
720 assert_eq!(routing.tiers[3].cost_per_1k_tokens, 0.05);
721 assert_eq!(routing.tiers[3].max_context_tokens, 200000);
722
723 assert_eq!(
724 routing.selection_strategy,
725 Some(TierSelectionStrategy::PreferenceOrder)
726 );
727 assert_eq!(routing.fallback_model.as_deref(), Some("groq/llama-3.1-8b"));
728
729 let zt = &routing.permissions.zero_trust;
730 assert_eq!(zt.level, Some(0));
731 assert_eq!(zt.max_tier.as_deref(), Some("free"));
732 assert_eq!(zt.tool_access.as_ref().map(|v| v.len()), Some(0));
733 assert_eq!(zt.max_context_tokens, Some(4096));
734 assert_eq!(zt.max_output_tokens, Some(1024));
735 assert_eq!(zt.rate_limit, Some(10));
736 assert_eq!(zt.streaming_allowed, Some(false));
737 assert_eq!(zt.escalation_allowed, Some(false));
738
739 let u = &routing.permissions.user;
740 assert_eq!(u.level, Some(1));
741 assert_eq!(u.max_tier.as_deref(), Some("standard"));
742 assert_eq!(u.tool_access.as_ref().map(|v| v.len()), Some(7));
743 assert_eq!(u.streaming_allowed, Some(true));
744 assert_eq!(u.escalation_allowed, Some(true));
745
746 let a = &routing.permissions.admin;
747 assert_eq!(a.level, Some(2));
748 assert_eq!(a.max_tier.as_deref(), Some("elite"));
749 assert_eq!(a.model_override, Some(true));
750 assert_eq!(a.rate_limit, Some(0));
751
752 assert!(routing.permissions.users.contains_key("alice_telegram_123"));
753 assert_eq!(
754 routing.permissions.users["alice_telegram_123"].level,
755 Some(2)
756 );
757 assert!(routing.permissions.users.contains_key("bob_discord_456"));
758 assert_eq!(routing.permissions.users["bob_discord_456"].level, Some(1));
759 assert_eq!(
760 routing.permissions.users["bob_discord_456"].cost_budget_daily_usd,
761 Some(2.00)
762 );
763
764 assert_eq!(routing.permissions.channels["cli"].level, Some(2));
765 assert_eq!(routing.permissions.channels["telegram"].level, Some(1));
766 assert_eq!(routing.permissions.channels["discord"].level, Some(0));
767
768 assert!(routing.escalation.enabled);
769 assert!((routing.escalation.threshold - 0.6).abs() < f32::EPSILON);
770 assert_eq!(routing.escalation.max_escalation_tiers, 1);
771
772 assert_eq!(routing.cost_budgets.global_daily_limit_usd, 50.0);
773 assert_eq!(routing.cost_budgets.global_monthly_limit_usd, 500.0);
774 assert!(routing.cost_budgets.tracking_persistence);
775 assert_eq!(routing.cost_budgets.reset_hour_utc, 0);
776
777 assert_eq!(routing.rate_limiting.window_seconds, 60);
778 assert_eq!(routing.rate_limiting.strategy, "sliding_window");
779 assert_eq!(routing.rate_limiting.global_rate_limit_rpm, 0);
780 }
781
782 #[test]
783 fn serde_roundtrip_routing_config() {
784 let cfg = load_tiered_fixture();
785 let json = serde_json::to_string(&cfg.routing).unwrap();
786 let restored: RoutingConfig = serde_json::from_str(&json).unwrap();
787 assert_eq!(restored.mode, cfg.routing.mode);
788 assert_eq!(restored.tiers.len(), cfg.routing.tiers.len());
789 assert_eq!(restored.tiers[0].name, cfg.routing.tiers[0].name);
790 assert_eq!(restored.fallback_model, cfg.routing.fallback_model);
791 assert_eq!(
792 restored.escalation.max_escalation_tiers,
793 cfg.routing.escalation.max_escalation_tiers
794 );
795 }
796
797 #[test]
798 fn camel_case_aliases() {
799 let json = r#"{
800 "mode": "tiered",
801 "selectionStrategy": "round_robin",
802 "fallbackModel": "groq/llama-3.1-8b",
803 "costBudgets": {
804 "globalDailyLimitUsd": 25.0,
805 "globalMonthlyLimitUsd": 250.0,
806 "trackingPersistence": true,
807 "resetHourUtc": 6
808 },
809 "rateLimiting": {
810 "windowSeconds": 120
811 },
812 "escalation": {
813 "maxEscalationTiers": 2
814 }
815 }"#;
816 let cfg: RoutingConfig = serde_json::from_str(json).unwrap();
817 assert_eq!(
818 cfg.selection_strategy,
819 Some(TierSelectionStrategy::RoundRobin)
820 );
821 assert_eq!(cfg.fallback_model.as_deref(), Some("groq/llama-3.1-8b"));
822 assert_eq!(cfg.cost_budgets.global_daily_limit_usd, 25.0);
823 assert_eq!(cfg.cost_budgets.global_monthly_limit_usd, 250.0);
824 assert!(cfg.cost_budgets.tracking_persistence);
825 assert_eq!(cfg.cost_budgets.reset_hour_utc, 6);
826 assert_eq!(cfg.rate_limiting.window_seconds, 120);
827 assert_eq!(cfg.escalation.max_escalation_tiers, 2);
828 }
829
830 #[test]
831 fn unknown_fields_ignored() {
832 let json = r#"{
833 "mode": "tiered",
834 "future_field": "should be ignored",
835 "escalation": {
836 "enabled": true,
837 "unknown_nested": 42
838 },
839 "tiers": [{
840 "name": "test",
841 "models": [],
842 "complexity_range": [0.0, 1.0],
843 "cost_per_1k_tokens": 0.0,
844 "some_future_field": true
845 }]
846 }"#;
847 let cfg: RoutingConfig = serde_json::from_str(json).unwrap();
848 assert_eq!(cfg.mode, "tiered");
849 assert!(cfg.escalation.enabled);
850 assert_eq!(cfg.tiers.len(), 1);
851 assert_eq!(cfg.tiers[0].name, "test");
852 }
853
854 #[test]
855 fn empty_routing_section() {
856 let json = r#"{}"#;
857 let cfg: RoutingConfig = serde_json::from_str(json).unwrap();
858 assert_eq!(cfg.mode, "static");
859 assert!(cfg.tiers.is_empty());
860 assert!(cfg.selection_strategy.is_none());
861 assert!(cfg.fallback_model.is_none());
862 assert!(!cfg.escalation.enabled);
863 }
864
865 #[test]
866 fn backward_compat_no_routing() {
867 let json = r#"{
868 "agents": { "defaults": { "model": "deepseek/deepseek-chat" } },
869 "providers": { "anthropic": { "apiKey": "test" } }
870 }"#;
871 let cfg: crate::config::Config = serde_json::from_str(json).unwrap();
872 assert_eq!(cfg.agents.defaults.model, "deepseek/deepseek-chat");
873 assert_eq!(cfg.routing.mode, "static");
874 assert!(cfg.routing.tiers.is_empty());
875 }
876
877 #[test]
878 fn per_user_partial_override() {
879 let json = r#"{
880 "users": {
881 "alice": { "level": 2 },
882 "bob": { "level": 1, "cost_budget_daily_usd": 3.50 }
883 }
884 }"#;
885 let cfg: PermissionsConfig = serde_json::from_str(json).unwrap();
886 assert_eq!(cfg.users["alice"].level, Some(2));
887 assert!(cfg.users["alice"].max_tier.is_none());
888 assert!(cfg.users["alice"].tool_access.is_none());
889 assert_eq!(cfg.users["bob"].level, Some(1));
890 assert_eq!(cfg.users["bob"].cost_budget_daily_usd, Some(3.50));
891 }
892
893 #[test]
894 fn complexity_range_array() {
895 let json = r#"{
896 "name": "test",
897 "models": ["provider/model"],
898 "complexity_range": [0.3, 0.7],
899 "cost_per_1k_tokens": 0.005
900 }"#;
901 let tier: ModelTierConfig = serde_json::from_str(json).unwrap();
902 assert_eq!(tier.complexity_range, [0.3, 0.7]);
903 assert_eq!(tier.name, "test");
904 assert_eq!(tier.models, vec!["provider/model"]);
905 assert_eq!(tier.cost_per_1k_tokens, 0.005);
906 }
907
908 #[test]
913 fn test_defaults_for_level_unknown_returns_zero_trust() {
914 let perms = UserPermissions::default();
915 assert_eq!(perms.level, 0, "default permissions should be zero-trust (level 0)");
916 assert_eq!(perms.max_tier, "free", "zero-trust should have 'free' tier");
917 assert!(
918 perms.tool_access.is_empty(),
919 "zero-trust should have no tool access"
920 );
921 assert!(!perms.streaming_allowed, "zero-trust should not allow streaming");
922 assert!(!perms.escalation_allowed, "zero-trust should not allow escalation");
923 assert!(!perms.model_override, "zero-trust should not allow model override");
924 }
925
926 #[test]
928 fn test_auth_context_default_is_zero_trust() {
929 let ctx = AuthContext::default();
930 assert!(ctx.sender_id.is_empty(), "default sender_id should be empty");
931 assert!(ctx.channel.is_empty(), "default channel should be empty");
932 assert_eq!(ctx.permissions.level, 0, "default permissions should be level 0");
933 }
934
935 #[test]
937 fn test_auth_context_cli_default_is_admin() {
938 let ctx = AuthContext::cli_default();
939 assert_eq!(ctx.sender_id, "local");
940 assert_eq!(ctx.channel, "cli");
941 assert_eq!(ctx.permissions.level, 2, "CLI default should be admin (level 2)");
942 assert_eq!(ctx.permissions.max_tier, "elite");
943 assert!(ctx.permissions.tool_access.contains(&"*".to_string()));
944 assert_eq!(ctx.permissions.rate_limit, 0, "CLI admin should have no rate limit");
945 assert!(ctx.permissions.streaming_allowed);
946 assert!(ctx.permissions.escalation_allowed);
947 assert!(ctx.permissions.model_override);
948 }
949}