huskies: merge 982

This commit is contained in:
dave
2026-05-13 15:30:03 +00:00
parent e6d051d016
commit 91fbad568a
15 changed files with 357 additions and 117 deletions
@@ -39,34 +39,26 @@ pub(super) fn is_story_blocked(story_id: &str) -> bool {
.unwrap_or(false)
}
/// Return `true` if the story's merge failure contains a git content-conflict
/// marker (`"Merge conflict"` or `"CONFLICT (content):"`).
/// 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 stage register is consulted first; the CRDT content store is then
/// scanned for conflict markers (the projection layer does not carry the reason
/// string). No YAML front-matter parsing is performed.
/// 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 {
let is_merge_failure = crate::pipeline_state::read_typed(story_id)
crate::pipeline_state::read_typed(story_id)
.ok()
.flatten()
.map(|item| {
matches!(
item.stage,
crate::pipeline_state::Stage::MergeFailure { .. }
crate::pipeline_state::Stage::MergeFailure {
kind: crate::pipeline_state::MergeFailureKind::ConflictDetected(_),
..
}
)
})
.unwrap_or(false);
if !is_merge_failure {
return false;
}
// The projection does not carry the reason string; read the gate output
// (where the merge runner persists the failure message) and scan for
// conflict markers.
crate::db::read_content(crate::db::ContentKey::GateOutput(story_id))
.map(|content| {
content.contains("Merge conflict") || content.contains("CONFLICT (content):")
})
.unwrap_or(false)
}
@@ -337,4 +329,81 @@ 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"
);
}
}