huskies: merge 1062

This commit is contained in:
dave
2026-05-14 20:29:12 +00:00
parent 5678f2a556
commit 8f666bd6b3
2 changed files with 51 additions and 10 deletions
@@ -533,4 +533,48 @@ mod tests {
let args = serde_json::json!({ "mode": "persistent" });
assert_eq!(parse_mode(&args), FireMode::Persistent);
}
/// Regression test: once-mode triggers with a server-restarting action (e.g.
/// rebuild_and_restart) must be removed from the store BEFORE the action is
/// dispatched. If cancellation happens after dispatch, a server restart
/// caused by the action reloads the persisted store and replays the trigger.
#[test]
fn once_mode_rebuild_trigger_cancelled_before_action() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("triggers.json");
let store = EventTriggerStore::load(path.clone());
let trigger = store
.add(
TriggerPredicate {
story_id: None,
from_stage: None,
to_stage: Some("Current".to_string()),
event_kind: None,
},
TriggerAction::Mcp {
method: "rebuild_and_restart".to_string(),
args: serde_json::json!({}),
},
FireMode::Once,
)
.unwrap();
let id = trigger.id.clone();
// Simulate the subscriber: cancel BEFORE the action runs.
store.cancel(&id);
// Trigger must be absent immediately (simulates "right after match").
assert!(
store.list().is_empty(),
"once-mode trigger must be absent before its action fires"
);
// Simulate server restart: reload from persisted store.
let reloaded = EventTriggerStore::load(path);
assert!(
reloaded.list().is_empty(),
"trigger must not survive a server restart caused by its own action"
);
}
}
+7 -10
View File
@@ -346,8 +346,6 @@ pub(crate) fn spawn_event_trigger_subscriber(
continue;
}
let mut to_cancel: Vec<String> = Vec::new();
for trigger in &triggers {
if !trigger.predicate.matches(&fired) {
continue;
@@ -361,6 +359,13 @@ pub(crate) fn spawn_event_trigger_subscriber(
crate::pipeline_state::stage_label(&fired.after),
);
// Cancel once-mode triggers before dispatching the action so
// that a server restart triggered by the action (e.g.
// rebuild_and_restart) cannot find and replay the trigger.
if trigger.mode == FireMode::Once {
store.cancel(&trigger.id);
}
match &trigger.action {
TriggerAction::Mcp { method, args } => {
execute_mcp_action(method, args.clone(), &ctx).await;
@@ -375,14 +380,6 @@ pub(crate) fn spawn_event_trigger_subscriber(
.await;
}
}
if trigger.mode == FireMode::Once {
to_cancel.push(trigger.id.clone());
}
}
if !to_cancel.is_empty() {
store.cancel_batch(&to_cancel);
}
}
});