diff --git a/server/src/chat/transport/slack.rs b/server/src/chat/transport/slack.rs index 262e7282..d6701088 100644 --- a/server/src/chat/transport/slack.rs +++ b/server/src/chat/transport/slack.rs @@ -863,6 +863,28 @@ async fn handle_incoming_message( return; } + if crate::chat::transport::matrix::reset::extract_reset_command( + message, + &ctx.bot_name, + &ctx.bot_user_id, + ) + .is_some() + { + slog!("[slack] Handling reset command from {user} in {channel}"); + { + let mut guard = ctx.history.lock().await; + let conv = guard.entry(channel.to_string()).or_insert_with(RoomConversation::default); + conv.session_id = None; + conv.entries.clear(); + save_slack_history(&ctx.project_root, &guard); + } + let _ = ctx + .transport + .send_message(channel, "Session cleared.", "") + .await; + return; + } + // No command matched — forward to LLM for conversational response. slog!("[slack] No command matched, forwarding to LLM for {user} in {channel}"); handle_llm_message(ctx, channel, user, message).await; @@ -1725,4 +1747,63 @@ mod tests { ); assert!(result.is_none(), "'status' should not be recognised as rebuild"); } + + // ── reset command extraction ─────────────────────────────────────── + + #[test] + fn reset_command_extracted_from_slack_message() { + let result = crate::chat::transport::matrix::reset::extract_reset_command( + "Storkit reset", + "Storkit", + "slack-bot", + ); + assert!(result.is_some(), "'Storkit reset' should be recognised"); + } + + #[test] + fn reset_command_extracted_plain_no_mention() { + let result = crate::chat::transport::matrix::reset::extract_reset_command( + "reset", + "Storkit", + "slack-bot", + ); + assert!(result.is_some(), "plain 'reset' should be recognised"); + } + + #[tokio::test] + async fn reset_command_clears_slack_session() { + use std::sync::Arc; + use tokio::sync::Mutex as TokioMutex; + + let channel = "C01ABCDEF"; + let history: SlackConversationHistory = Arc::new(TokioMutex::new({ + let mut m = HashMap::new(); + m.insert(channel.to_string(), RoomConversation { + session_id: Some("old-session".to_string()), + entries: vec![ConversationEntry { + role: ConversationRole::User, + sender: "U01GHIJKL".to_string(), + content: "previous message".to_string(), + }], + }); + m + })); + + let tmp = tempfile::tempdir().unwrap(); + let sk = tmp.path().join(".storkit"); + std::fs::create_dir_all(&sk).unwrap(); + + { + let mut guard = history.lock().await; + let conv = guard.entry(channel.to_string()).or_insert_with(RoomConversation::default); + conv.session_id = None; + conv.entries.clear(); + save_slack_history(tmp.path(), &guard); + } + + let guard = history.lock().await; + let conv = guard.get(channel).unwrap(); + assert!(conv.session_id.is_none(), "session_id should be cleared"); + assert!(conv.entries.is_empty(), "entries should be cleared"); + } } diff --git a/server/src/chat/transport/whatsapp.rs b/server/src/chat/transport/whatsapp.rs index c1cea399..805654ea 100644 --- a/server/src/chat/transport/whatsapp.rs +++ b/server/src/chat/transport/whatsapp.rs @@ -1124,6 +1124,28 @@ async fn handle_incoming_message(ctx: &WhatsAppWebhookContext, sender: &str, mes return; } + if crate::chat::transport::matrix::reset::extract_reset_command( + message, + &ctx.bot_name, + &ctx.bot_user_id, + ) + .is_some() + { + slog!("[whatsapp] Handling reset command from {sender}"); + { + let mut guard = ctx.history.lock().await; + let conv = guard.entry(sender.to_string()).or_insert_with(RoomConversation::default); + conv.session_id = None; + conv.entries.clear(); + save_whatsapp_history(&ctx.project_root, &guard); + } + let _ = ctx + .transport + .send_message(sender, "Session cleared.", "") + .await; + return; + } + // No command matched — forward to LLM for conversational response. slog!("[whatsapp] No command matched, forwarding to LLM for {sender}"); handle_llm_message(ctx, sender, message).await; @@ -2127,4 +2149,63 @@ mod tests { assert!(loaded.contains_key("222")); assert_eq!(loaded["222"].session_id.as_deref(), Some("sess-222")); } + + // ── reset command extraction ─────────────────────────────────────── + + #[test] + fn reset_command_extracted_from_plain_message() { + let result = crate::chat::transport::matrix::reset::extract_reset_command( + "reset", + "Timmy", + "@timmy:home.local", + ); + assert!(result.is_some(), "plain 'reset' should be recognised"); + } + + #[test] + fn reset_command_extracted_with_bot_name_prefix() { + let result = crate::chat::transport::matrix::reset::extract_reset_command( + "Timmy reset", + "Timmy", + "@timmy:home.local", + ); + assert!(result.is_some(), "'Timmy reset' should be recognised"); + } + + #[tokio::test] + async fn reset_command_clears_whatsapp_session() { + use std::sync::Arc; + use tokio::sync::Mutex as TokioMutex; + + let sender = "+15555550100"; + let history: WhatsAppConversationHistory = Arc::new(TokioMutex::new({ + let mut m = HashMap::new(); + m.insert(sender.to_string(), RoomConversation { + session_id: Some("old-session".to_string()), + entries: vec![ConversationEntry { + role: ConversationRole::User, + sender: sender.to_string(), + content: "previous message".to_string(), + }], + }); + m + })); + + let tmp = tempfile::tempdir().unwrap(); + let sk = tmp.path().join(".storkit"); + std::fs::create_dir_all(&sk).unwrap(); + + { + let mut guard = history.lock().await; + let conv = guard.entry(sender.to_string()).or_insert_with(RoomConversation::default); + conv.session_id = None; + conv.entries.clear(); + save_whatsapp_history(tmp.path(), &guard); + } + + let guard = history.lock().await; + let conv = guard.get(sender).unwrap(); + assert!(conv.session_id.is_none(), "session_id should be cleared"); + assert!(conv.entries.is_empty(), "entries should be cleared"); + } }