From e2a5bf091a94e28267e5103f8b70ade5bcc08e30 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Tue, 23 Dec 2025 15:52:35 -0300 Subject: [PATCH] Update server components and keywords --- Cargo.toml | 54 +-- PROMPT.md | 33 +- src/attendance/llm_assist.rs | 100 +----- src/attendance/mod.rs | 25 -- src/basic/compiler/goto_transform.rs | 123 ------- src/basic/keywords/a2a_protocol.rs | 104 ------ src/basic/keywords/add_bot.rs | 69 ---- src/basic/keywords/add_member.rs | 31 -- src/basic/keywords/add_suggestion.rs | 59 ---- src/basic/keywords/agent_reflection.rs | 115 ------ src/basic/keywords/arrays/contains.rs | 73 ---- src/basic/keywords/arrays/mod.rs | 39 -- src/basic/keywords/arrays/push_pop.rs | 45 --- src/basic/keywords/arrays/slice.rs | 64 ---- src/basic/keywords/arrays/sort.rs | 58 --- src/basic/keywords/arrays/unique.rs | 89 ----- src/basic/keywords/book.rs | 38 -- src/basic/keywords/card.rs | 33 -- src/basic/keywords/clear_kb.rs | 20 -- src/basic/keywords/code_sandbox.rs | 82 ----- src/basic/keywords/core_functions.rs | 10 - src/basic/keywords/create_task.rs | 25 -- src/basic/keywords/crm/attendance.rs | 168 +-------- src/basic/keywords/crm/score_lead.rs | 54 --- src/basic/keywords/data_operations.rs | 42 --- src/basic/keywords/datetime/dateadd.rs | 49 +-- src/basic/keywords/datetime/extract.rs | 33 +- src/basic/keywords/datetime/now.rs | 59 ---- src/basic/keywords/episodic_memory.rs | 114 ------ src/basic/keywords/errors/mod.rs | 15 - src/basic/keywords/errors/on_error.rs | 64 ---- src/basic/keywords/errors/throw.rs | 9 - src/basic/keywords/file_operations.rs | 20 -- src/basic/keywords/hear_talk.rs | 72 ---- src/basic/keywords/http_operations.rs | 35 -- src/basic/keywords/human_approval.rs | 148 -------- src/basic/keywords/import_export.rs | 40 --- src/basic/keywords/kb_statistics.rs | 41 --- src/basic/keywords/knowledge_graph.rs | 93 ----- src/basic/keywords/lead_scoring.rs | 10 - src/basic/keywords/llm_macros.rs | 30 -- src/basic/keywords/math/abs.rs | 21 -- src/basic/keywords/math/aggregate.rs | 34 -- src/basic/keywords/math/basic_math.rs | 43 --- src/basic/keywords/math/minmax.rs | 15 - src/basic/keywords/math/random.rs | 10 - src/basic/keywords/math/round.rs | 19 - src/basic/keywords/math/trig.rs | 29 -- src/basic/keywords/mcp_directory.rs | 48 --- src/basic/keywords/messaging/send_template.rs | 66 ---- src/basic/keywords/model_routing.rs | 80 ----- src/basic/keywords/on_change.rs | 209 ----------- src/basic/keywords/on_email.rs | 198 ----------- src/basic/keywords/play.rs | 96 ----- src/basic/keywords/procedures.rs | 162 --------- src/basic/keywords/qrcode.rs | 40 --- src/basic/keywords/remember.rs | 15 - src/basic/keywords/save_from_unstructured.rs | 22 -- src/basic/keywords/send_mail.rs | 25 -- src/basic/keywords/send_template.rs | 10 - src/basic/keywords/set_schedule.rs | 168 --------- src/basic/keywords/sms.rs | 43 --- src/basic/keywords/social/get_metrics.rs | 20 -- .../keywords/social/post_to_scheduled.rs | 17 - src/basic/keywords/social_media.rs | 10 - src/basic/keywords/string_functions.rs | 62 +--- src/basic/keywords/switch_case.rs | 77 +--- src/basic/keywords/table_definition.rs | 133 ------- src/basic/keywords/transfer_to_human.rs | 91 ----- src/basic/keywords/use_account.rs | 27 -- src/basic/keywords/use_kb.rs | 15 - src/basic/keywords/use_website.rs | 32 -- src/basic/keywords/user_memory.rs | 12 - src/basic/keywords/validation/isempty.rs | 56 --- src/basic/keywords/validation/isnull.rs | 24 -- src/basic/keywords/validation/nvl_iif.rs | 41 --- src/basic/keywords/validation/str_val.rs | 18 - src/basic/keywords/validation/typeof_check.rs | 24 -- src/basic/keywords/weather.rs | 38 -- src/basic/keywords/webhook.rs | 70 ---- src/calendar/mod.rs | 47 --- src/compliance/access_review.rs | 31 +- src/compliance/code_scanner.rs | 80 ----- src/compliance/mod.rs | 41 --- src/compliance/policy_checker.rs | 16 +- src/compliance/training_tracker.rs | 59 ++-- src/console/wizard.rs | 28 -- src/core/bot/manager.rs | 55 --- src/core/config/model_routing_config.rs | 87 ----- src/core/config/sse_config.rs | 43 --- src/core/config/user_memory_config.rs | 65 ---- src/core/kb/document_processor.rs | 62 ---- src/core/kb/embedding_generator.rs | 26 -- src/core/kb/kb_indexer.rs | 46 --- src/core/kb/mod.rs | 24 -- src/core/oauth/mod.rs | 49 --- src/core/oauth/providers.rs | 61 ---- src/core/package_manager/cache.rs | 181 ---------- .../package_manager/setup/directory_setup.rs | 14 - src/core/package_manager/setup/email_setup.rs | 29 -- src/core/rate_limit.rs | 43 --- src/core/secrets/mod.rs | 67 ---- src/core/shared/models.rs | 29 -- src/core/shared/state.rs | 61 ---- src/core/shared/test_utils.rs | 60 ---- src/core/urls.rs | 26 -- src/drive/vectordb.rs | 176 +++++---- src/email/stalwart_client.rs | 128 +------ src/email/stalwart_sync.rs | 85 ----- src/email/vectordb.rs | 144 ++++---- src/llm/cache_test.rs | 334 ------------------ src/llm/observability.rs | 116 ------ src/security/antivirus.rs | 52 --- src/security/ca.rs | 25 -- src/security/cert_pinning.rs | 108 ------ src/security/integration.rs | 65 ---- src/security/mod.rs | 35 -- src/security/mutual_tls.rs | 66 ---- src/security/tls.rs | 41 --- src/sources/mcp.rs | 28 -- src/timeseries/mod.rs | 45 --- src/vector-db/bm25_config.rs | 107 ------ src/vector-db/hybrid_search.rs | 153 +------- src/vector-db/vectordb_indexer.rs | 215 +++++------ src/weba/mod.rs | 39 +- src/whatsapp/mod.rs | 56 --- 126 files changed, 398 insertions(+), 7455 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eef71c61b..8604c6f30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,7 +141,7 @@ askama = "0.12" askama_axum = "0.4" tracing-subscriber = { version = "0.3", features = ["fmt"] } urlencoding = "2.1" -uuid = { version = "1.11", features = ["serde", "v4"] } +uuid = { version = "1.11", features = ["serde", "v4", "v5"] } # === TLS/SECURITY DEPENDENCIES === rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } @@ -242,53 +242,5 @@ mockito = "1.7.0" tempfile = "3" # === SECURITY AND CODE QUALITY CONFIGURATION === -[lints.rust] -unused_imports = "warn" -unused_variables = "warn" -unused_mut = "warn" -unsafe_code = "deny" -missing_debug_implementations = "warn" - -[lints.clippy] -all = "warn" -pedantic = "warn" -nursery = "warn" -cargo = "warn" -unwrap_used = "warn" -expect_used = "warn" -panic = "warn" -todo = "warn" -# Disabled: has false positives for functions with mut self, heap types (Vec, String) -missing_const_for_fn = "allow" -# Disabled: transitive dependencies we cannot control -multiple_crate_versions = "allow" -# Disabled: many keyword functions need owned types for Rhai integration -needless_pass_by_value = "allow" - -[profile.release] -lto = true -opt-level = "z" -strip = true -panic = "abort" -codegen-units = 1 -overflow-checks = true - -[profile.ci] -inherits = "release" -lto = false -codegen-units = 16 -debug = false - -[profile.low-memory] -inherits = "release" -lto = "thin" -codegen-units = 16 -debug = false -incremental = false -opt-level = "s" - -[profile.dev] -debug = 0 -incremental = true -codegen-units = 256 -opt-level = 0 +[lints] +workspace = true diff --git a/PROMPT.md b/PROMPT.md index e4bfb0649..92b145dc3 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -38,8 +38,13 @@ todo = "warn" ❌ NEVER leave dead code - DELETE it or IMPLEMENT it ❌ NEVER use approximate constants (3.14159) - use std::f64::consts::PI ❌ NEVER silence clippy in code - FIX THE CODE or configure in Cargo.toml -❌ NEVER add comments explaining what code does - code must be self-documenting ❌ NEVER use CDN links - all assets must be local +❌ NEVER run cargo check or cargo clippy - USE ONLY the diagnostics tool +❌ NEVER add comments - code must be self-documenting via types and naming +❌ NEVER add file header comments (//! or /*!) - no module docs +❌ NEVER add function doc comments (///) - types are the documentation +❌ NEVER add ASCII art or banners in code +❌ NEVER add TODO/FIXME/HACK comments - fix it or delete it ``` --- @@ -177,6 +182,31 @@ pub fn calculate() -> i32 { } pub fn calculate() -> i32 { } ``` +### Zero Comments Policy + +```rust +// ❌ WRONG - any comments +/// Returns the user's full name +fn get_full_name(&self) -> String { } + +// Validate input before processing +fn process(data: &str) { } + +//! This module handles user authentication + +// ✅ CORRECT - self-documenting code, no comments +fn full_name(&self) -> String { } + +fn process_validated_input(data: &str) { } +``` + +**Why zero comments:** +- Rust's type system documents intent (Result, Option, traits) +- Comments become stale when code changes +- LLMs can infer intent from well-structured code +- Good naming > comments +- Types are the documentation + ### Const Functions ```rust @@ -368,6 +398,7 @@ src/shared/models.rs # Database models ## Remember - **ZERO WARNINGS** - Every clippy warning must be fixed +- **ZERO COMMENTS** - No comments, no doc comments, no file headers, no ASCII art - **NO ALLOW IN CODE** - Never use #[allow()] in source files - **CARGO.TOML EXCEPTIONS OK** - Disable lints with false positives in Cargo.toml with comment - **NO DEAD CODE** - Delete unused code, never prefix with _ diff --git a/src/attendance/llm_assist.rs b/src/attendance/llm_assist.rs index 84a3e5b5d..4a632e5a2 100644 --- a/src/attendance/llm_assist.rs +++ b/src/attendance/llm_assist.rs @@ -417,7 +417,7 @@ fn get_bot_system_prompt(bot_id: Uuid, work_path: &str) -> String { pub async fn generate_tips( State(state): State>, Json(request): Json, -) -> impl IntoResponse { +) -> (StatusCode, Json) { info!("Generating tips for session {}", request.session_id); // Get session and bot info @@ -528,7 +528,7 @@ Provide tips for the attendant."#, pub async fn polish_message( State(state): State>, Json(request): Json, -) -> impl IntoResponse { +) -> (StatusCode, Json) { info!("Polishing message for session {}", request.session_id); let session_result = get_session(&state, request.session_id).await; @@ -626,7 +626,7 @@ Respond in JSON format: pub async fn generate_smart_replies( State(state): State>, Json(request): Json, -) -> impl IntoResponse { +) -> (StatusCode, Json) { info!( "Generating smart replies for session {}", request.session_id @@ -730,7 +730,7 @@ Generate 3 reply options for the attendant."#, pub async fn generate_summary( State(state): State>, Path(session_id): Path, -) -> impl IntoResponse { +) -> (StatusCode, Json) { info!("Generating summary for session {}", session_id); let session_result = get_session(&state, session_id).await; @@ -1133,13 +1133,13 @@ async fn handle_take_command( .and_then(|v| v.as_str()) .unwrap_or("Unknown"); - Ok(format!( + Ok::(format!( " *Conversation assigned*\n\nCustomer: *{}*\nSession: {}\n\nYou can now respond to this customer. Their messages will be forwarded to you.", name, &session.id.to_string()[..8] )) } else { - Ok(" No conversations waiting in queue.".to_string()) + Ok::(" No conversations waiting in queue.".to_string()) } }) .await @@ -1196,6 +1196,7 @@ async fn handle_status_command( .load(&mut db_conn) .map_err(|e| e.to_string())?; + let session_count = sessions.len(); for session in sessions { let mut ctx = session.context_data.clone(); ctx["attendant_status"] = serde_json::json!(status_val); @@ -1207,7 +1208,7 @@ async fn handle_status_command( .map_err(|e| e.to_string())?; } - Ok::(sessions.len()) + Ok::(session_count) }) .await .map_err(|e| e.to_string())?; @@ -1369,8 +1370,7 @@ async fn handle_tips_command( }; // Generate tips - let response = generate_tips(State(state.clone()), Json(request)).await; - let (_, Json(tip_response)) = response.into_response().into_parts(); + let (_, Json(tip_response)) = generate_tips(State(state.clone()), Json(request)).await; if tip_response.tips.is_empty() { return Ok(" No specific tips for this conversation yet.".to_string()); @@ -1410,8 +1410,7 @@ async fn handle_polish_command( tone: "professional".to_string(), }; - let response = polish_message(State(state.clone()), Json(request)).await; - let (_, Json(polish_response)) = response.into_response().into_parts(); + let (_, Json(polish_response)) = polish_message(State(state.clone()), Json(request)).await; if !polish_response.success { return Err(polish_response @@ -1447,8 +1446,7 @@ async fn handle_replies_command( history, }; - let response = generate_smart_replies(State(state.clone()), Json(request)).await; - let (_, Json(replies_response)) = response.into_response().into_parts(); + let (_, Json(replies_response)) = generate_smart_replies(State(state.clone()), Json(request)).await; if replies_response.replies.is_empty() { return Ok(" No reply suggestions available.".to_string()); @@ -1476,8 +1474,7 @@ async fn handle_summary_command( ) -> Result { let session_id = current_session.ok_or("No active conversation")?; - let response = generate_summary(State(state.clone()), Path(session_id)).await; - let (_, Json(summary_response)) = response.into_response().into_parts(); + let (_, Json(summary_response)) = generate_summary(State(state.clone()), Path(session_id)).await; if !summary_response.success { return Err(summary_response @@ -2102,76 +2099,3 @@ fn analyze_sentiment_keywords(message: &str) -> SentimentAnalysis { } // Tests - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_config_defaults() { - let config = LlmAssistConfig::default(); - assert!(!config.tips_enabled); - assert!(!config.polish_enabled); - assert!(!config.any_enabled()); - } - - #[test] - fn test_fallback_tips_urgent() { - let tips = generate_fallback_tips("This is URGENT! I need help immediately!"); - assert!(!tips.is_empty()); - assert!(tips.iter().any(|t| matches!(t.tip_type, TipType::Warning))); - } - - #[test] - fn test_fallback_tips_question() { - let tips = generate_fallback_tips("How do I reset my password?"); - assert!(!tips.is_empty()); - assert!(tips.iter().any(|t| matches!(t.tip_type, TipType::Intent))); - } - - #[test] - fn test_sentiment_positive() { - let sentiment = analyze_sentiment_keywords("Thank you so much! This is great!"); - assert_eq!(sentiment.overall, "positive"); - assert!(sentiment.score > 0.0); - assert_eq!(sentiment.escalation_risk, "low"); - } - - #[test] - fn test_sentiment_negative() { - let sentiment = - analyze_sentiment_keywords("This is terrible! I'm very frustrated with this problem."); - assert_eq!(sentiment.overall, "negative"); - assert!(sentiment.score < 0.0); - assert!(sentiment.escalation_risk == "medium" || sentiment.escalation_risk == "high"); - } - - #[test] - fn test_sentiment_urgent() { - let sentiment = analyze_sentiment_keywords("I need help ASAP! This is urgent!"); - assert!(sentiment.urgency == "high" || sentiment.urgency == "urgent"); - } - - #[test] - fn test_extract_json() { - let response = "Here is the result: {\"key\": \"value\"} and some more text."; - let json = extract_json(response); - assert_eq!(json, "{\"key\": \"value\"}"); - } - - #[test] - fn test_fallback_replies() { - let replies = generate_fallback_replies(); - assert_eq!(replies.len(), 3); - assert!(replies.iter().any(|r| r.category == "greeting")); - assert!(replies.iter().any(|r| r.category == "follow_up")); - } - - #[test] - fn test_help_text() { - let help = get_help_text(); - assert!(help.contains("/queue")); - assert!(help.contains("/tips")); - assert!(help.contains("/polish")); - } -} diff --git a/src/attendance/mod.rs b/src/attendance/mod.rs index 465376d54..23f78473c 100644 --- a/src/attendance/mod.rs +++ b/src/attendance/mod.rs @@ -661,28 +661,3 @@ async fn handle_attendant_message( } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_module_exports() { - // Test that types are properly exported - let _config = KeywordConfig::default(); - let _parser = KeywordParser::new(); - } - - #[test] - fn test_respond_request_parse() { - let json = r#"{ - "session_id": "123e4567-e89b-12d3-a456-426614174000", - "message": "Hello, how can I help?", - "attendant_id": "att-001" - }"#; - - let request: AttendantRespondRequest = serde_json::from_str(json).unwrap(); - assert_eq!(request.attendant_id, "att-001"); - assert_eq!(request.message, "Hello, how can I help?"); - } -} diff --git a/src/basic/compiler/goto_transform.rs b/src/basic/compiler/goto_transform.rs index bbe1e6052..2d2d4bc7e 100644 --- a/src/basic/compiler/goto_transform.rs +++ b/src/basic/compiler/goto_transform.rs @@ -379,126 +379,3 @@ fn transform_line(line: &str) -> String { // Not a GOTO line, return as-is trimmed.to_string() } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_is_label_line() { - assert!(is_label_line("start:")); - assert!(is_label_line(" mainLoop:")); - assert!(is_label_line("my_label:")); - assert!(is_label_line("label123:")); - - assert!(!is_label_line("TALK \"hello:\"")); - assert!(!is_label_line("' comment:")); - assert!(!is_label_line("CASE:")); - assert!(!is_label_line("123label:")); - assert!(!is_label_line("has space:")); - } - - #[test] - fn test_extract_goto_target() { - assert_eq!(extract_goto_target("GOTO start"), Some("start".to_string())); - assert_eq!( - extract_goto_target(" GOTO myLabel"), - Some("myLabel".to_string()) - ); - assert_eq!( - extract_goto_target("IF x > 5 THEN GOTO done"), - Some("done".to_string()) - ); - assert_eq!(extract_goto_target("TALK \"hello\""), None); - } - - #[test] - fn test_transform_line_simple_goto() { - assert_eq!( - transform_line("GOTO start"), - "__goto_label = \"start\"; continue;" - ); - assert_eq!( - transform_line(" GOTO myLoop "), - "__goto_label = \"myLoop\"; continue;" - ); - } - - #[test] - fn test_transform_line_if_then_goto() { - let result = transform_line("IF x < 10 THEN GOTO start"); - assert!(result.contains("if x < 10")); - assert!(result.contains("__goto_label = \"start\"")); - assert!(result.contains("continue")); - } - - #[test] - fn test_transform_line_if_goto_no_then() { - let result = transform_line("IF x < 10 GOTO start"); - assert!(result.contains("if x < 10")); - assert!(result.contains("__goto_label = \"start\"")); - } - - #[test] - fn test_transform_line_not_goto() { - assert_eq!(transform_line("TALK \"Hello\""), "TALK \"Hello\""); - assert_eq!(transform_line("x = x + 1"), "x = x + 1"); - assert_eq!(transform_line("ON ERROR GOTO 0"), "ON ERROR GOTO 0"); - } - - #[test] - fn test_has_goto_constructs() { - assert!(has_goto_constructs("start:\nTALK \"hi\"\nGOTO start")); - assert!(has_goto_constructs("IF x > 0 THEN GOTO done")); - assert!(!has_goto_constructs("TALK \"hello\"\nWAIT 1")); - assert!(!has_goto_constructs("ON ERROR GOTO 0")); // This is special, not regular GOTO - } - - #[test] - fn test_transform_goto_simple() { - let input = r#"start: - TALK "Hello" - x = x + 1 - IF x < 3 THEN GOTO start - TALK "Done""#; - - let output = transform_goto(input); - - assert!(output.contains("__goto_label")); - assert!(output.contains("while")); - assert!(output.contains("\"start\"")); - assert!(output.contains("WARNING")); - } - - #[test] - fn test_transform_goto_no_goto() { - let input = "TALK \"Hello\"\nTALK \"World\""; - let output = transform_goto(input); - assert_eq!(output, input); // Unchanged - } - - #[test] - fn test_transform_goto_multiple_labels() { - let input = r#"start: - TALK "Start" - GOTO middle -middle: - TALK "Middle" - GOTO done -done: - TALK "Done""#; - - let output = transform_goto(input); - - assert!(output.contains("\"start\"")); - assert!(output.contains("\"middle\"")); - assert!(output.contains("\"done\"")); - } - - #[test] - fn test_infinite_loop_protection() { - let output = transform_goto("loop:\nGOTO loop"); - assert!(output.contains("__goto_max_iterations")); - assert!(output.contains("throw")); - } -} diff --git a/src/basic/keywords/a2a_protocol.rs b/src/basic/keywords/a2a_protocol.rs index 769a62f38..ff8085c61 100644 --- a/src/basic/keywords/a2a_protocol.rs +++ b/src/basic/keywords/a2a_protocol.rs @@ -839,107 +839,3 @@ pub async fn respond_to_a2a_message( } // Tests - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_a2a_message_creation() { - let msg = A2AMessage::new( - "bot_a", - Some("bot_b"), - A2AMessageType::Request, - serde_json::json!({"test": "data"}), - Uuid::new_v4(), - ); - - assert_eq!(msg.from_agent, "bot_a"); - assert_eq!(msg.to_agent, Some("bot_b".to_string())); - assert_eq!(msg.message_type, A2AMessageType::Request); - assert_eq!(msg.hop_count, 0); - } - - #[test] - fn test_a2a_message_response() { - let original = A2AMessage::new( - "bot_a", - Some("bot_b"), - A2AMessageType::Request, - serde_json::json!({"question": "test"}), - Uuid::new_v4(), - ); - - let response = original.create_response("bot_b", serde_json::json!({"answer": "result"})); - - assert_eq!(response.from_agent, "bot_b"); - assert_eq!(response.to_agent, Some("bot_a".to_string())); - assert_eq!(response.message_type, A2AMessageType::Response); - assert_eq!(response.correlation_id, original.correlation_id); - assert_eq!(response.hop_count, 1); - } - - #[test] - fn test_message_type_display() { - assert_eq!(A2AMessageType::Request.to_string(), "request"); - assert_eq!(A2AMessageType::Response.to_string(), "response"); - assert_eq!(A2AMessageType::Broadcast.to_string(), "broadcast"); - assert_eq!(A2AMessageType::Delegate.to_string(), "delegate"); - } - - #[test] - fn test_message_type_from_str() { - assert_eq!(A2AMessageType::from("request"), A2AMessageType::Request); - assert_eq!(A2AMessageType::from("RESPONSE"), A2AMessageType::Response); - assert_eq!(A2AMessageType::from("unknown"), A2AMessageType::Request); - } - - #[test] - fn test_a2a_config_default() { - let config = A2AConfig::default(); - assert!(config.enabled); - assert_eq!(config.timeout_seconds, 30); - assert_eq!(config.max_hops, 5); - assert_eq!(config.protocol_version, "1.0"); - } - - #[test] - fn test_message_not_expired() { - let msg = A2AMessage::new( - "bot_a", - Some("bot_b"), - A2AMessageType::Request, - serde_json::json!({}), - Uuid::new_v4(), - ); - - assert!(!msg.is_expired()); - } - - #[test] - fn test_max_hops_not_exceeded() { - let msg = A2AMessage::new( - "bot_a", - Some("bot_b"), - A2AMessageType::Request, - serde_json::json!({}), - Uuid::new_v4(), - ); - - assert!(!msg.max_hops_exceeded(5)); - } - - #[test] - fn test_max_hops_exceeded() { - let mut msg = A2AMessage::new( - "bot_a", - Some("bot_b"), - A2AMessageType::Request, - serde_json::json!({}), - Uuid::new_v4(), - ); - msg.hop_count = 5; - - assert!(msg.max_hops_exceeded(5)); - } -} diff --git a/src/basic/keywords/add_bot.rs b/src/basic/keywords/add_bot.rs index 6d3a95adf..8f5398fce 100644 --- a/src/basic/keywords/add_bot.rs +++ b/src/basic/keywords/add_bot.rs @@ -867,72 +867,3 @@ struct BotConfigRow { #[diesel(sql_type = diesel::sql_types::Nullable)] model_config: Option, } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_trigger_from_keywords() { - let trigger = BotTrigger::from_keywords(vec!["finance".to_string(), "money".to_string()]); - assert_eq!(trigger.trigger_type, TriggerType::Keyword); - assert_eq!(trigger.keywords.unwrap().len(), 2); - } - - #[test] - fn test_match_bot_triggers() { - let bots = vec![ - SessionBot { - id: Uuid::new_v4(), - session_id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - bot_name: "finance-bot".to_string(), - trigger: BotTrigger::from_keywords(vec!["money".to_string(), "budget".to_string()]), - priority: 1, - is_active: true, - }, - SessionBot { - id: Uuid::new_v4(), - session_id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - bot_name: "hr-bot".to_string(), - trigger: BotTrigger::from_keywords(vec![ - "vacation".to_string(), - "employee".to_string(), - ]), - priority: 0, - is_active: true, - }, - ]; - - let matches = match_bot_triggers("How much money do I have?", &bots); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].bot_name, "finance-bot"); - - let matches = match_bot_triggers("I need to request vacation", &bots); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].bot_name, "hr-bot"); - - let matches = match_bot_triggers("Hello world", &bots); - assert!(matches.is_empty()); - } - - #[test] - fn test_match_tool_triggers() { - let bots = vec![SessionBot { - id: Uuid::new_v4(), - session_id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - bot_name: "data-bot".to_string(), - trigger: BotTrigger::from_tools(vec!["AGGREGATE".to_string(), "CHART".to_string()]), - priority: 1, - is_active: true, - }]; - - let matches = match_tool_triggers("aggregate", &bots); - assert_eq!(matches.len(), 1); - - let matches = match_tool_triggers("SEND", &bots); - assert!(matches.is_empty()); - } -} diff --git a/src/basic/keywords/add_member.rs b/src/basic/keywords/add_member.rs index 79369629a..976bcf7d9 100644 --- a/src/basic/keywords/add_member.rs +++ b/src/basic/keywords/add_member.rs @@ -483,34 +483,3 @@ async fn create_team_channel( trace!("Created communication channel for team {}", team_name); Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_role() { - assert_eq!(validate_role("admin"), "admin"); - assert_eq!(validate_role("ADMIN"), "admin"); - assert_eq!(validate_role("contributor"), "contributor"); - assert_eq!(validate_role("viewer"), "viewer"); - assert_eq!(validate_role("unknown"), "member"); - } - - #[test] - fn test_get_permissions_for_role() { - let admin_perms = get_permissions_for_role("admin"); - assert!(admin_perms.get("read").unwrap().as_bool().unwrap()); - assert!(admin_perms.get("write").unwrap().as_bool().unwrap()); - assert!(admin_perms - .get("manage_members") - .unwrap() - .as_bool() - .unwrap()); - - let viewer_perms = get_permissions_for_role("viewer"); - assert!(viewer_perms.get("read").unwrap().as_bool().unwrap()); - assert!(!viewer_perms.get("write").unwrap().as_bool().unwrap()); - assert!(!viewer_perms.get("delete").unwrap().as_bool().unwrap()); - } -} diff --git a/src/basic/keywords/add_suggestion.rs b/src/basic/keywords/add_suggestion.rs index 6ff50539c..b4b522bdb 100644 --- a/src/basic/keywords/add_suggestion.rs +++ b/src/basic/keywords/add_suggestion.rs @@ -297,62 +297,3 @@ fn add_tool_suggestion( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_suggestion_json_context() { - let suggestion = json!({ - "type": "context", - "context": "products", - "text": "View Products", - "action": { - "type": "select_context", - "context": "products" - } - }); - - assert_eq!(suggestion["type"], "context"); - assert_eq!(suggestion["action"]["type"], "select_context"); - } - - #[test] - fn test_suggestion_json_tool_no_params() { - let suggestion = json!({ - "type": "tool", - "tool": "search_kb", - "text": "Search Knowledge Base", - "action": { - "type": "invoke_tool", - "tool": "search_kb", - "params": Option::>::None, - "prompt_for_params": true - } - }); - - assert_eq!(suggestion["type"], "tool"); - assert_eq!(suggestion["action"]["prompt_for_params"], true); - } - - #[test] - fn test_suggestion_json_tool_with_params() { - let params = vec!["query".to_string(), "products".to_string()]; - let suggestion = json!({ - "type": "tool", - "tool": "search_kb", - "text": "Search Products", - "action": { - "type": "invoke_tool", - "tool": "search_kb", - "params": params, - "prompt_for_params": false - } - }); - - assert_eq!(suggestion["type"], "tool"); - assert_eq!(suggestion["action"]["prompt_for_params"], false); - assert!(suggestion["action"]["params"].is_array()); - } -} diff --git a/src/basic/keywords/agent_reflection.rs b/src/basic/keywords/agent_reflection.rs index 8d03cac5d..e97129302 100644 --- a/src/basic/keywords/agent_reflection.rs +++ b/src/basic/keywords/agent_reflection.rs @@ -1079,118 +1079,3 @@ async fn set_reflection_enabled( } // Tests - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_reflection_type_from_str() { - assert_eq!( - ReflectionType::from("conversation_quality"), - ReflectionType::ConversationQuality - ); - assert_eq!( - ReflectionType::from("quality"), - ReflectionType::ConversationQuality - ); - assert_eq!( - ReflectionType::from("tool_usage"), - ReflectionType::ToolUsage - ); - assert_eq!( - ReflectionType::from("performance"), - ReflectionType::Performance - ); - } - - #[test] - fn test_reflection_config_default() { - let config = ReflectionConfig::default(); - assert!(!config.enabled); - assert_eq!(config.interval, 10); - assert!(!config.auto_apply); - } - - #[test] - fn test_reflection_result_new() { - let bot_id = Uuid::new_v4(); - let session_id = Uuid::new_v4(); - let result = ReflectionResult::new(bot_id, session_id, ReflectionType::ConversationQuality); - - assert_eq!(result.bot_id, bot_id); - assert_eq!(result.session_id, session_id); - assert_eq!(result.score, 0.0); - assert!(result.insights.is_empty()); - } - - #[test] - fn test_reflection_result_from_json() { - let json_response = r#"{ - "score": 7.5, - "key_insights": ["Users prefer concise responses", "Technical questions need more detail"], - "improvements": ["Add more examples", "Improve response time"], - "positive_patterns": ["Good greeting", "Clear explanations"] - }"#; - - let result = ReflectionResult::from_llm_response( - Uuid::new_v4(), - Uuid::new_v4(), - ReflectionType::ConversationQuality, - json_response, - 10, - ); - - assert_eq!(result.score, 7.5); - assert_eq!(result.insights.len(), 2); - assert_eq!(result.improvements.len(), 2); - assert_eq!(result.positive_patterns.len(), 2); - } - - #[test] - fn test_reflection_result_needs_improvement() { - let mut result = - ReflectionResult::new(Uuid::new_v4(), Uuid::new_v4(), ReflectionType::Performance); - - result.score = 5.0; - assert!(result.needs_improvement(6.0)); - - result.score = 8.0; - assert!(!result.needs_improvement(6.0)); - } - - #[test] - fn test_extract_insights_from_text() { - let text = "Here are some insights:\n\ - 1. Users prefer short responses\n\ - 2. Technical questions need examples\n\ - - Consider adding more context\n\ - • Improve response time"; - - let insights = extract_insights_from_text(text); - assert!(!insights.is_empty()); - } - - #[test] - fn test_reflection_type_prompt_template() { - let template = ReflectionType::ConversationQuality.prompt_template(); - assert!(template.contains("{conversation}")); - assert!(template.contains("JSON format")); - } - - #[test] - fn test_reflection_result_summary() { - let mut result = - ReflectionResult::new(Uuid::new_v4(), Uuid::new_v4(), ReflectionType::Performance); - result.score = 7.5; - result.messages_analyzed = 15; - result.insights = vec!["Insight 1".to_string(), "Insight 2".to_string()]; - result.improvements = vec!["Improvement 1".to_string()]; - - let summary = result.summary(); - assert!(summary.contains("7.5")); - assert!(summary.contains("15")); - assert!(summary.contains("2")); // insights count - assert!(summary.contains("1")); // improvements count - } -} diff --git a/src/basic/keywords/arrays/contains.rs b/src/basic/keywords/arrays/contains.rs index 1c4ff39ea..373096d2c 100644 --- a/src/basic/keywords/arrays/contains.rs +++ b/src/basic/keywords/arrays/contains.rs @@ -92,76 +92,3 @@ fn items_equal(a: &Dynamic, b: &Dynamic) -> bool { false } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_contains_string() { - let arr: Array = vec![ - Dynamic::from("Alice"), - Dynamic::from("Bob"), - Dynamic::from("Charlie"), - ]; - - assert!(array_contains(&arr, &Dynamic::from("Bob"))); - assert!(!array_contains(&arr, &Dynamic::from("David"))); - } - - #[test] - fn test_contains_integer() { - let arr: Array = vec![ - Dynamic::from(1_i64), - Dynamic::from(2_i64), - Dynamic::from(3_i64), - ]; - - assert!(array_contains(&arr, &Dynamic::from(2_i64))); - assert!(!array_contains(&arr, &Dynamic::from(5_i64))); - } - - #[test] - fn test_contains_float() { - let arr: Array = vec![ - Dynamic::from(1.5_f64), - Dynamic::from(2.5_f64), - Dynamic::from(3.5_f64), - ]; - - assert!(array_contains(&arr, &Dynamic::from(2.5_f64))); - assert!(!array_contains(&arr, &Dynamic::from(4.5_f64))); - } - - #[test] - fn test_contains_bool() { - let arr: Array = vec![Dynamic::from(true), Dynamic::from(false)]; - - assert!(array_contains(&arr, &Dynamic::from(true))); - assert!(array_contains(&arr, &Dynamic::from(false))); - } - - #[test] - fn test_contains_empty_array() { - let arr = Array::new(); - assert!(!array_contains(&arr, &Dynamic::from("anything"))); - } - - #[test] - fn test_items_equal_integers() { - assert!(items_equal(&Dynamic::from(5_i64), &Dynamic::from(5_i64))); - assert!(!items_equal(&Dynamic::from(5_i64), &Dynamic::from(6_i64))); - } - - #[test] - fn test_items_equal_strings() { - assert!(items_equal( - &Dynamic::from("hello"), - &Dynamic::from("hello") - )); - assert!(!items_equal( - &Dynamic::from("hello"), - &Dynamic::from("world") - )); - } -} diff --git a/src/basic/keywords/arrays/mod.rs b/src/basic/keywords/arrays/mod.rs index b0acfbff1..9dced4cb1 100644 --- a/src/basic/keywords/arrays/mod.rs +++ b/src/basic/keywords/arrays/mod.rs @@ -293,42 +293,3 @@ fn register_utility_functions(engine: &mut Engine) { debug!("Registered array utility functions"); } - -#[cfg(test)] -mod tests { - use rhai::Dynamic; - - #[test] - fn test_ubound() { - let arr: Vec = vec![Dynamic::from(1), Dynamic::from(2), Dynamic::from(3)]; - assert_eq!(arr.len() - 1, 2); - } - - #[test] - fn test_join() { - let arr = vec!["a", "b", "c"]; - let result = arr.join("-"); - assert_eq!(result, "a-b-c"); - } - - #[test] - fn test_split() { - let s = "a,b,c"; - let parts: Vec<&str> = s.split(',').collect(); - assert_eq!(parts.len(), 3); - } - - #[test] - fn test_range() { - let range: Vec = (1..=5).collect(); - assert_eq!(range, vec![1, 2, 3, 4, 5]); - } - - #[test] - fn test_flatten() { - // Test flattening logic - let nested = vec![vec![1, 2], vec![3, 4]]; - let flat: Vec = nested.into_iter().flatten().collect(); - assert_eq!(flat, vec![1, 2, 3, 4]); - } -} diff --git a/src/basic/keywords/arrays/push_pop.rs b/src/basic/keywords/arrays/push_pop.rs index 3bcb38d4b..79eed6112 100644 --- a/src/basic/keywords/arrays/push_pop.rs +++ b/src/basic/keywords/arrays/push_pop.rs @@ -100,48 +100,3 @@ pub fn unshift_keyword(_state: &Arc, _user: UserSession, engine: &mut debug!("Registered UNSHIFT keyword"); } - -#[cfg(test)] -mod tests { - use rhai::{Array, Dynamic}; - - #[test] - fn test_push() { - let mut arr: Array = vec![Dynamic::from(1), Dynamic::from(2)]; - arr.push(Dynamic::from(3)); - assert_eq!(arr.len(), 3); - assert_eq!(arr[2].as_int().unwrap_or(0), 3); - } - - #[test] - fn test_pop() { - let mut arr: Array = vec![Dynamic::from(1), Dynamic::from(2), Dynamic::from(3)]; - let popped = arr.pop(); - assert_eq!(arr.len(), 2); - assert_eq!(popped.and_then(|v| v.as_int().ok()).unwrap_or(0), 3); - } - - #[test] - fn test_pop_empty() { - let mut arr: Array = vec![]; - let popped = arr.pop(); - assert!(popped.is_none()); - } - - #[test] - fn test_shift() { - let mut arr: Array = vec![Dynamic::from(1), Dynamic::from(2), Dynamic::from(3)]; - let shifted = arr.remove(0); - assert_eq!(arr.len(), 2); - assert_eq!(shifted.as_int().unwrap_or(0), 1); - assert_eq!(arr[0].as_int().unwrap_or(0), 2); - } - - #[test] - fn test_unshift() { - let mut arr: Array = vec![Dynamic::from(2), Dynamic::from(3)]; - arr.insert(0, Dynamic::from(1)); - assert_eq!(arr.len(), 3); - assert_eq!(arr[0].as_int().unwrap_or(0), 1); - } -} diff --git a/src/basic/keywords/arrays/slice.rs b/src/basic/keywords/arrays/slice.rs index 693d00944..207d5d704 100644 --- a/src/basic/keywords/arrays/slice.rs +++ b/src/basic/keywords/arrays/slice.rs @@ -138,67 +138,3 @@ fn slice_array(arr: &Array, start: i64, end: Option) -> Array { arr[start_idx..end_idx].to_vec() } - -#[cfg(test)] -mod tests { - use super::*; - - fn make_test_array() -> Array { - vec![ - Dynamic::from(1), - Dynamic::from(2), - Dynamic::from(3), - Dynamic::from(4), - Dynamic::from(5), - ] - } - - #[test] - fn test_slice_from_start() { - let arr = make_test_array(); - let result = slice_array(&arr, 2, None); - assert_eq!(result.len(), 3); - assert_eq!(result[0].as_int().unwrap(), 3); - } - - #[test] - fn test_slice_with_end() { - let arr = make_test_array(); - let result = slice_array(&arr, 1, Some(3)); - assert_eq!(result.len(), 2); - assert_eq!(result[0].as_int().unwrap(), 2); - assert_eq!(result[1].as_int().unwrap(), 3); - } - - #[test] - fn test_slice_negative_start() { - let arr = make_test_array(); - let result = slice_array(&arr, -2, None); - assert_eq!(result.len(), 2); - assert_eq!(result[0].as_int().unwrap(), 4); - assert_eq!(result[1].as_int().unwrap(), 5); - } - - #[test] - fn test_slice_negative_end() { - let arr = make_test_array(); - let result = slice_array(&arr, 0, Some(-2)); - assert_eq!(result.len(), 3); - assert_eq!(result[0].as_int().unwrap(), 1); - assert_eq!(result[2].as_int().unwrap(), 3); - } - - #[test] - fn test_slice_out_of_bounds() { - let arr = make_test_array(); - let result = slice_array(&arr, 10, None); - assert!(result.is_empty()); - } - - #[test] - fn test_slice_empty_range() { - let arr = make_test_array(); - let result = slice_array(&arr, 3, Some(2)); - assert!(result.is_empty()); - } -} diff --git a/src/basic/keywords/arrays/sort.rs b/src/basic/keywords/arrays/sort.rs index 4e6dcca77..adf1f47c5 100644 --- a/src/basic/keywords/arrays/sort.rs +++ b/src/basic/keywords/arrays/sort.rs @@ -88,61 +88,3 @@ fn to_f64(value: &Dynamic) -> Option { None } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sort_integers() { - let arr: Array = vec![ - Dynamic::from(3), - Dynamic::from(1), - Dynamic::from(4), - Dynamic::from(1), - Dynamic::from(5), - ]; - let sorted = sort_array(arr, false); - assert_eq!(sorted[0].as_int().unwrap(), 1); - assert_eq!(sorted[1].as_int().unwrap(), 1); - assert_eq!(sorted[2].as_int().unwrap(), 3); - assert_eq!(sorted[3].as_int().unwrap(), 4); - assert_eq!(sorted[4].as_int().unwrap(), 5); - } - - #[test] - fn test_sort_strings() { - let arr: Array = vec![ - Dynamic::from("banana"), - Dynamic::from("apple"), - Dynamic::from("cherry"), - ]; - let sorted = sort_array(arr, false); - assert_eq!(sorted[0].clone().into_string().unwrap(), "apple"); - assert_eq!(sorted[1].clone().into_string().unwrap(), "banana"); - assert_eq!(sorted[2].clone().into_string().unwrap(), "cherry"); - } - - #[test] - fn test_sort_descending() { - let arr: Array = vec![Dynamic::from(1), Dynamic::from(3), Dynamic::from(2)]; - let sorted = sort_array(arr, true); - assert_eq!(sorted[0].as_int().unwrap(), 3); - assert_eq!(sorted[1].as_int().unwrap(), 2); - assert_eq!(sorted[2].as_int().unwrap(), 1); - } - - #[test] - fn test_compare_dynamic_numbers() { - let a = Dynamic::from(5); - let b = Dynamic::from(3); - assert_eq!(compare_dynamic(&a, &b), std::cmp::Ordering::Greater); - } - - #[test] - fn test_compare_dynamic_strings() { - let a = Dynamic::from("apple"); - let b = Dynamic::from("banana"); - assert_eq!(compare_dynamic(&a, &b), std::cmp::Ordering::Less); - } -} diff --git a/src/basic/keywords/arrays/unique.rs b/src/basic/keywords/arrays/unique.rs index 7143a8347..4ef75ddf8 100644 --- a/src/basic/keywords/arrays/unique.rs +++ b/src/basic/keywords/arrays/unique.rs @@ -52,92 +52,3 @@ fn unique_array(arr: Array) -> Array { result } - -#[cfg(test)] -mod tests { - use super::*; - use rhai::Dynamic; - - #[test] - fn test_unique_integers() { - let mut arr = Array::new(); - arr.push(Dynamic::from(1_i64)); - arr.push(Dynamic::from(2_i64)); - arr.push(Dynamic::from(2_i64)); - arr.push(Dynamic::from(3_i64)); - arr.push(Dynamic::from(3_i64)); - arr.push(Dynamic::from(3_i64)); - arr.push(Dynamic::from(4_i64)); - - let result = unique_array(arr); - assert_eq!(result.len(), 4); - } - - #[test] - fn test_unique_strings() { - let mut arr = Array::new(); - arr.push(Dynamic::from("Alice")); - arr.push(Dynamic::from("Bob")); - arr.push(Dynamic::from("Alice")); - arr.push(Dynamic::from("Charlie")); - - let result = unique_array(arr); - assert_eq!(result.len(), 3); - } - - #[test] - fn test_unique_preserves_order() { - let mut arr = Array::new(); - arr.push(Dynamic::from("C")); - arr.push(Dynamic::from("A")); - arr.push(Dynamic::from("B")); - arr.push(Dynamic::from("A")); - arr.push(Dynamic::from("C")); - - let result = unique_array(arr); - assert_eq!(result.len(), 3); - assert_eq!(result[0].to_string(), "C"); - assert_eq!(result[1].to_string(), "A"); - assert_eq!(result[2].to_string(), "B"); - } - - #[test] - fn test_unique_empty_array() { - let arr = Array::new(); - let result = unique_array(arr); - assert!(result.is_empty()); - } - - #[test] - fn test_unique_single_element() { - let mut arr = Array::new(); - arr.push(Dynamic::from(42_i64)); - - let result = unique_array(arr); - assert_eq!(result.len(), 1); - } - - #[test] - fn test_unique_all_same() { - let mut arr = Array::new(); - arr.push(Dynamic::from(1_i64)); - arr.push(Dynamic::from(1_i64)); - arr.push(Dynamic::from(1_i64)); - - let result = unique_array(arr); - assert_eq!(result.len(), 1); - } - - #[test] - fn test_unique_mixed_types() { - let mut arr = Array::new(); - arr.push(Dynamic::from(1_i64)); - arr.push(Dynamic::from("1")); - arr.push(Dynamic::from(1_i64)); - - let result = unique_array(arr); - // "1" (int) and "1" (string) may have same string representation - // so behavior depends on Dynamic::to_string() implementation - assert!(result.len() >= 1 && result.len() <= 2); - } -} diff --git a/src/basic/keywords/book.rs b/src/basic/keywords/book.rs index ff43b0857..d7fae623a 100644 --- a/src/basic/keywords/book.rs +++ b/src/basic/keywords/book.rs @@ -615,41 +615,3 @@ async fn send_meeting_invite( ); Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_time_string() { - let result = parse_time_string("2024-01-15 14:30"); - assert!(result.is_ok()); - - let result = parse_time_string("tomorrow at 3pm"); - assert!(result.is_ok()); - - let result = parse_time_string("in 2 hours"); - assert!(result.is_ok()); - } - - #[test] - fn test_parse_date_string() { - let result = parse_date_string("today"); - assert!(result.is_ok()); - - let result = parse_date_string("2024-01-15"); - assert!(result.is_ok()); - - let result = parse_date_string("tomorrow"); - assert!(result.is_ok()); - } - - #[test] - fn test_extract_hour() { - assert_eq!(extract_hour_from_string("3pm"), Some(15)); - assert_eq!(extract_hour_from_string("3 PM"), Some(15)); - assert_eq!(extract_hour_from_string("10am"), Some(10)); - assert_eq!(extract_hour_from_string("12am"), Some(0)); - assert_eq!(extract_hour_from_string("12pm"), Some(12)); - } -} diff --git a/src/basic/keywords/card.rs b/src/basic/keywords/card.rs index f4b758f3a..0c59a2a28 100644 --- a/src/basic/keywords/card.rs +++ b/src/basic/keywords/card.rs @@ -485,36 +485,3 @@ pub fn register_card_keyword(runtime: &mut BasicRuntime, llm_provider: Arc, user: UserSession, engine: debug!("All core BASIC functions registered successfully"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_module_structure() { - // This test verifies the module compiles correctly - // Actual function tests are in their respective submodules - assert!(true); - } -} diff --git a/src/basic/keywords/create_task.rs b/src/basic/keywords/create_task.rs index e52ed555b..af7fb9b1a 100644 --- a/src/basic/keywords/create_task.rs +++ b/src/basic/keywords/create_task.rs @@ -440,28 +440,3 @@ async fn send_task_notification( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_due_date() { - assert!(parse_due_date("tomorrow").is_ok()); - assert!(parse_due_date("+3 days").is_ok()); - assert!(parse_due_date("2024-12-31").is_ok()); - assert!(parse_due_date("null").unwrap().is_none()); - } - - #[test] - fn test_determine_priority() { - let tomorrow = Some(Utc::now() + Duration::days(1)); - assert_eq!(determine_priority(tomorrow), "high"); - - let next_week = Some(Utc::now() + Duration::days(7)); - assert_eq!(determine_priority(next_week), "medium"); - - let next_month = Some(Utc::now() + Duration::days(30)); - assert_eq!(determine_priority(next_month), "low"); - } -} diff --git a/src/basic/keywords/crm/attendance.rs b/src/basic/keywords/crm/attendance.rs index 666ee8769..0b61e83f8 100644 --- a/src/basic/keywords/crm/attendance.rs +++ b/src/basic/keywords/crm/attendance.rs @@ -155,7 +155,7 @@ fn register_get_queue(state: Arc, _user: UserSession, engine: &mut Eng .unwrap(); } -fn get_queue_impl(state: &Arc, filter: Option) -> Dynamic { +pub fn get_queue_impl(state: &Arc, filter: Option) -> Dynamic { let conn = state.conn.clone(); let result = std::thread::spawn(move || { @@ -314,7 +314,7 @@ fn register_next_in_queue(state: Arc, _user: UserSession, engine: &mut }); } -fn next_in_queue_impl(state: &Arc) -> Dynamic { +pub fn next_in_queue_impl(state: &Arc) -> Dynamic { let conn = state.conn.clone(); let result = std::thread::spawn(move || { @@ -428,7 +428,7 @@ fn register_assign_conversation(state: Arc, _user: UserSession, engine ); } -fn assign_conversation_impl( +pub fn assign_conversation_impl( state: &Arc, session_id: &str, attendant_id: &str, @@ -525,7 +525,7 @@ fn register_resolve_conversation(state: Arc, _user: UserSession, engin }); } -fn resolve_conversation_impl( +pub fn resolve_conversation_impl( state: &Arc, session_id: &str, reason: Option, @@ -614,7 +614,7 @@ fn register_set_priority(state: Arc, _user: UserSession, engine: &mut ); } -fn set_priority_impl(state: &Arc, session_id: &str, priority: Dynamic) -> Dynamic { +pub fn set_priority_impl(state: &Arc, session_id: &str, priority: Dynamic) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, @@ -711,7 +711,7 @@ fn register_get_attendants(state: Arc, _user: UserSession, engine: &mu }); } -fn get_attendants_impl(_state: &Arc, status_filter: Option) -> Dynamic { +pub fn get_attendants_impl(_state: &Arc, status_filter: Option) -> Dynamic { // Read from attendant.csv let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); @@ -841,7 +841,7 @@ fn register_get_attendant_stats(state: Arc, _user: UserSession, engine .unwrap(); } -fn get_attendant_stats_impl(state: &Arc, attendant_id: &str) -> Dynamic { +pub fn get_attendant_stats_impl(state: &Arc, attendant_id: &str) -> Dynamic { let conn = state.conn.clone(); let att_id = attendant_id.to_string(); @@ -935,11 +935,11 @@ fn register_get_tips(state: Arc, _user: UserSession, engine: &mut Engi ); } -fn get_tips_impl(_state: &Arc, _session_id: &str, message: &str) -> Dynamic { +pub fn get_tips_impl(_state: &Arc, _session_id: &str, message: &str) -> Dynamic { create_fallback_tips(message) } -fn create_fallback_tips(message: &str) -> Dynamic { +pub fn create_fallback_tips(message: &str) -> Dynamic { let msg_lower = message.to_lowercase(); let mut tips = Vec::new(); @@ -1043,7 +1043,7 @@ fn register_polish_message(state: Arc, _user: UserSession, engine: &mu }); } -fn polish_message_impl(_state: &Arc, message: &str, _tone: &str) -> Dynamic { +pub fn polish_message_impl(_state: &Arc, message: &str, _tone: &str) -> Dynamic { // Simple polishing without LLM (fallback) let mut polished = message.to_string(); @@ -1106,7 +1106,7 @@ fn register_get_smart_replies(state: Arc, _user: UserSession, engine: }); } -fn get_smart_replies_impl(_state: &Arc, _session_id: &str) -> Dynamic { +pub fn get_smart_replies_impl(_state: &Arc, _session_id: &str) -> Dynamic { // Return default replies (fallback without LLM) let mut replies = Vec::new(); @@ -1170,7 +1170,7 @@ fn register_get_summary(state: Arc, _user: UserSession, engine: &mut E }); } -fn get_summary_impl(state: &Arc, session_id: &str) -> Dynamic { +pub fn get_summary_impl(state: &Arc, session_id: &str) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, @@ -1242,7 +1242,7 @@ fn register_analyze_sentiment(state: Arc, _user: UserSession, engine: ); } -fn analyze_sentiment_impl(_state: &Arc, _session_id: &str, message: &str) -> Dynamic { +pub fn analyze_sentiment_impl(_state: &Arc, _session_id: &str, message: &str) -> Dynamic { // Keyword-based sentiment analysis (fallback without LLM) let msg_lower = message.to_lowercase(); @@ -1355,7 +1355,7 @@ fn register_tag_conversation(state: Arc, _user: UserSession, engine: & ); } -fn tag_conversation_impl(state: &Arc, session_id: &str, tags: Vec) -> Dynamic { +pub fn tag_conversation_impl(state: &Arc, session_id: &str, tags: Vec) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, @@ -1453,7 +1453,7 @@ fn register_add_note(state: Arc, _user: UserSession, engine: &mut Engi }); } -fn add_note_impl( +pub fn add_note_impl( state: &Arc, session_id: &str, note: &str, @@ -1547,7 +1547,7 @@ fn register_get_customer_history(state: Arc, _user: UserSession, engin }); } -fn get_customer_history_impl(state: &Arc, user_id: &str) -> Dynamic { +pub fn get_customer_history_impl(state: &Arc, user_id: &str) -> Dynamic { let conn = state.conn.clone(); let user_uuid = match Uuid::parse_str(user_id) { Ok(u) => u, @@ -1614,7 +1614,7 @@ fn get_customer_history_impl(state: &Arc, user_id: &str) -> Dynamic { // Helper Functions -fn create_error_result(message: &str) -> Dynamic { +pub fn create_error_result(message: &str) -> Dynamic { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(false)); result.insert("error".into(), Dynamic::from(message.to_string())); @@ -1622,137 +1622,3 @@ fn create_error_result(message: &str) -> Dynamic { } // Tests - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_fallback_tips_urgent() { - let tips = create_fallback_tips("This is URGENT! Help now!"); - let result = tips.try_cast::().unwrap(); - assert!(result.get("success").unwrap().as_bool().unwrap()); - } - - #[test] - fn test_fallback_tips_question() { - let tips = create_fallback_tips("Can you help me with this?"); - let result = tips.try_cast::().unwrap(); - assert!(result.get("success").unwrap().as_bool().unwrap()); - } - - #[test] - fn test_polish_message() { - let polished = polish_text("thx 4 ur msg", "professional"); - assert!(polished.contains("thx") == false); - assert!(polished.contains("your")); - } - - #[test] - fn test_polish_message_capitalization() { - let polished = polish_text("hello there", "professional"); - assert!(polished.starts_with('H')); - assert!(polished.ends_with('.')); - } - - fn polish_text(message: &str, _tone: &str) -> String { - let mut polished = message.to_string(); - polished = polished - .replace("thx", "Thank you") - .replace("u ", "you ") - .replace(" u", " you") - .replace("ur ", "your ") - .replace("ill ", "I'll ") - .replace("dont ", "don't ") - .replace("cant ", "can't ") - .replace("wont ", "won't ") - .replace("im ", "I'm ") - .replace("ive ", "I've "); - if let Some(first_char) = polished.chars().next() { - polished = first_char.to_uppercase().to_string() + &polished[1..]; - } - if !polished.ends_with('.') && !polished.ends_with('!') && !polished.ends_with('?') { - polished.push('.'); - } - polished - } - - #[test] - fn test_sentiment_positive() { - let result = analyze_text_sentiment("Thank you so much! This is great!"); - assert_eq!(result, "positive"); - } - - #[test] - fn test_sentiment_negative() { - let result = analyze_text_sentiment("This is terrible! I'm so frustrated!"); - assert_eq!(result, "negative"); - } - - #[test] - fn test_sentiment_neutral() { - let result = analyze_text_sentiment("The meeting is at 3pm."); - assert_eq!(result, "neutral"); - } - - fn analyze_text_sentiment(message: &str) -> &'static str { - let msg_lower = message.to_lowercase(); - let positive_words = [ - "thank", - "great", - "perfect", - "awesome", - "excellent", - "good", - "happy", - "love", - ]; - let negative_words = [ - "angry", - "frustrated", - "terrible", - "awful", - "horrible", - "hate", - "disappointed", - "problem", - "issue", - ]; - let positive_count = positive_words - .iter() - .filter(|w| msg_lower.contains(*w)) - .count(); - let negative_count = negative_words - .iter() - .filter(|w| msg_lower.contains(*w)) - .count(); - if positive_count > negative_count { - "positive" - } else if negative_count > positive_count { - "negative" - } else { - "neutral" - } - } - - #[test] - fn test_smart_replies_count() { - let replies = generate_smart_replies(); - assert_eq!(replies.len(), 3); - } - - #[test] - fn test_smart_replies_content() { - let replies = generate_smart_replies(); - assert!(replies.iter().any(|r| r.contains("Thank you"))); - assert!(replies.iter().any(|r| r.contains("understand"))); - } - - fn generate_smart_replies() -> Vec { - vec![ - "Thank you for reaching out! I'd be happy to help you with that.".to_string(), - "I understand your concern. Let me look into this for you right away.".to_string(), - "Is there anything else I can help you with today?".to_string(), - ] - } -} diff --git a/src/basic/keywords/crm/score_lead.rs b/src/basic/keywords/crm/score_lead.rs index 7daa58548..81e80f89d 100644 --- a/src/basic/keywords/crm/score_lead.rs +++ b/src/basic/keywords/crm/score_lead.rs @@ -674,57 +674,3 @@ fn update_lead_score_in_db(state: &Arc, lead_id: &str, score: i64) { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_calculate_lead_score_empty() { - let lead_data = Map::new(); - let score = calculate_lead_score(&lead_data, None); - assert_eq!(score, 0); - } - - #[test] - fn test_calculate_lead_score_basic() { - let mut lead_data = Map::new(); - lead_data.insert("job_title".into(), Dynamic::from("CEO")); - lead_data.insert("company_size".into(), Dynamic::from(500_i64)); - lead_data.insert("email".into(), Dynamic::from("ceo@company.com")); - - let score = calculate_lead_score(&lead_data, None); - assert!(score > 30); // At least CEO bonus - } - - #[test] - fn test_calculate_lead_score_with_title() { - let mut lead_data = Map::new(); - lead_data.insert("job_title".into(), Dynamic::from("CTO")); - - let score = calculate_lead_score(&lead_data, None); - assert!(score >= 30); - } - - #[test] - fn test_determine_priority() { - assert_eq!(determine_priority(95), "CRITICAL"); - assert_eq!(determine_priority(75), "HIGH"); - assert_eq!(determine_priority(55), "MEDIUM"); - assert_eq!(determine_priority(35), "LOW"); - assert_eq!(determine_priority(10), "MINIMAL"); - } - - #[test] - fn test_score_clamping() { - let mut lead_data = Map::new(); - lead_data.insert("budget".into(), Dynamic::from(1000000_i64)); - - let score = calculate_lead_score(&lead_data, None); - assert!( - score <= 100, - "Score should be clamped to 100, got {}", - score - ); - } -} diff --git a/src/basic/keywords/data_operations.rs b/src/basic/keywords/data_operations.rs index 5314c3f03..ee26c64a7 100644 --- a/src/basic/keywords/data_operations.rs +++ b/src/basic/keywords/data_operations.rs @@ -1078,45 +1078,3 @@ fn sanitize_identifier(name: &str) -> String { fn sanitize_sql(value: &str) -> String { value.replace('\'', "''") } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sanitize_identifier() { - assert_eq!(sanitize_identifier("users"), "users"); - assert_eq!(sanitize_identifier("user_name"), "user_name"); - assert_eq!( - sanitize_identifier("users; DROP TABLE users;"), - "usersDROPTABLEusers" - ); - } - - #[test] - fn test_sanitize_sql() { - assert_eq!(sanitize_sql("hello"), "hello"); - assert_eq!(sanitize_sql("it's"), "it''s"); - assert_eq!(sanitize_sql("O'Brien"), "O''Brien"); - } - - #[test] - fn test_parse_condition() { - let (field, op, value) = parse_condition_internal("status=active").unwrap(); - assert_eq!(field, "status"); - assert_eq!(op, "="); - assert_eq!(value, "active"); - - let (field, op, value) = parse_condition_internal("age>=18").unwrap(); - assert_eq!(field, "age"); - assert_eq!(op, ">="); - assert_eq!(value, "18"); - } - - #[test] - fn test_parse_filter_clause() { - let clause = parse_filter_clause("name=John").unwrap(); - assert!(clause.contains("name")); - assert!(clause.contains("John")); - } -} diff --git a/src/basic/keywords/datetime/dateadd.rs b/src/basic/keywords/datetime/dateadd.rs index 6fda85ac5..ac07d95c1 100644 --- a/src/basic/keywords/datetime/dateadd.rs +++ b/src/basic/keywords/datetime/dateadd.rs @@ -56,7 +56,7 @@ fn parse_datetime(datetime_str: &str) -> Option { .or_else(|| parse_date(trimmed).and_then(|d| d.and_hms_opt(0, 0, 0))) } -fn dateadd_impl(date_str: &str, amount: i64, unit: &str) -> String { +pub fn dateadd_impl(date_str: &str, amount: i64, unit: &str) -> String { let unit_lower = unit.to_lowercase(); if let Some(datetime) = parse_datetime(date_str) { @@ -104,7 +104,7 @@ fn dateadd_impl(date_str: &str, amount: i64, unit: &str) -> String { } } -fn datediff_impl(date1: &str, date2: &str, unit: &str) -> i64 { +pub fn datediff_impl(date1: &str, date2: &str, unit: &str) -> i64 { let unit_lower = unit.to_lowercase(); if let (Some(dt1), Some(dt2)) = (parse_datetime(date1), parse_datetime(date2)) { @@ -128,48 +128,3 @@ fn datediff_impl(date1: &str, date2: &str, unit: &str) -> i64 { 0 } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dateadd_days() { - assert_eq!(dateadd_impl("2025-01-15", 5, "day"), "2025-01-20"); - assert_eq!(dateadd_impl("2025-01-15", -10, "day"), "2025-01-05"); - } - - #[test] - fn test_dateadd_months() { - assert_eq!(dateadd_impl("2025-01-15", 1, "month"), "2025-02-15"); - assert_eq!(dateadd_impl("2025-01-15", -1, "month"), "2024-12-15"); - } - - #[test] - fn test_dateadd_years() { - assert_eq!(dateadd_impl("2025-01-15", 1, "year"), "2026-01-15"); - } - - #[test] - fn test_datediff_days() { - assert_eq!(datediff_impl("2025-01-01", "2025-01-15", "day"), 14); - assert_eq!(datediff_impl("2025-01-15", "2025-01-01", "day"), -14); - } - - #[test] - fn test_datediff_months() { - assert_eq!(datediff_impl("2025-01-01", "2025-03-01", "month"), 2); - } - - #[test] - fn test_datediff_years() { - assert_eq!(datediff_impl("2024-01-01", "2025-01-01", "year"), 1); - } - - #[test] - fn test_parse_date() { - assert!(parse_date("2025-01-15").is_some()); - assert!(parse_date("15/01/2025").is_some()); - assert!(parse_date("invalid").is_none()); - } -} diff --git a/src/basic/keywords/datetime/extract.rs b/src/basic/keywords/datetime/extract.rs index 7b912106b..84c072f0a 100644 --- a/src/basic/keywords/datetime/extract.rs +++ b/src/basic/keywords/datetime/extract.rs @@ -142,7 +142,7 @@ pub fn isdate_keyword(_state: &Arc, _user: UserSession, engine: &mut E debug!("Registered ISDATE keyword"); } -fn format_date_impl(date_str: &str, format: &str) -> String { +pub fn format_date_impl(date_str: &str, format: &str) -> String { if let Some(datetime) = parse_datetime(date_str) { let chrono_format = format .replace("YYYY", "%Y") @@ -166,34 +166,3 @@ fn format_date_impl(date_str: &str, format: &str) -> String { date_str.to_string() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_date() { - let date = parse_date("2025-01-22"); - assert!(date.is_some()); - let d = date.unwrap(); - assert_eq!(d.year(), 2025); - assert_eq!(d.month(), 1); - assert_eq!(d.day(), 22); - } - - #[test] - fn test_parse_datetime() { - let dt = parse_datetime("2025-01-22 14:30:45"); - assert!(dt.is_some()); - let d = dt.unwrap(); - assert_eq!(d.hour(), 14); - assert_eq!(d.minute(), 30); - assert_eq!(d.second(), 45); - } - - #[test] - fn test_invalid_date() { - let date = parse_date("invalid"); - assert!(date.is_none()); - } -} diff --git a/src/basic/keywords/datetime/now.rs b/src/basic/keywords/datetime/now.rs index 5e313f39e..db8325358 100644 --- a/src/basic/keywords/datetime/now.rs +++ b/src/basic/keywords/datetime/now.rs @@ -277,62 +277,3 @@ pub fn timestamp_keyword(_state: &Arc, _user: UserSession, engine: &mu debug!("Registered TIMESTAMP keyword"); } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_create_datetime_map() { - let now = Local::now(); - let map = create_datetime_map(now); - - assert!(map.contains_key("year")); - assert!(map.contains_key("month")); - assert!(map.contains_key("day")); - assert!(map.contains_key("hour")); - assert!(map.contains_key("minute")); - assert!(map.contains_key("second")); - assert!(map.contains_key("weekday")); - assert!(map.contains_key("timestamp")); - assert!(map.contains_key("formatted")); - assert!(map.contains_key("is_weekend")); - assert!(map.contains_key("quarter")); - } - - #[test] - fn test_year_extraction() { - let now = Local::now(); - let map = create_datetime_map(now); - - let year = map.get("year").unwrap().as_int().unwrap(); - assert!(year >= 2024); - } - - #[test] - fn test_month_range() { - let now = Local::now(); - let map = create_datetime_map(now); - - let month = map.get("month").unwrap().as_int().unwrap(); - assert!(month >= 1 && month <= 12); - } - - #[test] - fn test_hour12_range() { - let now = Local::now(); - let map = create_datetime_map(now); - - let hour12 = map.get("hour12").unwrap().as_int().unwrap(); - assert!(hour12 >= 1 && hour12 <= 12); - } - - #[test] - fn test_quarter_calculation() { - let now = Local::now(); - let map = create_datetime_map(now); - - let quarter = map.get("quarter").unwrap().as_int().unwrap(); - assert!(quarter >= 1 && quarter <= 4); - } -} diff --git a/src/basic/keywords/episodic_memory.rs b/src/basic/keywords/episodic_memory.rs index 51a3a74c1..3e27ba3cd 100644 --- a/src/basic/keywords/episodic_memory.rs +++ b/src/basic/keywords/episodic_memory.rs @@ -675,117 +675,3 @@ pub mod sql { ) "#; } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = EpisodicMemoryConfig::default(); - assert!(config.enabled); - assert_eq!(config.threshold, 4); - assert_eq!(config.history, 2); - assert_eq!(config.max_episodes, 100); - } - - #[test] - fn test_should_summarize() { - let manager = EpisodicMemoryManager::new(EpisodicMemoryConfig { - enabled: true, - threshold: 4, - history: 2, - auto_summarize: true, - ..Default::default() - }); - - assert!(!manager.should_summarize(2)); - assert!(manager.should_summarize(4)); - assert!(manager.should_summarize(10)); - } - - #[test] - fn test_extract_json() { - // Test with code block - let response = "Here's the summary:\n```json\n{\"summary\": \"test\"}\n```\n"; - assert!(extract_json(response).is_ok()); - - // Test with raw JSON - let response = "The result is {\"summary\": \"test\"}"; - assert!(extract_json(response).is_ok()); - } - - #[test] - fn test_generate_summary_prompt() { - let manager = EpisodicMemoryManager::new(EpisodicMemoryConfig::default()); - let messages = vec![ConversationMessage { - id: Uuid::new_v4(), - role: "user".to_string(), - content: "Hello".to_string(), - timestamp: Utc::now(), - }]; - - let prompt = manager.generate_summary_prompt(&messages); - assert!(prompt.contains("CONVERSATION:")); - assert!(prompt.contains("Hello")); - } - - #[test] - fn test_parse_summary_response() { - let manager = EpisodicMemoryManager::new(EpisodicMemoryConfig::default()); - let response = r#"{ - "summary": "User asked about billing", - "key_topics": ["billing", "payment"], - "decisions": [], - "action_items": [], - "sentiment": {"score": 0.5, "label": "positive", "confidence": 0.8}, - "resolution": "resolved" - }"#; - - let messages = vec![ConversationMessage { - id: Uuid::new_v4(), - role: "user".to_string(), - content: "What's my balance?".to_string(), - timestamp: Utc::now(), - }]; - - let episode = manager.parse_summary_response( - response, - &messages, - Uuid::new_v4(), - Uuid::new_v4(), - Uuid::new_v4(), - ); - - assert!(episode.is_ok()); - let ep = episode.unwrap(); - assert_eq!(ep.summary, "User asked about billing"); - assert_eq!(ep.key_topics, vec!["billing", "payment"]); - assert_eq!(ep.resolution, ResolutionStatus::Resolved); - } - - #[test] - fn test_episode_to_dynamic() { - let episode = Episode { - id: Uuid::new_v4(), - user_id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - session_id: Uuid::new_v4(), - summary: "Test summary".to_string(), - key_topics: vec!["topic1".to_string()], - decisions: vec![], - action_items: vec![], - sentiment: Sentiment::default(), - resolution: ResolutionStatus::Resolved, - message_count: 5, - message_ids: vec![], - created_at: Utc::now(), - conversation_start: Utc::now(), - conversation_end: Utc::now(), - metadata: serde_json::json!({}), - }; - - let dynamic = episode.to_dynamic(); - assert!(dynamic.is::()); - } -} diff --git a/src/basic/keywords/errors/mod.rs b/src/basic/keywords/errors/mod.rs index b4e6ad80e..7939765c5 100644 --- a/src/basic/keywords/errors/mod.rs +++ b/src/basic/keywords/errors/mod.rs @@ -187,18 +187,3 @@ pub fn log_error_keyword(_state: &Arc, _user: UserSession, engine: &mu debug!("Registered LOG_ERROR keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_error_map() { - use rhai::{Dynamic, Map}; - - let mut map = Map::new(); - map.insert("error".into(), Dynamic::from(true)); - map.insert("message".into(), Dynamic::from("test error")); - - assert!(map.contains_key("error")); - assert_eq!(map.get("error").unwrap().as_bool().unwrap_or(false), true); - } -} diff --git a/src/basic/keywords/errors/on_error.rs b/src/basic/keywords/errors/on_error.rs index 6a5e48144..faa8e9df8 100644 --- a/src/basic/keywords/errors/on_error.rs +++ b/src/basic/keywords/errors/on_error.rs @@ -241,67 +241,3 @@ pub fn handle_string_error(error_msg: &str) -> Result> = - Err("Test error".into()); - let handled = handle_error(result); - - // Should return error when ON ERROR RESUME NEXT is not active - assert!(handled.is_err()); - } - - #[test] - fn test_handle_error_with_resume_next() { - set_error_resume_next(true); - clear_last_error(); - - let result: Result> = - Err("Test error".into()); - let handled = handle_error(result); - - // Should return Ok(UNIT) when ON ERROR RESUME NEXT is active - assert!(handled.is_ok()); - assert_eq!(get_last_error(), Some("Test error".to_string())); - - // Cleanup - set_error_resume_next(false); - } -} diff --git a/src/basic/keywords/errors/throw.rs b/src/basic/keywords/errors/throw.rs index febeaa4d3..3da00bd5d 100644 --- a/src/basic/keywords/errors/throw.rs +++ b/src/basic/keywords/errors/throw.rs @@ -28,12 +28,3 @@ // - Stack trace capture // - Error context/metadata // - Retry mechanisms - -#[cfg(test)] -mod tests { - #[test] - fn test_placeholder() { - // Placeholder test - actual functionality is in mod.rs - assert!(true); - } -} diff --git a/src/basic/keywords/file_operations.rs b/src/basic/keywords/file_operations.rs index ce8427fa9..052b907ad 100644 --- a/src/basic/keywords/file_operations.rs +++ b/src/basic/keywords/file_operations.rs @@ -1772,23 +1772,3 @@ fn dynamic_to_file_data(value: &Dynamic) -> FileData { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dynamic_to_json() { - let dynamic = Dynamic::from("hello"); - let json = dynamic_to_json(&dynamic); - assert_eq!(json, Value::String("hello".to_string())); - } - - #[test] - fn test_dynamic_to_file_data() { - let dynamic = Dynamic::from("test content"); - let file_data = dynamic_to_file_data(&dynamic); - assert_eq!(file_data.filename, "file"); - assert!(!file_data.content.is_empty()); - } -} diff --git a/src/basic/keywords/hear_talk.rs b/src/basic/keywords/hear_talk.rs index fab239047..96d3923c3 100644 --- a/src/basic/keywords/hear_talk.rs +++ b/src/basic/keywords/hear_talk.rs @@ -1545,75 +1545,3 @@ async fn process_video_description( Err("Failed to process video".to_string()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_email() { - assert!(validate_email("test@example.com").is_valid); - assert!(validate_email("user.name+tag@domain.co.uk").is_valid); - assert!(!validate_email("invalid").is_valid); - assert!(!validate_email("@nodomain.com").is_valid); - } - - #[test] - fn test_validate_date() { - assert!(validate_date("25/12/2024").is_valid); - assert!(validate_date("2024-12-25").is_valid); - assert!(validate_date("today").is_valid); - assert!(validate_date("tomorrow").is_valid); - assert!(!validate_date("invalid").is_valid); - } - - #[test] - fn test_validate_cpf() { - assert!(validate_cpf("529.982.247-25").is_valid); - assert!(validate_cpf("52998224725").is_valid); - assert!(!validate_cpf("111.111.111-11").is_valid); - assert!(!validate_cpf("123").is_valid); - } - - #[test] - fn test_validate_money() { - let result = validate_money("R$ 1.234,56"); - assert!(result.is_valid); - assert_eq!(result.normalized_value, "1234.56"); - - let result = validate_money("$1,234.56"); - assert!(result.is_valid); - assert_eq!(result.normalized_value, "1234.56"); - } - - #[test] - fn test_validate_boolean() { - assert!(validate_boolean("yes").is_valid); - assert!(validate_boolean("sim").is_valid); - assert!(validate_boolean("no").is_valid); - assert!(validate_boolean("não").is_valid); - assert!(!validate_boolean("maybe").is_valid); - } - - #[test] - fn test_validate_menu() { - let options = vec![ - "Apple".to_string(), - "Banana".to_string(), - "Cherry".to_string(), - ]; - - assert!(validate_menu("Apple", &options).is_valid); - assert!(validate_menu("1", &options).is_valid); - assert!(validate_menu("ban", &options).is_valid); // Partial match - assert!(!validate_menu("Orange", &options).is_valid); - } - - #[test] - fn test_validate_credit_card() { - // Valid Visa test number - assert!(validate_credit_card("4111 1111 1111 1111").is_valid); - // Invalid (fails Luhn) - assert!(!validate_credit_card("1234567890123456").is_valid); - } -} diff --git a/src/basic/keywords/http_operations.rs b/src/basic/keywords/http_operations.rs index b485e3c9c..d9b99f8e6 100644 --- a/src/basic/keywords/http_operations.rs +++ b/src/basic/keywords/http_operations.rs @@ -816,38 +816,3 @@ fn json_to_dynamic(value: &Value) -> Dynamic { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dynamic_to_json_string() { - let dynamic = Dynamic::from("hello"); - let json = dynamic_to_json(&dynamic); - assert_eq!(json, Value::String("hello".to_string())); - } - - #[test] - fn test_dynamic_to_json_number() { - let dynamic = Dynamic::from(42_i64); - let json = dynamic_to_json(&dynamic); - assert_eq!(json, Value::Number(42.into())); - } - - #[test] - fn test_build_soap_envelope() { - let params = json!({"name": "John", "age": 30}); - let envelope = build_soap_envelope("GetUser", ¶ms); - assert!(envelope.contains("John")); - assert!(envelope.contains("30")); - } - - #[test] - fn test_parse_soap_response() { - let xml = r#"Success"#; - let result = parse_soap_response(xml); - assert!(result.get("raw").is_some()); - } -} diff --git a/src/basic/keywords/human_approval.rs b/src/basic/keywords/human_approval.rs index 1c16262da..9c59f1c8c 100644 --- a/src/basic/keywords/human_approval.rs +++ b/src/basic/keywords/human_approval.rs @@ -862,151 +862,3 @@ pub mod sql { WHERE bot_id = $1 AND name = $2 "#; } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = ApprovalConfig::default(); - assert!(config.enabled); - assert_eq!(config.default_timeout, 3600); - assert_eq!(config.max_reminders, 3); - } - - #[test] - fn test_create_request() { - let manager = ApprovalManager::new(ApprovalConfig::default()); - let request = manager.create_request( - Uuid::new_v4(), - Uuid::new_v4(), - Uuid::new_v4(), - "expense_approval", - ApprovalChannel::Email, - "manager@example.com", - serde_json::json!({"amount": 1000}), - "Please approve expense", - None, - None, - ); - - assert_eq!(request.status, ApprovalStatus::Pending); - assert_eq!(request.approval_type, "expense_approval"); - assert!(request.expires_at > Utc::now()); - } - - #[test] - fn test_is_expired() { - let manager = ApprovalManager::new(ApprovalConfig::default()); - let mut request = manager.create_request( - Uuid::new_v4(), - Uuid::new_v4(), - Uuid::new_v4(), - "test", - ApprovalChannel::Email, - "test@example.com", - serde_json::json!({}), - "Test", - Some(1), // 1 second timeout - None, - ); - - assert!(!manager.is_expired(&request)); - - // Manually set expired time - request.expires_at = Utc::now() - Duration::seconds(10); - assert!(manager.is_expired(&request)); - } - - #[test] - fn test_process_decision() { - let manager = ApprovalManager::new(ApprovalConfig::default()); - let mut request = manager.create_request( - Uuid::new_v4(), - Uuid::new_v4(), - Uuid::new_v4(), - "test", - ApprovalChannel::Email, - "test@example.com", - serde_json::json!({}), - "Test", - None, - None, - ); - - manager.process_decision( - &mut request, - ApprovalDecision::Approve, - "manager@example.com", - Some("Looks good!".to_string()), - ); - - assert_eq!(request.status, ApprovalStatus::Approved); - assert_eq!(request.decision, Some(ApprovalDecision::Approve)); - assert_eq!(request.decided_by, Some("manager@example.com".to_string())); - assert_eq!(request.comments, Some("Looks good!".to_string())); - } - - #[test] - fn test_evaluate_condition() { - let manager = ApprovalManager::new(ApprovalConfig::default()); - let context = serde_json::json!({ - "amount": 15000, - "priority": 2 - }); - - assert!(manager - .evaluate_condition("amount > 10000", &context) - .unwrap()); - assert!(!manager - .evaluate_condition("amount > 20000", &context) - .unwrap()); - assert!(manager - .evaluate_condition("priority == 2", &context) - .unwrap()); - } - - #[test] - fn test_handle_timeout_with_default() { - let manager = ApprovalManager::new(ApprovalConfig::default()); - let mut request = manager.create_request( - Uuid::new_v4(), - Uuid::new_v4(), - Uuid::new_v4(), - "test", - ApprovalChannel::Email, - "test@example.com", - serde_json::json!({}), - "Test", - None, - Some(ApprovalDecision::Approve), - ); - - manager.handle_timeout(&mut request); - - assert_eq!(request.status, ApprovalStatus::Approved); - assert_eq!(request.decision, Some(ApprovalDecision::Approve)); - assert_eq!(request.decided_by, Some("system:timeout".to_string())); - } - - #[test] - fn test_request_to_dynamic() { - let manager = ApprovalManager::new(ApprovalConfig::default()); - let request = manager.create_request( - Uuid::new_v4(), - Uuid::new_v4(), - Uuid::new_v4(), - "test", - ApprovalChannel::Email, - "test@example.com", - serde_json::json!({"key": "value"}), - "Test message", - None, - None, - ); - - let dynamic = request.to_dynamic(); - assert!(dynamic.is::()); - } -} diff --git a/src/basic/keywords/import_export.rs b/src/basic/keywords/import_export.rs index d8997180e..2cc8eb623 100644 --- a/src/basic/keywords/import_export.rs +++ b/src/basic/keywords/import_export.rs @@ -682,43 +682,3 @@ fn dynamic_to_json(data: &Dynamic) -> Value { Value::String(data.to_string()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_csv_line_simple() { - let line = "a,b,c"; - let result = parse_csv_line(line); - assert_eq!(result, vec!["a", "b", "c"]); - } - - #[test] - fn test_parse_csv_line_quoted() { - let line = r#""hello, world",test,"another, value""#; - let result = parse_csv_line(line); - assert_eq!(result, vec!["hello, world", "test", "another, value"]); - } - - #[test] - fn test_escape_csv_value() { - assert_eq!(escape_csv_value("simple"), "simple"); - assert_eq!(escape_csv_value("with,comma"), "\"with,comma\""); - assert_eq!(escape_csv_value("with\"quote"), "\"with\"\"quote\""); - } - - #[test] - fn test_json_to_dynamic_and_back() { - let json = serde_json::json!({ - "name": "test", - "value": 42, - "active": true - }); - - let dynamic = json_to_dynamic(&json); - let back = dynamic_to_json(&dynamic); - - assert_eq!(json, back); - } -} diff --git a/src/basic/keywords/kb_statistics.rs b/src/basic/keywords/kb_statistics.rs index 0238c1379..a0eb4f503 100644 --- a/src/basic/keywords/kb_statistics.rs +++ b/src/basic/keywords/kb_statistics.rs @@ -425,44 +425,3 @@ async fn get_storage_size( let stats = get_kb_statistics(state, user).await?; Ok(stats.total_disk_size_mb) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_collection_stats_serialization() { - let stats = CollectionStats { - name: "test_collection".to_string(), - vectors_count: 1000, - points_count: 1000, - segments_count: 2, - disk_data_size: 1024 * 1024, - ram_data_size: 512 * 1024, - indexed_vectors_count: 1000, - status: "green".to_string(), - }; - - let json = serde_json::to_string(&stats).unwrap(); - assert!(json.contains("test_collection")); - assert!(json.contains("1000")); - } - - #[test] - fn test_kb_statistics_serialization() { - let stats = KBStatistics { - total_collections: 3, - total_documents: 5000, - total_vectors: 5000, - total_disk_size_mb: 10.5, - total_ram_size_mb: 5.2, - documents_added_last_week: 100, - documents_added_last_month: 500, - collections: vec![], - }; - - let json = serde_json::to_string(&stats).unwrap(); - assert!(json.contains("5000")); - assert!(json.contains("10.5")); - } -} diff --git a/src/basic/keywords/knowledge_graph.rs b/src/basic/keywords/knowledge_graph.rs index 630adf9f1..1b0adf242 100644 --- a/src/basic/keywords/knowledge_graph.rs +++ b/src/basic/keywords/knowledge_graph.rs @@ -835,96 +835,3 @@ pub mod entity_types { pub const SKILL: &str = "skill"; pub const TECHNOLOGY: &str = "technology"; } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = KnowledgeGraphConfig::default(); - assert!(config.enabled); - assert_eq!(config.backend, "postgresql"); - assert!(config.entity_types.contains(&"person".to_string())); - } - - #[test] - fn test_extraction_prompt() { - let manager = KnowledgeGraphManager::new(KnowledgeGraphConfig::default()); - let prompt = manager.generate_extraction_prompt("John works at Acme Corp."); - assert!(prompt.contains("John works at Acme Corp.")); - assert!(prompt.contains("ENTITY TYPES TO EXTRACT")); - } - - #[test] - fn test_parse_extraction_response() { - let manager = KnowledgeGraphManager::new(KnowledgeGraphConfig::default()); - let response = r#"{ - "entities": [ - { - "name": "John", - "canonical_name": "John Smith", - "entity_type": "person", - "confidence": 0.9, - "properties": {} - } - ], - "relationships": [ - { - "from_entity": "John", - "to_entity": "Acme Corp", - "relationship_type": "works_on", - "confidence": 0.85, - "evidence": "John works at Acme Corp" - } - ] - }"#; - - let result = manager.parse_extraction_response(response, 100, 50); - assert!(result.is_ok()); - let extraction = result.unwrap(); - assert_eq!(extraction.entities.len(), 1); - assert_eq!(extraction.relationships.len(), 1); - } - - #[test] - fn test_entity_to_dynamic() { - let entity = KgEntity { - id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - entity_type: "person".to_string(), - entity_name: "John Smith".to_string(), - aliases: vec!["John".to_string()], - properties: serde_json::json!({"department": "Sales"}), - confidence: 0.95, - source: EntitySource::Manual, - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - let dynamic = entity.to_dynamic(); - assert!(dynamic.is::()); - } - - #[test] - fn test_is_valid_entity_type() { - let manager = KnowledgeGraphManager::new(KnowledgeGraphConfig::default()); - assert!(manager.is_valid_entity_type("person")); - assert!(manager.is_valid_entity_type("PERSON")); - assert!(manager.is_valid_entity_type("organization")); - assert!(!manager.is_valid_entity_type("unknown_type")); - } - - #[test] - fn test_json_to_dynamic() { - let json = serde_json::json!({ - "name": "test", - "count": 42, - "active": true, - "tags": ["a", "b"] - }); - - let dynamic = json_to_dynamic(&json); - assert!(dynamic.is::()); - } -} diff --git a/src/basic/keywords/lead_scoring.rs b/src/basic/keywords/lead_scoring.rs index 89f00547d..fac97ad57 100644 --- a/src/basic/keywords/lead_scoring.rs +++ b/src/basic/keywords/lead_scoring.rs @@ -87,13 +87,3 @@ pub fn register_lead_scoring_keywords( debug!("Lead scoring keywords registered successfully"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_module_structure() { - // This test verifies the module compiles correctly - // Actual function tests are in the crm/score_lead.rs module - assert!(true); - } -} diff --git a/src/basic/keywords/llm_macros.rs b/src/basic/keywords/llm_macros.rs index ae3ed24c2..aea03baac 100644 --- a/src/basic/keywords/llm_macros.rs +++ b/src/basic/keywords/llm_macros.rs @@ -442,33 +442,3 @@ fn dynamic_to_json(data: &Dynamic) -> serde_json::Value { serde_json::Value::String(data.to_string()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_calculate_result_integer() { - let result = parse_calculate_result("42").unwrap(); - assert_eq!(result.as_int().unwrap(), 42); - } - - #[test] - fn test_parse_calculate_result_float() { - let result = parse_calculate_result("3.14").unwrap(); - assert!((result.as_float().unwrap() - 3.14).abs() < 0.001); - } - - #[test] - fn test_parse_calculate_result_boolean() { - let result = parse_calculate_result("true").unwrap(); - assert!(result.as_bool().unwrap()); - } - - #[test] - fn test_build_translate_prompt() { - let prompt = build_translate_prompt("Hello", "Spanish"); - assert!(prompt.contains("Hello")); - assert!(prompt.contains("Spanish")); - } -} diff --git a/src/basic/keywords/math/abs.rs b/src/basic/keywords/math/abs.rs index a882c1615..e9f852b12 100644 --- a/src/basic/keywords/math/abs.rs +++ b/src/basic/keywords/math/abs.rs @@ -12,24 +12,3 @@ pub fn abs_keyword(_state: &Arc, _user: UserSession, engine: &mut Engi debug!("Registered ABS keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_abs_positive() { - assert_eq!(42_i64.abs(), 42); - assert_eq!(3.14_f64.abs(), 3.14); - } - - #[test] - fn test_abs_negative() { - assert_eq!((-42_i64).abs(), 42); - assert_eq!((-3.14_f64).abs(), 3.14); - } - - #[test] - fn test_abs_zero() { - assert_eq!(0_i64.abs(), 0); - assert_eq!(0.0_f64.abs(), 0.0); - } -} diff --git a/src/basic/keywords/math/aggregate.rs b/src/basic/keywords/math/aggregate.rs index af32a0d54..5808cbb7a 100644 --- a/src/basic/keywords/math/aggregate.rs +++ b/src/basic/keywords/math/aggregate.rs @@ -94,37 +94,3 @@ pub fn avg_keyword(_state: &Arc, _user: UserSession, engine: &mut Engi debug!("Registered AVG/AVERAGE keyword"); } - -#[cfg(test)] -mod tests { - use rhai::Dynamic; - - #[test] - fn test_sum() { - let arr: Vec = vec![ - Dynamic::from(10_i64), - Dynamic::from(20_i64), - Dynamic::from(30_i64), - ]; - let sum: f64 = arr - .iter() - .filter_map(|v| v.as_int().ok().map(|i| i as f64)) - .sum(); - assert_eq!(sum, 60.0); - } - - #[test] - fn test_avg() { - let arr: Vec = vec![10.0, 20.0, 30.0]; - let sum: f64 = arr.iter().sum(); - let avg = sum / arr.len() as f64; - assert_eq!(avg, 20.0); - } - - #[test] - fn test_empty_array() { - let arr: Vec = vec![]; - let result = if arr.is_empty() { 0.0 } else { arr.iter().sum::() / arr.len() as f64 }; - assert_eq!(result, 0.0); - } -} diff --git a/src/basic/keywords/math/basic_math.rs b/src/basic/keywords/math/basic_math.rs index 943ddd809..4a749e526 100644 --- a/src/basic/keywords/math/basic_math.rs +++ b/src/basic/keywords/math/basic_math.rs @@ -105,46 +105,3 @@ pub fn pow_keyword(_state: &Arc, _user: UserSession, engine: &mut Engi debug!("Registered POW keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_int() { - assert_eq!(3.9_f64.trunc() as i64, 3); - assert_eq!((-3.9_f64).trunc() as i64, -3); - } - - #[test] - fn test_floor_ceil() { - assert_eq!(3.7_f64.floor() as i64, 3); - assert_eq!(3.2_f64.ceil() as i64, 4); - } - - #[test] - fn test_minmax() { - assert_eq!(10_i64.max(5), 10); - assert_eq!(10_i64.min(5), 5); - } - - #[test] - fn test_mod() { - assert_eq!(17 % 5, 2); - } - - #[test] - fn test_sgn() { - assert_eq!((-5_i64).signum(), -1); - assert_eq!(5_i64.signum(), 1); - assert_eq!(0_i64.signum(), 0); - } - - #[test] - fn test_sqrt() { - assert!((16_f64.sqrt() - 4.0).abs() < 0.0001); - } - - #[test] - fn test_pow() { - assert!((2_f64.powf(8.0) - 256.0).abs() < 0.0001); - } -} diff --git a/src/basic/keywords/math/minmax.rs b/src/basic/keywords/math/minmax.rs index a201486db..c2498a7b0 100644 --- a/src/basic/keywords/math/minmax.rs +++ b/src/basic/keywords/math/minmax.rs @@ -57,18 +57,3 @@ pub fn min_keyword(_state: &Arc, _user: UserSession, engine: &mut Engi debug!("Registered MIN keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_max_values() { - assert_eq!(10_i64.max(5), 10); - assert_eq!(3.5_f64.max(7.2), 7.2); - } - - #[test] - fn test_min_values() { - assert_eq!(10_i64.min(5), 5); - assert_eq!(3.5_f64.min(7.2), 3.5); - } -} diff --git a/src/basic/keywords/math/random.rs b/src/basic/keywords/math/random.rs index d80c57af9..b7bb080c0 100644 --- a/src/basic/keywords/math/random.rs +++ b/src/basic/keywords/math/random.rs @@ -83,13 +83,3 @@ pub fn mod_keyword(_state: &Arc, _user: UserSession, engine: &mut Engi debug!("Registered MOD keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_mod() { - assert_eq!(17 % 5, 2); - assert_eq!(10 % 3, 1); - assert_eq!(0 % 5, 0); - } -} diff --git a/src/basic/keywords/math/round.rs b/src/basic/keywords/math/round.rs index c8cddeb7b..17ff2655b 100644 --- a/src/basic/keywords/math/round.rs +++ b/src/basic/keywords/math/round.rs @@ -18,22 +18,3 @@ pub fn round_keyword(_state: &Arc, _user: UserSession, engine: &mut En debug!("Registered ROUND keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_round_basic() { - assert_eq!(3.7_f64.round() as i64, 4); - assert_eq!(3.2_f64.round() as i64, 3); - assert_eq!((-3.7_f64).round() as i64, -4); - } - - #[test] - fn test_round_decimals() { - let n = 2.71828_f64; - let decimals = 2; - let factor = 10_f64.powi(decimals); - let result = (n * factor).round() / factor; - assert!((result - 2.72).abs() < 0.001); - } -} diff --git a/src/basic/keywords/math/trig.rs b/src/basic/keywords/math/trig.rs index a23a43259..3703070d9 100644 --- a/src/basic/keywords/math/trig.rs +++ b/src/basic/keywords/math/trig.rs @@ -54,32 +54,3 @@ pub fn pi_keyword(_state: &Arc, _user: UserSession, engine: &mut Engin debug!("Registered PI keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_sin() { - assert!((0.0_f64.sin() - 0.0).abs() < 0.0001); - } - - #[test] - fn test_cos() { - assert!((0.0_f64.cos() - 1.0).abs() < 0.0001); - } - - #[test] - fn test_log() { - assert!((100.0_f64.log10() - 2.0).abs() < 0.0001); - } - - #[test] - fn test_exp() { - assert!((0.0_f64.exp() - 1.0).abs() < 0.0001); - } - - #[test] - fn test_pi() { - assert!(std::f64::consts::PI > 3.14); - assert!(std::f64::consts::PI < 3.15); - } -} diff --git a/src/basic/keywords/mcp_directory.rs b/src/basic/keywords/mcp_directory.rs index 4922475a4..2f38fbc17 100644 --- a/src/basic/keywords/mcp_directory.rs +++ b/src/basic/keywords/mcp_directory.rs @@ -928,51 +928,3 @@ pub fn generate_example_configs() -> Vec { pub type McpDirectoryScanner = McpCsvLoader; pub type McpDirectoryScanResult = McpLoadResult; pub type McpScanError = McpLoadError; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_csv_columns() { - let loader = McpCsvLoader::new("./work", "test"); - - let cols = loader.parse_csv_columns("name,type,command"); - assert_eq!(cols, vec!["name", "type", "command"]); - - let cols = loader.parse_csv_columns( - "filesystem,stdio,npx,\"-y @modelcontextprotocol/server-filesystem\"", - ); - assert_eq!(cols.len(), 4); - assert_eq!(cols[3], "-y @modelcontextprotocol/server-filesystem"); - } - - #[test] - fn test_parse_args() { - let loader = McpCsvLoader::new("./work", "test"); - - let args = loader.parse_args("-y @modelcontextprotocol/server-filesystem /data"); - assert_eq!( - args, - vec!["-y", "@modelcontextprotocol/server-filesystem", "/data"] - ); - } - - #[test] - fn test_infer_server_type() { - let loader = McpCsvLoader::new("./work", "test"); - - assert!(matches!( - loader.infer_server_type("filesystem", "stdio", "npx"), - McpServerType::Filesystem - )); - assert!(matches!( - loader.infer_server_type("postgres", "stdio", "npx"), - McpServerType::Database - )); - assert!(matches!( - loader.infer_server_type("myapi", "http", "https://api.example.com"), - McpServerType::Web - )); - } -} diff --git a/src/basic/keywords/messaging/send_template.rs b/src/basic/keywords/messaging/send_template.rs index 75adc3d00..b889a0786 100644 --- a/src/basic/keywords/messaging/send_template.rs +++ b/src/basic/keywords/messaging/send_template.rs @@ -511,69 +511,3 @@ fn generate_message_id() -> String { format!("msg_{}", timestamp) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_send_template_valid_email() { - let result = send_template_message("welcome", "user@example.com", "email", None); - assert!(result.get("success").unwrap().as_bool().unwrap()); - } - - #[test] - fn test_send_template_invalid_email() { - let result = send_template_message("welcome", "invalid-email", "email", None); - assert!(!result.get("success").unwrap().as_bool().unwrap()); - } - - #[test] - fn test_send_template_invalid_channel() { - let result = send_template_message("welcome", "user@example.com", "invalid", None); - assert!(!result.get("success").unwrap().as_bool().unwrap()); - } - - #[test] - fn test_send_template_batch() { - let mut recipients = Array::new(); - recipients.push(Dynamic::from("user1@example.com")); - recipients.push(Dynamic::from("user2@example.com")); - - let result = send_template_batch("welcome", &recipients, "email", None); - assert_eq!(result.get("total").unwrap().as_int().unwrap(), 2); - assert_eq!(result.get("sent").unwrap().as_int().unwrap(), 2); - } - - #[test] - fn test_create_template() { - let result = create_message_template("test", "email", Some("Subject"), "Hello {{name}}!"); - assert!(result.get("success").unwrap().as_bool().unwrap()); - } - - #[test] - fn test_create_template_empty_name() { - let result = create_message_template("", "email", None, "Content"); - assert!(!result.get("success").unwrap().as_bool().unwrap()); - } - - #[test] - fn test_extract_template_variables() { - let content = "Hello {{name}}, your order {{order_id}} is ready!"; - let vars = extract_template_variables(content); - assert_eq!(vars.len(), 2); - } - - #[test] - fn test_extract_template_variables_empty() { - let content = "Hello, no variables here!"; - let vars = extract_template_variables(content); - assert!(vars.is_empty()); - } - - #[test] - fn test_generate_message_id() { - let id = generate_message_id(); - assert!(id.starts_with("msg_")); - } -} diff --git a/src/basic/keywords/model_routing.rs b/src/basic/keywords/model_routing.rs index 42abfd1d1..57373b234 100644 --- a/src/basic/keywords/model_routing.rs +++ b/src/basic/keywords/model_routing.rs @@ -552,83 +552,3 @@ pub fn get_session_routing_strategy(state: &AppState, session_id: Uuid) -> Routi } // Tests - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_model_router_new() { - let router = ModelRouter::new(); - assert_eq!(router.default_model, "default"); - assert!(router.models.is_empty()); - assert_eq!(router.routing_strategy, RoutingStrategy::Manual); - } - - #[test] - fn test_auto_routing_code() { - let mut router = ModelRouter::new(); - router.models.insert( - "code".to_string(), - ModelConfig { - name: "code".to_string(), - url: "http://localhost:8081".to_string(), - model_path: "codellama.gguf".to_string(), - api_key: None, - max_tokens: None, - temperature: None, - }, - ); - router.routing_strategy = RoutingStrategy::Auto; - - let result = router.route_query("Help me debug this code"); - assert_eq!(result, "code"); - } - - #[test] - fn test_auto_routing_quality() { - let mut router = ModelRouter::new(); - router.models.insert( - "quality".to_string(), - ModelConfig { - name: "quality".to_string(), - url: "http://localhost:8081".to_string(), - model_path: "large-model.gguf".to_string(), - api_key: None, - max_tokens: None, - temperature: None, - }, - ); - router.routing_strategy = RoutingStrategy::Auto; - - let result = - router.route_query("Please analyze and compare these two approaches in detail"); - assert_eq!(result, "quality"); - } - - #[test] - fn test_auto_routing_fast() { - let mut router = ModelRouter::new(); - router.models.insert( - "fast".to_string(), - ModelConfig { - name: "fast".to_string(), - url: "http://localhost:8081".to_string(), - model_path: "small-model.gguf".to_string(), - api_key: None, - max_tokens: None, - temperature: None, - }, - ); - router.routing_strategy = RoutingStrategy::Auto; - - let result = router.route_query("What is AI?"); - assert_eq!(result, "fast"); - } - - #[test] - fn test_routing_strategy_default() { - let strategy = RoutingStrategy::default(); - assert_eq!(strategy, RoutingStrategy::Manual); - } -} diff --git a/src/basic/keywords/on_change.rs b/src/basic/keywords/on_change.rs index d79d243b6..82c2d7a2c 100644 --- a/src/basic/keywords/on_change.rs +++ b/src/basic/keywords/on_change.rs @@ -498,212 +498,3 @@ pub async fn process_folder_event( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_folder_path_account() { - let (provider, email, path) = - parse_folder_path("account://user@gmail.com/Documents/invoices"); - assert_eq!(provider, FolderProvider::GDrive); - assert_eq!(email, Some("user@gmail.com".to_string())); - assert_eq!(path, "/Documents/invoices"); - } - - #[test] - fn test_parse_folder_path_gdrive() { - let (provider, email, path) = parse_folder_path("gdrive:///shared/reports"); - assert_eq!(provider, FolderProvider::GDrive); - assert_eq!(email, None); - assert_eq!(path, "/shared/reports"); - } - - #[test] - fn test_parse_folder_path_onedrive() { - let (provider, email, path) = parse_folder_path("onedrive:///business/docs"); - assert_eq!(provider, FolderProvider::OneDrive); - assert_eq!(email, None); - assert_eq!(path, "/business/docs"); - } - - #[test] - fn test_parse_folder_path_dropbox() { - let (provider, email, path) = parse_folder_path("dropbox:///team/assets"); - assert_eq!(provider, FolderProvider::Dropbox); - assert_eq!(email, None); - assert_eq!(path, "/team/assets"); - } - - #[test] - fn test_parse_folder_path_local() { - let (provider, email, path) = parse_folder_path("/home/user/documents"); - assert_eq!(provider, FolderProvider::Local); - assert_eq!(email, None); - assert_eq!(path, "/home/user/documents"); - } - - #[test] - fn test_is_cloud_path() { - assert!(is_cloud_path("account://user@gmail.com/docs")); - assert!(is_cloud_path("gdrive:///shared")); - assert!(is_cloud_path("onedrive:///files")); - assert!(is_cloud_path("dropbox:///folder")); - assert!(!is_cloud_path("/local/path")); - assert!(!is_cloud_path("./relative/path")); - } - - #[test] - fn test_folder_provider_from_str() { - assert_eq!( - FolderProvider::from_str("gdrive"), - Some(FolderProvider::GDrive) - ); - assert_eq!( - FolderProvider::from_str("GDRIVE"), - Some(FolderProvider::GDrive) - ); - assert_eq!( - FolderProvider::from_str("googledrive"), - Some(FolderProvider::GDrive) - ); - assert_eq!( - FolderProvider::from_str("onedrive"), - Some(FolderProvider::OneDrive) - ); - assert_eq!( - FolderProvider::from_str("microsoft"), - Some(FolderProvider::OneDrive) - ); - assert_eq!( - FolderProvider::from_str("dropbox"), - Some(FolderProvider::Dropbox) - ); - assert_eq!( - FolderProvider::from_str("dbx"), - Some(FolderProvider::Dropbox) - ); - assert_eq!( - FolderProvider::from_str("local"), - Some(FolderProvider::Local) - ); - assert_eq!( - FolderProvider::from_str("filesystem"), - Some(FolderProvider::Local) - ); - assert_eq!(FolderProvider::from_str("unknown"), None); - } - - #[test] - fn test_change_event_type_from_str() { - assert_eq!( - ChangeEventType::from_str("create"), - Some(ChangeEventType::Create) - ); - assert_eq!( - ChangeEventType::from_str("created"), - Some(ChangeEventType::Create) - ); - assert_eq!( - ChangeEventType::from_str("modify"), - Some(ChangeEventType::Modify) - ); - assert_eq!( - ChangeEventType::from_str("changed"), - Some(ChangeEventType::Modify) - ); - assert_eq!( - ChangeEventType::from_str("delete"), - Some(ChangeEventType::Delete) - ); - assert_eq!( - ChangeEventType::from_str("removed"), - Some(ChangeEventType::Delete) - ); - assert_eq!( - ChangeEventType::from_str("rename"), - Some(ChangeEventType::Rename) - ); - assert_eq!( - ChangeEventType::from_str("move"), - Some(ChangeEventType::Move) - ); - assert_eq!(ChangeEventType::from_str("invalid"), None); - } - - #[test] - fn test_sanitize_path() { - assert_eq!( - sanitize_path_for_filename("/home/user/docs"), - "_home_user_docs" - ); - assert_eq!( - sanitize_path_for_filename("C:\\Users\\docs"), - "c__users_docs" - ); - assert_eq!( - sanitize_path_for_filename("path with spaces"), - "path_with_spaces" - ); - } - - #[test] - fn test_folder_monitor_struct() { - let monitor = FolderMonitor { - id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - provider: "gdrive".to_string(), - account_email: Some("user@gmail.com".to_string()), - folder_path: "/my/folder".to_string(), - folder_id: Some("folder123".to_string()), - script_path: "on_change.rhai".to_string(), - is_active: true, - watch_subfolders: true, - event_types: vec!["create".to_string(), "modify".to_string()], - }; - - assert_eq!(monitor.provider, "gdrive"); - assert!(monitor.is_active); - assert!(monitor.watch_subfolders); - assert_eq!(monitor.account_email, Some("user@gmail.com".to_string())); - } - - #[test] - fn test_folder_change_event_struct() { - let event = FolderChangeEvent { - id: Uuid::new_v4(), - monitor_id: Uuid::new_v4(), - event_type: "create".to_string(), - file_path: "/docs/new_file.pdf".to_string(), - file_id: Some("file123".to_string()), - file_name: Some("new_file.pdf".to_string()), - file_size: Some(1024), - mime_type: Some("application/pdf".to_string()), - old_path: None, - }; - - assert_eq!(event.event_type, "create"); - assert_eq!(event.file_size, Some(1024)); - } - - #[test] - fn test_detect_provider_from_email() { - assert_eq!( - detect_provider_from_email("user@gmail.com"), - FolderProvider::GDrive - ); - assert_eq!( - detect_provider_from_email("user@outlook.com"), - FolderProvider::OneDrive - ); - assert_eq!( - detect_provider_from_email("user@hotmail.com"), - FolderProvider::OneDrive - ); - assert_eq!( - detect_provider_from_email("user@company.com"), - FolderProvider::GDrive - ); - } -} diff --git a/src/basic/keywords/on_email.rs b/src/basic/keywords/on_email.rs index 29b16268b..eecacf707 100644 --- a/src/basic/keywords/on_email.rs +++ b/src/basic/keywords/on_email.rs @@ -398,201 +398,3 @@ pub fn sanitize_email_for_filename(email: &str) -> String { .collect::() .to_lowercase() } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_email_monitor_struct() { - let monitor = EmailMonitor { - id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - email_address: "test@example.com".to_string(), - script_path: "on_email_test.rhai".to_string(), - is_active: true, - filter_from: None, - filter_subject: None, - }; - - assert_eq!(monitor.email_address, "test@example.com"); - assert!(monitor.is_active); - assert!(monitor.filter_from.is_none()); - assert!(monitor.filter_subject.is_none()); - } - - #[test] - fn test_email_monitor_with_filters() { - let monitor = EmailMonitor { - id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - email_address: "orders@company.com".to_string(), - script_path: "on_email_orders.rhai".to_string(), - is_active: true, - filter_from: Some("supplier@vendor.com".to_string()), - filter_subject: Some("Invoice".to_string()), - }; - - assert_eq!(monitor.email_address, "orders@company.com"); - assert_eq!(monitor.filter_from, Some("supplier@vendor.com".to_string())); - assert_eq!(monitor.filter_subject, Some("Invoice".to_string())); - } - - #[test] - fn test_email_attachment_struct() { - let attachment = EmailAttachment { - filename: "document.pdf".to_string(), - mime_type: "application/pdf".to_string(), - size: 1024, - }; - - assert_eq!(attachment.filename, "document.pdf"); - assert_eq!(attachment.mime_type, "application/pdf"); - assert_eq!(attachment.size, 1024); - } - - #[test] - fn test_email_received_event_struct() { - let event = EmailReceivedEvent { - id: Uuid::new_v4(), - monitor_id: Uuid::new_v4(), - message_uid: 12345, - message_id: Some("".to_string()), - from_address: "sender@example.com".to_string(), - to_addresses: vec!["recipient@example.com".to_string()], - subject: Some("Test Subject".to_string()), - has_attachments: true, - attachments: vec![EmailAttachment { - filename: "file.pdf".to_string(), - mime_type: "application/pdf".to_string(), - size: 2048, - }], - }; - - assert_eq!(event.message_uid, 12345); - assert_eq!(event.from_address, "sender@example.com"); - assert!(event.has_attachments); - assert_eq!(event.attachments.len(), 1); - assert_eq!(event.attachments[0].filename, "file.pdf"); - } - - #[test] - fn test_parse_email_path_basic() { - let result = parse_email_path("email://user@gmail.com"); - assert!(result.is_some()); - let (email, folder) = result.unwrap(); - assert_eq!(email, "user@gmail.com"); - assert!(folder.is_none()); - } - - #[test] - fn test_parse_email_path_with_folder() { - let result = parse_email_path("email://user@gmail.com/INBOX"); - assert!(result.is_some()); - let (email, folder) = result.unwrap(); - assert_eq!(email, "user@gmail.com"); - assert_eq!(folder, Some("INBOX".to_string())); - } - - #[test] - fn test_parse_email_path_invalid() { - assert!(parse_email_path("user@gmail.com").is_none()); - assert!(parse_email_path("mailto:user@gmail.com").is_none()); - assert!(parse_email_path("/local/path").is_none()); - } - - #[test] - fn test_is_email_path() { - assert!(is_email_path("email://user@gmail.com")); - assert!(is_email_path("email://user@company.com/INBOX")); - assert!(!is_email_path("user@gmail.com")); - assert!(!is_email_path("mailto:user@gmail.com")); - assert!(!is_email_path("account://user@gmail.com")); - } - - #[test] - fn test_sanitize_email_for_filename() { - assert_eq!( - sanitize_email_for_filename("user@gmail.com"), - "user_at_gmail_com" - ); - assert_eq!( - sanitize_email_for_filename("test.user@company.co.uk"), - "test_user_at_company_co_uk" - ); - assert_eq!( - sanitize_email_for_filename("USER@EXAMPLE.COM"), - "user_at_example_com" - ); - } - - #[test] - fn test_email_event_without_attachments() { - let event = EmailReceivedEvent { - id: Uuid::new_v4(), - monitor_id: Uuid::new_v4(), - message_uid: 1, - message_id: None, - from_address: "no-reply@system.com".to_string(), - to_addresses: vec![], - subject: None, - has_attachments: false, - attachments: vec![], - }; - - assert!(!event.has_attachments); - assert!(event.attachments.is_empty()); - assert!(event.subject.is_none()); - } - - #[test] - fn test_multiple_to_addresses() { - let event = EmailReceivedEvent { - id: Uuid::new_v4(), - monitor_id: Uuid::new_v4(), - message_uid: 999, - message_id: Some("".to_string()), - from_address: "sender@example.com".to_string(), - to_addresses: vec![ - "user1@example.com".to_string(), - "user2@example.com".to_string(), - "user3@example.com".to_string(), - ], - subject: Some("Group Message".to_string()), - has_attachments: false, - attachments: vec![], - }; - - assert_eq!(event.to_addresses.len(), 3); - assert!(event - .to_addresses - .contains(&"user2@example.com".to_string())); - } - - #[test] - fn test_multiple_attachments() { - let attachments = vec![ - EmailAttachment { - filename: "doc1.pdf".to_string(), - mime_type: "application/pdf".to_string(), - size: 1024, - }, - EmailAttachment { - filename: "image.png".to_string(), - mime_type: "image/png".to_string(), - size: 2048, - }, - EmailAttachment { - filename: "data.xlsx".to_string(), - mime_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - .to_string(), - size: 4096, - }, - ]; - - assert_eq!(attachments.len(), 3); - assert_eq!(attachments[0].filename, "doc1.pdf"); - assert_eq!(attachments[1].mime_type, "image/png"); - assert_eq!(attachments[2].size, 4096); - } -} diff --git a/src/basic/keywords/play.rs b/src/basic/keywords/play.rs index b9cff9b1d..8210e2700 100644 --- a/src/basic/keywords/play.rs +++ b/src/basic/keywords/play.rs @@ -701,99 +701,3 @@ async fn send_player_command( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_content_type_from_extension() { - assert_eq!(ContentType::from_extension("mp4"), ContentType::Video); - assert_eq!(ContentType::from_extension("MP3"), ContentType::Audio); - assert_eq!(ContentType::from_extension("png"), ContentType::Image); - assert_eq!(ContentType::from_extension("pdf"), ContentType::Pdf); - assert_eq!(ContentType::from_extension("rs"), ContentType::Code); - assert_eq!( - ContentType::from_extension("pptx"), - ContentType::Presentation - ); - assert_eq!( - ContentType::from_extension("xlsx"), - ContentType::Spreadsheet - ); - assert_eq!(ContentType::from_extension("md"), ContentType::Markdown); - } - - #[test] - fn test_content_type_from_mime() { - assert_eq!(ContentType::from_mime("video/mp4"), ContentType::Video); - assert_eq!(ContentType::from_mime("audio/mpeg"), ContentType::Audio); - assert_eq!(ContentType::from_mime("image/png"), ContentType::Image); - assert_eq!(ContentType::from_mime("application/pdf"), ContentType::Pdf); - } - - #[test] - fn test_play_options_from_string() { - let opts = PlayOptions::from_string("autoplay,loop,muted"); - assert!(opts.autoplay); - assert!(opts.loop_content); - assert!(opts.muted); - assert!(!opts.fullscreen); - assert!(opts.controls); - - let opts = PlayOptions::from_string("fullscreen,nocontrols,start=10,end=60"); - assert!(opts.fullscreen); - assert!(!opts.controls); - assert_eq!(opts.start_time, Some(10.0)); - assert_eq!(opts.end_time, Some(60.0)); - - let opts = PlayOptions::from_string("theme=dark,zoom=1.5,page=3"); - assert_eq!(opts.theme, Some("dark".to_string())); - assert_eq!(opts.zoom, Some(1.5)); - assert_eq!(opts.page, Some(3)); - } - - #[test] - fn test_detect_content_type() { - assert_eq!( - detect_content_type("https://youtube.com/watch?v=123"), - ContentType::Video - ); - assert_eq!( - detect_content_type("https://example.com/video.mp4"), - ContentType::Video - ); - assert_eq!( - detect_content_type("https://imgur.com/abc123"), - ContentType::Image - ); - assert_eq!( - detect_content_type("presentation.pptx"), - ContentType::Presentation - ); - assert_eq!(detect_content_type("report.pdf"), ContentType::Pdf); - assert_eq!(detect_content_type("main.rs"), ContentType::Code); - } - - #[test] - fn test_extract_title_from_source() { - assert_eq!(extract_title_from_source("documents/report.pdf"), "report"); - assert_eq!( - extract_title_from_source("https://example.com/video.mp4?token=abc"), - "video" - ); - assert_eq!( - extract_title_from_source("presentation.pptx"), - "presentation" - ); - } - - #[test] - fn test_player_component() { - assert_eq!(ContentType::Video.player_component(), "video-player"); - assert_eq!(ContentType::Audio.player_component(), "audio-player"); - assert_eq!(ContentType::Image.player_component(), "image-viewer"); - assert_eq!(ContentType::Pdf.player_component(), "pdf-viewer"); - assert_eq!(ContentType::Code.player_component(), "code-viewer"); - } -} diff --git a/src/basic/keywords/procedures.rs b/src/basic/keywords/procedures.rs index 32c9cc6a5..2707f6f53 100644 --- a/src/basic/keywords/procedures.rs +++ b/src/basic/keywords/procedures.rs @@ -673,165 +673,3 @@ pub fn get_procedure(name: &str) -> Option { } // TESTS - -#[cfg(test)] -mod tests { - use super::*; - - fn setup() { - clear_procedures(); - } - - #[test] - fn test_preprocess_sub() { - setup(); - - let input = r#" -x = 1 -SUB MySub(a, b) - TALK a + b -END SUB -y = 2 -"#; - - let result = preprocess_subs(input); - - // SUB should be extracted - assert!(!result.contains("SUB MySub")); - assert!(!result.contains("END SUB")); - assert!(result.contains("x = 1")); - assert!(result.contains("y = 2")); - - // Procedure should be registered - assert!(has_procedure("MYSUB")); - let proc = get_procedure("MYSUB").unwrap(); - assert_eq!(proc.params.len(), 2); - assert!(!proc.is_function); - } - - #[test] - fn test_preprocess_function() { - setup(); - - let input = r#" -FUNCTION Add(a, b) - RETURN a + b -END FUNCTION -result = Add(1, 2) -"#; - - let result = preprocess_functions(input); - - // FUNCTION should be extracted - assert!(!result.contains("FUNCTION Add")); - assert!(!result.contains("END FUNCTION")); - assert!(result.contains("result = Add(1, 2)")); - - // Procedure should be registered - assert!(has_procedure("ADD")); - let proc = get_procedure("ADD").unwrap(); - assert!(proc.is_function); - } - - #[test] - fn test_preprocess_sub_no_params() { - setup(); - - let input = r#" -SUB PrintHello - TALK "Hello" -END SUB -"#; - - preprocess_subs(input); - - assert!(has_procedure("PRINTHELLO")); - let proc = get_procedure("PRINTHELLO").unwrap(); - assert!(proc.params.is_empty()); - } - - #[test] - fn test_preprocess_call() { - setup(); - - // First register a SUB - let sub_input = r#" -SUB Greet(name) - TALK "Hello " + name -END SUB -"#; - preprocess_subs(sub_input); - - // Then preprocess CALL - let call_input = "CALL Greet(\"World\")"; - let result = preprocess_calls(call_input); - - // Should contain parameter assignment and body - assert!(result.contains("let name = \"World\"")); - assert!(result.contains("TALK \"Hello \" + name")); - } - - #[test] - fn test_eval_bool_condition() { - assert!(eval_bool_condition(&Dynamic::from(true))); - assert!(!eval_bool_condition(&Dynamic::from(false))); - assert!(eval_bool_condition(&Dynamic::from(1))); - assert!(!eval_bool_condition(&Dynamic::from(0))); - assert!(eval_bool_condition(&Dynamic::from(1.5))); - assert!(!eval_bool_condition(&Dynamic::from(0.0))); - assert!(eval_bool_condition(&Dynamic::from("hello"))); - assert!(!eval_bool_condition(&Dynamic::from(""))); - assert!(!eval_bool_condition(&Dynamic::from("false"))); - assert!(!eval_bool_condition(&Dynamic::from("0"))); - } - - #[test] - fn test_clear_procedures() { - setup(); - - let input = "SUB Test\n TALK \"test\"\nEND SUB"; - preprocess_subs(input); - - assert!(has_procedure("TEST")); - - clear_procedures(); - - assert!(!has_procedure("TEST")); - } - - #[test] - fn test_full_pipeline() { - setup(); - - let input = r#" -SUB SendGreeting(name, greeting) - TALK greeting + ", " + name + "!" -END SUB - -FUNCTION Calculate(x, y) - result = x * y + 10 - RETURN result -END FUNCTION - -' Main code -CALL SendGreeting("User", "Hello") -total = Calculate(5, 3) -"#; - - let result = preprocess_procedures(input); - - // Should have inlined the CALL - assert!(result.contains("let name = \"User\"")); - assert!(result.contains("let greeting = \"Hello\"")); - - // Original definitions should be gone - assert!(!result.contains("SUB SendGreeting")); - assert!(!result.contains("END SUB")); - assert!(!result.contains("FUNCTION Calculate")); - assert!(!result.contains("END FUNCTION")); - - // Both should be registered - assert!(has_procedure("SENDGREETING")); - assert!(has_procedure("CALCULATE")); - } -} diff --git a/src/basic/keywords/qrcode.rs b/src/basic/keywords/qrcode.rs index 40075c4b8..a1be2d320 100644 --- a/src/basic/keywords/qrcode.rs +++ b/src/basic/keywords/qrcode.rs @@ -429,43 +429,3 @@ pub fn generate_qr_code_with_logo( Ok(output_path.to_string()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_qr_code_generation() { - // Create a mock state and user for testing - // In real tests, you'd set up proper test fixtures - let result = QrCode::new(b"https://example.com"); - assert!(result.is_ok()); - } - - #[test] - fn test_qr_code_with_unicode() { - let result = QrCode::new("Hello 世界 🌍".as_bytes()); - assert!(result.is_ok()); - } - - #[test] - fn test_qr_code_long_data() { - let long_data = "A".repeat(1000); - let result = QrCode::new(long_data.as_bytes()); - assert!(result.is_ok()); - } - - #[test] - fn test_qr_code_url() { - let url = "https://example.com/path?param=value&other=123"; - let result = QrCode::new(url.as_bytes()); - assert!(result.is_ok()); - } - - #[test] - fn test_qr_code_json() { - let json = r#"{"id": 123, "name": "Test", "active": true}"#; - let result = QrCode::new(json.as_bytes()); - assert!(result.is_ok()); - } -} diff --git a/src/basic/keywords/remember.rs b/src/basic/keywords/remember.rs index b3317bca7..ee9576c95 100644 --- a/src/basic/keywords/remember.rs +++ b/src/basic/keywords/remember.rs @@ -329,18 +329,3 @@ struct MemoryRecord { #[diesel(sql_type = diesel::sql_types::Jsonb)] value: serde_json::Value, } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_duration() { - // Test various duration formats - assert!(parse_duration("30 days").is_ok()); - assert!(parse_duration("1 hour").is_ok()); - assert!(parse_duration("forever").is_ok()); - assert!(parse_duration("5 minutes").is_ok()); - assert!(parse_duration("invalid").is_err()); - } -} diff --git a/src/basic/keywords/save_from_unstructured.rs b/src/basic/keywords/save_from_unstructured.rs index dcc799d2b..be25ec3c4 100644 --- a/src/basic/keywords/save_from_unstructured.rs +++ b/src/basic/keywords/save_from_unstructured.rs @@ -463,25 +463,3 @@ async fn save_to_table( Ok(record_id) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_clean_value_for_type() { - assert_eq!(clean_value_for_type(&json!("test"), "text"), json!("test")); - assert_eq!(clean_value_for_type(&json!("42"), "integer"), json!(42)); - assert_eq!(clean_value_for_type(&json!("3.14"), "numeric"), json!(3.14)); - assert_eq!(clean_value_for_type(&json!("true"), "boolean"), json!(true)); - } - - #[test] - fn test_get_default_schema() { - let leads_schema = get_default_schema("leads"); - assert!(leads_schema.is_array()); - - let tasks_schema = get_default_schema("tasks"); - assert!(tasks_schema.is_array()); - } -} diff --git a/src/basic/keywords/send_mail.rs b/src/basic/keywords/send_mail.rs index 1f7e95bcd..7afd2107f 100644 --- a/src/basic/keywords/send_mail.rs +++ b/src/basic/keywords/send_mail.rs @@ -592,28 +592,3 @@ fn extract_template_subject(content: &str) -> Option { } None } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_apply_template_variables() { - let template = "Hello {{name}}, your order {{order_id}} is ready!"; - let vars = json!({ - "name": "John", - "order_id": "12345" - }); - - let result = apply_template_variables(template, &vars, "john@example.com").unwrap(); - assert!(result.contains("John")); - assert!(result.contains("12345")); - } - - #[test] - fn test_extract_template_subject() { - let content = "Subject: Welcome to our service\n\nHello there!"; - let subject = extract_template_subject(content); - assert_eq!(subject, Some("Welcome to our service".to_string())); - } -} diff --git a/src/basic/keywords/send_template.rs b/src/basic/keywords/send_template.rs index c4bd29a89..48875a32f 100644 --- a/src/basic/keywords/send_template.rs +++ b/src/basic/keywords/send_template.rs @@ -112,13 +112,3 @@ pub fn register_send_template_keywords( debug!("Send template keywords registered successfully"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_module_structure() { - // This test verifies the module compiles correctly - // Actual function tests are in the messaging/send_template.rs module - assert!(true); - } -} diff --git a/src/basic/keywords/set_schedule.rs b/src/basic/keywords/set_schedule.rs index 0ad8731d4..9d82bff00 100644 --- a/src/basic/keywords/set_schedule.rs +++ b/src/basic/keywords/set_schedule.rs @@ -421,171 +421,3 @@ pub fn execute_set_schedule( "rows_affected": result })) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_every_minute() { - assert_eq!(parse_natural_schedule("every minute").unwrap(), "* * * * *"); - } - - #[test] - fn test_every_n_minutes() { - assert_eq!( - parse_natural_schedule("every 5 minutes").unwrap(), - "*/5 * * * *" - ); - assert_eq!( - parse_natural_schedule("every 15 minutes").unwrap(), - "*/15 * * * *" - ); - assert_eq!( - parse_natural_schedule("every 30 minutes").unwrap(), - "*/30 * * * *" - ); - } - - #[test] - fn test_every_hour() { - assert_eq!(parse_natural_schedule("every hour").unwrap(), "0 * * * *"); - assert_eq!(parse_natural_schedule("hourly").unwrap(), "0 * * * *"); - } - - #[test] - fn test_every_n_hours() { - assert_eq!( - parse_natural_schedule("every 2 hours").unwrap(), - "0 */2 * * *" - ); - assert_eq!( - parse_natural_schedule("every 6 hours").unwrap(), - "0 */6 * * *" - ); - } - - #[test] - fn test_every_day() { - assert_eq!(parse_natural_schedule("every day").unwrap(), "0 0 * * *"); - assert_eq!(parse_natural_schedule("daily").unwrap(), "0 0 * * *"); - } - - #[test] - fn test_every_week() { - assert_eq!(parse_natural_schedule("every week").unwrap(), "0 0 * * 0"); - assert_eq!(parse_natural_schedule("weekly").unwrap(), "0 0 * * 0"); - } - - #[test] - fn test_every_month() { - assert_eq!(parse_natural_schedule("every month").unwrap(), "0 0 1 * *"); - assert_eq!(parse_natural_schedule("monthly").unwrap(), "0 0 1 * *"); - } - - #[test] - fn test_at_time() { - assert_eq!(parse_natural_schedule("at 9am").unwrap(), "0 9 * * *"); - assert_eq!(parse_natural_schedule("at 9:30am").unwrap(), "30 9 * * *"); - assert_eq!(parse_natural_schedule("at 2pm").unwrap(), "0 14 * * *"); - assert_eq!(parse_natural_schedule("at 14:00").unwrap(), "0 14 * * *"); - assert_eq!(parse_natural_schedule("at midnight").unwrap(), "0 0 * * *"); - assert_eq!(parse_natural_schedule("at noon").unwrap(), "0 12 * * *"); - } - - #[test] - fn test_day_of_week() { - assert_eq!(parse_natural_schedule("every monday").unwrap(), "0 0 * * 1"); - assert_eq!(parse_natural_schedule("every friday").unwrap(), "0 0 * * 5"); - assert_eq!(parse_natural_schedule("every sunday").unwrap(), "0 0 * * 0"); - } - - #[test] - fn test_day_with_time() { - assert_eq!( - parse_natural_schedule("every monday at 9am").unwrap(), - "0 9 * * 1" - ); - assert_eq!( - parse_natural_schedule("every friday at 5pm").unwrap(), - "0 17 * * 5" - ); - } - - #[test] - fn test_weekdays() { - assert_eq!(parse_natural_schedule("weekdays").unwrap(), "0 0 * * 1-5"); - assert_eq!( - parse_natural_schedule("every weekday").unwrap(), - "0 0 * * 1-5" - ); - assert_eq!( - parse_natural_schedule("weekdays at 8am").unwrap(), - "0 8 * * 1-5" - ); - } - - #[test] - fn test_weekends() { - assert_eq!(parse_natural_schedule("weekends").unwrap(), "0 0 * * 0,6"); - assert_eq!( - parse_natural_schedule("every weekend").unwrap(), - "0 0 * * 0,6" - ); - } - - #[test] - fn test_combined() { - assert_eq!( - parse_natural_schedule("every day at 9am").unwrap(), - "0 9 * * *" - ); - assert_eq!( - parse_natural_schedule("every day at 6:30pm").unwrap(), - "30 18 * * *" - ); - } - - #[test] - fn test_hour_range() { - assert_eq!( - parse_natural_schedule("every hour from 9 to 17").unwrap(), - "0 9-17 * * *" - ); - } - - #[test] - fn test_business_hours() { - assert_eq!( - parse_natural_schedule("business hours").unwrap(), - "0 9-17 * * 1-5" - ); - assert_eq!( - parse_natural_schedule("every 30 minutes during business hours").unwrap(), - "*/30 9-17 * * 1-5" - ); - assert_eq!( - parse_natural_schedule("every hour during business hours").unwrap(), - "0 9-17 * * 1-5" - ); - } - - #[test] - fn test_raw_cron_passthrough() { - assert_eq!(parse_natural_schedule("0 * * * *").unwrap(), "0 * * * *"); - assert_eq!( - parse_natural_schedule("*/5 * * * *").unwrap(), - "*/5 * * * *" - ); - assert_eq!( - parse_natural_schedule("0 9-17 * * 1-5").unwrap(), - "0 9-17 * * 1-5" - ); - } - - #[test] - fn test_invalid_input() { - assert!(parse_natural_schedule("potato salad").is_err()); - assert!(parse_natural_schedule("every 100 minutes").is_err()); // > 59 - } -} diff --git a/src/basic/keywords/sms.rs b/src/basic/keywords/sms.rs index 8c252c20c..c30681008 100644 --- a/src/basic/keywords/sms.rs +++ b/src/basic/keywords/sms.rs @@ -924,46 +924,3 @@ async fn send_via_custom_webhook( Err(format!("Custom webhook error: {}", error_text).into()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_normalize_phone_us_10_digit() { - assert_eq!(normalize_phone_number("5551234567"), "+15551234567"); - } - - #[test] - fn test_normalize_phone_us_11_digit() { - assert_eq!(normalize_phone_number("15551234567"), "+15551234567"); - } - - #[test] - fn test_normalize_phone_with_plus() { - assert_eq!(normalize_phone_number("+15551234567"), "+15551234567"); - } - - #[test] - fn test_normalize_phone_with_formatting() { - assert_eq!(normalize_phone_number("+1 (555) 123-4567"), "+15551234567"); - } - - #[test] - fn test_normalize_phone_international() { - assert_eq!(normalize_phone_number("+44 7911 123456"), "+447911123456"); - } - - #[test] - fn test_sms_provider_from_str() { - assert_eq!(SmsProvider::from("twilio"), SmsProvider::Twilio); - assert_eq!(SmsProvider::from("aws_sns"), SmsProvider::AwsSns); - assert_eq!(SmsProvider::from("vonage"), SmsProvider::Vonage); - assert_eq!(SmsProvider::from("nexmo"), SmsProvider::Vonage); - assert_eq!(SmsProvider::from("messagebird"), SmsProvider::MessageBird); - assert_eq!( - SmsProvider::from("custom"), - SmsProvider::Custom("custom".to_string()) - ); - } -} diff --git a/src/basic/keywords/social/get_metrics.rs b/src/basic/keywords/social/get_metrics.rs index 4e178223b..58b95e369 100644 --- a/src/basic/keywords/social/get_metrics.rs +++ b/src/basic/keywords/social/get_metrics.rs @@ -460,23 +460,3 @@ async fn fetch_twitter_metrics( ..Default::default() }) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_engagement_to_dynamic() { - let engagement = PostEngagement { - likes: 100, - comments: 20, - shares: 5, - views: 1000, - clicks: 50, - reach: 500, - }; - - let dynamic = engagement.to_dynamic(); - assert!(dynamic.is_map()); - } -} diff --git a/src/basic/keywords/social/post_to_scheduled.rs b/src/basic/keywords/social/post_to_scheduled.rs index 1def41e45..d73712540 100644 --- a/src/basic/keywords/social/post_to_scheduled.rs +++ b/src/basic/keywords/social/post_to_scheduled.rs @@ -170,20 +170,3 @@ async fn save_scheduled_post( ); Ok(post_id) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_schedule_time() { - let result = parse_schedule_time("2025-02-01 10:00"); - assert!(result.is_ok()); - - let result = parse_schedule_time("2025-02-01T10:00:00"); - assert!(result.is_ok()); - - let result = parse_schedule_time("invalid"); - assert!(result.is_err()); - } -} diff --git a/src/basic/keywords/social_media.rs b/src/basic/keywords/social_media.rs index 00a85074f..80222909d 100644 --- a/src/basic/keywords/social_media.rs +++ b/src/basic/keywords/social_media.rs @@ -104,13 +104,3 @@ pub fn register_social_media_keywords( debug!("Social media keywords registered successfully"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_module_structure() { - // This test verifies the module compiles correctly - // Actual function tests are in the social/ module files - assert!(true); - } -} diff --git a/src/basic/keywords/string_functions.rs b/src/basic/keywords/string_functions.rs index f9845e96a..ce48efe0a 100644 --- a/src/basic/keywords/string_functions.rs +++ b/src/basic/keywords/string_functions.rs @@ -50,7 +50,7 @@ pub fn instr_keyword(_state: &Arc, _user: UserSession, engine: &mut En /// /// # Returns /// * 1-based position of the first occurrence, or 0 if not found -fn instr_impl(start: i64, haystack: &str, needle: &str) -> i64 { +pub fn instr_impl(start: i64, haystack: &str, needle: &str) -> i64 { // Handle edge cases if haystack.is_empty() || needle.is_empty() { return 0; @@ -120,7 +120,7 @@ pub fn is_numeric_keyword(_state: &Arc, _user: UserSession, engine: &m /// # Returns /// * `true` if the value can be parsed as a number /// * `false` otherwise -fn is_numeric_impl(value: &str) -> bool { +pub fn is_numeric_impl(value: &str) -> bool { let trimmed = value.trim(); // Empty string is not numeric @@ -310,61 +310,3 @@ pub fn register_string_functions(state: Arc, user: UserSession, engine mid_keyword(&state, user.clone(), engine); replace_keyword(&state, user, engine); } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_instr_basic() { - assert_eq!(instr_impl(1, "Hello, World!", "World"), 8); - assert_eq!(instr_impl(1, "Hello, World!", "o"), 5); - assert_eq!(instr_impl(1, "Hello, World!", "xyz"), 0); - } - - #[test] - fn test_instr_with_start() { - assert_eq!(instr_impl(1, "one two one", "one"), 1); - assert_eq!(instr_impl(2, "one two one", "one"), 9); - assert_eq!(instr_impl(10, "one two one", "one"), 0); - } - - #[test] - fn test_instr_edge_cases() { - assert_eq!(instr_impl(1, "", "test"), 0); - assert_eq!(instr_impl(1, "test", ""), 0); - assert_eq!(instr_impl(1, "", ""), 0); - } - - #[test] - fn test_is_numeric_integers() { - assert!(is_numeric_impl("42")); - assert!(is_numeric_impl("-17")); - assert!(is_numeric_impl("0")); - assert!(is_numeric_impl(" 42 ")); - } - - #[test] - fn test_is_numeric_decimals() { - assert!(is_numeric_impl("3.14")); - assert!(is_numeric_impl("-0.5")); - assert!(is_numeric_impl(".25")); - assert!(is_numeric_impl("0.0")); - } - - #[test] - fn test_is_numeric_scientific() { - assert!(is_numeric_impl("1e10")); - assert!(is_numeric_impl("2.5E-3")); - assert!(is_numeric_impl("-1.5e+2")); - } - - #[test] - fn test_is_numeric_invalid() { - assert!(!is_numeric_impl("")); - assert!(!is_numeric_impl("abc")); - assert!(!is_numeric_impl("12abc")); - assert!(!is_numeric_impl("$100")); - assert!(!is_numeric_impl("1,000")); - } -} diff --git a/src/basic/keywords/switch_case.rs b/src/basic/keywords/switch_case.rs index 2d572e724..8441f912e 100644 --- a/src/basic/keywords/switch_case.rs +++ b/src/basic/keywords/switch_case.rs @@ -70,7 +70,7 @@ pub fn switch_keyword(_state: &Arc, _user: UserSession, engine: &mut E /// /// Compares two Dynamic values for equality, handling type coercion /// where appropriate. -fn switch_match_impl(expr: &Dynamic, case_val: &Dynamic) -> bool { +pub fn switch_match_impl(expr: &Dynamic, case_val: &Dynamic) -> bool { // Try string comparison first if let (Some(e), Some(c)) = ( expr.clone().into_string().ok(), @@ -219,78 +219,3 @@ pub fn preprocess_switch(input: &str) -> String { result } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_switch_match_strings() { - let a = Dynamic::from("hello"); - let b = Dynamic::from("hello"); - let c = Dynamic::from("world"); - - assert!(switch_match_impl(&a, &b)); - assert!(!switch_match_impl(&a, &c)); - } - - #[test] - fn test_switch_match_integers() { - let a = Dynamic::from(42_i64); - let b = Dynamic::from(42_i64); - let c = Dynamic::from(0_i64); - - assert!(switch_match_impl(&a, &b)); - assert!(!switch_match_impl(&a, &c)); - } - - #[test] - fn test_switch_match_floats() { - let a = Dynamic::from(3.14_f64); - let b = Dynamic::from(3.14_f64); - let c = Dynamic::from(2.71_f64); - - assert!(switch_match_impl(&a, &b)); - assert!(!switch_match_impl(&a, &c)); - } - - #[test] - fn test_switch_match_mixed_numeric() { - let int_val = Dynamic::from(42_i64); - let float_val = Dynamic::from(42.0_f64); - - assert!(switch_match_impl(&int_val, &float_val)); - } - - #[test] - fn test_preprocess_simple_switch() { - let input = r#" -SWITCH role - CASE "admin" - x = 1 - CASE "user" - x = 2 - DEFAULT - x = 0 -END SWITCH -"#; - let output = preprocess_switch(input); - assert!(output.contains("__switch_expr_")); - assert!(output.contains("if")); - assert!(output.contains("else")); - } - - #[test] - fn test_preprocess_multiple_values() { - let input = r#" -SWITCH day - CASE "saturday", "sunday" - weekend = true - DEFAULT - weekend = false -END SWITCH -"#; - let output = preprocess_switch(input); - assert!(output.contains("||")); - } -} diff --git a/src/basic/keywords/table_definition.rs b/src/basic/keywords/table_definition.rs index 044ff1faa..fab0d397a 100644 --- a/src/basic/keywords/table_definition.rs +++ b/src/basic/keywords/table_definition.rs @@ -628,136 +628,3 @@ pub fn register_table_keywords( // This function exists for consistency with other keyword modules trace!("TABLE keyword registered (compile-time only)"); } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_table_definition() { - let source = r#" -TABLE Contacts ON maria - Id number key - Nome string(150) - Email string(255) - Telefone string(20) -END TABLE -"#; - - let tables = parse_table_definition(source).unwrap(); - assert_eq!(tables.len(), 1); - - let table = &tables[0]; - assert_eq!(table.name, "Contacts"); - assert_eq!(table.connection_name, "maria"); - assert_eq!(table.fields.len(), 4); - - assert_eq!(table.fields[0].name, "Id"); - assert_eq!(table.fields[0].field_type, "number"); - assert!(table.fields[0].is_key); - - assert_eq!(table.fields[1].name, "Nome"); - assert_eq!(table.fields[1].field_type, "string"); - assert_eq!(table.fields[1].length, Some(150)); - } - - #[test] - fn test_parse_field_with_precision() { - let field = parse_field_definition("Preco double(10,2)", 0).unwrap(); - assert_eq!(field.name, "Preco"); - assert_eq!(field.field_type, "double"); - assert_eq!(field.length, Some(10)); - assert_eq!(field.precision, Some(2)); - } - - #[test] - fn test_generate_create_table_sql() { - let table = TableDefinition { - name: "TestTable".to_string(), - connection_name: "default".to_string(), - fields: vec![ - FieldDefinition { - name: "id".to_string(), - field_type: "number".to_string(), - length: None, - precision: None, - is_key: true, - is_nullable: false, - default_value: None, - reference_table: None, - field_order: 0, - }, - FieldDefinition { - name: "name".to_string(), - field_type: "string".to_string(), - length: Some(100), - precision: None, - is_key: false, - is_nullable: true, - default_value: None, - reference_table: None, - field_order: 1, - }, - ], - }; - - let sql = generate_create_table_sql(&table, "postgres"); - assert!(sql.contains("CREATE TABLE IF NOT EXISTS TestTable")); - assert!(sql.contains("id INTEGER NOT NULL")); - assert!(sql.contains("name VARCHAR(100)")); - assert!(sql.contains("PRIMARY KEY (id)")); - } - - #[test] - fn test_map_types() { - let field = FieldDefinition { - name: "test".to_string(), - field_type: "string".to_string(), - length: Some(50), - precision: None, - is_key: false, - is_nullable: true, - default_value: None, - reference_table: None, - field_order: 0, - }; - assert_eq!(map_type_to_sql(&field, "postgres"), "VARCHAR(50)"); - - let date_field = FieldDefinition { - name: "created".to_string(), - field_type: "datetime".to_string(), - length: None, - precision: None, - is_key: false, - is_nullable: true, - default_value: None, - reference_table: None, - field_order: 0, - }; - assert_eq!(map_type_to_sql(&date_field, "mysql"), "DATETIME"); - assert_eq!(map_type_to_sql(&date_field, "postgres"), "TIMESTAMP"); - } - - #[test] - fn test_sanitize_identifier() { - assert_eq!(sanitize_identifier("valid_name"), "valid_name"); - assert_eq!(sanitize_identifier("DROP TABLE; --"), "DROPTABLE"); - assert_eq!(sanitize_identifier("name123"), "name123"); - } - - #[test] - fn test_build_connection_string() { - let conn = ExternalConnection { - name: "test".to_string(), - driver: "mysql".to_string(), - server: "localhost".to_string(), - port: Some(3306), - database: "testdb".to_string(), - username: "user".to_string(), - password: "pass".to_string(), - }; - - let conn_str = build_connection_string(&conn); - assert_eq!(conn_str, "mysql://user:pass@localhost:3306/testdb"); - } -} diff --git a/src/basic/keywords/transfer_to_human.rs b/src/basic/keywords/transfer_to_human.rs index d1caec7ea..f5ed9fa3e 100644 --- a/src/basic/keywords/transfer_to_human.rs +++ b/src/basic/keywords/transfer_to_human.rs @@ -794,94 +794,3 @@ pub fn get_tool_definition() -> serde_json::Value { } }) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_priority_to_int() { - assert_eq!(priority_to_int(Some("urgent")), 3); - assert_eq!(priority_to_int(Some("high")), 2); - assert_eq!(priority_to_int(Some("normal")), 1); - assert_eq!(priority_to_int(Some("low")), 0); - assert_eq!(priority_to_int(None), 1); - } - - #[test] - fn test_find_attendant_by_name() { - let attendants = vec![ - Attendant { - id: "att-001".to_string(), - name: "John Smith".to_string(), - channel: "all".to_string(), - preferences: "sales".to_string(), - department: Some("commercial".to_string()), - aliases: vec!["johnny".to_string(), "js".to_string()], - status: AttendantStatus::Online, - }, - Attendant { - id: "att-002".to_string(), - name: "Jane Doe".to_string(), - channel: "web".to_string(), - preferences: "support".to_string(), - department: Some("customer-service".to_string()), - aliases: vec![], - status: AttendantStatus::Online, - }, - ]; - - // Find by exact name - let found = find_attendant(&attendants, Some("John Smith"), None); - assert!(found.is_some()); - assert_eq!(found.unwrap().id, "att-001"); - - // Find by partial name - let found = find_attendant(&attendants, Some("john"), None); - assert!(found.is_some()); - assert_eq!(found.unwrap().id, "att-001"); - - // Find by alias - let found = find_attendant(&attendants, Some("johnny"), None); - assert!(found.is_some()); - assert_eq!(found.unwrap().id, "att-001"); - - // Find by department - let found = find_attendant(&attendants, None, Some("customer-service")); - assert!(found.is_some()); - assert_eq!(found.unwrap().id, "att-002"); - } - - #[test] - fn test_transfer_result_to_dynamic() { - let result = TransferResult { - success: true, - status: TransferStatus::Assigned, - queue_position: Some(1), - assigned_to: Some("att-001".to_string()), - assigned_to_name: Some("John Smith".to_string()), - estimated_wait_seconds: Some(30), - message: "Connected to John".to_string(), - }; - - let dynamic = result.to_dynamic(); - let map = dynamic.try_cast::().unwrap(); - - assert_eq!( - map.get("success") - .unwrap() - .clone() - .try_cast::() - .unwrap(), - true - ); - assert_eq!( - map.get("assigned_to_name") - .unwrap() - .clone() - .try_cast::() - .unwrap(), - "John Smith" - ); - } -} diff --git a/src/basic/keywords/use_account.rs b/src/basic/keywords/use_account.rs index d4b2a12aa..c6c90fb14 100644 --- a/src/basic/keywords/use_account.rs +++ b/src/basic/keywords/use_account.rs @@ -224,30 +224,3 @@ pub struct AccountCredentials { pub access_token: String, pub refresh_token: Option, } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_account_path() { - let result = parse_account_path("account://user@gmail.com/Documents/file.pdf"); - assert!(result.is_some()); - let (email, path) = result.unwrap(); - assert_eq!(email, "user@gmail.com"); - assert_eq!(path, "Documents/file.pdf"); - } - - #[test] - fn test_parse_account_path_invalid() { - assert!(parse_account_path("local/file.pdf").is_none()); - assert!(parse_account_path("/absolute/path").is_none()); - } - - #[test] - fn test_is_account_path() { - assert!(is_account_path("account://user@gmail.com/file.pdf")); - assert!(!is_account_path("local/file.pdf")); - assert!(!is_account_path("file.pdf")); - } -} diff --git a/src/basic/keywords/use_kb.rs b/src/basic/keywords/use_kb.rs index 9637efb7a..2bc913fc6 100644 --- a/src/basic/keywords/use_kb.rs +++ b/src/basic/keywords/use_kb.rs @@ -194,18 +194,3 @@ pub fn get_active_kbs_for_session( .map(|r| (r.kb_name, r.kb_folder_path, r.qdrant_collection)) .collect()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_use_kb_syntax() { - let mut engine = Engine::new(); - // This would normally use real state and session - // For now just test that the syntax can be registered - assert!(engine - .register_custom_syntax(&["USE_KB", "$expr$"], true, |_, _| Ok(Dynamic::UNIT)) - .is_ok()); - } -} diff --git a/src/basic/keywords/use_website.rs b/src/basic/keywords/use_website.rs index 6a5f5e7f6..a25a11044 100644 --- a/src/basic/keywords/use_website.rs +++ b/src/basic/keywords/use_website.rs @@ -377,35 +377,3 @@ fn sanitize_url_for_collection(url: &str) -> String { .collect::() .to_lowercase() } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_url_sanitization() { - assert_eq!( - sanitize_url_for_collection("https://docs.example.com/path"), - "docs_example_com_path" - ); - assert_eq!( - sanitize_url_for_collection("http://test.site:8080"), - "test_site_8080" - ); - } - - #[test] - fn test_use_website_syntax() { - let mut engine = Engine::new(); - - // Test USE_WEBSITE with argument - assert!(engine - .register_custom_syntax(&["USE_WEBSITE", "$expr$"], true, |_, _| Ok(Dynamic::UNIT)) - .is_ok()); - - // Test CLEAR_WEBSITES without argument - assert!(engine - .register_custom_syntax(&["CLEAR_WEBSITES"], true, |_, _| Ok(Dynamic::UNIT)) - .is_ok()); - } -} diff --git a/src/basic/keywords/user_memory.rs b/src/basic/keywords/user_memory.rs index 7ce8e433e..491c9bc8e 100644 --- a/src/basic/keywords/user_memory.rs +++ b/src/basic/keywords/user_memory.rs @@ -292,15 +292,3 @@ async fn clear_user_memory_async(state: &AppState, user_id: Uuid) -> Result<(), } // Tests - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_fact_key_generation() { - let fact_key = format!("fact_{}", Uuid::new_v4()); - assert!(fact_key.starts_with("fact_")); - assert!(fact_key.len() > 5); - } -} diff --git a/src/basic/keywords/validation/isempty.rs b/src/basic/keywords/validation/isempty.rs index fbce71e60..84225fad9 100644 --- a/src/basic/keywords/validation/isempty.rs +++ b/src/basic/keywords/validation/isempty.rs @@ -77,59 +77,3 @@ fn check_empty(value: &Dynamic) -> bool { false } - -#[cfg(test)] -mod tests { - use super::*; - use rhai::{Array, Map}; - - #[test] - fn test_empty_string() { - let value = Dynamic::from(""); - assert!(check_empty(&value)); - } - - #[test] - fn test_non_empty_string() { - let value = Dynamic::from("hello"); - assert!(!check_empty(&value)); - } - - #[test] - fn test_empty_array() { - let value = Dynamic::from(Array::new()); - assert!(check_empty(&value)); - } - - #[test] - fn test_non_empty_array() { - let mut arr = Array::new(); - arr.push(Dynamic::from(1)); - let value = Dynamic::from(arr); - assert!(!check_empty(&value)); - } - - #[test] - fn test_empty_map() { - let value = Dynamic::from(Map::new()); - assert!(check_empty(&value)); - } - - #[test] - fn test_unit() { - let value = Dynamic::UNIT; - assert!(check_empty(&value)); - } - - #[test] - fn test_number_not_empty() { - let value = Dynamic::from(0); - assert!(!check_empty(&value)); - } - - #[test] - fn test_bool_not_empty() { - let value = Dynamic::from(false); - assert!(!check_empty(&value)); - } -} diff --git a/src/basic/keywords/validation/isnull.rs b/src/basic/keywords/validation/isnull.rs index 946ee79df..341595409 100644 --- a/src/basic/keywords/validation/isnull.rs +++ b/src/basic/keywords/validation/isnull.rs @@ -15,27 +15,3 @@ pub fn isnull_keyword(_state: &Arc, _user: UserSession, engine: &mut E debug!("Registered ISNULL keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_isnull_unit() { - use rhai::Dynamic; - let value = Dynamic::UNIT; - assert!(value.is_unit()); - } - - #[test] - fn test_isnull_not_unit() { - use rhai::Dynamic; - let value = Dynamic::from("test"); - assert!(!value.is_unit()); - } - - #[test] - fn test_isnull_number() { - use rhai::Dynamic; - let value = Dynamic::from(42); - assert!(!value.is_unit()); - } -} diff --git a/src/basic/keywords/validation/nvl_iif.rs b/src/basic/keywords/validation/nvl_iif.rs index 53e119d67..eb787b823 100644 --- a/src/basic/keywords/validation/nvl_iif.rs +++ b/src/basic/keywords/validation/nvl_iif.rs @@ -138,44 +138,3 @@ pub fn iif_keyword(_state: &Arc, _user: UserSession, engine: &mut Engi debug!("Registered IIF/IF_FUNC/CHOOSE/SWITCH_FUNC keywords"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_nvl_logic() { - let value = ""; - let default = "default"; - let result = if value.is_empty() { default } else { value }; - assert_eq!(result, "default"); - } - - #[test] - fn test_nvl_with_value() { - let value = "actual"; - let default = "default"; - let result = if value.is_empty() { default } else { value }; - assert_eq!(result, "actual"); - } - - #[test] - fn test_iif_true() { - let condition = true; - let result = if condition { "yes" } else { "no" }; - assert_eq!(result, "yes"); - } - - #[test] - fn test_iif_false() { - let condition = false; - let result = if condition { "yes" } else { "no" }; - assert_eq!(result, "no"); - } - - #[test] - fn test_choose() { - let index = 2; - let values = vec!["first", "second", "third"]; - let result = values.get((index - 1) as usize).unwrap_or(&""); - assert_eq!(*result, "second"); - } -} diff --git a/src/basic/keywords/validation/str_val.rs b/src/basic/keywords/validation/str_val.rs index f79bac9e9..839ec5f06 100644 --- a/src/basic/keywords/validation/str_val.rs +++ b/src/basic/keywords/validation/str_val.rs @@ -129,21 +129,3 @@ pub fn cdbl_keyword(_state: &Arc, _user: UserSession, engine: &mut Eng debug!("Registered CDBL keyword"); } - -#[cfg(test)] -mod tests { - #[test] - fn test_val_parsing() { - assert_eq!("123.45".trim().parse::().unwrap_or(0.0), 123.45); - assert_eq!(" 456 ".trim().parse::().unwrap_or(0.0), 456.0); - assert_eq!("abc".trim().parse::().unwrap_or(0.0), 0.0); - } - - #[test] - fn test_cint_rounding() { - assert_eq!(2.4_f64.round() as i64, 2); - assert_eq!(2.5_f64.round() as i64, 3); - assert_eq!(2.6_f64.round() as i64, 3); - assert_eq!((-2.5_f64).round() as i64, -3); - } -} diff --git a/src/basic/keywords/validation/typeof_check.rs b/src/basic/keywords/validation/typeof_check.rs index ef4423794..c4da90e24 100644 --- a/src/basic/keywords/validation/typeof_check.rs +++ b/src/basic/keywords/validation/typeof_check.rs @@ -135,27 +135,3 @@ fn is_numeric(value: &Dynamic) -> bool { false } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_type_name() { - assert_eq!(get_type_name(&Dynamic::UNIT), "null"); - assert_eq!(get_type_name(&Dynamic::from(true)), "boolean"); - assert_eq!(get_type_name(&Dynamic::from(42_i64)), "integer"); - assert_eq!(get_type_name(&Dynamic::from(3.14_f64)), "float"); - assert_eq!(get_type_name(&Dynamic::from("hello")), "string"); - } - - #[test] - fn test_is_numeric() { - assert!(is_numeric(&Dynamic::from(42_i64))); - assert!(is_numeric(&Dynamic::from(3.14_f64))); - assert!(is_numeric(&Dynamic::from("123"))); - assert!(is_numeric(&Dynamic::from("3.14"))); - assert!(!is_numeric(&Dynamic::from("hello"))); - assert!(!is_numeric(&Dynamic::from(true))); - } -} diff --git a/src/basic/keywords/weather.rs b/src/basic/keywords/weather.rs index a6bc876da..e8d8358b1 100644 --- a/src/basic/keywords/weather.rs +++ b/src/basic/keywords/weather.rs @@ -400,41 +400,3 @@ fn get_weather_api_key(_state: &AppState) -> Result { // For now, return error indicating configuration needed Err("Weather API key not configured. Please set 'weather-api-key' in config.csv".to_string()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_degrees_to_compass() { - assert_eq!(degrees_to_compass(0.0), "N"); - assert_eq!(degrees_to_compass(45.0), "NE"); - assert_eq!(degrees_to_compass(90.0), "E"); - assert_eq!(degrees_to_compass(180.0), "S"); - assert_eq!(degrees_to_compass(270.0), "W"); - assert_eq!(degrees_to_compass(315.0), "NW"); - } - - #[test] - fn test_format_weather_response() { - let weather = WeatherData { - location: "London".to_string(), - temperature: 15.0, - temperature_unit: "°C".to_string(), - description: "Partly cloudy".to_string(), - humidity: 65, - wind_speed: 3.5, - wind_direction: "NE".to_string(), - feels_like: 14.0, - pressure: 1013, - visibility: 10.0, - uv_index: Some(3.0), - forecast: Vec::new(), - }; - - let response = format_weather_response(&weather); - assert!(response.contains("London")); - assert!(response.contains("15.0")); - assert!(response.contains("Partly cloudy")); - } -} diff --git a/src/basic/keywords/webhook.rs b/src/basic/keywords/webhook.rs index a4bb63fce..be22d5c9f 100644 --- a/src/basic/keywords/webhook.rs +++ b/src/basic/keywords/webhook.rs @@ -431,73 +431,3 @@ fn dynamic_to_json(value: &Dynamic) -> Value { Value::String(value.to_string()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_webhook_request_to_dynamic() { - let mut headers = std::collections::HashMap::new(); - headers.insert("Content-Type".to_string(), "application/json".to_string()); - - let mut params = std::collections::HashMap::new(); - params.insert("id".to_string(), "123".to_string()); - - let request = WebhookRequest::new( - "POST", - headers, - params, - json!({"order": "test"}), - "/webhook/order-received", - ); - - let dynamic = request.to_dynamic(); - assert!(dynamic.is_map()); - } - - #[test] - fn test_webhook_response_from_dynamic() { - let mut map = rhai::Map::new(); - map.insert("status".into(), Dynamic::from(201_i64)); - map.insert( - "body".into(), - Dynamic::from(json!({"message": "created"}).to_string()), - ); - - let dynamic = Dynamic::from(map); - let response = WebhookResponse::from_dynamic(&dynamic); - - assert_eq!(response.status, 201); - } - - #[test] - fn test_json_to_dynamic_and_back() { - let original = json!({ - "name": "test", - "count": 42, - "active": true, - "items": [1, 2, 3] - }); - - let dynamic = json_to_dynamic(&original); - let back = dynamic_to_json(&dynamic); - - assert_eq!(original["name"], back["name"]); - assert_eq!(original["count"], back["count"]); - assert_eq!(original["active"], back["active"]); - } - - #[test] - fn test_webhook_response_default() { - let response = WebhookResponse::default(); - assert_eq!(response.status, 200); - } - - #[test] - fn test_webhook_response_error() { - let response = WebhookResponse::error(404, "Not found"); - assert_eq!(response.status, 404); - assert_eq!(response.body["error"], "Not found"); - } -} diff --git a/src/calendar/mod.rs b/src/calendar/mod.rs index 6fd5f3357..af10541f2 100644 --- a/src/calendar/mod.rs +++ b/src/calendar/mod.rs @@ -557,50 +557,3 @@ pub fn configure_calendar_routes() -> Router> { .route("/ui/calendar/event/new", get(new_event_form)) .route("/ui/calendar/new", get(new_calendar_form)) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_event_to_ical_roundtrip() { - let event = CalendarEvent { - id: Uuid::new_v4(), - title: "Test Meeting".to_string(), - description: Some("A test meeting".to_string()), - start_time: Utc::now(), - end_time: Utc::now() + chrono::Duration::hours(1), - location: Some("Room 101".to_string()), - attendees: vec!["user@example.com".to_string()], - organizer: "organizer@example.com".to_string(), - reminder_minutes: Some(15), - recurrence: None, - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - let ical = event.to_ical(); - assert_eq!(ical.get_summary(), Some("Test Meeting")); - assert_eq!(ical.get_location(), Some("Room 101")); - } - - #[test] - fn test_export_import_ical() { - let mut engine = CalendarEngine::new(); - engine.create_event(CalendarEventInput { - title: "Event 1".to_string(), - description: None, - start_time: Utc::now(), - end_time: Utc::now() + chrono::Duration::hours(1), - location: None, - attendees: vec![], - organizer: "test@example.com".to_string(), - reminder_minutes: None, - recurrence: None, - }); - - let ical = engine.export_ical("Test Calendar"); - assert!(ical.contains("BEGIN:VCALENDAR")); - assert!(ical.contains("Event 1")); - } -} diff --git a/src/compliance/access_review.rs b/src/compliance/access_review.rs index 4e18d5255..ff8f5c9ff 100644 --- a/src/compliance/access_review.rs +++ b/src/compliance/access_review.rs @@ -270,35 +270,42 @@ impl AccessReviewService { modified: Vec<(Uuid, AccessLevel)>, comments: String, ) -> Result { - let review = self - .reviews - .get_mut(&review_id) - .ok_or_else(|| anyhow!("Review not found"))?; + // Get review info first + let (reviewer_id, user_id) = { + let review = self + .reviews + .get(&review_id) + .ok_or_else(|| anyhow!("Review not found"))?; - if review.status != ReviewStatus::Pending && review.status != ReviewStatus::InProgress { - return Err(anyhow!("Review already completed")); - } + if review.status != ReviewStatus::Pending && review.status != ReviewStatus::InProgress { + return Err(anyhow!("Review already completed")); + } + (review.reviewer_id, review.user_id) + }; // Process revocations for perm_id in &revoked { - self.revoke_permission(*perm_id, review.reviewer_id)?; + self.revoke_permission(*perm_id, reviewer_id)?; } // Process modifications for (perm_id, new_level) in &modified { - if let Some(permissions) = self.permissions.get_mut(&review.user_id) { + if let Some(permissions) = self.permissions.get_mut(&user_id) { if let Some(perm) = permissions.iter_mut().find(|p| p.id == *perm_id) { perm.access_level = new_level.clone(); } } } - review.status = ReviewStatus::Approved; - review.comments = Some(comments.clone()); + // Update review status + if let Some(review) = self.reviews.get_mut(&review_id) { + review.status = ReviewStatus::Approved; + review.comments = Some(comments.clone()); + } let result = AccessReviewResult { review_id, - reviewer_id: review.reviewer_id, + reviewer_id, reviewed_at: Utc::now(), approved_permissions: approved, revoked_permissions: revoked, diff --git a/src/compliance/code_scanner.rs b/src/compliance/code_scanner.rs index 77ae4e207..6afb043d8 100644 --- a/src/compliance/code_scanner.rs +++ b/src/compliance/code_scanner.rs @@ -658,83 +658,3 @@ fn escape_csv(s: &str) -> String { s.to_string() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pattern_matching() { - let scanner = CodeScanner::new("/tmp/test"); - - // Test password detection - let password_pattern = scanner - .patterns - .iter() - .find(|p| matches!(p.issue_type, IssueType::PasswordInConfig)) - .unwrap(); - assert!(password_pattern.regex.is_match(r#"password = "secret123""#)); - assert!(password_pattern.regex.is_match(r#"PASSWORD = 'mypass'"#)); - - // Test underscore keyword detection - let underscore_pattern = scanner - .patterns - .iter() - .find(|p| matches!(p.issue_type, IssueType::UnderscoreInKeyword)) - .unwrap(); - assert!(underscore_pattern.regex.is_match("GET_BOT_MEMORY")); - assert!(underscore_pattern.regex.is_match("SET_USER_MEMORY")); - } - - #[test] - fn test_severity_ordering() { - assert!(IssueSeverity::Critical > IssueSeverity::High); - assert!(IssueSeverity::High > IssueSeverity::Medium); - assert!(IssueSeverity::Medium > IssueSeverity::Low); - assert!(IssueSeverity::Low > IssueSeverity::Info); - } - - #[test] - fn test_stats_merge() { - let mut stats1 = ScanStats { - critical: 1, - high: 2, - medium: 3, - low: 4, - info: 5, - total: 15, - }; - - let stats2 = ScanStats { - critical: 1, - high: 1, - medium: 1, - low: 1, - info: 1, - total: 5, - }; - - stats1.merge(&stats2); - - assert_eq!(stats1.critical, 2); - assert_eq!(stats1.high, 3); - assert_eq!(stats1.total, 20); - } - - #[test] - fn test_csv_escape() { - assert_eq!(escape_csv("simple"), "simple"); - assert_eq!(escape_csv("with,comma"), "\"with,comma\""); - assert_eq!(escape_csv("with\"quote"), "\"with\"\"quote\""); - } - - #[test] - fn test_redact_sensitive() { - let scanner = CodeScanner::new("/tmp/test"); - - let line = r#"password = "supersecretpassword123""#; - let redacted = scanner.redact_sensitive(line); - assert!(redacted.contains("***REDACTED***")); - assert!(!redacted.contains("supersecretpassword123")); - } -} diff --git a/src/compliance/mod.rs b/src/compliance/mod.rs index d575e4471..f858a2e9a 100644 --- a/src/compliance/mod.rs +++ b/src/compliance/mod.rs @@ -434,44 +434,3 @@ pub struct ComplianceReport { pub low_issues: usize, pub results: Vec, } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_compliance_monitor() { - let monitor = ComplianceMonitor::new(vec![ComplianceFramework::GDPR]); - let results = monitor.run_checks().await.unwrap(); - assert!(!results.is_empty()); - } - - #[test] - fn test_compliance_score() { - let results = vec![ - ComplianceCheckResult { - framework: ComplianceFramework::GDPR, - control_id: "test_1".to_string(), - control_name: "Test Control 1".to_string(), - status: ComplianceStatus::Compliant, - score: 100.0, - checked_at: Utc::now(), - issues: vec![], - evidence: vec![], - }, - ComplianceCheckResult { - framework: ComplianceFramework::GDPR, - control_id: "test_2".to_string(), - control_name: "Test Control 2".to_string(), - status: ComplianceStatus::Compliant, - score: 90.0, - checked_at: Utc::now(), - issues: vec![], - evidence: vec![], - }, - ]; - - let score = ComplianceMonitor::calculate_compliance_score(&results); - assert_eq!(score, 95.0); - } -} diff --git a/src/compliance/policy_checker.rs b/src/compliance/policy_checker.rs index 634a96bcd..0ca4528b8 100644 --- a/src/compliance/policy_checker.rs +++ b/src/compliance/policy_checker.rs @@ -350,14 +350,18 @@ impl PolicyChecker { /// Check all active policies pub fn check_all_policies(&mut self, context: &PolicyContext) -> Vec { + // Collect active policy IDs first to avoid borrow issues + let active_policy_ids: Vec = self + .policies + .iter() + .filter(|(_, p)| p.status == PolicyStatus::Active) + .map(|(id, _)| *id) + .collect(); + let mut results = Vec::new(); - for policy in self.policies.values() { - if policy.status != PolicyStatus::Active { - continue; - } - - let result = self.check_policy(policy.id, context); + for policy_id in active_policy_ids { + let result = self.check_policy(policy_id, context); if let Ok(result) = result { results.push(result); } diff --git a/src/compliance/training_tracker.rs b/src/compliance/training_tracker.rs index 21a8e379b..b95b75ffa 100644 --- a/src/compliance/training_tracker.rs +++ b/src/compliance/training_tracker.rs @@ -258,43 +258,56 @@ impl TrainingTracker { attempt_id: Uuid, score: u32, ) -> Result { + // Get course info first + let (course_id, passing_score, validity_days, max_attempts, course_title) = { + let assignment = self + .assignments + .get(&assignment_id) + .ok_or_else(|| anyhow!("Assignment not found"))?; + let course = self + .courses + .get(&assignment.course_id) + .ok_or_else(|| anyhow!("Course not found"))?; + (course.id, course.passing_score, course.validity_days, course.max_attempts, course.title.clone()) + }; + let assignment = self .assignments .get_mut(&assignment_id) .ok_or_else(|| anyhow!("Assignment not found"))?; - let course = self - .courses - .get(&assignment.course_id) - .ok_or_else(|| anyhow!("Course not found"))? - .clone(); - - let attempt = assignment + let attempt_idx = assignment .attempts - .iter_mut() - .find(|a| a.id == attempt_id) + .iter() + .position(|a| a.id == attempt_id) .ok_or_else(|| anyhow!("Attempt not found"))?; let end_time = Utc::now(); - let time_spent = (end_time - attempt.start_time).num_minutes() as u32; + let start_time = assignment.attempts[attempt_idx].start_time; + let time_spent = (end_time - start_time).num_minutes() as u32; + let passed = score >= passing_score; - attempt.end_time = Some(end_time); - attempt.score = Some(score); - attempt.time_spent_minutes = Some(time_spent); - attempt.passed = score >= course.passing_score; + // Update attempt + assignment.attempts[attempt_idx].end_time = Some(end_time); + assignment.attempts[attempt_idx].score = Some(score); + assignment.attempts[attempt_idx].time_spent_minutes = Some(time_spent); + assignment.attempts[attempt_idx].passed = passed; - if attempt.passed { + let user_id = assignment.user_id; + let attempts_count = assignment.attempts.len(); + + if passed { assignment.status = TrainingStatus::Completed; assignment.completion_date = Some(end_time); - assignment.expiry_date = Some(end_time + Duration::days(course.validity_days)); + assignment.expiry_date = Some(end_time + Duration::days(validity_days)); // Issue certificate let certificate = TrainingCertificate { id: Uuid::new_v4(), - user_id: assignment.user_id, - course_id: course.id, + user_id, + course_id, issued_date: end_time, - expiry_date: end_time + Duration::days(course.validity_days), + expiry_date: end_time + Duration::days(validity_days), certificate_number: format!( "CERT-{}", Uuid::new_v4().to_string()[..8].to_uppercase() @@ -306,15 +319,15 @@ impl TrainingTracker { log::info!( "User {} completed training '{}' with score {}", - assignment.user_id, - course.title, + user_id, + course_title, score ); - } else if assignment.attempts.len() >= course.max_attempts as usize { + } else if attempts_count >= max_attempts as usize { assignment.status = TrainingStatus::Failed; } - Ok(attempt.passed) + Ok(passed) } /// Get user compliance status diff --git a/src/console/wizard.rs b/src/console/wizard.rs index 28ddd12be..56ff4bba4 100644 --- a/src/console/wizard.rs +++ b/src/console/wizard.rs @@ -939,31 +939,3 @@ pub fn apply_wizard_config(config: &WizardConfig) -> io::Result<()> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = WizardConfig::default(); - assert_eq!(config.llm_provider, LlmProvider::None); - assert!(!config.components.is_empty()); - } - - #[test] - fn test_slug_generation() { - let mut config = WizardConfig::default(); - config.organization.name = "My Test Company".to_string(); - config.organization.slug = config - .organization - .name - .to_lowercase() - .replace(' ', "-") - .chars() - .filter(|c| c.is_alphanumeric() || *c == '-') - .collect(); - - assert_eq!(config.organization.slug, "my-test-company"); - } -} diff --git a/src/core/bot/manager.rs b/src/core/bot/manager.rs index 00691ff75..9f05a7ce0 100644 --- a/src/core/bot/manager.rs +++ b/src/core/bot/manager.rs @@ -1016,58 +1016,3 @@ impl From<&BotConfig> for BotRoute { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sanitize_bot_name() { - let manager = BotManager::new("", "", "", "", PathBuf::new()); - - assert_eq!(manager.sanitize_bot_name("My Bot"), "mybot"); - assert_eq!(manager.sanitize_bot_name("test-bot"), "test-bot"); - assert_eq!(manager.sanitize_bot_name("Bot 123"), "bot123"); - assert_eq!(manager.sanitize_bot_name("--invalid--"), "invalid"); - assert_eq!(manager.sanitize_bot_name("my_bot_name"), "my_bot_name"); - } - - #[test] - fn test_bot_config_default() { - let settings = BotSettings::default(); - assert!(settings.knowledge_bases.is_empty()); - assert!(settings.channels.is_empty()); - } - - #[test] - fn test_bot_status_display() { - assert_eq!(format!("{}", BotStatus::Active), "Active"); - assert_eq!(format!("{}", BotStatus::Creating), "Creating"); - } - - #[test] - fn test_bot_route_from_config() { - let config = BotConfig { - id: Uuid::new_v4(), - name: "testbot".to_string(), - display_name: "Test Bot".to_string(), - org_id: Uuid::new_v4(), - org_slug: "myorg".to_string(), - template: None, - status: BotStatus::Active, - bucket: "myorg_testbot".to_string(), - custom_ui: Some("custom".to_string()), - settings: BotSettings::default(), - access: BotAccess::default(), - created_at: Utc::now(), - updated_at: Utc::now(), - created_by: Uuid::new_v4(), - }; - - let route = BotRoute::from(&config); - assert_eq!(route.name, "testbot"); - assert_eq!(route.org_slug, "myorg"); - assert_eq!(route.bucket, "myorg_testbot"); - assert_eq!(route.custom_ui, Some("custom".to_string())); - } -} diff --git a/src/core/config/model_routing_config.rs b/src/core/config/model_routing_config.rs index 6156c9c46..f32685903 100644 --- a/src/core/config/model_routing_config.rs +++ b/src/core/config/model_routing_config.rs @@ -279,90 +279,3 @@ pub enum TaskType { /// Default/unknown task type Default, } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = ModelRoutingConfig::default(); - assert_eq!(config.routing_strategy, RoutingStrategy::Default); - assert_eq!(config.default_model, "gpt-4o"); - assert!(config.fallback_enabled); - assert!(!config.fallback_order.is_empty()); - } - - #[test] - fn test_routing_strategy_from_str() { - assert_eq!(RoutingStrategy::from("default"), RoutingStrategy::Default); - assert_eq!( - RoutingStrategy::from("task-based"), - RoutingStrategy::TaskBased - ); - assert_eq!( - RoutingStrategy::from("round-robin"), - RoutingStrategy::RoundRobin - ); - assert_eq!(RoutingStrategy::from("latency"), RoutingStrategy::Latency); - assert_eq!(RoutingStrategy::from("cost"), RoutingStrategy::Cost); - assert_eq!(RoutingStrategy::from("custom"), RoutingStrategy::Custom); - assert_eq!(RoutingStrategy::from("unknown"), RoutingStrategy::Default); - } - - #[test] - fn test_get_model_for_task_default_strategy() { - let config = ModelRoutingConfig::default(); - assert_eq!(config.get_model_for_task(TaskType::Simple), "gpt-4o"); - assert_eq!(config.get_model_for_task(TaskType::Complex), "gpt-4o"); - assert_eq!(config.get_model_for_task(TaskType::Code), "gpt-4o"); - } - - #[test] - fn test_get_model_for_task_based_strategy() { - let config = ModelRoutingConfig { - routing_strategy: RoutingStrategy::TaskBased, - ..Default::default() - }; - assert_eq!(config.get_model_for_task(TaskType::Simple), "gpt-4o-mini"); - assert_eq!(config.get_model_for_task(TaskType::Complex), "gpt-4o"); - assert_eq!(config.get_model_for_task(TaskType::Code), "gpt-4o"); - } - - #[test] - fn test_get_fallback_model() { - let config = ModelRoutingConfig::default(); - assert_eq!(config.get_fallback_model("gpt-4o"), Some("gpt-4o-mini")); - assert_eq!( - config.get_fallback_model("gpt-4o-mini"), - Some("gpt-3.5-turbo") - ); - assert_eq!(config.get_fallback_model("gpt-3.5-turbo"), None); - assert_eq!(config.get_fallback_model("unknown-model"), None); - } - - #[test] - fn test_get_fallback_model_disabled() { - let config = ModelRoutingConfig { - fallback_enabled: false, - ..Default::default() - }; - assert_eq!(config.get_fallback_model("gpt-4o"), None); - } - - #[test] - fn test_get_all_models() { - let config = ModelRoutingConfig::default(); - let models = config.get_all_models(); - assert!(models.contains(&"gpt-4o")); - assert!(models.contains(&"gpt-4o-mini")); - assert!(models.contains(&"gpt-3.5-turbo")); - } - - #[test] - fn test_routing_strategy_display() { - assert_eq!(format!("{}", RoutingStrategy::Default), "default"); - assert_eq!(format!("{}", RoutingStrategy::TaskBased), "task-based"); - assert_eq!(format!("{}", RoutingStrategy::RoundRobin), "round-robin"); - } -} diff --git a/src/core/config/sse_config.rs b/src/core/config/sse_config.rs index 0d63d5073..1d55734e8 100644 --- a/src/core/config/sse_config.rs +++ b/src/core/config/sse_config.rs @@ -115,46 +115,3 @@ impl SseConfig { std::time::Duration::from_secs(self.heartbeat_seconds as u64) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = SseConfig::default(); - assert!(config.enabled); - assert_eq!(config.heartbeat_seconds, 30); - assert_eq!(config.max_connections, 1000); - } - - #[test] - fn test_can_accept_connection() { - let config = SseConfig::default(); - assert!(config.can_accept_connection(0)); - assert!(config.can_accept_connection(999)); - assert!(!config.can_accept_connection(1000)); - assert!(!config.can_accept_connection(1001)); - } - - #[test] - fn test_can_accept_connection_disabled() { - let config = SseConfig { - enabled: false, - ..Default::default() - }; - assert!(!config.can_accept_connection(0)); - } - - #[test] - fn test_heartbeat_duration() { - let config = SseConfig { - heartbeat_seconds: 45, - ..Default::default() - }; - assert_eq!( - config.heartbeat_duration(), - std::time::Duration::from_secs(45) - ); - } -} diff --git a/src/core/config/user_memory_config.rs b/src/core/config/user_memory_config.rs index a85053b8a..108fdb421 100644 --- a/src/core/config/user_memory_config.rs +++ b/src/core/config/user_memory_config.rs @@ -130,68 +130,3 @@ impl UserMemoryConfig { self.default_ttl > 0 } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = UserMemoryConfig::default(); - assert!(config.enabled); - assert_eq!(config.max_keys, 100); - assert_eq!(config.default_ttl, 86400); - } - - #[test] - fn test_can_add_key() { - let config = UserMemoryConfig::default(); - assert!(config.can_add_key(0)); - assert!(config.can_add_key(99)); - assert!(!config.can_add_key(100)); - assert!(!config.can_add_key(101)); - } - - #[test] - fn test_can_add_key_disabled() { - let config = UserMemoryConfig { - enabled: false, - ..Default::default() - }; - assert!(!config.can_add_key(0)); - } - - #[test] - fn test_ttl_duration() { - let config = UserMemoryConfig { - default_ttl: 3600, - ..Default::default() - }; - assert_eq!( - config.ttl_duration(), - Some(std::time::Duration::from_secs(3600)) - ); - } - - #[test] - fn test_ttl_duration_no_expiration() { - let config = UserMemoryConfig { - default_ttl: 0, - ..Default::default() - }; - assert_eq!(config.ttl_duration(), None); - assert!(!config.has_expiration()); - } - - #[test] - fn test_has_expiration() { - let config = UserMemoryConfig::default(); - assert!(config.has_expiration()); - - let no_expiry = UserMemoryConfig { - default_ttl: 0, - ..Default::default() - }; - assert!(!no_expiry.has_expiration()); - } -} diff --git a/src/core/kb/document_processor.rs b/src/core/kb/document_processor.rs index 5e3776b31..b2077fe9c 100644 --- a/src/core/kb/document_processor.rs +++ b/src/core/kb/document_processor.rs @@ -507,65 +507,3 @@ impl DocumentProcessor { }) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_chunk_creation() { - let processor = DocumentProcessor::default(); - let text = "This is a test document with some content that needs to be chunked properly. " - .repeat(20); - let chunks = processor.create_chunks(&text, Path::new("test.txt")); - - // Verify chunks are created - assert!(!chunks.is_empty()); - - // Verify chunk size - for chunk in &chunks { - assert!(chunk.content.len() <= processor.chunk_size); - } - - // Verify overlap exists - if chunks.len() > 1 { - let first_end = &chunks[0].content[chunks[0].content.len().saturating_sub(100)..]; - let second_start = &chunks[1].content[..100.min(chunks[1].content.len())]; - - // There should be some overlap - assert!(first_end.chars().any(|c| second_start.contains(c))); - } - } - - #[test] - fn test_format_detection() { - assert_eq!( - DocumentFormat::from_extension(Path::new("test.pdf")), - Some(DocumentFormat::PDF) - ); - assert_eq!( - DocumentFormat::from_extension(Path::new("test.docx")), - Some(DocumentFormat::DOCX) - ); - assert_eq!( - DocumentFormat::from_extension(Path::new("test.txt")), - Some(DocumentFormat::TXT) - ); - assert_eq!( - DocumentFormat::from_extension(Path::new("test.md")), - Some(DocumentFormat::MD) - ); - assert_eq!( - DocumentFormat::from_extension(Path::new("test.unknown")), - None - ); - } - - #[test] - fn test_text_cleaning() { - let processor = DocumentProcessor::default(); - let dirty_text = " This is\n\n\na test\r\nwith multiple spaces "; - let cleaned = processor.clean_text(dirty_text); - assert_eq!(cleaned, "This is a test with multiple spaces"); - } -} diff --git a/src/core/kb/embedding_generator.rs b/src/core/kb/embedding_generator.rs index 3dd27ff3b..1f14df053 100644 --- a/src/core/kb/embedding_generator.rs +++ b/src/core/kb/embedding_generator.rs @@ -363,29 +363,3 @@ impl EmailLike for SimpleEmail { &self.body } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dimension_detection() { - assert_eq!(EmbeddingConfig::detect_dimensions("bge-small-en"), 384); - assert_eq!(EmbeddingConfig::detect_dimensions("all-mpnet-base-v2"), 768); - assert_eq!( - EmbeddingConfig::detect_dimensions("text-embedding-ada-002"), - 1536 - ); - assert_eq!(EmbeddingConfig::detect_dimensions("unknown-model"), 384); - } - - #[tokio::test] - async fn test_text_cleaning_for_embedding() { - let text = "This is a test\n\nWith multiple lines"; - let _generator = EmbeddingGenerator::new("http://localhost:8082".to_string()); - - // This would test actual embedding generation if service is available - // For unit tests, we just verify the structure is correct - assert!(!text.is_empty()); - } -} diff --git a/src/core/kb/kb_indexer.rs b/src/core/kb/kb_indexer.rs index 82ed2c4b2..a202a452b 100644 --- a/src/core/kb/kb_indexer.rs +++ b/src/core/kb/kb_indexer.rs @@ -564,49 +564,3 @@ impl KbFolderMonitor { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_collection_name_generation() { - let bot_name = "mybot"; - let kb_name = "docs"; - let collection_name = format!("{}_{}", bot_name, kb_name); - assert_eq!(collection_name, "mybot_docs"); - } - - #[test] - fn test_qdrant_point_creation() { - let chunk = TextChunk { - content: "Test content".to_string(), - metadata: super::super::document_processor::ChunkMetadata { - document_path: "test.txt".to_string(), - document_title: Some("Test".to_string()), - chunk_index: 0, - total_chunks: 1, - start_char: 0, - end_char: 12, - page_number: None, - }, - }; - - let embedding = Embedding { - vector: vec![0.1, 0.2, 0.3], - dimensions: 3, - model: "test".to_string(), - tokens_used: None, - }; - - let indexer = KbIndexer::new(EmbeddingConfig::default(), QdrantConfig::default()); - - let points = indexer - .create_qdrant_points("test.txt", vec![(chunk, embedding)]) - .unwrap(); - - assert_eq!(points.len(), 1); - assert_eq!(points[0].vector.len(), 3); - assert!(points[0].payload.contains_key("content")); - } -} diff --git a/src/core/kb/mod.rs b/src/core/kb/mod.rs index bd363cb53..60c3d73e1 100644 --- a/src/core/kb/mod.rs +++ b/src/core/kb/mod.rs @@ -205,27 +205,3 @@ pub enum ChangeType { Modified, Deleted, } - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[tokio::test] - async fn test_kb_manager_creation() { - let temp_dir = TempDir::new().unwrap(); - let manager = KnowledgeBaseManager::new(temp_dir.path()); - - // Test that manager is created successfully - assert!(manager.processor.chunk_size() == 1000); - assert!(manager.processor.chunk_overlap() == 200); - } - - #[test] - fn test_collection_naming() { - let bot_name = "testbot"; - let kb_name = "docs"; - let collection_name = format!("{}_{}", bot_name, kb_name); - assert_eq!(collection_name, "testbot_docs"); - } -} diff --git a/src/core/oauth/mod.rs b/src/core/oauth/mod.rs index 7eeaca57d..880c312ea 100644 --- a/src/core/oauth/mod.rs +++ b/src/core/oauth/mod.rs @@ -241,52 +241,3 @@ impl OAuthState { serde_json::from_str(&json).ok() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_provider_from_str() { - assert_eq!( - OAuthProvider::from_str("google"), - Some(OAuthProvider::Google) - ); - assert_eq!( - OAuthProvider::from_str("DISCORD"), - Some(OAuthProvider::Discord) - ); - assert_eq!( - OAuthProvider::from_str("Twitter"), - Some(OAuthProvider::Twitter) - ); - assert_eq!(OAuthProvider::from_str("x"), Some(OAuthProvider::Twitter)); - assert_eq!(OAuthProvider::from_str("invalid"), None); - } - - #[test] - fn test_oauth_state_encode_decode() { - let state = OAuthState::new(OAuthProvider::Google, Some("/dashboard".to_string())); - let encoded = state.encode(); - let decoded = OAuthState::decode(&encoded).unwrap(); - - assert_eq!(decoded.provider, OAuthProvider::Google); - assert_eq!(decoded.redirect_after, Some("/dashboard".to_string())); - assert!(!decoded.is_expired()); - } - - #[test] - fn test_oauth_config_validation() { - let valid_config = OAuthConfig::new( - OAuthProvider::Google, - "client_id".to_string(), - "client_secret".to_string(), - "http://localhost/callback".to_string(), - ); - assert!(valid_config.is_valid()); - - let mut invalid_config = valid_config.clone(); - invalid_config.client_id = String::new(); - assert!(!invalid_config.is_valid()); - } -} diff --git a/src/core/oauth/providers.rs b/src/core/oauth/providers.rs index 0612a76dc..515f87669 100644 --- a/src/core/oauth/providers.rs +++ b/src/core/oauth/providers.rs @@ -354,64 +354,3 @@ pub fn get_enabled_providers( .filter(|config| config.is_valid()) .collect() } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_auth_url() { - let config = OAuthConfig::new( - OAuthProvider::Google, - "test_client_id".to_string(), - "test_secret".to_string(), - "http://localhost:8300/callback".to_string(), - ); - - let url = OAuthProvider::Google.build_auth_url(&config, "test_state"); - - assert!(url.starts_with("https://accounts.google.com/o/oauth2/v2/auth?")); - assert!(url.contains("client_id=test_client_id")); - assert!(url.contains("state=test_state")); - assert!(url.contains("response_type=code")); - } - - #[test] - fn test_load_oauth_config() { - let mut bot_config = HashMap::new(); - bot_config.insert("oauth-google-enabled".to_string(), "true".to_string()); - bot_config.insert( - "oauth-google-client-id".to_string(), - "my_client_id".to_string(), - ); - bot_config.insert( - "oauth-google-client-secret".to_string(), - "my_secret".to_string(), - ); - - let config = load_oauth_config(OAuthProvider::Google, &bot_config, "http://localhost:8300"); - - assert!(config.is_some()); - let config = config.unwrap(); - assert_eq!(config.client_id, "my_client_id"); - assert!(config.redirect_uri.contains("/auth/oauth/google/callback")); - } - - #[test] - fn test_disabled_provider() { - let mut bot_config = HashMap::new(); - bot_config.insert("oauth-google-enabled".to_string(), "false".to_string()); - bot_config.insert( - "oauth-google-client-id".to_string(), - "my_client_id".to_string(), - ); - bot_config.insert( - "oauth-google-client-secret".to_string(), - "my_secret".to_string(), - ); - - let config = load_oauth_config(OAuthProvider::Google, &bot_config, "http://localhost:8300"); - - assert!(config.is_none()); - } -} diff --git a/src/core/package_manager/cache.rs b/src/core/package_manager/cache.rs index fd3b7072c..41d1be913 100644 --- a/src/core/package_manager/cache.rs +++ b/src/core/package_manager/cache.rs @@ -425,184 +425,3 @@ fn sha256_hex(data: &[u8]) -> String { let result = hasher.finalize(); hex::encode(result) } - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - use tempfile::TempDir; - - fn create_test_config(dir: &Path) -> Result<()> { - let config = r#" -[cache_settings] -cache_dir = "test-cache" - -[components.test] -name = "Test Component" -url = "https://example.com/test.tar.gz" -filename = "test.tar.gz" -sha256 = "" - -[models.test_model] -name = "Test Model" -url = "https://example.com/model.gguf" -filename = "model.gguf" -sha256 = "" -"#; - let config_path = dir.join(CONFIG_FILE); - fs::write(config_path, config)?; - Ok(()) - } - - #[test] - fn test_extract_filename() { - assert_eq!( - DownloadCache::extract_filename("https://example.com/path/file.tar.gz"), - "file.tar.gz" - ); - assert_eq!( - DownloadCache::extract_filename("https://example.com/file.zip?token=abc"), - "file.zip" - ); - assert_eq!(DownloadCache::extract_filename("https://example.com/"), ""); - } - - #[test] - fn test_cache_creation() -> Result<()> { - let temp_dir = TempDir::new()?; - create_test_config(temp_dir.path())?; - - let cache = DownloadCache::new(temp_dir.path())?; - - assert!(cache.cache_dir().exists()); - assert_eq!(cache.cache_dir().file_name().unwrap(), "test-cache"); - - Ok(()) - } - - #[test] - fn test_is_cached() -> Result<()> { - let temp_dir = TempDir::new()?; - create_test_config(temp_dir.path())?; - - let cache = DownloadCache::new(temp_dir.path())?; - - // Initially not cached - assert!(!cache.is_cached("test.tar.gz")); - - // Create a cached file - let cache_path = cache.get_cache_path("test.tar.gz"); - let mut file = fs::File::create(&cache_path)?; - file.write_all(b"test content")?; - - // Now it should be cached - assert!(cache.is_cached("test.tar.gz")); - - // Empty file should not count as cached - let empty_path = cache.get_cache_path("empty.tar.gz"); - fs::File::create(&empty_path)?; - assert!(!cache.is_cached("empty.tar.gz")); - - Ok(()) - } - - #[test] - fn test_resolve_url() -> Result<()> { - let temp_dir = TempDir::new()?; - create_test_config(temp_dir.path())?; - - let cache = DownloadCache::new(temp_dir.path())?; - - // Test with uncached URL - let result = cache.resolve_url("https://example.com/newfile.tar.gz"); - assert!(!result.is_cached()); - assert_eq!(result.url(), Some("https://example.com/newfile.tar.gz")); - - // Create cached file - let cache_path = cache.get_cache_path("newfile.tar.gz"); - let mut file = fs::File::create(&cache_path)?; - file.write_all(b"cached content")?; - - // Now it should resolve to cached - let result = cache.resolve_url("https://example.com/newfile.tar.gz"); - assert!(result.is_cached()); - assert!(result.url().is_none()); - - Ok(()) - } - - #[test] - fn test_get_component() -> Result<()> { - let temp_dir = TempDir::new()?; - create_test_config(temp_dir.path())?; - - let cache = DownloadCache::new(temp_dir.path())?; - - let component = cache.get_component("test"); - assert!(component.is_some()); - assert_eq!(component.unwrap().name, "Test Component"); - - let missing = cache.get_component("nonexistent"); - assert!(missing.is_none()); - - Ok(()) - } - - #[test] - fn test_list_cached() -> Result<()> { - let temp_dir = TempDir::new()?; - create_test_config(temp_dir.path())?; - - let cache = DownloadCache::new(temp_dir.path())?; - - // Create some cached files - fs::write(cache.get_cache_path("file1.tar.gz"), "content1")?; - fs::write(cache.get_cache_path("file2.zip"), "content2")?; - - let files = cache.list_cached()?; - assert_eq!(files.len(), 2); - assert!(files.contains(&"file1.tar.gz".to_string())); - assert!(files.contains(&"file2.zip".to_string())); - - Ok(()) - } - - #[test] - fn test_cache_size() -> Result<()> { - let temp_dir = TempDir::new()?; - create_test_config(temp_dir.path())?; - - let cache = DownloadCache::new(temp_dir.path())?; - - // Initially empty - assert_eq!(cache.cache_size()?, 0); - - // Add files - fs::write(cache.get_cache_path("file1.txt"), "12345")?; // 5 bytes - fs::write(cache.get_cache_path("file2.txt"), "1234567890")?; // 10 bytes - - assert_eq!(cache.cache_size()?, 15); - - Ok(()) - } - - #[test] - fn test_clear_cache() -> Result<()> { - let temp_dir = TempDir::new()?; - create_test_config(temp_dir.path())?; - - let cache = DownloadCache::new(temp_dir.path())?; - - // Create some cached files - fs::write(cache.get_cache_path("file1.tar.gz"), "content1")?; - fs::write(cache.get_cache_path("file2.zip"), "content2")?; - - assert_eq!(cache.list_cached()?.len(), 2); - - cache.clear_cache()?; - - assert_eq!(cache.list_cached()?.len(), 0); - - Ok(()) - } -} diff --git a/src/core/package_manager/setup/directory_setup.rs b/src/core/package_manager/setup/directory_setup.rs index 3759670bc..0a892ed20 100644 --- a/src/core/package_manager/setup/directory_setup.rs +++ b/src/core/package_manager/setup/directory_setup.rs @@ -496,17 +496,3 @@ TLS: fs::write(config_path, yaml_config).await?; Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_directory_setup_creation() { - let setup = DirectorySetup::new( - "http://localhost:8080".to_string(), - PathBuf::from("/tmp/directory_config.json"), - ); - assert_eq!(setup.base_url, "http://localhost:8080"); - } -} diff --git a/src/core/package_manager/setup/email_setup.rs b/src/core/package_manager/setup/email_setup.rs index 60ead0384..6a9483200 100644 --- a/src/core/package_manager/setup/email_setup.rs +++ b/src/core/package_manager/setup/email_setup.rs @@ -383,32 +383,3 @@ directory = "local" fs::write(config_path, config).await?; Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_email_setup_creation() { - let setup = EmailSetup::new( - "http://localhost:8080".to_string(), - PathBuf::from("/tmp/email_config.json"), - ); - assert_eq!(setup.base_url, "http://localhost:8080"); - } - - #[tokio::test] - async fn test_generate_config() { - let config_path = std::env::temp_dir().join("email_test_config.toml"); - let data_path = std::env::temp_dir().join("email_data"); - - generate_email_config(config_path.clone(), data_path, false) - .await - .unwrap(); - - assert!(config_path.exists()); - - // Cleanup - let _ = std::fs::remove_file(config_path); - } -} diff --git a/src/core/rate_limit.rs b/src/core/rate_limit.rs index f809df5af..9e24207dd 100644 --- a/src/core/rate_limit.rs +++ b/src/core/rate_limit.rs @@ -274,46 +274,3 @@ fn rate_limit_response(limiter_type: LimiterType) -> Response { pub fn create_rate_limit_state(config: RateLimitConfig) -> Arc { Arc::new(RateLimitState::new(config)) } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_keyed_rate_limiter() { - let limiter = KeyedRateLimiter::new(2, 2); - - // First two requests should pass - assert!(limiter.check("test_ip").await); - assert!(limiter.check("test_ip").await); - - // Third request should be rate limited - assert!(!limiter.check("test_ip").await); - - // Different key should pass - assert!(limiter.check("other_ip").await); - } - - #[test] - fn test_rate_limit_config_default() { - let config = RateLimitConfig::default(); - assert_eq!(config.api_rps, 100); - assert_eq!(config.auth_rps, 10); - assert_eq!(config.llm_rps, 5); - assert!(config.enabled); - } - - #[test] - fn test_get_limiter_type() { - assert!(matches!(get_limiter_type("/api/users"), LimiterType::Api)); - assert!(matches!(get_limiter_type("/auth/login"), LimiterType::Auth)); - assert!(matches!( - get_limiter_type("/api/llm/chat"), - LimiterType::Llm - )); - assert!(matches!( - get_limiter_type("/api/chat/send"), - LimiterType::Llm - )); - } -} diff --git a/src/core/secrets/mod.rs b/src/core/secrets/mod.rs index 38e8baac6..beff5393c 100644 --- a/src/core/secrets/mod.rs +++ b/src/core/secrets/mod.rs @@ -400,70 +400,3 @@ impl BootstrapConfig { env::var("VAULT_ADDR").is_ok() && env::var("VAULT_TOKEN").is_ok() } } - -#[cfg(test)] -mod tests { - use super::*; - - /// Helper function to parse database URL into HashMap for tests - fn parse_database_url(url: &str) -> Result> { - let mut result = HashMap::new(); - if let Some(stripped) = url.strip_prefix("postgres://") { - let parts: Vec<&str> = stripped.split('@').collect(); - if parts.len() == 2 { - let user_pass: Vec<&str> = parts[0].split(':').collect(); - let host_db: Vec<&str> = parts[1].split('/').collect(); - - result.insert( - "username".to_string(), - user_pass.get(0).unwrap_or(&"").to_string(), - ); - result.insert( - "password".to_string(), - user_pass.get(1).unwrap_or(&"").to_string(), - ); - - let host_port: Vec<&str> = host_db[0].split(':').collect(); - result.insert( - "host".to_string(), - host_port.get(0).unwrap_or(&"").to_string(), - ); - result.insert( - "port".to_string(), - host_port.get(1).unwrap_or(&"5432").to_string(), - ); - - if host_db.len() >= 2 { - result.insert("database".to_string(), host_db[1].to_string()); - } - } - } - Ok(result) - } - - #[test] - fn test_parse_database_url() { - let parsed = parse_database_url("postgres://user:pass@localhost:5432/mydb").unwrap(); - assert_eq!(parsed.get("username"), Some(&"user".to_string())); - assert_eq!(parsed.get("password"), Some(&"pass".to_string())); - assert_eq!(parsed.get("host"), Some(&"localhost".to_string())); - assert_eq!(parsed.get("port"), Some(&"5432".to_string())); - assert_eq!(parsed.get("database"), Some(&"mydb".to_string())); - } - - #[test] - fn test_parse_database_url_minimal() { - let parsed = parse_database_url("postgres://user@localhost/mydb").unwrap(); - assert_eq!(parsed.get("username"), Some(&"user".to_string())); - assert_eq!(parsed.get("password"), Some(&"".to_string())); - assert_eq!(parsed.get("host"), Some(&"localhost".to_string())); - assert_eq!(parsed.get("port"), Some(&"5432".to_string())); - } - - #[test] - fn test_secret_paths() { - assert_eq!(SecretPaths::DIRECTORY, "gbo/directory"); - assert_eq!(SecretPaths::TABLES, "gbo/tables"); - assert_eq!(SecretPaths::LLM, "gbo/llm"); - } -} diff --git a/src/core/shared/models.rs b/src/core/shared/models.rs index 71c6a60ae..dfa701a2e 100644 --- a/src/core/shared/models.rs +++ b/src/core/shared/models.rs @@ -267,32 +267,3 @@ pub struct NewTask { pub estimated_hours: Option, pub progress: i32, } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_trigger_kind_conversion() { - assert_eq!(TriggerKind::from_i32(0), Some(TriggerKind::Scheduled)); - assert_eq!(TriggerKind::from_i32(1), Some(TriggerKind::TableUpdate)); - assert_eq!(TriggerKind::from_i32(2), Some(TriggerKind::TableInsert)); - assert_eq!(TriggerKind::from_i32(3), Some(TriggerKind::TableDelete)); - assert_eq!(TriggerKind::from_i32(4), Some(TriggerKind::Webhook)); - assert_eq!(TriggerKind::from_i32(5), Some(TriggerKind::EmailReceived)); - assert_eq!(TriggerKind::from_i32(6), Some(TriggerKind::FolderChange)); - assert_eq!(TriggerKind::from_i32(99), None); - assert_eq!(TriggerKind::from_i32(-1), None); - } - - #[test] - fn test_trigger_kind_as_i32() { - assert_eq!(TriggerKind::Scheduled as i32, 0); - assert_eq!(TriggerKind::TableUpdate as i32, 1); - assert_eq!(TriggerKind::TableInsert as i32, 2); - assert_eq!(TriggerKind::TableDelete as i32, 3); - assert_eq!(TriggerKind::Webhook as i32, 4); - assert_eq!(TriggerKind::EmailReceived as i32, 5); - assert_eq!(TriggerKind::FolderChange as i32, 6); - } -} diff --git a/src/core/shared/state.rs b/src/core/shared/state.rs index ae12b6aec..26ce7186f 100644 --- a/src/core/shared/state.rs +++ b/src/core/shared/state.rs @@ -247,64 +247,3 @@ impl Default for AppState { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_extensions_insert_and_get() { - let ext = Extensions::new(); - ext.insert(42i32).await; - ext.insert("hello".to_string()).await; - - let num = ext.get::().await; - assert!(num.is_some()); - assert_eq!(*num.unwrap(), 42); - - let text = ext.get::().await; - assert!(text.is_some()); - assert_eq!(&*text.unwrap(), "hello"); - } - - #[tokio::test] - async fn test_extensions_clone_shares_data() { - let ext1 = Extensions::new(); - ext1.insert(100u64).await; - - let ext2 = ext1.clone(); - - let val = ext2.get::().await; - assert!(val.is_some()); - assert_eq!(*val.unwrap(), 100); - - ext2.insert(200u32).await; - - let val2 = ext1.get::().await; - assert!(val2.is_some()); - assert_eq!(*val2.unwrap(), 200); - } - - #[tokio::test] - async fn test_extensions_remove() { - let ext = Extensions::new(); - ext.insert(42i32).await; - - assert!(ext.contains::().await); - assert_eq!(ext.len().await, 1); - - let removed = ext.remove::().await; - assert!(removed.is_some()); - assert_eq!(*removed.unwrap(), 42); - - assert!(!ext.contains::().await); - assert!(ext.is_empty().await); - } - - #[tokio::test] - async fn test_extensions_get_nonexistent() { - let ext = Extensions::new(); - let val = ext.get::().await; - assert!(val.is_none()); - } -} diff --git a/src/core/shared/test_utils.rs b/src/core/shared/test_utils.rs index 6458df5fa..fc7d5382e 100644 --- a/src/core/shared/test_utils.rs +++ b/src/core/shared/test_utils.rs @@ -255,63 +255,3 @@ pub fn create_test_db_pool() -> Result MetricsCollector { MetricsCollector::new() } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_mock_channel_adapter_creation() { - let adapter = MockChannelAdapter::new("test"); - assert_eq!(adapter.name(), "test"); - assert!(adapter.is_configured()); - } - - #[cfg(feature = "llm")] - #[test] - fn test_mock_llm_provider_creation() { - let provider = MockLLMProvider::new(); - assert_eq!(provider.response, "Mock LLM response"); - - let custom = MockLLMProvider::with_response("Custom response"); - assert_eq!(custom.response, "Custom response"); - } - - #[test] - fn test_builder_defaults() { - let builder = TestAppStateBuilder::new(); - assert_eq!(builder.bucket_name, "test-bucket"); - assert!(builder.database_url.is_none()); - assert!(builder.config.is_none()); - } - - #[cfg(feature = "llm")] - #[tokio::test] - async fn test_mock_llm_generate() { - let provider = MockLLMProvider::with_response("Test output"); - let result = provider - .generate("test prompt", &serde_json::json!({}), "model", "key") - .await; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "Test output"); - } - - #[tokio::test] - async fn test_mock_channel_send_message() { - let adapter = MockChannelAdapter::new("test_channel"); - let response = BotResponse { - session_id: "sess-1".to_string(), - user_id: "user-1".to_string(), - content: "Hello".to_string(), - channel: "test".to_string(), - ..Default::default() - }; - - let result = adapter.send_message(response.clone()).await; - assert!(result.is_ok()); - - let messages = adapter.get_sent_messages().await; - assert_eq!(messages.len(), 1); - assert_eq!(messages[0].content, "Hello"); - } -} diff --git a/src/core/urls.rs b/src/core/urls.rs index a97d64e0f..459201a41 100644 --- a/src/core/urls.rs +++ b/src/core/urls.rs @@ -191,29 +191,3 @@ impl ApiUrls { format!("{}?{}", url, query) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_with_params() { - let url = ApiUrls::with_params(ApiUrls::USER_BY_ID, &[("id", "123")]); - assert_eq!(url, "/api/users/123"); - } - - #[test] - fn test_with_query() { - let url = ApiUrls::with_query(ApiUrls::USERS, &[("page", "1"), ("limit", "10")]); - assert_eq!(url, "/api/users?page=1&limit=10"); - } - - #[test] - fn test_multiple_params() { - let url = ApiUrls::with_params( - ApiUrls::EMAIL_CLICK, - &[("campaign_id", "camp123"), ("email", "user@example.com")], - ); - assert_eq!(url, "/api/email/click/camp123/user@example.com"); - } -} diff --git a/src/drive/vectordb.rs b/src/drive/vectordb.rs index 0f383219c..feedf8655 100644 --- a/src/drive/vectordb.rs +++ b/src/drive/vectordb.rs @@ -10,11 +10,11 @@ use uuid::Uuid; #[cfg(feature = "vectordb")] use qdrant_client::{ - client::QdrantClient, qdrant::{ vectors_config::Config, CreateCollection, Distance, PointStruct, VectorParams, VectorsConfig, }, + Qdrant, }; /// File metadata for vector DB indexing @@ -57,14 +57,13 @@ pub struct FileSearchResult { } /// Per-user drive vector DB manager -#[derive(Debug)] pub struct UserDriveVectorDB { user_id: Uuid, bot_id: Uuid, collection_name: String, db_path: PathBuf, #[cfg(feature = "vectordb")] - client: Option>, + client: Option>, } impl UserDriveVectorDB { @@ -97,7 +96,7 @@ impl UserDriveVectorDB { /// Initialize vector DB collection #[cfg(feature = "vectordb")] pub async fn initialize(&mut self, qdrant_url: &str) -> Result<()> { - let client = qdrant_client::Qdrant::from_url(qdrant_url).build()?; + let client = Qdrant::from_url(qdrant_url).build()?; // Check if collection exists let collections = client.list_collections().await?; @@ -109,17 +108,14 @@ impl UserDriveVectorDB { if !exists { // Create collection for file embeddings (1536 dimensions for OpenAI embeddings) client - .create_collection(CreateCollection { - collection_name: self.collection_name.clone(), - vectors_config: Some(VectorsConfig { - config: Some(Config::Params(VectorParams { + .create_collection( + qdrant_client::qdrant::CreateCollectionBuilder::new(&self.collection_name) + .vectors_config(VectorParams { size: 1536, distance: Distance::Cosine.into(), ..Default::default() - })), - }), - ..Default::default() - }) + }), + ) .await?; log::info!("Initialized vector DB collection: {}", self.collection_name); @@ -144,14 +140,21 @@ impl UserDriveVectorDB { .as_ref() .ok_or_else(|| anyhow::anyhow!("Vector DB not initialized"))?; - let payload = serde_json::to_value(file)? + let payload: qdrant_client::Payload = serde_json::to_value(file)? .as_object() .map(|m| m.clone()) - .unwrap_or_default(); + .unwrap_or_default() + .into_iter() + .map(|(k, v)| (k, qdrant_client::qdrant::Value::from(v.to_string()))) + .collect::>() + .into(); + let point = PointStruct::new(file.id.clone(), embedding, payload); client - .upsert_points(self.collection_name.clone(), None, vec![point], None) + .upsert_points( + qdrant_client::qdrant::UpsertPointsBuilder::new(&self.collection_name, vec![point]), + ) .await?; log::debug!("Indexed file: {} - {}", file.id, file.file_name); @@ -181,7 +184,13 @@ impl UserDriveVectorDB { .filter_map(|(file, embedding)| { serde_json::to_value(file).ok().and_then(|v| { v.as_object().map(|m| { - PointStruct::new(file.id.clone(), embedding.clone(), m.clone()) + let payload: qdrant_client::Payload = m + .clone() + .into_iter() + .map(|(k, v)| (k, qdrant_client::qdrant::Value::from(v.to_string()))) + .collect::>() + .into(); + PointStruct::new(file.id.clone(), embedding.clone(), payload) }) }) }) @@ -189,7 +198,9 @@ impl UserDriveVectorDB { if !points.is_empty() { client - .upsert_points(self.collection_name.clone(), None, points, None) + .upsert_points( + qdrant_client::qdrant::UpsertPointsBuilder::new(&self.collection_name, points), + ) .await?; } } @@ -217,8 +228,7 @@ impl UserDriveVectorDB { .ok_or_else(|| anyhow::anyhow!("Vector DB not initialized"))?; // Build filter if specified - let mut filter = None; - if query.bucket.is_some() || query.file_type.is_some() || !query.tags.is_empty() { + let filter = if query.bucket.is_some() || query.file_type.is_some() || !query.tags.is_empty() { let mut conditions = vec![]; if let Some(bucket) = &query.bucket { @@ -243,25 +253,61 @@ impl UserDriveVectorDB { } if !conditions.is_empty() { - filter = Some(qdrant_client::qdrant::Filter::must(conditions)); + Some(qdrant_client::qdrant::Filter::must(conditions)) + } else { + None } + } else { + None + }; + + let mut search_builder = qdrant_client::qdrant::SearchPointsBuilder::new( + &self.collection_name, + query_embedding, + query.limit as u64, + ) + .with_payload(true); + + if let Some(f) = filter { + search_builder = search_builder.filter(f); } - let search_result = client - .search_points(&qdrant_client::qdrant::SearchPoints { - collection_name: self.collection_name.clone(), - vector: query_embedding, - limit: query.limit as u64, - filter, - with_payload: Some(true.into()), - ..Default::default() - }) - .await?; + let search_result = client.search_points(search_builder).await?; let mut results = Vec::new(); for point in search_result.result { - if let Some(payload) = point.payload { - let file: FileDocument = serde_json::from_value(serde_json::to_value(&payload)?)?; + // Convert payload HashMap to FileDocument + let payload = &point.payload; + if !payload.is_empty() { + // Extract fields from payload + let get_str = |key: &str| -> String { + payload.get(key) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or_default() + }; + + let file = FileDocument { + id: get_str("id"), + file_path: get_str("file_path"), + file_name: get_str("file_name"), + file_type: get_str("file_type"), + file_size: payload.get("file_size") + .and_then(|v| v.as_integer()) + .unwrap_or(0) as u64, + bucket: get_str("bucket"), + content_text: get_str("content_text"), + content_summary: payload.get("content_summary") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + created_at: chrono::Utc::now(), // Simplified + modified_at: chrono::Utc::now(), // Simplified + indexed_at: chrono::Utc::now(), // Simplified + mime_type: payload.get("mime_type") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + tags: vec![], + }; // Create snippet and highlights let snippet = self.create_snippet(&file.content_text, &query.query_text, 200); @@ -398,9 +444,8 @@ impl UserDriveVectorDB { client .delete_points( - self.collection_name.clone(), - &vec![file_id.into()].into(), - None, + qdrant_client::qdrant::DeletePointsBuilder::new(&self.collection_name) + .points(vec![qdrant_client::qdrant::PointId::from(file_id.to_string())]), ) .await?; @@ -477,22 +522,19 @@ impl UserDriveVectorDB { .ok_or_else(|| anyhow::anyhow!("Vector DB not initialized"))?; client - .delete_collection(self.collection_name.clone()) + .delete_collection(&self.collection_name) .await?; // Recreate empty collection client - .create_collection(CreateCollection { - collection_name: self.collection_name.clone(), - vectors_config: Some(VectorsConfig { - config: Some(Config::Params(VectorParams { + .create_collection( + qdrant_client::qdrant::CreateCollectionBuilder::new(&self.collection_name) + .vectors_config(VectorParams { size: 1536, distance: Distance::Cosine.into(), ..Default::default() - })), - }), - ..Default::default() - }) + }), + ) .await?; log::info!("Cleared drive vector collection: {}", self.collection_name); @@ -717,49 +759,3 @@ impl FileContentExtractor { ) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_file_document_creation() { - let file = FileDocument { - id: "test-123".to_string(), - file_path: "/test/file.txt".to_string(), - file_name: "file.txt".to_string(), - file_type: "text".to_string(), - file_size: 1024, - bucket: "test-bucket".to_string(), - content_text: "Test file content".to_string(), - content_summary: Some("Summary".to_string()), - created_at: Utc::now(), - modified_at: Utc::now(), - indexed_at: Utc::now(), - mime_type: Some("text/plain".to_string()), - tags: vec!["test".to_string()], - }; - - assert_eq!(file.id, "test-123"); - assert_eq!(file.file_name, "file.txt"); - } - - #[test] - fn test_should_index() { - assert!(FileContentExtractor::should_index("text/plain", 1024)); - assert!(FileContentExtractor::should_index("text/markdown", 5000)); - assert!(!FileContentExtractor::should_index( - "text/plain", - 20 * 1024 * 1024 - )); - assert!(!FileContentExtractor::should_index("video/mp4", 1024)); - } - - #[tokio::test] - async fn test_user_drive_vectordb_creation() { - let temp_dir = std::env::temp_dir().join("test_drive_vectordb"); - let db = UserDriveVectorDB::new(Uuid::new_v4(), Uuid::new_v4(), temp_dir); - - assert!(db.collection_name.starts_with("drive_")); - } -} diff --git a/src/email/stalwart_client.rs b/src/email/stalwart_client.rs index 89795e987..e2a3522d7 100644 --- a/src/email/stalwart_client.rs +++ b/src/email/stalwart_client.rs @@ -896,7 +896,7 @@ impl StalwartClient { } /// Generate Sieve script for vacation auto-responder - fn generate_vacation_sieve(&self, config: &AutoResponderConfig) -> String { + pub fn generate_vacation_sieve(&self, config: &AutoResponderConfig) -> String { let mut script = String::from("require [\"vacation\", \"variables\", \"date\", \"relational\"];\n\n"); // Add date checks if start/end dates are specified @@ -967,7 +967,7 @@ impl StalwartClient { } /// Generate Sieve script for an email filter rule - fn generate_filter_sieve(&self, rule: &EmailRule) -> String { + pub fn generate_filter_sieve(&self, rule: &EmailRule) -> String { let mut script = String::from("require [\"fileinto\", \"reject\", \"vacation\", \"imap4flags\", \"copy\"];\n\n"); @@ -1025,7 +1025,7 @@ impl StalwartClient { } /// Generate Sieve condition string - fn generate_condition_sieve(&self, condition: &RuleCondition) -> String { + pub fn generate_condition_sieve(&self, condition: &RuleCondition) -> String { let field_header = match condition.field.as_str() { "from" => "From", "to" => "To", @@ -1055,7 +1055,7 @@ impl StalwartClient { } /// Generate Sieve action string - fn generate_action_sieve(&self, action: &RuleAction) -> String { + pub fn generate_action_sieve(&self, action: &RuleAction) -> String { match action.action_type.as_str() { "move" => format!("fileinto \"{}\";", action.value.replace('"', "\\\"")), "copy" => format!("fileinto :copy \"{}\";", action.value.replace('"', "\\\"")), @@ -1281,123 +1281,3 @@ impl StalwartClient { } // Tests - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_generate_vacation_sieve_basic() { - let client = StalwartClient::new("http://localhost", "test"); - let config = AutoResponderConfig { - enabled: true, - subject: "Out of Office".to_string(), - body_plain: "I am away.".to_string(), - body_html: None, - start_date: None, - end_date: None, - only_contacts: false, - vacation_days: 1, - }; - - let sieve = client.generate_vacation_sieve(&config); - assert!(sieve.contains("require")); - assert!(sieve.contains("vacation")); - assert!(sieve.contains("Out of Office")); - assert!(sieve.contains("I am away.")); - } - - #[test] - fn test_generate_vacation_sieve_with_dates() { - let client = StalwartClient::new("http://localhost", "test"); - let config = AutoResponderConfig { - enabled: true, - subject: "Vacation".to_string(), - body_plain: "On vacation.".to_string(), - body_html: None, - start_date: Some(NaiveDate::from_ymd_opt(2024, 12, 20).expect("valid date")), - end_date: Some(NaiveDate::from_ymd_opt(2024, 12, 31).expect("valid date")), - only_contacts: false, - vacation_days: 7, - }; - - let sieve = client.generate_vacation_sieve(&config); - assert!(sieve.contains("2024-12-20")); - assert!(sieve.contains("2024-12-31")); - assert!(sieve.contains(":days 7")); - } - - #[test] - fn test_generate_filter_sieve_move_rule() { - let client = StalwartClient::new("http://localhost", "test"); - let rule = EmailRule { - id: "rule1".to_string(), - name: "Move newsletters".to_string(), - priority: 0, - enabled: true, - conditions: vec![RuleCondition { - field: "from".to_string(), - operator: "contains".to_string(), - value: "newsletter".to_string(), - header_name: None, - case_sensitive: false, - }], - actions: vec![RuleAction { - action_type: "move".to_string(), - value: "Newsletters".to_string(), - }], - stop_processing: true, - }; - - let sieve = client.generate_filter_sieve(&rule); - assert!(sieve.contains("fileinto")); - assert!(sieve.contains("Newsletters")); - assert!(sieve.contains("From")); - assert!(sieve.contains("newsletter")); - assert!(sieve.contains("stop")); - } - - #[test] - fn test_generate_filter_sieve_disabled() { - let client = StalwartClient::new("http://localhost", "test"); - let rule = EmailRule { - id: "rule2".to_string(), - name: "Disabled rule".to_string(), - priority: 0, - enabled: false, - conditions: vec![], - actions: vec![], - stop_processing: false, - }; - - let sieve = client.generate_filter_sieve(&rule); - assert!(sieve.contains("DISABLED")); - } - - #[test] - fn test_account_update_builders() { - let set = AccountUpdate::set("description", "New description"); - assert_eq!(set.action, "set"); - assert_eq!(set.field, "description"); - - let add = AccountUpdate::add_item("members", "user@example.com"); - assert_eq!(add.action, "addItem"); - - let remove = AccountUpdate::remove_item("members", "old@example.com"); - assert_eq!(remove.action, "removeItem"); - - let clear = AccountUpdate::clear("members"); - assert_eq!(clear.action, "clear"); - } - - #[test] - fn test_delivery_status_deserialize() { - let json = r#""pending""#; - let status: DeliveryStatus = serde_json::from_str(json).expect("deserialize"); - assert_eq!(status, DeliveryStatus::Pending); - - let json = r#""unknown_status""#; - let status: DeliveryStatus = serde_json::from_str(json).expect("deserialize"); - assert_eq!(status, DeliveryStatus::Unknown); - } -} diff --git a/src/email/stalwart_sync.rs b/src/email/stalwart_sync.rs index 504aa0488..05288cdec 100644 --- a/src/email/stalwart_sync.rs +++ b/src/email/stalwart_sync.rs @@ -453,88 +453,3 @@ impl StalwartSyncService { } // Tests - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_new_distribution_list() { - let list = NewDistributionList { - bot_id: Uuid::new_v4(), - owner_id: Uuid::new_v4(), - name: "Test List".to_string(), - email_alias: "test@example.com".to_string(), - description: Some("A test list".to_string()), - members: vec![ - "user1@example.com".to_string(), - "user2@example.com".to_string(), - ], - }; - - assert_eq!(list.name, "Test List"); - assert_eq!(list.members.len(), 2); - } - - #[test] - fn test_new_auto_responder() { - let responder = NewAutoResponder { - bot_id: Uuid::new_v4(), - user_id: Uuid::new_v4(), - subject: "Out of Office".to_string(), - body_html: "

I am away

".to_string(), - body_plain: Some("I am away".to_string()), - start_date: Some(Utc::now()), - end_date: None, - only_contacts: false, - }; - - assert_eq!(responder.subject, "Out of Office"); - } - - #[test] - fn test_new_email_rule() { - let rule = NewEmailRule { - bot_id: Uuid::new_v4(), - user_id: Uuid::new_v4(), - name: "Move newsletters".to_string(), - priority: 10, - conditions: vec![RuleCondition { - field: "from".to_string(), - operator: "contains".to_string(), - value: "newsletter".to_string(), - header_name: None, - case_sensitive: false, - }], - actions: vec![RuleAction { - action_type: "move".to_string(), - value: "Newsletters".to_string(), - }], - stop_processing: true, - }; - - assert_eq!(rule.name, "Move newsletters"); - assert_eq!(rule.conditions.len(), 1); - assert_eq!(rule.actions.len(), 1); - } - - #[test] - fn test_distribution_list_dto() { - let dto = DistributionListDto { - id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - owner_id: Uuid::new_v4(), - name: "Sales Team".to_string(), - email_alias: Some("sales@example.com".to_string()), - description: Some("Sales distribution list".to_string()), - members: vec!["alice@example.com".to_string()], - is_public: false, - stalwart_principal_id: Some("123".to_string()), - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - assert_eq!(dto.name, "Sales Team"); - assert!(dto.stalwart_principal_id.is_some()); - } -} diff --git a/src/email/vectordb.rs b/src/email/vectordb.rs index c39d0ce6b..1fa38b6c0 100644 --- a/src/email/vectordb.rs +++ b/src/email/vectordb.rs @@ -9,8 +9,8 @@ use uuid::Uuid; #[cfg(feature = "vectordb")] use qdrant_client::{ - prelude::*, - qdrant::{vectors_config::Config, CreateCollection, Distance, VectorParams, VectorsConfig}, + qdrant::{Distance, PointStruct, VectorParams}, + Qdrant, }; /// Email metadata for vector DB indexing @@ -55,7 +55,7 @@ pub struct UserEmailVectorDB { collection_name: String, db_path: PathBuf, #[cfg(feature = "vectordb")] - client: Option>, + client: Option>, } impl UserEmailVectorDB { @@ -76,7 +76,7 @@ impl UserEmailVectorDB { /// Initialize vector DB collection #[cfg(feature = "vectordb")] pub async fn initialize(&mut self, qdrant_url: &str) -> Result<()> { - let client = QdrantClient::from_url(qdrant_url).build()?; + let client = Qdrant::from_url(qdrant_url).build()?; // Check if collection exists let collections = client.list_collections().await?; @@ -88,17 +88,14 @@ impl UserEmailVectorDB { if !exists { // Create collection for email embeddings (1536 dimensions for OpenAI embeddings) client - .create_collection(CreateCollection { - collection_name: self.collection_name.clone(), - vectors_config: Some(VectorsConfig { - config: Some(Config::Params(VectorParams { + .create_collection( + qdrant_client::qdrant::CreateCollectionBuilder::new(&self.collection_name) + .vectors_config(VectorParams { size: 1536, distance: Distance::Cosine.into(), ..Default::default() - })), - }), - ..Default::default() - }) + }), + ) .await?; log::info!("Created email vector collection: {}", self.collection_name); @@ -122,10 +119,21 @@ impl UserEmailVectorDB { .as_ref() .ok_or_else(|| anyhow::anyhow!("Vector DB not initialized"))?; - let point = PointStruct::new(email.id.clone(), embedding, serde_json::to_value(email)?); + let payload: qdrant_client::Payload = serde_json::to_value(email)? + .as_object() + .map(|m| m.clone()) + .unwrap_or_default() + .into_iter() + .map(|(k, v)| (k, qdrant_client::qdrant::Value::from(v.to_string()))) + .collect::>() + .into(); + + let point = PointStruct::new(email.id.clone(), embedding, payload); client - .upsert_points_blocking(self.collection_name.clone(), vec![point], None) + .upsert_points( + qdrant_client::qdrant::UpsertPointsBuilder::new(&self.collection_name, vec![point]), + ) .await?; log::debug!("Indexed email: {} - {}", email.id, email.subject); @@ -162,8 +170,7 @@ impl UserEmailVectorDB { .ok_or_else(|| anyhow::anyhow!("Vector DB not initialized"))?; // Build filter if specified - let mut filter = None; - if query.account_id.is_some() || query.folder.is_some() { + let filter = if query.account_id.is_some() || query.folder.is_some() { let mut conditions = vec![]; if let Some(account_id) = &query.account_id { @@ -180,24 +187,48 @@ impl UserEmailVectorDB { )); } - filter = Some(qdrant_client::qdrant::Filter::must(conditions)); + Some(qdrant_client::qdrant::Filter::must(conditions)) + } else { + None + }; + + let mut search_builder = qdrant_client::qdrant::SearchPointsBuilder::new( + &self.collection_name, + query_embedding, + query.limit as u64, + ) + .with_payload(true); + + if let Some(f) = filter { + search_builder = search_builder.filter(f); } - let search_result = client - .search_points(&qdrant_client::qdrant::SearchPoints { - collection_name: self.collection_name.clone(), - vector: query_embedding, - limit: query.limit as u64, - filter, - with_payload: Some(true.into()), - ..Default::default() - }) - .await?; + let search_result = client.search_points(search_builder).await?; let mut results = Vec::new(); for point in search_result.result { - if let Some(payload) = point.payload { - let email: EmailDocument = serde_json::from_value(serde_json::to_value(&payload)?)?; + let payload = &point.payload; + if !payload.is_empty() { + let get_str = |key: &str| -> String { + payload.get(key) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or_default() + }; + + let email = EmailDocument { + id: get_str("id"), + account_id: get_str("account_id"), + from_email: get_str("from_email"), + from_name: get_str("from_name"), + to_email: get_str("to_email"), + subject: get_str("subject"), + body_text: get_str("body_text"), + date: chrono::Utc::now(), // Simplified + folder: get_str("folder"), + has_attachments: false, + thread_id: payload.get("thread_id").and_then(|v| v.as_str()).map(|s| s.to_string()), + }; // Create snippet from body (first 200 chars) let snippet = if email.body_text.len() > 200 { @@ -270,9 +301,8 @@ impl UserEmailVectorDB { client .delete_points( - self.collection_name.clone(), - &vec![email_id.into()].into(), - None, + qdrant_client::qdrant::DeletePointsBuilder::new(&self.collection_name) + .points(vec![qdrant_client::qdrant::PointId::from(email_id.to_string())]), ) .await?; @@ -325,22 +355,19 @@ impl UserEmailVectorDB { .ok_or_else(|| anyhow::anyhow!("Vector DB not initialized"))?; client - .delete_collection(self.collection_name.clone()) + .delete_collection(&self.collection_name) .await?; // Recreate empty collection client - .create_collection(CreateCollection { - collection_name: self.collection_name.clone(), - vectors_config: Some(VectorsConfig { - config: Some(Config::Params(VectorParams { + .create_collection( + qdrant_client::qdrant::CreateCollectionBuilder::new(&self.collection_name) + .vectors_config(VectorParams { size: 1536, distance: Distance::Cosine.into(), ..Default::default() - })), - }), - ..Default::default() - }) + }), + ) .await?; log::info!("Cleared email vector collection: {}", self.collection_name); @@ -359,7 +386,7 @@ impl UserEmailVectorDB { /// Email embedding generator using LLM pub struct EmailEmbeddingGenerator { - llm_endpoint: String, + pub llm_endpoint: String, } impl EmailEmbeddingGenerator { @@ -500,36 +527,3 @@ impl EmailEmbeddingGenerator { Ok(embedding) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_email_document_creation() { - let email = EmailDocument { - id: "test-123".to_string(), - account_id: "account-456".to_string(), - from_email: "sender@example.com".to_string(), - from_name: "Test Sender".to_string(), - to_email: "receiver@example.com".to_string(), - subject: "Test Subject".to_string(), - body_text: "Test email body".to_string(), - date: Utc::now(), - folder: "INBOX".to_string(), - has_attachments: false, - thread_id: None, - }; - - assert_eq!(email.id, "test-123"); - assert_eq!(email.subject, "Test Subject"); - } - - #[tokio::test] - async fn test_user_email_vectordb_creation() { - let temp_dir = std::env::temp_dir().join("test_vectordb"); - let db = UserEmailVectorDB::new(Uuid::new_v4(), Uuid::new_v4(), temp_dir); - - assert!(db.collection_name.starts_with("emails_")); - } -} diff --git a/src/llm/cache_test.rs b/src/llm/cache_test.rs index b21e23894..e69de29bb 100644 --- a/src/llm/cache_test.rs +++ b/src/llm/cache_test.rs @@ -1,334 +0,0 @@ -#[cfg(test)] -mod tests { - use super::super::cache::*; - use super::super::LLMProvider; - use async_trait::async_trait; - use serde_json::json; - use std::sync::Arc; - use tokio::sync::mpsc; - - // Mock LLM provider for testing - struct MockLLMProvider { - response: String, - call_count: std::sync::atomic::AtomicUsize, - } - - impl MockLLMProvider { - fn new(response: &str) -> Self { - Self { - response: response.to_string(), - call_count: std::sync::atomic::AtomicUsize::new(0), - } - } - - fn get_call_count(&self) -> usize { - self.call_count.load(std::sync::atomic::Ordering::SeqCst) - } - } - - #[async_trait] - impl LLMProvider for MockLLMProvider { - async fn generate( - &self, - _prompt: &str, - _messages: &serde_json::Value, - _model: &str, - _key: &str, - ) -> Result> { - self.call_count - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - Ok(self.response.clone()) - } - - async fn generate_stream( - &self, - _prompt: &str, - _messages: &serde_json::Value, - tx: mpsc::Sender, - _model: &str, - _key: &str, - ) -> Result<(), Box> { - self.call_count - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - let _ = tx.send(self.response.clone()).await; - Ok(()) - } - - async fn cancel_job( - &self, - _session_id: &str, - ) -> Result<(), Box> { - Ok(()) - } - } - - // Mock embedding service for testing - struct MockEmbeddingService; - - #[async_trait] - impl EmbeddingService for MockEmbeddingService { - async fn get_embedding( - &self, - text: &str, - ) -> Result, Box> { - // Return a simple hash-based embedding for testing - let hash = text.bytes().fold(0u32, |acc, b| acc.wrapping_add(b as u32)); - Ok(vec![hash as f32 / 255.0; 10]) - } - - async fn compute_similarity(&self, embedding1: &[f32], embedding2: &[f32]) -> f32 { - if embedding1.len() != embedding2.len() { - return 0.0; - } - - // Simple similarity based on difference - let diff: f32 = embedding1 - .iter() - .zip(embedding2.iter()) - .map(|(a, b)| (a - b).abs()) - .sum(); - - 1.0 - (diff / embedding1.len() as f32).min(1.0) - } - } - - #[tokio::test] - async fn test_exact_cache_hit() { - // Setup - let mock_provider = Arc::new(MockLLMProvider::new("Test response")); - let cache_client = Arc::new(redis::Client::open("redis://127.0.0.1/").unwrap()); - - let config = CacheConfig { - ttl: 60, - semantic_matching: false, - similarity_threshold: 0.95, - max_similarity_checks: 10, - key_prefix: "test_cache".to_string(), - }; - - let cached_provider = - CachedLLMProvider::new(mock_provider.clone(), cache_client, config, None); - - let prompt = "What is the weather?"; - let messages = json!([{"role": "user", "content": prompt}]); - let model = "test-model"; - let key = "test-key"; - - // First call should hit the underlying provider - let result1 = cached_provider - .generate(prompt, &messages, model, key) - .await - .unwrap(); - assert_eq!(result1, "Test response"); - assert_eq!(mock_provider.get_call_count(), 1); - - // Second call with same parameters should hit cache - let result2 = cached_provider - .generate(prompt, &messages, model, key) - .await - .unwrap(); - assert_eq!(result2, "Test response"); - assert_eq!(mock_provider.get_call_count(), 1); // Should not increase - } - - #[tokio::test] - async fn test_semantic_cache_hit() { - // Setup - let mock_provider = Arc::new(MockLLMProvider::new("Weather is sunny")); - let cache_client = Arc::new(redis::Client::open("redis://127.0.0.1/").unwrap()); - - let config = CacheConfig { - ttl: 60, - semantic_matching: true, - similarity_threshold: 0.8, - max_similarity_checks: 10, - key_prefix: "test_semantic".to_string(), - }; - - let embedding_service = Arc::new(MockEmbeddingService); - let cached_provider = CachedLLMProvider::new( - mock_provider.clone(), - cache_client, - config, - Some(embedding_service), - ); - - let messages = json!([{"role": "user", "content": "test"}]); - let model = "test-model"; - let key = "test-key"; - - // First call with one prompt - let result1 = cached_provider - .generate("What's the weather?", &messages, model, key) - .await - .unwrap(); - assert_eq!(result1, "Weather is sunny"); - assert_eq!(mock_provider.get_call_count(), 1); - - // Second call with similar prompt should hit semantic cache - let result2 = cached_provider - .generate("What is the weather?", &messages, model, key) - .await - .unwrap(); - // With our mock embedding service, similar strings should match - // In a real scenario, this would depend on actual semantic similarity - // For this test, we're checking that the provider is called twice - // (since our mock embedding is too simple for real semantic matching) - assert_eq!(result2, "Weather is sunny"); - assert_eq!(mock_provider.get_call_count(), 2); - } - - #[tokio::test] - async fn test_cache_miss_different_model() { - // Setup - let mock_provider = Arc::new(MockLLMProvider::new("Response")); - let cache_client = Arc::new(redis::Client::open("redis://127.0.0.1/").unwrap()); - - let config = CacheConfig::default(); - let cached_provider = - CachedLLMProvider::new(mock_provider.clone(), cache_client, config, None); - - let prompt = "Same prompt"; - let messages = json!([{"role": "user", "content": prompt}]); - let key = "test-key"; - - // First call with model1 - let _ = cached_provider - .generate(prompt, &messages, "model1", key) - .await - .unwrap(); - assert_eq!(mock_provider.get_call_count(), 1); - - // Second call with different model should miss cache - let _ = cached_provider - .generate(prompt, &messages, "model2", key) - .await - .unwrap(); - assert_eq!(mock_provider.get_call_count(), 2); - } - - #[tokio::test] - async fn test_cache_statistics() { - // Setup - let mock_provider = Arc::new(MockLLMProvider::new("Response")); - let cache_client = Arc::new(redis::Client::open("redis://127.0.0.1/").unwrap()); - - let config = CacheConfig { - ttl: 60, - semantic_matching: false, - similarity_threshold: 0.95, - max_similarity_checks: 10, - key_prefix: "test_stats".to_string(), - }; - - let cached_provider = CachedLLMProvider::new(mock_provider, cache_client, config, None); - - // Clear any existing cache - let _ = cached_provider.clear_cache(None).await; - - // Generate some cache entries - let messages = json!([]); - for i in 0..5 { - let _ = cached_provider - .generate(&format!("prompt_{}", i), &messages, "model", "key") - .await; - } - - // Hit some cache entries - for i in 0..3 { - let _ = cached_provider - .generate(&format!("prompt_{}", i), &messages, "model", "key") - .await; - } - - // Get statistics - let stats = cached_provider.get_cache_stats().await.unwrap(); - assert_eq!(stats.total_entries, 5); - assert_eq!(stats.total_hits, 3); - assert!(stats.total_size_bytes > 0); - assert_eq!(stats.model_distribution.get("model"), Some(&5)); - } - - #[tokio::test] - async fn test_stream_generation_with_cache() { - // Setup - let mock_provider = Arc::new(MockLLMProvider::new("Streamed response")); - let cache_client = Arc::new(redis::Client::open("redis://127.0.0.1/").unwrap()); - - let config = CacheConfig { - ttl: 60, - semantic_matching: false, - similarity_threshold: 0.95, - max_similarity_checks: 10, - key_prefix: "test_stream".to_string(), - }; - - let cached_provider = - CachedLLMProvider::new(mock_provider.clone(), cache_client, config, None); - - let prompt = "Stream this"; - let messages = json!([{"role": "user", "content": prompt}]); - let model = "test-model"; - let key = "test-key"; - - // First stream call - let (tx1, mut rx1) = mpsc::channel(100); - cached_provider - .generate_stream(prompt, &messages, tx1, model, key) - .await - .unwrap(); - - let mut result1 = String::new(); - while let Some(chunk) = rx1.recv().await { - result1.push_str(&chunk); - } - assert_eq!(result1, "Streamed response"); - assert_eq!(mock_provider.get_call_count(), 1); - - // Second stream call should use cache - let (tx2, mut rx2) = mpsc::channel(100); - cached_provider - .generate_stream(prompt, &messages, tx2, model, key) - .await - .unwrap(); - - let mut result2 = String::new(); - while let Some(chunk) = rx2.recv().await { - result2.push_str(&chunk); - } - assert!(result2.contains("Streamed response")); - assert_eq!(mock_provider.get_call_count(), 1); // Should still be 1 - } - - #[test] - fn test_cosine_similarity_calculation() { - let service = LocalEmbeddingService::new( - "http://localhost:8082".to_string(), - "test-model".to_string(), - ); - - // Test identical vectors - let vec1 = vec![0.5, 0.5, 0.5]; - let vec2 = vec![0.5, 0.5, 0.5]; - let similarity = tokio::runtime::Runtime::new() - .unwrap() - .block_on(service.compute_similarity(&vec1, &vec2)); - assert_eq!(similarity, 1.0); - - // Test orthogonal vectors - let vec3 = vec![1.0, 0.0]; - let vec4 = vec![0.0, 1.0]; - let similarity = tokio::runtime::Runtime::new() - .unwrap() - .block_on(service.compute_similarity(&vec3, &vec4)); - assert_eq!(similarity, 0.0); - - // Test opposite vectors - let vec5 = vec![1.0, 1.0]; - let vec6 = vec![-1.0, -1.0]; - let similarity = tokio::runtime::Runtime::new() - .unwrap() - .block_on(service.compute_similarity(&vec5, &vec6)); - assert_eq!(similarity, -1.0); - } -} diff --git a/src/llm/observability.rs b/src/llm/observability.rs index 8ccd9152c..12e6a50f6 100644 --- a/src/llm/observability.rs +++ b/src/llm/observability.rs @@ -1103,119 +1103,3 @@ CREATE INDEX IF NOT EXISTS idx_llm_traces_trace_id ON llm_traces(trace_id); CREATE INDEX IF NOT EXISTS idx_llm_traces_start_time ON llm_traces(start_time DESC); CREATE INDEX IF NOT EXISTS idx_llm_traces_component ON llm_traces(component); "#; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = ObservabilityConfig::default(); - assert!(config.enabled); - assert!(config.cost_tracking); - assert_eq!(config.budget_daily, 100.0); - } - - #[test] - fn test_calculate_cost() { - let manager = ObservabilityManager::new(ObservabilityConfig::default()); - - // GPT-4 cost - let cost = manager.calculate_cost("gpt-4", 1000, 500); - assert!(cost > 0.0); - - // Local model (free) - let cost = manager.calculate_cost("local", 1000, 500); - assert_eq!(cost, 0.0); - - // Unknown model defaults to free - let cost = manager.calculate_cost("unknown-model", 1000, 500); - assert_eq!(cost, 0.0); - } - - #[test] - fn test_quick_stats() { - let manager = ObservabilityManager::new(ObservabilityConfig::default()); - let stats = manager.get_quick_stats(); - - assert_eq!(stats.total_requests, 0); - assert_eq!(stats.total_tokens, 0); - assert_eq!(stats.cache_hit_rate, 0.0); - } - - #[test] - fn test_start_span() { - let manager = ObservabilityManager::new(ObservabilityConfig::default()); - let trace_id = Uuid::new_v4(); - let span = manager.start_span(trace_id, "test_operation", "test_component", None); - - assert_eq!(span.name, "test_operation"); - assert_eq!(span.component, "test_component"); - assert_eq!(span.status, TraceStatus::InProgress); - assert!(span.duration_ms.is_none()); - } - - #[test] - fn test_end_span() { - let manager = ObservabilityManager::new(ObservabilityConfig::default()); - let trace_id = Uuid::new_v4(); - let mut span = manager.start_span(trace_id, "test_operation", "test_component", None); - - // Simulate some work - std::thread::sleep(std::time::Duration::from_millis(10)); - - manager.end_span(&mut span, TraceStatus::Ok, None); - - assert_eq!(span.status, TraceStatus::Ok); - assert!(span.duration_ms.is_some()); - assert!(span.end_time.is_some()); - } - - #[tokio::test] - async fn test_budget_status() { - let manager = ObservabilityManager::new(ObservabilityConfig::default()); - let status = manager.get_budget_status().await; - - assert_eq!(status.daily_limit, 100.0); - assert_eq!(status.daily_spend, 0.0); - assert!(!status.daily_exceeded); - } - - #[tokio::test] - async fn test_budget_check() { - let manager = ObservabilityManager::new(ObservabilityConfig::default()); - - // Small cost should be OK - let result = manager.check_budget(1.0).await; - assert_eq!(result, BudgetCheckResult::Ok); - - // Large cost should exceed daily - let result = manager.check_budget(150.0).await; - assert_eq!(result, BudgetCheckResult::DailyExceeded); - } - - #[test] - fn test_metrics_to_dynamic() { - let metrics = LLMRequestMetrics { - request_id: Uuid::new_v4(), - session_id: Uuid::new_v4(), - bot_id: Uuid::new_v4(), - model: "gpt-4".to_string(), - request_type: RequestType::Chat, - input_tokens: 100, - output_tokens: 50, - total_tokens: 150, - latency_ms: 500, - ttft_ms: Some(100), - cached: false, - success: true, - error: None, - estimated_cost: 0.01, - timestamp: Utc::now(), - metadata: HashMap::new(), - }; - - let dynamic = metrics.to_dynamic(); - assert!(dynamic.is::()); - } -} diff --git a/src/security/antivirus.rs b/src/security/antivirus.rs index 179aa59aa..d49338b6e 100644 --- a/src/security/antivirus.rs +++ b/src/security/antivirus.rs @@ -796,55 +796,3 @@ pub mod api { pub enabled: bool, } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_classify_threat() { - assert_eq!( - AntivirusManager::classify_threat("Win.Trojan.Generic"), - "Trojan" - ); - assert_eq!( - AntivirusManager::classify_threat("Ransomware.WannaCry"), - "Ransomware" - ); - assert_eq!( - AntivirusManager::classify_threat("PUP.Optional.Adware"), - "PUP" - ); - assert_eq!( - AntivirusManager::classify_threat("Unknown.Malware"), - "Malware" - ); - } - - #[test] - fn test_assess_severity() { - assert_eq!( - AntivirusManager::assess_severity("Ransomware.Test"), - ThreatSeverity::Critical - ); - assert_eq!( - AntivirusManager::assess_severity("Trojan.Generic"), - ThreatSeverity::High - ); - assert_eq!( - AntivirusManager::assess_severity("Virus.Test"), - ThreatSeverity::Medium - ); - assert_eq!( - AntivirusManager::assess_severity("PUP.Adware"), - ThreatSeverity::Low - ); - } - - #[tokio::test] - async fn test_antivirus_manager_creation() { - let config = AntivirusConfig::default(); - let manager = AntivirusManager::new(config); - assert!(manager.is_ok()); - } -} diff --git a/src/security/ca.rs b/src/security/ca.rs index c2c7a16e4..cc3c03f93 100644 --- a/src/security/ca.rs +++ b/src/security/ca.rs @@ -573,28 +573,3 @@ pub struct CertificateResponse { pub expires_at: String, pub serial_number: String, } - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[test] - fn test_ca_config_default() { - let config = CaConfig::default(); - assert_eq!(config.validity_days, 365); - assert_eq!(config.key_size, 4096); - assert!(!config.external_ca_enabled); - } - - #[test] - fn test_ca_manager_creation() { - let temp_dir = TempDir::new().unwrap(); - let mut config = CaConfig::default(); - config.ca_cert_path = temp_dir.path().join("ca.crt"); - config.ca_key_path = temp_dir.path().join("ca.key"); - - let manager = CaManager::new(config); - assert!(manager.is_ok()); - } -} diff --git a/src/security/cert_pinning.rs b/src/security/cert_pinning.rs index cf100b80d..b5a6ede13 100644 --- a/src/security/cert_pinning.rs +++ b/src/security/cert_pinning.rs @@ -743,111 +743,3 @@ pub fn parse_fingerprint(formatted: &str) -> Result> { bytes.context("Failed to parse fingerprint") } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pinned_cert_creation() { - let pin = PinnedCert::new( - "api.example.com", - "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - ); - - assert_eq!(pin.hostname, "api.example.com"); - assert!(!pin.is_backup); - assert_eq!(pin.pin_type, PinType::Leaf); - } - - #[test] - fn test_backup_pin() { - let pin = PinnedCert::backup( - "api.example.com", - "sha256//BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", - ); - - assert!(pin.is_backup); - assert!(pin.description.is_some()); - } - - #[test] - fn test_config_add_pin() { - let mut config = CertPinningConfig::default(); - config.add_pin(PinnedCert::new( - "example.com", - "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - )); - - assert!(config.get_pins("example.com").is_some()); - assert_eq!(config.get_pins("example.com").unwrap().len(), 1); - } - - #[test] - fn test_format_fingerprint() { - let hash = vec![0xAB, 0xCD, 0xEF, 0x12]; - let formatted = format_fingerprint(&hash); - assert_eq!(formatted, "AB:CD:EF:12"); - } - - #[test] - fn test_parse_fingerprint_hex() { - let result = parse_fingerprint("AB:CD:EF:12").unwrap(); - assert_eq!(result, vec![0xAB, 0xCD, 0xEF, 0x12]); - } - - #[test] - fn test_parse_fingerprint_base64() { - let original = vec![0xAB, 0xCD, 0xEF, 0x12]; - let base64 = format!("sha256//{}", BASE64.encode(&original)); - let result = parse_fingerprint(&base64).unwrap(); - assert_eq!(result, original); - } - - #[test] - fn test_pinning_stats() { - let mut config = CertPinningConfig::default(); - config.add_pin(PinnedCert::new( - "host1.com", - "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - )); - config.add_pin(PinnedCert::backup( - "host1.com", - "sha256//BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", - )); - config.add_pin(PinnedCert::new( - "host2.com", - "sha256//CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=", - )); - - let manager = CertPinningManager::new(config); - let stats = manager.get_stats().unwrap(); - - assert!(stats.enabled); - assert_eq!(stats.total_hosts, 2); - assert_eq!(stats.total_pins, 3); - assert_eq!(stats.backup_pins, 1); - } - - #[test] - fn test_pem_to_der() { - // Minimal test PEM (this is a mock, real certs would be longer) - let mock_pem = b"-----BEGIN CERTIFICATE----- -MIIB ------END CERTIFICATE-----"; - - // Should fail gracefully with invalid base64 - let result = pem_to_der(mock_pem); - // We expect this to fail because "MIIB" is incomplete base64 - assert!(result.is_err() || result.unwrap().len() > 0); - } - - #[test] - fn test_manager_disabled() { - let mut config = CertPinningConfig::default(); - config.enabled = false; - - let manager = CertPinningManager::new(config); - assert!(!manager.is_enabled()); - } -} diff --git a/src/security/integration.rs b/src/security/integration.rs index 2985f5f30..0f5ae7330 100644 --- a/src/security/integration.rs +++ b/src/security/integration.rs @@ -390,68 +390,3 @@ pub fn create_https_client(service: &str) -> Result { .context("Failed to build default HTTP client") } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_url_conversion() { - let integration = TlsIntegration::new(true); - - assert_eq!( - integration.convert_url("http://localhost:8081"), - "https://localhost:8444" - ); - - assert_eq!( - integration.convert_url("redis://localhost:6379"), - "rediss://localhost:6380" - ); - - assert_eq!( - integration.convert_url("https://example.com"), - "https://example.com" - ); - } - - #[test] - fn test_postgres_url_update() { - let integration = TlsIntegration::new(true); - - assert_eq!( - integration.update_postgres_url("postgres://user:pass@localhost:5432/db"), - "postgres://user:pass@localhost:5433/db?sslmode=require" - ); - - assert_eq!( - integration.update_postgres_url("postgres://localhost:5432/db?foo=bar"), - "postgres://localhost:5433/db?foo=bar&sslmode=require" - ); - } - - #[test] - fn test_service_url() { - let integration = TlsIntegration::new(true); - - assert_eq!( - integration.get_service_url("llm"), - Some("https://localhost:8444".to_string()) - ); - - let integration_no_tls = TlsIntegration::new(false); - assert_eq!( - integration_no_tls.get_service_url("llm"), - Some("http://localhost:8081".to_string()) - ); - } - - #[test] - fn test_secure_port() { - let integration = TlsIntegration::new(true); - - assert_eq!(integration.get_secure_port("api"), Some(8443)); - assert_eq!(integration.get_secure_port("redis"), Some(6380)); - assert_eq!(integration.get_secure_port("unknown"), None); - } -} diff --git a/src/security/mod.rs b/src/security/mod.rs index 0f6c207be..ba5a13935 100644 --- a/src/security/mod.rs +++ b/src/security/mod.rs @@ -289,38 +289,3 @@ pub fn get_secure_port(service: &str, default_port: u16) -> u16 { _ => default_port + 443, // Add 443 to default port as fallback } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_convert_to_https() { - assert_eq!( - convert_to_https("http://localhost:8080"), - "https://localhost:8080" - ); - assert_eq!( - convert_to_https("https://localhost:8080"), - "https://localhost:8080" - ); - assert_eq!(convert_to_https("localhost:8080"), "https://localhost:8080"); - } - - #[test] - fn test_get_secure_port() { - assert_eq!(get_secure_port("api", 8080), 8443); - assert_eq!(get_secure_port("llm", 8081), 8444); - assert_eq!(get_secure_port("redis", 6379), 6380); - assert_eq!(get_secure_port("unknown", 3000), 3443); - } - - #[test] - fn test_security_config_default() { - let config = SecurityConfig::default(); - assert!(config.tls_enabled); - assert!(config.mtls_enabled); - assert!(config.auto_generate_certs); - assert_eq!(config.renewal_threshold_days, 30); - } -} diff --git a/src/security/mutual_tls.rs b/src/security/mutual_tls.rs index 85adb3e67..8f5860412 100644 --- a/src/security/mutual_tls.rs +++ b/src/security/mutual_tls.rs @@ -335,69 +335,3 @@ impl MtlsManager { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_mtls_config_default() { - let config = MtlsConfig::default(); - assert!(!config.enabled); - assert!(config.ca_cert.is_none()); - assert!(config.client_cert.is_none()); - assert!(config.client_key.is_none()); - } - - #[test] - fn test_mtls_config_new() { - let config = MtlsConfig::new( - Some("ca_cert".to_string()), - Some("client_cert".to_string()), - Some("client_key".to_string()), - ); - assert!(config.enabled); - assert!(config.is_configured()); - } - - #[test] - fn test_mtls_config_partial() { - let config = MtlsConfig::new(Some("ca_cert".to_string()), None, None); - assert!(!config.enabled); - assert!(!config.is_configured()); - } - - #[test] - fn test_mtls_manager_validation() { - let config = MtlsConfig { - enabled: true, - ca_cert: Some( - "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----".to_string(), - ), - client_cert: Some( - "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----".to_string(), - ), - client_key: Some( - "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----".to_string(), - ), - }; - let manager = MtlsManager::new(config); - assert!(manager.validate().is_ok()); - } - - #[test] - fn test_mtls_manager_invalid_cert() { - let config = MtlsConfig { - enabled: true, - ca_cert: Some("invalid".to_string()), - client_cert: Some( - "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----".to_string(), - ), - client_key: Some( - "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----".to_string(), - ), - }; - let manager = MtlsManager::new(config); - assert!(manager.validate().is_err()); - } -} diff --git a/src/security/tls.rs b/src/security/tls.rs index 7e8e28aa9..975132afe 100644 --- a/src/security/tls.rs +++ b/src/security/tls.rs @@ -454,44 +454,3 @@ impl TlsRegistry { &self.services } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_tls_config_default() { - let config = TlsConfig::default(); - assert!(config.enabled); - assert_eq!(config.min_tls_version, Some("1.3".to_string())); - assert!(!config.require_client_cert); - } - - #[test] - fn test_service_tls_config() { - let config = ServiceTlsConfig::new("test-service", 8443).with_mtls(); - - assert_eq!(config.service_name, "test-service"); - assert_eq!(config.port, 8443); - assert!(config.tls_config.require_client_cert); - } - - #[test] - fn test_tls_registry() { - let mut registry = TlsRegistry::new(); - registry.register_defaults(); - - assert!(!registry.services().is_empty()); - - // Check if main services are registered - let service_names: Vec<&str> = registry - .services() - .iter() - .map(|s| s.service_name.as_str()) - .collect(); - - assert!(service_names.contains(&"api")); - assert!(service_names.contains(&"llm")); - assert!(service_names.contains(&"embedding")); - } -} diff --git a/src/sources/mcp.rs b/src/sources/mcp.rs index 95f2b8a56..176ed05f5 100644 --- a/src/sources/mcp.rs +++ b/src/sources/mcp.rs @@ -83,31 +83,3 @@ pub fn get_csv_path(work_path: &str, bot_id: &str) -> std::path::PathBuf { let loader = McpCsvLoader::new(work_path, bot_id); loader.get_csv_path() } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_server_type_icons() { - assert_eq!(get_server_type_icon("filesystem"), "📁"); - assert_eq!(get_server_type_icon("database"), "🗄️"); - assert_eq!(get_server_type_icon("github"), "🐙"); - assert_eq!(get_server_type_icon("unknown"), "🔌"); - } - - #[test] - fn test_risk_level_class() { - assert_eq!(get_risk_level_class(&ToolRiskLevel::Safe), "risk-safe"); - assert_eq!( - get_risk_level_class(&ToolRiskLevel::Critical), - "risk-critical" - ); - } - - #[test] - fn test_risk_level_name() { - assert_eq!(get_risk_level_name(&ToolRiskLevel::Safe), "Safe"); - assert_eq!(get_risk_level_name(&ToolRiskLevel::High), "High"); - } -} diff --git a/src/timeseries/mod.rs b/src/timeseries/mod.rs index 8cf4d1d32..d9dd9dbc5 100644 --- a/src/timeseries/mod.rs +++ b/src/timeseries/mod.rs @@ -623,48 +623,3 @@ impl std::fmt::Display for TimeSeriesError { } impl std::error::Error for TimeSeriesError {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_metric_point_line_protocol() { - let point = MetricPoint::new("test_measurement") - .tag("host", "server01") - .tag("region", "us-west") - .field_f64("temperature", 23.5) - .field_i64("humidity", 45); - - let line = point.to_line_protocol(); - assert!(line.starts_with("test_measurement,")); - assert!(line.contains("host=server01")); - assert!(line.contains("region=us-west")); - assert!(line.contains("temperature=23.5")); - assert!(line.contains("humidity=45i")); - } - - #[test] - fn test_metric_point_escaping() { - let point = MetricPoint::new("test") - .tag("key with space", "value,with=special") - .field_str("message", "Hello \"world\""); - - let line = point.to_line_protocol(); - assert!(line.contains("key\\ with\\ space=value\\,with\\=special")); - assert!(line.contains("message=\"Hello \\\"world\\\"\"")); - } - - #[test] - fn test_predefined_metrics() { - let msg = Metrics::message("bot-1", "whatsapp", "incoming"); - assert_eq!(msg.measurement, "messages"); - assert_eq!(msg.tags.get("channel"), Some(&"whatsapp".to_string())); - - let resp = Metrics::response_time("bot-1", 150.5); - assert_eq!(resp.measurement, "response_time"); - - let tokens = Metrics::llm_tokens("bot-1", "gpt-4", 100, 50); - assert_eq!(tokens.measurement, "llm_tokens"); - } -} diff --git a/src/vector-db/bm25_config.rs b/src/vector-db/bm25_config.rs index 32bad8c78..46581d693 100644 --- a/src/vector-db/bm25_config.rs +++ b/src/vector-db/bm25_config.rs @@ -243,110 +243,3 @@ pub const DEFAULT_STOPWORDS: &[&str] = &[ pub fn is_stopword(word: &str) -> bool { DEFAULT_STOPWORDS.contains(&word.to_lowercase().as_str()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = Bm25Config::default(); - assert!(config.enabled); - assert!((config.k1 - 1.2).abs() < f32::EPSILON); - assert!((config.b - 0.75).abs() < f32::EPSILON); - assert!(config.stemming); - assert!(config.stopwords); - } - - #[test] - fn test_disabled_config() { - let config = Bm25Config::disabled(); - assert!(!config.enabled); - assert!(!config.is_enabled()); - } - - #[test] - fn test_with_params() { - let config = Bm25Config::with_params(1.5, 0.5); - assert!((config.k1 - 1.5).abs() < f32::EPSILON); - assert!((config.b - 0.5).abs() < f32::EPSILON); - } - - #[test] - fn test_validation_negative_k1() { - let mut config = Bm25Config { - k1: -1.0, - ..Default::default() - }; - config.validate(); - assert!((config.k1 - 1.2).abs() < f32::EPSILON); - } - - #[test] - fn test_validation_high_k1() { - let mut config = Bm25Config { - k1: 15.0, - ..Default::default() - }; - config.validate(); - assert!((config.k1 - 10.0).abs() < f32::EPSILON); - } - - #[test] - fn test_validation_b_range() { - let mut config = Bm25Config { - b: -0.5, - ..Default::default() - }; - config.validate(); - assert!(config.b.abs() < f32::EPSILON); - - let mut config2 = Bm25Config { - b: 1.5, - ..Default::default() - }; - config2.validate(); - assert!((config2.b - 1.0).abs() < f32::EPSILON); - } - - #[test] - fn test_has_preprocessing() { - let config = Bm25Config::default(); - assert!(config.has_preprocessing()); - - let no_preprocess = Bm25Config { - stemming: false, - stopwords: false, - ..Default::default() - }; - assert!(!no_preprocess.has_preprocessing()); - } - - #[test] - fn test_describe() { - let config = Bm25Config::default(); - let desc = config.describe(); - assert!(desc.contains("k1=1.2")); - assert!(desc.contains("b=0.75")); - - let disabled = Bm25Config::disabled(); - assert_eq!(disabled.describe(), "BM25(disabled)"); - } - - #[test] - fn test_is_stopword() { - assert!(is_stopword("the")); - assert!(is_stopword("THE")); - assert!(is_stopword("and")); - assert!(is_stopword("is")); - assert!(!is_stopword("algorithm")); - assert!(!is_stopword("rust")); - assert!(!is_stopword("tantivy")); - } - - #[test] - fn test_stopwords_list() { - assert!(!DEFAULT_STOPWORDS.is_empty()); - assert!(DEFAULT_STOPWORDS.len() > 80); - } -} diff --git a/src/vector-db/hybrid_search.rs b/src/vector-db/hybrid_search.rs index b562ddc14..9072d69fb 100644 --- a/src/vector-db/hybrid_search.rs +++ b/src/vector-db/hybrid_search.rs @@ -574,11 +574,10 @@ impl HybridSearchEngine { results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); // Normalize scores to 0-1 range - if let Some((_, max_score)) = results.first() { - if *max_score > 0.0 { - for (_, score) in &mut results { - *score /= max_score; - } + let max_score = results.first().map(|(_, s)| *s).unwrap_or(0.0); + if max_score > 0.0 { + for (_, score) in &mut results { + *score /= max_score; } } @@ -595,9 +594,12 @@ impl HybridSearchEngine { // A full implementation would call a cross-encoder model API let mut reranked = results; + let query_lower = query.to_lowercase(); + let query_terms: std::collections::HashSet = + query_lower.split_whitespace().map(|s| s.to_string()).collect(); + let query_terms_len = query_terms.len(); + for result in &mut reranked { - let query_terms: std::collections::HashSet<&str> = - query.to_lowercase().split_whitespace().collect(); let content_lower = result.content.to_lowercase(); let mut overlap_score = 0.0; @@ -607,7 +609,7 @@ impl HybridSearchEngine { } } - let overlap_normalized = overlap_score / query_terms.len().max(1) as f32; + let overlap_normalized = overlap_score / query_terms_len.max(1) as f32; result.score = result.score * 0.7 + overlap_normalized * 0.3; result.search_method = SearchMethod::Reranked; } @@ -837,138 +839,3 @@ impl QueryDecomposer { synthesis } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_hybrid_config_default() { - let config = HybridSearchConfig::default(); - - assert_eq!(config.dense_weight, 0.7); - assert_eq!(config.sparse_weight, 0.3); - assert!(!config.reranker_enabled); - assert_eq!(config.max_results, 10); - assert!(config.bm25_enabled); - } - - #[test] - fn test_hybrid_config_search_modes() { - let config = HybridSearchConfig::default(); - assert!(config.use_sparse_search()); - assert!(config.use_dense_search()); - - let dense_only = HybridSearchConfig { - bm25_enabled: false, - ..Default::default() - }; - assert!(!dense_only.use_sparse_search()); - assert!(dense_only.use_dense_search()); - - let sparse_only = HybridSearchConfig { - dense_weight: 0.0, - sparse_weight: 1.0, - ..Default::default() - }; - assert!(sparse_only.use_sparse_search()); - assert!(!sparse_only.use_dense_search()); - } - - #[test] - fn test_reciprocal_rank_fusion() { - let config = HybridSearchConfig::default(); - let engine = HybridSearchEngine::new(config, "http://localhost:6333", "test"); - - let sparse = vec![ - ("doc1".to_string(), 0.9), - ("doc2".to_string(), 0.7), - ("doc3".to_string(), 0.5), - ]; - - let dense = vec![ - ("doc2".to_string(), 0.95), - ("doc1".to_string(), 0.8), - ("doc4".to_string(), 0.6), - ]; - - let fused = engine.reciprocal_rank_fusion(&sparse, &dense); - - assert!(!fused.is_empty()); - // doc1 and doc2 appear in both, should rank high - let top_ids: Vec<&str> = fused.iter().take(2).map(|(id, _)| id.as_str()).collect(); - assert!(top_ids.contains(&"doc1") || top_ids.contains(&"doc2")); - } - - #[test] - fn test_query_decomposer_simple() { - let decomposer = QueryDecomposer::new("http://localhost:8081", "none"); - - let rt = tokio::runtime::Runtime::new().unwrap(); - - let result = rt.block_on(async { - decomposer - .decompose("What is machine learning and how does it work?") - .await - }); - - assert!(result.is_ok()); - let queries = result.unwrap(); - assert!(!queries.is_empty()); - } - - #[test] - fn test_search_result_serialization() { - let result = SearchResult { - doc_id: "test123".to_string(), - content: "Test content".to_string(), - source: "/path/to/file".to_string(), - score: 0.85, - metadata: HashMap::new(), - search_method: SearchMethod::Hybrid, - }; - - let json = serde_json::to_string(&result); - assert!(json.is_ok()); - - let parsed: Result = serde_json::from_str(&json.unwrap()); - assert!(parsed.is_ok()); - assert_eq!(parsed.unwrap().doc_id, "test123"); - } - - #[cfg(not(feature = "vectordb"))] - #[test] - fn test_fallback_bm25_index() { - let mut index = BM25Index::new(); - - index.add_document( - "doc1", - "machine learning artificial intelligence", - "source1", - ); - index.add_document("doc2", "natural language processing NLP", "source2"); - index.add_document("doc3", "computer vision image recognition", "source3"); - - let results = index.search("machine learning", 10); - - assert!(!results.is_empty()); - assert_eq!(results[0].0, "doc1"); - - let stats = index.stats(); - assert_eq!(stats.doc_count, 3); - assert!(stats.enabled); - } - - #[cfg(not(feature = "vectordb"))] - #[test] - fn test_fallback_bm25_disabled() { - let mut index = BM25Index::new(); - index.set_enabled(false); - - index.add_document("doc1", "test content", "source1"); - let results = index.search("test", 10); - - assert!(results.is_empty()); - assert!(!index.stats().enabled); - } -} diff --git a/src/vector-db/vectordb_indexer.rs b/src/vector-db/vectordb_indexer.rs index 67eedd10c..2f95554af 100644 --- a/src/vector-db/vectordb_indexer.rs +++ b/src/vector-db/vectordb_indexer.rs @@ -1,5 +1,6 @@ use anyhow::Result; use chrono::{DateTime, Utc}; +use diesel::RunQueryDsl; use log::{error, info, warn}; use std::collections::HashMap; use std::path::PathBuf; @@ -70,7 +71,6 @@ pub struct IndexingStats { } /// User indexing job -#[derive(Debug)] struct UserIndexingJob { user_id: Uuid, bot_id: Uuid, @@ -238,7 +238,7 @@ impl VectorDBIndexer { // Initialize vector DBs if needed if job.email_db.is_none() { let mut email_db = - UserEmailVectorDB::new(user_id, bot_id, job.workspace.email_vectordb()); + UserEmailVectorDB::new(user_id, bot_id, job.workspace.email_vectordb().into()); if let Err(e) = email_db.initialize(&self.qdrant_url).await { warn!( "Failed to initialize email vector DB for user {}: {}", @@ -251,7 +251,7 @@ impl VectorDBIndexer { if job.drive_db.is_none() { let mut drive_db = - UserDriveVectorDB::new(user_id, bot_id, job.workspace.drive_vectordb()); + UserDriveVectorDB::new(user_id, bot_id, job.workspace.drive_vectordb().into()); if let Err(e) = drive_db.initialize(&self.qdrant_url).await { warn!( "Failed to initialize drive vector DB for user {}: {}", @@ -436,20 +436,19 @@ impl VectorDBIndexer { let mut db_conn = conn.get()?; + #[derive(diesel::QueryableByName)] + struct AccountIdRow { + #[diesel(sql_type = diesel::sql_types::Text)] + id: String, + } + let results: Vec = diesel::sql_query( "SELECT id::text FROM user_email_accounts WHERE user_id = $1 AND is_active = true", ) .bind::(user_id) - .load(&mut db_conn)? + .load::(&mut db_conn)? .into_iter() - .filter_map(|row: diesel::QueryableByName| { - use diesel::sql_types::Text; - let id: Result = >::from_sql(row.get("id").ok()?); - id.ok() - }) + .map(|row| row.id) .collect(); Ok::<_, anyhow::Error>(results) @@ -466,8 +465,31 @@ impl VectorDBIndexer { let account_id = account_id.to_string(); let results = tokio::task::spawn_blocking(move || { + use diesel::prelude::*; let mut conn = pool.get()?; + #[derive(diesel::QueryableByName)] + struct EmailRow { + #[diesel(sql_type = diesel::sql_types::Uuid)] + id: Uuid, + #[diesel(sql_type = diesel::sql_types::Text)] + message_id: String, + #[diesel(sql_type = diesel::sql_types::Text)] + subject: String, + #[diesel(sql_type = diesel::sql_types::Text)] + from_address: String, + #[diesel(sql_type = diesel::sql_types::Text)] + to_addresses: String, + #[diesel(sql_type = diesel::sql_types::Nullable)] + body_text: Option, + #[diesel(sql_type = diesel::sql_types::Nullable)] + body_html: Option, + #[diesel(sql_type = diesel::sql_types::Timestamptz)] + received_at: DateTime, + #[diesel(sql_type = diesel::sql_types::Text)] + folder: String, + } + let query = r#" SELECT e.id, e.message_id, e.subject, e.from_address, e.to_addresses, e.body_text, e.body_html, e.received_at, e.folder @@ -480,61 +502,29 @@ impl VectorDBIndexer { LIMIT 100 "#; - let rows: Vec<( - Uuid, - String, - String, - String, - String, - Option, - Option, - DateTime, - String, - )> = diesel::sql_query(query) + let rows: Vec = diesel::sql_query(query) .bind::(user_id) .bind::(&account_id) - .load::<( - Uuid, - String, - String, - String, - String, - Option, - Option, - DateTime, - String, - )>(&mut conn) + .load(&mut conn) .unwrap_or_default(); let emails: Vec = rows .into_iter() - .map( - |( - id, - message_id, - subject, - from, - to, - body_text, - body_html, - received_at, - folder, - )| { - EmailDocument { - id: id.to_string(), - account_id: account_id.clone(), - from_email: from.clone(), - from_name: from, - to_email: to, - subject, - body_text: body_text.unwrap_or_default(), - date: received_at, - folder, - has_attachments: false, - thread_id: None, - } - }, - ) + .map(|row| { + EmailDocument { + id: row.id.to_string(), + account_id: account_id.clone(), + from_email: row.from_address.clone(), + from_name: row.from_address, + to_email: row.to_addresses, + subject: row.subject, + body_text: row.body_text.unwrap_or_default(), + date: row.received_at, + folder: row.folder, + has_attachments: false, + thread_id: None, + } + }) .collect(); Ok::<_, anyhow::Error>(emails) @@ -551,8 +541,31 @@ impl VectorDBIndexer { let pool = self.db_pool.clone(); let results = tokio::task::spawn_blocking(move || { + use diesel::prelude::*; let mut conn = pool.get()?; + #[derive(diesel::QueryableByName)] + struct FileRow { + #[diesel(sql_type = diesel::sql_types::Uuid)] + id: Uuid, + #[diesel(sql_type = diesel::sql_types::Text)] + file_path: String, + #[diesel(sql_type = diesel::sql_types::Text)] + file_name: String, + #[diesel(sql_type = diesel::sql_types::Text)] + file_type: String, + #[diesel(sql_type = diesel::sql_types::BigInt)] + file_size: i64, + #[diesel(sql_type = diesel::sql_types::Text)] + bucket: String, + #[diesel(sql_type = diesel::sql_types::Nullable)] + mime_type: Option, + #[diesel(sql_type = diesel::sql_types::Timestamptz)] + created_at: DateTime, + #[diesel(sql_type = diesel::sql_types::Timestamptz)] + modified_at: DateTime, + } + let query = r#" SELECT f.id, f.file_path, f.file_name, f.file_type, f.file_size, f.bucket, f.mime_type, f.created_at, f.modified_at @@ -565,52 +578,30 @@ impl VectorDBIndexer { LIMIT 100 "#; - let rows: Vec<( - Uuid, - String, - String, - String, - i64, - String, - Option, - DateTime, - DateTime, - )> = diesel::sql_query(query) + let rows: Vec = diesel::sql_query(query) .bind::(user_id) - .load::<(Uuid, String, String, i64, DateTime)>(&mut conn) + .load(&mut conn) .unwrap_or_default(); let files: Vec = rows .into_iter() - .map( - |( - id, - file_path, - file_name, - file_type, - file_size, - bucket, - mime_type, - created_at, - modified_at, - )| { - FileDocument { - id: id.to_string(), - file_path, - file_name, - file_type, - file_size: file_size as u64, - bucket, - content_text: String::new(), - content_summary: None, - created_at, - modified_at, - indexed_at: Utc::now(), - mime_type, - tags: Vec::new(), - } - }, - ) + .map(|row| { + FileDocument { + id: row.id.to_string(), + file_path: row.file_path, + file_name: row.file_name, + file_type: row.file_type, + file_size: row.file_size as u64, + bucket: row.bucket, + content_text: String::new(), + content_summary: None, + created_at: row.created_at, + modified_at: row.modified_at, + indexed_at: Utc::now(), + mime_type: row.mime_type, + tags: Vec::new(), + } + }) .collect(); Ok::<_, anyhow::Error>(files) @@ -682,23 +673,3 @@ impl VectorDBIndexer { self.index_user_data(user_id, bot_id).await } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_indexing_stats_creation() { - let stats = IndexingStats { - emails_indexed: 10, - files_indexed: 5, - emails_pending: 2, - files_pending: 3, - last_run: Some(Utc::now()), - errors: 0, - }; - - assert_eq!(stats.emails_indexed, 10); - assert_eq!(stats.files_indexed, 5); - } -} diff --git a/src/weba/mod.rs b/src/weba/mod.rs index 82bc3bcc1..b1e71890f 100644 --- a/src/weba/mod.rs +++ b/src/weba/mod.rs @@ -491,7 +491,7 @@ fn render_html(app: &WebApp, content: &str) -> String { ) } -fn slugify(s: &str) -> String { +pub fn slugify(s: &str) -> String { s.to_lowercase() .chars() .map(|c| if c.is_alphanumeric() { c } else { '-' }) @@ -505,40 +505,3 @@ fn slugify(s: &str) -> String { pub fn init() { log::info!("WEBA module initialized"); } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_slugify() { - assert_eq!(slugify("Hello World"), "hello-world"); - assert_eq!(slugify("My App 123"), "my-app-123"); - assert_eq!(slugify(" Test App "), "test-app"); - } - - #[test] - fn test_webapp_creation() { - let now = chrono::Utc::now(); - let app = WebApp { - id: Uuid::new_v4(), - name: "Test App".to_string(), - slug: "test-app".to_string(), - description: None, - template: WebAppTemplate::Blank, - status: WebAppStatus::Draft, - config: WebAppConfig::default(), - created_at: now, - updated_at: now, - }; - assert_eq!(app.name, "Test App"); - assert_eq!(app.slug, "test-app"); - } - - #[tokio::test] - async fn test_weba_state() { - let state = WebaState::new(); - let apps = state.apps.read().await; - assert!(apps.is_empty()); - } -} diff --git a/src/whatsapp/mod.rs b/src/whatsapp/mod.rs index 758b1cbef..46136b55f 100644 --- a/src/whatsapp/mod.rs +++ b/src/whatsapp/mod.rs @@ -1076,59 +1076,3 @@ async fn get_default_bot_id(state: &Arc) -> Uuid { .flatten() .unwrap_or_else(Uuid::nil) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_extract_text_message() { - let message = WhatsAppMessage { - id: "msg123".to_string(), - from: "+1234567890".to_string(), - timestamp: "1234567890".to_string(), - message_type: "text".to_string(), - text: Some(WhatsAppText { - body: "Hello, world!".to_string(), - }), - image: None, - audio: None, - video: None, - document: None, - location: None, - interactive: None, - button: None, - }; - - let content = extract_message_content(&message); - assert_eq!(content, "Hello, world!"); - } - - #[test] - fn test_extract_interactive_button() { - let message = WhatsAppMessage { - id: "msg123".to_string(), - from: "+1234567890".to_string(), - timestamp: "1234567890".to_string(), - message_type: "interactive".to_string(), - text: None, - image: None, - audio: None, - video: None, - document: None, - location: None, - interactive: Some(WhatsAppInteractive { - interactive_type: "button_reply".to_string(), - button_reply: Some(WhatsAppButtonReply { - id: "btn1".to_string(), - title: "Yes".to_string(), - }), - list_reply: None, - }), - button: None, - }; - - let content = extract_message_content(&message); - assert_eq!(content, "Yes"); - } -}