huskies: merge 893
This commit is contained in:
@@ -334,7 +334,9 @@ fn map_stage_move_to_event(
|
||||
}),
|
||||
(Stage::Coding | Stage::Qa | Stage::Backlog, "done") => Ok(PipelineEvent::Close),
|
||||
(Stage::Blocked { .. }, "current") => Ok(PipelineEvent::Unblock),
|
||||
(Stage::MergeFailure { .. }, "backlog") => Ok(PipelineEvent::Unblock),
|
||||
// Story 893: MergeFailure + Unblock now goes to Coding (retry), so
|
||||
// manual demotion to backlog uses Demote instead.
|
||||
(Stage::MergeFailure { .. }, "backlog") => Ok(PipelineEvent::Demote),
|
||||
(
|
||||
Stage::Archived {
|
||||
reason: ArchiveReason::Blocked { .. },
|
||||
@@ -592,6 +594,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Story 893: MergeFailure unblock → Coding regression ─────────────────
|
||||
|
||||
/// Regression test (story 893): unblocking a story in `MergeFailure` via
|
||||
/// `transition_to_unblocked` transitions it to `Stage::Coding`, not `Backlog`.
|
||||
/// After the unblock, the auto-assigner can pick it up normally (it looks for
|
||||
/// stories in `Coding` / active stages).
|
||||
#[test]
|
||||
fn unblock_merge_failure_story_lands_in_coding() {
|
||||
crate::db::ensure_content_store();
|
||||
crate::db::write_item_with_content(
|
||||
"99893_story_merge_failure_unblock",
|
||||
"merge_failure",
|
||||
"---\nname: MergeFailure Unblock Test\n---\n# Story\n",
|
||||
crate::db::ItemMeta::named("MergeFailure Unblock Test"),
|
||||
);
|
||||
|
||||
// Verify starting state is MergeFailure.
|
||||
let item = crate::pipeline_state::read_typed("99893_story_merge_failure_unblock")
|
||||
.expect("CRDT read should succeed")
|
||||
.expect("item should exist");
|
||||
assert!(
|
||||
matches!(item.stage, Stage::MergeFailure { .. }),
|
||||
"should start in MergeFailure: {:?}",
|
||||
item.stage
|
||||
);
|
||||
|
||||
// Unblock routes through transition_to_unblocked (same path as unblock_story MCP).
|
||||
transition_to_unblocked("99893_story_merge_failure_unblock")
|
||||
.expect("transition_to_unblocked should succeed for MergeFailure story");
|
||||
|
||||
// Story must land in Coding, not Backlog — the auto-assigner picks up
|
||||
// Coding-stage stories without an extra DepsMet promotion step.
|
||||
let item = crate::pipeline_state::read_typed("99893_story_merge_failure_unblock")
|
||||
.expect("CRDT read should succeed")
|
||||
.expect("item should exist after unblock");
|
||||
assert_eq!(
|
||||
item.stage.dir_name(),
|
||||
"coding",
|
||||
"MergeFailure story should land in Coding after unblock for immediate retry: {:?}",
|
||||
item.stage
|
||||
);
|
||||
assert!(
|
||||
matches!(item.stage, Stage::Coding),
|
||||
"stage should be Stage::Coding after unblock, got: {:?}",
|
||||
item.stage
|
||||
);
|
||||
// auto_assign checks is_active() — Coding satisfies it.
|
||||
assert!(
|
||||
item.stage.is_active(),
|
||||
"Coding satisfies is_active() so auto_assign can pick it up: {:?}",
|
||||
item.stage
|
||||
);
|
||||
}
|
||||
|
||||
// ── feature_branch_has_unmerged_changes tests ────────────────────────────
|
||||
|
||||
fn init_git_repo(repo: &std::path::Path) {
|
||||
|
||||
@@ -750,6 +750,34 @@ fn repeated_merge_failure_apply_transition_no_error_no_duplicate_notification()
|
||||
);
|
||||
}
|
||||
|
||||
// ── Story 893: MergeFailure + Unblock → Coding (retry) ─────────────
|
||||
|
||||
/// AC1: `MergeFailure + Unblock` transitions to `Coding` (retry), not `Backlog`.
|
||||
#[test]
|
||||
fn merge_failure_unblock_returns_to_coding() {
|
||||
let s = Stage::MergeFailure {
|
||||
reason: "conflicts in server/src/main.rs".into(),
|
||||
};
|
||||
let result = transition(s, PipelineEvent::Unblock).unwrap();
|
||||
assert!(
|
||||
matches!(result, Stage::Coding),
|
||||
"MergeFailure + Unblock should return to Coding for immediate retry, got: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// AC1 (complement): `MergeFailure + Demote` still goes to `Backlog` for manual parking.
|
||||
#[test]
|
||||
fn merge_failure_demote_returns_to_backlog() {
|
||||
let s = Stage::MergeFailure {
|
||||
reason: "conflicts".into(),
|
||||
};
|
||||
let result = transition(s, PipelineEvent::Demote).unwrap();
|
||||
assert!(
|
||||
matches!(result, Stage::Backlog),
|
||||
"MergeFailure + Demote should park the story in Backlog, got: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Story 892: MergeFailure → Done (manual recovery) ───────────────
|
||||
|
||||
/// Regression test (story 892): `accept_story` on a story in `MergeFailure`
|
||||
|
||||
@@ -242,8 +242,17 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
||||
// ── Unblock: Blocked → Coding ─────────────────────────────────
|
||||
(Blocked { .. }, Unblock) => Ok(Coding),
|
||||
|
||||
// ── Unblock MergeFailure → Backlog ───────────────────────────────
|
||||
(MergeFailure { .. }, Unblock) => Ok(Backlog),
|
||||
// ── Unblock MergeFailure → Coding (retry) ────────────────────────
|
||||
// `unblock_story` on a failed merge re-enters the coding stage so a
|
||||
// coder agent can rework the branch, rather than routing back through
|
||||
// the backlog (which would require an extra DepsMet promotion step).
|
||||
(MergeFailure { .. }, Unblock) => Ok(Coding),
|
||||
|
||||
// ── Demote MergeFailure → Backlog (manual parking) ───────────────
|
||||
// Lets operators park a failed-merge story in the backlog without an
|
||||
// agent retry. Complements `Unblock` (→ Coding) which triggers an
|
||||
// immediate retry by a coder agent.
|
||||
(MergeFailure { .. }, Demote) => Ok(Backlog),
|
||||
|
||||
// ── Legacy unblock: Archived(Blocked|MergeFailed) → Backlog ──
|
||||
(
|
||||
|
||||
@@ -108,7 +108,8 @@ pub enum Stage {
|
||||
|
||||
/// Merge pipeline failed (conflicts or gate failures). Story is held here
|
||||
/// awaiting human intervention or retry. Unlike `Archived(MergeFailed)`,
|
||||
/// this is a recoverable intermediate state — `Unblock` returns to `Backlog`.
|
||||
/// this is a recoverable intermediate state — `Unblock` returns to `Coding`
|
||||
/// (immediate agent retry) and `Demote` returns to `Backlog` (manual park).
|
||||
MergeFailure { reason: String },
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user