huskies: merge 629_refactor_migrate_commanddispatch_and_commandcontext_to_services_bundle
This commit is contained in:
@@ -28,11 +28,9 @@ mod triage;
|
||||
pub(crate) mod unblock;
|
||||
mod unreleased;
|
||||
|
||||
use crate::agents::AgentPool;
|
||||
use crate::chat::util::strip_bot_mention;
|
||||
use std::collections::HashSet;
|
||||
use crate::services::Services;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// A bot-level command that is handled without LLM invocation.
|
||||
pub struct BotCommand {
|
||||
@@ -48,41 +46,69 @@ pub struct BotCommand {
|
||||
|
||||
/// Dispatch parameters passed to `try_handle_command`.
|
||||
///
|
||||
/// Groups all the caller-supplied context needed to dispatch and execute bot
|
||||
/// commands. Construct one per incoming message and pass it alongside the raw
|
||||
/// message body.
|
||||
/// Groups the [`Services`] bundle with per-message dispatch context.
|
||||
/// Construct one per incoming message and pass it alongside the raw message
|
||||
/// body.
|
||||
///
|
||||
/// All identifiers are platform-agnostic strings so this struct works with
|
||||
/// any [`ChatTransport`](crate::chat::ChatTransport) implementation.
|
||||
pub struct CommandDispatch<'a> {
|
||||
/// The bot's display name (e.g., "Timmy").
|
||||
pub bot_name: &'a str,
|
||||
/// The bot's full user ID (e.g., `"@timmy:homeserver.local"` on Matrix).
|
||||
pub bot_user_id: &'a str,
|
||||
/// Project root directory (needed by status, ambient).
|
||||
/// Shared services bundle (project root, agent pool, ambient rooms, …).
|
||||
pub services: &'a Services,
|
||||
/// Effective project root — usually `services.project_root`, but the Matrix
|
||||
/// transport overrides this in gateway mode to point at the active project.
|
||||
pub project_root: &'a Path,
|
||||
/// Agent pool (needed by status).
|
||||
pub agents: &'a AgentPool,
|
||||
/// Set of room IDs with ambient mode enabled (needed by ambient).
|
||||
pub ambient_rooms: &'a Arc<Mutex<HashSet<String>>>,
|
||||
/// Bot user ID for mention-stripping — transport-specific (e.g. Matrix's
|
||||
/// `OwnedUserId` string differs from `services.bot_user_id`).
|
||||
pub bot_user_id: &'a str,
|
||||
/// The room this message came from (needed by ambient).
|
||||
pub room_id: &'a str,
|
||||
}
|
||||
|
||||
/// Context passed to individual command handlers.
|
||||
///
|
||||
/// Holds a reference to the shared [`Services`] bundle so that handlers access
|
||||
/// project-wide state via `ctx.services.*`. The effective project root may
|
||||
/// differ from `services.project_root` in gateway mode — use
|
||||
/// [`effective_root()`](Self::effective_root) to get the correct path.
|
||||
pub struct CommandContext<'a> {
|
||||
/// The bot's display name (e.g., "Timmy").
|
||||
pub bot_name: &'a str,
|
||||
/// Shared services bundle.
|
||||
pub services: &'a Services,
|
||||
/// Any text after the command keyword, trimmed.
|
||||
pub args: &'a str,
|
||||
/// Project root directory (needed by status, ambient).
|
||||
pub project_root: &'a Path,
|
||||
/// Agent pool (needed by status).
|
||||
pub agents: &'a AgentPool,
|
||||
/// Set of room IDs with ambient mode enabled (needed by ambient).
|
||||
pub ambient_rooms: &'a Arc<Mutex<HashSet<String>>>,
|
||||
/// The room this message came from (needed by ambient).
|
||||
pub room_id: &'a str,
|
||||
/// Effective project root for this dispatch. Equals `services.project_root`
|
||||
/// in standalone mode; in gateway mode the Matrix transport sets this to
|
||||
/// the active-project subdirectory.
|
||||
project_root: &'a Path,
|
||||
}
|
||||
|
||||
impl<'a> CommandContext<'a> {
|
||||
/// Returns the effective project root for this command invocation.
|
||||
///
|
||||
/// In standalone mode this equals `services.project_root`. In gateway mode
|
||||
/// (Matrix transport) it resolves to the active project subdirectory.
|
||||
pub fn effective_root(&self) -> &Path {
|
||||
self.project_root
|
||||
}
|
||||
|
||||
/// Test-only constructor that allows submodule tests to build a
|
||||
/// `CommandContext` despite the private `project_root` field.
|
||||
#[cfg(test)]
|
||||
pub fn new_test(
|
||||
services: &'a Services,
|
||||
args: &'a str,
|
||||
room_id: &'a str,
|
||||
project_root: &'a Path,
|
||||
) -> Self {
|
||||
Self {
|
||||
services,
|
||||
args,
|
||||
room_id,
|
||||
project_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the full list of registered bot commands.
|
||||
@@ -245,7 +271,8 @@ pub fn try_handle_command_with_html(
|
||||
dispatch: &CommandDispatch<'_>,
|
||||
message: &str,
|
||||
) -> Option<(String, String)> {
|
||||
let command_text = strip_bot_mention(message, dispatch.bot_name, dispatch.bot_user_id);
|
||||
let command_text =
|
||||
strip_bot_mention(message, &dispatch.services.bot_name, dispatch.bot_user_id);
|
||||
let trimmed = command_text.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let (cmd_name, args) = match trimmed.split_once(char::is_whitespace) {
|
||||
@@ -254,7 +281,8 @@ pub fn try_handle_command_with_html(
|
||||
};
|
||||
// Status command: emoji indicators render natively in all clients.
|
||||
if cmd_name.eq_ignore_ascii_case("status") && args.is_empty() {
|
||||
let body = status::build_pipeline_status(dispatch.project_root, dispatch.agents);
|
||||
let body =
|
||||
status::build_pipeline_status(dispatch.project_root, &dispatch.services.agents);
|
||||
let html = plain_to_html(&body);
|
||||
return Some((body, html));
|
||||
}
|
||||
@@ -288,7 +316,8 @@ fn plain_to_html(markdown: &str) -> String {
|
||||
/// Returns `Some(response)` if a command matched and was handled, `None`
|
||||
/// otherwise (the caller should fall through to the LLM).
|
||||
pub fn try_handle_command(dispatch: &CommandDispatch<'_>, message: &str) -> Option<String> {
|
||||
let command_text = strip_bot_mention(message, dispatch.bot_name, dispatch.bot_user_id);
|
||||
let command_text =
|
||||
strip_bot_mention(message, &dispatch.services.bot_name, dispatch.bot_user_id);
|
||||
let trimmed = command_text.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
@@ -301,12 +330,10 @@ pub fn try_handle_command(dispatch: &CommandDispatch<'_>, message: &str) -> Opti
|
||||
let cmd_lower = cmd_name.to_ascii_lowercase();
|
||||
|
||||
let ctx = CommandContext {
|
||||
bot_name: dispatch.bot_name,
|
||||
services: dispatch.services,
|
||||
args,
|
||||
project_root: dispatch.project_root,
|
||||
agents: dispatch.agents,
|
||||
ambient_rooms: dispatch.ambient_rooms,
|
||||
room_id: dispatch.room_id,
|
||||
project_root: dispatch.project_root,
|
||||
};
|
||||
|
||||
commands()
|
||||
@@ -382,39 +409,38 @@ fn handle_rebuild_fallback(_ctx: &CommandContext) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::agents::AgentPool;
|
||||
use crate::services::Services;
|
||||
use std::sync::Arc;
|
||||
|
||||
// -- test helpers (shared with submodule tests) -------------------------
|
||||
|
||||
pub fn test_ambient_rooms() -> Arc<Mutex<HashSet<String>>> {
|
||||
Arc::new(Mutex::new(HashSet::new()))
|
||||
/// Build a [`Services`] bundle for tests with the given bot name and a `/tmp`
|
||||
/// project root.
|
||||
pub fn test_services_named(bot_name: &str) -> Arc<Services> {
|
||||
Services::new_test(std::path::PathBuf::from("/tmp"), bot_name.to_string())
|
||||
}
|
||||
|
||||
pub fn test_agents() -> Arc<AgentPool> {
|
||||
Arc::new(AgentPool::new_test(3000))
|
||||
}
|
||||
|
||||
pub fn try_cmd(
|
||||
bot_name: &str,
|
||||
/// Dispatch a message through the command registry using the given Services.
|
||||
pub fn try_cmd_with_services(
|
||||
bot_user_id: &str,
|
||||
message: &str,
|
||||
ambient_rooms: &Arc<Mutex<HashSet<String>>>,
|
||||
services: &Services,
|
||||
) -> Option<String> {
|
||||
let agents = test_agents();
|
||||
let room_id = "!test:example.com".to_string();
|
||||
let dispatch = CommandDispatch {
|
||||
bot_name,
|
||||
services,
|
||||
project_root: &services.project_root,
|
||||
bot_user_id,
|
||||
project_root: std::path::Path::new("/tmp"),
|
||||
agents: &agents,
|
||||
ambient_rooms,
|
||||
room_id: &room_id,
|
||||
};
|
||||
try_handle_command(&dispatch, message)
|
||||
}
|
||||
|
||||
/// Convenience helper: create a temporary [`Services`] with the given bot
|
||||
/// name and dispatch `message`.
|
||||
pub fn try_cmd_addressed(bot_name: &str, bot_user_id: &str, message: &str) -> Option<String> {
|
||||
try_cmd(bot_name, bot_user_id, message, &test_ambient_rooms())
|
||||
let services = test_services_named(bot_name);
|
||||
try_cmd_with_services(bot_user_id, message, &services)
|
||||
}
|
||||
|
||||
// Re-export commands() for submodule tests
|
||||
|
||||
Reference in New Issue
Block a user