diff --git a/crates/zx_document_core/src/events_v2.rs b/crates/zx_document_core/src/events_v2.rs index ce8a633..f3f5883 100644 --- a/crates/zx_document_core/src/events_v2.rs +++ b/crates/zx_document_core/src/events_v2.rs @@ -196,6 +196,9 @@ pub fn mark_events_failed_v2(event_ids: &[String]) -> u32 { let mut count = 0; for item in buf.iter_mut() { if event_ids.contains(&item.event.event_id) { + if item.state == BufferedEventState::Failed { + continue; // already Failed, skip to avoid double-counting retry + } item.state = BufferedEventState::Failed; item.retry_count += 1; count += 1; @@ -657,6 +660,22 @@ mod tests { teardown_session(&sid); } + #[test] + fn test_mark_failed_idempotent() { + let _guard = TEST_LOCK.lock().unwrap(); + clear_all_events_v2(); + let sid = setup_session("mat_idem"); + let e1 = push_heartbeat_v2(&sid, "mat_idem", 15, None, 1000).unwrap(); + export_pending_events_v2(100, 2000); + // Mark twice — second should be a no-op + let first = mark_events_failed_v2(&[e1.event_id.clone()]); + assert_eq!(first, 1); + let second = mark_events_failed_v2(&[e1.event_id.clone()]); + assert_eq!(second, 0, "duplicate mark_failed should return 0, not double-count"); + ack_events_v2(&[e1.event_id.clone()]); + teardown_session(&sid); + } + #[test] fn test_delta_semantics_per_event_type() { let _guard = lock_test();