huskies: merge 1034
This commit is contained in:
@@ -18,22 +18,14 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option<String> {
|
||||
let args = ctx.args.trim();
|
||||
|
||||
if args.is_empty() {
|
||||
return Some(format!(
|
||||
"Usage: `{} depends <number> [dep1 dep2 ...]`\n\nExamples:\n\
|
||||
• `{0} depends 484 477 478` — set depends_on: [477, 478]\n\
|
||||
• `{0} depends 484` — clear all dependencies",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut parts = args.split_whitespace();
|
||||
let num_str = parts.next().unwrap_or("");
|
||||
|
||||
if !num_str.chars().all(|c| c.is_ascii_digit()) || num_str.is_empty() {
|
||||
return Some(format!(
|
||||
"Invalid story number: `{num_str}`. Usage: `{} depends <number> [dep1 dep2 ...]`",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Parse dependency numbers.
|
||||
@@ -129,22 +121,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depends_no_args_returns_usage() {
|
||||
fn depends_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = depends_cmd_with_root(tmp.path(), "").unwrap();
|
||||
let result = depends_cmd_with_root(tmp.path(), "");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage: {output}"
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depends_non_numeric_number_returns_error() {
|
||||
fn depends_non_numeric_number_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = depends_cmd_with_root(tmp.path(), "abc 1 2").unwrap();
|
||||
let result = depends_cmd_with_root(tmp.path(), "on 477 and 478");
|
||||
assert!(
|
||||
output.contains("Invalid story number"),
|
||||
"non-numeric story number should error: {output}"
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depends_well_formed_runs_handler() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = depends_cmd_with_root(tmp.path(), "999");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric story number should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,8 @@ use std::process::Command;
|
||||
/// Usage: `diff <number>`
|
||||
pub(super) fn handle_diff(ctx: &CommandContext) -> Option<String> {
|
||||
let num_str = ctx.args.trim();
|
||||
if num_str.is_empty() {
|
||||
return Some(format!(
|
||||
"Usage: `{} diff <number>`\n\nShows the git diff from the main branch to the story's worktree HEAD.",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
}
|
||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Invalid story number: `{num_str}`. Usage: `{} diff <number>`",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let story_id = match find_story_id(num_str) {
|
||||
@@ -169,22 +160,33 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_command_no_args_returns_usage() {
|
||||
fn diff_command_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = diff_cmd(tmp.path(), "").unwrap();
|
||||
let result = diff_cmd(tmp.path(), "");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage: {output}"
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_command_non_numeric_returns_error() {
|
||||
fn diff_command_non_numeric_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = diff_cmd(tmp.path(), "abc").unwrap();
|
||||
let result = diff_cmd(tmp.path(), "the login feature branch");
|
||||
assert!(
|
||||
output.contains("Invalid"),
|
||||
"non-numeric arg should return error: {output}"
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_command_well_formed_runs_handler() {
|
||||
crate::db::ensure_content_store();
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = diff_cmd(tmp.path(), "99994");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric arg should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,7 @@ use std::path::Path;
|
||||
pub(super) fn handle_freeze(ctx: &CommandContext) -> Option<String> {
|
||||
let num_str = ctx.args.trim();
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Usage: `{} freeze <number>` (e.g. `freeze 42`)",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
return None;
|
||||
}
|
||||
Some(freeze_by_number(ctx.effective_root(), num_str))
|
||||
}
|
||||
@@ -57,10 +54,7 @@ fn freeze_by_story_id(story_id: &str) -> String {
|
||||
pub(super) fn handle_unfreeze(ctx: &CommandContext) -> Option<String> {
|
||||
let num_str = ctx.args.trim();
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Usage: `{} unfreeze <number>` (e.g. `unfreeze 42`)",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
return None;
|
||||
}
|
||||
Some(unfreeze_by_number(ctx.effective_root(), num_str))
|
||||
}
|
||||
@@ -155,22 +149,62 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeze_command_no_args_returns_usage() {
|
||||
fn freeze_command_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = freeze_cmd_with_root(tmp.path(), "").unwrap();
|
||||
let result = freeze_cmd_with_root(tmp.path(), "");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage: {output}"
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfreeze_command_no_args_returns_usage() {
|
||||
fn freeze_command_non_numeric_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = unfreeze_cmd_with_root(tmp.path(), "").unwrap();
|
||||
let result = freeze_cmd_with_root(tmp.path(), "the login story");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage: {output}"
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeze_command_well_formed_runs_handler() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = freeze_cmd_with_root(tmp.path(), "999");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric arg should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfreeze_command_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = unfreeze_cmd_with_root(tmp.path(), "");
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfreeze_command_non_numeric_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = unfreeze_cmd_with_root(tmp.path(), "the login story");
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfreeze_command_well_formed_runs_handler() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = unfreeze_cmd_with_root(tmp.path(), "999");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric arg should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,8 @@ use std::path::{Path, PathBuf};
|
||||
/// recently modified agent log file for that story.
|
||||
pub(super) fn handle_logs(ctx: &CommandContext) -> Option<String> {
|
||||
let num_str = ctx.args.trim();
|
||||
if num_str.is_empty() {
|
||||
return Some(format!(
|
||||
"Usage: `{} logs <number>`\n\nShows the last agent log lines for a story.",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
}
|
||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Invalid story number: `{num_str}`. Usage: `{} logs <number>`",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let story_id = match find_story_id_by_number(num_str) {
|
||||
@@ -155,22 +146,32 @@ mod tests {
|
||||
// -- input validation ---------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn logs_no_args_returns_usage() {
|
||||
fn logs_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = logs_cmd(tmp.path(), "").unwrap();
|
||||
let result = logs_cmd(tmp.path(), "");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage: {output}"
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logs_non_numeric_returns_error() {
|
||||
fn logs_non_numeric_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = logs_cmd(tmp.path(), "abc").unwrap();
|
||||
let result = logs_cmd(tmp.path(), "for the login story");
|
||||
assert!(
|
||||
output.contains("Invalid"),
|
||||
"non-numeric arg should return error: {output}"
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logs_well_formed_runs_handler() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = logs_cmd(tmp.path(), "99999");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric arg should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -495,6 +495,84 @@ pub(crate) mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -- malformed-args routing (story 1034) -----------------------------------
|
||||
|
||||
#[test]
|
||||
fn malformed_unblock_args_route_to_timmy() {
|
||||
// "unblock to fix the blocking issue" — verb recognised, args look like
|
||||
// natural language rather than a numeric story ID. Must return None so
|
||||
// the message is forwarded to the LLM instead of showing a usage error.
|
||||
let result = try_cmd_addressed(
|
||||
"Timmy",
|
||||
"@timmy:homeserver.local",
|
||||
"@timmy unblock to fix the blocking issue",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"unblock with natural-language args must route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_start_args_route_to_timmy_via_registry() {
|
||||
// "start to get the cheese grater working" — the registry entry for
|
||||
// start always returns None (handled async), so even bad args produce None.
|
||||
let result = try_cmd_addressed(
|
||||
"Timmy",
|
||||
"@timmy:homeserver.local",
|
||||
"@timmy start to get the cheese grater working",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"start with natural-language args must route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_show_args_route_to_timmy() {
|
||||
let result = try_cmd_addressed(
|
||||
"Timmy",
|
||||
"@timmy:homeserver.local",
|
||||
"@timmy show me the login bug",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"show with natural-language args must route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_freeze_args_route_to_timmy() {
|
||||
let result = try_cmd_addressed(
|
||||
"Timmy",
|
||||
"@timmy:homeserver.local",
|
||||
"@timmy freeze the pipeline until Friday",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"freeze with natural-language args must route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn well_formed_unblock_runs_handler() {
|
||||
// Numeric story ID → command handler runs (story not found, but Some is returned).
|
||||
let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy unblock 1010");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed unblock command should run the handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn well_formed_show_runs_handler() {
|
||||
let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy show 984");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed show command should run the handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// -- commands registry --------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,19 +22,21 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option<String> {
|
||||
let (num_str, stage_raw) = match args.split_once(char::is_whitespace) {
|
||||
Some((n, s)) => (n.trim(), s.trim()),
|
||||
None => {
|
||||
// No stage argument: if args looks like a number show usage, otherwise
|
||||
// fall through to the LLM (looks like natural language).
|
||||
if !args.is_empty() && args.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Usage: `{} move <number> <stage>`\n\nValid stages: {}",
|
||||
ctx.services.bot_name,
|
||||
VALID_STAGES.join(", ")
|
||||
));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Invalid story number: `{num_str}`. Usage: `{} move <number> <stage>`",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
return None;
|
||||
}
|
||||
|
||||
let target_stage = stage_raw.to_ascii_lowercase();
|
||||
@@ -113,12 +115,22 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_command_no_args_returns_usage() {
|
||||
fn move_command_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = move_cmd_with_root(tmp.path(), "").unwrap();
|
||||
let result = move_cmd_with_root(tmp.path(), "");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage hint: {output}"
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_command_natural_language_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = move_cmd_with_root(tmp.path(), "the auth story to done");
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,7 +140,7 @@ mod tests {
|
||||
let output = move_cmd_with_root(tmp.path(), "42").unwrap();
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"missing stage should show usage hint: {output}"
|
||||
"numeric number without stage should show usage hint: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,12 +155,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_command_non_numeric_number_returns_error() {
|
||||
fn move_command_non_numeric_number_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = move_cmd_with_root(tmp.path(), "abc current").unwrap();
|
||||
let result = move_cmd_with_root(tmp.path(), "abc current");
|
||||
assert!(
|
||||
output.contains("Invalid story number"),
|
||||
"non-numeric number should return error: {output}"
|
||||
result.is_none(),
|
||||
"non-numeric story number should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,8 @@ use super::CommandContext;
|
||||
#[allow(clippy::string_slice)] // commit_hash is hex (ASCII), min(8) always within bounds
|
||||
pub(super) fn handle_overview(ctx: &CommandContext) -> Option<String> {
|
||||
let num_str = ctx.args.trim();
|
||||
if num_str.is_empty() {
|
||||
return Some(format!(
|
||||
"Usage: `{} overview <number>`\n\nShows the implementation summary for a story.",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
}
|
||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Invalid story number: `{num_str}`. Usage: `{} overview <number>`",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let commit_hash = match find_story_merge_commit(ctx.effective_root(), num_str) {
|
||||
@@ -232,22 +223,34 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_command_no_args_returns_usage() {
|
||||
fn overview_command_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = overview_cmd_with_root(tmp.path(), "").unwrap();
|
||||
let result = overview_cmd_with_root(tmp.path(), "");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage hint: {output}"
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_command_non_numeric_arg_returns_error() {
|
||||
fn overview_command_non_numeric_arg_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = overview_cmd_with_root(tmp.path(), "abc").unwrap();
|
||||
let result = overview_cmd_with_root(tmp.path(), "of the auth refactor");
|
||||
assert!(
|
||||
output.contains("Invalid"),
|
||||
"non-numeric arg should return error: {output}"
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_command_well_formed_runs_handler() {
|
||||
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap_or(std::path::Path::new("."));
|
||||
let result = overview_cmd_with_root(repo_root, "99999");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric arg should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,17 +69,8 @@ fn strip_front_matter(text: &str) -> (String, String) {
|
||||
/// Returns a friendly message when no match is found.
|
||||
pub(super) fn handle_show(ctx: &CommandContext) -> Option<String> {
|
||||
let num_str = ctx.args.trim();
|
||||
if num_str.is_empty() {
|
||||
return Some(format!(
|
||||
"Usage: `{} show <number>`\n\nDisplays the full text of a story, bug, or spike.",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
}
|
||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Invalid story number: `{num_str}`. Usage: `{} show <number>`",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the story by numeric prefix: CRDT → content store.
|
||||
@@ -169,22 +160,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_command_no_args_returns_usage() {
|
||||
fn show_command_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = show_cmd_with_root(tmp.path(), "").unwrap();
|
||||
let result = show_cmd_with_root(tmp.path(), "");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage hint: {output}"
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None), not return a usage error: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_command_non_numeric_args_returns_error() {
|
||||
fn show_command_non_numeric_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = show_cmd_with_root(tmp.path(), "abc").unwrap();
|
||||
let result = show_cmd_with_root(tmp.path(), "me the story about the login bug");
|
||||
assert!(
|
||||
output.contains("Invalid"),
|
||||
"non-numeric arg should return error message: {output}"
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_command_well_formed_runs_handler() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = show_cmd_with_root(tmp.path(), "999");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric arg should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,17 +18,8 @@ use std::process::Command;
|
||||
/// Handle `{bot_name} status {number}`.
|
||||
pub(super) fn handle_triage(ctx: &CommandContext) -> Option<String> {
|
||||
let num_str = ctx.args.trim();
|
||||
if num_str.is_empty() {
|
||||
return Some(format!(
|
||||
"Usage: `{} status <number>`\n\nShows pipeline info for a story: stage, ACs, git diff, recent commits.",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
}
|
||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Invalid story number: `{num_str}`. Usage: `{} status <number>`",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
match find_story_by_number(num_str) {
|
||||
@@ -276,12 +267,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whatsup_non_numeric_returns_error() {
|
||||
fn whatsup_non_numeric_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = status_triage_cmd(tmp.path(), "abc").unwrap();
|
||||
let result = status_triage_cmd(tmp.path(), "what is going on");
|
||||
assert!(
|
||||
output.contains("Invalid"),
|
||||
"non-numeric arg should return error: {output}"
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whatsup_well_formed_runs_handler() {
|
||||
crate::db::ensure_content_store();
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = status_triage_cmd(tmp.path(), "99996");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric arg should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,7 @@ pub(super) fn handle_unblock(ctx: &CommandContext) -> Option<String> {
|
||||
let num_str = ctx.args.trim();
|
||||
|
||||
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(format!(
|
||||
"Usage: `{} unblock <number>` (e.g. `unblock 42`)",
|
||||
ctx.services.bot_name
|
||||
));
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(unblock_by_number(ctx.effective_root(), num_str))
|
||||
@@ -152,22 +149,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unblock_command_no_args_returns_usage() {
|
||||
fn unblock_command_no_args_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = unblock_cmd_with_root(tmp.path(), "").unwrap();
|
||||
let result = unblock_cmd_with_root(tmp.path(), "");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"no args should show usage hint: {output}"
|
||||
result.is_none(),
|
||||
"no args should route to LLM (None), not return a usage error: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unblock_command_non_numeric_returns_usage() {
|
||||
fn unblock_command_non_numeric_routes_to_timmy() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let output = unblock_cmd_with_root(tmp.path(), "abc").unwrap();
|
||||
let result = unblock_cmd_with_root(tmp.path(), "to fix the blocking issue");
|
||||
assert!(
|
||||
output.contains("Usage"),
|
||||
"non-numeric arg should show usage hint: {output}"
|
||||
result.is_none(),
|
||||
"natural-language args should route to LLM (None): {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unblock_command_well_formed_runs_handler() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let result = unblock_cmd_with_root(tmp.path(), "666");
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"well-formed numeric arg should run the command handler: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -272,35 +272,26 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
|
||||
|
||||
// Check for the assign command, which requires async agent ops (stop +
|
||||
// start) and cannot be handled by the sync command registry.
|
||||
if let Some(assign_cmd) = super::super::super::assign::extract_assign_command(
|
||||
// Only handle the well-formed variant; BadArgs falls through to the LLM.
|
||||
if let Some(super::super::super::assign::AssignCommand::Assign {
|
||||
story_number,
|
||||
model,
|
||||
}) = super::super::super::assign::extract_assign_command(
|
||||
&user_message,
|
||||
&ctx.services.bot_name,
|
||||
ctx.matrix_user_id.as_str(),
|
||||
) {
|
||||
let response = match assign_cmd {
|
||||
super::super::super::assign::AssignCommand::Assign {
|
||||
story_number,
|
||||
model,
|
||||
} => {
|
||||
slog!(
|
||||
"[matrix-bot] Handling assign command from {sender}: story {story_number} model={model}"
|
||||
);
|
||||
super::super::super::assign::handle_assign(
|
||||
let response = super::super::super::assign::handle_assign(
|
||||
&ctx.services.bot_name,
|
||||
&story_number,
|
||||
&model,
|
||||
&effective_root,
|
||||
&ctx.services.agents,
|
||||
)
|
||||
.await
|
||||
}
|
||||
super::super::super::assign::AssignCommand::BadArgs => {
|
||||
format!(
|
||||
"Usage: `{} assign <number> <model>` (e.g. `assign 42 opus`)",
|
||||
ctx.services.bot_name
|
||||
)
|
||||
}
|
||||
};
|
||||
.await;
|
||||
let html = markdown_to_html(&response);
|
||||
if let Ok(msg_id) = ctx
|
||||
.transport
|
||||
@@ -346,26 +337,22 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
|
||||
|
||||
// Check for the delete command, which requires async agent/worktree ops
|
||||
// and cannot be handled by the sync command registry.
|
||||
if let Some(del_cmd) = super::super::super::delete::extract_delete_command(
|
||||
// Only handle the well-formed variant; BadArgs falls through to the LLM.
|
||||
if let Some(super::super::super::delete::DeleteCommand::Delete { story_number }) =
|
||||
super::super::super::delete::extract_delete_command(
|
||||
&user_message,
|
||||
&ctx.services.bot_name,
|
||||
ctx.matrix_user_id.as_str(),
|
||||
) {
|
||||
let response = match del_cmd {
|
||||
super::super::super::delete::DeleteCommand::Delete { story_number } => {
|
||||
)
|
||||
{
|
||||
slog!("[matrix-bot] Handling delete command from {sender}: story {story_number}");
|
||||
super::super::super::delete::handle_delete(
|
||||
let response = super::super::super::delete::handle_delete(
|
||||
&ctx.services.bot_name,
|
||||
&story_number,
|
||||
&effective_root,
|
||||
&ctx.services.agents,
|
||||
)
|
||||
.await
|
||||
}
|
||||
super::super::super::delete::DeleteCommand::BadArgs => {
|
||||
format!("Usage: `{} delete <number>`", ctx.services.bot_name)
|
||||
}
|
||||
};
|
||||
.await;
|
||||
let html = markdown_to_html(&response);
|
||||
if let Ok(msg_id) = ctx
|
||||
.transport
|
||||
@@ -380,26 +367,22 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
|
||||
|
||||
// Check for the rmtree command, which requires async agent/worktree ops
|
||||
// and cannot be handled by the sync command registry.
|
||||
if let Some(rmtree_cmd) = super::super::super::rmtree::extract_rmtree_command(
|
||||
// Only handle the well-formed variant; BadArgs falls through to the LLM.
|
||||
if let Some(super::super::super::rmtree::RmtreeCommand::Rmtree { story_number }) =
|
||||
super::super::super::rmtree::extract_rmtree_command(
|
||||
&user_message,
|
||||
&ctx.services.bot_name,
|
||||
ctx.matrix_user_id.as_str(),
|
||||
) {
|
||||
let response = match rmtree_cmd {
|
||||
super::super::super::rmtree::RmtreeCommand::Rmtree { story_number } => {
|
||||
)
|
||||
{
|
||||
slog!("[matrix-bot] Handling rmtree command from {sender}: story {story_number}");
|
||||
super::super::super::rmtree::handle_rmtree(
|
||||
let response = super::super::super::rmtree::handle_rmtree(
|
||||
&ctx.services.bot_name,
|
||||
&story_number,
|
||||
&effective_root,
|
||||
&ctx.services.agents,
|
||||
)
|
||||
.await
|
||||
}
|
||||
super::super::super::rmtree::RmtreeCommand::BadArgs => {
|
||||
format!("Usage: `{} rmtree <number>`", ctx.services.bot_name)
|
||||
}
|
||||
};
|
||||
.await;
|
||||
let html = markdown_to_html(&response);
|
||||
if let Ok(msg_id) = ctx
|
||||
.transport
|
||||
@@ -414,35 +397,26 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
|
||||
|
||||
// Check for the start command, which requires async agent ops and cannot
|
||||
// be handled by the sync command registry.
|
||||
if let Some(start_cmd) = super::super::super::start::extract_start_command(
|
||||
// Only handle the well-formed variant; BadArgs falls through to the LLM.
|
||||
if let Some(super::super::super::start::StartCommand::Start {
|
||||
story_number,
|
||||
agent_hint,
|
||||
}) = super::super::super::start::extract_start_command(
|
||||
&user_message,
|
||||
&ctx.services.bot_name,
|
||||
ctx.matrix_user_id.as_str(),
|
||||
) {
|
||||
let response = match start_cmd {
|
||||
super::super::super::start::StartCommand::Start {
|
||||
story_number,
|
||||
agent_hint,
|
||||
} => {
|
||||
slog!(
|
||||
"[matrix-bot] Handling start command from {sender}: story {story_number} agent={agent_hint:?}"
|
||||
);
|
||||
super::super::super::start::handle_start(
|
||||
let response = super::super::super::start::handle_start(
|
||||
&ctx.services.bot_name,
|
||||
&story_number,
|
||||
agent_hint.as_deref(),
|
||||
&effective_root,
|
||||
&ctx.services.agents,
|
||||
)
|
||||
.await
|
||||
}
|
||||
super::super::super::start::StartCommand::BadArgs => {
|
||||
format!(
|
||||
"Usage: `{} start <number>` or `{} start <number> opus`",
|
||||
ctx.services.bot_name, ctx.services.bot_name
|
||||
)
|
||||
}
|
||||
};
|
||||
.await;
|
||||
let html = markdown_to_html(&response);
|
||||
if let Ok(msg_id) = ctx
|
||||
.transport
|
||||
|
||||
Reference in New Issue
Block a user