2026-03-28 08:44:27 +00:00
|
|
|
//! Front-matter checks for story files: review holds, blocked state, and merge failures.
|
|
|
|
|
|
2026-04-30 16:36:18 +00:00
|
|
|
/// Read the optional `agent:` pin for a story.
|
2026-03-28 08:44:27 +00:00
|
|
|
///
|
2026-04-30 16:36:18 +00:00
|
|
|
/// After story 871 the agent assignment lives in the CRDT typed register
|
|
|
|
|
/// (`PipelineItemView.agent`), not the YAML front matter. We check the CRDT
|
|
|
|
|
/// first; falling back to legacy YAML parsing keeps behaviour intact for any
|
|
|
|
|
/// stories whose CRDT entry doesn't yet have the field set.
|
2026-05-13 13:17:46 +00:00
|
|
|
pub(super) fn read_story_front_matter_agent(story_id: &str) -> Option<String> {
|
2026-05-12 19:03:51 +01:00
|
|
|
// Story 929: agent name comes from the CRDT register. The previous
|
|
|
|
|
// YAML fallback is gone — post-891 every story has its CRDT entry,
|
|
|
|
|
// and any story without one is treated as having no pinned agent.
|
2026-05-13 11:58:50 +00:00
|
|
|
crate::crdt_state::read_item(story_id).and_then(|w| w.agent().map(|a| a.to_string()))
|
2026-03-28 08:44:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-13 06:05:01 +00:00
|
|
|
/// Return `true` if the story is in `Stage::ReviewHold`.
|
2026-05-12 14:43:27 +00:00
|
|
|
///
|
2026-05-13 06:05:01 +00:00
|
|
|
/// Story 945: `Stage::ReviewHold { resume_to, reason }` is the single source
|
|
|
|
|
/// of truth — the legacy `review_hold: bool` CRDT register has been deleted.
|
|
|
|
|
/// The auto-assigner uses this to keep human-QA items / spikes parked after
|
|
|
|
|
/// gates pass until a reviewer explicitly clears the hold (e.g. via
|
|
|
|
|
/// `tool_approve_qa`).
|
2026-05-13 13:17:46 +00:00
|
|
|
pub(super) fn has_review_hold(story_id: &str) -> bool {
|
2026-05-12 19:49:36 +01:00
|
|
|
crate::crdt_state::read_item(story_id)
|
2026-05-13 06:05:01 +00:00
|
|
|
.map(|w| w.stage().is_review_hold())
|
2026-03-28 08:44:27 +00:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 14:43:27 +00:00
|
|
|
/// Return `true` if the story is blocked via the typed `Stage::Blocked` or
|
|
|
|
|
/// `Stage::MergeFailure` variant (or the legacy `Archived(Blocked)` state).
|
|
|
|
|
///
|
|
|
|
|
/// The typed pipeline stage register is the only source consulted — the legacy
|
|
|
|
|
/// `blocked: true` YAML front-matter field is no longer checked.
|
2026-05-13 13:17:46 +00:00
|
|
|
pub(super) fn is_story_blocked(story_id: &str) -> bool {
|
2026-05-12 14:43:27 +00:00
|
|
|
crate::pipeline_state::read_typed(story_id)
|
2026-03-28 08:44:27 +00:00
|
|
|
.ok()
|
2026-05-12 14:43:27 +00:00
|
|
|
.flatten()
|
|
|
|
|
.map(|item| item.stage.is_blocked())
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 15:30:03 +00:00
|
|
|
/// Return `true` if the story's merge failure is a git content-conflict
|
|
|
|
|
/// (`Stage::MergeFailure { kind: ConflictDetected(_), .. }`).
|
2026-05-12 14:43:27 +00:00
|
|
|
///
|
|
|
|
|
/// Used by the auto-assigner to decide whether to spawn mergemaster automatically.
|
2026-05-13 15:30:03 +00:00
|
|
|
/// 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).
|
2026-05-13 13:17:46 +00:00
|
|
|
pub(super) fn has_content_conflict_failure(story_id: &str) -> bool {
|
2026-05-13 15:30:03 +00:00
|
|
|
crate::pipeline_state::read_typed(story_id)
|
2026-05-12 14:43:27 +00:00
|
|
|
.ok()
|
|
|
|
|
.flatten()
|
|
|
|
|
.map(|item| {
|
|
|
|
|
matches!(
|
|
|
|
|
item.stage,
|
2026-05-13 15:30:03 +00:00
|
|
|
crate::pipeline_state::Stage::MergeFailure {
|
|
|
|
|
kind: crate::pipeline_state::MergeFailureKind::ConflictDetected(_),
|
|
|
|
|
..
|
|
|
|
|
}
|
2026-05-12 14:43:27 +00:00
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 06:05:01 +00:00
|
|
|
/// Return `true` if the story is in `Stage::MergeFailureFinal`.
|
2026-05-12 14:43:27 +00:00
|
|
|
///
|
2026-05-13 06:05:01 +00:00
|
|
|
/// Story 945: `Stage::MergeFailureFinal` is the single source of truth —
|
|
|
|
|
/// the legacy `mergemaster_attempted: bool` CRDT register has been deleted.
|
2026-05-12 14:43:27 +00:00
|
|
|
/// Used to prevent the auto-assigner from repeatedly spawning mergemaster for
|
2026-05-13 06:05:01 +00:00
|
|
|
/// the same story after a failed mergemaster session.
|
2026-05-13 13:17:46 +00:00
|
|
|
pub(super) fn has_mergemaster_attempted(story_id: &str) -> bool {
|
2026-05-12 14:43:27 +00:00
|
|
|
crate::crdt_state::read_item(story_id)
|
2026-05-13 06:05:01 +00:00
|
|
|
.map(|view| view.stage().is_mergemaster_attempted())
|
2026-03-28 08:44:27 +00:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 21:43:29 +00:00
|
|
|
/// Return `true` if the story has any `depends_on` entries that are not yet in
|
2026-05-12 19:14:54 +01:00
|
|
|
/// `5_done` or `6_archived`. Reads dependency state from the CRDT (story 929).
|
2026-05-13 13:17:46 +00:00
|
|
|
pub(super) fn has_unmet_dependencies(story_id: &str) -> bool {
|
2026-05-12 19:14:54 +01:00
|
|
|
!crate::crdt_state::check_unmet_deps_crdt(story_id).is_empty()
|
2026-04-04 21:43:29 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 18:27:25 +00:00
|
|
|
/// Return the list of dependency story numbers that are in `6_archived` (satisfied
|
2026-05-12 19:14:54 +01:00
|
|
|
/// via archive rather than via a clean `5_done` completion). Reads from the CRDT
|
|
|
|
|
/// (story 929).
|
2026-05-13 13:17:46 +00:00
|
|
|
pub(super) fn check_archived_dependencies(story_id: &str) -> Vec<u32> {
|
2026-05-12 19:14:54 +01:00
|
|
|
crate::crdt_state::check_archived_deps_crdt(story_id)
|
2026-04-09 18:27:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-13 06:05:01 +00:00
|
|
|
/// Return `true` if the story is in `Stage::Frozen`.
|
2026-04-29 22:12:23 +00:00
|
|
|
///
|
2026-05-13 06:05:01 +00:00
|
|
|
/// Story 945: `Stage::Frozen { resume_to }` is the single source of truth —
|
|
|
|
|
/// the legacy `frozen: bool` CRDT register has been deleted. Frozen stories
|
|
|
|
|
/// are skipped by the auto-assigner until `Unfreeze` returns them to
|
|
|
|
|
/// `resume_to`.
|
2026-05-13 13:17:46 +00:00
|
|
|
pub(super) fn is_story_frozen(story_id: &str) -> bool {
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::read_item(story_id)
|
2026-05-13 06:05:01 +00:00
|
|
|
.map(|view| view.stage().is_frozen())
|
2026-04-15 17:57:56 +00:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 08:44:27 +00:00
|
|
|
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
2026-05-12 14:43:27 +00:00
|
|
|
// ── has_review_hold ───────────────────────────────────────────────────────
|
|
|
|
|
|
2026-03-28 08:44:27 +00:00
|
|
|
#[test]
|
2026-05-12 19:49:36 +01:00
|
|
|
fn has_review_hold_returns_true_when_flag_set() {
|
2026-05-12 14:43:27 +00:00
|
|
|
crate::crdt_state::init_for_test();
|
|
|
|
|
crate::db::ensure_content_store();
|
2026-05-13 06:05:01 +00:00
|
|
|
// Story 945: review_hold is now a typed Stage variant, seeded via
|
|
|
|
|
// the wire-form stage register directly.
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:49:36 +01:00
|
|
|
"890_spike_held",
|
2026-05-13 06:05:01 +00:00
|
|
|
"review_hold",
|
2026-05-12 19:49:36 +01:00
|
|
|
Some("Held Spike"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
2026-05-12 14:43:27 +00:00
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
assert!(has_review_hold("890_spike_held"));
|
2026-05-12 14:43:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-05-12 19:49:36 +01:00
|
|
|
fn has_review_hold_returns_false_when_flag_unset() {
|
2026-05-12 14:43:27 +00:00
|
|
|
crate::crdt_state::init_for_test();
|
2026-04-10 14:56:13 +00:00
|
|
|
crate::db::ensure_content_store();
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 14:43:27 +00:00
|
|
|
"890_spike_active_qa",
|
2026-04-10 14:56:13 +00:00
|
|
|
"3_qa",
|
2026-05-12 19:49:36 +01:00
|
|
|
Some("Active QA Spike"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
2026-04-10 14:56:13 +00:00
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
assert!(!has_review_hold("890_spike_active_qa"));
|
2026-03-28 08:44:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-05-12 14:43:27 +00:00
|
|
|
fn has_review_hold_returns_false_when_story_unknown() {
|
2026-05-13 13:17:46 +00:00
|
|
|
assert!(!has_review_hold("99_spike_missing"));
|
2026-03-28 08:44:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 14:43:27 +00:00
|
|
|
// ── is_story_blocked — regression: typed stage is sole authority ──────────
|
|
|
|
|
|
2026-03-28 08:44:27 +00:00
|
|
|
#[test]
|
2026-05-12 14:43:27 +00:00
|
|
|
fn is_story_blocked_set_via_typed_stage_returns_true() {
|
|
|
|
|
crate::crdt_state::init_for_test();
|
|
|
|
|
crate::db::ensure_content_store();
|
|
|
|
|
crate::db::write_item_with_content(
|
|
|
|
|
"890_story_blocked_set",
|
|
|
|
|
"2_blocked",
|
|
|
|
|
"---\nname: Blocked Story\n---\n",
|
|
|
|
|
crate::db::ItemMeta::named("Blocked Story"),
|
|
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
assert!(is_story_blocked("890_story_blocked_set"));
|
2026-05-12 14:43:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn is_story_blocked_cleared_via_typed_stage_returns_false() {
|
|
|
|
|
crate::crdt_state::init_for_test();
|
|
|
|
|
crate::db::ensure_content_store();
|
|
|
|
|
// First set to blocked.
|
|
|
|
|
crate::db::write_item_with_content(
|
|
|
|
|
"890_story_blocked_clear",
|
|
|
|
|
"2_blocked",
|
|
|
|
|
"---\nname: Clearable Story\n---\n",
|
|
|
|
|
crate::db::ItemMeta::named("Clearable Story"),
|
|
|
|
|
);
|
|
|
|
|
// Then clear by transitioning to an active stage.
|
|
|
|
|
crate::db::write_item_with_content(
|
|
|
|
|
"890_story_blocked_clear",
|
|
|
|
|
"2_current",
|
|
|
|
|
"---\nname: Clearable Story\n---\n",
|
|
|
|
|
crate::db::ItemMeta::named("Clearable Story"),
|
|
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
assert!(!is_story_blocked("890_story_blocked_clear"));
|
2026-03-28 08:44:27 +00:00
|
|
|
}
|
2026-04-04 21:43:29 +00:00
|
|
|
|
2026-05-12 14:43:27 +00:00
|
|
|
#[test]
|
|
|
|
|
fn is_story_blocked_stale_yaml_is_ignored() {
|
|
|
|
|
crate::crdt_state::init_for_test();
|
|
|
|
|
crate::db::ensure_content_store();
|
|
|
|
|
// YAML front matter says `blocked: true`, but the typed CRDT stage is backlog.
|
|
|
|
|
// After removing the YAML fallback, the function must return false.
|
|
|
|
|
crate::db::write_item_with_content(
|
|
|
|
|
"890_story_stale_yaml",
|
|
|
|
|
"1_backlog",
|
|
|
|
|
"---\nname: Stale\nblocked: true\n---\n",
|
|
|
|
|
crate::db::ItemMeta::named("Stale"),
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
2026-05-13 13:17:46 +00:00
|
|
|
!is_story_blocked("890_story_stale_yaml"),
|
2026-05-12 14:43:27 +00:00
|
|
|
"stale YAML `blocked: true` must not be reported as blocked when typed stage is Backlog"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── has_unmet_dependencies ────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-04 21:43:29 +00:00
|
|
|
#[test]
|
|
|
|
|
fn has_unmet_dependencies_returns_true_when_dep_not_done() {
|
2026-05-12 19:14:54 +01:00
|
|
|
crate::crdt_state::init_for_test();
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:14:54 +01:00
|
|
|
"10_story_blocked",
|
|
|
|
|
"2_current",
|
|
|
|
|
Some("Blocked"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
Some("[999]"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
assert!(has_unmet_dependencies("10_story_blocked"));
|
2026-04-04 21:43:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn has_unmet_dependencies_returns_false_when_dep_done() {
|
2026-05-12 19:14:54 +01:00
|
|
|
crate::crdt_state::init_for_test();
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:14:54 +01:00
|
|
|
"999_story_dep",
|
|
|
|
|
"5_done",
|
|
|
|
|
Some("Dep"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:14:54 +01:00
|
|
|
"10_story_ok",
|
|
|
|
|
"2_current",
|
|
|
|
|
Some("Ok"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
Some("[999]"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
assert!(!has_unmet_dependencies("10_story_ok"));
|
2026-04-04 21:43:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn has_unmet_dependencies_returns_false_when_no_deps() {
|
2026-05-12 19:14:54 +01:00
|
|
|
crate::crdt_state::init_for_test();
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:14:54 +01:00
|
|
|
"5_story_free",
|
|
|
|
|
"2_current",
|
|
|
|
|
Some("Free"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
assert!(!has_unmet_dependencies("5_story_free"));
|
2026-04-04 21:43:29 +00:00
|
|
|
}
|
2026-04-09 18:27:25 +00:00
|
|
|
|
|
|
|
|
// ── Bug 503: archived-dep visibility ─────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// check_archived_dependencies returns dep IDs that are in 6_archived.
|
|
|
|
|
#[test]
|
|
|
|
|
fn check_archived_dependencies_returns_archived_ids() {
|
2026-05-12 19:14:54 +01:00
|
|
|
crate::crdt_state::init_for_test();
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:14:54 +01:00
|
|
|
"500_spike_crdt",
|
|
|
|
|
"6_archived",
|
|
|
|
|
Some("CRDT Spike"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:14:54 +01:00
|
|
|
"503_story_dependent",
|
|
|
|
|
"1_backlog",
|
|
|
|
|
Some("Dependent"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
Some("[500]"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
let archived_deps = check_archived_dependencies("503_story_dependent");
|
2026-04-09 18:27:25 +00:00
|
|
|
assert_eq!(archived_deps, vec![500]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// check_archived_dependencies returns empty when dep is in 5_done (not archived).
|
|
|
|
|
#[test]
|
|
|
|
|
fn check_archived_dependencies_empty_when_dep_in_done() {
|
2026-05-12 19:14:54 +01:00
|
|
|
crate::crdt_state::init_for_test();
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:14:54 +01:00
|
|
|
"490_story_done",
|
|
|
|
|
"5_done",
|
|
|
|
|
Some("Done"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2026-05-12 22:31:59 +01:00
|
|
|
crate::crdt_state::write_item_str(
|
2026-05-12 19:14:54 +01:00
|
|
|
"503_story_waiting",
|
|
|
|
|
"1_backlog",
|
|
|
|
|
Some("Waiting"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
Some("[490]"),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
2026-05-13 13:17:46 +00:00
|
|
|
let archived_deps = check_archived_dependencies("503_story_waiting");
|
2026-04-09 18:27:25 +00:00
|
|
|
assert!(archived_deps.is_empty());
|
|
|
|
|
}
|
2026-05-13 15:30:03 +00:00
|
|
|
|
|
|
|
|
// ── 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"
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-28 08:44:27 +00:00
|
|
|
}
|