huskies: merge 1178 bug MergeFailureFinal is a trap state: successful re-merge cannot mark the story done

This commit is contained in:
Huskies Agent
2026-07-16 14:19:55 +00:00
parent 61acf98909
commit e5df6232cf
6 changed files with 226 additions and 34 deletions
+59 -25
View File
@@ -5,30 +5,41 @@
use crate::agents::merge::{MergeReport, MergeResult};
#[allow(dead_code)]
/// Derive a human-readable status message from a completed [`MergeReport`].
///
/// The message explains what happened and (on failure) what the caller
/// should do next.
pub fn format_merge_status_message(report: &MergeReport) -> &'static str {
/// should do next. On success, the wording reflects what
/// [`MergeReport::story_archived`] and [`MergeReport::worktree_cleaned_up`]
/// actually recorded — story 1178: the message must never claim "moved to
/// done" when the CRDT state transition to `Done` failed.
pub fn format_merge_status_message(report: &MergeReport) -> String {
match &report.result {
MergeResult::Success {
conflicts_resolved: true,
..
conflicts_resolved, ..
} => {
"Merge complete: conflicts were auto-resolved and all quality gates passed. Story moved to done and worktree cleaned up."
}
MergeResult::Success { .. } => {
"Merge complete: all quality gates passed. Story moved to done and worktree cleaned up."
let prefix = if *conflicts_resolved {
"Merge complete: conflicts were auto-resolved and all quality gates passed."
} else {
"Merge complete: all quality gates passed."
};
match (report.story_archived, report.worktree_cleaned_up) {
(true, true) => format!("{prefix} Story moved to done and worktree cleaned up."),
(true, false) => {
format!("{prefix} Story moved to done, but worktree cleanup failed.")
}
(false, _) => format!(
"{prefix} The merge landed on master, but the story could not be moved to done — it needs manual follow-up."
),
}
}
MergeResult::Conflict { .. } => {
"Merge failed: conflicts detected that could not be auto-resolved. Merge was aborted — master is untouched. Call report_merge_failure with the conflict details so the human can resolve them. Do NOT manually move the story file or call accept_story."
"Merge failed: conflicts detected that could not be auto-resolved. Merge was aborted — master is untouched. Call report_merge_failure with the conflict details so the human can resolve them. Do NOT manually move the story file or call accept_story.".to_string()
}
MergeResult::GateFailure { .. } => {
"Merge committed but quality gates failed. Review gate_output and fix issues before re-running."
"Merge committed but quality gates failed. Review gate_output and fix issues before re-running.".to_string()
}
MergeResult::NoCommits { .. } | MergeResult::Other { .. } => {
"Merge failed. Review gate_output for details. Call report_merge_failure to record the failure. Do NOT manually move the story file or call accept_story."
"Merge failed. Review gate_output for details. Call report_merge_failure to record the failure. Do NOT manually move the story file or call accept_story.".to_string()
}
}
}
@@ -51,25 +62,48 @@ mod tests {
#[test]
fn clean_merge_message() {
let mut r = make_report(MergeResult::Success {
conflicts_resolved: false,
conflict_details: None,
gate_output: String::new(),
});
r.story_archived = true;
r.worktree_cleaned_up = true;
let msg = format_merge_status_message(&r);
assert!(msg.contains("quality gates passed"));
assert!(msg.contains("moved to done"));
}
#[test]
fn conflicts_resolved_message() {
let mut r = make_report(MergeResult::Success {
conflicts_resolved: true,
conflict_details: None,
gate_output: String::new(),
});
r.story_archived = true;
r.worktree_cleaned_up = true;
let msg = format_merge_status_message(&r);
assert!(msg.contains("auto-resolved"));
}
/// Regression test (story 1178): a git-level merge success whose CRDT
/// state transition to Done failed (`story_archived: false`) must never
/// produce a message claiming the story was "moved to done".
#[test]
fn success_without_archival_does_not_claim_moved_to_done() {
let r = make_report(MergeResult::Success {
conflicts_resolved: false,
conflict_details: None,
gate_output: String::new(),
});
assert!(!r.story_archived);
let msg = format_merge_status_message(&r);
assert!(msg.contains("quality gates passed"));
assert!(msg.contains("done"));
}
#[test]
fn conflicts_resolved_message() {
let r = make_report(MergeResult::Success {
conflicts_resolved: true,
conflict_details: None,
gate_output: String::new(),
});
let msg = format_merge_status_message(&r);
assert!(msg.contains("auto-resolved"));
assert!(
!msg.contains("Story moved to done"),
"message must not claim the story moved to done when story_archived is false: {msg}"
);
assert!(msg.contains("could not be moved to done"));
}
#[test]