diff --git a/server/src/matrix/commands/status.rs b/server/src/matrix/commands/status.rs index 89f535a..e9617ce 100644 --- a/server/src/matrix/commands/status.rs +++ b/server/src/matrix/commands/status.rs @@ -16,23 +16,36 @@ pub(super) fn handle_status(ctx: &CommandContext) -> Option { /// Format a short display label for a work item. /// -/// Extracts the leading numeric ID from the file stem (e.g. `"293"` from -/// `"293_story_register_all_bot_commands"`) and combines it with the human- -/// readable name from the front matter when available. +/// Extracts the leading numeric ID and optional type tag from the file stem +/// (e.g. `"293"` and `"story"` from `"293_story_register_all_bot_commands"`) +/// and combines them with the human-readable name from the front matter when +/// available. Known types (`story`, `bug`, `spike`, `refactor`) are shown as +/// bracketed labels; unknown or missing types are omitted silently. /// /// Examples: -/// - `("293_story_foo", Some("Register all bot commands"))` → `"293 — Register all bot commands"` -/// - `("293_story_foo", None)` → `"293"` +/// - `("293_story_foo", Some("Register all bot commands"))` → `"293 [story] — Register all bot commands"` +/// - `("375_bug_foo", None)` → `"375 [bug]"` +/// - `("293_story_foo", None)` → `"293 [story]"` /// - `("no_number_here", None)` → `"no_number_here"` pub(super) fn story_short_label(stem: &str, name: Option<&str>) -> String { - let number = stem - .split('_') - .next() - .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())) - .unwrap_or(stem); - match name { - Some(n) => format!("{number} — {n}"), + let mut parts = stem.splitn(3, '_'); + let first = parts.next().unwrap_or(stem); + let (number, type_label) = if !first.is_empty() && first.chars().all(|c| c.is_ascii_digit()) { + let t = parts.next().and_then(|t| match t { + "story" | "bug" | "spike" | "refactor" => Some(t), + _ => None, + }); + (first, t) + } else { + (stem, None) + }; + let prefix = match type_label { + Some(t) => format!("{number} [{t}]"), None => number.to_string(), + }; + match name { + Some(n) => format!("{prefix} — {n}"), + None => prefix, } } @@ -200,13 +213,13 @@ 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")); - assert_eq!(label, "293 — Register all bot commands"); + assert_eq!(label, "293 [story] — Register all bot commands"); } #[test] fn short_label_number_only_when_no_name() { let label = story_short_label("297_story_improve_bot_status_command_formatting", None); - assert_eq!(label, "297"); + assert_eq!(label, "297 [story]"); } #[test] @@ -224,6 +237,37 @@ mod tests { ); } + #[test] + fn short_label_shows_bug_type() { + 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")); + 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")); + assert_eq!(label, "260 [refactor] — Upgrade libsqlite3-sys"); + } + + #[test] + fn short_label_omits_unknown_type() { + let label = story_short_label("42_task_do_something", Some("Do something")); + assert_eq!(label, "42 — Do something"); + } + + #[test] + fn short_label_no_type_when_only_id() { + // Stem with only a numeric ID and no type segment + let label = story_short_label("42", Some("Some item")); + assert_eq!(label, "42 — Some item"); + } + // -- build_pipeline_status formatting ----------------------------------- #[test] @@ -248,8 +292,8 @@ mod tests { "output must not show full filename stem: {output}" ); assert!( - output.contains("293 — Register all bot commands"), - "output must show number and title: {output}" + output.contains("293 [story] — Register all bot commands"), + "output must show number, type, and title: {output}" ); } @@ -288,7 +332,7 @@ mod tests { let output = build_pipeline_status(tmp.path(), &agents); assert!( - output.contains("293 — Register all bot commands — $0.29"), + output.contains("293 [story] — Register all bot commands — $0.29"), "output must show cost next to story: {output}" ); } @@ -351,7 +395,7 @@ mod tests { let output = build_pipeline_status(tmp.path(), &agents); assert!( - output.contains("293 — Register all bot commands — $0.29"), + output.contains("293 [story] — Register all bot commands — $0.29"), "output must show aggregated cost: {output}" ); }