huskies: merge 866
This commit is contained in:
@@ -230,6 +230,45 @@ pub fn reject_story_from_qa(story_id: &str, notes: &str) -> Result<(), String> {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Transition a story to the `Blocked` stage via the state machine.
|
||||
///
|
||||
/// Builds a `PipelineEvent::Block { reason }`, validates the transition, and
|
||||
/// writes the resulting `Stage::Blocked` to the CRDT. Returns `Err` on
|
||||
/// `TransitionError` — callers must NOT fall back to direct register writes.
|
||||
pub fn transition_to_blocked(story_id: &str, reason: &str) -> Result<(), String> {
|
||||
let transform = fields_to_clear_transform(&["blocked"]);
|
||||
apply_transition(
|
||||
story_id,
|
||||
PipelineEvent::Block {
|
||||
reason: reason.to_string(),
|
||||
},
|
||||
transform.as_ref().map(|f| f.as_ref()),
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Transition a story out of `Blocked` back to `Coding` via the state machine.
|
||||
///
|
||||
/// Builds a `PipelineEvent::Unblock`, validates the transition, writes the
|
||||
/// resulting `Stage::Coding` to the CRDT, and resets `retry_count` to 0.
|
||||
/// Returns `Err` on `TransitionError` — callers must NOT fall back to direct
|
||||
/// register writes.
|
||||
pub fn transition_to_unblocked(story_id: &str) -> Result<(), String> {
|
||||
let transform = fields_to_clear_transform(&["blocked", "merge_failure", "retry_count"]);
|
||||
apply_transition(
|
||||
story_id,
|
||||
PipelineEvent::Unblock,
|
||||
transform.as_ref().map(|f| f.as_ref()),
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Reset the retry counter in the CRDT so the story gets a fresh budget.
|
||||
crate::crdt_state::set_retry_count(story_id, 0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Map a (current stage, target stage name) pair to the appropriate PipelineEvent.
|
||||
fn map_stage_move_to_event(
|
||||
from: &Stage,
|
||||
@@ -261,6 +300,7 @@ fn map_stage_move_to_event(
|
||||
merge_commit: GitSha("manual".to_string()),
|
||||
}),
|
||||
(Stage::Coding | Stage::Qa | Stage::Backlog, "done") => Ok(PipelineEvent::Close),
|
||||
(Stage::Blocked { .. }, "current") => Ok(PipelineEvent::Unblock),
|
||||
(
|
||||
Stage::Archived {
|
||||
reason: ArchiveReason::Blocked { .. },
|
||||
@@ -345,6 +385,7 @@ fn stage_to_name(s: &Stage) -> &'static str {
|
||||
Stage::Upcoming => "upcoming",
|
||||
Stage::Backlog => "backlog",
|
||||
Stage::Coding => "current",
|
||||
Stage::Blocked { .. } => "blocked",
|
||||
Stage::Qa => "qa",
|
||||
Stage::Merge { .. } => "merge",
|
||||
Stage::Done { .. } => "done",
|
||||
@@ -452,6 +493,68 @@ mod tests {
|
||||
assert_eq!(item_type_from_id("99999"), "story");
|
||||
}
|
||||
|
||||
// ── Story 866: block/unblock round-trip regression test ──────────────────
|
||||
|
||||
/// Regression test (story 866): block a story via the new state-machine path,
|
||||
/// verify it lands in `Stage::Blocked`, then unblock and verify it returns
|
||||
/// to `Stage::Coding`.
|
||||
#[test]
|
||||
fn block_unblock_round_trip_via_state_machine() {
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"99866_story_block_test",
|
||||
"2_current",
|
||||
"---\nname: Block Round Trip\n---\n# Story\n",
|
||||
);
|
||||
|
||||
// Verify starting state is Coding.
|
||||
let item = crate::pipeline_state::read_typed("99866_story_block_test")
|
||||
.expect("read should succeed")
|
||||
.expect("item should exist");
|
||||
assert_eq!(
|
||||
item.stage.dir_name(),
|
||||
"2_current",
|
||||
"should start in 2_current"
|
||||
);
|
||||
|
||||
// Block via the state machine.
|
||||
transition_to_blocked("99866_story_block_test", "retry limit exceeded")
|
||||
.expect("transition_to_blocked should succeed");
|
||||
|
||||
// Verify the CRDT now shows Stage::Blocked.
|
||||
let item = crate::pipeline_state::read_typed("99866_story_block_test")
|
||||
.expect("read should succeed")
|
||||
.expect("item should exist after block");
|
||||
assert_eq!(
|
||||
item.stage.dir_name(),
|
||||
"2_blocked",
|
||||
"should be in 2_blocked after transition_to_blocked"
|
||||
);
|
||||
assert!(item.stage.is_blocked(), "is_blocked() should return true");
|
||||
assert!(
|
||||
matches!(item.stage, Stage::Blocked { .. }),
|
||||
"stage should be Stage::Blocked variant"
|
||||
);
|
||||
|
||||
// Unblock via the state machine.
|
||||
transition_to_unblocked("99866_story_block_test")
|
||||
.expect("transition_to_unblocked should succeed");
|
||||
|
||||
// Verify the story returned to Coding.
|
||||
let item = crate::pipeline_state::read_typed("99866_story_block_test")
|
||||
.expect("read should succeed")
|
||||
.expect("item should exist after unblock");
|
||||
assert_eq!(
|
||||
item.stage.dir_name(),
|
||||
"2_current",
|
||||
"should return to 2_current after unblock"
|
||||
);
|
||||
assert!(
|
||||
matches!(item.stage, Stage::Coding),
|
||||
"stage should be Stage::Coding after unblock"
|
||||
);
|
||||
}
|
||||
|
||||
// ── feature_branch_has_unmerged_changes tests ────────────────────────────
|
||||
|
||||
fn init_git_repo(repo: &std::path::Path) {
|
||||
|
||||
Reference in New Issue
Block a user