huskies: merge 984
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user