use std::collections::HashMap; use vapora_llm_router::{BudgetManager, RoleBudget}; fn create_test_budgets() -> HashMap { let mut budgets = HashMap::new(); budgets.insert( "architect".to_string(), RoleBudget { role: "architect".to_string(), monthly_limit_cents: 50000, // $500 weekly_limit_cents: 12500, // $125 fallback_provider: "gemini".to_string(), alert_threshold: 0.8, }, ); budgets.insert( "developer".to_string(), RoleBudget { role: "developer".to_string(), monthly_limit_cents: 30000, weekly_limit_cents: 7500, fallback_provider: "ollama".to_string(), alert_threshold: 0.8, }, ); budgets } #[tokio::test] async fn test_budget_initialization() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); let status = manager.check_budget("architect").await.unwrap(); assert_eq!(status.role, "architect"); assert_eq!(status.monthly_remaining_cents, 50000); assert_eq!(status.monthly_utilization, 0.0); assert!(!status.exceeded); assert!(!status.near_threshold); } #[tokio::test] async fn test_budget_spending() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); manager.record_spend("developer", 3000).await.unwrap(); let status = manager.check_budget("developer").await.unwrap(); assert_eq!(status.monthly_remaining_cents, 27000); assert!((status.monthly_utilization - 0.1).abs() < 0.01); } #[tokio::test] async fn test_multiple_spends_accumulate() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); manager.record_spend("developer", 5000).await.unwrap(); manager.record_spend("developer", 3000).await.unwrap(); manager.record_spend("developer", 2000).await.unwrap(); let status = manager.check_budget("developer").await.unwrap(); assert_eq!(status.monthly_remaining_cents, 20000); // 30000 - 10000 } #[tokio::test] async fn test_alert_threshold_near() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); // Spend 81% of weekly budget (12500 * 0.81 = 10125) to trigger near_threshold // This keeps us under both monthly and weekly limits while triggering alert let spend_amount = (12500.0 * 0.81) as u32; // 10125 manager.record_spend("architect", spend_amount).await.unwrap(); let status = manager.check_budget("architect").await.unwrap(); assert!(!status.exceeded); assert!(status.near_threshold); } #[tokio::test] async fn test_budget_exceeded() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); // Spend entire monthly budget manager.record_spend("developer", 30000).await.unwrap(); let status = manager.check_budget("developer").await.unwrap(); assert!(status.exceeded); assert_eq!(status.monthly_remaining_cents, 0); } #[tokio::test] async fn test_budget_overspend() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); // Spend more than budget (overflow protection) manager.record_spend("developer", 35000).await.unwrap(); let status = manager.check_budget("developer").await.unwrap(); assert!(status.exceeded); assert_eq!(status.monthly_remaining_cents, 0); // Saturating subtract } #[tokio::test] async fn test_weekly_budget_independent() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); // Spend 100% of weekly budget but only 25% of monthly manager.record_spend("developer", 7500).await.unwrap(); let status = manager.check_budget("developer").await.unwrap(); assert_eq!(status.monthly_remaining_cents, 22500); assert_eq!(status.weekly_remaining_cents, 0); assert!(status.exceeded); // Both budgets checked } #[tokio::test] async fn test_fallback_provider() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); let fallback_dev = manager.get_fallback_provider("developer").await.unwrap(); assert_eq!(fallback_dev, "ollama"); let fallback_arch = manager.get_fallback_provider("architect").await.unwrap(); assert_eq!(fallback_arch, "gemini"); } #[tokio::test] async fn test_unknown_role_error() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); let result = manager.check_budget("unknown").await; assert!(result.is_err()); let result = manager.record_spend("unknown", 100).await; assert!(result.is_err()); } #[tokio::test] async fn test_get_all_budgets() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); manager.record_spend("architect", 5000).await.unwrap(); manager.record_spend("developer", 3000).await.unwrap(); let all_statuses = manager.get_all_budgets().await; assert_eq!(all_statuses.len(), 2); let arch_status = all_statuses .iter() .find(|s| s.role == "architect") .unwrap(); assert_eq!(arch_status.monthly_remaining_cents, 45000); let dev_status = all_statuses .iter() .find(|s| s.role == "developer") .unwrap(); assert_eq!(dev_status.monthly_remaining_cents, 27000); } #[tokio::test] async fn test_budget_status_comprehensive() { let budgets = create_test_budgets(); let manager = BudgetManager::new(budgets); // Spend 6000 cents: keeps us at 12% of monthly and 48% of weekly (both safe) manager.record_spend("architect", 6000).await.unwrap(); let status = manager.check_budget("architect").await.unwrap(); assert_eq!(status.monthly_remaining_cents, 44000); assert!((status.monthly_utilization - 0.12).abs() < 0.01); assert!(!status.exceeded); assert!(!status.near_threshold); // 12% < 80% assert_eq!(status.fallback_provider, "gemini"); }