huskies: merge 873
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
Reference in New Issue
Block a user