huskies: merge 984
This commit is contained in:
@@ -11,7 +11,7 @@ use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::pipeline_state::{
|
||||
ApplyError, ArchiveReason, BranchName, GitSha, MergeFailureKind, PipelineEvent, Stage,
|
||||
ApplyError, ArchiveReason, BranchName, GitSha, MergeFailureKind, PipelineEvent, Stage, StoryId,
|
||||
TransitionFired, apply_transition, stage_label,
|
||||
};
|
||||
use crate::slog;
|
||||
@@ -106,7 +106,14 @@ pub fn move_story_to_done(story_id: &str) -> Result<(), String> {
|
||||
let item = read_typed_or_err(story_id)?;
|
||||
|
||||
// Idempotent: already at or past done.
|
||||
if matches!(item.stage, Stage::Done { .. } | Stage::Archived { .. }) {
|
||||
if matches!(
|
||||
item.stage,
|
||||
Stage::Done { .. }
|
||||
| Stage::Archived { .. }
|
||||
| Stage::Abandoned { .. }
|
||||
| Stage::Superseded { .. }
|
||||
| Stage::Rejected { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -142,6 +149,9 @@ pub fn move_story_to_merge(story_id: &str) -> Result<(), String> {
|
||||
| Stage::MergeFailure { .. }
|
||||
| Stage::Done { .. }
|
||||
| Stage::Archived { .. }
|
||||
| Stage::Abandoned { .. }
|
||||
| Stage::Superseded { .. }
|
||||
| Stage::Rejected { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -184,6 +194,9 @@ pub fn move_story_to_qa(story_id: &str) -> Result<(), String> {
|
||||
| Stage::MergeFailure { .. }
|
||||
| Stage::Done { .. }
|
||||
| Stage::Archived { .. }
|
||||
| Stage::Abandoned { .. }
|
||||
| Stage::Superseded { .. }
|
||||
| Stage::Rejected { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -313,6 +326,51 @@ pub fn transition_to_unblocked(story_id: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Abandon a work item, transitioning it to `Stage::Abandoned`.
|
||||
///
|
||||
/// Valid from any active or done stage. Returns `Err` when the item is not
|
||||
/// found or the transition is invalid for the current stage.
|
||||
#[allow(dead_code)]
|
||||
pub fn abandon_story(story_id: &str) -> Result<(), String> {
|
||||
apply_transition(story_id, PipelineEvent::Abandon, None)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Mark a work item as superseded by another, transitioning to `Stage::Superseded`.
|
||||
///
|
||||
/// `superseded_by` is the story ID of the replacement work item. Valid from
|
||||
/// any active or done stage. Returns `Err` on unknown item or invalid transition.
|
||||
#[allow(dead_code)]
|
||||
pub fn supersede_story(story_id: &str, superseded_by: &str) -> Result<(), String> {
|
||||
apply_transition(
|
||||
story_id,
|
||||
PipelineEvent::Supersede {
|
||||
by: StoryId(superseded_by.to_string()),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Permanently reject a work item, transitioning it to `Stage::Rejected`.
|
||||
///
|
||||
/// `reason` must be non-empty. Valid from any active stage (backlog, coding,
|
||||
/// qa, or merge). Returns `Err` on unknown item or invalid transition.
|
||||
#[allow(dead_code)]
|
||||
pub fn reject_story(story_id: &str, reason: &str) -> Result<(), String> {
|
||||
apply_transition(
|
||||
story_id,
|
||||
PipelineEvent::Reject {
|
||||
reason: reason.to_string(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Map a (current stage, target stage name) pair to the appropriate PipelineEvent.
|
||||
fn map_stage_move_to_event(
|
||||
from: &Stage,
|
||||
@@ -441,7 +499,14 @@ pub fn move_story_to_stage(story_id: &str, target_stage: &str) -> Result<(String
|
||||
pub fn close_bug_to_archive(bug_id: &str) -> Result<(), String> {
|
||||
let item = read_typed_or_err(bug_id)?;
|
||||
|
||||
if matches!(item.stage, Stage::Done { .. } | Stage::Archived { .. }) {
|
||||
if matches!(
|
||||
item.stage,
|
||||
Stage::Done { .. }
|
||||
| Stage::Archived { .. }
|
||||
| Stage::Abandoned { .. }
|
||||
| Stage::Superseded { .. }
|
||||
| Stage::Rejected { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -472,6 +537,9 @@ fn stage_to_name(s: &Stage) -> &'static str {
|
||||
Stage::ReviewHold { .. } => "review_hold",
|
||||
Stage::Done { .. } => "done",
|
||||
Stage::Archived { .. } => "archived",
|
||||
Stage::Abandoned { .. } => "abandoned",
|
||||
Stage::Superseded { .. } => "superseded",
|
||||
Stage::Rejected { .. } => "rejected",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,6 +945,74 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Story 984: abandon_story / supersede_story / reject_story ───────────────
|
||||
|
||||
#[test]
|
||||
fn abandon_story_transitions_to_abandoned() {
|
||||
crate::crdt_state::init_for_test();
|
||||
crate::db::ensure_content_store();
|
||||
let story_id = "99984_story_abandon";
|
||||
crate::db::write_item_with_content(
|
||||
story_id,
|
||||
"2_current",
|
||||
"---\nname: Abandon Test\n---\n",
|
||||
crate::db::ItemMeta::named("Abandon Test"),
|
||||
);
|
||||
abandon_story(story_id).expect("abandon_story must succeed");
|
||||
let item = crate::pipeline_state::read_typed(story_id)
|
||||
.expect("read must succeed")
|
||||
.expect("item must exist");
|
||||
assert!(
|
||||
matches!(item.stage, Stage::Abandoned { .. }),
|
||||
"stage must be Abandoned after abandon_story: {:?}",
|
||||
item.stage
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supersede_story_transitions_to_superseded() {
|
||||
crate::crdt_state::init_for_test();
|
||||
crate::db::ensure_content_store();
|
||||
let story_id = "99985_story_supersede";
|
||||
crate::db::write_item_with_content(
|
||||
story_id,
|
||||
"1_backlog",
|
||||
"---\nname: Supersede Test\n---\n",
|
||||
crate::db::ItemMeta::named("Supersede Test"),
|
||||
);
|
||||
supersede_story(story_id, "999_story_replacement").expect("supersede_story must succeed");
|
||||
let item = crate::pipeline_state::read_typed(story_id)
|
||||
.expect("read must succeed")
|
||||
.expect("item must exist");
|
||||
assert!(
|
||||
matches!(item.stage, Stage::Superseded { ref superseded_by, .. } if superseded_by.0 == "999_story_replacement"),
|
||||
"stage must be Superseded with correct ID: {:?}",
|
||||
item.stage
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_story_transitions_to_rejected() {
|
||||
crate::crdt_state::init_for_test();
|
||||
crate::db::ensure_content_store();
|
||||
let story_id = "99986_story_reject";
|
||||
crate::db::write_item_with_content(
|
||||
story_id,
|
||||
"3_qa",
|
||||
"---\nname: Reject Test\n---\n",
|
||||
crate::db::ItemMeta::named("Reject Test"),
|
||||
);
|
||||
reject_story(story_id, "not aligned with roadmap").expect("reject_story must succeed");
|
||||
let item = crate::pipeline_state::read_typed(story_id)
|
||||
.expect("read must succeed")
|
||||
.expect("item must exist");
|
||||
assert!(
|
||||
matches!(item.stage, Stage::Rejected { ref reason, .. } if reason == "not aligned with roadmap"),
|
||||
"stage must be Rejected with correct reason: {:?}",
|
||||
item.stage
|
||||
);
|
||||
}
|
||||
|
||||
/// Bug 226: feature_branch_has_unmerged_changes returns false when no
|
||||
/// feature branch exists.
|
||||
#[test]
|
||||
|
||||
@@ -506,6 +506,9 @@ impl AgentPool {
|
||||
typed_item.stage,
|
||||
crate::pipeline_state::Stage::Done { .. }
|
||||
| crate::pipeline_state::Stage::Archived { .. }
|
||||
| crate::pipeline_state::Stage::Abandoned { .. }
|
||||
| crate::pipeline_state::Stage::Superseded { .. }
|
||||
| crate::pipeline_state::Stage::Rejected { .. }
|
||||
)
|
||||
{
|
||||
let current_dir = typed_item.stage.dir_name();
|
||||
|
||||
Reference in New Issue
Block a user