huskies: merge 974

This commit is contained in:
dave
2026-05-13 14:21:49 +00:00
parent 7854fbd78a
commit e5d2465f66
3 changed files with 102 additions and 0 deletions
+57
View File
@@ -344,6 +344,8 @@ fn map_stage_move_to_event(
(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.
(Stage::MergeFailure { .. }, "qa") => Ok(PipelineEvent::ReQueuedForQa), (Stage::MergeFailure { .. }, "qa") => Ok(PipelineEvent::ReQueuedForQa),
// Story 974: reopen a Done story for a post-merge hotfix.
(Stage::Done { .. }, "current") => Ok(PipelineEvent::HotfixRequested),
( (
Stage::Archived { Stage::Archived {
reason: ArchiveReason::Blocked { .. }, reason: ArchiveReason::Blocked { .. },
@@ -809,6 +811,61 @@ mod tests {
); );
} }
// ── Story 974: Done → Coding hotfix tests ────────────────────────────────
/// AC1 (story 974): move_story_to_stage to "current" from Done succeeds
/// and lands the story in Coding stage so auto-assign can pick it up.
#[test]
fn move_story_to_stage_from_done_lands_in_coding() {
crate::crdt_state::init_for_test();
crate::db::ensure_content_store();
let story_id = "99974_story_hotfix_done_to_coding";
crate::db::write_item_with_content(
story_id,
"done",
"---\nname: Hotfix Test\n---\n",
crate::db::ItemMeta::named("Hotfix Test"),
);
move_story_to_stage(story_id, "current").expect("move from Done to Coding 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 hotfix move"
);
}
/// AC1 (story 974): retry_count is reset to 0 after Done→Coding so the
/// fresh coder session starts clean.
#[test]
fn move_story_from_done_to_coding_resets_retry_count() {
crate::crdt_state::init_for_test();
crate::db::ensure_content_store();
let story_id = "99975_story_hotfix_retry_count";
crate::db::write_item_with_content(
story_id,
"done",
"---\nname: Hotfix Retry Count\n---\n",
crate::db::ItemMeta::named("Hotfix Retry Count"),
);
move_story_to_stage(story_id, "current").expect("move from Done to Coding must succeed");
let retry_count = crate::crdt_state::read_item(story_id)
.expect("CRDT item must exist")
.retry_count();
assert_eq!(
retry_count, 0,
"retry_count must be 0 after hotfix move so coder starts fresh"
);
}
/// 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]
+36
View File
@@ -993,4 +993,40 @@ fn move_story_merge_to_current_succeeds() {
); );
} }
// ── Story 974: Done → Coding (hotfix) ─────────────────────────────
#[test]
fn hotfix_requested_from_done_lands_in_coding() {
let done = Stage::Done {
merged_at: chrono::Utc::now(),
merge_commit: sha("abc123"),
};
let result = transition(done, PipelineEvent::HotfixRequested).unwrap();
assert!(
matches!(result, Stage::Coding),
"Done + HotfixRequested must land in Coding; got: {:?}",
result
);
}
#[test]
fn hotfix_requested_rejected_from_non_done_stages() {
for stage in [
Stage::Backlog,
Stage::Coding,
Stage::Qa,
Stage::Merge {
feature_branch: fb("feature/story-1"),
commits_ahead: nz(1),
},
] {
let result = transition(stage.clone(), PipelineEvent::HotfixRequested);
assert!(
result.is_err(),
"HotfixRequested must be rejected from {:?}",
stage
);
}
}
// ── ProjectionError Display ───────────────────────────────────────── // ── ProjectionError Display ─────────────────────────────────────────
+9
View File
@@ -74,6 +74,8 @@ pub enum PipelineEvent {
ReQueuedForQa, ReQueuedForQa,
/// Story 973: user aborts an in-flight merge, sending the story back to Coding. /// Story 973: user aborts an in-flight merge, sending the story back to Coding.
MergeAborted, MergeAborted,
/// Story 974: user re-opens a Done story for a post-merge hotfix, sending it back to Coding.
HotfixRequested,
} }
// ── Per-node execution events ─────────────────────────────────────────────── // ── Per-node execution events ───────────────────────────────────────────────
@@ -120,6 +122,7 @@ pub fn event_label(e: &PipelineEvent) -> &'static str {
PipelineEvent::FixupRequested => "FixupRequested", PipelineEvent::FixupRequested => "FixupRequested",
PipelineEvent::ReQueuedForQa => "ReQueuedForQa", PipelineEvent::ReQueuedForQa => "ReQueuedForQa",
PipelineEvent::MergeAborted => "MergeAborted", PipelineEvent::MergeAborted => "MergeAborted",
PipelineEvent::HotfixRequested => "HotfixRequested",
} }
} }
@@ -316,6 +319,12 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
// ── MergeAborted: Merge → Coding (abort in-flight merge) ───────── // ── MergeAborted: Merge → Coding (abort in-flight merge) ─────────
(Merge { .. }, MergeAborted) => Ok(Coding), (Merge { .. }, MergeAborted) => Ok(Coding),
// ── HotfixRequested: Done → Coding (post-merge hotfix) ───────────
// Allows reopening a completed story so a coder can apply a hotfix.
// A fresh feature branch is forked from master when auto-assign spawns
// the coder.
(Done { .. }, HotfixRequested) => 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 }),