huskies: merge 984

This commit is contained in:
dave
2026-05-13 16:43:19 +00:00
parent c3c9db3d8b
commit 580480094e
25 changed files with 501 additions and 97 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ pub(super) use labels::{story_short_label, traffic_light_dot};
pub(super) use render::{build_pipeline_status, unmet_deps_from_items};
#[cfg(test)]
pub(super) use render::{build_status_from_items, first_non_empty_snippet};
pub(super) use render::{build_status_from_items, display_section, first_non_empty_snippet};
use super::CommandContext;
+36 -3
View File
@@ -28,7 +28,10 @@ pub(crate) fn display_section(s: &Stage) -> Option<&'static str> {
}
Stage::Done { .. } => Some("Done"),
Stage::Frozen { resume_to } => display_section(resume_to),
Stage::Archived { .. } => None, // other archived variants are hidden
Stage::Abandoned { .. } | Stage::Superseded { .. } | Stage::Rejected { .. } => {
Some("Closed")
}
Stage::Archived { .. } => None, // Completed/MergeFailed/ReviewHeld stay hidden
}
}
@@ -50,7 +53,18 @@ pub(crate) fn unmet_deps_from_items(item: &PipelineItem, all_items: &[PipelineIt
|| i.story_id.0.split('_').next() == Some(dep_id.0.as_str())
});
match dep {
Some(d) if matches!(d.stage, Stage::Done { .. } | Stage::Archived { .. }) => None,
Some(d)
if matches!(
d.stage,
Stage::Done { .. }
| Stage::Archived { .. }
| Stage::Abandoned { .. }
| Stage::Superseded { .. }
| Stage::Rejected { .. }
) =>
{
None
}
Some(_) => Some(dep_num), // Found but not done = unmet
None => None, // Not in CRDT; treat as met
}
@@ -126,7 +140,7 @@ pub(crate) fn build_status_from_items(
// under their stage section (determined by `display_section`); there is
// no separate "Blocked" section. Frozen items appear under the section
// their `resume_to` stage maps to.
let sections = ["Backlog", "In Progress", "QA", "Merge", "Done"];
let sections = ["Backlog", "In Progress", "QA", "Merge", "Done", "Closed"];
for label in sections {
let mut section_items: Vec<&PipelineItem> = items
@@ -225,6 +239,25 @@ fn render_item_line(
format!(" *(waiting on: {})*", nums.join(", "))
};
// Closed-stage items (abandoned / superseded / rejected) each get a
// distinct indicator and optionally display their metadata.
match &item.stage {
Stage::Abandoned { .. } => {
return format!(" \u{1F5D1}\u{FE0F} {display}{cost_suffix}\n"); // 🗑️
}
Stage::Superseded { superseded_by, .. } => {
return format!(
" \u{1F500} {display}{cost_suffix} — superseded by {}\n", // 🔀
superseded_by.0
);
}
Stage::Rejected { reason, .. } => {
let snippet = first_non_empty_snippet(reason, 120);
return format!(" \u{1F6AB} {display}{cost_suffix}{snippet}\n"); // 🚫
}
_ => {}
}
// Merge-stage items get dedicated breakdown indicators instead of the
// generic traffic-light dot. MergeFailure / MergeFailureFinal items
// now also appear in the Merge section (in-place) so they are handled
+124
View File
@@ -938,3 +938,127 @@ fn merge_failure_item_appears_in_merge_section_not_blocked() {
"merge failure reason should be shown: {output}"
);
}
// -- Story 984: Abandoned / Superseded / Rejected appear in Closed section ----
#[test]
fn abandoned_item_appears_in_closed_section_with_wastebasket() {
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let items = vec![make_item(
"984_story_abandoned",
"Abandoned Story",
Stage::Abandoned { ts: Utc::now() },
)];
let agents = AgentPool::new_test(3000);
let output = build_status_from_items(tmp.path(), &agents, &items);
assert!(
output.contains("**Closed**"),
"output must have a Closed section: {output}"
);
let closed_pos = output.find("**Closed**").unwrap();
let story_pos = output
.find("984 [story]")
.expect("story must appear in output");
assert!(
story_pos > closed_pos,
"abandoned story should be after Closed header: {output}"
);
assert!(
output.contains("\u{1F5D1}\u{FE0F}"), // 🗑️
"abandoned story should show wastebasket icon: {output}"
);
}
#[test]
fn superseded_item_appears_in_closed_section_with_shuffle() {
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let items = vec![make_item(
"985_story_superseded",
"Superseded Story",
Stage::Superseded {
ts: Utc::now(),
superseded_by: crate::pipeline_state::StoryId("999_story_new".to_string()),
},
)];
let agents = AgentPool::new_test(3000);
let output = build_status_from_items(tmp.path(), &agents, &items);
let closed_pos = output
.find("**Closed**")
.expect("Closed section must exist");
let story_pos = output
.find("985 [story]")
.expect("story must appear in output");
assert!(story_pos > closed_pos, "superseded story must be in Closed");
assert!(
output.contains("\u{1F500}"), // 🔀
"superseded story should show shuffle icon: {output}"
);
assert!(
output.contains("999_story_new"),
"superseded story should show the replacement ID: {output}"
);
}
#[test]
fn rejected_item_appears_in_closed_section_with_no_entry() {
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let items = vec![make_item(
"986_story_rejected",
"Rejected Story",
Stage::Rejected {
ts: Utc::now(),
reason: "not aligned with roadmap".to_string(),
},
)];
let agents = AgentPool::new_test(3000);
let output = build_status_from_items(tmp.path(), &agents, &items);
let closed_pos = output
.find("**Closed**")
.expect("Closed section must exist");
let story_pos = output
.find("986 [story]")
.expect("story must appear in output");
assert!(story_pos > closed_pos, "rejected story must be in Closed");
assert!(
output.contains("\u{1F6AB}"), // 🚫
"rejected story should show no-entry icon: {output}"
);
assert!(
output.contains("not aligned with roadmap"),
"rejected story should show the rejection reason: {output}"
);
}
#[test]
fn display_section_returns_closed_for_new_terminal_variants() {
assert_eq!(
display_section(&Stage::Abandoned { ts: Utc::now() }),
Some("Closed")
);
assert_eq!(
display_section(&Stage::Superseded {
ts: Utc::now(),
superseded_by: crate::pipeline_state::StoryId("1".to_string()),
}),
Some("Closed")
);
assert_eq!(
display_section(&Stage::Rejected {
ts: Utc::now(),
reason: "x".to_string(),
}),
Some("Closed")
);
}