//! Handler for the `ambient` command. use super::CommandContext; use crate::chat::transport::matrix::config::save_ambient_rooms; /// Toggle ambient mode for this room. /// /// Works whether or not the message directly addressed the bot — the user can /// say "timmy ambient on", "@timmy ambient on", or just "ambient on" in an /// ambient-mode room. The command is specific enough (must be the first word /// after any bot-mention prefix) that accidental triggering is very unlikely. pub(super) fn handle_ambient(ctx: &CommandContext) -> Option { let enable = match ctx.args { "on" => true, "off" => false, _ => return Some("Usage: `ambient on` or `ambient off`".to_string()), }; let room_ids: Vec = { let mut ambient = ctx.services.ambient_rooms.lock().unwrap(); if enable { ambient.insert(ctx.room_id.to_string()); } else { ambient.remove(ctx.room_id); } ambient.iter().cloned().collect() }; save_ambient_rooms(ctx.effective_root(), &room_ids); let msg = if enable { "Ambient mode on. I'll respond to all messages in this room." } else { "Ambient mode off. I'll only respond when mentioned." }; Some(msg.to_string()) } #[cfg(test)] mod tests { use super::super::{CommandDispatch, try_handle_command}; // Bug 352: ambient commands were being forwarded to LLM after refactors // 328/330 because handle_ambient required is_addressed=true, but // mentions_bot() only matches @-prefixed mentions, not bare bot names. // "timmy ambient off" sets is_addressed=false even though it names the bot. #[test] fn ambient_on_works_when_unaddressed() { let services = crate::services::Services::new_test( std::path::PathBuf::from("/tmp"), "Timmy".to_string(), ); let room_id = "!myroom:example.com".to_string(); let dispatch = CommandDispatch { services: &services, project_root: &services.project_root, bot_user_id: "@timmy:homeserver.local", room_id: &room_id, }; // "timmy ambient on" — bot name mentioned but not @-prefixed, so // is_addressed is false; strip_bot_mention still strips "timmy ". let result = try_handle_command(&dispatch, "timmy ambient on"); assert!( result.is_some(), "ambient on should fire even when is_addressed=false" ); assert!( services.ambient_rooms.lock().unwrap().contains(&room_id), "room should be in ambient_rooms after ambient on" ); } #[test] fn ambient_off_works_bare_in_ambient_room() { let services = crate::services::Services::new_test( std::path::PathBuf::from("/tmp"), "Timmy".to_string(), ); let room_id = "!myroom:example.com".to_string(); services .ambient_rooms .lock() .unwrap() .insert(room_id.clone()); let dispatch = CommandDispatch { services: &services, project_root: &services.project_root, bot_user_id: "@timmy:homeserver.local", room_id: &room_id, }; // Bare "ambient off" in an ambient room (is_addressed=false). let result = try_handle_command(&dispatch, "ambient off"); assert!( result.is_some(), "bare ambient off should be handled without LLM" ); let output = result.unwrap(); assert!( output.contains("Ambient mode off"), "response should confirm ambient off: {output}" ); assert!( !services.ambient_rooms.lock().unwrap().contains(&room_id), "room should be removed from ambient_rooms after ambient off" ); } #[test] fn ambient_on_enables_ambient_mode() { let services = crate::services::Services::new_test( std::path::PathBuf::from("/tmp"), "Timmy".to_string(), ); let room_id = "!myroom:example.com".to_string(); let dispatch = CommandDispatch { services: &services, project_root: &services.project_root, bot_user_id: "@timmy:homeserver.local", room_id: &room_id, }; let result = try_handle_command(&dispatch, "@timmy ambient on"); assert!(result.is_some(), "ambient on should produce a response"); let output = result.unwrap(); assert!( output.contains("Ambient mode on"), "response should confirm ambient on: {output}" ); assert!( services.ambient_rooms.lock().unwrap().contains(&room_id), "room should be in ambient_rooms after ambient on" ); } #[test] fn ambient_off_disables_ambient_mode() { let services = crate::services::Services::new_test( std::path::PathBuf::from("/tmp"), "Timmy".to_string(), ); let room_id = "!myroom:example.com".to_string(); // Pre-insert the room services .ambient_rooms .lock() .unwrap() .insert(room_id.clone()); let dispatch = CommandDispatch { services: &services, project_root: &services.project_root, bot_user_id: "@timmy:homeserver.local", room_id: &room_id, }; let result = try_handle_command(&dispatch, "@timmy ambient off"); assert!(result.is_some(), "ambient off should produce a response"); let output = result.unwrap(); assert!( output.contains("Ambient mode off"), "response should confirm ambient off: {output}" ); assert!( !services.ambient_rooms.lock().unwrap().contains(&room_id), "room should be removed from ambient_rooms after ambient off" ); } #[test] fn ambient_invalid_args_returns_usage() { let result = super::super::tests::try_cmd_addressed( "Timmy", "@timmy:homeserver.local", "@timmy ambient", ); let output = result.unwrap(); assert!( output.contains("Usage"), "invalid ambient args should show usage: {output}" ); } }