huskies: merge 591_story_gateway_chat_commands_use_active_project_root_instead_of_gateway_config_dir
This commit is contained in:
@@ -67,6 +67,23 @@ pub struct BotContext {
|
|||||||
pub gateway_projects: Vec<String>,
|
pub gateway_projects: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BotContext {
|
||||||
|
/// Resolve the effective project root for command dispatch.
|
||||||
|
///
|
||||||
|
/// In gateway mode the bot's `project_root` is the gateway config directory.
|
||||||
|
/// Each project lives in a subdirectory named after the project, so the
|
||||||
|
/// effective root for commands is `project_root / active_project_name`.
|
||||||
|
/// In standalone (single-project) mode this returns `project_root` unchanged.
|
||||||
|
pub async fn effective_project_root(&self) -> PathBuf {
|
||||||
|
if let Some(ref ap) = self.gateway_active_project {
|
||||||
|
let name = ap.read().await.clone();
|
||||||
|
self.project_root.join(&name)
|
||||||
|
} else {
|
||||||
|
self.project_root.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -88,6 +105,126 @@ mod tests {
|
|||||||
assert_clone::<BotContext>();
|
assert_clone::<BotContext>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn effective_project_root_standalone_returns_project_root() {
|
||||||
|
// In standalone mode (gateway_active_project is None), the effective root
|
||||||
|
// must equal the project_root exactly.
|
||||||
|
let (_perm_tx, perm_rx) = mpsc::unbounded_channel();
|
||||||
|
let ctx = BotContext {
|
||||||
|
bot_user_id: make_user_id("@bot:example.com"),
|
||||||
|
target_room_ids: vec![],
|
||||||
|
project_root: PathBuf::from("/projects/myapp"),
|
||||||
|
allowed_users: vec![],
|
||||||
|
history: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
history_size: 20,
|
||||||
|
bot_sent_event_ids: Arc::new(TokioMutex::new(std::collections::HashSet::new())),
|
||||||
|
perm_rx: Arc::new(TokioMutex::new(perm_rx)),
|
||||||
|
pending_perm_replies: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
permission_timeout_secs: 120,
|
||||||
|
bot_name: "Assistant".to_string(),
|
||||||
|
ambient_rooms: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
|
||||||
|
agents: Arc::new(crate::agents::AgentPool::new_test(3000)),
|
||||||
|
htop_sessions: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
transport: Arc::new(crate::chat::transport::whatsapp::WhatsAppTransport::new(
|
||||||
|
"test-phone".to_string(),
|
||||||
|
"test-token".to_string(),
|
||||||
|
"pipeline_notification".to_string(),
|
||||||
|
)),
|
||||||
|
timer_store: Arc::new(crate::chat::timer::TimerStore::load(
|
||||||
|
std::path::PathBuf::from("/tmp/timers.json"),
|
||||||
|
)),
|
||||||
|
gateway_active_project: None,
|
||||||
|
gateway_projects: vec![],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ctx.effective_project_root().await,
|
||||||
|
PathBuf::from("/projects/myapp")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn effective_project_root_gateway_uses_active_project_subdir() {
|
||||||
|
// In gateway mode, the effective root must be config_dir / active_project_name.
|
||||||
|
let (_perm_tx, perm_rx) = mpsc::unbounded_channel();
|
||||||
|
let active = Arc::new(RwLock::new("huskies".to_string()));
|
||||||
|
let ctx = BotContext {
|
||||||
|
bot_user_id: make_user_id("@bot:example.com"),
|
||||||
|
target_room_ids: vec![],
|
||||||
|
project_root: PathBuf::from("/gateway"),
|
||||||
|
allowed_users: vec![],
|
||||||
|
history: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
history_size: 20,
|
||||||
|
bot_sent_event_ids: Arc::new(TokioMutex::new(std::collections::HashSet::new())),
|
||||||
|
perm_rx: Arc::new(TokioMutex::new(perm_rx)),
|
||||||
|
pending_perm_replies: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
permission_timeout_secs: 120,
|
||||||
|
bot_name: "Assistant".to_string(),
|
||||||
|
ambient_rooms: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
|
||||||
|
agents: Arc::new(crate::agents::AgentPool::new_test(3000)),
|
||||||
|
htop_sessions: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
transport: Arc::new(crate::chat::transport::whatsapp::WhatsAppTransport::new(
|
||||||
|
"test-phone".to_string(),
|
||||||
|
"test-token".to_string(),
|
||||||
|
"pipeline_notification".to_string(),
|
||||||
|
)),
|
||||||
|
timer_store: Arc::new(crate::chat::timer::TimerStore::load(
|
||||||
|
std::path::PathBuf::from("/tmp/timers.json"),
|
||||||
|
)),
|
||||||
|
gateway_active_project: Some(Arc::clone(&active)),
|
||||||
|
gateway_projects: vec!["huskies".into(), "robot-studio".into()],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ctx.effective_project_root().await,
|
||||||
|
PathBuf::from("/gateway/huskies")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn effective_project_root_gateway_reflects_project_switch() {
|
||||||
|
// Switching the active project must change the effective root.
|
||||||
|
let (_perm_tx, perm_rx) = mpsc::unbounded_channel();
|
||||||
|
let active = Arc::new(RwLock::new("huskies".to_string()));
|
||||||
|
let ctx = BotContext {
|
||||||
|
bot_user_id: make_user_id("@bot:example.com"),
|
||||||
|
target_room_ids: vec![],
|
||||||
|
project_root: PathBuf::from("/gateway"),
|
||||||
|
allowed_users: vec![],
|
||||||
|
history: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
history_size: 20,
|
||||||
|
bot_sent_event_ids: Arc::new(TokioMutex::new(std::collections::HashSet::new())),
|
||||||
|
perm_rx: Arc::new(TokioMutex::new(perm_rx)),
|
||||||
|
pending_perm_replies: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
permission_timeout_secs: 120,
|
||||||
|
bot_name: "Assistant".to_string(),
|
||||||
|
ambient_rooms: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
|
||||||
|
agents: Arc::new(crate::agents::AgentPool::new_test(3000)),
|
||||||
|
htop_sessions: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
|
||||||
|
transport: Arc::new(crate::chat::transport::whatsapp::WhatsAppTransport::new(
|
||||||
|
"test-phone".to_string(),
|
||||||
|
"test-token".to_string(),
|
||||||
|
"pipeline_notification".to_string(),
|
||||||
|
)),
|
||||||
|
timer_store: Arc::new(crate::chat::timer::TimerStore::load(
|
||||||
|
std::path::PathBuf::from("/tmp/timers.json"),
|
||||||
|
)),
|
||||||
|
gateway_active_project: Some(Arc::clone(&active)),
|
||||||
|
gateway_projects: vec!["huskies".into(), "robot-studio".into()],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ctx.effective_project_root().await,
|
||||||
|
PathBuf::from("/gateway/huskies")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate switch_project changing the active project.
|
||||||
|
*active.write().await = "robot-studio".to_string();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ctx.effective_project_root().await,
|
||||||
|
PathBuf::from("/gateway/robot-studio")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bot_context_has_no_require_verified_devices_field() {
|
fn bot_context_has_no_require_verified_devices_field() {
|
||||||
// Verification is always on — BotContext no longer has a toggle field.
|
// Verification is always on — BotContext no longer has a toggle field.
|
||||||
|
|||||||
@@ -174,13 +174,18 @@ pub(super) async fn on_room_message(
|
|||||||
let user_message = body;
|
let user_message = body;
|
||||||
slog!("[matrix-bot] Message from {sender}: {user_message}");
|
slog!("[matrix-bot] Message from {sender}: {user_message}");
|
||||||
|
|
||||||
|
// In gateway mode, resolve commands against the active project's root directory.
|
||||||
|
// The gateway's own project_root is the gateway config dir; each project lives in
|
||||||
|
// a subdirectory named after the project. Standalone mode is unaffected.
|
||||||
|
let effective_root = ctx.effective_project_root().await;
|
||||||
|
|
||||||
// Check for bot-level commands (help, status, ambient, …) before invoking
|
// Check for bot-level commands (help, status, ambient, …) before invoking
|
||||||
// the LLM. All commands are registered in commands.rs — no special-casing
|
// the LLM. All commands are registered in commands.rs — no special-casing
|
||||||
// needed here.
|
// needed here.
|
||||||
let dispatch = super::super::commands::CommandDispatch {
|
let dispatch = super::super::commands::CommandDispatch {
|
||||||
bot_name: &ctx.bot_name,
|
bot_name: &ctx.bot_name,
|
||||||
bot_user_id: ctx.bot_user_id.as_str(),
|
bot_user_id: ctx.bot_user_id.as_str(),
|
||||||
project_root: &ctx.project_root,
|
project_root: &effective_root,
|
||||||
agents: &ctx.agents,
|
agents: &ctx.agents,
|
||||||
ambient_rooms: &ctx.ambient_rooms,
|
ambient_rooms: &ctx.ambient_rooms,
|
||||||
room_id: &room_id_str,
|
room_id: &room_id_str,
|
||||||
@@ -219,7 +224,7 @@ pub(super) async fn on_room_message(
|
|||||||
&ctx.bot_name,
|
&ctx.bot_name,
|
||||||
&story_number,
|
&story_number,
|
||||||
&model,
|
&model,
|
||||||
&ctx.project_root,
|
&effective_root,
|
||||||
&ctx.agents,
|
&ctx.agents,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -287,7 +292,7 @@ pub(super) async fn on_room_message(
|
|||||||
super::super::delete::handle_delete(
|
super::super::delete::handle_delete(
|
||||||
&ctx.bot_name,
|
&ctx.bot_name,
|
||||||
&story_number,
|
&story_number,
|
||||||
&ctx.project_root,
|
&effective_root,
|
||||||
&ctx.agents,
|
&ctx.agents,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -321,7 +326,7 @@ pub(super) async fn on_room_message(
|
|||||||
super::super::rmtree::handle_rmtree(
|
super::super::rmtree::handle_rmtree(
|
||||||
&ctx.bot_name,
|
&ctx.bot_name,
|
||||||
&story_number,
|
&story_number,
|
||||||
&ctx.project_root,
|
&effective_root,
|
||||||
&ctx.agents,
|
&ctx.agents,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -361,7 +366,7 @@ pub(super) async fn on_room_message(
|
|||||||
&ctx.bot_name,
|
&ctx.bot_name,
|
||||||
&story_number,
|
&story_number,
|
||||||
agent_hint.as_deref(),
|
agent_hint.as_deref(),
|
||||||
&ctx.project_root,
|
&effective_root,
|
||||||
&ctx.agents,
|
&ctx.agents,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -587,7 +592,12 @@ pub(super) async fn handle_message(
|
|||||||
let sent_any_chunk = Arc::new(AtomicBool::new(false));
|
let sent_any_chunk = Arc::new(AtomicBool::new(false));
|
||||||
let sent_any_chunk_for_callback = Arc::clone(&sent_any_chunk);
|
let sent_any_chunk_for_callback = Arc::clone(&sent_any_chunk);
|
||||||
|
|
||||||
let project_root_str = ctx.project_root.to_string_lossy().to_string();
|
// In gateway mode, run Claude Code in the active project's directory.
|
||||||
|
let project_root_str = ctx
|
||||||
|
.effective_project_root()
|
||||||
|
.await
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
let chat_fut = provider.chat_stream(
|
let chat_fut = provider.chat_stream(
|
||||||
&prompt,
|
&prompt,
|
||||||
&project_root_str,
|
&project_root_str,
|
||||||
|
|||||||
Reference in New Issue
Block a user