172 lines
6.3 KiB
Rust
172 lines
6.3 KiB
Rust
|
|
//! Handler for the `ambient` command.
|
||
|
|
|
||
|
|
use super::CommandContext;
|
||
|
|
use crate::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<String> {
|
||
|
|
let enable = match ctx.args {
|
||
|
|
"on" => true,
|
||
|
|
"off" => false,
|
||
|
|
_ => return Some("Usage: `ambient on` or `ambient off`".to_string()),
|
||
|
|
};
|
||
|
|
let room_ids: Vec<String> = {
|
||
|
|
let mut ambient = ctx.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.project_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 crate::agents::AgentPool;
|
||
|
|
use std::collections::HashSet;
|
||
|
|
use std::sync::{Arc, Mutex};
|
||
|
|
|
||
|
|
use super::super::{CommandDispatch, try_handle_command};
|
||
|
|
|
||
|
|
fn test_ambient_rooms() -> Arc<Mutex<HashSet<String>>> {
|
||
|
|
Arc::new(Mutex::new(HashSet::new()))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn test_agents() -> Arc<AgentPool> {
|
||
|
|
Arc::new(AgentPool::new_test(3000))
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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 ambient_rooms = test_ambient_rooms();
|
||
|
|
let room_id = "!myroom:example.com".to_string();
|
||
|
|
let agents = test_agents();
|
||
|
|
let dispatch = CommandDispatch {
|
||
|
|
bot_name: "Timmy",
|
||
|
|
bot_user_id: "@timmy:homeserver.local",
|
||
|
|
project_root: std::path::Path::new("/tmp"),
|
||
|
|
agents: &agents,
|
||
|
|
ambient_rooms: &ambient_rooms,
|
||
|
|
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!(
|
||
|
|
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 ambient_rooms = test_ambient_rooms();
|
||
|
|
let room_id = "!myroom:example.com".to_string();
|
||
|
|
ambient_rooms.lock().unwrap().insert(room_id.clone());
|
||
|
|
let agents = test_agents();
|
||
|
|
let dispatch = CommandDispatch {
|
||
|
|
bot_name: "Timmy",
|
||
|
|
bot_user_id: "@timmy:homeserver.local",
|
||
|
|
project_root: std::path::Path::new("/tmp"),
|
||
|
|
agents: &agents,
|
||
|
|
ambient_rooms: &ambient_rooms,
|
||
|
|
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!(
|
||
|
|
!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 ambient_rooms = test_ambient_rooms();
|
||
|
|
let agents = test_agents();
|
||
|
|
let room_id = "!myroom:example.com".to_string();
|
||
|
|
let dispatch = CommandDispatch {
|
||
|
|
bot_name: "Timmy",
|
||
|
|
bot_user_id: "@timmy:homeserver.local",
|
||
|
|
project_root: std::path::Path::new("/tmp"),
|
||
|
|
agents: &agents,
|
||
|
|
ambient_rooms: &ambient_rooms,
|
||
|
|
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!(
|
||
|
|
ambient_rooms.lock().unwrap().contains(&room_id),
|
||
|
|
"room should be in ambient_rooms after ambient on"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn ambient_off_disables_ambient_mode() {
|
||
|
|
let ambient_rooms = test_ambient_rooms();
|
||
|
|
let agents = test_agents();
|
||
|
|
let room_id = "!myroom:example.com".to_string();
|
||
|
|
// Pre-insert the room
|
||
|
|
ambient_rooms.lock().unwrap().insert(room_id.clone());
|
||
|
|
|
||
|
|
let dispatch = CommandDispatch {
|
||
|
|
bot_name: "Timmy",
|
||
|
|
bot_user_id: "@timmy:homeserver.local",
|
||
|
|
project_root: std::path::Path::new("/tmp"),
|
||
|
|
agents: &agents,
|
||
|
|
ambient_rooms: &ambient_rooms,
|
||
|
|
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!(
|
||
|
|
!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}"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|