huskies: merge 873
This commit is contained in:
@@ -85,6 +85,7 @@ impl AgentPool {
|
||||
crate::db::write_content(story_id, &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
|
||||
.start_agent(project_root, story_id, Some(&agent_name), None, None)
|
||||
.await
|
||||
|
||||
@@ -53,7 +53,7 @@ pub use types::{
|
||||
};
|
||||
pub use write::{
|
||||
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)]
|
||||
|
||||
@@ -320,6 +320,11 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mergemaster_attempted = match item.mergemaster_attempted.view() {
|
||||
JsonValue::Bool(b) => Some(b),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Some(PipelineItemView {
|
||||
story_id,
|
||||
stage,
|
||||
@@ -332,6 +337,7 @@ pub(super) fn extract_item_view(item: &PipelineItemCrdt) -> Option<PipelineItemV
|
||||
claimed_at,
|
||||
merged_at,
|
||||
qa_mode,
|
||||
mergemaster_attempted,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,10 @@ pub struct PipelineItemCrdt {
|
||||
/// QA mode override for this item: `"server"`, `"agent"`, or `"human"`.
|
||||
/// Empty string means "use the project default".
|
||||
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.
|
||||
@@ -128,6 +132,9 @@ pub struct PipelineItemView {
|
||||
/// QA mode override from the CRDT register: `"server"`, `"agent"`, or `"human"`.
|
||||
/// `None` means the register is unset (use project default).
|
||||
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.
|
||||
|
||||
@@ -265,6 +265,31 @@ pub fn set_qa_mode(story_id: &str, mode: Option<QaMode>) -> bool {
|
||||
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.
|
||||
///
|
||||
/// 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),
|
||||
"merged_at": merged_at.unwrap_or(0.0),
|
||||
"qa_mode": "",
|
||||
"mergemaster_attempted": false,
|
||||
})
|
||||
.into();
|
||||
|
||||
@@ -394,6 +420,7 @@ pub fn write_item(
|
||||
item.claimed_at.advance_seq(floor);
|
||||
item.merged_at.advance_seq(floor);
|
||||
item.qa_mode.advance_seq(floor);
|
||||
item.mergemaster_attempted.advance_seq(floor);
|
||||
}
|
||||
|
||||
// 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]
|
||||
fn set_qa_mode_returns_false_for_unknown_story() {
|
||||
init_for_test();
|
||||
|
||||
@@ -206,6 +206,7 @@ mod tests {
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert_eq!(item.story_id, StoryId("42_story_test".to_string()));
|
||||
@@ -229,6 +230,7 @@ mod tests {
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(item.stage, Stage::Coding));
|
||||
@@ -249,6 +251,7 @@ mod tests {
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(item.stage, Stage::Merge { .. }));
|
||||
@@ -276,6 +279,7 @@ mod tests {
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(
|
||||
@@ -301,6 +305,7 @@ mod tests {
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let item = PipelineItem::try_from(&view).unwrap();
|
||||
assert!(matches!(
|
||||
@@ -326,6 +331,7 @@ mod tests {
|
||||
claimed_at: None,
|
||||
merged_at: None,
|
||||
qa_mode: None,
|
||||
mergemaster_attempted: None,
|
||||
};
|
||||
let result = PipelineItem::try_from(&view);
|
||||
assert!(matches!(
|
||||
|
||||
Reference in New Issue
Block a user