use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TierSelectionStrategy {
PreferenceOrder,
RoundRobin,
LowestCost,
Random,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingConfig {
#[serde(default = "default_routing_mode")]
pub mode: String,
#[serde(default)]
pub tiers: Vec<ModelTierConfig>,
#[serde(default, alias = "selectionStrategy")]
pub selection_strategy: Option<TierSelectionStrategy>,
#[serde(default, alias = "fallbackModel")]
pub fallback_model: Option<String>,
#[serde(default)]
pub permissions: PermissionsConfig,
#[serde(default)]
pub escalation: EscalationConfig,
#[serde(default, alias = "costBudgets")]
pub cost_budgets: CostBudgetConfig,
#[serde(default, alias = "rateLimiting")]
pub rate_limiting: RateLimitConfig,
}
fn default_routing_mode() -> String {
"static".into()
}
impl Default for RoutingConfig {
fn default() -> Self {
Self {
mode: default_routing_mode(),
tiers: Vec::new(),
selection_strategy: None,
fallback_model: None,
permissions: PermissionsConfig::default(),
escalation: EscalationConfig::default(),
cost_budgets: CostBudgetConfig::default(),
rate_limiting: RateLimitConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelTierConfig {
pub name: String,
#[serde(default)]
pub models: Vec<String>,
#[serde(default = "default_complexity_range", alias = "complexityRange")]
pub complexity_range: [f32; 2],
#[serde(default, alias = "costPer1kTokens")]
pub cost_per_1k_tokens: f64,
#[serde(default = "default_tier_max_context", alias = "maxContextTokens")]
pub max_context_tokens: usize,
}
fn default_complexity_range() -> [f32; 2] {
[0.0, 1.0]
}
fn default_tier_max_context() -> usize {
8192
}
impl Default for ModelTierConfig {
fn default() -> Self {
Self {
name: String::new(),
models: Vec::new(),
complexity_range: default_complexity_range(),
cost_per_1k_tokens: 0.0,
max_context_tokens: default_tier_max_context(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PermissionsConfig {
#[serde(default)]
pub zero_trust: PermissionLevelConfig,
#[serde(default)]
pub user: PermissionLevelConfig,
#[serde(default)]
pub admin: PermissionLevelConfig,
#[serde(default)]
pub users: HashMap<String, PermissionLevelConfig>,
#[serde(default)]
pub channels: HashMap<String, PermissionLevelConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PermissionLevelConfig {
#[serde(default)]
pub level: Option<u8>,
#[serde(default, alias = "maxTier")]
pub max_tier: Option<String>,
#[serde(default, alias = "modelAccess")]
pub model_access: Option<Vec<String>>,
#[serde(default, alias = "modelDenylist")]
pub model_denylist: Option<Vec<String>>,
#[serde(default, alias = "toolAccess")]
pub tool_access: Option<Vec<String>>,
#[serde(default, alias = "toolDenylist")]
pub tool_denylist: Option<Vec<String>>,
#[serde(default, alias = "maxContextTokens")]
pub max_context_tokens: Option<usize>,
#[serde(default, alias = "maxOutputTokens")]
pub max_output_tokens: Option<usize>,
#[serde(default, alias = "rateLimit")]
pub rate_limit: Option<u32>,
#[serde(default, alias = "streamingAllowed")]
pub streaming_allowed: Option<bool>,
#[serde(default, alias = "escalationAllowed")]
pub escalation_allowed: Option<bool>,
#[serde(default, alias = "escalationThreshold")]
pub escalation_threshold: Option<f32>,
#[serde(default, alias = "modelOverride")]
pub model_override: Option<bool>,
#[serde(default, alias = "costBudgetDailyUsd")]
pub cost_budget_daily_usd: Option<f64>,
#[serde(default, alias = "costBudgetMonthlyUsd")]
pub cost_budget_monthly_usd: Option<f64>,
#[serde(default, alias = "customPermissions")]
pub custom_permissions: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPermissions {
#[serde(default)]
pub level: u8,
#[serde(default, alias = "maxTier")]
pub max_tier: String,
#[serde(default, alias = "modelAccess")]
pub model_access: Vec<String>,
#[serde(default, alias = "modelDenylist")]
pub model_denylist: Vec<String>,
#[serde(default, alias = "toolAccess")]
pub tool_access: Vec<String>,
#[serde(default, alias = "toolDenylist")]
pub tool_denylist: Vec<String>,
#[serde(default = "default_max_context_tokens", alias = "maxContextTokens")]
pub max_context_tokens: usize,
#[serde(default = "default_max_output_tokens", alias = "maxOutputTokens")]
pub max_output_tokens: usize,
#[serde(default = "default_rate_limit", alias = "rateLimit")]
pub rate_limit: u32,
#[serde(default, alias = "streamingAllowed")]
pub streaming_allowed: bool,
#[serde(default, alias = "escalationAllowed")]
pub escalation_allowed: bool,
#[serde(default = "default_escalation_threshold", alias = "escalationThreshold")]
pub escalation_threshold: f32,
#[serde(default, alias = "modelOverride")]
pub model_override: bool,
#[serde(default = "default_cost_budget_daily_usd", alias = "costBudgetDailyUsd")]
pub cost_budget_daily_usd: f64,
#[serde(default = "default_cost_budget_monthly_usd", alias = "costBudgetMonthlyUsd")]
pub cost_budget_monthly_usd: f64,
#[serde(default, alias = "customPermissions")]
pub custom_permissions: HashMap<String, serde_json::Value>,
}
fn default_cost_budget_daily_usd() -> f64 {
0.10
}
fn default_cost_budget_monthly_usd() -> f64 {
2.00
}
fn default_max_context_tokens() -> usize {
4096
}
fn default_max_output_tokens() -> usize {
1024
}
fn default_rate_limit() -> u32 {
10
}
fn default_escalation_threshold() -> f32 {
1.0
}
impl Default for UserPermissions {
fn default() -> Self {
Self {
level: 0,
max_tier: "free".into(),
model_access: Vec::new(),
model_denylist: Vec::new(),
tool_access: Vec::new(),
tool_denylist: Vec::new(),
max_context_tokens: default_max_context_tokens(),
max_output_tokens: default_max_output_tokens(),
rate_limit: default_rate_limit(),
streaming_allowed: false,
escalation_allowed: false,
escalation_threshold: default_escalation_threshold(),
model_override: false,
cost_budget_daily_usd: default_cost_budget_daily_usd(),
cost_budget_monthly_usd: default_cost_budget_monthly_usd(),
custom_permissions: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthContext {
#[serde(default, alias = "senderId")]
pub sender_id: String,
#[serde(default)]
pub channel: String,
#[serde(default)]
pub permissions: UserPermissions,
}
impl Default for AuthContext {
fn default() -> Self {
Self {
sender_id: String::new(),
channel: String::new(),
permissions: UserPermissions::default(),
}
}
}
impl AuthContext {
pub fn cli_default() -> Self {
Self {
sender_id: "local".into(),
channel: "cli".into(),
permissions: UserPermissions {
level: 2,
max_tier: "elite".into(),
tool_access: vec!["*".into()],
max_context_tokens: 200_000,
max_output_tokens: 16_384,
rate_limit: 0,
streaming_allowed: true,
escalation_allowed: true,
escalation_threshold: 0.0,
model_override: true,
cost_budget_daily_usd: 0.0,
cost_budget_monthly_usd: 0.0,
..UserPermissions::default()
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EscalationConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_global_escalation_threshold")]
pub threshold: f32,
#[serde(default = "default_max_escalation_tiers", alias = "maxEscalationTiers")]
pub max_escalation_tiers: u32,
}
fn default_global_escalation_threshold() -> f32 {
0.6
}
fn default_max_escalation_tiers() -> u32 {
1
}
impl Default for EscalationConfig {
fn default() -> Self {
Self {
enabled: false,
threshold: default_global_escalation_threshold(),
max_escalation_tiers: default_max_escalation_tiers(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostBudgetConfig {
#[serde(default, alias = "globalDailyLimitUsd")]
pub global_daily_limit_usd: f64,
#[serde(default, alias = "globalMonthlyLimitUsd")]
pub global_monthly_limit_usd: f64,
#[serde(default, alias = "trackingPersistence")]
pub tracking_persistence: bool,
#[serde(default, alias = "resetHourUtc")]
pub reset_hour_utc: u8,
}
impl Default for CostBudgetConfig {
fn default() -> Self {
Self {
global_daily_limit_usd: 0.0,
global_monthly_limit_usd: 0.0,
tracking_persistence: false,
reset_hour_utc: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
#[serde(default = "default_window_seconds", alias = "windowSeconds")]
pub window_seconds: u32,
#[serde(default = "default_rate_limit_strategy")]
pub strategy: String,
#[serde(default, alias = "globalRateLimitRpm")]
pub global_rate_limit_rpm: u32,
}
fn default_window_seconds() -> u32 {
60
}
fn default_rate_limit_strategy() -> String {
"sliding_window".into()
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
window_seconds: default_window_seconds(),
strategy: default_rate_limit_strategy(),
global_rate_limit_rpm: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TIERED_FIXTURE_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/config_tiered.json"
);
fn load_tiered_fixture() -> crate::config::Config {
let content = std::fs::read_to_string(TIERED_FIXTURE_PATH)
.expect("config_tiered.json fixture should exist");
serde_json::from_str(&content).expect("tiered fixture should deserialize")
}
#[test]
fn routing_config_defaults() {
let cfg = RoutingConfig::default();
assert_eq!(cfg.mode, "static");
assert!(cfg.tiers.is_empty());
assert!(cfg.selection_strategy.is_none());
assert!(cfg.fallback_model.is_none());
assert!(!cfg.escalation.enabled);
}
#[test]
fn model_tier_config_defaults() {
let cfg = ModelTierConfig::default();
assert!(cfg.name.is_empty());
assert!(cfg.models.is_empty());
assert_eq!(cfg.complexity_range, [0.0, 1.0]);
assert_eq!(cfg.cost_per_1k_tokens, 0.0);
assert_eq!(cfg.max_context_tokens, 8192);
}
#[test]
fn permission_level_config_defaults() {
let cfg = PermissionLevelConfig::default();
assert!(cfg.level.is_none());
assert!(cfg.max_tier.is_none());
assert!(cfg.tool_access.is_none());
assert!(cfg.rate_limit.is_none());
assert!(cfg.streaming_allowed.is_none());
}
#[test]
fn user_permissions_defaults() {
let perms = UserPermissions::default();
assert_eq!(perms.level, 0);
assert_eq!(perms.max_tier, "free");
assert!(perms.tool_access.is_empty());
assert_eq!(perms.max_context_tokens, 4096);
assert_eq!(perms.max_output_tokens, 1024);
assert_eq!(perms.rate_limit, 10);
assert!(!perms.streaming_allowed);
assert!(!perms.escalation_allowed);
assert!((perms.escalation_threshold - 1.0).abs() < f32::EPSILON);
assert!(!perms.model_override);
assert!((perms.cost_budget_daily_usd - 0.10).abs() < f64::EPSILON);
assert!((perms.cost_budget_monthly_usd - 2.00).abs() < f64::EPSILON);
assert!(perms.custom_permissions.is_empty());
}
#[test]
fn auth_context_defaults() {
let ctx = AuthContext::default();
assert!(ctx.sender_id.is_empty());
assert!(ctx.channel.is_empty());
assert_eq!(ctx.permissions.level, 0);
assert!((ctx.permissions.cost_budget_daily_usd - 0.10).abs() < f64::EPSILON);
assert!((ctx.permissions.cost_budget_monthly_usd - 2.00).abs() < f64::EPSILON);
}
#[test]
fn auth_context_cli_default() {
let ctx = AuthContext::cli_default();
assert_eq!(ctx.sender_id, "local");
assert_eq!(ctx.channel, "cli");
assert_eq!(ctx.permissions.level, 2);
assert_eq!(ctx.permissions.max_tier, "elite");
assert_eq!(ctx.permissions.tool_access, vec!["*"]);
assert_eq!(ctx.permissions.max_context_tokens, 200_000);
assert_eq!(ctx.permissions.max_output_tokens, 16_384);
assert_eq!(ctx.permissions.rate_limit, 0);
assert!(ctx.permissions.streaming_allowed);
assert!(ctx.permissions.escalation_allowed);
assert!((ctx.permissions.escalation_threshold - 0.0).abs() < f32::EPSILON);
assert!(ctx.permissions.model_override);
assert_eq!(ctx.permissions.cost_budget_daily_usd, 0.0);
assert_eq!(ctx.permissions.cost_budget_monthly_usd, 0.0);
}
#[test]
fn tier_selection_strategy_serde() {
let json = serde_json::to_string(&TierSelectionStrategy::PreferenceOrder).unwrap();
assert_eq!(json, "\"preference_order\"");
let json = serde_json::to_string(&TierSelectionStrategy::RoundRobin).unwrap();
assert_eq!(json, "\"round_robin\"");
let json = serde_json::to_string(&TierSelectionStrategy::LowestCost).unwrap();
assert_eq!(json, "\"lowest_cost\"");
let json = serde_json::to_string(&TierSelectionStrategy::Random).unwrap();
assert_eq!(json, "\"random\"");
let strategy: TierSelectionStrategy =
serde_json::from_str("\"preference_order\"").unwrap();
assert_eq!(strategy, TierSelectionStrategy::PreferenceOrder);
let strategy: TierSelectionStrategy =
serde_json::from_str("\"round_robin\"").unwrap();
assert_eq!(strategy, TierSelectionStrategy::RoundRobin);
let result = serde_json::from_str::<TierSelectionStrategy>("\"invalid_strategy\"");
assert!(result.is_err());
}
#[test]
fn escalation_config_defaults() {
let cfg = EscalationConfig::default();
assert!(!cfg.enabled);
assert!((cfg.threshold - 0.6).abs() < f32::EPSILON);
assert_eq!(cfg.max_escalation_tiers, 1);
}
#[test]
fn cost_budget_config_defaults() {
let cfg = CostBudgetConfig::default();
assert_eq!(cfg.global_daily_limit_usd, 0.0);
assert_eq!(cfg.global_monthly_limit_usd, 0.0);
assert!(!cfg.tracking_persistence);
assert_eq!(cfg.reset_hour_utc, 0);
}
#[test]
fn rate_limit_config_defaults() {
let cfg = RateLimitConfig::default();
assert_eq!(cfg.window_seconds, 60);
assert_eq!(cfg.strategy, "sliding_window");
assert_eq!(cfg.global_rate_limit_rpm, 0);
}
#[test]
fn deserialize_full_tiered_config() {
let cfg = load_tiered_fixture();
let routing = &cfg.routing;
assert_eq!(routing.mode, "tiered");
assert_eq!(routing.tiers.len(), 4);
assert_eq!(routing.tiers[0].name, "free");
assert_eq!(routing.tiers[0].models.len(), 2);
assert_eq!(routing.tiers[0].complexity_range, [0.0, 0.3]);
assert_eq!(routing.tiers[0].cost_per_1k_tokens, 0.0);
assert_eq!(routing.tiers[0].max_context_tokens, 8192);
assert_eq!(routing.tiers[1].name, "standard");
assert_eq!(routing.tiers[2].name, "premium");
assert_eq!(routing.tiers[3].name, "elite");
assert_eq!(routing.tiers[3].cost_per_1k_tokens, 0.05);
assert_eq!(routing.tiers[3].max_context_tokens, 200000);
assert_eq!(
routing.selection_strategy,
Some(TierSelectionStrategy::PreferenceOrder)
);
assert_eq!(routing.fallback_model.as_deref(), Some("groq/llama-3.1-8b"));
let zt = &routing.permissions.zero_trust;
assert_eq!(zt.level, Some(0));
assert_eq!(zt.max_tier.as_deref(), Some("free"));
assert_eq!(zt.tool_access.as_ref().map(|v| v.len()), Some(0));
assert_eq!(zt.max_context_tokens, Some(4096));
assert_eq!(zt.max_output_tokens, Some(1024));
assert_eq!(zt.rate_limit, Some(10));
assert_eq!(zt.streaming_allowed, Some(false));
assert_eq!(zt.escalation_allowed, Some(false));
let u = &routing.permissions.user;
assert_eq!(u.level, Some(1));
assert_eq!(u.max_tier.as_deref(), Some("standard"));
assert_eq!(u.tool_access.as_ref().map(|v| v.len()), Some(7));
assert_eq!(u.streaming_allowed, Some(true));
assert_eq!(u.escalation_allowed, Some(true));
let a = &routing.permissions.admin;
assert_eq!(a.level, Some(2));
assert_eq!(a.max_tier.as_deref(), Some("elite"));
assert_eq!(a.model_override, Some(true));
assert_eq!(a.rate_limit, Some(0));
assert!(routing.permissions.users.contains_key("alice_telegram_123"));
assert_eq!(
routing.permissions.users["alice_telegram_123"].level,
Some(2)
);
assert!(routing.permissions.users.contains_key("bob_discord_456"));
assert_eq!(routing.permissions.users["bob_discord_456"].level, Some(1));
assert_eq!(
routing.permissions.users["bob_discord_456"].cost_budget_daily_usd,
Some(2.00)
);
assert_eq!(routing.permissions.channels["cli"].level, Some(2));
assert_eq!(routing.permissions.channels["telegram"].level, Some(1));
assert_eq!(routing.permissions.channels["discord"].level, Some(0));
assert!(routing.escalation.enabled);
assert!((routing.escalation.threshold - 0.6).abs() < f32::EPSILON);
assert_eq!(routing.escalation.max_escalation_tiers, 1);
assert_eq!(routing.cost_budgets.global_daily_limit_usd, 50.0);
assert_eq!(routing.cost_budgets.global_monthly_limit_usd, 500.0);
assert!(routing.cost_budgets.tracking_persistence);
assert_eq!(routing.cost_budgets.reset_hour_utc, 0);
assert_eq!(routing.rate_limiting.window_seconds, 60);
assert_eq!(routing.rate_limiting.strategy, "sliding_window");
assert_eq!(routing.rate_limiting.global_rate_limit_rpm, 0);
}
#[test]
fn serde_roundtrip_routing_config() {
let cfg = load_tiered_fixture();
let json = serde_json::to_string(&cfg.routing).unwrap();
let restored: RoutingConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.mode, cfg.routing.mode);
assert_eq!(restored.tiers.len(), cfg.routing.tiers.len());
assert_eq!(restored.tiers[0].name, cfg.routing.tiers[0].name);
assert_eq!(restored.fallback_model, cfg.routing.fallback_model);
assert_eq!(
restored.escalation.max_escalation_tiers,
cfg.routing.escalation.max_escalation_tiers
);
}
#[test]
fn camel_case_aliases() {
let json = r#"{
"mode": "tiered",
"selectionStrategy": "round_robin",
"fallbackModel": "groq/llama-3.1-8b",
"costBudgets": {
"globalDailyLimitUsd": 25.0,
"globalMonthlyLimitUsd": 250.0,
"trackingPersistence": true,
"resetHourUtc": 6
},
"rateLimiting": {
"windowSeconds": 120
},
"escalation": {
"maxEscalationTiers": 2
}
}"#;
let cfg: RoutingConfig = serde_json::from_str(json).unwrap();
assert_eq!(
cfg.selection_strategy,
Some(TierSelectionStrategy::RoundRobin)
);
assert_eq!(cfg.fallback_model.as_deref(), Some("groq/llama-3.1-8b"));
assert_eq!(cfg.cost_budgets.global_daily_limit_usd, 25.0);
assert_eq!(cfg.cost_budgets.global_monthly_limit_usd, 250.0);
assert!(cfg.cost_budgets.tracking_persistence);
assert_eq!(cfg.cost_budgets.reset_hour_utc, 6);
assert_eq!(cfg.rate_limiting.window_seconds, 120);
assert_eq!(cfg.escalation.max_escalation_tiers, 2);
}
#[test]
fn unknown_fields_ignored() {
let json = r#"{
"mode": "tiered",
"future_field": "should be ignored",
"escalation": {
"enabled": true,
"unknown_nested": 42
},
"tiers": [{
"name": "test",
"models": [],
"complexity_range": [0.0, 1.0],
"cost_per_1k_tokens": 0.0,
"some_future_field": true
}]
}"#;
let cfg: RoutingConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.mode, "tiered");
assert!(cfg.escalation.enabled);
assert_eq!(cfg.tiers.len(), 1);
assert_eq!(cfg.tiers[0].name, "test");
}
#[test]
fn empty_routing_section() {
let json = r#"{}"#;
let cfg: RoutingConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.mode, "static");
assert!(cfg.tiers.is_empty());
assert!(cfg.selection_strategy.is_none());
assert!(cfg.fallback_model.is_none());
assert!(!cfg.escalation.enabled);
}
#[test]
fn backward_compat_no_routing() {
let json = r#"{
"agents": { "defaults": { "model": "deepseek/deepseek-chat" } },
"providers": { "anthropic": { "apiKey": "test" } }
}"#;
let cfg: crate::config::Config = serde_json::from_str(json).unwrap();
assert_eq!(cfg.agents.defaults.model, "deepseek/deepseek-chat");
assert_eq!(cfg.routing.mode, "static");
assert!(cfg.routing.tiers.is_empty());
}
#[test]
fn per_user_partial_override() {
let json = r#"{
"users": {
"alice": { "level": 2 },
"bob": { "level": 1, "cost_budget_daily_usd": 3.50 }
}
}"#;
let cfg: PermissionsConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.users["alice"].level, Some(2));
assert!(cfg.users["alice"].max_tier.is_none());
assert!(cfg.users["alice"].tool_access.is_none());
assert_eq!(cfg.users["bob"].level, Some(1));
assert_eq!(cfg.users["bob"].cost_budget_daily_usd, Some(3.50));
}
#[test]
fn complexity_range_array() {
let json = r#"{
"name": "test",
"models": ["provider/model"],
"complexity_range": [0.3, 0.7],
"cost_per_1k_tokens": 0.005
}"#;
let tier: ModelTierConfig = serde_json::from_str(json).unwrap();
assert_eq!(tier.complexity_range, [0.3, 0.7]);
assert_eq!(tier.name, "test");
assert_eq!(tier.models, vec!["provider/model"]);
assert_eq!(tier.cost_per_1k_tokens, 0.005);
}
#[test]
fn test_defaults_for_level_unknown_returns_zero_trust() {
let perms = UserPermissions::default();
assert_eq!(perms.level, 0, "default permissions should be zero-trust (level 0)");
assert_eq!(perms.max_tier, "free", "zero-trust should have 'free' tier");
assert!(
perms.tool_access.is_empty(),
"zero-trust should have no tool access"
);
assert!(!perms.streaming_allowed, "zero-trust should not allow streaming");
assert!(!perms.escalation_allowed, "zero-trust should not allow escalation");
assert!(!perms.model_override, "zero-trust should not allow model override");
}
#[test]
fn test_auth_context_default_is_zero_trust() {
let ctx = AuthContext::default();
assert!(ctx.sender_id.is_empty(), "default sender_id should be empty");
assert!(ctx.channel.is_empty(), "default channel should be empty");
assert_eq!(ctx.permissions.level, 0, "default permissions should be level 0");
}
#[test]
fn test_auth_context_cli_default_is_admin() {
let ctx = AuthContext::cli_default();
assert_eq!(ctx.sender_id, "local");
assert_eq!(ctx.channel, "cli");
assert_eq!(ctx.permissions.level, 2, "CLI default should be admin (level 2)");
assert_eq!(ctx.permissions.max_tier, "elite");
assert!(ctx.permissions.tool_access.contains(&"*".to_string()));
assert_eq!(ctx.permissions.rate_limit, 0, "CLI admin should have no rate limit");
assert!(ctx.permissions.streaming_allowed);
assert!(ctx.permissions.escalation_allowed);
assert!(ctx.permissions.model_override);
}
}