diff --git a/Cargo.toml b/Cargo.toml index 94affd4..c435b54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ minidom = "0.15" jid = "0.10" # TLS -rustls = "0.23" +rustls = { version = "0.23", features = ["ring"] } tokio-rustls = "0.26" rustls-pemfile = "2" diff --git a/chattermax-core/src/sm.rs b/chattermax-core/src/sm.rs index d4bd2c5..8e15d87 100644 --- a/chattermax-core/src/sm.rs +++ b/chattermax-core/src/sm.rs @@ -92,7 +92,10 @@ impl Enabled { let max = elem .attr("max") - .map(|s| s.parse::().map_err(|_| "Invalid 'max' value".to_string())) + .map(|s| { + s.parse::() + .map_err(|_| "Invalid 'max' value".to_string()) + }) .transpose()?; Ok(Self { id, resume, max }) diff --git a/chattermax-core/src/types/message.rs b/chattermax-core/src/types/message.rs index 01aa166..1d9eb50 100644 --- a/chattermax-core/src/types/message.rs +++ b/chattermax-core/src/types/message.rs @@ -424,7 +424,10 @@ mod tests { reason: FreezeReason::TaskComplete, conversation_context: ConversationContext { room_jid: Some("room@example.com".to_string()), - participants: vec!["user1@example.com".to_string(), "user2@example.com".to_string()], + participants: vec![ + "user1@example.com".to_string(), + "user2@example.com".to_string(), + ], last_message_id: Some("msg-123".to_string()), }, active_context_ref: Some("ctx-ref-1".to_string()), @@ -433,7 +436,8 @@ mod tests { }; let json = serde_json::to_string(&freeze_notif).expect("serialization failed"); - let deserialized: FreezeNotification = serde_json::from_str(&json).expect("deserialization failed"); + let deserialized: FreezeNotification = + serde_json::from_str(&json).expect("deserialization failed"); assert_eq!(deserialized.agent_jid, freeze_notif.agent_jid); assert_eq!(deserialized.frozen_at, freeze_notif.frozen_at); @@ -443,7 +447,8 @@ mod tests { fn test_freeze_reason_task_complete() { let reason = FreezeReason::TaskComplete; let json = serde_json::to_string(&reason).expect("serialization failed"); - let deserialized: FreezeReason = serde_json::from_str(&json).expect("deserialization failed"); + let deserialized: FreezeReason = + serde_json::from_str(&json).expect("deserialization failed"); assert_eq!(deserialized, FreezeReason::TaskComplete); } @@ -451,7 +456,8 @@ mod tests { fn test_freeze_reason_user_requested() { let reason = FreezeReason::UserRequested; let json = serde_json::to_string(&reason).expect("serialization failed"); - let deserialized: FreezeReason = serde_json::from_str(&json).expect("deserialization failed"); + let deserialized: FreezeReason = + serde_json::from_str(&json).expect("deserialization failed"); assert_eq!(deserialized, FreezeReason::UserRequested); } @@ -459,7 +465,8 @@ mod tests { fn test_freeze_reason_error() { let reason = FreezeReason::Error("Something went wrong".to_string()); let json = serde_json::to_string(&reason).expect("serialization failed"); - let deserialized: FreezeReason = serde_json::from_str(&json).expect("deserialization failed"); + let deserialized: FreezeReason = + serde_json::from_str(&json).expect("deserialization failed"); match deserialized { FreezeReason::Error(msg) => assert_eq!(msg, "Something went wrong"), _ => panic!("Expected Error variant"), @@ -470,7 +477,8 @@ mod tests { fn test_freeze_reason_timeout() { let reason = FreezeReason::Timeout; let json = serde_json::to_string(&reason).expect("serialization failed"); - let deserialized: FreezeReason = serde_json::from_str(&json).expect("deserialization failed"); + let deserialized: FreezeReason = + serde_json::from_str(&json).expect("deserialization failed"); assert_eq!(deserialized, FreezeReason::Timeout); } @@ -483,7 +491,8 @@ mod tests { }; let json = serde_json::to_string(&context).expect("serialization failed"); - let deserialized: ConversationContext = serde_json::from_str(&json).expect("deserialization failed"); + let deserialized: ConversationContext = + serde_json::from_str(&json).expect("deserialization failed"); assert_eq!(deserialized.room_jid, context.room_jid); assert_eq!(deserialized.participants.len(), 1); @@ -522,7 +531,10 @@ mod tests { #[test] fn test_freeze_notification_as_str() { - assert_eq!(MessageType::FreezeNotification.as_str(), "freeze_notification"); + assert_eq!( + MessageType::FreezeNotification.as_str(), + "freeze_notification" + ); } #[test] diff --git a/chattermax-core/src/types/mod.rs b/chattermax-core/src/types/mod.rs index fc80d37..be8fc32 100644 --- a/chattermax-core/src/types/mod.rs +++ b/chattermax-core/src/types/mod.rs @@ -10,6 +10,6 @@ pub mod serialization; pub use context_ref::{ContextRef, ContextRefParseError}; pub use message::{ Answer, CodeChange, FeatureComplete, Integration, Message, MessageType, Metadata, Question, - ReviewComment, StatusUpdate, Thought, ThawRequest, Todo, ToolCall, ToolResult, WorkAvailable, + ReviewComment, StatusUpdate, ThawRequest, Thought, Todo, ToolCall, ToolResult, WorkAvailable, }; pub use serialization::{from_xml, to_xml}; diff --git a/chattermax-core/src/types/serialization.rs b/chattermax-core/src/types/serialization.rs index 66b63db..d465610 100644 --- a/chattermax-core/src/types/serialization.rs +++ b/chattermax-core/src/types/serialization.rs @@ -799,25 +799,33 @@ fn deserialize_feature_complete(element: &Element) -> Result { } fn serialize_freeze_notification(freeze_notification: &FreezeNotification) -> Result { - let mut elem = Element::builder("freeze_notification", "urn:chattermax:xep:freeze-notification:0") - .append( - Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") - .append(freeze_notification.agent_jid.clone()) - .build(), - ) - .append(serialize_freeze_reason(&freeze_notification.reason)) - .append(serialize_conversation_context(&freeze_notification.conversation_context)) - .append( - Element::builder("frozen_at", "urn:chattermax:xep:freeze-notification:0") - .append(freeze_notification.frozen_at.clone()) - .build(), - ); + let mut elem = Element::builder( + "freeze_notification", + "urn:chattermax:xep:freeze-notification:0", + ) + .append( + Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") + .append(freeze_notification.agent_jid.clone()) + .build(), + ) + .append(serialize_freeze_reason(&freeze_notification.reason)) + .append(serialize_conversation_context( + &freeze_notification.conversation_context, + )) + .append( + Element::builder("frozen_at", "urn:chattermax:xep:freeze-notification:0") + .append(freeze_notification.frozen_at.clone()) + .build(), + ); if let Some(active_context_ref) = &freeze_notification.active_context_ref { elem = elem.append( - Element::builder("active_context_ref", "urn:chattermax:xep:freeze-notification:0") - .append(active_context_ref.clone()) - .build(), + Element::builder( + "active_context_ref", + "urn:chattermax:xep:freeze-notification:0", + ) + .append(active_context_ref.clone()) + .build(), ); } @@ -875,7 +883,10 @@ fn serialize_freeze_reason(reason: &FreezeReason) -> Element { } fn serialize_conversation_context(context: &ConversationContext) -> Element { - let mut elem = Element::builder("conversation_context", "urn:chattermax:xep:freeze-notification:0"); + let mut elem = Element::builder( + "conversation_context", + "urn:chattermax:xep:freeze-notification:0", + ); if let Some(room_jid) = &context.room_jid { elem = elem.append( @@ -885,7 +896,8 @@ fn serialize_conversation_context(context: &ConversationContext) -> Element { ); } - let mut participants_elem = Element::builder("participants", "urn:chattermax:xep:freeze-notification:0"); + let mut participants_elem = + Element::builder("participants", "urn:chattermax:xep:freeze-notification:0"); for participant in &context.participants { participants_elem = participants_elem.append( Element::builder("participant", "urn:chattermax:xep:freeze-notification:0") @@ -897,9 +909,12 @@ fn serialize_conversation_context(context: &ConversationContext) -> Element { if let Some(last_message_id) = &context.last_message_id { elem = elem.append( - Element::builder("last_message_id", "urn:chattermax:xep:freeze-notification:0") - .append(last_message_id.clone()) - .build(), + Element::builder( + "last_message_id", + "urn:chattermax:xep:freeze-notification:0", + ) + .append(last_message_id.clone()) + .build(), ); } @@ -918,7 +933,10 @@ fn deserialize_freeze_notification(element: &Element) -> Result { .and_then(deserialize_freeze_reason)?; let conversation_context = element - .get_child("conversation_context", "urn:chattermax:xep:freeze-notification:0") + .get_child( + "conversation_context", + "urn:chattermax:xep:freeze-notification:0", + ) .ok_or_else(|| { Error::ParseError("Missing conversation_context in freeze_notification".to_string()) }) @@ -930,7 +948,10 @@ fn deserialize_freeze_notification(element: &Element) -> Result { .ok_or_else(|| Error::ParseError("Missing frozen_at in freeze_notification".to_string()))?; let active_context_ref = element - .get_child("active_context_ref", "urn:chattermax:xep:freeze-notification:0") + .get_child( + "active_context_ref", + "urn:chattermax:xep:freeze-notification:0", + ) .map(|e| e.text()); let metadata = extract_metadata(element)?; @@ -989,7 +1010,10 @@ fn deserialize_conversation_context(element: &Element) -> Result { - assert_eq!(tr.resurrection_room_jid, Some("room2@conference.chattermax.local".to_string())); - assert_eq!(tr.additional_context, Some("Resume context from checkpoint 5".to_string())); + assert_eq!( + tr.resurrection_room_jid, + Some("room2@conference.chattermax.local".to_string()) + ); + assert_eq!( + tr.additional_context, + Some("Resume context from checkpoint 5".to_string()) + ); } _ => panic!("Wrong message type"), } diff --git a/chattermax-server/src/context_resolver.rs b/chattermax-server/src/context_resolver.rs index cc20122..e1aa4de 100644 --- a/chattermax-server/src/context_resolver.rs +++ b/chattermax-server/src/context_resolver.rs @@ -3,7 +3,7 @@ //! This module wraps the core ContextResolver with server-specific configuration, //! including base URL loading from environment and cache settings. -use chattermax_core::chizu::{ChizuClient, ContextResolver, ChizuError, KnowledgePack}; +use chattermax_core::chizu::{ChizuClient, ChizuError, ContextResolver, KnowledgePack}; use chattermax_core::types::ContextRef; use std::time::Duration; use tracing::{debug, warn}; @@ -28,8 +28,8 @@ impl ServerContextResolver { /// let resolver = ServerContextResolver::new(); /// ``` pub fn new() -> Self { - let base_url = std::env::var("CHIZU_BASE_URL") - .unwrap_or_else(|_| "http://localhost:8080".to_string()); + let base_url = + std::env::var("CHIZU_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); debug!("Creating ServerContextResolver with base URL: {}", base_url); diff --git a/chattermax-server/src/db.rs b/chattermax-server/src/db.rs index 7d66896..d3e4206 100644 --- a/chattermax-server/src/db.rs +++ b/chattermax-server/src/db.rs @@ -575,7 +575,8 @@ impl Database { expires_seconds: u32, ) -> Result<()> { let expires_at = Utc::now() + chrono::Duration::seconds(expires_seconds as i64); - let stanzas_json = serde_json::to_string(unacked_stanzas).unwrap_or_else(|_| "[]".to_string()); + let stanzas_json = + serde_json::to_string(unacked_stanzas).unwrap_or_else(|_| "[]".to_string()); sqlx::query( r#" @@ -621,8 +622,11 @@ impl Database { let created_str: String = row.get("created_at"); let expires_str: String = row.get("expires_at"); - let jid: Jid = jid_str.parse().map_err(|e| anyhow::anyhow!("Invalid JID: {}", e))?; - let unacked_stanzas: Vec = serde_json::from_str(&stanzas_json).unwrap_or_default(); + let jid: Jid = jid_str + .parse() + .map_err(|e| anyhow::anyhow!("Invalid JID: {}", e))?; + let unacked_stanzas: Vec = + serde_json::from_str(&stanzas_json).unwrap_or_default(); // SQLite datetime format is "YYYY-MM-DD HH:MM:SS", convert to RFC3339 let created_at = parse_sqlite_datetime(&created_str)?; @@ -722,9 +726,14 @@ mod tests { let (_dir, db) = setup_test_db().await; let jid: Jid = "user@example.com/resource".parse().unwrap(); let token = "test-token-12345"; - let unacked = vec!["".to_string(), "".to_string()]; + let unacked = vec![ + "".to_string(), + "".to_string(), + ]; - db.store_stream_session(token, &jid, 5, 10, &unacked, 300).await.unwrap(); + db.store_stream_session(token, &jid, 5, 10, &unacked, 300) + .await + .unwrap(); let session = db.get_stream_session(token).await.unwrap(); assert!(session.is_some()); @@ -749,7 +758,9 @@ mod tests { let jid: Jid = "user@example.com/resource".parse().unwrap(); let token = "test-token-delete"; - db.store_stream_session(token, &jid, 0, 0, &[], 300).await.unwrap(); + db.store_stream_session(token, &jid, 0, 0, &[], 300) + .await + .unwrap(); assert!(db.get_stream_session(token).await.unwrap().is_some()); db.delete_stream_session(token).await.unwrap(); @@ -762,12 +773,16 @@ mod tests { let jid: Jid = "user@example.com/resource".parse().unwrap(); let token = "test-token-upsert"; - db.store_stream_session(token, &jid, 0, 0, &[], 300).await.unwrap(); + db.store_stream_session(token, &jid, 0, 0, &[], 300) + .await + .unwrap(); let session = db.get_stream_session(token).await.unwrap().unwrap(); assert_eq!(session.last_handled_inbound, 0); // Update with new values - db.store_stream_session(token, &jid, 10, 20, &["".to_string()], 300).await.unwrap(); + db.store_stream_session(token, &jid, 10, 20, &["".to_string()], 300) + .await + .unwrap(); let session = db.get_stream_session(token).await.unwrap().unwrap(); assert_eq!(session.last_handled_inbound, 10); assert_eq!(session.last_handled_outbound, 20); diff --git a/chattermax-server/src/freeze/mod.rs b/chattermax-server/src/freeze/mod.rs index 3fcceb4..9c12c1d 100644 --- a/chattermax-server/src/freeze/mod.rs +++ b/chattermax-server/src/freeze/mod.rs @@ -93,7 +93,10 @@ impl FreezeHandler { /// # Returns /// Vector containing references to all FrozenAgentState entries pub fn list_frozen_agents(&self) -> Vec<&FrozenAgentState> { - debug!("Listing all frozen agents (count: {})", self.frozen_agents.len()); + debug!( + "Listing all frozen agents (count: {})", + self.frozen_agents.len() + ); self.frozen_agents.values().collect() } } diff --git a/chattermax-server/src/hooks/filter.rs b/chattermax-server/src/hooks/filter.rs index 72df1d8..6d41a32 100644 --- a/chattermax-server/src/hooks/filter.rs +++ b/chattermax-server/src/hooks/filter.rs @@ -13,9 +13,19 @@ use std::collections::HashMap; fn is_custom_message_element_name(name: &str) -> bool { matches!( name, - "thought" | "tool_call" | "tool_result" | "todo" | "code_change" | - "integration" | "review_comment" | "work_available" | "question" | - "answer" | "status_update" | "feature_complete" | "freeze_notification" + "thought" + | "tool_call" + | "tool_result" + | "todo" + | "code_change" + | "integration" + | "review_comment" + | "work_available" + | "question" + | "answer" + | "status_update" + | "feature_complete" + | "freeze_notification" ) } @@ -37,7 +47,9 @@ pub fn extract_custom_message_type(message: &Element) -> Option { "jabber:x:chibi:answer" => return Some(MessageType::Answer), "jabber:x:chibi:status_update" => return Some(MessageType::StatusUpdate), "jabber:x:chibi:feature_complete" => return Some(MessageType::FeatureComplete), - "urn:chattermax:xep:freeze-notification:0" => return Some(MessageType::FreezeNotification), + "urn:chattermax:xep:freeze-notification:0" => { + return Some(MessageType::FreezeNotification); + } _ => continue, } } @@ -81,8 +93,8 @@ pub fn extract_variables(message: &Element) -> HashMap { // Handle both namespace patterns: // 1. jabber:x:chibi:tool_call (namespace includes message type) // 2. jabber:x:chibi (namespace is generic, check element name for custom types) - let is_custom_element = child_ns.starts_with("jabber:x:chibi:") || - (child_ns == "jabber:x:chibi" && is_custom_message_element_name(child_name)); + let is_custom_element = child_ns.starts_with("jabber:x:chibi:") + || (child_ns == "jabber:x:chibi" && is_custom_message_element_name(child_name)); if is_custom_element { // Look for context_ref child element within this custom message type @@ -488,13 +500,19 @@ mod tests { .attr("to", "room@conference.localhost") .attr("type", "groupchat") .append( - minidom::Element::builder("freeze_notification", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") - .append("agent@localhost") - .build(), + minidom::Element::builder( + "freeze_notification", + "urn:chattermax:xep:freeze-notification:0", + ) + .append( + minidom::Element::builder( + "agent_jid", + "urn:chattermax:xep:freeze-notification:0", ) + .append("agent@localhost") .build(), + ) + .build(), ) .build(); @@ -507,13 +525,19 @@ mod tests { let message = minidom::Element::builder("message", "jabber:client") .attr("type", "groupchat") .append( - minidom::Element::builder("freeze_notification", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") - .append("agent@localhost") - .build(), + minidom::Element::builder( + "freeze_notification", + "urn:chattermax:xep:freeze-notification:0", + ) + .append( + minidom::Element::builder( + "agent_jid", + "urn:chattermax:xep:freeze-notification:0", ) + .append("agent@localhost") .build(), + ) + .build(), ) .build(); diff --git a/chattermax-server/src/hooks/manager.rs b/chattermax-server/src/hooks/manager.rs index 483115f..3466cf4 100644 --- a/chattermax-server/src/hooks/manager.rs +++ b/chattermax-server/src/hooks/manager.rs @@ -2,13 +2,13 @@ use crate::context_resolver::ServerContextResolver; use crate::freeze::FreezeHandler; -use crate::thaw::{ThawHandler, ResurrectionService}; use crate::hooks::config::HookConfig; use crate::hooks::errors::{HookError, Result}; use crate::hooks::exec; use crate::hooks::filter; -use chattermax_core::types::{ContextRef, MessageType}; +use crate::thaw::{ResurrectionService, ThawHandler}; use chattermax_core::types::serialization; +use chattermax_core::types::{ContextRef, MessageType}; use minidom::Element; use std::collections::HashMap; use std::str::FromStr; @@ -65,7 +65,8 @@ impl HookManager { let variables = filter::extract_variables(message); // Check if this is a FreezeNotification message and handle it - if let Some(MessageType::FreezeNotification) = filter::extract_custom_message_type(message) { + if let Some(MessageType::FreezeNotification) = filter::extract_custom_message_type(message) + { debug!("Detected FreezeNotification message, routing to FreezeHandler"); if let Err(e) = self.process_freeze_notification(message).await { warn!("FreezeNotification processing failed: {}", e); @@ -181,31 +182,34 @@ impl HookManager { /// Returns the path to the temporary file containing the serialized knowledge pack. async fn resolve_and_save_context(&self, context_ref_str: &str) -> Result { // Parse the context reference - let context_ref = ContextRef::from_str(context_ref_str) - .map_err(|e| HookError::SubstitutionError(format!("Invalid context reference: {}", e)))?; + let context_ref = ContextRef::from_str(context_ref_str).map_err(|e| { + HookError::SubstitutionError(format!("Invalid context reference: {}", e)) + })?; // Resolve the context let mut resolver = self.context_resolver.write().await; let knowledge_pack = resolver .resolve_context_for_message(&context_ref) .await - .map_err(|e| HookError::SubstitutionError(format!("Failed to resolve context: {}", e)))?; + .map_err(|e| { + HookError::SubstitutionError(format!("Failed to resolve context: {}", e)) + })?; // Serialize the knowledge pack to JSON - let json_content = serde_json::to_string(&knowledge_pack) - .map_err(|e| HookError::IoError(std::io::Error::other( - format!("Failed to serialize knowledge pack: {}", e) - )))?; + let json_content = serde_json::to_string(&knowledge_pack).map_err(|e| { + HookError::IoError(std::io::Error::other(format!( + "Failed to serialize knowledge pack: {}", + e + ))) + })?; // Create a temporary file - let temp_file = tempfile::NamedTempFile::new() - .map_err(HookError::IoError)?; + let temp_file = tempfile::NamedTempFile::new().map_err(HookError::IoError)?; let temp_path = temp_file.path().to_string_lossy().to_string(); // Write the JSON content to the file - std::fs::write(&temp_path, json_content) - .map_err(HookError::IoError)?; + std::fs::write(&temp_path, json_content).map_err(HookError::IoError)?; debug!("Context saved to temporary file: {}", temp_path); Ok(temp_path) @@ -218,10 +222,15 @@ impl HookManager { async fn process_freeze_notification(&self, message: &Element) -> Result<()> { // Find the freeze_notification child element let freeze_notif_elem = message - .get_child("freeze_notification", "urn:chattermax:xep:freeze-notification:0") - .ok_or_else(|| HookError::SubstitutionError( - "No freeze_notification element found in message".to_string() - ))?; + .get_child( + "freeze_notification", + "urn:chattermax:xep:freeze-notification:0", + ) + .ok_or_else(|| { + HookError::SubstitutionError( + "No freeze_notification element found in message".to_string(), + ) + })?; // Try to deserialize the FreezeNotification from the element match serialization::from_xml(freeze_notif_elem) { @@ -253,7 +262,7 @@ impl HookManager { } } else { Err(HookError::SubstitutionError( - "Deserialized message is not a FreezeNotification".to_string() + "Deserialized message is not a FreezeNotification".to_string(), )) } } @@ -276,9 +285,9 @@ impl HookManager { // Find the thaw_request child element let thaw_req_elem = message .get_child("thaw_request", "urn:chattermax:xep:thaw-request:0") - .ok_or_else(|| HookError::SubstitutionError( - "No thaw_request element found in message".to_string() - ))?; + .ok_or_else(|| { + HookError::SubstitutionError("No thaw_request element found in message".to_string()) + })?; // Try to deserialize the ThawRequest from the element match serialization::from_xml(thaw_req_elem) { @@ -337,7 +346,7 @@ impl HookManager { } } else { Err(HookError::SubstitutionError( - "Deserialized message is not a ThawRequest".to_string() + "Deserialized message is not a ThawRequest".to_string(), )) } } @@ -414,50 +423,74 @@ mod tests { .attr("to", "room@conference.localhost") .attr("type", "groupchat") .append( - minidom::Element::builder("freeze_notification", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") - .append("agent@localhost") - .build(), + minidom::Element::builder( + "freeze_notification", + "urn:chattermax:xep:freeze-notification:0", + ) + .append( + minidom::Element::builder( + "agent_jid", + "urn:chattermax:xep:freeze-notification:0", ) - .append( - minidom::Element::builder("reason", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("type", "urn:chattermax:xep:freeze-notification:0") - .append("task_complete") - .build(), + .append("agent@localhost") + .build(), + ) + .append( + minidom::Element::builder("reason", "urn:chattermax:xep:freeze-notification:0") + .append( + minidom::Element::builder( + "type", + "urn:chattermax:xep:freeze-notification:0", ) + .append("task_complete") .build(), + ) + .build(), + ) + .append( + minidom::Element::builder( + "conversation_context", + "urn:chattermax:xep:freeze-notification:0", ) .append( - minidom::Element::builder("conversation_context", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("room_jid", "urn:chattermax:xep:freeze-notification:0") - .append("room@conference.localhost") - .build(), - ) - .append( - minidom::Element::builder("participants", "urn:chattermax:xep:freeze-notification:0") - .build(), - ) - .append( - minidom::Element::builder("last_message_id", "urn:chattermax:xep:freeze-notification:0") - .append("msg-123") - .build(), - ) - .build(), + minidom::Element::builder( + "room_jid", + "urn:chattermax:xep:freeze-notification:0", + ) + .append("room@conference.localhost") + .build(), ) .append( - minidom::Element::builder("frozen_at", "urn:chattermax:xep:freeze-notification:0") - .append("2024-01-01T12:00:00Z") - .build(), + minidom::Element::builder( + "participants", + "urn:chattermax:xep:freeze-notification:0", + ) + .build(), ) .append( - minidom::Element::builder("timestamp", "jabber:x:chibi") - .append("2024-01-01T12:00:00Z") - .build(), + minidom::Element::builder( + "last_message_id", + "urn:chattermax:xep:freeze-notification:0", + ) + .append("msg-123") + .build(), + ) + .build(), + ) + .append( + minidom::Element::builder( + "frozen_at", + "urn:chattermax:xep:freeze-notification:0", ) + .append("2024-01-01T12:00:00Z") .build(), + ) + .append( + minidom::Element::builder("timestamp", "jabber:x:chibi") + .append("2024-01-01T12:00:00Z") + .build(), + ) + .build(), ) .build(); @@ -465,7 +498,11 @@ mod tests { let result = manager.process_freeze_notification(&message).await; // Verify it was processed successfully - assert!(result.is_ok(), "process_freeze_notification should succeed: {:?}", result.err()); + assert!( + result.is_ok(), + "process_freeze_notification should succeed: {:?}", + result.err() + ); // Verify the frozen agent was stored in the handler let handler = manager.freeze_handler.read().await; @@ -490,56 +527,83 @@ mod tests { .attr("to", "room@conference.localhost") .attr("type", "groupchat") .append( - minidom::Element::builder("freeze_notification", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") - .append("agent@localhost") - .build(), + minidom::Element::builder( + "freeze_notification", + "urn:chattermax:xep:freeze-notification:0", + ) + .append( + minidom::Element::builder( + "agent_jid", + "urn:chattermax:xep:freeze-notification:0", ) - .append( - minidom::Element::builder("reason", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("type", "urn:chattermax:xep:freeze-notification:0") - .append("task_complete") - .build(), + .append("agent@localhost") + .build(), + ) + .append( + minidom::Element::builder("reason", "urn:chattermax:xep:freeze-notification:0") + .append( + minidom::Element::builder( + "type", + "urn:chattermax:xep:freeze-notification:0", ) + .append("task_complete") .build(), + ) + .build(), + ) + .append( + minidom::Element::builder( + "conversation_context", + "urn:chattermax:xep:freeze-notification:0", ) .append( - minidom::Element::builder("conversation_context", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("room_jid", "urn:chattermax:xep:freeze-notification:0") - .append("room@conference.localhost") - .build(), - ) - .append( - minidom::Element::builder("participants", "urn:chattermax:xep:freeze-notification:0") - .build(), - ) - .append( - minidom::Element::builder("last_message_id", "urn:chattermax:xep:freeze-notification:0") - .append("msg-123") - .build(), - ) - .build(), + minidom::Element::builder( + "room_jid", + "urn:chattermax:xep:freeze-notification:0", + ) + .append("room@conference.localhost") + .build(), ) .append( - minidom::Element::builder("frozen_at", "urn:chattermax:xep:freeze-notification:0") - .append("2024-01-01T12:00:00Z") - .build(), + minidom::Element::builder( + "participants", + "urn:chattermax:xep:freeze-notification:0", + ) + .build(), ) .append( - minidom::Element::builder("timestamp", "jabber:x:chibi") - .append("2024-01-01T12:00:00Z") - .build(), + minidom::Element::builder( + "last_message_id", + "urn:chattermax:xep:freeze-notification:0", + ) + .append("msg-123") + .build(), + ) + .build(), + ) + .append( + minidom::Element::builder( + "frozen_at", + "urn:chattermax:xep:freeze-notification:0", ) + .append("2024-01-01T12:00:00Z") .build(), + ) + .append( + minidom::Element::builder("timestamp", "jabber:x:chibi") + .append("2024-01-01T12:00:00Z") + .build(), + ) + .build(), ) .build(); // Process the freeze notification to create a frozen agent let freeze_result = manager.process_freeze_notification(&freeze_message).await; - assert!(freeze_result.is_ok(), "Freeze notification should process successfully"); + assert!( + freeze_result.is_ok(), + "Freeze notification should process successfully" + ); // Get the freeze_id from the frozen agent let freeze_handler = manager.freeze_handler.read().await; @@ -561,24 +625,36 @@ mod tests { .build(), ) .append( - minidom::Element::builder("target_agent_jid", "urn:chattermax:xep:thaw-request:0") - .append("agent@localhost") - .build(), + minidom::Element::builder( + "target_agent_jid", + "urn:chattermax:xep:thaw-request:0", + ) + .append("agent@localhost") + .build(), ) .append( - minidom::Element::builder("resurrection_room_jid", "urn:chattermax:xep:thaw-request:0") - .append("room@conference.localhost") - .build(), + minidom::Element::builder( + "resurrection_room_jid", + "urn:chattermax:xep:thaw-request:0", + ) + .append("room@conference.localhost") + .build(), ) .append( - minidom::Element::builder("requestor_jid", "urn:chattermax:xep:thaw-request:0") - .append("user@localhost") - .build(), + minidom::Element::builder( + "requestor_jid", + "urn:chattermax:xep:thaw-request:0", + ) + .append("user@localhost") + .build(), ) .append( - minidom::Element::builder("requested_at", "urn:chattermax:xep:thaw-request:0") - .append("2024-01-01T12:01:00Z") - .build(), + minidom::Element::builder( + "requested_at", + "urn:chattermax:xep:thaw-request:0", + ) + .append("2024-01-01T12:01:00Z") + .build(), ) .append( minidom::Element::builder("timestamp", "jabber:x:chibi") @@ -591,6 +667,10 @@ mod tests { // Process the thaw request message let thaw_result = manager.process_thaw_request(&thaw_message).await; - assert!(thaw_result.is_ok(), "ThawRequest processing should succeed: {:?}", thaw_result.err()); + assert!( + thaw_result.is_ok(), + "ThawRequest processing should succeed: {:?}", + thaw_result.err() + ); } } diff --git a/chattermax-server/src/session.rs b/chattermax-server/src/session.rs index fa6a644..5f0517d 100644 --- a/chattermax-server/src/session.rs +++ b/chattermax-server/src/session.rs @@ -1,8 +1,8 @@ //! Client session management +use crate::sm::SmState; use chattermax_core::Jid; use chattermax_core::stream::StreamState; -use crate::sm::SmState; use std::sync::atomic::{AtomicU64, Ordering}; use tokio::sync::mpsc; diff --git a/chattermax-server/src/stream.rs b/chattermax-server/src/stream.rs index 658a1e1..193a2ee 100644 --- a/chattermax-server/src/stream.rs +++ b/chattermax-server/src/stream.rs @@ -20,7 +20,7 @@ use crate::session::Session; use crate::tls::XmppStream; use crate::xml::XmlBuilder; use crate::{auth, disco, mam, muc, roster}; -use chattermax_core::sm::{Enable, Enabled, AckRequest, Ack, Resume, Failed}; +use chattermax_core::sm::{Ack, AckRequest, Enable, Enabled, Failed, Resume}; /// Check if XML trace logging is enabled fn xml_trace_enabled() -> bool { @@ -758,7 +758,11 @@ async fn handle_presence( Ok(()) } -async fn handle_sm_enable(element: Element, session: &mut Session, router: &Arc) -> Result<()> { +async fn handle_sm_enable( + element: Element, + session: &mut Session, + router: &Arc, +) -> Result<()> { debug!("Handling SM enable request"); // Parse the enable stanza @@ -772,15 +776,20 @@ async fn handle_sm_enable(element: Element, session: &mut Session, router: &Arc< // Persist stream session to database for later resumption if let Some(jid) = &session.jid { - let unacked_stanzas: Vec = session.sm.unacked_stanzas.iter().cloned().collect(); - if let Err(e) = router.db().store_stream_session( - &token, - jid, - session.sm.inbound_count, - session.sm.outbound_count, - &unacked_stanzas, - 300, - ).await { + let unacked_stanzas: Vec = + session.sm.unacked_stanzas.iter().cloned().collect(); + if let Err(e) = router + .db() + .store_stream_session( + &token, + jid, + session.sm.inbound_count, + session.sm.outbound_count, + &unacked_stanzas, + 300, + ) + .await + { warn!(error = %e, "Failed to persist stream session to database"); } else { debug!("Stream session persisted to database"); @@ -788,9 +797,7 @@ async fn handle_sm_enable(element: Element, session: &mut Session, router: &Arc< } // Build and send enabled response - let enabled = Enabled::new(token) - .with_resume(true) - .with_max(300); + let enabled = Enabled::new(token).with_resume(true).with_max(300); let response_xml = enabled.to_xml(); trace_xml("send", &response_xml); @@ -842,7 +849,11 @@ fn handle_sm_ack(element: Element, session: &mut Session) -> Result<()> { Ok(ack) => { // Acknowledge stanzas up to h session.sm.acknowledge(ack.h); - debug!(h = ack.h, unacked_remaining = session.sm.unacked_stanzas.len(), "SM ack processed"); + debug!( + h = ack.h, + unacked_remaining = session.sm.unacked_stanzas.len(), + "SM ack processed" + ); metrics::record_stanza("a"); } Err(e) => { @@ -854,7 +865,11 @@ fn handle_sm_ack(element: Element, session: &mut Session) -> Result<()> { Ok(()) } -async fn handle_sm_resume(element: Element, session: &mut Session, router: &Arc) -> Result<()> { +async fn handle_sm_resume( + element: Element, + session: &mut Session, + router: &Arc, +) -> Result<()> { debug!("Handling SM resume"); // Parse the resume stanza @@ -888,7 +903,10 @@ async fn handle_sm_resume(element: Element, session: &mut Session, router: &Arc< } // Send Resumed response with h value - let resumed = chattermax_core::sm::Resumed::new(resume.previd.clone(), stored_session.last_handled_inbound); + let resumed = chattermax_core::sm::Resumed::new( + resume.previd.clone(), + stored_session.last_handled_inbound, + ); let response_xml = resumed.to_xml(); trace_xml("send", &response_xml); session.send(&response_xml)?; @@ -950,8 +968,8 @@ mod tests { #[tokio::test] async fn test_sm_enable_generates_valid_response() { - use tempfile::tempdir; use crate::db::Database; + use tempfile::tempdir; // Create a test database let dir = tempdir().unwrap(); @@ -976,8 +994,14 @@ mod tests { // Handle the enable stanza let result = handle_sm_enable(element, &mut session, &router).await; assert!(result.is_ok(), "handle_sm_enable should not error"); - assert!(session.sm_enabled(), "SM should be enabled after handle_sm_enable"); - assert!(session.sm.resumption_token.is_some(), "Resumption token should be set"); + assert!( + session.sm_enabled(), + "SM should be enabled after handle_sm_enable" + ); + assert!( + session.sm.resumption_token.is_some(), + "Resumption token should be set" + ); } #[test] @@ -998,7 +1022,11 @@ mod tests { // Handle the request let result = handle_sm_request(element, &mut session); assert!(result.is_ok(), "handle_sm_request should not error"); - assert_eq!(session.sm.get_inbound_count(), 3, "Inbound count should be 3"); + assert_eq!( + session.sm.get_inbound_count(), + 3, + "Inbound count should be 3" + ); } #[test] @@ -1008,9 +1036,15 @@ mod tests { // Enable SM and add some unacked stanzas session.enable_sm("test-token".to_string()); - session.sm.increment_outbound("".to_string()); - session.sm.increment_outbound("".to_string()); - session.sm.increment_outbound("".to_string()); + session + .sm + .increment_outbound("".to_string()); + session + .sm + .increment_outbound("".to_string()); + session + .sm + .increment_outbound("".to_string()); assert_eq!( session.sm.unacked_stanzas.len(), @@ -1036,7 +1070,10 @@ mod tests { fn test_extract_complete_element_sm_enable() { let xml = ""; let result = extract_complete_element(xml).expect("Should parse without error"); - assert!(result.is_some(), "Should extract complete SM enable element"); + assert!( + result.is_some(), + "Should extract complete SM enable element" + ); let (elem, remaining) = result.unwrap(); assert_eq!(elem.name(), "enable"); @@ -1047,7 +1084,10 @@ mod tests { fn test_extract_complete_element_sm_ack_request() { let xml = ""; let result = extract_complete_element(xml).expect("Should parse without error"); - assert!(result.is_some(), "Should extract complete SM request element"); + assert!( + result.is_some(), + "Should extract complete SM request element" + ); let (elem, remaining) = result.unwrap(); assert_eq!(elem.name(), "r"); @@ -1108,16 +1148,19 @@ mod tests { let xml = enabled.to_xml(); assert!(xml.contains("enabled"), "Should contain 'enabled' element"); - assert!(xml.contains(&token), "Should contain token"); - assert!(xml.contains("resume='true'"), "Should contain resume attribute"); + assert!(xml.contains(token), "Should contain token"); + assert!( + xml.contains("resume='true'"), + "Should contain resume attribute" + ); assert!(xml.contains("max='300'"), "Should contain max attribute"); } #[tokio::test] async fn test_sm_resume_success() { // Create a test database - use tempfile::tempdir; use crate::db::Database; + use tempfile::tempdir; let dir = tempdir().unwrap(); let path = dir.path().join("test.db"); @@ -1131,9 +1174,16 @@ mod tests { // Store a stream session let jid: Jid = "user@example.com/resource".parse().unwrap(); let token = "resumption-token-test"; - let unacked = vec!["".to_string(), "".to_string()]; + let unacked = vec![ + "".to_string(), + "".to_string(), + ]; - router.db().store_stream_session(token, &jid, 5, 10, &unacked, 300).await.unwrap(); + router + .db() + .store_stream_session(token, &jid, 5, 10, &unacked, 300) + .await + .unwrap(); // Create a resume stanza let resume_xml = format!("", token); @@ -1156,13 +1206,16 @@ mod tests { // Verify session was deleted from DB let retrieved = router.db().get_stream_session(token).await.unwrap(); - assert!(retrieved.is_none(), "Session should be deleted after resumption"); + assert!( + retrieved.is_none(), + "Session should be deleted after resumption" + ); } #[tokio::test] async fn test_sm_resume_invalid_token() { - use tempfile::tempdir; use crate::db::Database; + use tempfile::tempdir; let dir = tempdir().unwrap(); let path = dir.path().join("test_invalid.db"); @@ -1182,10 +1235,20 @@ mod tests { let mut session = Session::new(tx); let result = handle_sm_resume(element, &mut session, &router).await; - assert!(result.is_ok(), "handle_sm_resume should not error for invalid token"); + assert!( + result.is_ok(), + "handle_sm_resume should not error for invalid token" + ); // Verify that session was NOT restored (jid should be None) - assert!(session.jid.is_none(), "JID should not be set for failed resumption"); - assert_ne!(session.state, StreamState::Ready, "State should not be Ready for failed resumption"); + assert!( + session.jid.is_none(), + "JID should not be set for failed resumption" + ); + assert_ne!( + session.state, + StreamState::Ready, + "State should not be Ready for failed resumption" + ); } } diff --git a/chattermax-server/src/thaw/mod.rs b/chattermax-server/src/thaw/mod.rs index c5e0b51..581df3f 100644 --- a/chattermax-server/src/thaw/mod.rs +++ b/chattermax-server/src/thaw/mod.rs @@ -5,7 +5,7 @@ pub mod resurrection; -pub use resurrection::{ResurrectionService, ResurrectionError}; +pub use resurrection::{ResurrectionError, ResurrectionService}; use crate::freeze::FrozenAgentState; use chattermax_core::types::message::ThawRequest; @@ -258,7 +258,10 @@ mod tests { .expect("Failed to handle thaw request"); assert_eq!(result.frozen_agent.freeze_id, freeze_id.clone()); - assert_eq!(result.frozen_agent.agent_jid, format!("agent{}@test.local", i)); + assert_eq!( + result.frozen_agent.agent_jid, + format!("agent{}@test.local", i) + ); } } } diff --git a/chattermax-server/src/thaw/resurrection.rs b/chattermax-server/src/thaw/resurrection.rs index 3d9f86d..0340480 100644 --- a/chattermax-server/src/thaw/resurrection.rs +++ b/chattermax-server/src/thaw/resurrection.rs @@ -79,18 +79,13 @@ impl ResurrectionService { // Write conversation context to temporary file let context_temp_file = self.write_context_file(&resurrection_context)?; - debug!( - "Conversation context written to: {}", - context_temp_file - ); + debug!("Conversation context written to: {}", context_temp_file); // Build environment variables for Chibi let env_vars = self.build_environment(&thaw_data, &context_temp_file); // Spawn Chibi process with context - let pid = self - .spawn_chibi_process(&env_vars) - .await?; + let pid = self.spawn_chibi_process(&env_vars).await?; info!( freeze_id = %thaw_data.frozen_agent.freeze_id, @@ -103,7 +98,10 @@ impl ResurrectionService { } /// Prepares the resurrection context data from ThawData - fn prepare_resurrection_context(&self, thaw_data: &ThawData) -> Result { + fn prepare_resurrection_context( + &self, + thaw_data: &ThawData, + ) -> Result { let frozen = &thaw_data.frozen_agent; let request = &thaw_data.thaw_request; @@ -128,16 +126,10 @@ impl ResurrectionService { } /// Writes the resurrection context to a temporary file - fn write_context_file( - &self, - context: &serde_json::Value, - ) -> Result { + fn write_context_file(&self, context: &serde_json::Value) -> Result { // Create a temporary file for the context let temp_file = tempfile::NamedTempFile::new()?; - let temp_path = temp_file - .path() - .to_string_lossy() - .to_string(); + let temp_path = temp_file.path().to_string_lossy().to_string(); // Serialize context to JSON and write to file let json_str = serde_json::to_string_pretty(context)?; @@ -164,7 +156,10 @@ impl ResurrectionService { let mut env_vars = HashMap::new(); // Set context path - env_vars.insert("CHIBI_CONTEXT_PATH".to_string(), context_file_path.to_string()); + env_vars.insert( + "CHIBI_CONTEXT_PATH".to_string(), + context_file_path.to_string(), + ); // Set resurrection-specific environment variables env_vars.insert( @@ -199,10 +194,7 @@ impl ResurrectionService { &self, env_vars: &HashMap, ) -> Result { - debug!( - "Spawning Chibi process: {}", - self.chibi_executable - ); + debug!("Spawning Chibi process: {}", self.chibi_executable); let mut cmd = Command::new(&self.chibi_executable); @@ -216,22 +208,15 @@ impl ResurrectionService { cmd.stderr(Stdio::piped()); // Spawn the process - let child = cmd - .spawn() - .map_err(|e| { - warn!( - "Failed to spawn Chibi process: {}", - e - ); - ResurrectionError::SpawnError(format!("{}: {}", self.chibi_executable, e)) - })?; - - let pid = child - .id() - .ok_or_else(|| { - warn!("Could not get process ID from spawned Chibi"); - ResurrectionError::SpawnError("Could not get process ID".to_string()) - })?; + let child = cmd.spawn().map_err(|e| { + warn!("Failed to spawn Chibi process: {}", e); + ResurrectionError::SpawnError(format!("{}: {}", self.chibi_executable, e)) + })?; + + let pid = child.id().ok_or_else(|| { + warn!("Could not get process ID from spawned Chibi"); + ResurrectionError::SpawnError("Could not get process ID".to_string()) + })?; info!("Chibi process spawned with PID: {}", pid); @@ -248,8 +233,8 @@ impl Default for ResurrectionService { #[cfg(test)] mod tests { use super::*; - use chattermax_core::types::message::{ConversationContext, FreezeReason, Metadata}; use crate::freeze::FrozenAgentState; + use chattermax_core::types::message::{ConversationContext, FreezeReason, Metadata}; fn create_test_thaw_data() -> ThawData { use crate::thaw::ThawData; @@ -303,14 +288,8 @@ mod tests { .expect("Failed to prepare context"); // Verify context contains expected fields - assert_eq!( - context["freeze_id"].as_str(), - Some("test-freeze-123") - ); - assert_eq!( - context["agent_jid"].as_str(), - Some("agent@test.local") - ); + assert_eq!(context["freeze_id"].as_str(), Some("test-freeze-123")); + assert_eq!(context["agent_jid"].as_str(), Some("agent@test.local")); assert_eq!( context["conversation_context"]["room_jid"].as_str(), Some("room@test.local") @@ -330,15 +309,11 @@ mod tests { .expect("Failed to write context file"); // Verify file exists and contains valid JSON - let contents = std::fs::read_to_string(&file_path) - .expect("Failed to read context file"); - let parsed: serde_json::Value = serde_json::from_str(&contents) - .expect("Context file does not contain valid JSON"); + let contents = std::fs::read_to_string(&file_path).expect("Failed to read context file"); + let parsed: serde_json::Value = + serde_json::from_str(&contents).expect("Context file does not contain valid JSON"); - assert_eq!( - parsed["freeze_id"].as_str(), - Some("test-freeze-123") - ); + assert_eq!(parsed["freeze_id"].as_str(), Some("test-freeze-123")); // Clean up let _ = std::fs::remove_file(&file_path); diff --git a/chattermax-server/src/tls.rs b/chattermax-server/src/tls.rs index da3bd0a..c5628f7 100644 --- a/chattermax-server/src/tls.rs +++ b/chattermax-server/src/tls.rs @@ -17,6 +17,10 @@ use crate::config::TlsConfig; /// Load TLS configuration from certificate and key files pub fn load_tls_config(config: &TlsConfig) -> Result> { + // Install ring as the default crypto provider (required for rustls 0.23+) + // This is idempotent - if already installed, it returns an error we can ignore + let _ = rustls::crypto::ring::default_provider().install_default(); + let certs = load_certs(&config.cert_path)?; let key = load_private_key(&config.key_path)?; diff --git a/chattermax-server/tests/common/xmpp_client.rs b/chattermax-server/tests/common/xmpp_client.rs index 9eafcca..14889a8 100644 --- a/chattermax-server/tests/common/xmpp_client.rs +++ b/chattermax-server/tests/common/xmpp_client.rs @@ -173,6 +173,7 @@ impl XmppTestClient { } /// Establish session (legacy, but some clients need it) + #[allow(dead_code)] pub async fn establish_session(&mut self) -> io::Result { let session_xml = ""; self.send(session_xml).await?; @@ -262,6 +263,7 @@ impl XmppTestClient { } /// Request roster + #[allow(dead_code)] pub async fn get_roster(&mut self) -> io::Result { let roster_xml = ""; self.send(roster_xml).await?; @@ -276,6 +278,7 @@ impl XmppTestClient { } /// Helper to check if response indicates success +#[allow(dead_code)] pub fn is_success(response: &str) -> bool { response.contains(" bool { } /// Helper to check if response indicates failure +#[allow(dead_code)] pub fn is_failure(response: &str) -> bool { response.contains(" extract context_ref -> parse -> create resolver #[test] fn test_context_resolution_flow() { - use chattermax_server::hooks::filter; use chattermax_server::ServerContextResolver; + use chattermax_server::hooks::filter; use std::str::FromStr; // Create a message with context_ref @@ -142,7 +145,9 @@ fn test_context_resolution_flow() { let vars = filter::extract_variables(&message); // Verify context_ref is present - let context_ref_str = vars.get("context_ref").expect("context_ref should be extracted"); + let context_ref_str = vars + .get("context_ref") + .expect("context_ref should be extracted"); // Parse the context reference let context_ref = @@ -161,10 +166,10 @@ fn test_context_resolution_flow() { /// Test extraction of context_ref from nested custom message elements using serialization #[test] fn test_extract_variables_nested_context_ref_with_serialization() { - use chattermax_server::hooks::filter; - use chattermax_core::types::serialization::to_xml; - use chattermax_core::types::message::{Message, ToolCall, Metadata}; use chattermax_core::types::ContextRef; + use chattermax_core::types::message::{Message, Metadata, ToolCall}; + use chattermax_core::types::serialization::to_xml; + use chattermax_server::hooks::filter; use std::str::FromStr; // Create a ToolCall message with context_ref in metadata @@ -182,8 +187,8 @@ fn test_extract_variables_nested_context_ref_with_serialization() { }; // Serialize to XML (this creates the nested structure) - let xml_element = to_xml(&Message::ToolCall(tool_call)) - .expect("Should serialize to XML successfully"); + let xml_element = + to_xml(&Message::ToolCall(tool_call)).expect("Should serialize to XML successfully"); // Now wrap it in a message element let message = minidom::Element::builder("message", "jabber:client") @@ -198,7 +203,10 @@ fn test_extract_variables_nested_context_ref_with_serialization() { let vars = filter::extract_variables(&message); // Verify all standard attributes - assert_eq!(vars.get("from").map(|s| s.as_str()), Some("deployer@example.com")); + assert_eq!( + vars.get("from").map(|s| s.as_str()), + Some("deployer@example.com") + ); assert_eq!( vars.get("to").map(|s| s.as_str()), Some("deploy/channel@conference.example.com") @@ -217,8 +225,8 @@ fn test_extract_variables_nested_context_ref_with_serialization() { assert_eq!(extracted_context_ref, "chizu://project-gamma/deployment@v2"); // Verify we can parse the extracted context_ref - let parsed = ContextRef::from_str(extracted_context_ref) - .expect("Should parse extracted context_ref"); + let parsed = + ContextRef::from_str(extracted_context_ref).expect("Should parse extracted context_ref"); assert_eq!(parsed.context_id, "project-gamma"); assert_eq!(parsed.section, Some("deployment".to_string())); assert_eq!(parsed.version, Some("v2".to_string())); diff --git a/chattermax-server/tests/freeze_thaw_integration.rs b/chattermax-server/tests/freeze_thaw_integration.rs index 1e7de40..0b46dc1 100644 --- a/chattermax-server/tests/freeze_thaw_integration.rs +++ b/chattermax-server/tests/freeze_thaw_integration.rs @@ -6,68 +6,74 @@ use chattermax_server::hooks::config::HookConfig; use chattermax_server::hooks::manager::HookManager; /// Helper function to create a freeze notification message element -fn create_freeze_notification_element( - agent_jid: &str, - room_jid: &str, -) -> minidom::Element { +fn create_freeze_notification_element(agent_jid: &str, room_jid: &str) -> minidom::Element { minidom::Element::builder("message", "jabber:client") .attr("from", agent_jid) .attr("to", room_jid) .attr("type", "groupchat") .append( - minidom::Element::builder("freeze_notification", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") - .append(agent_jid) - .build(), - ) - .append( - minidom::Element::builder("reason", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("type", "urn:chattermax:xep:freeze-notification:0") - .append("task_complete") - .build(), - ) - .build(), - ) - .append( - minidom::Element::builder( - "conversation_context", - "urn:chattermax:xep:freeze-notification:0", - ) - .append( - minidom::Element::builder("room_jid", "urn:chattermax:xep:freeze-notification:0") - .append(room_jid) - .build(), - ) + minidom::Element::builder( + "freeze_notification", + "urn:chattermax:xep:freeze-notification:0", + ) + .append( + minidom::Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") + .append(agent_jid) + .build(), + ) + .append( + minidom::Element::builder("reason", "urn:chattermax:xep:freeze-notification:0") .append( minidom::Element::builder( - "participants", + "type", "urn:chattermax:xep:freeze-notification:0", ) + .append("task_complete") .build(), ) - .append( - minidom::Element::builder( - "last_message_id", - "urn:chattermax:xep:freeze-notification:0", - ) - .append("msg-123") - .build(), + .build(), + ) + .append( + minidom::Element::builder( + "conversation_context", + "urn:chattermax:xep:freeze-notification:0", + ) + .append( + minidom::Element::builder( + "room_jid", + "urn:chattermax:xep:freeze-notification:0", ) + .append(room_jid) .build(), ) .append( - minidom::Element::builder("frozen_at", "urn:chattermax:xep:freeze-notification:0") - .append("2024-01-01T12:00:00Z") - .build(), + minidom::Element::builder( + "participants", + "urn:chattermax:xep:freeze-notification:0", + ) + .build(), ) .append( - minidom::Element::builder("timestamp", "jabber:x:chibi") - .append("2024-01-01T12:00:00Z") - .build(), + minidom::Element::builder( + "last_message_id", + "urn:chattermax:xep:freeze-notification:0", + ) + .append("msg-123") + .build(), ) .build(), + ) + .append( + minidom::Element::builder("frozen_at", "urn:chattermax:xep:freeze-notification:0") + .append("2024-01-01T12:00:00Z") + .build(), + ) + .append( + minidom::Element::builder("timestamp", "jabber:x:chibi") + .append("2024-01-01T12:00:00Z") + .build(), + ) + .build(), ) .build() } @@ -91,9 +97,12 @@ fn create_thaw_request_element( .build(), ) .append( - minidom::Element::builder("target_agent_jid", "urn:chattermax:xep:thaw-request:0") - .append(agent_jid) - .build(), + minidom::Element::builder( + "target_agent_jid", + "urn:chattermax:xep:thaw-request:0", + ) + .append(agent_jid) + .build(), ) .append( minidom::Element::builder( @@ -126,9 +135,7 @@ fn create_thaw_request_element( #[tokio::test] async fn test_complete_freeze_thaw_workflow() { // Create a hook manager - let config = HookConfig { - exec_hooks: None, - }; + let config = HookConfig { exec_hooks: None }; let manager = HookManager::new(config); let agent_jid = "agent@localhost"; @@ -177,82 +184,82 @@ async fn test_complete_freeze_thaw_workflow() { #[tokio::test] async fn test_freeze_thaw_with_missing_optional_context() { - let config = HookConfig { - exec_hooks: None, - }; + let config = HookConfig { exec_hooks: None }; let manager = HookManager::new(config); let agent_jid = "agent2@localhost"; let room_jid = "room2@conference.localhost"; // Create freeze notification without active_context_ref - let freeze_message = - minidom::Element::builder("message", "jabber:client") - .attr("from", agent_jid) - .attr("to", room_jid) - .attr("type", "groupchat") + let freeze_message = minidom::Element::builder("message", "jabber:client") + .attr("from", agent_jid) + .attr("to", room_jid) + .attr("type", "groupchat") + .append( + minidom::Element::builder( + "freeze_notification", + "urn:chattermax:xep:freeze-notification:0", + ) + .append( + minidom::Element::builder("agent_jid", "urn:chattermax:xep:freeze-notification:0") + .append(agent_jid) + .build(), + ) + .append( + minidom::Element::builder("reason", "urn:chattermax:xep:freeze-notification:0") + .append( + minidom::Element::builder( + "type", + "urn:chattermax:xep:freeze-notification:0", + ) + .append("timeout") + .build(), + ) + .build(), + ) .append( minidom::Element::builder( - "freeze_notification", + "conversation_context", "urn:chattermax:xep:freeze-notification:0", ) .append( minidom::Element::builder( - "agent_jid", + "room_jid", "urn:chattermax:xep:freeze-notification:0", ) - .append(agent_jid) + .append(room_jid) .build(), ) - .append( - minidom::Element::builder("reason", "urn:chattermax:xep:freeze-notification:0") - .append( - minidom::Element::builder("type", "urn:chattermax:xep:freeze-notification:0") - .append("timeout") - .build(), - ) - .build(), - ) .append( minidom::Element::builder( - "conversation_context", + "participants", "urn:chattermax:xep:freeze-notification:0", ) - .append( - minidom::Element::builder("room_jid", "urn:chattermax:xep:freeze-notification:0") - .append(room_jid) - .build(), - ) - .append( - minidom::Element::builder( - "participants", - "urn:chattermax:xep:freeze-notification:0", - ) - .build(), - ) - .append( - minidom::Element::builder( - "last_message_id", - "urn:chattermax:xep:freeze-notification:0", - ) - .append("msg-456") - .build(), - ) .build(), ) .append( - minidom::Element::builder("frozen_at", "urn:chattermax:xep:freeze-notification:0") - .append("2024-01-01T13:00:00Z") - .build(), - ) - .append( - minidom::Element::builder("timestamp", "jabber:x:chibi") - .append("2024-01-01T13:00:00Z") - .build(), + minidom::Element::builder( + "last_message_id", + "urn:chattermax:xep:freeze-notification:0", + ) + .append("msg-456") + .build(), ) .build(), ) - .build(); + .append( + minidom::Element::builder("frozen_at", "urn:chattermax:xep:freeze-notification:0") + .append("2024-01-01T13:00:00Z") + .build(), + ) + .append( + minidom::Element::builder("timestamp", "jabber:x:chibi") + .append("2024-01-01T13:00:00Z") + .build(), + ) + .build(), + ) + .build(); // Process freeze notification let freeze_result = manager.process_message(&freeze_message).await; @@ -288,9 +295,7 @@ async fn test_freeze_thaw_with_missing_optional_context() { #[tokio::test] async fn test_thaw_request_with_nonexistent_freeze_id() { - let config = HookConfig { - exec_hooks: None, - }; + let config = HookConfig { exec_hooks: None }; let manager = HookManager::new(config); let agent_jid = "agent@localhost"; @@ -299,12 +304,8 @@ async fn test_thaw_request_with_nonexistent_freeze_id() { let nonexistent_freeze_id = "nonexistent-freeze-id-123"; // Send ThawRequest without first creating a frozen agent - let thaw_message = create_thaw_request_element( - nonexistent_freeze_id, - agent_jid, - room_jid, - requestor_jid, - ); + let thaw_message = + create_thaw_request_element(nonexistent_freeze_id, agent_jid, room_jid, requestor_jid); // This should fail gracefully because the freeze_id doesn't exist let thaw_result = manager.process_message(&thaw_message).await; diff --git a/chattermax-server/tests/stream_management_integration.rs b/chattermax-server/tests/stream_management_integration.rs index 20a261d..aecd4ae 100644 --- a/chattermax-server/tests/stream_management_integration.rs +++ b/chattermax-server/tests/stream_management_integration.rs @@ -69,10 +69,16 @@ async fn test_sm_enable_flow() { // Send enable request with resume='true' let enable_request = ""; - client.send(enable_request).await.expect("Failed to send enable"); + client + .send(enable_request) + .await + .expect("Failed to send enable"); // Receive enabled response - let enable_response = client.read_response(5000).await.expect("Failed to read response"); + let enable_response = client + .read_response(5000) + .await + .expect("Failed to read response"); eprintln!("[SM] Enable response: {}", enable_response); // Verify response contains element @@ -112,7 +118,10 @@ async fn test_sm_enable_flow() { let id_start_pos = id_start + 4; // Find the quote character used - let quote_char = enable_response.chars().nth(id_start_pos - 1).expect("Should have quote"); + let quote_char = enable_response + .chars() + .nth(id_start_pos - 1) + .expect("Should have quote"); let id_end = enable_response[id_start_pos..] .find(quote_char) .expect("Should find closing quote"); @@ -138,10 +147,16 @@ async fn test_sm_enable_without_resume() { // Send enable request without resume attribute let enable_request = ""; - client.send(enable_request).await.expect("Failed to send enable"); + client + .send(enable_request) + .await + .expect("Failed to send enable"); // Receive enabled response - let enable_response = client.read_response(5000).await.expect("Failed to read response"); + let enable_response = client + .read_response(5000) + .await + .expect("Failed to read response"); eprintln!("[SM] Enable response (no resume): {}", enable_response); // Verify response contains element @@ -178,16 +193,28 @@ async fn test_sm_ack_request_response() { // Enable SM let enable_request = ""; - client.send(enable_request).await.expect("Failed to send enable"); - let enable_resp = client.read_response(5000).await.expect("Failed to read enable response"); + client + .send(enable_request) + .await + .expect("Failed to send enable"); + let enable_resp = client + .read_response(5000) + .await + .expect("Failed to read enable response"); eprintln!("[SM] Enable response: {}", enable_resp); // Request ack from server (SM is now enabled and tracking stanzas) let ack_request = ""; - client.send(ack_request).await.expect("Failed to send ack request"); + client + .send(ack_request) + .await + .expect("Failed to send ack request"); // Receive ack response - give more time since the server might be processing - let ack_response = client.read_response(10000).await.expect("Failed to read ack response"); + let ack_response = client + .read_response(10000) + .await + .expect("Failed to read ack response"); eprintln!("[SM] Ack response: {}", ack_response); // Verify response contains element (allow for various quote styles) @@ -230,8 +257,14 @@ async fn test_sm_multiple_messages_with_ack() { // Enable SM let enable_request = ""; - client.send(enable_request).await.expect("Failed to send enable"); - let enable_response = client.read_response(5000).await.expect("Failed to read enable response"); + client + .send(enable_request) + .await + .expect("Failed to send enable"); + let enable_response = client + .read_response(5000) + .await + .expect("Failed to read enable response"); eprintln!("[SM] Enable response: {}", enable_response); // Send 3 messages @@ -247,10 +280,16 @@ async fn test_sm_multiple_messages_with_ack() { // Request ack let ack_request = ""; - client.send(ack_request).await.expect("Failed to send ack request"); + client + .send(ack_request) + .await + .expect("Failed to send ack request"); // Receive ack response - let ack_response = client.read_response(5000).await.expect("Failed to read ack response"); + let ack_response = client + .read_response(5000) + .await + .expect("Failed to read ack response"); eprintln!("[SM] Ack after 3 messages: {}", ack_response); // Should have h >= 3 (at least 3 stanzas handled) @@ -281,12 +320,17 @@ async fn test_sm_resume_invalid_token() { .expect("Login failed"); // Try to resume with invalid token - let resume_request = - ""; - client.send(resume_request).await.expect("Failed to send resume"); + let resume_request = ""; + client + .send(resume_request) + .await + .expect("Failed to send resume"); // Should receive - let response = client.read_response(5000).await.expect("Failed to read response"); + let response = client + .read_response(5000) + .await + .expect("Failed to read response"); eprintln!("[SM] Resume with invalid token response: {}", response); // Response should contain either or indicate error @@ -317,9 +361,15 @@ async fn test_sm_resume_fresh_session_flow() { // Enable SM and extract the resumption token let enable_request = ""; - client.send(enable_request).await.expect("Failed to send enable"); + client + .send(enable_request) + .await + .expect("Failed to send enable"); - let enable_response = client.read_response(5000).await.expect("Failed to read enable response"); + let enable_response = client + .read_response(5000) + .await + .expect("Failed to read enable response"); eprintln!("[SM] Enable response: {}", enable_response); // Parse the SM id - handle both single and double quotes @@ -328,7 +378,10 @@ async fn test_sm_resume_fresh_session_flow() { .or_else(|| enable_response.find("id=\"")) .expect("Should find id attribute"); let sm_id_start_pos = sm_id_start + 4; - let quote_char = enable_response.chars().nth(sm_id_start_pos - 1).expect("Should have quote"); + let quote_char = enable_response + .chars() + .nth(sm_id_start_pos - 1) + .expect("Should have quote"); let sm_id_end = enable_response[sm_id_start_pos..] .find(quote_char) .expect("Should find closing quote"); @@ -347,9 +400,15 @@ async fn test_sm_resume_fresh_session_flow() { // Request ack to establish h value let ack_request = ""; - client.send(ack_request).await.expect("Failed to send ack request"); + client + .send(ack_request) + .await + .expect("Failed to send ack request"); - let ack_response = client.read_response(5000).await.expect("Failed to read ack response"); + let ack_response = client + .read_response(5000) + .await + .expect("Failed to read ack response"); eprintln!("[SM] Ack response before resume: {}", ack_response); // ========== SIMULATE DISCONNECT & RECONNECT ========== @@ -366,31 +425,40 @@ async fn test_sm_resume_fresh_session_flow() { // Open stream let stream_response = client2.open_stream().await.expect("Failed to open stream"); - eprintln!("[SM] Post-reconnect stream: {}", &stream_response[..stream_response.len().min(200)]); + eprintln!( + "[SM] Post-reconnect stream: {}", + &stream_response[..stream_response.len().min(200)] + ); // Authenticate (second connection) let auth_result = client2 .auth_plain("alice", "password123") .await .expect("Auth failed"); - assert!(auth_result.contains("", - sm_id - ); + let resume_request = format!("", sm_id); client2 .send(&resume_request) .await .expect("Failed to send resume"); // Receive resume response - let resume_response = client2.read_response(5000).await.expect("Failed to read resume response"); + let resume_response = client2 + .read_response(5000) + .await + .expect("Failed to read resume response"); eprintln!("[SM] Resume response: {}", resume_response); // Should contain either (success) or (token not found) @@ -399,7 +467,10 @@ async fn test_sm_resume_fresh_session_flow() { let is_resumed = resume_response.contains(" or . Got: {}", @@ -433,8 +504,14 @@ async fn test_sm_counter_wrapping_theoretical() { // Enable SM let enable_request = ""; - client.send(enable_request).await.expect("Failed to send enable"); - let enable_response = client.read_response(5000).await.expect("Failed to read enable response"); + client + .send(enable_request) + .await + .expect("Failed to send enable"); + let enable_response = client + .read_response(5000) + .await + .expect("Failed to read enable response"); // Verify server is tracking counters correctly assert!(enable_response.contains("