huskies: merge 971
This commit is contained in:
@@ -338,6 +338,8 @@ fn map_stage_move_to_event(
|
|||||||
// Story 919: MergeFailure + Unblock goes to Merge (re-attempt); manual
|
// Story 919: MergeFailure + Unblock goes to Merge (re-attempt); manual
|
||||||
// demotion to backlog uses Demote to park it without a retry.
|
// demotion to backlog uses Demote to park it without a retry.
|
||||||
(Stage::MergeFailure { .. }, "backlog") => Ok(PipelineEvent::Demote),
|
(Stage::MergeFailure { .. }, "backlog") => Ok(PipelineEvent::Demote),
|
||||||
|
// Story 971: send MergeFailure story back to Coding so a coder can fix it.
|
||||||
|
(Stage::MergeFailure { .. }, "current") => Ok(PipelineEvent::FixupRequested),
|
||||||
(
|
(
|
||||||
Stage::Archived {
|
Stage::Archived {
|
||||||
reason: ArchiveReason::Blocked { .. },
|
reason: ArchiveReason::Blocked { .. },
|
||||||
@@ -395,7 +397,14 @@ pub fn move_story_to_stage(story_id: &str, target_stage: &str) -> Result<(String
|
|||||||
|
|
||||||
let event = map_stage_move_to_event(&item.stage, target_stage, story_id)?;
|
let event = map_stage_move_to_event(&item.stage, target_stage, story_id)?;
|
||||||
|
|
||||||
apply_transition(story_id, event, None).map_err(|e| e.to_string())?;
|
apply_transition(story_id, event.clone(), None).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Story 971: after moving MergeFailure → Coding, set retry_count=1 so
|
||||||
|
// maybe_inject_gate_failure fires on the next spawn. Must happen AFTER
|
||||||
|
// apply_transition because move_item_stage resets retry_count to 0.
|
||||||
|
if matches!(event, PipelineEvent::FixupRequested) {
|
||||||
|
crate::crdt_state::set_retry_count(story_id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
Ok((from_name.to_string(), target_stage.to_string()))
|
Ok((from_name.to_string(), target_stage.to_string()))
|
||||||
}
|
}
|
||||||
@@ -718,6 +727,75 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Story 971: MergeFailure → Coding fixup tests ─────────────────────────
|
||||||
|
|
||||||
|
/// AC1 (story 971): move_story_to_stage to "current" from MergeFailure
|
||||||
|
/// succeeds and lands the story in Coding stage.
|
||||||
|
#[test]
|
||||||
|
fn move_story_to_stage_from_merge_failure_lands_in_coding() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
|
||||||
|
let story_id = "99960_story_merge_failure_fixup_971";
|
||||||
|
crate::db::write_item_with_content(
|
||||||
|
story_id,
|
||||||
|
"merge_failure",
|
||||||
|
"---\nname: Merge Failure Fixup\n---\n",
|
||||||
|
crate::db::ItemMeta::named("Merge Failure Fixup"),
|
||||||
|
);
|
||||||
|
|
||||||
|
move_story_to_stage(story_id, "current").expect("move to current must succeed");
|
||||||
|
|
||||||
|
let item = crate::pipeline_state::read_typed(story_id)
|
||||||
|
.expect("CRDT read must succeed")
|
||||||
|
.expect("item must exist");
|
||||||
|
assert_eq!(
|
||||||
|
item.stage.dir_name(),
|
||||||
|
"coding",
|
||||||
|
"story must be in coding after fixup move"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AC3 (story 971): retry_count is set to 1 after fixup move so that
|
||||||
|
/// spawn's maybe_inject_gate_failure will inject the pre-existing gate_output.
|
||||||
|
/// gate_output is seeded here the same way the merge pipeline seeds it.
|
||||||
|
#[test]
|
||||||
|
fn merge_failure_fixup_sets_retry_count_for_gate_output_injection() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
|
||||||
|
let story_id = "99961_story_merge_failure_context_971";
|
||||||
|
crate::db::write_item_with_content(
|
||||||
|
story_id,
|
||||||
|
"merge_failure",
|
||||||
|
"---\nname: Merge Failure Context\n---\n",
|
||||||
|
crate::db::ItemMeta::named("Merge Failure Context"),
|
||||||
|
);
|
||||||
|
// Simulate what the merge pipeline stores when a merge fails.
|
||||||
|
crate::db::write_content(
|
||||||
|
crate::db::ContentKey::GateOutput(story_id),
|
||||||
|
"CONFLICT (content): server/src/lib.rs",
|
||||||
|
);
|
||||||
|
|
||||||
|
move_story_to_stage(story_id, "current").expect("move to current must succeed");
|
||||||
|
|
||||||
|
let retry_count = crate::crdt_state::read_item(story_id)
|
||||||
|
.expect("CRDT item must exist")
|
||||||
|
.retry_count();
|
||||||
|
assert_eq!(
|
||||||
|
retry_count, 1,
|
||||||
|
"retry_count must be 1 after fixup move so gate_output injection fires on spawn"
|
||||||
|
);
|
||||||
|
|
||||||
|
// gate_output must still hold the merge pipeline's output unchanged.
|
||||||
|
let gate_output = crate::db::read_content(crate::db::ContentKey::GateOutput(story_id))
|
||||||
|
.expect("gate_output must still be present after fixup move");
|
||||||
|
assert!(
|
||||||
|
gate_output.contains("CONFLICT"),
|
||||||
|
"gate_output must retain merge failure details; got: {gate_output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Bug 226: feature_branch_has_unmerged_changes returns false when no
|
/// Bug 226: feature_branch_has_unmerged_changes returns false when no
|
||||||
/// feature branch exists.
|
/// feature branch exists.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ pub enum PipelineEvent {
|
|||||||
/// Story 945: mergemaster has been auto-spawned and gave up; transitions
|
/// Story 945: mergemaster has been auto-spawned and gave up; transitions
|
||||||
/// `Stage::MergeFailure` → `Stage::MergeFailureFinal`.
|
/// `Stage::MergeFailure` → `Stage::MergeFailureFinal`.
|
||||||
MergemasterAttempted,
|
MergemasterAttempted,
|
||||||
|
/// Story 971: user sends a MergeFailure story back to Coding for coder fixup.
|
||||||
|
FixupRequested,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Per-node execution events ───────────────────────────────────────────────
|
// ── Per-node execution events ───────────────────────────────────────────────
|
||||||
@@ -111,6 +113,7 @@ pub fn event_label(e: &PipelineEvent) -> &'static str {
|
|||||||
PipelineEvent::Unfreeze => "Unfreeze",
|
PipelineEvent::Unfreeze => "Unfreeze",
|
||||||
PipelineEvent::ReviewHoldCleared => "ReviewHoldCleared",
|
PipelineEvent::ReviewHoldCleared => "ReviewHoldCleared",
|
||||||
PipelineEvent::MergemasterAttempted => "MergemasterAttempted",
|
PipelineEvent::MergemasterAttempted => "MergemasterAttempted",
|
||||||
|
PipelineEvent::FixupRequested => "FixupRequested",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +301,9 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
|||||||
// ── ReviewHoldCleared: ReviewHold → resume_to ──────────────────
|
// ── ReviewHoldCleared: ReviewHold → resume_to ──────────────────
|
||||||
(Stage::ReviewHold { resume_to, .. }, ReviewHoldCleared) => Ok(*resume_to),
|
(Stage::ReviewHold { resume_to, .. }, ReviewHoldCleared) => Ok(*resume_to),
|
||||||
|
|
||||||
|
// ── FixupRequested: MergeFailure → Coding (coder fixup) ────────
|
||||||
|
(MergeFailure { .. }, FixupRequested) => Ok(Coding),
|
||||||
|
|
||||||
// ── MergemasterAttempted: MergeFailure → MergeFailureFinal ─────
|
// ── MergemasterAttempted: MergeFailure → MergeFailureFinal ─────
|
||||||
(MergeFailure { reason, .. }, MergemasterAttempted) => Ok(MergeFailureFinal { reason }),
|
(MergeFailure { reason, .. }, MergemasterAttempted) => Ok(MergeFailureFinal { reason }),
|
||||||
(MergeFailureFinal { reason }, MergemasterAttempted) => Ok(MergeFailureFinal { reason }),
|
(MergeFailureFinal { reason }, MergemasterAttempted) => Ok(MergeFailureFinal { reason }),
|
||||||
|
|||||||
Reference in New Issue
Block a user