huskies: merge 973
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 973: abort an in-flight merge, sending the story back to Coding.
|
||||||
|
(Stage::Merge { .. }, "current") => Ok(PipelineEvent::MergeAborted),
|
||||||
// Story 971: send MergeFailure story back to Coding so a coder can fix it.
|
// Story 971: send MergeFailure story back to Coding so a coder can fix it.
|
||||||
(Stage::MergeFailure { .. }, "current") => Ok(PipelineEvent::FixupRequested),
|
(Stage::MergeFailure { .. }, "current") => Ok(PipelineEvent::FixupRequested),
|
||||||
// Story 972: send MergeFailure story back to Qa for a QA agent to re-review.
|
// Story 972: send MergeFailure story back to Qa for a QA agent to re-review.
|
||||||
@@ -408,6 +410,15 @@ pub fn move_story_to_stage(story_id: &str, target_stage: &str) -> Result<(String
|
|||||||
crate::crdt_state::set_retry_count(story_id, 1);
|
crate::crdt_state::set_retry_count(story_id, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Story 973: when aborting an in-flight merge, mark the CRDT merge job as
|
||||||
|
// "cancelled" so the background task skips state-machine transitions and
|
||||||
|
// watcher notifications once the git operation finishes.
|
||||||
|
if matches!(event, PipelineEvent::MergeAborted)
|
||||||
|
&& let Some(job) = crate::crdt_state::read_merge_job(story_id)
|
||||||
|
{
|
||||||
|
crate::crdt_state::write_merge_job(story_id, "cancelled", job.started_at, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
Ok((from_name.to_string(), target_stage.to_string()))
|
Ok((from_name.to_string(), target_stage.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,18 @@ impl AgentPool {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let report = pool.run_merge_pipeline(&root, &sid).await;
|
let report = pool.run_merge_pipeline(&root, &sid).await;
|
||||||
|
|
||||||
|
// Story 973: if the story was aborted (Merge → Coding) while the git
|
||||||
|
// operation was running, skip state-machine transitions and watcher
|
||||||
|
// notifications — they would reference the wrong stage.
|
||||||
|
if let Some(job) = crate::crdt_state::read_merge_job(&sid)
|
||||||
|
&& job.status == "cancelled"
|
||||||
|
{
|
||||||
|
crate::crdt_state::delete_merge_job(&sid);
|
||||||
|
pool.auto_assign_available_work(&root).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let success = matches!(&report, Ok(r) if r.success);
|
let success = matches!(&report, Ok(r) if r.success);
|
||||||
|
|
||||||
let finished_at = unix_now();
|
let finished_at = unix_now();
|
||||||
|
|||||||
@@ -905,4 +905,92 @@ fn merge_failure_unblock_moves_to_merge_via_crdt() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Story 973: Merge → Coding (abort in-flight merge) ───────────────
|
||||||
|
|
||||||
|
/// AC1 (pure): `Merge + MergeAborted` transitions to `Coding`.
|
||||||
|
#[test]
|
||||||
|
fn merge_aborted_returns_to_coding() {
|
||||||
|
let s = Stage::Merge {
|
||||||
|
feature_branch: fb("feature/story-73"),
|
||||||
|
commits_ahead: nz(2),
|
||||||
|
};
|
||||||
|
let result = transition(s, PipelineEvent::MergeAborted).unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(result, Stage::Coding),
|
||||||
|
"Merge + MergeAborted should return to Coding, got: {result:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AC1 (CRDT): set stage to `Merge`, apply `MergeAborted`, assert CRDT stage is `coding`.
|
||||||
|
#[test]
|
||||||
|
fn merge_aborted_moves_to_coding_via_crdt() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
|
||||||
|
let story_id = "99973_story_merge_aborted";
|
||||||
|
crate::db::write_item_with_content(
|
||||||
|
story_id,
|
||||||
|
"merge",
|
||||||
|
"---\nname: Merge Aborted Test\n---\n# Story\n",
|
||||||
|
crate::db::ItemMeta::named("Merge Aborted Test"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let fired = super::apply::apply_transition(story_id, PipelineEvent::MergeAborted, None)
|
||||||
|
.expect("Merge + MergeAborted should succeed");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(fired.before, Stage::Merge { .. }),
|
||||||
|
"fired.before should be Merge: {:?}",
|
||||||
|
fired.before
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(fired.after, Stage::Coding),
|
||||||
|
"fired.after should be Coding: {:?}",
|
||||||
|
fired.after
|
||||||
|
);
|
||||||
|
|
||||||
|
let item = read_typed(story_id)
|
||||||
|
.expect("CRDT read should succeed")
|
||||||
|
.expect("item should exist");
|
||||||
|
assert_eq!(
|
||||||
|
item.stage.dir_name(),
|
||||||
|
"coding",
|
||||||
|
"CRDT stage should be coding after Merge + MergeAborted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AC1 (move_story): `move_story_to_stage` with target "current" on a Merge story succeeds.
|
||||||
|
#[test]
|
||||||
|
fn move_story_merge_to_current_succeeds() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
|
||||||
|
let story_id = "99973_story_move_merge_to_current";
|
||||||
|
crate::db::write_item_with_content(
|
||||||
|
story_id,
|
||||||
|
"merge",
|
||||||
|
"---\nname: Move Merge To Current\n---\n",
|
||||||
|
crate::db::ItemMeta::named("Move Merge To Current"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = crate::agents::lifecycle::move_story_to_stage(story_id, "current");
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"move_story_to_stage(merge → current) should succeed: {result:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (from, to) = result.unwrap();
|
||||||
|
assert_eq!(from, "merge", "from_stage should be 'merge'");
|
||||||
|
assert_eq!(to, "current", "to_stage should be 'current'");
|
||||||
|
|
||||||
|
let item = read_typed(story_id)
|
||||||
|
.expect("CRDT read should succeed")
|
||||||
|
.expect("item should exist");
|
||||||
|
assert!(
|
||||||
|
matches!(item.stage, Stage::Coding),
|
||||||
|
"story should be in Coding after move_story_to_stage(merge → current): {:?}",
|
||||||
|
item.stage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── ProjectionError Display ─────────────────────────────────────────
|
// ── ProjectionError Display ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ pub enum PipelineEvent {
|
|||||||
FixupRequested,
|
FixupRequested,
|
||||||
/// Story 972: user sends a MergeFailure story back to Qa for re-review.
|
/// Story 972: user sends a MergeFailure story back to Qa for re-review.
|
||||||
ReQueuedForQa,
|
ReQueuedForQa,
|
||||||
|
/// Story 973: user aborts an in-flight merge, sending the story back to Coding.
|
||||||
|
MergeAborted,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Per-node execution events ───────────────────────────────────────────────
|
// ── Per-node execution events ───────────────────────────────────────────────
|
||||||
@@ -117,6 +119,7 @@ pub fn event_label(e: &PipelineEvent) -> &'static str {
|
|||||||
PipelineEvent::MergemasterAttempted => "MergemasterAttempted",
|
PipelineEvent::MergemasterAttempted => "MergemasterAttempted",
|
||||||
PipelineEvent::FixupRequested => "FixupRequested",
|
PipelineEvent::FixupRequested => "FixupRequested",
|
||||||
PipelineEvent::ReQueuedForQa => "ReQueuedForQa",
|
PipelineEvent::ReQueuedForQa => "ReQueuedForQa",
|
||||||
|
PipelineEvent::MergeAborted => "MergeAborted",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +313,9 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
|||||||
// ── ReQueuedForQa: MergeFailure → Qa (re-review) ────────────────
|
// ── ReQueuedForQa: MergeFailure → Qa (re-review) ────────────────
|
||||||
(MergeFailure { .. }, ReQueuedForQa) => Ok(Qa),
|
(MergeFailure { .. }, ReQueuedForQa) => Ok(Qa),
|
||||||
|
|
||||||
|
// ── MergeAborted: Merge → Coding (abort in-flight merge) ─────────
|
||||||
|
(Merge { .. }, MergeAborted) => 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