huskies: merge 998

This commit is contained in:
dave
2026-05-13 19:29:08 +00:00
parent 75dc1fc15a
commit bbdee1239b
9 changed files with 427 additions and 586 deletions
@@ -50,46 +50,6 @@ pub(super) fn is_story_blocked(story_id: &str) -> bool {
.unwrap_or(false)
}
/// Return `true` if the story's merge failure is a git content-conflict
/// (`Stage::MergeFailure { kind: ConflictDetected(_), .. }`).
///
/// Used by the auto-assigner to decide whether to spawn mergemaster automatically.
/// The typed kind is carried by the CRDT projection layer (which reads
/// `ContentKey::GateOutput` on projection to reconstruct the kind on restart),
/// so no direct content-store access is needed here (story 982).
pub(super) fn has_content_conflict_failure(story_id: &str) -> bool {
crate::pipeline_state::read_typed(story_id)
.ok()
.flatten()
.map(|item| {
matches!(
item.stage,
crate::pipeline_state::Stage::MergeFailure {
kind: crate::pipeline_state::MergeFailureKind::ConflictDetected(_),
..
}
)
})
.unwrap_or(false)
}
/// Return `true` if the story is in `Stage::MergeFailureFinal`.
///
/// Story 945: `Stage::MergeFailureFinal` is the single source of truth —
/// the legacy `mergemaster_attempted: bool` CRDT register has been deleted.
/// Used to prevent the auto-assigner from repeatedly spawning mergemaster for
/// the same story after a failed mergemaster session.
pub(super) fn has_mergemaster_attempted(story_id: &str) -> bool {
crate::crdt_state::read_item(story_id)
.map(|view| {
matches!(
view.stage(),
crate::pipeline_state::Stage::MergeFailureFinal { .. }
)
})
.unwrap_or(false)
}
/// Return `true` if the story has any `depends_on` entries that are not yet in
/// `5_done` or `6_archived`. Reads dependency state from the CRDT (story 929).
pub(super) fn has_unmet_dependencies(story_id: &str) -> bool {
@@ -345,81 +305,4 @@ mod tests {
let archived_deps = check_archived_dependencies("503_story_waiting");
assert!(archived_deps.is_empty());
}
// ── Story 982: typed MergeFailureKind — has_content_conflict_failure ──────
/// AC2 (story 982): `has_content_conflict_failure` returns `true` when the
/// story is in `Stage::MergeFailure { kind: ConflictDetected(_), .. }`.
/// The test seeds the stage via `transition_to_merge_failure` (no direct
/// content-store or MergeJob writes in the test body).
#[test]
fn has_content_conflict_failure_true_for_conflict_detected_kind() {
crate::crdt_state::init_for_test();
crate::db::ensure_content_store();
let story_id = "982_ac2_conflict_detected";
// Seed at Merge stage so the transition is valid.
crate::db::write_item_with_content(
story_id,
"4_merge",
"---\nname: AC2 conflict test\n---\n",
crate::db::ItemMeta::named("AC2 conflict test"),
);
// Transition via the lifecycle helper — internally writes ContentKey::GateOutput
// so the CRDT projection can reconstruct the kind; no content-store writes here.
crate::agents::lifecycle::transition_to_merge_failure(
story_id,
crate::pipeline_state::MergeFailureKind::ConflictDetected(Some(
"CONFLICT (content): server/src/lib.rs".to_string(),
)),
)
.expect("transition should succeed");
// The typed match now drives the predicate — no substring scan.
assert!(
has_content_conflict_failure(story_id),
"has_content_conflict_failure must be true for ConflictDetected kind"
);
// Verify the projected stage carries the typed kind.
let item = crate::pipeline_state::read_typed(story_id)
.unwrap()
.unwrap();
assert!(
matches!(
item.stage,
crate::pipeline_state::Stage::MergeFailure {
kind: crate::pipeline_state::MergeFailureKind::ConflictDetected(_),
..
}
),
"stage must be MergeFailure(ConflictDetected): {:?}",
item.stage
);
}
/// AC2 (story 982): `has_content_conflict_failure` returns `false` when the
/// kind is `GatesFailed` — no mergemaster spawn for gate-only failures.
#[test]
fn has_content_conflict_failure_false_for_gates_failed_kind() {
crate::crdt_state::init_for_test();
crate::db::ensure_content_store();
let story_id = "982_ac2_gates_failed";
crate::db::write_item_with_content(
story_id,
"4_merge",
"---\nname: AC2 gates test\n---\n",
crate::db::ItemMeta::named("AC2 gates test"),
);
crate::agents::lifecycle::transition_to_merge_failure(
story_id,
crate::pipeline_state::MergeFailureKind::GatesFailed(
"error[clippy::unused_variable]".to_string(),
),
)
.expect("transition should succeed");
assert!(
!has_content_conflict_failure(story_id),
"has_content_conflict_failure must be false for GatesFailed kind"
);
}
}