huskies: merge 892
This commit is contained in:
@@ -133,6 +133,7 @@ pub fn move_story_to_done(story_id: &str) -> Result<(), String> {
|
|||||||
Stage::Merge { .. } => PipelineEvent::MergeSucceeded {
|
Stage::Merge { .. } => PipelineEvent::MergeSucceeded {
|
||||||
merge_commit: GitSha("accepted".to_string()),
|
merge_commit: GitSha("accepted".to_string()),
|
||||||
},
|
},
|
||||||
|
Stage::MergeFailure { .. } => PipelineEvent::Accepted,
|
||||||
Stage::Coding | Stage::Qa | Stage::Backlog => PipelineEvent::Close,
|
Stage::Coding | Stage::Qa | Stage::Backlog => PipelineEvent::Close,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use crate::agents::{feature_branch_has_unmerged_changes, move_story_to_done};
|
use crate::agents::{feature_branch_has_unmerged_changes, move_story_to_done};
|
||||||
use crate::http::context::AppContext;
|
use crate::http::context::AppContext;
|
||||||
|
use crate::pipeline_state::{Stage, read_typed};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
pub(crate) fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
pub(crate) fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
@@ -12,9 +13,18 @@ pub(crate) fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String
|
|||||||
|
|
||||||
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
||||||
|
|
||||||
|
// Determine whether the story is in MergeFailure (manual-recovery path).
|
||||||
|
// For MergeFailure, the feature branch deliberately has unmerged code — the
|
||||||
|
// operator is accepting the story without a clean merge, so skip the check.
|
||||||
|
let in_merge_failure = read_typed(story_id)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|item| matches!(item.stage, Stage::MergeFailure { .. }))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Bug 226: Refuse to accept if the feature branch has unmerged code.
|
// Bug 226: Refuse to accept if the feature branch has unmerged code.
|
||||||
// The code must be squash-merged via merge_agent_work first.
|
// The code must be squash-merged via merge_agent_work first.
|
||||||
if feature_branch_has_unmerged_changes(&project_root, story_id) {
|
if !in_merge_failure && feature_branch_has_unmerged_changes(&project_root, story_id) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Cannot accept story '{story_id}': feature branch 'feature/story-{story_id}' \
|
"Cannot accept story '{story_id}': feature branch 'feature/story-{story_id}' \
|
||||||
has unmerged changes. Use merge_agent_work to squash-merge the code into \
|
has unmerged changes. Use merge_agent_work to squash-merge the code into \
|
||||||
|
|||||||
@@ -828,4 +828,70 @@ fn repeated_merge_failure_apply_transition_no_error_no_duplicate_notification()
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Story 892: MergeFailure → Done (manual recovery) ───────────────
|
||||||
|
|
||||||
|
/// Regression test (story 892): `accept_story` on a story in `MergeFailure`
|
||||||
|
/// transitions it to `Done` and emits a `TransitionFired` event.
|
||||||
|
#[test]
|
||||||
|
fn merge_failure_accept_pure_transition() {
|
||||||
|
let s = Stage::MergeFailure {
|
||||||
|
reason: "conflicts unresolvable".into(),
|
||||||
|
};
|
||||||
|
let result = transition(s, PipelineEvent::Accepted).unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(result, Stage::Done { .. }),
|
||||||
|
"MergeFailure + Accepted should yield Done, got: {result:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression test (story 892): `apply_transition` on a CRDT-stored `MergeFailure`
|
||||||
|
/// story moves it to `Done` and the emitted `TransitionFired` event is present.
|
||||||
|
#[test]
|
||||||
|
fn merge_failure_accept_moves_to_done_via_crdt() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
|
||||||
|
let story_id = "99892_story_merge_failure_accept";
|
||||||
|
crate::db::write_item_with_content(
|
||||||
|
story_id,
|
||||||
|
"4_merge_failure",
|
||||||
|
"---\nname: MergeFailure Accept Test\n---\n# Story\n",
|
||||||
|
crate::db::ItemMeta::from_yaml("---\nname: MergeFailure Accept Test\n---\n# Story\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let fired = super::apply::apply_transition(story_id, PipelineEvent::Accepted, None)
|
||||||
|
.expect("MergeFailure + Accepted should succeed");
|
||||||
|
|
||||||
|
// The before-stage was MergeFailure.
|
||||||
|
assert!(
|
||||||
|
matches!(fired.before, Stage::MergeFailure { .. }),
|
||||||
|
"fired.before should be MergeFailure: {:?}",
|
||||||
|
fired.before
|
||||||
|
);
|
||||||
|
|
||||||
|
// The after-stage is Done.
|
||||||
|
assert!(
|
||||||
|
matches!(fired.after, Stage::Done { .. }),
|
||||||
|
"fired.after should be Done: {:?}",
|
||||||
|
fired.after
|
||||||
|
);
|
||||||
|
|
||||||
|
// TransitionFired carries the Accepted event.
|
||||||
|
assert!(
|
||||||
|
matches!(fired.event, PipelineEvent::Accepted),
|
||||||
|
"fired.event should be Accepted: {:?}",
|
||||||
|
fired.event
|
||||||
|
);
|
||||||
|
|
||||||
|
// CRDT reflects 5_done.
|
||||||
|
let item = read_typed(story_id)
|
||||||
|
.expect("CRDT read should succeed")
|
||||||
|
.expect("item should exist");
|
||||||
|
assert_eq!(
|
||||||
|
item.stage.dir_name(),
|
||||||
|
"5_done",
|
||||||
|
"CRDT stage should be 5_done after MergeFailure + Accepted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── ProjectionError Display ─────────────────────────────────────────
|
// ── ProjectionError Display ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -164,6 +164,14 @@ pub fn transition(state: Stage, event: PipelineEvent) -> Result<Stage, Transitio
|
|||||||
reason: ArchiveReason::Completed,
|
reason: ArchiveReason::Completed,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// ── MergeFailure → Done (manual recovery) ───────────────────────
|
||||||
|
// Allows a human operator to accept a story whose merge permanently
|
||||||
|
// failed, marking it Done without going through the normal merge path.
|
||||||
|
(MergeFailure { .. }, Accepted) => Ok(Done {
|
||||||
|
merged_at: now,
|
||||||
|
merge_commit: GitSha("manual".to_string()),
|
||||||
|
}),
|
||||||
|
|
||||||
// ── Block: any active → Blocked ──────────────────────────────
|
// ── Block: any active → Blocked ──────────────────────────────
|
||||||
(Backlog, Block { reason })
|
(Backlog, Block { reason })
|
||||||
| (Coding, Block { reason })
|
| (Coding, Block { reason })
|
||||||
|
|||||||
Reference in New Issue
Block a user