fix: add --all to cargo fmt in script/test and autoformat codebase

cargo fmt without --all fails with "Failed to find targets" in
workspace repos. This was blocking every story's gates. Also ran
cargo fmt --all to fix all existing formatting issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dave
2026-04-13 14:07:08 +00:00
parent ed2526ce41
commit 845b85e7a7
128 changed files with 3566 additions and 2395 deletions
+13 -3
View File
@@ -69,7 +69,10 @@ mod tests {
// "timmy ambient on" — bot name mentioned but not @-prefixed, so
// is_addressed is false; strip_bot_mention still strips "timmy ".
let result = try_handle_command(&dispatch, "timmy ambient on");
assert!(result.is_some(), "ambient on should fire even when is_addressed=false");
assert!(
result.is_some(),
"ambient on should fire even when is_addressed=false"
);
assert!(
ambient_rooms.lock().unwrap().contains(&room_id),
"room should be in ambient_rooms after ambient on"
@@ -92,7 +95,10 @@ mod tests {
};
// Bare "ambient off" in an ambient room (is_addressed=false).
let result = try_handle_command(&dispatch, "ambient off");
assert!(result.is_some(), "bare ambient off should be handled without LLM");
assert!(
result.is_some(),
"bare ambient off should be handled without LLM"
);
let output = result.unwrap();
assert!(
output.contains("Ambient mode off"),
@@ -161,7 +167,11 @@ mod tests {
#[test]
fn ambient_invalid_args_returns_usage() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy ambient");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy ambient",
);
let output = result.unwrap();
assert!(
output.contains("Usage"),
+45 -14
View File
@@ -1,8 +1,8 @@
//! Handler for the `backlog` command — shows only Stage::Backlog items.
use crate::pipeline_state::{PipelineItem, Stage};
use super::CommandContext;
use super::status::{story_short_label, unmet_deps_from_items};
use crate::pipeline_state::{PipelineItem, Stage};
pub(super) fn handle_backlog(_ctx: &CommandContext) -> Option<String> {
Some(build_backlog_output())
@@ -94,16 +94,29 @@ mod tests {
make_item("30_story_in_qa", "In QA", Stage::Qa),
];
let output = build_backlog_from_items(&items);
assert!(output.contains("In Backlog"), "should show backlog item: {output}");
assert!(!output.contains("In Progress"), "should not show coding items: {output}");
assert!(!output.contains("In QA"), "should not show QA items: {output}");
assert!(
output.contains("In Backlog"),
"should show backlog item: {output}"
);
assert!(
!output.contains("In Progress"),
"should not show coding items: {output}"
);
assert!(
!output.contains("In QA"),
"should not show QA items: {output}"
);
}
// -- AC: shows number, type, name -----------------------------------------
#[test]
fn backlog_shows_number_type_and_name() {
let items = vec![make_item("42_story_my_feature", "My Feature", Stage::Backlog)];
let items = vec![make_item(
"42_story_my_feature",
"My Feature",
Stage::Backlog,
)];
let output = build_backlog_from_items(&items);
assert!(
output.contains("42 [story] — My Feature"),
@@ -116,7 +129,12 @@ mod tests {
#[test]
fn backlog_shows_waiting_on_for_unmet_deps() {
let items = vec![
make_item_with_deps("10_story_waiting", "Waiting Story", Stage::Backlog, vec![999]),
make_item_with_deps(
"10_story_waiting",
"Waiting Story",
Stage::Backlog,
vec![999],
),
make_item("999_story_dep", "Dep Story", Stage::Backlog),
];
let output = build_backlog_from_items(&items);
@@ -150,16 +168,17 @@ mod tests {
fn backlog_no_waiting_on_when_no_deps() {
let items = vec![make_item("5_story_nodeps", "No Deps", Stage::Backlog)];
let output = build_backlog_from_items(&items);
assert!(!output.contains("waiting on"), "no dep suffix when no deps: {output}");
assert!(
!output.contains("waiting on"),
"no dep suffix when no deps: {output}"
);
}
// -- AC: command is registered in the registry ----------------------------
#[test]
fn backlog_command_in_registry() {
let found = super::super::commands()
.iter()
.any(|c| c.name == "backlog");
let found = super::super::commands().iter().any(|c| c.name == "backlog");
assert!(found, "backlog must be registered in commands()");
}
@@ -171,7 +190,10 @@ mod tests {
"@timmy help",
);
let output = result.unwrap_or_default();
assert!(output.contains("backlog"), "backlog should appear in help output: {output}");
assert!(
output.contains("backlog"),
"backlog should appear in help output: {output}"
);
}
#[test]
@@ -181,7 +203,10 @@ mod tests {
"@timmy:homeserver.local",
"@timmy backlog",
);
assert!(result.is_some(), "backlog command should match and return Some");
assert!(
result.is_some(),
"backlog command should match and return Some"
);
}
#[test]
@@ -192,7 +217,10 @@ mod tests {
"@timmy backlog",
);
let output = result.unwrap_or_default();
assert!(output.contains("Backlog"), "backlog output should contain Backlog header: {output}");
assert!(
output.contains("Backlog"),
"backlog output should contain Backlog header: {output}"
);
}
// -- empty backlog --------------------------------------------------------
@@ -201,6 +229,9 @@ mod tests {
fn backlog_shows_none_when_empty() {
let items = vec![make_item("1_story_done", "Done", Stage::Coding)];
let output = build_backlog_from_items(&items);
assert!(output.contains("*(none)*"), "should show none when no backlog items: {output}");
assert!(
output.contains("*(none)*"),
"should show none when no backlog items: {output}"
);
}
}
+112 -40
View File
@@ -2,8 +2,8 @@
use std::collections::HashMap;
use super::status::story_short_label;
use super::CommandContext;
use super::status::story_short_label;
/// Show token spend: 24h total, top 5 stories, agent-type breakdown, and
/// all-time total.
@@ -102,7 +102,10 @@ mod tests {
use crate::agents::AgentPool;
use std::sync::Arc;
fn write_token_records(root: &std::path::Path, records: &[crate::agents::token_usage::TokenUsageRecord]) {
fn write_token_records(
root: &std::path::Path,
records: &[crate::agents::token_usage::TokenUsageRecord],
) {
for r in records {
crate::agents::token_usage::append_record(root, r).unwrap();
}
@@ -118,7 +121,12 @@ mod tests {
}
}
fn make_record(story_id: &str, agent_name: &str, cost: f64, hours_ago: i64) -> crate::agents::token_usage::TokenUsageRecord {
fn make_record(
story_id: &str,
agent_name: &str,
cost: f64,
hours_ago: i64,
) -> crate::agents::token_usage::TokenUsageRecord {
let ts = (chrono::Utc::now() - chrono::Duration::hours(hours_ago)).to_rfc3339();
crate::agents::token_usage::TokenUsageRecord {
story_id: story_id.to_string(),
@@ -157,55 +165,89 @@ mod tests {
#[test]
fn cost_command_appears_in_help() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy help",
);
let output = result.unwrap();
assert!(output.contains("cost"), "help should list cost command: {output}");
assert!(
output.contains("cost"),
"help should list cost command: {output}"
);
}
#[test]
fn cost_command_no_records() {
let tmp = tempfile::TempDir::new().unwrap();
let output = cost_cmd_with_root(tmp.path()).unwrap();
assert!(output.contains("No usage records found"), "should show empty message: {output}");
assert!(
output.contains("No usage records found"),
"should show empty message: {output}"
);
}
#[test]
fn cost_command_shows_24h_total() {
let tmp = tempfile::TempDir::new().unwrap();
write_token_records(tmp.path(), &[
make_record("42_story_foo", "coder-1", 1.50, 2),
make_record("42_story_foo", "coder-1", 0.50, 5),
]);
write_token_records(
tmp.path(),
&[
make_record("42_story_foo", "coder-1", 1.50, 2),
make_record("42_story_foo", "coder-1", 0.50, 5),
],
);
let output = cost_cmd_with_root(tmp.path()).unwrap();
assert!(output.contains("**Last 24h:** $2.00"), "should show 24h total: {output}");
assert!(
output.contains("**Last 24h:** $2.00"),
"should show 24h total: {output}"
);
}
#[test]
fn cost_command_excludes_old_from_24h() {
let tmp = tempfile::TempDir::new().unwrap();
write_token_records(tmp.path(), &[
make_record("42_story_foo", "coder-1", 1.00, 2), // within 24h
make_record("43_story_bar", "coder-1", 5.00, 48), // older
]);
write_token_records(
tmp.path(),
&[
make_record("42_story_foo", "coder-1", 1.00, 2), // within 24h
make_record("43_story_bar", "coder-1", 5.00, 48), // older
],
);
let output = cost_cmd_with_root(tmp.path()).unwrap();
assert!(output.contains("**Last 24h:** $1.00"), "should only count recent: {output}");
assert!(output.contains("**All-time:** $6.00"), "all-time should include everything: {output}");
assert!(
output.contains("**Last 24h:** $1.00"),
"should only count recent: {output}"
);
assert!(
output.contains("**All-time:** $6.00"),
"all-time should include everything: {output}"
);
}
#[test]
fn cost_command_shows_top_stories() {
let tmp = tempfile::TempDir::new().unwrap();
write_token_records(tmp.path(), &[
make_record("42_story_foo", "coder-1", 3.00, 1),
make_record("43_story_bar", "coder-1", 1.00, 1),
make_record("42_story_foo", "qa-1", 2.00, 1),
]);
write_token_records(
tmp.path(),
&[
make_record("42_story_foo", "coder-1", 3.00, 1),
make_record("43_story_bar", "coder-1", 1.00, 1),
make_record("42_story_foo", "qa-1", 2.00, 1),
],
);
let output = cost_cmd_with_root(tmp.path()).unwrap();
assert!(output.contains("Top Stories"), "should have top stories section: {output}");
assert!(
output.contains("Top Stories"),
"should have top stories section: {output}"
);
// Story 42 ($5.00) should appear before story 43 ($1.00)
let pos_42 = output.find("42").unwrap();
let pos_43 = output.find("43").unwrap();
assert!(pos_42 < pos_43, "story 42 should appear before 43 (sorted by cost): {output}");
assert!(
pos_42 < pos_43,
"story 42 should appear before 43 (sorted by cost): {output}"
);
}
#[test]
@@ -213,45 +255,75 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let mut records = Vec::new();
for i in 1..=7 {
records.push(make_record(&format!("{i}_story_s{i}"), "coder-1", i as f64, 1));
records.push(make_record(
&format!("{i}_story_s{i}"),
"coder-1",
i as f64,
1,
));
}
write_token_records(tmp.path(), &records);
let output = cost_cmd_with_root(tmp.path()).unwrap();
// The top 5 most expensive are stories 7,6,5,4,3. Stories 1 and 2 should be excluded.
let top_section = output.split("**By Agent Type").next().unwrap();
assert!(!top_section.contains("• 1 —"), "story 1 should not be in top 5: {output}");
assert!(!top_section.contains("2"), "story 2 should not be in top 5: {output}");
assert!(
!top_section.contains("1"),
"story 1 should not be in top 5: {output}"
);
assert!(
!top_section.contains("• 2 —"),
"story 2 should not be in top 5: {output}"
);
}
#[test]
fn cost_command_shows_agent_type_breakdown() {
let tmp = tempfile::TempDir::new().unwrap();
write_token_records(tmp.path(), &[
make_record("42_story_foo", "coder-1", 2.00, 1),
make_record("42_story_foo", "qa-1", 1.50, 1),
make_record("42_story_foo", "mergemaster", 0.50, 1),
]);
write_token_records(
tmp.path(),
&[
make_record("42_story_foo", "coder-1", 2.00, 1),
make_record("42_story_foo", "qa-1", 1.50, 1),
make_record("42_story_foo", "mergemaster", 0.50, 1),
],
);
let output = cost_cmd_with_root(tmp.path()).unwrap();
assert!(output.contains("By Agent Type"), "should have agent type section: {output}");
assert!(
output.contains("By Agent Type"),
"should have agent type section: {output}"
);
assert!(output.contains("coder"), "should show coder type: {output}");
assert!(output.contains("qa"), "should show qa type: {output}");
assert!(output.contains("mergemaster"), "should show mergemaster type: {output}");
assert!(
output.contains("mergemaster"),
"should show mergemaster type: {output}"
);
}
#[test]
fn cost_command_shows_all_time_total() {
let tmp = tempfile::TempDir::new().unwrap();
write_token_records(tmp.path(), &[
make_record("42_story_foo", "coder-1", 1.00, 2),
make_record("43_story_bar", "coder-1", 9.00, 100),
]);
write_token_records(
tmp.path(),
&[
make_record("42_story_foo", "coder-1", 1.00, 2),
make_record("43_story_bar", "coder-1", 9.00, 100),
],
);
let output = cost_cmd_with_root(tmp.path()).unwrap();
assert!(output.contains("**All-time:** $10.00"), "should show all-time total: {output}");
assert!(
output.contains("**All-time:** $10.00"),
"should show all-time total: {output}"
);
}
#[test]
fn cost_command_case_insensitive() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy COST");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy COST",
);
assert!(result.is_some(), "COST should match case-insensitively");
}
+72 -18
View File
@@ -59,12 +59,16 @@ fn read_cached_coverage(project_root: &std::path::Path) -> String {
fn read_coverage_report(path: &std::path::Path) -> String {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return format!("**Coverage (cached)**\n\nError reading `.coverage_report.json`: {e}"),
Err(e) => {
return format!("**Coverage (cached)**\n\nError reading `.coverage_report.json`: {e}");
}
};
let report: CoverageReport = match serde_json::from_str(&content) {
Ok(r) => r,
Err(e) => return format!("**Coverage (cached)**\n\nFailed to parse `.coverage_report.json`: {e}"),
Err(e) => {
return format!("**Coverage (cached)**\n\nFailed to parse `.coverage_report.json`: {e}");
}
};
format_coverage_report(&report)
@@ -81,13 +85,22 @@ fn format_coverage_report(report: &CoverageReport) -> String {
// Top 5 lowest-covered files (already sorted ascending in the JSON, but sort
// defensively here so the display is correct even if the file was hand-edited).
let mut sorted: Vec<&FileCoverage> = report.files.iter().collect();
sorted.sort_by(|a, b| a.coverage.partial_cmp(&b.coverage).unwrap_or(std::cmp::Ordering::Equal));
sorted.sort_by(|a, b| {
a.coverage
.partial_cmp(&b.coverage)
.unwrap_or(std::cmp::Ordering::Equal)
});
let targets: Vec<&FileCoverage> = sorted.into_iter().take(5).collect();
if !targets.is_empty() {
out.push_str("\n**Top 5 files needing coverage:**\n");
for (i, file) in targets.iter().enumerate() {
out.push_str(&format!("{}. {}{:.1}%\n", i + 1, file.path, file.coverage));
out.push_str(&format!(
"{}. {}{:.1}%\n",
i + 1,
file.path,
file.coverage
));
}
}
@@ -162,8 +175,13 @@ fn run_coverage(project_root: &std::path::Path) -> String {
// Replace the "cached" label with "fresh".
result = result.replacen("Coverage (cached)", "Coverage (fresh)", 1);
// Replace the cached hint with a pass/fail indicator.
let pass_indicator = if out.status.success() { "PASS" } else { "FAIL: coverage below threshold" };
result = result.replacen("*Run `coverage run` for fresh results.*", pass_indicator, 1);
let pass_indicator = if out.status.success() {
"PASS"
} else {
"FAIL: coverage below threshold"
};
result =
result.replacen("*Run `coverage run` for fresh results.*", pass_indicator, 1);
return result;
}
@@ -322,9 +340,18 @@ mod tests {
let output = handle_coverage(&ctx).unwrap();
assert!(output.contains("72.5"), "should include overall: {output}");
assert!(output.contains("60.0"), "should include threshold: {output}");
assert!(output.contains("15.0"), "should include lowest-covered file pct: {output}");
assert!(output.contains("server/src/low.rs"), "should include lowest-covered file path: {output}");
assert!(
output.contains("60.0"),
"should include threshold: {output}"
);
assert!(
output.contains("15.0"),
"should include lowest-covered file pct: {output}"
);
assert!(
output.contains("server/src/low.rs"),
"should include lowest-covered file path: {output}"
);
}
#[test]
@@ -348,9 +375,18 @@ mod tests {
let output = handle_coverage(&ctx).unwrap();
assert!(output.contains("a.rs"), "should show lowest file: {output}");
assert!(output.contains("e.rs"), "should show 5th lowest file: {output}");
assert!(!output.contains("f.rs"), "should not show 6th file: {output}");
assert!(!output.contains("g.rs"), "should not show 7th file: {output}");
assert!(
output.contains("e.rs"),
"should show 5th lowest file: {output}"
);
assert!(
!output.contains("f.rs"),
"should not show 6th file: {output}"
);
assert!(
!output.contains("g.rs"),
"should not show 7th file: {output}"
);
}
#[test]
@@ -466,15 +502,24 @@ mod tests {
overall: 66.25,
threshold: 60.0,
files: vec![
FileCoverage { path: "a.rs".to_string(), coverage: 10.0 },
FileCoverage { path: "b.rs".to_string(), coverage: 80.0 },
FileCoverage {
path: "a.rs".to_string(),
coverage: 10.0,
},
FileCoverage {
path: "b.rs".to_string(),
coverage: 80.0,
},
],
};
let result = format_coverage_report(&report);
assert!(result.contains("66.2"), "should show overall: {result}");
assert!(result.contains("60.0"), "should show threshold: {result}");
assert!(result.contains("a.rs"), "should show lowest file: {result}");
assert!(result.contains("10.0"), "should show lowest file pct: {result}");
assert!(
result.contains("10.0"),
"should show lowest file pct: {result}"
);
}
#[test]
@@ -490,9 +535,18 @@ Frontend line coverage: 70.0%\n\
PASS: Coverage 66.25% meets threshold 60.00%\n\
";
let result = parse_coverage_output(sample, true);
assert!(result.contains("62.5"), "should include Rust coverage: {result}");
assert!(result.contains("70.0"), "should include Frontend coverage: {result}");
assert!(result.contains("66.25"), "should include Overall coverage: {result}");
assert!(
result.contains("62.5"),
"should include Rust coverage: {result}"
);
assert!(
result.contains("70.0"),
"should include Frontend coverage: {result}"
);
assert!(
result.contains("66.25"),
"should include Overall coverage: {result}"
);
assert!(result.contains("PASS"), "should indicate PASS: {result}");
}
+14 -10
View File
@@ -128,14 +128,20 @@ mod tests {
"@timmy help",
);
let output = result.unwrap();
assert!(output.contains("depends"), "help should list depends command: {output}");
assert!(
output.contains("depends"),
"help should list depends command: {output}"
);
}
#[test]
fn depends_no_args_returns_usage() {
let tmp = tempfile::TempDir::new().unwrap();
let output = depends_cmd_with_root(tmp.path(), "").unwrap();
assert!(output.contains("Usage"), "no args should show usage: {output}");
assert!(
output.contains("Usage"),
"no args should show usage: {output}"
);
}
#[test]
@@ -188,10 +194,9 @@ mod tests {
output.contains("477") && output.contains("478"),
"response should mention dep numbers: {output}"
);
let contents = std::fs::read_to_string(
tmp.path().join(".huskies/work/1_backlog/42_story_foo.md"),
)
.unwrap();
let contents =
std::fs::read_to_string(tmp.path().join(".huskies/work/1_backlog/42_story_foo.md"))
.unwrap();
assert!(
contents.contains("depends_on: [477, 478]"),
"file should have depends_on set: {contents}"
@@ -212,10 +217,9 @@ mod tests {
output.contains("Cleared"),
"should confirm clearing deps: {output}"
);
let contents = std::fs::read_to_string(
tmp.path().join(".huskies/work/2_current/10_story_bar.md"),
)
.unwrap();
let contents =
std::fs::read_to_string(tmp.path().join(".huskies/work/2_current/10_story_bar.md"))
.unwrap();
assert!(
!contents.contains("depends_on"),
"file should have depends_on cleared: {contents}"
+14 -3
View File
@@ -100,9 +100,16 @@ mod tests {
#[test]
fn git_command_appears_in_help() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy help",
);
let output = result.unwrap();
assert!(output.contains("git"), "help should list git command: {output}");
assert!(
output.contains("git"),
"help should list git command: {output}"
);
}
#[test]
@@ -197,7 +204,11 @@ mod tests {
#[test]
fn git_command_case_insensitive() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy GIT");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy GIT",
);
assert!(result.is_some(), "GIT should match case-insensitively");
}
}
+21 -7
View File
@@ -1,6 +1,6 @@
//! Handler for the `help` command.
use super::{commands, CommandContext};
use super::{CommandContext, commands};
pub(super) fn handle_help(ctx: &CommandContext) -> Option<String> {
let mut output = format!("**{} Commands**\n\n", ctx.bot_name);
@@ -14,7 +14,7 @@ pub(super) fn handle_help(ctx: &CommandContext) -> Option<String> {
#[cfg(test)]
mod tests {
use super::super::tests::{try_cmd_addressed, commands};
use super::super::tests::{commands, try_cmd_addressed};
#[test]
fn help_command_matches() {
@@ -74,7 +74,10 @@ mod tests {
fn help_output_includes_status() {
let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help");
let output = result.unwrap();
assert!(output.contains("status"), "help should list status command: {output}");
assert!(
output.contains("status"),
"help should list status command: {output}"
);
}
#[test]
@@ -86,7 +89,9 @@ mod tests {
.iter()
.map(|c| {
let marker = format!("**{}**", c.name);
let pos = output.find(&marker).expect("command must appear in help as **name**");
let pos = output
.find(&marker)
.expect("command must appear in help as **name**");
(pos, c.name)
})
.collect();
@@ -94,20 +99,29 @@ mod tests {
let names_in_order: Vec<&str> = positions.iter().map(|(_, n)| *n).collect();
let mut sorted = names_in_order.clone();
sorted.sort();
assert_eq!(names_in_order, sorted, "commands must appear in alphabetical order");
assert_eq!(
names_in_order, sorted,
"commands must appear in alphabetical order"
);
}
#[test]
fn help_output_includes_ambient() {
let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help");
let output = result.unwrap();
assert!(output.contains("ambient"), "help should list ambient command: {output}");
assert!(
output.contains("ambient"),
"help should list ambient command: {output}"
);
}
#[test]
fn help_output_includes_htop() {
let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help");
let output = result.unwrap();
assert!(output.contains("htop"), "help should list htop command: {output}");
assert!(
output.contains("htop"),
"help should list htop command: {output}"
);
}
}
+63 -9
View File
@@ -152,11 +152,53 @@ fn loc_top_n(project_root: &std::path::Path, top_n: usize) -> String {
fn is_source_extension(ext: &str) -> bool {
matches!(
ext,
"rs" | "ts" | "tsx" | "js" | "jsx" | "py" | "go" | "java" | "c" | "cpp" | "h"
| "hpp" | "cs" | "rb" | "swift" | "kt" | "scala" | "hs" | "ml" | "ex" | "exs"
| "clj" | "lua" | "sh" | "bash" | "zsh" | "fish" | "ps1" | "toml" | "yaml"
| "yml" | "json" | "md" | "html" | "css" | "scss" | "less" | "sql" | "graphql"
| "proto" | "tf" | "hcl" | "nix" | "r" | "jl" | "dart" | "vue" | "svelte"
"rs" | "ts"
| "tsx"
| "js"
| "jsx"
| "py"
| "go"
| "java"
| "c"
| "cpp"
| "h"
| "hpp"
| "cs"
| "rb"
| "swift"
| "kt"
| "scala"
| "hs"
| "ml"
| "ex"
| "exs"
| "clj"
| "lua"
| "sh"
| "bash"
| "zsh"
| "fish"
| "ps1"
| "toml"
| "yaml"
| "yml"
| "json"
| "md"
| "html"
| "css"
| "scss"
| "less"
| "sql"
| "graphql"
| "proto"
| "tf"
| "hcl"
| "nix"
| "r"
| "jl"
| "dart"
| "vue"
| "svelte"
)
}
@@ -202,7 +244,10 @@ mod tests {
"@timmy help",
);
let output = result.unwrap();
assert!(output.contains("loc"), "help should list loc command: {output}");
assert!(
output.contains("loc"),
"help should list loc command: {output}"
);
}
#[test]
@@ -220,7 +265,10 @@ mod tests {
);
// At most 10 entries (numbered lines "1." through "10.")
let count = output.lines().filter(|l| l.contains(". `")).count();
assert!(count <= 10, "default should return at most 10 files, got {count}");
assert!(
count <= 10,
"default should return at most 10 files, got {count}"
);
}
#[test]
@@ -233,7 +281,10 @@ mod tests {
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "5");
let output = handle_loc(&ctx).unwrap();
let count = output.lines().filter(|l| l.contains(". `")).count();
assert!(count <= 5, "loc 5 should return at most 5 files, got {count}");
assert!(
count <= 5,
"loc 5 should return at most 5 files, got {count}"
);
}
#[test]
@@ -246,7 +297,10 @@ mod tests {
let ctx = make_ctx(&agents, &ambient_rooms, repo_root, "20");
let output = handle_loc(&ctx).unwrap();
let count = output.lines().filter(|l| l.contains(". `")).count();
assert!(count <= 20, "loc 20 should return at most 20 files, got {count}");
assert!(
count <= 20,
"loc 20 should return at most 20 files, got {count}"
);
}
#[test]
+9 -2
View File
@@ -110,7 +110,9 @@ fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
// Try content store first.
for id in crate::db::all_content_ids() {
let file_num = id.split('_').next().unwrap_or("");
if file_num == num_str && let Some(c) = crate::db::read_content(&id) {
if file_num == num_str
&& let Some(c) = crate::db::read_content(&id)
{
return crate::io::story_metadata::parse_front_matter(&c)
.ok()
.and_then(|m| m.name);
@@ -119,7 +121,12 @@ fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
// Fallback: filesystem scan.
let stages = [
"1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived",
"1_backlog",
"2_current",
"3_qa",
"4_merge",
"5_done",
"6_archived",
];
for stage in &stages {
let dir = root.join(".huskies").join("work").join(stage);
+27 -13
View File
@@ -86,9 +86,7 @@ pub(super) fn handle_test(ctx: &CommandContext) -> Option<String> {
let mut result = format!("**Test: {status}**\n\n");
if tests_passed > 0 || tests_failed > 0 {
result.push_str(&format!(
"{tests_passed} passed, {tests_failed} failed\n\n"
));
result.push_str(&format!("{tests_passed} passed, {tests_failed} failed\n\n"));
}
result.push_str(&format!("```\n{truncated}\n```"));
@@ -128,7 +126,11 @@ fn parse_test_counts(output: &str) -> (u64, u64) {
fn extract_count(line: &str, label: &str) -> Option<u64> {
let pos = line.find(label)?;
let before = line[..pos].trim_end();
let num_str: String = before.chars().rev().take_while(|c| c.is_ascii_digit()).collect();
let num_str: String = before
.chars()
.rev()
.take_while(|c| c.is_ascii_digit())
.collect();
if num_str.is_empty() {
return None;
}
@@ -250,10 +252,7 @@ mod tests {
#[test]
fn test_command_works_via_dispatch() {
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 ambient = test_ambient();
let room_id = "!test:example.com".to_string();
@@ -317,8 +316,14 @@ mod tests {
let ambient = test_ambient();
let ctx = make_ctx(&agents, &ambient, dir.path(), "");
let output = handle_test(&ctx).unwrap();
assert!(output.contains("PASS"), "no-arg should use project root: {output}");
assert!(output.contains('7'), "should show count from project root script: {output}");
assert!(
output.contains("PASS"),
"no-arg should use project root: {output}"
);
assert!(
output.contains('7'),
"should show count from project root script: {output}"
);
}
#[test]
@@ -329,8 +334,14 @@ mod tests {
let ambient = test_ambient();
let ctx = make_ctx(&agents, &ambient, dir.path(), "541");
let output = handle_test(&ctx).unwrap();
assert!(output.contains("PASS"), "should run tests in worktree: {output}");
assert!(output.contains('2'), "should show count from worktree script: {output}");
assert!(
output.contains("PASS"),
"should run tests in worktree: {output}"
);
assert!(
output.contains('2'),
"should show count from worktree script: {output}"
);
}
#[test]
@@ -382,6 +393,9 @@ mod tests {
"run_tests with story number must respond via dispatch"
);
let output = result.unwrap();
assert!(output.contains("PASS"), "should PASS for valid worktree: {output}");
assert!(
output.contains("PASS"),
"should PASS for valid worktree: {output}"
);
}
}
+12 -16
View File
@@ -12,7 +12,7 @@ use super::CommandContext;
use crate::http::mcp::wizard_tools::{
generation_hint, is_script_step, step_output_path, write_if_missing,
};
use crate::io::wizard::{format_wizard_state, StepStatus, WizardState};
use crate::io::wizard::{StepStatus, WizardState, format_wizard_state};
pub(super) fn handle_setup(ctx: &CommandContext) -> Option<String> {
let sub = ctx.args.trim().to_ascii_lowercase();
@@ -84,17 +84,16 @@ fn wizard_confirm_reply(ctx: &CommandContext) -> String {
let content = state.steps[idx].content.clone();
// Write content to disk (only if a file path exists and the file is absent).
let write_msg =
if let (Some(c), Some(ref path)) = (&content, step_output_path(root, step)) {
let executable = is_script_step(step);
match write_if_missing(path, c, executable) {
Ok(true) => format!(" File written: `{}`.", path.display()),
Ok(false) => format!(" File `{}` already exists — skipped.", path.display()),
Err(e) => return format!("Error: {e}"),
}
} else {
String::new()
};
let write_msg = if let (Some(c), Some(ref path)) = (&content, step_output_path(root, step)) {
let executable = is_script_step(step);
match write_if_missing(path, c, executable) {
Ok(true) => format!(" File written: `{}`.", path.display()),
Ok(false) => format!(" File `{}` already exists — skipped.", path.display()),
Err(e) => return format!("Error: {e}"),
}
} else {
String::new()
};
if let Err(e) = state.confirm_step(step) {
return format!("Cannot confirm step: {e}");
@@ -140,10 +139,7 @@ fn wizard_skip_reply(ctx: &CommandContext) -> String {
}
if state.completed {
format!(
"Step '{}' skipped. Setup wizard complete!",
step.label()
)
format!("Step '{}' skipped. Setup wizard complete!", step.label())
} else {
let next = &state.steps[state.current_step_index()];
format!(
+14 -3
View File
@@ -78,9 +78,16 @@ mod tests {
#[test]
fn show_command_appears_in_help() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy help");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy help",
);
let output = result.unwrap();
assert!(output.contains("show"), "help should list show command: {output}");
assert!(
output.contains("show"),
"help should list show command: {output}"
);
}
#[test]
@@ -167,7 +174,11 @@ mod tests {
#[test]
fn show_command_case_insensitive() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy SHOW 1");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy SHOW 1",
);
assert!(result.is_some(), "SHOW should match case-insensitively");
}
}
+70 -28
View File
@@ -119,14 +119,13 @@ fn build_status_from_items(
.collect();
// Read token usage once for all stories to avoid repeated file I/O.
let cost_by_story: HashMap<String, f64> =
crate::agents::token_usage::read_all(project_root)
.unwrap_or_default()
.into_iter()
.fold(HashMap::new(), |mut map, r| {
*map.entry(r.story_id).or_insert(0.0) += r.usage.total_cost_usd;
map
});
let cost_by_story: HashMap<String, f64> = crate::agents::token_usage::read_all(project_root)
.unwrap_or_default()
.into_iter()
.fold(HashMap::new(), |mut map, r| {
*map.entry(r.story_id).or_insert(0.0) += r.usage.total_cost_usd;
map
});
let config = ProjectConfig::load(project_root).ok();
@@ -165,10 +164,8 @@ fn build_status_from_items(
}
// Blocked items: Archived { reason: Blocked } shown with 🔴 indicator.
let mut blocked_items: Vec<&PipelineItem> = items
.iter()
.filter(|i| i.stage.is_blocked())
.collect();
let mut blocked_items: Vec<&PipelineItem> =
items.iter().filter(|i| i.stage.is_blocked()).collect();
blocked_items.sort_by(|a, b| a.story_id.0.cmp(&b.story_id.0));
if !blocked_items.is_empty() {
out.push_str(&format!("**Blocked** ({})\n", blocked_items.len()));
@@ -294,13 +291,21 @@ mod tests {
#[test]
fn status_command_matches() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy status");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy status",
);
assert!(result.is_some(), "status command should match");
}
#[test]
fn status_command_returns_pipeline_text() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy status");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy status",
);
let output = result.unwrap();
assert!(
output.contains("Pipeline Status"),
@@ -310,7 +315,11 @@ mod tests {
#[test]
fn status_command_case_insensitive() {
let result = super::super::tests::try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy STATUS");
let result = super::super::tests::try_cmd_addressed(
"Timmy",
"@timmy:homeserver.local",
"@timmy STATUS",
);
assert!(result.is_some(), "STATUS should match case-insensitively");
}
@@ -318,7 +327,10 @@ mod tests {
#[test]
fn short_label_extracts_number_and_name() {
let label = story_short_label("293_story_register_all_bot_commands", Some("Register all bot commands"));
let label = story_short_label(
"293_story_register_all_bot_commands",
Some("Register all bot commands"),
);
assert_eq!(label, "293 [story] — Register all bot commands");
}
@@ -336,7 +348,10 @@ mod tests {
#[test]
fn short_label_does_not_include_underscore_slug() {
let label = story_short_label("293_story_register_all_bot_commands_in_the_command_registry", Some("Register all bot commands"));
let label = story_short_label(
"293_story_register_all_bot_commands_in_the_command_registry",
Some("Register all bot commands"),
);
assert!(
!label.contains("story_register"),
"label should not contain the slug portion: {label}"
@@ -345,19 +360,28 @@ mod tests {
#[test]
fn short_label_shows_bug_type() {
let label = story_short_label("375_bug_default_project_toml", Some("Default project.toml issue"));
let label = story_short_label(
"375_bug_default_project_toml",
Some("Default project.toml issue"),
);
assert_eq!(label, "375 [bug] — Default project.toml issue");
}
#[test]
fn short_label_shows_spike_type() {
let label = story_short_label("61_spike_filesystem_watcher_architecture", Some("Filesystem watcher architecture"));
let label = story_short_label(
"61_spike_filesystem_watcher_architecture",
Some("Filesystem watcher architecture"),
);
assert_eq!(label, "61 [spike] — Filesystem watcher architecture");
}
#[test]
fn short_label_shows_refactor_type() {
let label = story_short_label("260_refactor_upgrade_libsqlite3_sys", Some("Upgrade libsqlite3-sys"));
let label = story_short_label(
"260_refactor_upgrade_libsqlite3_sys",
Some("Upgrade libsqlite3-sys"),
);
assert_eq!(label, "260 [refactor] — Upgrade libsqlite3-sys");
}
@@ -506,7 +530,12 @@ mod tests {
// Story 10 depends on story 999, which is NOT in all_items (treated as met)
// OR present in backlog (unmet). Let's add dep 999 in Backlog stage (unmet).
let items = vec![
make_item_with_deps("10_story_waiting", "Waiting Story", Stage::Coding, vec![999]),
make_item_with_deps(
"10_story_waiting",
"Waiting Story",
Stage::Coding,
vec![999],
),
make_item("999_story_dep", "Dep Story", Stage::Backlog),
];
@@ -526,11 +555,20 @@ mod tests {
// Dep 999 is in Done stage — met.
let items = vec![
make_item_with_deps("10_story_unblocked", "Unblocked Story", Stage::Coding, vec![999]),
make_item("999_story_dep", "Dep Story", Stage::Done {
merged_at: Utc::now(),
merge_commit: crate::pipeline_state::GitSha("abc123".to_string()),
}),
make_item_with_deps(
"10_story_unblocked",
"Unblocked Story",
Stage::Coding,
vec![999],
),
make_item(
"999_story_dep",
"Dep Story",
Stage::Done {
merged_at: Utc::now(),
merge_commit: crate::pipeline_state::GitSha("abc123".to_string()),
},
),
];
let agents = AgentPool::new_test(3000);
@@ -678,8 +716,12 @@ mod tests {
// Must appear under Done, not Backlog.
let done_pos = output.find("**Done**").expect("Done section must exist");
let backlog_pos = output.find("**Backlog**").expect("Backlog section must exist");
let story_pos = output.find("503 [story]").expect("story must appear in output");
let backlog_pos = output
.find("**Backlog**")
.expect("Backlog section must exist");
let story_pos = output
.find("503 [story]")
.expect("story must appear in output");
assert!(
story_pos > done_pos,
+31 -25
View File
@@ -33,17 +33,13 @@ pub(super) fn handle_triage(ctx: &CommandContext) -> Option<String> {
match find_story_by_number(num_str) {
Some((story_id, item)) => Some(build_triage_dump(ctx, &story_id, &item, num_str)),
None => Some(format!(
"Story **{num_str}** not found in the pipeline."
)),
None => Some(format!("Story **{num_str}** not found in the pipeline.")),
}
}
/// Find a pipeline item whose numeric prefix matches `num_str` by querying the
/// CRDT state. Returns `(story_id, PipelineItem)` for the first match.
fn find_story_by_number(
num_str: &str,
) -> Option<(String, crate::pipeline_state::PipelineItem)> {
fn find_story_by_number(num_str: &str) -> Option<(String, crate::pipeline_state::PipelineItem)> {
let items = crate::pipeline_state::read_all_typed();
for item in items {
let file_num = item
@@ -74,7 +70,10 @@ fn build_triage_dump(
};
let meta = crate::io::story_metadata::parse_front_matter(&contents).ok();
let name = meta.as_ref().and_then(|m| m.name.as_deref()).unwrap_or("(unnamed)");
let name = meta
.as_ref()
.and_then(|m| m.name.as_deref())
.unwrap_or("(unnamed)");
let mut out = String::new();
@@ -147,10 +146,7 @@ fn build_triage_dump(
out.push_str(&format!("**Branch:** `{branch}`\n\n"));
// ---- git diff --stat ----
let diff_stat = run_git(
&wt_path,
&["diff", "--stat", "master...HEAD"],
);
let diff_stat = run_git(&wt_path, &["diff", "--stat", "master...HEAD"]);
if !diff_stat.is_empty() {
out.push_str("**Diff stat (vs master):**\n```\n");
out.push_str(&diff_stat);
@@ -162,12 +158,7 @@ fn build_triage_dump(
// ---- Last 5 commits on feature branch ----
let log = run_git(
&wt_path,
&[
"log",
"master..HEAD",
"--pretty=format:%h %s",
"-5",
],
&["log", "master..HEAD", "--pretty=format:%h %s", "-5"],
);
if !log.is_empty() {
out.push_str("**Recent commits (branch only):**\n```\n");
@@ -192,10 +183,15 @@ fn parse_acceptance_criteria(contents: &str) -> Vec<(bool, String)> {
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if let Some(text) = trimmed.strip_prefix("- [x] ").or_else(|| trimmed.strip_prefix("- [X] ")) {
if let Some(text) = trimmed
.strip_prefix("- [x] ")
.or_else(|| trimmed.strip_prefix("- [X] "))
{
Some((true, text.to_string()))
} else {
trimmed.strip_prefix("- [ ] ").map(|text| (false, text.to_string()))
trimmed
.strip_prefix("- [ ] ")
.map(|text| (false, text.to_string()))
}
})
.collect()
@@ -248,7 +244,10 @@ mod tests {
#[test]
fn whatsup_command_is_not_registered() {
let found = super::super::commands().iter().any(|c| c.name == "whatsup");
assert!(!found, "whatsup command must not be in the registry (renamed to status)");
assert!(
!found,
"whatsup command must not be in the registry (renamed to status)"
);
}
#[test]
@@ -340,7 +339,10 @@ mod tests {
"---\nname: Backlog Item\n---\n",
);
let output = status_triage_cmd(tmp.path(), "9901").unwrap();
assert!(output.contains("9901"), "should show story number: {output}");
assert!(
output.contains("9901"),
"should show story number: {output}"
);
assert!(
output.contains("Backlog Item"),
"should show story name: {output}"
@@ -361,7 +363,10 @@ mod tests {
"---\nname: QA Item\n---\n",
);
let output = status_triage_cmd(tmp.path(), "9902").unwrap();
assert!(output.contains("9902"), "should show story number: {output}");
assert!(
output.contains("9902"),
"should show story number: {output}"
);
assert!(
output.contains("QA Item"),
"should show story name: {output}"
@@ -439,7 +444,10 @@ mod tests {
output.contains("depends_on") || output.contains("#477"),
"should show depends_on field: {output}"
);
assert!(output.contains("478"), "should list all dependency numbers: {output}");
assert!(
output.contains("478"),
"should list all dependency numbers: {output}"
);
}
#[test]
@@ -459,7 +467,6 @@ mod tests {
);
}
// -- parse_acceptance_criteria -----------------------------------------
#[test]
@@ -479,5 +486,4 @@ mod tests {
let result = parse_acceptance_criteria(input);
assert!(result.is_empty());
}
}
+29 -17
View File
@@ -5,7 +5,10 @@
//! and returns a confirmation.
use super::CommandContext;
use crate::io::story_metadata::{clear_front_matter_field, clear_front_matter_field_in_content, parse_front_matter, set_front_matter_field};
use crate::io::story_metadata::{
clear_front_matter_field, clear_front_matter_field_in_content, parse_front_matter,
set_front_matter_field,
};
use std::path::Path;
/// Handle the `unblock` command.
@@ -37,9 +40,7 @@ pub(crate) fn unblock_by_number(project_root: &Path, story_number: &str) -> Stri
match crate::chat::lookup::find_story_by_number(project_root, story_number) {
Some(found) => found,
None => {
return format!(
"No story, bug, or spike with number **{story_number}** found."
);
return format!("No story, bug, or spike with number **{story_number}** found.");
}
};
@@ -71,9 +72,7 @@ fn unblock_by_story_id(story_id: &str) -> String {
let has_merge_failure = meta.merge_failure.is_some();
if !has_blocked && !has_merge_failure {
return format!(
"**{story_name}** ({story_id}) is not blocked. Nothing to unblock."
);
return format!("**{story_name}** ({story_id}) is not blocked. Nothing to unblock.");
}
let mut updated = contents;
@@ -94,9 +93,16 @@ fn unblock_by_story_id(story_id: &str) -> String {
crate::db::write_item_with_content(story_id, &stage, &updated);
let mut cleared = Vec::new();
if has_blocked { cleared.push("blocked"); }
if has_merge_failure { cleared.push("merge_failure"); }
format!("Unblocked **{story_name}** ({story_id}). Cleared: {}. Retry count reset to 0.", cleared.join(", "))
if has_blocked {
cleared.push("blocked");
}
if has_merge_failure {
cleared.push("merge_failure");
}
format!(
"Unblocked **{story_name}** ({story_id}). Cleared: {}. Retry count reset to 0.",
cleared.join(", ")
)
}
/// Core unblock logic: reset blocked state for a known story file path.
@@ -121,9 +127,7 @@ pub(crate) fn unblock_by_path(path: &Path, story_id: &str) -> String {
let has_merge_failure = meta.merge_failure.is_some();
if !has_blocked && !has_merge_failure {
return format!(
"**{story_name}** ({story_id}) is not blocked. Nothing to unblock."
);
return format!("**{story_name}** ({story_id}) is not blocked. Nothing to unblock.");
}
// Clear the blocked flag if present.
@@ -147,9 +151,16 @@ pub(crate) fn unblock_by_path(path: &Path, story_id: &str) -> String {
}
let mut cleared = Vec::new();
if has_blocked { cleared.push("blocked"); }
if has_merge_failure { cleared.push("merge_failure"); }
format!("Unblocked **{story_name}** ({story_id}). Cleared: {}. Retry count reset to 0.", cleared.join(", "))
if has_blocked {
cleared.push("blocked");
}
if has_merge_failure {
cleared.push("merge_failure");
}
format!(
"Unblocked **{story_name}** ({story_id}). Cleared: {}. Retry count reset to 0.",
cleared.join(", ")
)
}
// ---------------------------------------------------------------------------
@@ -276,7 +287,8 @@ mod tests {
let contents = crate::db::read_content("9903_story_stuck")
.or_else(|| {
std::fs::read_to_string(
tmp.path().join(".huskies/work/2_current/9903_story_stuck.md"),
tmp.path()
.join(".huskies/work/2_current/9903_story_stuck.md"),
)
.ok()
})
+28 -18
View File
@@ -17,9 +17,7 @@ pub(super) fn handle_unreleased(ctx: &CommandContext) -> Option<String> {
if commits.is_empty() {
let msg = match &tag {
Some(t) => format!(
"No unreleased stories since the last release tag **{t}**."
),
Some(t) => format!("No unreleased stories since the last release tag **{t}**."),
None => "No release tags found and no story merge commits on master.".to_string(),
};
return Some(msg);
@@ -36,9 +34,7 @@ pub(super) fn handle_unreleased(ctx: &CommandContext) -> Option<String> {
if stories.is_empty() {
let msg = match &tag {
Some(t) => format!(
"No unreleased stories since the last release tag **{t}**."
),
Some(t) => format!("No unreleased stories since the last release tag **{t}**."),
None => "No release tags found and no story merge commits on master.".to_string(),
};
return Some(msg);
@@ -50,8 +46,7 @@ pub(super) fn handle_unreleased(ctx: &CommandContext) -> Option<String> {
None => "**Unreleased stories (no prior release tag):**\n\n".to_string(),
};
for (num, slug) in &stories {
let name = find_story_name(root, &num.to_string())
.unwrap_or_else(|| slug_to_name(slug));
let name = find_story_name(root, &num.to_string()).unwrap_or_else(|| slug_to_name(slug));
out.push_str(&format!("- **{num}** — {name}\n"));
}
Some(out)
@@ -79,10 +74,7 @@ fn find_last_release_tag(root: &std::path::Path) -> Option<String> {
/// Return the subjects of all `huskies: merge …` commits reachable from HEAD
/// but not from `since_tag` (or all commits when `since_tag` is `None`).
fn list_merge_commits_since(
root: &std::path::Path,
since_tag: Option<&str>,
) -> Vec<String> {
fn list_merge_commits_since(root: &std::path::Path, since_tag: Option<&str>) -> Vec<String> {
use std::process::Command;
let range = match since_tag {
@@ -153,7 +145,9 @@ fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
// Try content store first.
for id in crate::db::all_content_ids() {
let file_num = id.split('_').next().unwrap_or("");
if file_num == num_str && let Some(c) = crate::db::read_content(&id) {
if file_num == num_str
&& let Some(c) = crate::db::read_content(&id)
{
return crate::io::story_metadata::parse_front_matter(&c)
.ok()
.and_then(|m| m.name);
@@ -162,7 +156,12 @@ fn find_story_name(root: &std::path::Path, num_str: &str) -> Option<String> {
// Fallback: filesystem scan.
const STAGES: &[&str] = &[
"1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived",
"1_backlog",
"2_current",
"3_qa",
"4_merge",
"5_done",
"6_archived",
];
for stage in STAGES {
let dir = root.join(".huskies").join("work").join(stage);
@@ -225,7 +224,9 @@ mod tests {
#[test]
fn unreleased_command_is_registered() {
let found = super::super::commands().iter().any(|c| c.name == "unreleased");
let found = super::super::commands()
.iter()
.any(|c| c.name == "unreleased");
assert!(found, "unreleased command must be in the registry");
}
@@ -249,7 +250,10 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let output = unreleased_cmd_with_root(tmp.path()).unwrap();
// Should return some message (not panic), either about no tags or no commits.
assert!(!output.is_empty(), "should return a non-empty message: {output}");
assert!(
!output.is_empty(),
"should return a non-empty message: {output}"
);
}
#[test]
@@ -261,7 +265,10 @@ mod tests {
let output = unreleased_cmd_with_root(repo_root).unwrap();
// The response should mention "unreleased" or "no unreleased" — just make
// sure it's non-empty and doesn't panic.
assert!(!output.is_empty(), "should return a non-empty message: {output}");
assert!(
!output.is_empty(),
"should return a non-empty message: {output}"
);
}
#[test]
@@ -271,7 +278,10 @@ mod tests {
"@timmy:homeserver.local",
"@timmy UNRELEASED",
);
assert!(result.is_some(), "UNRELEASED should match case-insensitively");
assert!(
result.is_some(),
"UNRELEASED should match case-insensitively"
);
}
// -- parse_story_from_subject ------------------------------------------