huskies: merge 629_refactor_migrate_commanddispatch_and_commandcontext_to_services_bundle
This commit is contained in:
@@ -16,7 +16,7 @@ pub(super) fn handle_ambient(ctx: &CommandContext) -> Option<String> {
|
|||||||
_ => return Some("Usage: `ambient on` or `ambient off`".to_string()),
|
_ => return Some("Usage: `ambient on` or `ambient off`".to_string()),
|
||||||
};
|
};
|
||||||
let room_ids: Vec<String> = {
|
let room_ids: Vec<String> = {
|
||||||
let mut ambient = ctx.ambient_rooms.lock().unwrap();
|
let mut ambient = ctx.services.ambient_rooms.lock().unwrap();
|
||||||
if enable {
|
if enable {
|
||||||
ambient.insert(ctx.room_id.to_string());
|
ambient.insert(ctx.room_id.to_string());
|
||||||
} else {
|
} else {
|
||||||
@@ -24,7 +24,7 @@ pub(super) fn handle_ambient(ctx: &CommandContext) -> Option<String> {
|
|||||||
}
|
}
|
||||||
ambient.iter().cloned().collect()
|
ambient.iter().cloned().collect()
|
||||||
};
|
};
|
||||||
save_ambient_rooms(ctx.project_root, &room_ids);
|
save_ambient_rooms(ctx.effective_root(), &room_ids);
|
||||||
let msg = if enable {
|
let msg = if enable {
|
||||||
"Ambient mode on. I'll respond to all messages in this room."
|
"Ambient mode on. I'll respond to all messages in this room."
|
||||||
} else {
|
} else {
|
||||||
@@ -35,35 +35,23 @@ pub(super) fn handle_ambient(ctx: &CommandContext) -> Option<String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
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
|
// Bug 352: ambient commands were being forwarded to LLM after refactors
|
||||||
// 328/330 because handle_ambient required is_addressed=true, but
|
// 328/330 because handle_ambient required is_addressed=true, but
|
||||||
// mentions_bot() only matches @-prefixed mentions, not bare bot names.
|
// mentions_bot() only matches @-prefixed mentions, not bare bot names.
|
||||||
// "timmy ambient off" sets is_addressed=false even though it names the bot.
|
// "timmy ambient off" sets is_addressed=false even though it names the bot.
|
||||||
#[test]
|
#[test]
|
||||||
fn ambient_on_works_when_unaddressed() {
|
fn ambient_on_works_when_unaddressed() {
|
||||||
let ambient_rooms = test_ambient_rooms();
|
let services = crate::services::Services::new_test(
|
||||||
|
std::path::PathBuf::from("/tmp"),
|
||||||
|
"Timmy".to_string(),
|
||||||
|
);
|
||||||
let room_id = "!myroom:example.com".to_string();
|
let room_id = "!myroom:example.com".to_string();
|
||||||
let agents = test_agents();
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: std::path::Path::new("/tmp"),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
// "timmy ambient on" — bot name mentioned but not @-prefixed, so
|
// "timmy ambient on" — bot name mentioned but not @-prefixed, so
|
||||||
@@ -74,23 +62,27 @@ mod tests {
|
|||||||
"ambient on should fire even when is_addressed=false"
|
"ambient on should fire even when is_addressed=false"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
ambient_rooms.lock().unwrap().contains(&room_id),
|
services.ambient_rooms.lock().unwrap().contains(&room_id),
|
||||||
"room should be in ambient_rooms after ambient on"
|
"room should be in ambient_rooms after ambient on"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ambient_off_works_bare_in_ambient_room() {
|
fn ambient_off_works_bare_in_ambient_room() {
|
||||||
let ambient_rooms = test_ambient_rooms();
|
let services = crate::services::Services::new_test(
|
||||||
|
std::path::PathBuf::from("/tmp"),
|
||||||
|
"Timmy".to_string(),
|
||||||
|
);
|
||||||
let room_id = "!myroom:example.com".to_string();
|
let room_id = "!myroom:example.com".to_string();
|
||||||
ambient_rooms.lock().unwrap().insert(room_id.clone());
|
services
|
||||||
let agents = test_agents();
|
.ambient_rooms
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(room_id.clone());
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: std::path::Path::new("/tmp"),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
// Bare "ambient off" in an ambient room (is_addressed=false).
|
// Bare "ambient off" in an ambient room (is_addressed=false).
|
||||||
@@ -105,22 +97,22 @@ mod tests {
|
|||||||
"response should confirm ambient off: {output}"
|
"response should confirm ambient off: {output}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!ambient_rooms.lock().unwrap().contains(&room_id),
|
!services.ambient_rooms.lock().unwrap().contains(&room_id),
|
||||||
"room should be removed from ambient_rooms after ambient off"
|
"room should be removed from ambient_rooms after ambient off"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ambient_on_enables_ambient_mode() {
|
fn ambient_on_enables_ambient_mode() {
|
||||||
let ambient_rooms = test_ambient_rooms();
|
let services = crate::services::Services::new_test(
|
||||||
let agents = test_agents();
|
std::path::PathBuf::from("/tmp"),
|
||||||
|
"Timmy".to_string(),
|
||||||
|
);
|
||||||
let room_id = "!myroom:example.com".to_string();
|
let room_id = "!myroom:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: std::path::Path::new("/tmp"),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let result = try_handle_command(&dispatch, "@timmy ambient on");
|
let result = try_handle_command(&dispatch, "@timmy ambient on");
|
||||||
@@ -131,25 +123,29 @@ mod tests {
|
|||||||
"response should confirm ambient on: {output}"
|
"response should confirm ambient on: {output}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
ambient_rooms.lock().unwrap().contains(&room_id),
|
services.ambient_rooms.lock().unwrap().contains(&room_id),
|
||||||
"room should be in ambient_rooms after ambient on"
|
"room should be in ambient_rooms after ambient on"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ambient_off_disables_ambient_mode() {
|
fn ambient_off_disables_ambient_mode() {
|
||||||
let ambient_rooms = test_ambient_rooms();
|
let services = crate::services::Services::new_test(
|
||||||
let agents = test_agents();
|
std::path::PathBuf::from("/tmp"),
|
||||||
|
"Timmy".to_string(),
|
||||||
|
);
|
||||||
let room_id = "!myroom:example.com".to_string();
|
let room_id = "!myroom:example.com".to_string();
|
||||||
// Pre-insert the room
|
// Pre-insert the room
|
||||||
ambient_rooms.lock().unwrap().insert(room_id.clone());
|
services
|
||||||
|
.ambient_rooms
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(room_id.clone());
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: std::path::Path::new("/tmp"),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let result = try_handle_command(&dispatch, "@timmy ambient off");
|
let result = try_handle_command(&dispatch, "@timmy ambient off");
|
||||||
@@ -160,7 +156,7 @@ mod tests {
|
|||||||
"response should confirm ambient off: {output}"
|
"response should confirm ambient off: {output}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!ambient_rooms.lock().unwrap().contains(&room_id),
|
!services.ambient_rooms.lock().unwrap().contains(&room_id),
|
||||||
"room should be removed from ambient_rooms after ambient off"
|
"room should be removed from ambient_rooms after ambient off"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use super::status::story_short_label;
|
|||||||
/// Show token spend: 24h total, top 5 stories, agent-type breakdown, and
|
/// Show token spend: 24h total, top 5 stories, agent-type breakdown, and
|
||||||
/// all-time total.
|
/// all-time total.
|
||||||
pub(super) fn handle_cost(ctx: &CommandContext) -> Option<String> {
|
pub(super) fn handle_cost(ctx: &CommandContext) -> Option<String> {
|
||||||
let records = match crate::agents::token_usage::read_all(ctx.project_root) {
|
let records = match crate::agents::token_usage::read_all(ctx.effective_root()) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => return Some(format!("Failed to read token usage: {e}")),
|
Err(e) => return Some(format!("Failed to read token usage: {e}")),
|
||||||
};
|
};
|
||||||
@@ -99,8 +99,6 @@ pub(super) fn extract_agent_type(agent_name: &str) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
fn write_token_records(
|
fn write_token_records(
|
||||||
root: &std::path::Path,
|
root: &std::path::Path,
|
||||||
@@ -139,18 +137,13 @@ mod tests {
|
|||||||
|
|
||||||
fn cost_cmd_with_root(root: &std::path::Path) -> Option<String> {
|
fn cost_cmd_with_root(root: &std::path::Path) -> Option<String> {
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, "@timmy cost")
|
try_handle_command(&dispatch, "@timmy cost")
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ pub(super) fn handle_coverage(ctx: &CommandContext) -> Option<String> {
|
|||||||
let args = ctx.args.trim();
|
let args = ctx.args.trim();
|
||||||
|
|
||||||
match args {
|
match args {
|
||||||
"run" => Some(run_coverage(ctx.project_root)),
|
"run" => Some(run_coverage(ctx.effective_root())),
|
||||||
"" => Some(read_cached_coverage(ctx.project_root)),
|
"" => Some(read_cached_coverage(ctx.effective_root())),
|
||||||
other => Some(format!(
|
other => Some(format!(
|
||||||
"Usage: `coverage` (cached) or `coverage run` (fresh)\n\nUnknown argument: `{other}`"
|
"Usage: `coverage` (cached) or `coverage run` (fresh)\n\nUnknown argument: `{other}`"
|
||||||
)),
|
)),
|
||||||
@@ -262,32 +262,13 @@ fn extract_summary_field(output: &str, label: &str) -> Option<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
fn make_ctx<'a>(
|
fn make_ctx<'a>(
|
||||||
agents: &'a Arc<AgentPool>,
|
services: &'a crate::services::Services,
|
||||||
ambient_rooms: &'a Arc<Mutex<HashSet<String>>>,
|
|
||||||
project_root: &'a std::path::Path,
|
project_root: &'a std::path::Path,
|
||||||
args: &'a str,
|
args: &'a str,
|
||||||
) -> super::super::CommandContext<'a> {
|
) -> super::super::CommandContext<'a> {
|
||||||
super::super::CommandContext {
|
super::super::CommandContext::new_test(services, args, "!test:example.com", project_root)
|
||||||
bot_name: "Timmy",
|
|
||||||
args,
|
|
||||||
project_root,
|
|
||||||
agents,
|
|
||||||
ambient_rooms,
|
|
||||||
room_id: "!test:example.com",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_agents() -> Arc<AgentPool> {
|
|
||||||
Arc::new(AgentPool::new_test(3000))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_ambient() -> Arc<Mutex<HashSet<String>>> {
|
|
||||||
Arc::new(Mutex::new(HashSet::new()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_coverage_report(overall: f64, threshold: f64, files: Vec<(&str, f64)>) -> String {
|
fn sample_coverage_report(overall: f64, threshold: f64, files: Vec<(&str, f64)>) -> String {
|
||||||
@@ -336,9 +317,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
std::fs::write(dir.path().join(".coverage_report.json"), &report).unwrap();
|
std::fs::write(dir.path().join(".coverage_report.json"), &report).unwrap();
|
||||||
|
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_coverage(&ctx).unwrap();
|
let output = handle_coverage(&ctx).unwrap();
|
||||||
|
|
||||||
assert!(output.contains("72.5"), "should include overall: {output}");
|
assert!(output.contains("72.5"), "should include overall: {output}");
|
||||||
@@ -371,9 +352,9 @@ mod tests {
|
|||||||
let report = sample_coverage_report(40.0, 30.0, files);
|
let report = sample_coverage_report(40.0, 30.0, files);
|
||||||
std::fs::write(dir.path().join(".coverage_report.json"), &report).unwrap();
|
std::fs::write(dir.path().join(".coverage_report.json"), &report).unwrap();
|
||||||
|
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_coverage(&ctx).unwrap();
|
let output = handle_coverage(&ctx).unwrap();
|
||||||
|
|
||||||
assert!(output.contains("a.rs"), "should show lowest file: {output}");
|
assert!(output.contains("a.rs"), "should show lowest file: {output}");
|
||||||
@@ -396,9 +377,9 @@ mod tests {
|
|||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
std::fs::write(dir.path().join(".coverage_baseline"), "72.5\n").unwrap();
|
std::fs::write(dir.path().join(".coverage_baseline"), "72.5\n").unwrap();
|
||||||
|
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_coverage(&ctx).unwrap();
|
let output = handle_coverage(&ctx).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -416,9 +397,9 @@ mod tests {
|
|||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
std::fs::write(dir.path().join(".coverage_baseline"), "60.00\n65.21\n").unwrap();
|
std::fs::write(dir.path().join(".coverage_baseline"), "60.00\n65.21\n").unwrap();
|
||||||
|
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_coverage(&ctx).unwrap();
|
let output = handle_coverage(&ctx).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -435,9 +416,9 @@ mod tests {
|
|||||||
fn coverage_missing_both_files_reports_clearly() {
|
fn coverage_missing_both_files_reports_clearly() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_coverage(&ctx).unwrap();
|
let output = handle_coverage(&ctx).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -450,9 +431,9 @@ mod tests {
|
|||||||
fn coverage_run_missing_script_reports_error() {
|
fn coverage_run_missing_script_reports_error() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "run");
|
let ctx = make_ctx(&services, dir.path(), "run");
|
||||||
let output = handle_coverage(&ctx).unwrap();
|
let output = handle_coverage(&ctx).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -464,9 +445,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn coverage_unknown_arg_returns_usage() {
|
fn coverage_unknown_arg_returns_usage() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "blah");
|
let ctx = make_ctx(&services, dir.path(), "blah");
|
||||||
let output = handle_coverage(&ctx).unwrap();
|
let output = handle_coverage(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("Usage"),
|
output.contains("Usage"),
|
||||||
@@ -480,15 +461,13 @@ mod tests {
|
|||||||
let report = sample_coverage_report(55.0, 50.0, vec![]);
|
let report = sample_coverage_report(55.0, 50.0, vec![]);
|
||||||
std::fs::write(dir.path().join(".coverage_report.json"), &report).unwrap();
|
std::fs::write(dir.path().join(".coverage_report.json"), &report).unwrap();
|
||||||
|
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = super::super::CommandDispatch {
|
let dispatch = super::super::CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: dir.path(),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let result = super::super::try_handle_command(&dispatch, "@timmy coverage");
|
let result = super::super::try_handle_command(&dispatch, "@timmy coverage");
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
|||||||
"Usage: `{} depends <number> [dep1 dep2 ...]`\n\nExamples:\n\
|
"Usage: `{} depends <number> [dep1 dep2 ...]`\n\nExamples:\n\
|
||||||
• `{0} depends 484 477 478` — set depends_on: [477, 478]\n\
|
• `{0} depends 484 477 478` — set depends_on: [477, 478]\n\
|
||||||
• `{0} depends 484` — clear all dependencies",
|
• `{0} depends 484` — clear all dependencies",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
|||||||
if !num_str.chars().all(|c| c.is_ascii_digit()) || num_str.is_empty() {
|
if !num_str.chars().all(|c| c.is_ascii_digit()) || num_str.is_empty() {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Invalid story number: `{num_str}`. Usage: `{} depends <number> [dep1 dep2 ...]`",
|
"Invalid story number: `{num_str}`. Usage: `{} depends <number> [dep1 dep2 ...]`",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
|||||||
|
|
||||||
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
||||||
let (story_id, stage_dir, path, content) =
|
let (story_id, stage_dir, path, content) =
|
||||||
match crate::chat::lookup::find_story_by_number(ctx.project_root, num_str) {
|
match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
|
||||||
Some(found) => found,
|
Some(found) => found,
|
||||||
None => {
|
None => {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
@@ -115,22 +115,15 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn depends_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
fn depends_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy depends {args}"))
|
try_handle_command(&dispatch, &format!("@timmy depends {args}"))
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ pub(super) fn handle_diff(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() {
|
if num_str.is_empty() {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} diff <number>`\n\nShows the git diff from the main branch to the story's worktree HEAD.",
|
"Usage: `{} diff <number>`\n\nShows the git diff from the main branch to the story's worktree HEAD.",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Invalid story number: `{num_str}`. Usage: `{} diff <number>`",
|
"Invalid story number: `{num_str}`. Usage: `{} diff <number>`",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,14 +34,14 @@ pub(super) fn handle_diff(ctx: &CommandContext) -> Option<String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let wt_path = crate::worktree::worktree_path(ctx.project_root, &story_id);
|
let wt_path = crate::worktree::worktree_path(ctx.effective_root(), &story_id);
|
||||||
if !wt_path.is_dir() {
|
if !wt_path.is_dir() {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Story **{num_str}** has no worktree. The diff is only available once a coder has started working on it."
|
"Story **{num_str}** has no worktree. The diff is only available once a coder has started working on it."
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_branch = resolve_base_branch(ctx.project_root);
|
let base_branch = resolve_base_branch(ctx.effective_root());
|
||||||
let range = format!("{base_branch}...HEAD");
|
let range = format!("{base_branch}...HEAD");
|
||||||
|
|
||||||
let stat = run_git(&wt_path, &["diff", "--stat", &range]);
|
let stat = run_git(&wt_path, &["diff", "--stat", &range]);
|
||||||
@@ -144,22 +144,16 @@ fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn diff_cmd(root: &std::path::Path, args: &str) -> Option<String> {
|
fn diff_cmd(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy diff {args}"))
|
try_handle_command(&dispatch, &format!("@timmy diff {args}"))
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ pub(super) fn handle_freeze(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} freeze <number>` (e.g. `freeze 42`)",
|
"Usage: `{} freeze <number>` (e.g. `freeze 42`)",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Some(freeze_by_number(ctx.project_root, num_str))
|
Some(freeze_by_number(ctx.effective_root(), num_str))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Core freeze logic: find story by numeric prefix and set `frozen: true`.
|
/// Core freeze logic: find story by numeric prefix and set `frozen: true`.
|
||||||
@@ -80,10 +80,10 @@ pub(super) fn handle_unfreeze(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} unfreeze <number>` (e.g. `unfreeze 42`)",
|
"Usage: `{} unfreeze <number>` (e.g. `unfreeze 42`)",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Some(unfreeze_by_number(ctx.project_root, num_str))
|
Some(unfreeze_by_number(ctx.effective_root(), num_str))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Core unfreeze logic: find story by numeric prefix and clear `frozen` flag.
|
/// Core unfreeze logic: find story by numeric prefix and clear `frozen` flag.
|
||||||
@@ -135,38 +135,29 @@ fn unfreeze_by_story_id(story_id: &str) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use crate::chat::test_helpers::write_story_file;
|
use crate::chat::test_helpers::write_story_file;
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn freeze_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
fn freeze_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy freeze {args}"))
|
try_handle_command(&dispatch, &format!("@timmy freeze {args}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unfreeze_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
fn unfreeze_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy unfreeze {args}"))
|
try_handle_command(&dispatch, &format!("@timmy unfreeze {args}"))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub(super) fn handle_git(ctx: &CommandContext) -> Option<String> {
|
|||||||
// Current branch
|
// Current branch
|
||||||
let branch = Command::new("git")
|
let branch = Command::new("git")
|
||||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||||
.current_dir(ctx.project_root)
|
.current_dir(ctx.effective_root())
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|o| o.status.success())
|
.filter(|o| o.status.success())
|
||||||
@@ -19,7 +19,7 @@ pub(super) fn handle_git(ctx: &CommandContext) -> Option<String> {
|
|||||||
// Porcelain status for staged + unstaged changes
|
// Porcelain status for staged + unstaged changes
|
||||||
let status_output = Command::new("git")
|
let status_output = Command::new("git")
|
||||||
.args(["status", "--porcelain"])
|
.args(["status", "--porcelain"])
|
||||||
.current_dir(ctx.project_root)
|
.current_dir(ctx.effective_root())
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|o| o.status.success())
|
.filter(|o| o.status.success())
|
||||||
@@ -32,7 +32,7 @@ pub(super) fn handle_git(ctx: &CommandContext) -> Option<String> {
|
|||||||
// Ahead/behind: --left-right gives "N\tM" (ahead\tbehind)
|
// Ahead/behind: --left-right gives "N\tM" (ahead\tbehind)
|
||||||
let ahead_behind = Command::new("git")
|
let ahead_behind = Command::new("git")
|
||||||
.args(["rev-list", "--count", "--left-right", "HEAD...@{u}"])
|
.args(["rev-list", "--count", "--left-right", "HEAD...@{u}"])
|
||||||
.current_dir(ctx.project_root)
|
.current_dir(ctx.effective_root())
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|o| o.status.success())
|
.filter(|o| o.status.success())
|
||||||
@@ -77,20 +77,8 @@ pub(super) fn handle_git(ctx: &CommandContext) -> Option<String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn git_command_is_registered() {
|
fn git_command_is_registered() {
|
||||||
use super::super::commands;
|
use super::super::commands;
|
||||||
@@ -118,15 +106,13 @@ mod tests {
|
|||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient_rooms = test_ambient_rooms();
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: repo_root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let result = try_handle_command(&dispatch, "@timmy git");
|
let result = try_handle_command(&dispatch, "@timmy git");
|
||||||
@@ -138,15 +124,13 @@ mod tests {
|
|||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient_rooms = test_ambient_rooms();
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: repo_root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let output = try_handle_command(&dispatch, "@timmy git").unwrap();
|
let output = try_handle_command(&dispatch, "@timmy git").unwrap();
|
||||||
@@ -161,15 +145,13 @@ mod tests {
|
|||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient_rooms = test_ambient_rooms();
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: repo_root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let output = try_handle_command(&dispatch, "@timmy git").unwrap();
|
let output = try_handle_command(&dispatch, "@timmy git").unwrap();
|
||||||
@@ -184,15 +166,13 @@ mod tests {
|
|||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient_rooms = test_ambient_rooms();
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: repo_root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let output = try_handle_command(&dispatch, "@timmy git").unwrap();
|
let output = try_handle_command(&dispatch, "@timmy git").unwrap();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use super::{CommandContext, commands};
|
use super::{CommandContext, commands};
|
||||||
|
|
||||||
pub(super) fn handle_help(ctx: &CommandContext) -> Option<String> {
|
pub(super) fn handle_help(ctx: &CommandContext) -> Option<String> {
|
||||||
let mut output = format!("**{} Commands**\n\n", ctx.bot_name);
|
let mut output = format!("**{} Commands**\n\n", ctx.services.bot_name);
|
||||||
let mut sorted: Vec<_> = commands().iter().collect();
|
let mut sorted: Vec<_> = commands().iter().collect();
|
||||||
sorted.sort_by_key(|c| c.name);
|
sorted.sort_by_key(|c| c.name);
|
||||||
for cmd in sorted {
|
for cmd in sorted {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub(super) fn handle_loc(ctx: &CommandContext) -> Option<String> {
|
|||||||
let args = ctx.args.trim();
|
let args = ctx.args.trim();
|
||||||
|
|
||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
return Some(loc_top_n(ctx.project_root, DEFAULT_TOP_N));
|
return Some(loc_top_n(ctx.effective_root(), DEFAULT_TOP_N));
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_token = args.split_whitespace().next().unwrap_or("");
|
let first_token = args.split_whitespace().next().unwrap_or("");
|
||||||
@@ -49,8 +49,8 @@ pub(super) fn handle_loc(ctx: &CommandContext) -> Option<String> {
|
|||||||
Ok(0) => format!(
|
Ok(0) => format!(
|
||||||
"Usage: `loc [N]` or `loc <filepath>` — show top N source files by line count (default {DEFAULT_TOP_N}), or line count for a specific file"
|
"Usage: `loc [N]` or `loc <filepath>` — show top N source files by line count (default {DEFAULT_TOP_N}), or line count for a specific file"
|
||||||
),
|
),
|
||||||
Ok(n) => loc_top_n(ctx.project_root, n),
|
Ok(n) => loc_top_n(ctx.effective_root(), n),
|
||||||
Err(_) => loc_single_file(ctx.project_root, args),
|
Err(_) => loc_single_file(ctx.effective_root(), args),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,24 +209,13 @@ fn is_source_extension(ext: &str) -> bool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
fn make_ctx<'a>(
|
fn make_ctx<'a>(
|
||||||
agents: &'a Arc<AgentPool>,
|
services: &'a crate::services::Services,
|
||||||
ambient_rooms: &'a Arc<Mutex<HashSet<String>>>,
|
|
||||||
project_root: &'a std::path::Path,
|
project_root: &'a std::path::Path,
|
||||||
args: &'a str,
|
args: &'a str,
|
||||||
) -> super::super::CommandContext<'a> {
|
) -> super::super::CommandContext<'a> {
|
||||||
super::super::CommandContext {
|
super::super::CommandContext::new_test(services, args, "!test:example.com", project_root)
|
||||||
bot_name: "Timmy",
|
|
||||||
args,
|
|
||||||
project_root,
|
|
||||||
agents,
|
|
||||||
ambient_rooms,
|
|
||||||
room_id: "!test:example.com",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -252,12 +241,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loc_default_returns_top_10() {
|
fn loc_default_returns_top_10() {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "");
|
let services =
|
||||||
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
|
let ctx = make_ctx(&services, repo_root, "");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("Top"),
|
output.contains("Top"),
|
||||||
@@ -273,12 +262,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loc_with_arg_5_returns_at_most_5() {
|
fn loc_with_arg_5_returns_at_most_5() {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "5");
|
let services =
|
||||||
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
|
let ctx = make_ctx(&services, repo_root, "5");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
let count = output.lines().filter(|l| l.contains(". `")).count();
|
let count = output.lines().filter(|l| l.contains(". `")).count();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -289,12 +278,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loc_with_arg_20_returns_at_most_20() {
|
fn loc_with_arg_20_returns_at_most_20() {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "20");
|
let services =
|
||||||
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
|
let ctx = make_ctx(&services, repo_root, "20");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
let count = output.lines().filter(|l| l.contains(". `")).count();
|
let count = output.lines().filter(|l| l.contains(". `")).count();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -305,12 +294,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loc_output_contains_rank_and_line_count() {
|
fn loc_output_contains_rank_and_line_count() {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "");
|
let services =
|
||||||
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
|
let ctx = make_ctx(&services, repo_root, "");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
// Each entry should have "N. `path` — N lines"
|
// Each entry should have "N. `path` — N lines"
|
||||||
assert!(
|
assert!(
|
||||||
@@ -325,12 +314,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loc_zero_arg_returns_usage() {
|
fn loc_zero_arg_returns_usage() {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "0");
|
let services =
|
||||||
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
|
let ctx = make_ctx(&services, repo_root, "0");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("Usage"),
|
output.contains("Usage"),
|
||||||
@@ -349,9 +338,9 @@ mod tests {
|
|||||||
writeln!(f, "fn line_{i}() {{}}").unwrap();
|
writeln!(f, "fn line_{i}() {{}}").unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services =
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, dir.path(), "hello.rs");
|
let ctx = make_ctx(&services, dir.path(), "hello.rs");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("42"),
|
output.contains("42"),
|
||||||
@@ -366,9 +355,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn loc_filepath_nonexistent_returns_error() {
|
fn loc_filepath_nonexistent_returns_error() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services =
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, dir.path(), "does_not_exist.rs");
|
let ctx = make_ctx(&services, dir.path(), "does_not_exist.rs");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("not found") || output.contains("Error"),
|
output.contains("not found") || output.contains("Error"),
|
||||||
@@ -378,12 +367,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn loc_skips_worktrees_directory() {
|
fn loc_skips_worktrees_directory() {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or(std::path::Path::new("."));
|
.unwrap_or(std::path::Path::new("."));
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "");
|
let services =
|
||||||
|
crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
|
||||||
|
let ctx = make_ctx(&services, repo_root, "");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
!output.contains(".huskies/worktrees"),
|
!output.contains(".huskies/worktrees"),
|
||||||
@@ -413,9 +402,9 @@ mod tests {
|
|||||||
writeln!(f, "fn f{i}() {{}}").unwrap();
|
writeln!(f, "fn f{i}() {{}}").unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services =
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, dir.path(), "50");
|
let ctx = make_ctx(&services, dir.path(), "50");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
!output.contains("target/"),
|
!output.contains("target/"),
|
||||||
@@ -447,9 +436,9 @@ mod tests {
|
|||||||
writeln!(f, "fn line_{i}() {{}}").unwrap();
|
writeln!(f, "fn line_{i}() {{}}").unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services =
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, dir.path(), "50");
|
let ctx = make_ctx(&services, dir.path(), "50");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
!output.contains("package-lock.json"),
|
!output.contains("package-lock.json"),
|
||||||
@@ -486,9 +475,9 @@ mod tests {
|
|||||||
std::io::Write::write_all(&mut f, format!("fn f{i}() {{}}\n").as_bytes()).unwrap();
|
std::io::Write::write_all(&mut f, format!("fn f{i}() {{}}\n").as_bytes()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services =
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient_rooms, dir.path(), "50");
|
let ctx = make_ctx(&services, dir.path(), "50");
|
||||||
let output = handle_loc(&ctx).unwrap();
|
let output = handle_loc(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
!output.contains("Cargo.lock"),
|
!output.contains("Cargo.lock"),
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ pub(super) fn handle_logs(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() {
|
if num_str.is_empty() {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} logs <number>`\n\nShows the last agent log lines for a story.",
|
"Usage: `{} logs <number>`\n\nShows the last agent log lines for a story.",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Invalid story number: `{num_str}`. Usage: `{} logs <number>`",
|
"Invalid story number: `{num_str}`. Usage: `{} logs <number>`",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,22 +115,16 @@ fn read_log_tail(path: &Path, n: usize) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn logs_cmd(root: &Path, args: &str) -> Option<String> {
|
fn logs_cmd(root: &Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy logs {args}"))
|
try_handle_command(&dispatch, &format!("@timmy logs {args}"))
|
||||||
|
|||||||
@@ -28,11 +28,9 @@ mod triage;
|
|||||||
pub(crate) mod unblock;
|
pub(crate) mod unblock;
|
||||||
mod unreleased;
|
mod unreleased;
|
||||||
|
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use crate::chat::util::strip_bot_mention;
|
use crate::chat::util::strip_bot_mention;
|
||||||
use std::collections::HashSet;
|
use crate::services::Services;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
/// A bot-level command that is handled without LLM invocation.
|
/// A bot-level command that is handled without LLM invocation.
|
||||||
pub struct BotCommand {
|
pub struct BotCommand {
|
||||||
@@ -48,41 +46,69 @@ pub struct BotCommand {
|
|||||||
|
|
||||||
/// Dispatch parameters passed to `try_handle_command`.
|
/// Dispatch parameters passed to `try_handle_command`.
|
||||||
///
|
///
|
||||||
/// Groups all the caller-supplied context needed to dispatch and execute bot
|
/// Groups the [`Services`] bundle with per-message dispatch context.
|
||||||
/// commands. Construct one per incoming message and pass it alongside the raw
|
/// Construct one per incoming message and pass it alongside the raw message
|
||||||
/// message body.
|
/// body.
|
||||||
///
|
///
|
||||||
/// All identifiers are platform-agnostic strings so this struct works with
|
/// All identifiers are platform-agnostic strings so this struct works with
|
||||||
/// any [`ChatTransport`](crate::chat::ChatTransport) implementation.
|
/// any [`ChatTransport`](crate::chat::ChatTransport) implementation.
|
||||||
pub struct CommandDispatch<'a> {
|
pub struct CommandDispatch<'a> {
|
||||||
/// The bot's display name (e.g., "Timmy").
|
/// Shared services bundle (project root, agent pool, ambient rooms, …).
|
||||||
pub bot_name: &'a str,
|
pub services: &'a Services,
|
||||||
/// The bot's full user ID (e.g., `"@timmy:homeserver.local"` on Matrix).
|
/// Effective project root — usually `services.project_root`, but the Matrix
|
||||||
pub bot_user_id: &'a str,
|
/// transport overrides this in gateway mode to point at the active project.
|
||||||
/// Project root directory (needed by status, ambient).
|
|
||||||
pub project_root: &'a Path,
|
pub project_root: &'a Path,
|
||||||
/// Agent pool (needed by status).
|
/// Bot user ID for mention-stripping — transport-specific (e.g. Matrix's
|
||||||
pub agents: &'a AgentPool,
|
/// `OwnedUserId` string differs from `services.bot_user_id`).
|
||||||
/// Set of room IDs with ambient mode enabled (needed by ambient).
|
pub bot_user_id: &'a str,
|
||||||
pub ambient_rooms: &'a Arc<Mutex<HashSet<String>>>,
|
|
||||||
/// The room this message came from (needed by ambient).
|
/// The room this message came from (needed by ambient).
|
||||||
pub room_id: &'a str,
|
pub room_id: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Context passed to individual command handlers.
|
/// 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> {
|
pub struct CommandContext<'a> {
|
||||||
/// The bot's display name (e.g., "Timmy").
|
/// Shared services bundle.
|
||||||
pub bot_name: &'a str,
|
pub services: &'a Services,
|
||||||
/// Any text after the command keyword, trimmed.
|
/// Any text after the command keyword, trimmed.
|
||||||
pub args: &'a str,
|
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).
|
/// The room this message came from (needed by ambient).
|
||||||
pub room_id: &'a str,
|
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.
|
/// Returns the full list of registered bot commands.
|
||||||
@@ -245,7 +271,8 @@ pub fn try_handle_command_with_html(
|
|||||||
dispatch: &CommandDispatch<'_>,
|
dispatch: &CommandDispatch<'_>,
|
||||||
message: &str,
|
message: &str,
|
||||||
) -> Option<(String, String)> {
|
) -> 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();
|
let trimmed = command_text.trim();
|
||||||
if !trimmed.is_empty() {
|
if !trimmed.is_empty() {
|
||||||
let (cmd_name, args) = match trimmed.split_once(char::is_whitespace) {
|
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.
|
// Status command: emoji indicators render natively in all clients.
|
||||||
if cmd_name.eq_ignore_ascii_case("status") && args.is_empty() {
|
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);
|
let html = plain_to_html(&body);
|
||||||
return Some((body, html));
|
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`
|
/// Returns `Some(response)` if a command matched and was handled, `None`
|
||||||
/// otherwise (the caller should fall through to the LLM).
|
/// otherwise (the caller should fall through to the LLM).
|
||||||
pub fn try_handle_command(dispatch: &CommandDispatch<'_>, message: &str) -> Option<String> {
|
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();
|
let trimmed = command_text.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
return None;
|
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 cmd_lower = cmd_name.to_ascii_lowercase();
|
||||||
|
|
||||||
let ctx = CommandContext {
|
let ctx = CommandContext {
|
||||||
bot_name: dispatch.bot_name,
|
services: dispatch.services,
|
||||||
args,
|
args,
|
||||||
project_root: dispatch.project_root,
|
|
||||||
agents: dispatch.agents,
|
|
||||||
ambient_rooms: dispatch.ambient_rooms,
|
|
||||||
room_id: dispatch.room_id,
|
room_id: dispatch.room_id,
|
||||||
|
project_root: dispatch.project_root,
|
||||||
};
|
};
|
||||||
|
|
||||||
commands()
|
commands()
|
||||||
@@ -382,39 +409,38 @@ fn handle_rebuild_fallback(_ctx: &CommandContext) -> Option<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
use crate::services::Services;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
// -- test helpers (shared with submodule tests) -------------------------
|
// -- test helpers (shared with submodule tests) -------------------------
|
||||||
|
|
||||||
pub fn test_ambient_rooms() -> Arc<Mutex<HashSet<String>>> {
|
/// Build a [`Services`] bundle for tests with the given bot name and a `/tmp`
|
||||||
Arc::new(Mutex::new(HashSet::new()))
|
/// 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> {
|
/// Dispatch a message through the command registry using the given Services.
|
||||||
Arc::new(AgentPool::new_test(3000))
|
pub fn try_cmd_with_services(
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_cmd(
|
|
||||||
bot_name: &str,
|
|
||||||
bot_user_id: &str,
|
bot_user_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
ambient_rooms: &Arc<Mutex<HashSet<String>>>,
|
services: &Services,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let agents = test_agents();
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name,
|
services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id,
|
bot_user_id,
|
||||||
project_root: std::path::Path::new("/tmp"),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, message)
|
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> {
|
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
|
// Re-export commands() for submodule tests
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
|||||||
None => {
|
None => {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} move <number> <stage>`\n\nValid stages: {}",
|
"Usage: `{} move <number> <stage>`\n\nValid stages: {}",
|
||||||
ctx.bot_name,
|
ctx.services.bot_name,
|
||||||
VALID_STAGES.join(", ")
|
VALID_STAGES.join(", ")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Invalid story number: `{num_str}`. Usage: `{} move <number> <stage>`",
|
"Invalid story number: `{num_str}`. Usage: `{} move <number> <stage>`",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
|||||||
|
|
||||||
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
// Find the story by numeric prefix: CRDT → content store → filesystem.
|
||||||
let (story_id, _stage_dir, _path, content) =
|
let (story_id, _stage_dir, _path, content) =
|
||||||
match crate::chat::lookup::find_story_by_number(ctx.project_root, num_str) {
|
match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
|
||||||
Some(found) => found,
|
Some(found) => found,
|
||||||
None => {
|
None => {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
@@ -62,7 +62,7 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
|||||||
|
|
||||||
let display_name = found_name.as_deref().unwrap_or(&story_id);
|
let display_name = found_name.as_deref().unwrap_or(&story_id);
|
||||||
|
|
||||||
match move_story_to_stage(ctx.project_root, &story_id, &target_stage) {
|
match move_story_to_stage(ctx.effective_root(), &story_id, &target_stage) {
|
||||||
Ok((from_stage, to_stage)) => Some(format!(
|
Ok((from_stage, to_stage)) => Some(format!(
|
||||||
"Moved **{display_name}** from **{from_stage}** to **{to_stage}**."
|
"Moved **{display_name}** from **{from_stage}** to **{to_stage}**."
|
||||||
)),
|
)),
|
||||||
@@ -76,22 +76,15 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn move_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
fn move_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy move {args}"))
|
try_handle_command(&dispatch, &format!("@timmy move {args}"))
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ pub(super) fn handle_overview(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() {
|
if num_str.is_empty() {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} overview <number>`\n\nShows the implementation summary for a story.",
|
"Usage: `{} overview <number>`\n\nShows the implementation summary for a story.",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Invalid story number: `{num_str}`. Usage: `{} overview <number>`",
|
"Invalid story number: `{num_str}`. Usage: `{} overview <number>`",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let commit_hash = match find_story_merge_commit(ctx.project_root, num_str) {
|
let commit_hash = match find_story_merge_commit(ctx.effective_root(), num_str) {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
None => {
|
None => {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
@@ -33,9 +33,9 @@ pub(super) fn handle_overview(ctx: &CommandContext) -> Option<String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let stat_output = get_commit_stat(ctx.project_root, &commit_hash);
|
let stat_output = get_commit_stat(ctx.effective_root(), &commit_hash);
|
||||||
let symbols = extract_diff_symbols(ctx.project_root, &commit_hash);
|
let symbols = extract_diff_symbols(ctx.effective_root(), &commit_hash);
|
||||||
let story_name = find_story_name(ctx.project_root, num_str);
|
let story_name = find_story_name(ctx.effective_root(), num_str);
|
||||||
|
|
||||||
let short_hash = &commit_hash[..commit_hash.len().min(8)];
|
let short_hash = &commit_hash[..commit_hash.len().min(8)];
|
||||||
let mut out = match story_name {
|
let mut out = match story_name {
|
||||||
@@ -195,22 +195,16 @@ fn parse_symbol_definition(code: &str) -> Option<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn overview_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
fn overview_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy overview {args}"))
|
try_handle_command(&dispatch, &format!("@timmy overview {args}"))
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const MAX_OUTPUT_LINES: usize = 80;
|
|||||||
fn resolve_run_dir(ctx: &CommandContext) -> Result<PathBuf, String> {
|
fn resolve_run_dir(ctx: &CommandContext) -> Result<PathBuf, String> {
|
||||||
let number = ctx.args.trim();
|
let number = ctx.args.trim();
|
||||||
if number.is_empty() {
|
if number.is_empty() {
|
||||||
return Ok(ctx.project_root.to_path_buf());
|
return Ok(ctx.effective_root().to_path_buf());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate: must be all digits.
|
// Validate: must be all digits.
|
||||||
@@ -32,7 +32,7 @@ fn resolve_run_dir(ctx: &CommandContext) -> Result<PathBuf, String> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let worktrees_dir = ctx.project_root.join(".huskies/worktrees");
|
let worktrees_dir = ctx.effective_root().join(".huskies/worktrees");
|
||||||
let prefix = format!("{number}_");
|
let prefix = format!("{number}_");
|
||||||
match std::fs::read_dir(&worktrees_dir) {
|
match std::fs::read_dir(&worktrees_dir) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
@@ -145,32 +145,13 @@ fn extract_count(line: &str, label: &str) -> Option<u64> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
fn make_ctx<'a>(
|
fn make_ctx<'a>(
|
||||||
agents: &'a Arc<AgentPool>,
|
services: &'a crate::services::Services,
|
||||||
ambient_rooms: &'a Arc<Mutex<HashSet<String>>>,
|
|
||||||
project_root: &'a std::path::Path,
|
project_root: &'a std::path::Path,
|
||||||
args: &'a str,
|
args: &'a str,
|
||||||
) -> super::super::CommandContext<'a> {
|
) -> super::super::CommandContext<'a> {
|
||||||
super::super::CommandContext {
|
super::super::CommandContext::new_test(services, args, "!test:example.com", project_root)
|
||||||
bot_name: "Timmy",
|
|
||||||
args,
|
|
||||||
project_root,
|
|
||||||
agents,
|
|
||||||
ambient_rooms,
|
|
||||||
room_id: "!test:example.com",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_agents() -> Arc<AgentPool> {
|
|
||||||
Arc::new(AgentPool::new_test(3000))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_ambient() -> Arc<Mutex<HashSet<String>>> {
|
|
||||||
Arc::new(Mutex::new(HashSet::new()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_script(dir: &std::path::Path, content: &str) {
|
fn write_script(dir: &std::path::Path, content: &str) {
|
||||||
@@ -209,9 +190,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_command_missing_script_returns_error() {
|
fn test_command_missing_script_returns_error() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_test(&ctx).unwrap();
|
let output = handle_test(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("not found") || output.contains("script"),
|
output.contains("not found") || output.contains("script"),
|
||||||
@@ -226,9 +207,9 @@ mod tests {
|
|||||||
dir.path(),
|
dir.path(),
|
||||||
"#!/usr/bin/env bash\necho 'test result: ok. 4 passed; 0 failed'\nexit 0\n",
|
"#!/usr/bin/env bash\necho 'test result: ok. 4 passed; 0 failed'\nexit 0\n",
|
||||||
);
|
);
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_test(&ctx).unwrap();
|
let output = handle_test(&ctx).unwrap();
|
||||||
assert!(output.contains("PASS"), "should show PASS: {output}");
|
assert!(output.contains("PASS"), "should show PASS: {output}");
|
||||||
assert!(output.contains('4'), "should show test count: {output}");
|
assert!(output.contains('4'), "should show test count: {output}");
|
||||||
@@ -241,9 +222,9 @@ mod tests {
|
|||||||
dir.path(),
|
dir.path(),
|
||||||
"#!/usr/bin/env bash\necho 'test result: FAILED. 1 passed; 2 failed'\nexit 1\n",
|
"#!/usr/bin/env bash\necho 'test result: FAILED. 1 passed; 2 failed'\nexit 1\n",
|
||||||
);
|
);
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_test(&ctx).unwrap();
|
let output = handle_test(&ctx).unwrap();
|
||||||
assert!(output.contains("FAIL"), "should show FAIL: {output}");
|
assert!(output.contains("FAIL"), "should show FAIL: {output}");
|
||||||
assert!(output.contains('2'), "should show failed count: {output}");
|
assert!(output.contains('2'), "should show failed count: {output}");
|
||||||
@@ -253,15 +234,13 @@ mod tests {
|
|||||||
fn test_command_works_via_dispatch() {
|
fn test_command_works_via_dispatch() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
write_script(dir.path(), "#!/usr/bin/env bash\necho 'ok'\nexit 0\n");
|
write_script(dir.path(), "#!/usr/bin/env bash\necho 'ok'\nexit 0\n");
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = super::super::CommandDispatch {
|
let dispatch = super::super::CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: dir.path(),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let result = super::super::try_handle_command(&dispatch, "@timmy run_tests");
|
let result = super::super::try_handle_command(&dispatch, "@timmy run_tests");
|
||||||
@@ -312,9 +291,9 @@ mod tests {
|
|||||||
dir.path(),
|
dir.path(),
|
||||||
"#!/usr/bin/env bash\necho 'test result: ok. 7 passed; 0 failed'\nexit 0\n",
|
"#!/usr/bin/env bash\necho 'test result: ok. 7 passed; 0 failed'\nexit 0\n",
|
||||||
);
|
);
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
|
let ctx = make_ctx(&services, dir.path(), "");
|
||||||
let output = handle_test(&ctx).unwrap();
|
let output = handle_test(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("PASS"),
|
output.contains("PASS"),
|
||||||
@@ -330,9 +309,9 @@ mod tests {
|
|||||||
fn run_tests_with_story_number_uses_worktree() {
|
fn run_tests_with_story_number_uses_worktree() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
create_worktree(dir.path(), 541, true);
|
create_worktree(dir.path(), 541, true);
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "541");
|
let ctx = make_ctx(&services, dir.path(), "541");
|
||||||
let output = handle_test(&ctx).unwrap();
|
let output = handle_test(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("PASS"),
|
output.contains("PASS"),
|
||||||
@@ -349,9 +328,9 @@ mod tests {
|
|||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
// Create the worktrees dir but no matching entry
|
// Create the worktrees dir but no matching entry
|
||||||
std::fs::create_dir_all(dir.path().join(".huskies/worktrees")).unwrap();
|
std::fs::create_dir_all(dir.path().join(".huskies/worktrees")).unwrap();
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "999");
|
let ctx = make_ctx(&services, dir.path(), "999");
|
||||||
let output = handle_test(&ctx).unwrap();
|
let output = handle_test(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("No worktree found") || output.contains("999"),
|
output.contains("No worktree found") || output.contains("999"),
|
||||||
@@ -362,9 +341,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn run_tests_with_invalid_arg_returns_error() {
|
fn run_tests_with_invalid_arg_returns_error() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let ctx = make_ctx(&agents, &ambient, dir.path(), "notanumber");
|
let ctx = make_ctx(&services, dir.path(), "notanumber");
|
||||||
let output = handle_test(&ctx).unwrap();
|
let output = handle_test(&ctx).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("Invalid argument") || output.contains("notanumber"),
|
output.contains("Invalid argument") || output.contains("notanumber"),
|
||||||
@@ -376,15 +355,13 @@ mod tests {
|
|||||||
fn run_tests_with_story_number_via_dispatch() {
|
fn run_tests_with_story_number_via_dispatch() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
create_worktree(dir.path(), 541, true);
|
create_worktree(dir.path(), 541, true);
|
||||||
let agents = test_agents();
|
let services =
|
||||||
let ambient = test_ambient();
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Timmy".to_string());
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = super::super::CommandDispatch {
|
let dispatch = super::super::CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: dir.path(),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
let result = super::super::try_handle_command(&dispatch, "@timmy run_tests 541");
|
let result = super::super::try_handle_command(&dispatch, "@timmy run_tests 541");
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub(super) fn handle_setup(ctx: &CommandContext) -> Option<String> {
|
|||||||
/// This mirrors `wizard_generate` (with no content) from the MCP tools, making
|
/// This mirrors `wizard_generate` (with no content) from the MCP tools, making
|
||||||
/// the interview flow accessible from chat transports (Matrix, Slack, WhatsApp).
|
/// the interview flow accessible from chat transports (Matrix, Slack, WhatsApp).
|
||||||
fn wizard_generate_reply(ctx: &CommandContext) -> String {
|
fn wizard_generate_reply(ctx: &CommandContext) -> String {
|
||||||
let root = ctx.project_root;
|
let root = ctx.effective_root();
|
||||||
let mut state = match WizardState::load(root) {
|
let mut state = match WizardState::load(root) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return "No wizard active.".to_string(),
|
None => return "No wizard active.".to_string(),
|
||||||
@@ -63,10 +63,10 @@ fn wizard_generate_reply(ctx: &CommandContext) -> String {
|
|||||||
/// If no wizard state exists, automatically initializes it so the user does
|
/// If no wizard state exists, automatically initializes it so the user does
|
||||||
/// not need to run `huskies init` manually.
|
/// not need to run `huskies init` manually.
|
||||||
fn wizard_status_reply(ctx: &CommandContext) -> String {
|
fn wizard_status_reply(ctx: &CommandContext) -> String {
|
||||||
if WizardState::load(ctx.project_root).is_none() {
|
if WizardState::load(ctx.effective_root()).is_none() {
|
||||||
WizardState::init_if_missing(ctx.project_root);
|
WizardState::init_if_missing(ctx.effective_root());
|
||||||
}
|
}
|
||||||
match WizardState::load(ctx.project_root) {
|
match WizardState::load(ctx.effective_root()) {
|
||||||
Some(state) => format_wizard_state(&state),
|
Some(state) => format_wizard_state(&state),
|
||||||
None => "Unable to initialize setup wizard. Ensure the `.huskies/` directory exists."
|
None => "Unable to initialize setup wizard. Ensure the `.huskies/` directory exists."
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -75,7 +75,7 @@ fn wizard_status_reply(ctx: &CommandContext) -> String {
|
|||||||
|
|
||||||
/// Confirm the current wizard step, writing any staged content to disk.
|
/// Confirm the current wizard step, writing any staged content to disk.
|
||||||
fn wizard_confirm_reply(ctx: &CommandContext) -> String {
|
fn wizard_confirm_reply(ctx: &CommandContext) -> String {
|
||||||
let root = ctx.project_root;
|
let root = ctx.effective_root();
|
||||||
let mut state = match WizardState::load(root) {
|
let mut state = match WizardState::load(root) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return "No wizard active.".to_string(),
|
None => return "No wizard active.".to_string(),
|
||||||
@@ -124,7 +124,7 @@ fn wizard_confirm_reply(ctx: &CommandContext) -> String {
|
|||||||
|
|
||||||
/// Skip the current wizard step without writing any file.
|
/// Skip the current wizard step without writing any file.
|
||||||
fn wizard_skip_reply(ctx: &CommandContext) -> String {
|
fn wizard_skip_reply(ctx: &CommandContext) -> String {
|
||||||
let root = ctx.project_root;
|
let root = ctx.effective_root();
|
||||||
let mut state = match WizardState::load(root) {
|
let mut state = match WizardState::load(root) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return "No wizard active.".to_string(),
|
None => return "No wizard active.".to_string(),
|
||||||
@@ -157,7 +157,7 @@ fn wizard_skip_reply(ctx: &CommandContext) -> String {
|
|||||||
|
|
||||||
/// Discard staged content and reset the current step to pending.
|
/// Discard staged content and reset the current step to pending.
|
||||||
fn wizard_retry_reply(ctx: &CommandContext) -> String {
|
fn wizard_retry_reply(ctx: &CommandContext) -> String {
|
||||||
let root = ctx.project_root;
|
let root = ctx.effective_root();
|
||||||
let mut state = match WizardState::load(root) {
|
let mut state = match WizardState::load(root) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return "No wizard active.".to_string(),
|
None => return "No wizard active.".to_string(),
|
||||||
@@ -189,33 +189,23 @@ fn wizard_retry_reply(ctx: &CommandContext) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::io::wizard::WizardState;
|
use crate::io::wizard::WizardState;
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
fn make_ctx<'a>(
|
fn make_ctx<'a>(
|
||||||
args: &'a str,
|
args: &'a str,
|
||||||
project_root: &'a std::path::Path,
|
project_root: &'a std::path::Path,
|
||||||
agents: &'a Arc<crate::agents::AgentPool>,
|
services: &'a crate::services::Services,
|
||||||
ambient_rooms: &'a Arc<Mutex<HashSet<String>>>,
|
|
||||||
) -> CommandContext<'a> {
|
) -> CommandContext<'a> {
|
||||||
CommandContext {
|
CommandContext::new_test(services, args, "!test:example.com", project_root)
|
||||||
bot_name: "Bot",
|
|
||||||
args,
|
|
||||||
project_root,
|
|
||||||
agents,
|
|
||||||
ambient_rooms,
|
|
||||||
room_id: "!test:example.com",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn setup_no_wizard_auto_initializes() {
|
fn setup_no_wizard_auto_initializes() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4000));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
// Bot should auto-initialize and return wizard status, not ask user to run huskies init.
|
// Bot should auto-initialize and return wizard status, not ask user to run huskies init.
|
||||||
assert!(result.contains("Setup wizard"));
|
assert!(result.contains("Setup wizard"));
|
||||||
@@ -229,9 +219,9 @@ mod tests {
|
|||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||||
WizardState::init_if_missing(dir.path());
|
WizardState::init_if_missing(dir.path());
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4001));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
assert!(result.contains("Setup wizard"));
|
assert!(result.contains("Setup wizard"));
|
||||||
}
|
}
|
||||||
@@ -241,9 +231,9 @@ mod tests {
|
|||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||||
WizardState::init_if_missing(dir.path());
|
WizardState::init_if_missing(dir.path());
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4002));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("skip", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("skip", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
assert!(result.contains("skipped"));
|
assert!(result.contains("skipped"));
|
||||||
let state = WizardState::load(dir.path()).unwrap();
|
let state = WizardState::load(dir.path()).unwrap();
|
||||||
@@ -255,9 +245,9 @@ mod tests {
|
|||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||||
WizardState::init_if_missing(dir.path());
|
WizardState::init_if_missing(dir.path());
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4003));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("confirm", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("confirm", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
assert!(result.contains("confirmed"));
|
assert!(result.contains("confirmed"));
|
||||||
let state = WizardState::load(dir.path()).unwrap();
|
let state = WizardState::load(dir.path()).unwrap();
|
||||||
@@ -279,9 +269,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
state.save(dir.path()).unwrap();
|
state.save(dir.path()).unwrap();
|
||||||
}
|
}
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4004));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("retry", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("retry", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
assert!(result.contains("reset"));
|
assert!(result.contains("reset"));
|
||||||
let state = WizardState::load(dir.path()).unwrap();
|
let state = WizardState::load(dir.path()).unwrap();
|
||||||
@@ -294,9 +284,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn setup_unknown_sub_command_returns_usage() {
|
fn setup_unknown_sub_command_returns_usage() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4005));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("foobar", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("foobar", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
assert!(result.contains("Unknown sub-command"));
|
assert!(result.contains("Unknown sub-command"));
|
||||||
assert!(result.contains("Usage"));
|
assert!(result.contains("Usage"));
|
||||||
@@ -307,9 +297,9 @@ mod tests {
|
|||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||||
WizardState::init_if_missing(dir.path());
|
WizardState::init_if_missing(dir.path());
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4006));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("generate", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("generate", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
assert!(result.contains("generating"));
|
assert!(result.contains("generating"));
|
||||||
let state = WizardState::load(dir.path()).unwrap();
|
let state = WizardState::load(dir.path()).unwrap();
|
||||||
@@ -325,9 +315,9 @@ mod tests {
|
|||||||
// Bare project — only scaffolding files
|
// Bare project — only scaffolding files
|
||||||
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||||
WizardState::init_if_missing(dir.path());
|
WizardState::init_if_missing(dir.path());
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4007));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("generate", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("generate", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
assert!(result.contains("bare project"));
|
assert!(result.contains("bare project"));
|
||||||
assert!(result.contains("Ask the user"));
|
assert!(result.contains("Ask the user"));
|
||||||
@@ -336,9 +326,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn setup_generate_no_wizard_returns_error() {
|
fn setup_generate_no_wizard_returns_error() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let agents = Arc::new(crate::agents::AgentPool::new_test(4008));
|
let services =
|
||||||
let rooms = Arc::new(Mutex::new(HashSet::new()));
|
crate::services::Services::new_test(dir.path().to_path_buf(), "Bot".to_string());
|
||||||
let ctx = make_ctx("generate", dir.path(), &agents, &rooms);
|
let ctx = make_ctx("generate", dir.path(), &services);
|
||||||
let result = handle_setup(&ctx).unwrap();
|
let result = handle_setup(&ctx).unwrap();
|
||||||
assert!(result.contains("No wizard active"));
|
assert!(result.contains("No wizard active"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,19 +70,19 @@ pub(super) fn handle_show(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() {
|
if num_str.is_empty() {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} show <number>`\n\nDisplays the full text of a story, bug, or spike.",
|
"Usage: `{} show <number>`\n\nDisplays the full text of a story, bug, or spike.",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Invalid story number: `{num_str}`. Usage: `{} show <number>`",
|
"Invalid story number: `{num_str}`. Usage: `{} show <number>`",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the story by numeric prefix: CRDT → content store.
|
// Find the story by numeric prefix: CRDT → content store.
|
||||||
let (story_id, _stage_dir, _path, content) =
|
let (story_id, _stage_dir, _path, content) =
|
||||||
match crate::chat::lookup::find_story_by_number(ctx.project_root, num_str) {
|
match crate::chat::lookup::find_story_by_number(ctx.effective_root(), num_str) {
|
||||||
Some(found) => found,
|
Some(found) => found,
|
||||||
None => {
|
None => {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
@@ -129,22 +129,15 @@ pub(super) fn handle_show(ctx: &CommandContext) -> Option<String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn show_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
fn show_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy show {args}"))
|
try_handle_command(&dispatch, &format!("@timmy show {args}"))
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ use super::CommandContext;
|
|||||||
|
|
||||||
pub(super) fn handle_status(ctx: &CommandContext) -> Option<String> {
|
pub(super) fn handle_status(ctx: &CommandContext) -> Option<String> {
|
||||||
if ctx.args.trim().is_empty() {
|
if ctx.args.trim().is_empty() {
|
||||||
Some(build_pipeline_status(ctx.project_root, ctx.agents))
|
Some(build_pipeline_status(
|
||||||
|
ctx.effective_root(),
|
||||||
|
&ctx.services.agents,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
super::triage::handle_triage(ctx)
|
super::triage::handle_triage(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ pub(super) fn handle_triage(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() {
|
if num_str.is_empty() {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} status <number>`\n\nShows pipeline info for a story: stage, ACs, git diff, recent commits.",
|
"Usage: `{} status <number>`\n\nShows pipeline info for a story: stage, ACs, git diff, recent commits.",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Invalid story number: `{num_str}`. Usage: `{} status <number>`",
|
"Invalid story number: `{num_str}`. Usage: `{} status <number>`",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ fn build_triage_dump(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Worktree and branch ----
|
// ---- Worktree and branch ----
|
||||||
let wt_path = crate::worktree::worktree_path(ctx.project_root, story_id);
|
let wt_path = crate::worktree::worktree_path(ctx.effective_root(), story_id);
|
||||||
let branch = format!("feature/story-{story_id}");
|
let branch = format!("feature/story-{story_id}");
|
||||||
if wt_path.is_dir() {
|
if wt_path.is_dir() {
|
||||||
out.push_str(&format!("**Worktree:** `{}`\n", wt_path.display()));
|
out.push_str(&format!("**Worktree:** `{}`\n", wt_path.display()));
|
||||||
@@ -216,22 +216,16 @@ fn run_git(dir: &Path, args: &[&str]) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn status_triage_cmd(root: &Path, args: &str) -> Option<String> {
|
fn status_triage_cmd(root: &Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy status {args}"))
|
try_handle_command(&dispatch, &format!("@timmy status {args}"))
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ pub(super) fn handle_unblock(ctx: &CommandContext) -> Option<String> {
|
|||||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Usage: `{} unblock <number>` (e.g. `unblock 42`)",
|
"Usage: `{} unblock <number>` (e.g. `unblock 42`)",
|
||||||
ctx.bot_name
|
ctx.services.bot_name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(unblock_by_number(ctx.project_root, num_str))
|
Some(unblock_by_number(ctx.effective_root(), num_str))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Core unblock logic: find story by numeric prefix and reset its blocked state.
|
/// Core unblock logic: find story by numeric prefix and reset its blocked state.
|
||||||
@@ -102,22 +102,15 @@ fn unblock_by_story_id(story_id: &str) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn unblock_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
fn unblock_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, &format!("@timmy unblock {args}"))
|
try_handle_command(&dispatch, &format!("@timmy unblock {args}"))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use super::CommandContext;
|
|||||||
/// that tag and HEAD on master. Each entry shows the story number and name.
|
/// that tag and HEAD on master. Each entry shows the story number and name.
|
||||||
/// Returns a clear message when there are no unreleased stories or no tags.
|
/// Returns a clear message when there are no unreleased stories or no tags.
|
||||||
pub(super) fn handle_unreleased(ctx: &CommandContext) -> Option<String> {
|
pub(super) fn handle_unreleased(ctx: &CommandContext) -> Option<String> {
|
||||||
let root = ctx.project_root;
|
let root = ctx.effective_root();
|
||||||
let tag = find_last_release_tag(root);
|
let tag = find_last_release_tag(root);
|
||||||
|
|
||||||
let commits = list_merge_commits_since(root, tag.as_deref());
|
let commits = list_merge_commits_since(root, tag.as_deref());
|
||||||
@@ -201,22 +201,16 @@ fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agents::AgentPool;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use super::super::{CommandDispatch, try_handle_command};
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
fn unreleased_cmd_with_root(root: &std::path::Path) -> Option<String> {
|
fn unreleased_cmd_with_root(root: &std::path::Path) -> Option<String> {
|
||||||
let agents = Arc::new(AgentPool::new_test(3000));
|
let services = crate::services::Services::new_test(root.to_path_buf(), "Timmy".to_string());
|
||||||
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let room_id = "!test:example.com".to_string();
|
let room_id = "!test:example.com".to_string();
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: "Timmy",
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "@timmy:homeserver.local",
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
project_root: root,
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
try_handle_command(&dispatch, "@timmy unreleased")
|
try_handle_command(&dispatch, "@timmy unreleased")
|
||||||
|
|||||||
@@ -67,11 +67,9 @@ pub(super) async fn handle_incoming_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: &ctx.services.bot_name,
|
services: &ctx.services,
|
||||||
bot_user_id: &ctx.services.bot_user_id,
|
|
||||||
project_root: &ctx.services.project_root,
|
project_root: &ctx.services.project_root,
|
||||||
agents: &ctx.services.agents,
|
bot_user_id: &ctx.services.bot_user_id,
|
||||||
ambient_rooms: &ctx.services.ambient_rooms,
|
|
||||||
room_id: channel,
|
room_id: channel,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -483,35 +481,25 @@ async fn handle_llm_message(ctx: &DiscordContext, channel: &str, user: &str, use
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
|
||||||
use tokio::sync::Mutex as TokioMutex;
|
use tokio::sync::Mutex as TokioMutex;
|
||||||
|
|
||||||
fn test_agents() -> Arc<crate::agents::AgentPool> {
|
|
||||||
Arc::new(crate::agents::AgentPool::new_test(3000))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_ambient_rooms() -> Arc<Mutex<HashSet<String>>> {
|
|
||||||
Arc::new(Mutex::new(HashSet::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_dispatches_through_command_registry() {
|
fn command_dispatches_through_command_registry() {
|
||||||
use crate::chat::commands::{CommandDispatch, try_handle_command};
|
use crate::chat::commands::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
let agents = test_agents();
|
let services = crate::services::Services::new_test(
|
||||||
let ambient_rooms = test_ambient_rooms();
|
std::path::PathBuf::from("/tmp"),
|
||||||
|
"Huskies".to_string(),
|
||||||
|
);
|
||||||
let room_id = "123456789".to_string();
|
let room_id = "123456789".to_string();
|
||||||
|
|
||||||
let bot_name = "Huskies";
|
let synthetic = "Huskies status".to_string();
|
||||||
let synthetic = format!("{bot_name} status");
|
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name,
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "discord-bot",
|
bot_user_id: "discord-bot",
|
||||||
project_root: std::path::Path::new("/tmp"),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -262,11 +262,9 @@ pub(super) async fn on_room_message(
|
|||||||
// 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.services.bot_name,
|
services: &ctx.services,
|
||||||
bot_user_id: ctx.matrix_user_id.as_str(),
|
|
||||||
project_root: &effective_root,
|
project_root: &effective_root,
|
||||||
agents: &ctx.services.agents,
|
bot_user_id: ctx.matrix_user_id.as_str(),
|
||||||
ambient_rooms: &ctx.services.ambient_rooms,
|
|
||||||
room_id: &room_id_str,
|
room_id: &room_id_str,
|
||||||
};
|
};
|
||||||
if let Some((response, response_html)) =
|
if let Some((response, response_html)) =
|
||||||
|
|||||||
@@ -108,11 +108,9 @@ pub(super) async fn handle_incoming_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: &ctx.services.bot_name,
|
services: &ctx.services,
|
||||||
bot_user_id: &ctx.services.bot_user_id,
|
|
||||||
project_root: &ctx.services.project_root,
|
project_root: &ctx.services.project_root,
|
||||||
agents: &ctx.services.agents,
|
bot_user_id: &ctx.services.bot_user_id,
|
||||||
ambient_rooms: &ctx.services.ambient_rooms,
|
|
||||||
room_id: channel,
|
room_id: channel,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -533,7 +531,6 @@ async fn handle_llm_message(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
// ── Slash command types ────────────────────────────────────────────
|
// ── Slash command types ────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -611,35 +608,26 @@ mod tests {
|
|||||||
|
|
||||||
// ── Slash command shares handlers with mention-based commands ──────
|
// ── Slash command shares handlers with mention-based commands ──────
|
||||||
|
|
||||||
fn test_agents() -> Arc<crate::agents::AgentPool> {
|
|
||||||
Arc::new(crate::agents::AgentPool::new_test(3000))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_ambient_rooms() -> Arc<Mutex<HashSet<String>>> {
|
|
||||||
Arc::new(Mutex::new(HashSet::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn slash_command_dispatches_through_command_registry() {
|
fn slash_command_dispatches_through_command_registry() {
|
||||||
// Verify that the synthetic message built by the slash handler
|
// Verify that the synthetic message built by the slash handler
|
||||||
// correctly dispatches through try_handle_command.
|
// correctly dispatches through try_handle_command.
|
||||||
use crate::chat::commands::{CommandDispatch, try_handle_command};
|
use crate::chat::commands::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
let agents = test_agents();
|
let services = crate::services::Services::new_test(
|
||||||
let ambient_rooms = test_ambient_rooms();
|
std::path::PathBuf::from("/tmp"),
|
||||||
|
"Huskies".to_string(),
|
||||||
|
);
|
||||||
let room_id = "C01ABCDEF".to_string();
|
let room_id = "C01ABCDEF".to_string();
|
||||||
|
|
||||||
// Simulate what slash_command_receive does: build a synthetic message.
|
// Simulate what slash_command_receive does: build a synthetic message.
|
||||||
let bot_name = "Huskies";
|
|
||||||
let keyword = slash_command_to_bot_keyword("/huskies-status").unwrap();
|
let keyword = slash_command_to_bot_keyword("/huskies-status").unwrap();
|
||||||
let synthetic = format!("{bot_name} {keyword}");
|
let synthetic = format!("Huskies {keyword}");
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name,
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "slack-bot",
|
bot_user_id: "slack-bot",
|
||||||
project_root: std::path::Path::new("/tmp"),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -655,21 +643,20 @@ mod tests {
|
|||||||
fn slash_command_show_passes_args_through_registry() {
|
fn slash_command_show_passes_args_through_registry() {
|
||||||
use crate::chat::commands::{CommandDispatch, try_handle_command};
|
use crate::chat::commands::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
let agents = test_agents();
|
let services = crate::services::Services::new_test(
|
||||||
let ambient_rooms = test_ambient_rooms();
|
std::path::PathBuf::from("/tmp"),
|
||||||
|
"Huskies".to_string(),
|
||||||
|
);
|
||||||
let room_id = "C01ABCDEF".to_string();
|
let room_id = "C01ABCDEF".to_string();
|
||||||
|
|
||||||
let bot_name = "Huskies";
|
|
||||||
let keyword = slash_command_to_bot_keyword("/huskies-show").unwrap();
|
let keyword = slash_command_to_bot_keyword("/huskies-show").unwrap();
|
||||||
// Simulate /huskies-show with text "999"
|
// Simulate /huskies-show with text "999"
|
||||||
let synthetic = format!("{bot_name} {keyword} 999");
|
let synthetic = format!("Huskies {keyword} 999");
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name,
|
services: &services,
|
||||||
|
project_root: &services.project_root,
|
||||||
bot_user_id: "slack-bot",
|
bot_user_id: "slack-bot",
|
||||||
project_root: std::path::Path::new("/tmp"),
|
|
||||||
agents: &agents,
|
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id: &room_id,
|
room_id: &room_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -209,11 +209,9 @@ pub async fn slash_command_receive(
|
|||||||
use crate::chat::commands::{CommandDispatch, try_handle_command};
|
use crate::chat::commands::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: &ctx.services.bot_name,
|
services: &ctx.services,
|
||||||
bot_user_id: &ctx.services.bot_user_id,
|
|
||||||
project_root: &ctx.services.project_root,
|
project_root: &ctx.services.project_root,
|
||||||
agents: &ctx.services.agents,
|
bot_user_id: &ctx.services.bot_user_id,
|
||||||
ambient_rooms: &ctx.services.ambient_rooms,
|
|
||||||
room_id: &payload.channel_id,
|
room_id: &payload.channel_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,9 @@ pub(super) async fn handle_incoming_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name: &ctx.services.bot_name,
|
services: &ctx.services,
|
||||||
bot_user_id: &ctx.services.bot_user_id,
|
|
||||||
project_root: &ctx.services.project_root,
|
project_root: &ctx.services.project_root,
|
||||||
agents: &ctx.services.agents,
|
bot_user_id: &ctx.services.bot_user_id,
|
||||||
ambient_rooms: &ctx.services.ambient_rooms,
|
|
||||||
room_id: sender,
|
room_id: sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -129,20 +129,29 @@ pub(super) fn call_sync(
|
|||||||
agents: &Arc<AgentPool>,
|
agents: &Arc<AgentPool>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
use crate::chat::commands::CommandDispatch;
|
use crate::chat::commands::CommandDispatch;
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
let ambient_rooms: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
|
|
||||||
let bot_name = "__web_ui__";
|
let bot_name = "__web_ui__";
|
||||||
let bot_user_id = "@__web_ui__:localhost";
|
let bot_user_id = "@__web_ui__:localhost";
|
||||||
let room_id = "__web_ui__";
|
let room_id = "__web_ui__";
|
||||||
|
|
||||||
|
let (_, perm_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
let services = Arc::new(crate::services::Services {
|
||||||
|
project_root: project_root.to_path_buf(),
|
||||||
|
agents: Arc::clone(agents),
|
||||||
|
bot_name: bot_name.to_string(),
|
||||||
|
bot_user_id: bot_user_id.to_string(),
|
||||||
|
ambient_rooms: Arc::new(Mutex::new(HashSet::new())),
|
||||||
|
perm_rx: Arc::new(tokio::sync::Mutex::new(perm_rx)),
|
||||||
|
pending_perm_replies: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
|
||||||
|
permission_timeout_secs: 120,
|
||||||
|
});
|
||||||
|
|
||||||
let dispatch = CommandDispatch {
|
let dispatch = CommandDispatch {
|
||||||
bot_name,
|
services: &services,
|
||||||
bot_user_id,
|
|
||||||
project_root,
|
project_root,
|
||||||
agents,
|
bot_user_id,
|
||||||
ambient_rooms: &ambient_rooms,
|
|
||||||
room_id,
|
room_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,3 +36,22 @@ pub struct Services {
|
|||||||
/// auto-denying (fail-closed).
|
/// auto-denying (fail-closed).
|
||||||
pub permission_timeout_secs: u64,
|
pub permission_timeout_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl Services {
|
||||||
|
/// Build a minimal `Services` for testing with the given project root and
|
||||||
|
/// bot display name.
|
||||||
|
pub fn new_test(project_root: std::path::PathBuf, bot_name: String) -> std::sync::Arc<Self> {
|
||||||
|
let (_perm_tx, perm_rx) = mpsc::unbounded_channel();
|
||||||
|
std::sync::Arc::new(Self {
|
||||||
|
project_root,
|
||||||
|
agents: std::sync::Arc::new(crate::agents::AgentPool::new_test(3000)),
|
||||||
|
bot_name,
|
||||||
|
bot_user_id: String::new(),
|
||||||
|
ambient_rooms: std::sync::Arc::new(std::sync::Mutex::new(HashSet::new())),
|
||||||
|
perm_rx: std::sync::Arc::new(TokioMutex::new(perm_rx)),
|
||||||
|
pending_perm_replies: std::sync::Arc::new(TokioMutex::new(HashMap::new())),
|
||||||
|
permission_timeout_secs: 120,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user