huskies: merge 873

This commit is contained in:
dave
2026-04-29 16:05:54 +00:00
parent cf35027b5a
commit 8a7e1aa036
6 changed files with 104 additions and 1 deletions
@@ -85,6 +85,7 @@ impl AgentPool {
crate::db::write_content(story_id, &updated); crate::db::write_content(story_id, &updated);
crate::db::write_item_with_content(story_id, "4_merge", &updated); crate::db::write_item_with_content(story_id, "4_merge", &updated);
} }
crate::crdt_state::set_mergemaster_attempted(story_id, true);
if let Err(e) = self if let Err(e) = self
.start_agent(project_root, story_id, Some(&agent_name), None, None) .start_agent(project_root, story_id, Some(&agent_name), None, None)
.await .await
+1 -1
View File
@@ -53,7 +53,7 @@ pub use types::{
}; };
pub use write::{ pub use write::{
bump_retry_count, migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id, bump_retry_count, migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id,
set_agent, set_depends_on, set_qa_mode, set_retry_count, write_item, set_agent, set_depends_on, set_mergemaster_attempted, set_qa_mode, set_retry_count, write_item,
}; };
#[cfg(test)] #[cfg(test)]
+6
View File
@@ -320,6 +320,11 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
_ => None, _ => None,
}; };
let mergemaster_attempted = match item.mergemaster_attempted.view() {
JsonValue::Bool(b) => Some(b),
_ => None,
};
Some(PipelineItemView { Some(PipelineItemView {
story_id, story_id,
stage, stage,
@@ -332,6 +337,7 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
claimed_at, claimed_at,
merged_at, merged_at,
qa_mode, qa_mode,
mergemaster_attempted,
}) })
} }
+7
View File
@@ -84,6 +84,10 @@ pub struct PipelineItemCrdt {
/// QA mode override for this item: `"server"`, `"agent"`, or `"human"`. /// QA mode override for this item: `"server"`, `"agent"`, or `"human"`.
/// Empty string means "use the project default". /// Empty string means "use the project default".
pub qa_mode: LwwRegisterCrdt<String>, pub qa_mode: LwwRegisterCrdt<String>,
/// Set to `true` when the auto-assigner has already spawned a mergemaster
/// session for a content-conflict failure. Prevents repeated auto-spawns
/// across restarts. Written as `false` (not removed) when cleared.
pub mergemaster_attempted: LwwRegisterCrdt<bool>,
} }
/// CRDT node that holds a single peer's presence entry. /// CRDT node that holds a single peer's presence entry.
@@ -128,6 +132,9 @@ pub struct PipelineItemView {
/// QA mode override from the CRDT register: `"server"`, `"agent"`, or `"human"`. /// QA mode override from the CRDT register: `"server"`, `"agent"`, or `"human"`.
/// `None` means the register is unset (use project default). /// `None` means the register is unset (use project default).
pub qa_mode: Option<String>, pub qa_mode: Option<String>,
/// Whether the auto-assigner has already spawned a mergemaster session for
/// this item. `None` means the register has never been set (treat as false).
pub mergemaster_attempted: Option<bool>,
} }
/// A snapshot of a single node presence entry derived from the CRDT document. /// A snapshot of a single node presence entry derived from the CRDT document.
+83
View File
@@ -265,6 +265,31 @@ pub fn set_qa_mode(story_id: &str, mode: Option<QaMode>) -> bool {
true true
} }
/// Set the `mergemaster_attempted` CRDT flag for a pipeline item.
///
/// Passing `true` records that a mergemaster session has been spawned for this
/// item, preventing repeated auto-spawns across restarts.
/// Passing `false` explicitly writes `false` (does not remove the register) so
/// the cleared state is distinguishable from an unset register and survives
/// CRDT replay correctly.
///
/// Returns `true` if the item was found and the op was applied, `false` otherwise.
pub fn set_mergemaster_attempted(story_id: &str, value: bool) -> bool {
let Some(state_mutex) = get_crdt() else {
return false;
};
let Ok(mut state) = state_mutex.lock() else {
return false;
};
let Some(&idx) = state.index.get(story_id) else {
return false;
};
apply_and_persist(&mut state, |s| {
s.crdt.doc.items[idx].mergemaster_attempted.set(value)
});
true
}
/// Write a pipeline item state through CRDT operations. /// Write a pipeline item state through CRDT operations.
/// ///
/// If the item exists, updates its registers. If not, inserts a new item /// If the item exists, updates its registers. If not, inserts a new item
@@ -368,6 +393,7 @@ pub fn write_item(
"claimed_at": claimed_at.unwrap_or(0.0), "claimed_at": claimed_at.unwrap_or(0.0),
"merged_at": merged_at.unwrap_or(0.0), "merged_at": merged_at.unwrap_or(0.0),
"qa_mode": "", "qa_mode": "",
"mergemaster_attempted": false,
}) })
.into(); .into();
@@ -394,6 +420,7 @@ pub fn write_item(
item.claimed_at.advance_seq(floor); item.claimed_at.advance_seq(floor);
item.merged_at.advance_seq(floor); item.merged_at.advance_seq(floor);
item.qa_mode.advance_seq(floor); item.qa_mode.advance_seq(floor);
item.mergemaster_attempted.advance_seq(floor);
} }
// Broadcast a CrdtEvent for the new item. // Broadcast a CrdtEvent for the new item.
@@ -921,6 +948,62 @@ mod tests {
); );
} }
// ── set_mergemaster_attempted regression tests ───────────────────────────
#[test]
fn set_mergemaster_attempted_true_then_false_flips_register() {
init_for_test();
write_item(
"873_story_mergemaster_flip",
"4_merge",
None,
None,
None,
None,
None,
None,
None,
None,
);
// Set true — register must read back as true.
let ok = set_mergemaster_attempted("873_story_mergemaster_flip", true);
assert!(
ok,
"set_mergemaster_attempted should return true for known item"
);
let view = read_item("873_story_mergemaster_flip").unwrap();
assert_eq!(
view.mergemaster_attempted,
Some(true),
"CRDT register should hold true after setting true"
);
// Set false — register must flip back to false (not unset).
let ok = set_mergemaster_attempted("873_story_mergemaster_flip", false);
assert!(
ok,
"set_mergemaster_attempted(false) should return true for known item"
);
let view = read_item("873_story_mergemaster_flip").unwrap();
assert_eq!(
view.mergemaster_attempted,
Some(false),
"CRDT register should hold false after explicit clear"
);
}
#[test]
fn set_mergemaster_attempted_returns_false_for_unknown_story() {
init_for_test();
let ok = set_mergemaster_attempted("nonexistent_story_mm", true);
assert!(
!ok,
"set_mergemaster_attempted should return false for unknown story_id"
);
}
#[test] #[test]
fn set_qa_mode_returns_false_for_unknown_story() { fn set_qa_mode_returns_false_for_unknown_story() {
init_for_test(); init_for_test();
+6
View File
@@ -206,6 +206,7 @@ mod tests {
claimed_at: None, claimed_at: None,
merged_at: None, merged_at: None,
qa_mode: None, qa_mode: None,
mergemaster_attempted: None,
}; };
let item = PipelineItem::try_from(&view).unwrap(); let item = PipelineItem::try_from(&view).unwrap();
assert_eq!(item.story_id, StoryId("42_story_test".to_string())); assert_eq!(item.story_id, StoryId("42_story_test".to_string()));
@@ -229,6 +230,7 @@ mod tests {
claimed_at: None, claimed_at: None,
merged_at: None, merged_at: None,
qa_mode: None, qa_mode: None,
mergemaster_attempted: None,
}; };
let item = PipelineItem::try_from(&view).unwrap(); let item = PipelineItem::try_from(&view).unwrap();
assert!(matches!(item.stage, Stage::Coding)); assert!(matches!(item.stage, Stage::Coding));
@@ -249,6 +251,7 @@ mod tests {
claimed_at: None, claimed_at: None,
merged_at: None, merged_at: None,
qa_mode: None, qa_mode: None,
mergemaster_attempted: None,
}; };
let item = PipelineItem::try_from(&view).unwrap(); let item = PipelineItem::try_from(&view).unwrap();
assert!(matches!(item.stage, Stage::Merge { .. })); assert!(matches!(item.stage, Stage::Merge { .. }));
@@ -276,6 +279,7 @@ mod tests {
claimed_at: None, claimed_at: None,
merged_at: None, merged_at: None,
qa_mode: None, qa_mode: None,
mergemaster_attempted: None,
}; };
let item = PipelineItem::try_from(&view).unwrap(); let item = PipelineItem::try_from(&view).unwrap();
assert!(matches!( assert!(matches!(
@@ -301,6 +305,7 @@ mod tests {
claimed_at: None, claimed_at: None,
merged_at: None, merged_at: None,
qa_mode: None, qa_mode: None,
mergemaster_attempted: None,
}; };
let item = PipelineItem::try_from(&view).unwrap(); let item = PipelineItem::try_from(&view).unwrap();
assert!(matches!( assert!(matches!(
@@ -326,6 +331,7 @@ mod tests {
claimed_at: None, claimed_at: None,
merged_at: None, merged_at: None,
qa_mode: None, qa_mode: None,
mergemaster_attempted: None,
}; };
let result = PipelineItem::try_from(&view); let result = PipelineItem::try_from(&view);
assert!(matches!( assert!(matches!(