huskies: merge 868

This commit is contained in:
dave
2026-04-29 23:28:57 +00:00
parent e02e566648
commit 1d86202abb
15 changed files with 135 additions and 60 deletions
+8
View File
@@ -91,6 +91,14 @@ pub fn project_stage(view: &PipelineItemView) -> Result<Stage, ProjectionError>
commits_ahead: NonZeroU32::new(1).expect("1 is non-zero"),
})
}
"4_merge_failure" => {
// The reason is persisted in front-matter (merge_failure: "...") but
// is not part of the raw CRDT view; the projection uses an empty
// string here. Consumers that need the reason should read content.
Ok(Stage::MergeFailure {
reason: String::new(),
})
}
"5_done" => {
// Use the stored merged_at timestamp if present. Legacy items
// that pre-date this field have merged_at = None, so we fall back
+53
View File
@@ -655,4 +655,57 @@ fn regression_freeze_unfreeze_restores_crdt_stage() {
);
}
// ── Story 868: MergeFailure regression ─────────────────────────────
/// Regression test (story 868): applying `PipelineEvent::MergeFailed` to a story
/// in `Stage::Merge` transitions it to `Stage::MergeFailure` and the emitted
/// `TransitionFired` event carries the full reason string in its payload.
#[test]
fn merge_failure_transition_emits_event_with_full_reason() {
crate::crdt_state::init_for_test();
crate::db::ensure_content_store();
let story_id = "99868_story_merge_failure_event";
crate::db::write_item_with_content(
story_id,
"4_merge",
"---\nname: Merge Failure Event Test\n---\n# Story\n",
);
let reason = "Conflict in server/src/main.rs: both modified";
let fired = super::apply::apply_transition(
story_id,
PipelineEvent::MergeFailed {
reason: reason.to_string(),
},
None,
)
.expect("MergeFailed transition should succeed");
// The emitted event payload carries the full reason string.
match &fired.event {
PipelineEvent::MergeFailed { reason: r } => {
assert_eq!(r, reason, "emitted event should carry the full reason");
}
other => panic!("expected MergeFailed event, got: {other:?}"),
}
// The story transitioned to MergeFailure.
assert!(
matches!(fired.after, Stage::MergeFailure { .. }),
"after-stage should be MergeFailure: {:?}",
fired.after
);
// Verify CRDT reflects the new stage.
let item = read_typed(story_id)
.expect("CRDT read should succeed")
.expect("item should exist");
assert_eq!(
item.stage.dir_name(),
"4_merge_failure",
"CRDT stage should be 4_merge_failure"
);
}
// ── ProjectionError Display ─────────────────────────────────────────
+10
View File
@@ -33,6 +33,9 @@ pub enum PipelineEvent {
},
/// Mergemaster squash succeeded.
MergeSucceeded { merge_commit: GitSha },
/// Merge pipeline failed (conflicts or gate failures); story moves to
/// `Stage::MergeFailure` awaiting human intervention or retry.
MergeFailed { reason: String },
/// Mergemaster gave up after retry budget.
MergeFailedFinal { reason: String },
/// Story accepted (Done → Archived).
@@ -87,6 +90,7 @@ pub fn event_label(e: &PipelineEvent) -> &'static str {
PipelineEvent::GatesFailed { .. } => "GatesFailed",
PipelineEvent::QaSkipped { .. } => "QaSkipped",
PipelineEvent::MergeSucceeded { .. } => "MergeSucceeded",
PipelineEvent::MergeFailed { .. } => "MergeFailed",
PipelineEvent::MergeFailedFinal { .. } => "MergeFailedFinal",
PipelineEvent::Accepted => "Accepted",
PipelineEvent::Block { .. } => "Block",
@@ -174,6 +178,9 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
reason: ArchiveReason::ReviewHeld { reason },
}),
// ── MergeFailed: Merge → MergeFailure (recoverable intermediate) ──
(Merge { .. }, MergeFailed { reason }) => Ok(MergeFailure { reason }),
(Merge { .. }, MergeFailedFinal { reason }) => Ok(Archived {
archived_at: now,
reason: ArchiveReason::MergeFailed { reason },
@@ -221,6 +228,9 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
// ── Unblock: Blocked → Coding ─────────────────────────────────
(Blocked { .. }, Unblock) => Ok(Coding),
// ── Unblock MergeFailure → Backlog ───────────────────────────────
(MergeFailure { .. }, Unblock) => Ok(Backlog),
// ── Legacy unblock: Archived(Blocked|MergeFailed) → Backlog ──
(
Archived {
+16 -3
View File
@@ -60,9 +60,9 @@ impl fmt::Display for AgentName {
/// | current | `Coding` |
/// | qa_pending | `Qa` |
/// | merge_pending | `Merge { .. }` |
/// | merge_failure | `MergeFailure { .. }` |
/// | done | `Done { .. }` |
/// | blocked | `Blocked { .. }` |
/// | merge_failure | `Archived { MergeFailed { .. } }` |
/// | archived | `Archived { Completed }` |
/// | superseded | `Archived { Superseded { .. } }` |
/// | rejected | `Archived { Rejected { .. } }` |
@@ -106,6 +106,11 @@ pub enum Stage {
reason: ArchiveReason,
},
/// 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`.
MergeFailure { reason: String },
/// Pipeline advancement and auto-assign are suspended. Resumes to
/// `resume_to` when unfrozen.
Frozen { resume_to: Box<Stage> },
@@ -154,12 +159,13 @@ impl Stage {
stage_dir_name(self)
}
/// Returns true if this is the `Blocked` variant (or the legacy
/// `Archived(Blocked)` for backward-compatible reads).
/// Returns true if this is the `Blocked` or `MergeFailure` variant (or the
/// legacy `Archived(Blocked)` for backward-compatible reads).
pub fn is_blocked(&self) -> bool {
matches!(
self,
Stage::Blocked { .. }
| Stage::MergeFailure { .. }
| Stage::Archived {
reason: ArchiveReason::Blocked { .. },
..
@@ -198,6 +204,11 @@ impl Stage {
}),
// Frozen: stub with Coding as resume_to — rich resume_to is loaded
// from front matter by the projection layer.
"4_merge_failure" => Some(Stage::MergeFailure {
reason: String::new(),
}),
// Frozen: stub with Coding as resume_to — rich resume_to is loaded
// from front matter by the projection layer.
"7_frozen" => Some(Stage::Frozen {
resume_to: Box::new(Stage::Coding),
}),
@@ -278,6 +289,7 @@ pub fn stage_label(s: &Stage) -> &'static str {
Stage::Coding => "Coding",
Stage::Qa => "Qa",
Stage::Merge { .. } => "Merge",
Stage::MergeFailure { .. } => "MergeFailure",
Stage::Done { .. } => "Done",
Stage::Blocked { .. } => "Blocked",
Stage::Archived { .. } => "Archived",
@@ -294,6 +306,7 @@ pub fn stage_dir_name(s: &Stage) -> &'static str {
Stage::Blocked { .. } => "2_blocked",
Stage::Qa => "3_qa",
Stage::Merge { .. } => "4_merge",
Stage::MergeFailure { .. } => "4_merge_failure",
Stage::Done { .. } => "5_done",
Stage::Archived { .. } => "6_archived",
Stage::Frozen { .. } => "7_frozen",