agentic_core/
lib.rs

1//! # Agentic Core Library
2//!
3//! This crate provides the core functionality for the Agentic TUI application.
4//! It contains all the business logic, data structures, and communication protocols
5//! that are independent of any specific user interface.
6//!
7//! ## Modules
8//!
9//! - `models`: Data structures and validation logic for AI models
10//! - `settings`: Application configuration management
11//! - `theme`: UI theming system
12
13pub mod cloud;
14pub mod models;
15pub mod orchestrator;
16pub mod settings;
17pub mod theme;
18
19#[cfg(test)]
20mod tests {
21    use crate::models::ModelValidator;
22    use crate::settings::Settings;
23    use crate::theme::ThemeVariant;
24
25    #[test]
26    fn test_settings_default() {
27        let settings = Settings::default();
28        assert_eq!(settings.theme, ThemeVariant::EverforestDark);
29        assert_eq!(settings.endpoint, "localhost:11434");
30        assert_eq!(settings.local_model, "[SELECT]");
31        assert_eq!(settings.api_key, "sk-or-v1-982...b52");
32        assert_eq!(settings.cloud_model, "[SELECT]");
33    }
34
35    #[test]
36    fn test_settings_validation() {
37        let settings = Settings::default();
38        let result = settings.is_valid();
39
40        // Default settings should fail validation (placeholder values)
41        assert!(result.is_err());
42    }
43
44    #[tokio::test]
45    async fn test_openrouter_integration() {
46        let validator = ModelValidator::new();
47        let api_key = "sk-or-v1-test-key-redacted";
48
49        let result = validator.fetch_openrouter_models(api_key).await;
50
51        match result {
52            Ok(models) => {
53                println!("โœ… OpenRouter test passed! Found {} models", models.len());
54                assert!(!models.is_empty(), "Should find at least some models");
55
56                // Count free vs paid models
57                let free_models: Vec<_> = models
58                    .iter()
59                    .filter(|m| m.pricing.prompt == "0" && m.pricing.completion == "0")
60                    .collect();
61                let paid_models: Vec<_> = models
62                    .iter()
63                    .filter(|m| m.pricing.prompt != "0" || m.pricing.completion != "0")
64                    .collect();
65
66                println!(
67                    "  ๐Ÿ“Š Found {} free models, {} paid models",
68                    free_models.len(),
69                    paid_models.len()
70                );
71                assert!(
72                    !free_models.is_empty(),
73                    "Should have at least some free models"
74                );
75                assert!(
76                    !paid_models.is_empty(),
77                    "Should have at least some paid models"
78                );
79
80                // Print first few free models for verification
81                println!("  ๐Ÿ†“ Free models:");
82                for (i, model) in free_models.iter().enumerate().take(3) {
83                    println!(
84                        "    {}. {} ({}k context)",
85                        i + 1,
86                        model.name,
87                        model.context_length / 1000
88                    );
89                }
90
91                // Print first few paid models for verification
92                println!("  ๐Ÿ’ฐ Paid models:");
93                for (i, model) in paid_models.iter().enumerate().take(2) {
94                    println!(
95                        "    {}. {} ({}k context)",
96                        i + 1,
97                        model.name,
98                        model.context_length / 1000
99                    );
100                }
101            }
102            Err(e) => {
103                println!("โŒ OpenRouter test failed: {}", e);
104                panic!("OpenRouter integration test failed: {}", e);
105            }
106        }
107    }
108
109    #[tokio::test]
110    async fn test_ollama_integration() {
111        let validator = ModelValidator::new();
112        let endpoint = "localhost:11434";
113
114        let result = validator.fetch_ollama_models(endpoint).await;
115
116        match result {
117            Ok(models) => {
118                println!("โœ… Ollama test passed! Found {} local models", models.len());
119
120                // Print available models
121                for (i, model) in models.iter().enumerate() {
122                    println!("  {}. {} ({})", i + 1, model.name, model.size);
123                }
124
125                // If we have models, test validation with the first one
126                if let Some(first_model) = models.first() {
127                    println!("๐Ÿ” Testing validation with model: {}", first_model.name);
128                    let validation_result = validator
129                        .validate_local_endpoint(endpoint, &first_model.name)
130                        .await;
131                    match validation_result {
132                        Ok(()) => println!("โœ… Local endpoint validation passed!"),
133                        Err(e) => println!("โŒ Local endpoint validation failed: {}", e),
134                    }
135                }
136            }
137            Err(e) => {
138                println!("โš ๏ธ Ollama test skipped - not running or accessible: {}", e);
139                // Don't panic here since Ollama might not be running in all test environments
140            }
141        }
142    }
143
144    #[tokio::test]
145    async fn test_dual_endpoint_validation() {
146        use crate::settings::Settings;
147
148        println!("๐Ÿ”„ Testing dual endpoint validation...");
149
150        let validator = ModelValidator::new();
151
152        // First, get actual working models
153        let ollama_models = validator.fetch_ollama_models("localhost:11434").await;
154        let api_key = "sk-or-v1-test-key-redacted";
155        let openrouter_models = validator.fetch_openrouter_models(api_key).await;
156
157        match (ollama_models, openrouter_models) {
158            (Ok(local_models), Ok(cloud_models))
159                if !local_models.is_empty() && !cloud_models.is_empty() =>
160            {
161                println!("๐Ÿ“ฆ Testing with real models:");
162                println!("  Local: {}", local_models[0].name);
163                println!("  Cloud: {}", cloud_models[0].name);
164
165                // Create settings with actual working models
166                let mut settings = Settings::default();
167                settings.local_model = local_models[0].name.clone();
168                settings.cloud_model = cloud_models[0].id.clone();
169                settings.api_key = api_key.to_string();
170
171                // Test local-only validation
172                match settings.validate_local_only().await {
173                    Ok(()) => println!("โœ… Local-only validation passed!"),
174                    Err(e) => println!("โŒ Local-only validation failed: {:?}", e),
175                }
176
177                // Test cloud-only validation
178                match settings.validate_cloud_only().await {
179                    Ok(()) => println!("โœ… Cloud-only validation passed!"),
180                    Err(e) => println!("โŒ Cloud-only validation failed: {:?}", e),
181                }
182
183                // Test full dual validation
184                match settings.validate_endpoints().await {
185                    Ok(()) => println!("โœ… Dual endpoint validation passed!"),
186                    Err(e) => println!("โŒ Dual endpoint validation failed: {:?}", e),
187                }
188            }
189            (Ok(local_models), Ok(cloud_models)) => {
190                // Handle case where models exist but might be empty
191                println!("๐Ÿ“ฆ Models found but lists might be empty:");
192                println!("  Local models: {}", local_models.len());
193                println!("  Cloud models: {}", cloud_models.len());
194            }
195            (Err(local_err), Ok(cloud_models)) => {
196                println!("โš ๏ธ Local endpoint not available: {}", local_err);
197                println!(
198                    "โœ… Cloud endpoint working with {} models",
199                    cloud_models.len()
200                );
201            }
202            (Ok(local_models), Err(cloud_err)) => {
203                println!(
204                    "โœ… Local endpoint working with {} models",
205                    local_models.len()
206                );
207                println!("โš ๏ธ Cloud endpoint not available: {}", cloud_err);
208            }
209            (Err(local_err), Err(cloud_err)) => {
210                println!("โš ๏ธ Both endpoints unavailable:");
211                println!("  Local: {}", local_err);
212                println!("  Cloud: {}", cloud_err);
213            }
214        }
215    }
216
217    #[tokio::test]
218    async fn test_modal_model_loading() {
219        println!("๐Ÿงช Testing modal model loading scenarios...");
220
221        let validator = ModelValidator::new();
222        let api_key = "sk-or-v1-test-key-redacted";
223
224        // Test OpenRouter models for modal display
225        match validator.fetch_openrouter_models(api_key).await {
226            Ok(models) => {
227                println!("โœ… Cloud models loaded for modal: {} models", models.len());
228                assert!(!models.is_empty(), "Should have models for display");
229
230                // Count free vs paid models and verify sorting
231                let mut free_count = 0;
232                let mut paid_count = 0;
233                let mut found_paid_after_free = false;
234
235                for model in models.iter() {
236                    let is_free = model.pricing.prompt == "0" && model.pricing.completion == "0";
237                    if is_free {
238                        free_count += 1;
239                        // If we already found paid models, this breaks the sorting
240                        assert!(
241                            !found_paid_after_free,
242                            "Free models should come before paid models"
243                        );
244                    } else {
245                        paid_count += 1;
246                        found_paid_after_free = true;
247                    }
248                }
249
250                println!(
251                    "  ๐Ÿ“Š Model breakdown: {} free, {} paid",
252                    free_count, paid_count
253                );
254                assert!(free_count > 0, "Should have at least some free models");
255                assert!(paid_count > 0, "Should have at least some paid models");
256
257                // Verify the first few models have the required fields for UI display
258                for model in models.iter().take(3) {
259                    let pricing_type =
260                        if model.pricing.prompt == "0" && model.pricing.completion == "0" {
261                            "(free)"
262                        } else {
263                            "(paid)"
264                        };
265                    println!(
266                        "  - {} {} ({} tokens)",
267                        model.name, pricing_type, model.context_length
268                    );
269                    assert!(!model.name.is_empty(), "Model name should not be empty");
270                    assert!(!model.id.is_empty(), "Model ID should not be empty");
271                    assert!(
272                        model.context_length > 0,
273                        "Context length should be positive"
274                    );
275                }
276            }
277            Err(e) => {
278                panic!("Failed to load OpenRouter models for modal: {}", e);
279            }
280        }
281
282        // Test Ollama models for modal display
283        match validator.fetch_ollama_models("localhost:11434").await {
284            Ok(models) => {
285                println!("โœ… Local models loaded for modal: {} models", models.len());
286                for model in models.iter().take(3) {
287                    println!("  - {} ({})", model.name, model.size);
288                    assert!(!model.name.is_empty(), "Model name should not be empty");
289                    assert!(!model.size.is_empty(), "Model size should not be empty");
290                }
291            }
292            Err(e) => {
293                println!("โš ๏ธ Local models test skipped (Ollama not running): {}", e);
294            }
295        }
296    }
297
298    #[test]
299    fn test_api_key_truncation() {
300        // Test the API key display formatting (simulating the settings modal function)
301        fn format_api_key_display(api_key: &str) -> String {
302            if api_key.is_empty() {
303                return String::new();
304            }
305
306            if api_key.len() <= 21 {
307                api_key.to_string()
308            } else {
309                format!("{}...{}", &api_key[..15], &api_key[api_key.len() - 3..])
310            }
311        }
312
313        let test_key = "sk-or-v1-test-key-redacted";
314        let formatted = format_api_key_display(test_key);
315
316        println!("๐Ÿงช Testing API key truncation:");
317        println!("  Original: {} (len: {})", test_key, test_key.len());
318        println!("  Formatted: {} (len: {})", formatted, formatted.len());
319
320        assert_eq!(formatted, "sk-or-v1-7d9200...3ac");
321        assert_eq!(formatted.len(), 21); // 15 chars + "..." + 3 chars = 21 total
322
323        // Test shorter key (should not be truncated)
324        let short_key = "sk-test-123";
325        let short_formatted = format_api_key_display(short_key);
326        assert_eq!(short_formatted, short_key);
327
328        // Test empty key
329        let empty_formatted = format_api_key_display("");
330        assert_eq!(empty_formatted, "");
331
332        println!("โœ… API key truncation test passed!");
333    }
334}