1use std::{
6 collections::{HashMap, HashSet, VecDeque},
7 sync::{Arc, Mutex},
8 time::Duration,
9};
10
11use super::{
12 catalog::EventCatalog,
13 types::{
14 BrokerEvent, EventBroker, EventCursor, EventError, LifecycleStatus, Subscription,
15 SubscriptionId, SubscriptionPoll, TraverseEvent,
16 },
17 validation::{EventValidationEvidence, EventValidationMode, validate_event},
18};
19
20pub trait BrokerClock: Send + Sync {
22 fn now(&self) -> std::time::SystemTime;
23}
24
25#[derive(Debug)]
26pub struct SystemClock;
27
28impl BrokerClock for SystemClock {
29 fn now(&self) -> std::time::SystemTime {
30 std::time::SystemTime::now()
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct BrokerConfig {
37 pub retention_window: Duration,
38 pub max_queue_len: usize,
39}
40
41impl Default for BrokerConfig {
42 fn default() -> Self {
43 Self {
44 retention_window: Duration::from_mins(5),
45 max_queue_len: 1024,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
51struct BufferedEvent {
52 cursor: u64,
53 published_at: std::time::SystemTime,
54 event: TraverseEvent,
55}
56
57#[derive(Debug)]
58struct SubscriptionState {
59 subscription_id: SubscriptionId,
60 event_type: String,
61 subject_id: Option<String>,
62 consumer_id: Option<String>,
63 cursor: u64,
64 queue: VecDeque<BufferedEvent>,
65}
66
67#[derive(Debug, Default)]
68struct BrokerState {
69 next_subscription: u64,
70 next_cursor: HashMap<String, u64>,
71 buffers: HashMap<String, VecDeque<BufferedEvent>>,
72 seen_event_ids: HashMap<String, HashSet<String>>,
73 subscriptions: HashMap<SubscriptionId, SubscriptionState>,
74 subscriptions_by_event_type: HashMap<String, HashSet<SubscriptionId>>,
75 restart_floor: u64,
77 validation_evidence: Vec<EventValidationEvidence>,
78 quarantine_records: Vec<EventQuarantineRecord>,
79 observed_lineage: Vec<EventLineageRecord>,
80 telemetry: Vec<EventTelemetryRecord>,
81 metrics: EventRuntimeMetrics,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct EventLineageRecord {
90 pub contract_id: String,
91 pub contract_version: String,
92 pub event_id: String,
93 pub producer_id: String,
94 pub consumer_id: String,
95 pub subscription_id: SubscriptionId,
96 pub cursor: EventCursor,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct EventQuarantineRecord {
105 pub evidence: EventValidationEvidence,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct EventTelemetryRecord {
115 pub operation: &'static str,
116 pub outcome: &'static str,
117 pub contract_id: String,
118 pub contract_version: String,
119 pub event_id: String,
120 pub deduplication_id: Option<String>,
121 pub ordering_scope: Option<String>,
122 pub correlation_id: Option<String>,
123 pub causation_id: Option<String>,
124 pub consumer_id: Option<String>,
125 pub cursor: Option<EventCursor>,
126 pub retry_count: u32,
127 pub latency_ms: u64,
128}
129
130#[derive(Debug, Clone, Default, PartialEq, Eq)]
132pub struct EventRuntimeMetrics {
133 pub publications: u64,
134 pub deliveries: u64,
135 pub validation_failures: u64,
136 pub quarantines: u64,
137}
138
139pub struct InProcessBroker {
145 catalog: Arc<EventCatalog>,
146 config: BrokerConfig,
147 clock: Arc<dyn BrokerClock>,
148 state: Mutex<BrokerState>,
149 validation_mode: EventValidationMode,
150}
151
152impl std::fmt::Debug for InProcessBroker {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.debug_struct("InProcessBroker").finish_non_exhaustive()
155 }
156}
157
158impl InProcessBroker {
159 pub fn new(catalog: Arc<EventCatalog>) -> Result<Self, EventError> {
165 Self::with_clock(catalog, BrokerConfig::default(), Arc::new(SystemClock))
166 }
167
168 pub fn with_clock(
174 catalog: Arc<EventCatalog>,
175 config: BrokerConfig,
176 clock: Arc<dyn BrokerClock>,
177 ) -> Result<Self, EventError> {
178 Self::with_clock_and_validation(catalog, config, clock, EventValidationMode::Migration)
179 }
180
181 pub fn with_clock_and_validation(
191 catalog: Arc<EventCatalog>,
192 config: BrokerConfig,
193 clock: Arc<dyn BrokerClock>,
194 validation_mode: EventValidationMode,
195 ) -> Result<Self, EventError> {
196 if config.retention_window == Duration::from_secs(0) {
197 return Err(EventError::InvalidRetentionWindow(
198 "retention_window must be > 0".to_string(),
199 ));
200 }
201 if config.max_queue_len == 0 {
202 return Err(EventError::InvalidRetentionWindow(
203 "max_queue_len must be > 0".to_string(),
204 ));
205 }
206
207 Ok(Self {
208 catalog,
209 config,
210 clock,
211 state: Mutex::new(BrokerState::default()),
212 validation_mode,
213 })
214 }
215
216 #[must_use]
219 pub fn validation_evidence(&self) -> Vec<EventValidationEvidence> {
220 self.state
221 .lock()
222 .map(|state| state.validation_evidence.clone())
223 .unwrap_or_default()
224 }
225
226 #[must_use]
228 pub fn quarantine_records(&self) -> Vec<EventQuarantineRecord> {
229 self.state
230 .lock()
231 .map(|state| state.quarantine_records.clone())
232 .unwrap_or_default()
233 }
234
235 #[must_use]
237 pub fn observed_lineage(&self) -> Vec<EventLineageRecord> {
238 self.state
239 .lock()
240 .map(|state| state.observed_lineage.clone())
241 .unwrap_or_default()
242 }
243
244 #[must_use]
246 pub fn telemetry(&self) -> Vec<EventTelemetryRecord> {
247 self.state
248 .lock()
249 .map(|state| state.telemetry.clone())
250 .unwrap_or_default()
251 }
252
253 #[must_use]
255 pub fn metrics(&self) -> EventRuntimeMetrics {
256 self.state
257 .lock()
258 .map(|state| state.metrics.clone())
259 .unwrap_or_default()
260 }
261
262 fn subscribe_with_subject(
263 &self,
264 event_type: &str,
265 from_cursor: &str,
266 subject_id: Option<&str>,
267 consumer_id: Option<&str>,
268 ) -> Result<Subscription, EventError> {
269 if self.catalog.get(event_type).is_none() {
270 return Err(EventError::UnregisteredEventType(event_type.to_owned()));
271 }
272
273 let from_cursor = parse_cursor(from_cursor)?;
274 let now = self.clock.now();
275 let mut state = self
276 .state
277 .lock()
278 .map_err(|_| EventError::LifecycleViolation("broker lock poisoned".to_owned()))?;
279 prune_expired(&mut state, event_type, self.config.retention_window, now);
280 validate_from_cursor(&state, event_type, from_cursor)?;
281 self.catalog.increment_consumer_count(event_type);
282
283 state.next_subscription = state.next_subscription.saturating_add(1);
284 let subscription_id = format!("sub-{}", state.next_subscription);
285 let mut queue = VecDeque::new();
286 for item in state
287 .buffers
288 .get(event_type)
289 .into_iter()
290 .flat_map(|buffer| buffer.iter())
291 {
292 if (from_cursor == 0 || item.cursor > from_cursor)
293 && subject_id
294 .is_none_or(|subject| item.event.subject_id.as_deref() == Some(subject))
295 {
296 enqueue_with_drop_oldest(&mut queue, self.config.max_queue_len, item.clone());
297 }
298 }
299
300 state.subscriptions.insert(
301 subscription_id.clone(),
302 SubscriptionState {
303 subscription_id: subscription_id.clone(),
304 event_type: event_type.to_string(),
305 subject_id: subject_id.map(str::to_owned),
306 consumer_id: consumer_id.map(str::to_owned),
307 cursor: from_cursor,
308 queue,
309 },
310 );
311 state
312 .subscriptions_by_event_type
313 .entry(event_type.to_string())
314 .or_default()
315 .insert(subscription_id.clone());
316
317 Ok(Subscription {
318 subscription_id,
319 event_type: event_type.to_string(),
320 cursor: cursor_to_string(from_cursor),
321 })
322 }
323
324 pub fn subscribe_for_consumer(
333 &self,
334 event_type: &str,
335 from_cursor: &str,
336 consumer_id: &str,
337 subject_id: Option<&str>,
338 ) -> Result<Subscription, EventError> {
339 if consumer_id.trim().is_empty() {
340 return Err(EventError::LifecycleViolation(
341 "consumer_id must not be empty".to_string(),
342 ));
343 }
344 self.subscribe_with_subject(event_type, from_cursor, subject_id, Some(consumer_id))
345 }
346
347 fn validate_boundary(&self, event: &TraverseEvent) -> Result<(), EventError> {
348 let validation = validate_event(event, self.validation_mode);
349 let validation_outcome = if validation.is_valid() {
350 "accepted"
351 } else if validation.accepted {
352 "reported"
353 } else {
354 "rejected"
355 };
356 let evidence = EventValidationEvidence::from_result(&validation);
357 let mut state = self
358 .state
359 .lock()
360 .map_err(|_| EventError::LifecycleViolation("broker lock poisoned".to_owned()))?;
361 if let Some(evidence) = evidence {
362 state.validation_evidence.push(evidence.clone());
363 state.metrics.validation_failures = state.metrics.validation_failures.saturating_add(1);
364 if !validation.accepted {
365 state
366 .quarantine_records
367 .push(EventQuarantineRecord { evidence });
368 state.metrics.quarantines = state.metrics.quarantines.saturating_add(1);
369 }
370 }
371 state.telemetry.push(telemetry_record(
372 "traverse.event.validation",
373 validation_outcome,
374 event,
375 None,
376 None,
377 ));
378 if !validation.accepted {
379 let code = validation
380 .diagnostics
381 .first()
382 .map_or("EVP-000", |diagnostic| diagnostic.code);
383 return Err(EventError::ValidationRejected(code.to_owned()));
384 }
385 Ok(())
386 }
387
388 fn publish_internal(
396 &self,
397 event: &TraverseEvent,
398 assigned_cursor: Option<u64>,
399 ) -> Result<(), EventError> {
400 self.validate_boundary(event)?;
401 let entry = self
402 .catalog
403 .get(&event.event_type)
404 .ok_or_else(|| EventError::UnregisteredEventType(event.event_type.clone()))?;
405
406 match entry.lifecycle_status {
407 LifecycleStatus::Active => {}
408 LifecycleStatus::Deprecated => {
409 return Err(EventError::LifecycleViolation(format!(
410 "event type '{}' is Deprecated and cannot be published",
411 event.event_type
412 )));
413 }
414 LifecycleStatus::Draft => {
415 return Err(EventError::LifecycleViolation(format!(
416 "event type '{}' is Draft and cannot be published",
417 event.event_type
418 )));
419 }
420 }
421
422 let now = self.clock.now();
423
424 let mut state = self
425 .state
426 .lock()
427 .map_err(|_| EventError::LifecycleViolation("broker lock poisoned".to_owned()))?;
428
429 prune_expired(
430 &mut state,
431 &event.event_type,
432 self.config.retention_window,
433 now,
434 );
435
436 let seen = state
437 .seen_event_ids
438 .entry(event.event_type.clone())
439 .or_default();
440 if seen.contains(&event.id) {
441 return Ok(());
443 }
444 seen.insert(event.id.clone());
445 state.metrics.publications = state.metrics.publications.saturating_add(1);
446 state.telemetry.push(telemetry_record(
447 "traverse.event.publish",
448 "accepted",
449 event,
450 None,
451 None,
452 ));
453
454 let next = state
455 .next_cursor
456 .entry(event.event_type.clone())
457 .or_insert(0);
458 let cursor = if let Some(assigned) = assigned_cursor {
459 *next = (*next).max(assigned);
460 assigned
461 } else {
462 *next = next.saturating_add(1);
463 *next
464 };
465
466 let buffered = BufferedEvent {
467 cursor,
468 published_at: now,
469 event: event.clone(),
470 };
471
472 state
473 .buffers
474 .entry(event.event_type.clone())
475 .or_default()
476 .push_back(buffered.clone());
477
478 let subscription_ids = subscription_ids_for_event_type(&state, &event.event_type);
479 for subscription_id in subscription_ids {
480 let Some(sub) = state.subscriptions.get_mut(&subscription_id) else {
481 continue;
482 };
483 if sub
484 .subject_id
485 .as_deref()
486 .is_some_and(|subject_id| event.subject_id.as_deref() != Some(subject_id))
487 {
488 continue;
489 }
490 enqueue_with_drop_oldest(&mut sub.queue, self.config.max_queue_len, buffered.clone());
491 }
492
493 Ok(())
494 }
495}
496
497fn parse_cursor(raw: &str) -> Result<u64, EventError> {
498 let trimmed = raw.trim();
499 if trimmed == "0" {
500 return Ok(0);
501 }
502 trimmed.parse::<u64>().map_err(|_| {
503 EventError::InvalidCursor("cursor must be \"0\" or a base-10 unsigned integer".to_string())
504 })
505}
506
507fn cursor_to_string(cursor: u64) -> EventCursor {
508 cursor.to_string()
509}
510
511fn enqueue_with_drop_oldest(
512 queue: &mut VecDeque<BufferedEvent>,
513 max_len: usize,
514 item: BufferedEvent,
515) {
516 while queue.len() >= max_len {
517 let _ = queue.pop_front();
518 }
519 queue.push_back(item);
520}
521
522fn prune_expired(
523 state: &mut BrokerState,
524 event_type: &str,
525 retention_window: Duration,
526 now: std::time::SystemTime,
527) {
528 let buffer = state.buffers.entry(event_type.to_string()).or_default();
529 let mut oldest_retained_cursor = None;
530 while let Some(front) = buffer.pop_front() {
531 let age = now
532 .duration_since(front.published_at)
533 .unwrap_or(Duration::from_secs(0));
534 if age <= retention_window {
535 oldest_retained_cursor = Some(front.cursor);
536 buffer.push_front(front);
537 break;
538 }
539
540 if let Some(ids) = state.seen_event_ids.get_mut(event_type) {
541 let _ = ids.remove(&front.event.id);
542 }
543 }
544
545 let Some(oldest_cursor) = oldest_retained_cursor else {
546 return;
548 };
549
550 let subscription_ids = subscription_ids_for_event_type(state, event_type);
552 for subscription_id in subscription_ids {
553 let Some(sub) = state.subscriptions.get_mut(&subscription_id) else {
554 continue;
555 };
556 while let Some(front) = sub.queue.front() {
557 if front.cursor >= oldest_cursor {
558 break;
559 }
560 let _ = sub.queue.pop_front();
561 }
562 if sub.cursor != 0 && sub.cursor < oldest_cursor.saturating_sub(1) {
563 }
565 }
566}
567
568fn validate_from_cursor(
569 state: &BrokerState,
570 event_type: &str,
571 from_cursor: u64,
572) -> Result<(), EventError> {
573 if from_cursor == 0 {
574 return Ok(());
575 }
576
577 let last_cursor = state
578 .next_cursor
579 .get(event_type)
580 .copied()
581 .unwrap_or(0)
582 .max(state.restart_floor);
583 if let Some(buffer) = state.buffers.get(event_type)
584 && let Some(front) = buffer.front()
585 {
586 let oldest_ok = front.cursor.saturating_sub(1);
587 if from_cursor < oldest_ok {
588 return Err(EventError::CursorExpired {
589 event_type: event_type.to_string(),
590 oldest_available_cursor: cursor_to_string(oldest_ok),
591 });
592 }
593 return Ok(());
594 }
595
596 if last_cursor > 0 && from_cursor < last_cursor {
599 return Err(EventError::CursorExpired {
600 event_type: event_type.to_string(),
601 oldest_available_cursor: cursor_to_string(last_cursor),
602 });
603 }
604
605 Ok(())
606}
607
608fn subscription_ids_for_event_type(
609 state: &BrokerState,
610 event_type: &str,
611) -> HashSet<SubscriptionId> {
612 state
613 .subscriptions_by_event_type
614 .get(event_type)
615 .cloned()
616 .unwrap_or_default()
617}
618
619impl EventBroker for InProcessBroker {
620 fn subscribe_for_subject(
621 &self,
622 event_type: &str,
623 from_cursor: &str,
624 subject_id: Option<&str>,
625 ) -> Result<Subscription, EventError> {
626 self.subscribe_with_subject(event_type, from_cursor, subject_id, None)
627 }
628
629 fn seed_restart_floor(&self, floor: u64) {
630 if let Ok(mut state) = self.state.lock() {
631 state.restart_floor = state.restart_floor.max(floor);
632 }
633 }
634
635 fn publish(&self, event: TraverseEvent) -> Result<(), EventError> {
642 self.publish_internal(&event, None)
643 }
644
645 fn publish_with_cursor(&self, event: TraverseEvent, cursor: &str) -> Result<(), EventError> {
657 let assigned = parse_cursor(cursor)?;
658 self.publish_internal(&event, Some(assigned))
659 }
660
661 fn subscribe(&self, event_type: &str, from_cursor: &str) -> Result<Subscription, EventError> {
669 self.subscribe_with_subject(event_type, from_cursor, None, None)
670 }
671
672 fn poll(
677 &self,
678 subscription_id: &str,
679 max_events: usize,
680 ) -> Result<SubscriptionPoll, EventError> {
681 let now = self.clock.now();
682 let mut state = self
683 .state
684 .lock()
685 .map_err(|_| EventError::LifecycleViolation("broker lock poisoned".to_owned()))?;
686
687 let mut subscription = state
688 .subscriptions
689 .remove(subscription_id)
690 .ok_or_else(|| EventError::SubscriptionNotFound(subscription_id.to_string()))?;
691 let event_type = subscription.event_type.clone();
692 let cursor = subscription.cursor;
693
694 prune_expired(&mut state, &event_type, self.config.retention_window, now);
695
696 validate_from_cursor(&state, &event_type, cursor)?;
697
698 if let Some(buffer) = state.buffers.get(&event_type)
699 && let Some(oldest_cursor) = buffer.front().map(|e| e.cursor)
700 {
701 while let Some(front) = subscription.queue.front() {
702 if front.cursor >= oldest_cursor {
703 break;
704 }
705 let _ = subscription.queue.pop_front();
706 }
707 }
708
709 if max_events == 0 {
710 let cursor_str = cursor_to_string(subscription.cursor);
711 state
712 .subscriptions
713 .insert(subscription.subscription_id.clone(), subscription);
714 return Ok(SubscriptionPoll {
715 subscription_id: subscription_id.to_string(),
716 event_type,
717 cursor: cursor_str,
718 events: Vec::new(),
719 });
720 }
721
722 let mut out = Vec::new();
723 let mut delivered_cursor = subscription.cursor;
724 for _ in 0..max_events {
725 let Some(item) = subscription.queue.pop_front() else {
726 break;
727 };
728 delivered_cursor = item.cursor;
729 state.observed_lineage.push(EventLineageRecord {
730 contract_id: item.event.event_type.clone(),
731 contract_version: item.event.version.clone(),
732 event_id: item.event.id.clone(),
733 producer_id: item.event.owner.clone(),
734 consumer_id: subscription
735 .consumer_id
736 .clone()
737 .unwrap_or_else(|| subscription.subscription_id.clone()),
738 subscription_id: subscription.subscription_id.clone(),
739 cursor: cursor_to_string(item.cursor),
740 });
741 let consumer_id = subscription
742 .consumer_id
743 .clone()
744 .unwrap_or_else(|| subscription.subscription_id.clone());
745 state.telemetry.push(telemetry_record(
746 "traverse.event.delivery",
747 "delivered",
748 &item.event,
749 Some(consumer_id),
750 Some(cursor_to_string(item.cursor)),
751 ));
752 out.push(BrokerEvent {
753 cursor: cursor_to_string(item.cursor),
754 event: item.event,
755 });
756 }
757 subscription.cursor = delivered_cursor;
758 state.metrics.deliveries = state.metrics.deliveries.saturating_add(out.len() as u64);
759
760 let subscription_id_value = subscription.subscription_id.clone();
761 let event_type_value = subscription.event_type.clone();
762 let cursor_value = cursor_to_string(subscription.cursor);
763 state
764 .subscriptions
765 .insert(subscription.subscription_id.clone(), subscription);
766
767 Ok(SubscriptionPoll {
768 subscription_id: subscription_id_value,
769 event_type: event_type_value,
770 cursor: cursor_value,
771 events: out,
772 })
773 }
774
775 fn cancel(&self, subscription_id: &str) -> Result<(), EventError> {
781 let mut state = self
782 .state
783 .lock()
784 .map_err(|_| EventError::LifecycleViolation("broker lock poisoned".to_owned()))?;
785
786 let Some(subscription) = state.subscriptions.remove(subscription_id) else {
787 return Err(EventError::SubscriptionNotFound(
788 subscription_id.to_string(),
789 ));
790 };
791 if let Some(ids) = state
792 .subscriptions_by_event_type
793 .get_mut(&subscription.event_type)
794 {
795 let _ = ids.remove(subscription_id);
796 if ids.is_empty() {
797 let _ = state
798 .subscriptions_by_event_type
799 .remove(&subscription.event_type);
800 }
801 }
802 Ok(())
803 }
804}
805
806fn telemetry_record(
807 operation: &'static str,
808 outcome: &'static str,
809 event: &TraverseEvent,
810 consumer_id: Option<String>,
811 cursor: Option<EventCursor>,
812) -> EventTelemetryRecord {
813 EventTelemetryRecord {
814 operation,
815 outcome,
816 contract_id: event.event_type.clone(),
817 contract_version: event.version.clone(),
818 event_id: event.id.clone(),
819 deduplication_id: event.deduplication_id.clone(),
820 ordering_scope: event.ordering_scope.clone(),
821 correlation_id: event.correlation_id.clone(),
822 causation_id: event.causation_id.clone(),
823 consumer_id,
824 cursor,
825 retry_count: 0,
826 latency_ms: 0,
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 #![allow(clippy::expect_used)]
833 #![allow(clippy::panic)]
834 #![allow(clippy::unwrap_used)]
835
836 use super::*;
837 use crate::events::catalog::EventCatalogEntry;
838
839 fn cursor_expired_oldest(err: &EventError) -> Option<String> {
840 if let EventError::CursorExpired {
841 oldest_available_cursor,
842 ..
843 } = err
844 {
845 Some(oldest_available_cursor.clone())
846 } else {
847 None
848 }
849 }
850
851 fn make_catalog(event_type: &str, status: LifecycleStatus) -> Arc<EventCatalog> {
852 let catalog = Arc::new(EventCatalog::new());
853 catalog
854 .register(EventCatalogEntry {
855 event_type: event_type.to_string(),
856 owner: "cap.test".to_string(),
857 version: "1.0.0".to_string(),
858 lifecycle_status: status,
859 consumer_count: 0,
860 })
861 .expect("catalog register must succeed");
862 catalog
863 }
864
865 fn sample_event(event_type: &str, id: &str) -> TraverseEvent {
866 TraverseEvent {
867 id: id.to_string(),
868 source: "traverse-runtime/cap.test".to_string(),
869 event_type: event_type.to_string(),
870 datacontenttype: "application/json".to_string(),
871 time: "2026-04-08T00:00:00Z".to_string(),
872 data: serde_json::json!({}),
873 owner: "cap.test".to_string(),
874 version: "1.0.0".to_string(),
875 lifecycle_status: LifecycleStatus::Active,
876 deduplication_id: Some(id.to_string()),
877 ordering_scope: Some("test".to_string()),
878 correlation_id: Some("correlation-test".to_string()),
879 causation_id: Some("command-test".to_string()),
880 subject_id: None,
881 actor_id: None,
882 }
883 }
884
885 #[test]
886 fn broker_debug_impl_is_accessible() {
887 let catalog = make_catalog("dev.traverse.debug", LifecycleStatus::Active);
888 let broker = InProcessBroker::new(catalog).expect("broker must be created");
889 let rendered = format!("{broker:?}");
890 assert!(rendered.contains("InProcessBroker"));
891 }
892
893 #[test]
894 fn invalid_max_queue_len_is_rejected() {
895 let catalog = make_catalog("dev.traverse.invalid", LifecycleStatus::Active);
896 let err = InProcessBroker::with_clock(
897 catalog,
898 BrokerConfig {
899 retention_window: Duration::from_secs(1),
900 max_queue_len: 0,
901 },
902 Arc::new(SystemClock),
903 )
904 .expect_err("max_queue_len=0 must be rejected");
905 assert!(matches!(err, EventError::InvalidRetentionWindow(_)));
906 }
907
908 #[test]
909 fn publish_with_cursor_adopts_the_given_cursor_instead_of_self_assigning() {
910 let event_type = "dev.traverse.injected-cursor";
911 let catalog = make_catalog(event_type, LifecycleStatus::Active);
912 let broker = InProcessBroker::new(catalog).expect("broker must be created");
913
914 broker
915 .publish_with_cursor(sample_event(event_type, "evt-1"), "42")
916 .expect("publish_with_cursor must succeed");
917
918 let subscription = broker
919 .subscribe(event_type, "0")
920 .expect("subscribe must succeed");
921 let poll = broker
922 .poll(&subscription.subscription_id, 10)
923 .expect("poll must succeed");
924 assert_eq!(poll.events.len(), 1);
925 assert_eq!(poll.events[0].cursor, "42");
926 assert_eq!(poll.cursor, "42");
927 }
928
929 #[test]
930 fn publish_with_cursor_rejects_a_malformed_cursor() {
931 let event_type = "dev.traverse.injected-cursor-invalid";
932 let catalog = make_catalog(event_type, LifecycleStatus::Active);
933 let broker = InProcessBroker::new(catalog).expect("broker must be created");
934
935 let err = broker
936 .publish_with_cursor(sample_event(event_type, "evt-1"), "not-a-cursor")
937 .expect_err("malformed cursor must be rejected");
938 assert!(matches!(err, EventError::InvalidCursor(_)));
939 }
940
941 #[test]
942 fn default_publish_with_cursor_ignores_the_cursor_and_self_assigns() {
943 struct SelfAssigningOnlyBroker(InProcessBroker);
944
945 impl EventBroker for SelfAssigningOnlyBroker {
946 fn publish(&self, event: TraverseEvent) -> Result<(), EventError> {
947 self.0.publish(event)
948 }
949 fn subscribe(
950 &self,
951 event_type: &str,
952 from_cursor: &str,
953 ) -> Result<Subscription, EventError> {
954 self.0.subscribe(event_type, from_cursor)
955 }
956 fn subscribe_for_subject(
957 &self,
958 event_type: &str,
959 from_cursor: &str,
960 subject_id: Option<&str>,
961 ) -> Result<Subscription, EventError> {
962 self.0
963 .subscribe_for_subject(event_type, from_cursor, subject_id)
964 }
965 fn poll(
966 &self,
967 subscription_id: &str,
968 max_events: usize,
969 ) -> Result<SubscriptionPoll, EventError> {
970 self.0.poll(subscription_id, max_events)
971 }
972 fn cancel(&self, subscription_id: &str) -> Result<(), EventError> {
973 self.0.cancel(subscription_id)
974 }
975 }
976
977 let event_type = "dev.traverse.default-publish-with-cursor";
978 let catalog = make_catalog(event_type, LifecycleStatus::Active);
979 let broker =
980 SelfAssigningOnlyBroker(InProcessBroker::new(catalog).expect("broker must be created"));
981
982 broker
985 .publish_with_cursor(sample_event(event_type, "evt-1"), "999")
986 .expect("default publish_with_cursor must succeed");
987
988 let subscription = broker
989 .subscribe(event_type, "0")
990 .expect("subscribe must succeed");
991 let poll = broker
992 .poll(&subscription.subscription_id, 10)
993 .expect("poll must succeed");
994 assert_eq!(poll.events[0].cursor, "1");
995
996 broker.seed_restart_floor(999);
999 let subject_subscription = broker
1000 .subscribe_for_subject(event_type, "0", None)
1001 .expect("subscribe_for_subject must succeed");
1002 broker
1003 .cancel(&subject_subscription.subscription_id)
1004 .expect("cancel must succeed");
1005 }
1006
1007 #[test]
1008 fn invalid_cursor_is_rejected() {
1009 let catalog = make_catalog("dev.traverse.cursor", LifecycleStatus::Active);
1010 let broker = InProcessBroker::new(catalog).expect("broker must be created");
1011 let err = broker
1012 .subscribe("dev.traverse.cursor", "not-a-cursor")
1013 .expect_err("invalid cursor must fail");
1014 assert!(matches!(err, EventError::InvalidCursor(_)));
1015 }
1016
1017 #[test]
1018 fn subject_subscription_filters_backlog_and_live_delivery() {
1019 let event_type = "dev.traverse.subject-filter";
1020 let broker = InProcessBroker::new(make_catalog(event_type, LifecycleStatus::Active))
1021 .expect("broker must be created");
1022 let mut other = sample_event(event_type, "evt-other");
1023 other.subject_id = Some("subject-other".to_string());
1024 let mut expected = sample_event(event_type, "evt-match");
1025 expected.subject_id = Some("subject-match".to_string());
1026 broker.publish(other).expect("backlog publish must succeed");
1027 broker
1028 .publish(expected.clone())
1029 .expect("backlog publish must succeed");
1030
1031 let subscription = broker
1032 .subscribe_for_subject(event_type, "0", Some("subject-match"))
1033 .expect("subject subscription must succeed");
1034 let backlog = broker
1035 .poll(&subscription.subscription_id, 10)
1036 .expect("backlog poll must succeed");
1037 assert_eq!(backlog.events.len(), 1);
1038 assert_eq!(backlog.events[0].event.id, expected.id);
1039
1040 let mut live_other = sample_event(event_type, "evt-live-other");
1041 live_other.subject_id = Some("subject-other".to_string());
1042 let mut live_expected = sample_event(event_type, "evt-live-match");
1043 live_expected.subject_id = Some("subject-match".to_string());
1044 broker
1045 .publish(live_other)
1046 .expect("non-matching live publish must succeed");
1047 broker
1048 .publish(live_expected.clone())
1049 .expect("matching live publish must succeed");
1050
1051 let live = broker
1052 .poll(&subscription.subscription_id, 10)
1053 .expect("live poll must succeed");
1054 assert_eq!(live.events.len(), 1);
1055 assert_eq!(live.events[0].event.id, live_expected.id);
1056 }
1057
1058 #[test]
1059 fn consumer_subscription_records_sanitized_observed_lineage() {
1060 let event_type = "dev.traverse.lineage.observed";
1061 let broker = InProcessBroker::new(make_catalog(event_type, LifecycleStatus::Active))
1062 .expect("broker must be created");
1063 let subscription = broker
1064 .subscribe_for_consumer(event_type, "0", "capability.audit", None)
1065 .expect("consumer subscription must succeed");
1066 let mut event = sample_event(event_type, "evt-lineage");
1067 event.owner = "capability.orders".to_string();
1068 event.version = "1.2.3".to_string();
1069 event.data = serde_json::json!({"secret":"not lineage"});
1070 broker.publish(event).expect("publish must succeed");
1071
1072 let _ = broker
1073 .poll(&subscription.subscription_id, 1)
1074 .expect("poll must succeed");
1075 assert_eq!(
1076 broker.observed_lineage(),
1077 vec![EventLineageRecord {
1078 contract_id: event_type.to_string(),
1079 contract_version: "1.2.3".to_string(),
1080 event_id: "evt-lineage".to_string(),
1081 producer_id: "capability.orders".to_string(),
1082 consumer_id: "capability.audit".to_string(),
1083 subscription_id: subscription.subscription_id,
1084 cursor: "1".to_string(),
1085 }]
1086 );
1087 assert_eq!(
1088 broker.metrics(),
1089 EventRuntimeMetrics {
1090 publications: 1,
1091 deliveries: 1,
1092 validation_failures: 0,
1093 quarantines: 0,
1094 }
1095 );
1096 let telemetry = broker.telemetry();
1097 assert_eq!(telemetry.len(), 3);
1098 assert_eq!(telemetry[0].operation, "traverse.event.validation");
1099 assert_eq!(telemetry[0].outcome, "accepted");
1100 assert_eq!(telemetry[1].operation, "traverse.event.publish");
1101 assert_eq!(telemetry[2].operation, "traverse.event.delivery");
1102 assert_eq!(
1103 telemetry[2].consumer_id.as_deref(),
1104 Some("capability.audit")
1105 );
1106 assert_eq!(telemetry[2].cursor.as_deref(), Some("1"));
1107 assert_eq!(telemetry[2].contract_version, "1.2.3");
1108 assert_eq!(telemetry[2].retry_count, 0);
1109 assert_eq!(telemetry[2].latency_ms, 0);
1110 assert!(!format!("{telemetry:?}").contains("not lineage"));
1111 }
1112
1113 #[test]
1114 fn consumer_subscription_rejects_empty_identity() {
1115 let event_type = "dev.traverse.lineage.identity";
1116 let broker = InProcessBroker::new(make_catalog(event_type, LifecycleStatus::Active))
1117 .expect("broker must be created");
1118 let err = broker
1119 .subscribe_for_consumer(event_type, "0", " ", None)
1120 .expect_err("empty consumer identity must fail");
1121 assert!(matches!(err, EventError::LifecycleViolation(_)));
1122 }
1123
1124 #[test]
1125 fn quarantine_records_fail_closed_when_broker_state_is_poisoned() {
1126 let broker = InProcessBroker::new(make_catalog(
1127 "dev.traverse.quarantine.poison",
1128 LifecycleStatus::Active,
1129 ))
1130 .expect("broker must be created");
1131 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1132 let _guard = broker.state.lock().expect("state lock must be available");
1133 panic!("poison state lock");
1134 }));
1135
1136 assert!(broker.quarantine_records().is_empty());
1137 assert_eq!(broker.metrics(), EventRuntimeMetrics::default());
1138 }
1139
1140 #[test]
1141 fn publish_rejects_deprecated_and_draft_event_types() {
1142 let deprecated = InProcessBroker::new(make_catalog(
1143 "dev.traverse.deprecated",
1144 LifecycleStatus::Deprecated,
1145 ))
1146 .expect("broker must be created");
1147 let err = deprecated
1148 .publish(sample_event("dev.traverse.deprecated", "evt-001"))
1149 .expect_err("deprecated publish must fail");
1150 assert!(matches!(err, EventError::LifecycleViolation(_)));
1151
1152 let draft =
1153 InProcessBroker::new(make_catalog("dev.traverse.draft", LifecycleStatus::Draft))
1154 .expect("broker must be created");
1155 let err = draft
1156 .publish(sample_event("dev.traverse.draft", "evt-001"))
1157 .expect_err("draft publish must fail");
1158 assert!(matches!(err, EventError::LifecycleViolation(_)));
1159 }
1160
1161 #[test]
1162 fn enforcement_rejects_invalid_events_and_retains_sanitized_evidence() {
1163 let event_type = "dev.traverse.orders.created";
1164 let broker = InProcessBroker::with_clock_and_validation(
1165 make_catalog(event_type, LifecycleStatus::Active),
1166 BrokerConfig::default(),
1167 Arc::new(SystemClock),
1168 EventValidationMode::Enforcement,
1169 )
1170 .expect("broker must be created");
1171 let mut invalid = sample_event(event_type, "evt-invalid");
1172 invalid.owner.clear();
1173 invalid.data = serde_json::json!({"customer_email": "private@example.test"});
1174
1175 let error = broker
1176 .publish(invalid)
1177 .expect_err("enforcement must reject a missing owner");
1178 assert!(matches!(error, EventError::ValidationRejected(code) if code == "EVP-005"));
1179 let evidence = broker.validation_evidence();
1180 assert_eq!(evidence.len(), 1);
1181 assert_eq!(evidence[0].contract_id, event_type);
1182 assert_eq!(evidence[0].diagnostics[0].code, "EVP-005");
1183 assert!(!format!("{evidence:?}").contains("private@example.test"));
1184 let quarantine = broker.quarantine_records();
1185 assert_eq!(quarantine.len(), 1);
1186 assert_eq!(quarantine[0].evidence, evidence[0]);
1187 assert!(!format!("{quarantine:?}").contains("private@example.test"));
1188 assert_eq!(
1189 broker.metrics(),
1190 EventRuntimeMetrics {
1191 publications: 0,
1192 deliveries: 0,
1193 validation_failures: 1,
1194 quarantines: 1,
1195 }
1196 );
1197 }
1198
1199 #[test]
1200 fn migration_records_invalid_event_evidence_without_rejecting_delivery() {
1201 let event_type = "dev.traverse.orders.created";
1202 let broker = InProcessBroker::new(make_catalog(event_type, LifecycleStatus::Active))
1203 .expect("broker must be created");
1204 let mut invalid = sample_event(event_type, "evt-migration");
1205 invalid.owner.clear();
1206
1207 broker
1208 .publish(invalid)
1209 .expect("migration mode must preserve delivery");
1210 assert_eq!(broker.validation_evidence().len(), 1);
1211 assert!(broker.quarantine_records().is_empty());
1212 assert_eq!(broker.metrics().validation_failures, 1);
1213 assert_eq!(broker.metrics().quarantines, 0);
1214 }
1215
1216 #[test]
1217 fn broker_lock_poisoning_surfaces_lifecycle_violation() {
1218 let broker =
1219 InProcessBroker::new(make_catalog("dev.traverse.poison", LifecycleStatus::Active))
1220 .expect("broker must be created");
1221
1222 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1223 let _guard = broker.state.lock().unwrap();
1224 panic!("poison lock");
1225 }));
1226
1227 let err = broker
1228 .publish(sample_event("dev.traverse.poison", "evt-001"))
1229 .expect_err("poisoned publish must fail");
1230 assert!(matches!(err, EventError::LifecycleViolation(_)));
1231
1232 let err = broker
1233 .subscribe("dev.traverse.poison", "0")
1234 .expect_err("poisoned subscribe must fail");
1235 assert!(matches!(err, EventError::LifecycleViolation(_)));
1236
1237 let err = broker
1238 .poll("sub-1", 1)
1239 .expect_err("poisoned poll must fail");
1240 assert!(matches!(err, EventError::LifecycleViolation(_)));
1241
1242 let err = broker
1243 .cancel("sub-1")
1244 .expect_err("poisoned cancel must fail");
1245 assert!(matches!(err, EventError::LifecycleViolation(_)));
1246 }
1247
1248 #[derive(Debug)]
1249 struct ManualClock(std::sync::Mutex<std::time::SystemTime>);
1250
1251 impl ManualClock {
1252 fn new(now: std::time::SystemTime) -> Self {
1253 Self(std::sync::Mutex::new(now))
1254 }
1255
1256 fn advance(&self, by: Duration) {
1257 if let Ok(mut guard) = self.0.lock()
1258 && let Some(next) = guard.checked_add(by)
1259 {
1260 *guard = next;
1261 }
1262 }
1263
1264 fn set(&self, now: std::time::SystemTime) {
1265 if let Ok(mut guard) = self.0.lock() {
1266 *guard = now;
1267 }
1268 }
1269 }
1270
1271 impl BrokerClock for ManualClock {
1272 fn now(&self) -> std::time::SystemTime {
1273 self.0
1274 .lock()
1275 .ok()
1276 .map_or(std::time::SystemTime::UNIX_EPOCH, |guard| *guard)
1277 }
1278 }
1279
1280 #[test]
1281 fn clock_regression_does_not_break_retention_pruning() {
1282 let clock = Arc::new(ManualClock::new(std::time::SystemTime::UNIX_EPOCH));
1283 let broker = InProcessBroker::with_clock(
1284 make_catalog("dev.traverse.clock", LifecycleStatus::Active),
1285 BrokerConfig {
1286 retention_window: Duration::from_mins(1),
1287 max_queue_len: 16,
1288 },
1289 clock.clone(),
1290 )
1291 .expect("broker must be created");
1292
1293 clock.set(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(10));
1294 broker
1295 .publish(sample_event("dev.traverse.clock", "evt-001"))
1296 .expect("publish must succeed");
1297
1298 clock.set(std::time::SystemTime::UNIX_EPOCH);
1300 broker
1301 .publish(sample_event("dev.traverse.clock", "evt-002"))
1302 .expect("publish must succeed");
1303 }
1304
1305 #[test]
1306 fn publish_pruning_syncs_subscription_queues_and_skips_other_event_types() {
1307 let catalog = Arc::new(EventCatalog::new());
1308 catalog
1309 .register(EventCatalogEntry {
1310 event_type: "dev.traverse.a".to_string(),
1311 owner: "cap.test".to_string(),
1312 version: "1.0.0".to_string(),
1313 lifecycle_status: LifecycleStatus::Active,
1314 consumer_count: 0,
1315 })
1316 .expect("register must succeed");
1317 catalog
1318 .register(EventCatalogEntry {
1319 event_type: "dev.traverse.b".to_string(),
1320 owner: "cap.test".to_string(),
1321 version: "1.0.0".to_string(),
1322 lifecycle_status: LifecycleStatus::Active,
1323 consumer_count: 0,
1324 })
1325 .expect("register must succeed");
1326
1327 let clock = Arc::new(ManualClock::new(std::time::SystemTime::UNIX_EPOCH));
1328 let broker = InProcessBroker::with_clock(
1329 catalog,
1330 BrokerConfig {
1331 retention_window: Duration::from_secs(5),
1332 max_queue_len: 64,
1333 },
1334 clock.clone(),
1335 )
1336 .expect("broker must be created");
1337
1338 let sub_a = broker
1339 .subscribe("dev.traverse.a", "1")
1340 .expect("subscribe must succeed");
1341 let sub_b = broker
1342 .subscribe("dev.traverse.b", "0")
1343 .expect("subscribe must succeed");
1344
1345 broker
1346 .publish(sample_event("dev.traverse.a", "evt-001"))
1347 .expect("publish must succeed");
1348 clock.advance(Duration::from_secs(1));
1349 broker
1350 .publish(sample_event("dev.traverse.a", "evt-002"))
1351 .expect("publish must succeed");
1352 clock.advance(Duration::from_secs(1));
1353 broker
1354 .publish(sample_event("dev.traverse.a", "evt-003"))
1355 .expect("publish must succeed");
1356
1357 clock.advance(Duration::from_secs(5));
1359 broker
1360 .publish(sample_event("dev.traverse.a", "evt-004"))
1361 .expect("publish must succeed");
1362
1363 let err = broker
1364 .poll(&sub_a.subscription_id, 10)
1365 .expect_err("poll must surface cursor_expired after retention pruning");
1366 let oldest_available_cursor = cursor_expired_oldest(&err).expect("must be cursor_expired");
1367
1368 let sub_a_resumed = broker
1369 .subscribe("dev.traverse.a", &oldest_available_cursor)
1370 .expect("subscribe must succeed");
1371 let poll_a = broker
1372 .poll(&sub_a_resumed.subscription_id, 10)
1373 .expect("poll must succeed");
1374 assert!(
1375 poll_a
1376 .events
1377 .first()
1378 .is_some_and(|e| e.event.id == "evt-003"),
1379 "queue must resume from oldest retained event"
1380 );
1381
1382 let poll_b = broker
1383 .poll(&sub_b.subscription_id, 10)
1384 .expect("poll must succeed");
1385 assert!(
1386 poll_b.events.is_empty(),
1387 "event_type mismatch must not enqueue"
1388 );
1389
1390 let other_err = broker
1392 .poll("sub-missing", 10)
1393 .expect_err("poll must fail when subscription is missing");
1394 assert!(cursor_expired_oldest(&other_err).is_none());
1395 }
1396
1397 #[test]
1398 fn subscribe_replays_events_from_existing_buffer() {
1399 let clock = Arc::new(ManualClock::new(std::time::SystemTime::UNIX_EPOCH));
1400 let broker = InProcessBroker::with_clock(
1401 make_catalog("dev.traverse.replay", LifecycleStatus::Active),
1402 BrokerConfig {
1403 retention_window: Duration::from_secs(5),
1404 max_queue_len: 64,
1405 },
1406 clock,
1407 )
1408 .expect("broker must be created");
1409
1410 broker
1411 .publish(sample_event("dev.traverse.replay", "evt-001"))
1412 .expect("publish must succeed");
1413
1414 let sub = broker
1415 .subscribe("dev.traverse.replay", "0")
1416 .expect("subscribe must succeed");
1417 let poll = broker
1418 .poll(&sub.subscription_id, 10)
1419 .expect("poll must succeed");
1420 assert_eq!(poll.events.len(), 1);
1421 assert_eq!(poll.events[0].event.id, "evt-001");
1422 }
1423
1424 #[test]
1425 fn subscribe_rejects_cursor_expired_when_buffer_non_empty() {
1426 let clock = Arc::new(ManualClock::new(std::time::SystemTime::UNIX_EPOCH));
1427 let broker = InProcessBroker::with_clock(
1428 make_catalog("dev.traverse.expire", LifecycleStatus::Active),
1429 BrokerConfig {
1430 retention_window: Duration::from_secs(5),
1431 max_queue_len: 64,
1432 },
1433 clock.clone(),
1434 )
1435 .expect("broker must be created");
1436
1437 for i in 1..=5 {
1438 broker
1439 .publish(sample_event("dev.traverse.expire", &format!("evt-{i:03}")))
1440 .expect("publish must succeed");
1441 clock.advance(Duration::from_secs(1));
1442 }
1443
1444 clock.advance(Duration::from_secs(5));
1446
1447 let err = broker
1448 .subscribe("dev.traverse.expire", "1")
1449 .expect_err("subscribe must fail with cursor_expired");
1450 assert!(matches!(err, EventError::CursorExpired { .. }));
1451 }
1452
1453 #[test]
1454 fn poll_with_zero_max_events_returns_empty() {
1455 let broker =
1456 InProcessBroker::new(make_catalog("dev.traverse.poll0", LifecycleStatus::Active))
1457 .expect("broker must be created");
1458 let sub = broker
1459 .subscribe("dev.traverse.poll0", "0")
1460 .expect("subscribe must succeed");
1461 let poll = broker
1462 .poll(&sub.subscription_id, 0)
1463 .expect("poll must succeed");
1464 assert!(poll.events.is_empty());
1465 }
1466
1467 #[test]
1468 fn poll_prunes_subscription_queue_based_on_retention() {
1469 let clock = Arc::new(ManualClock::new(std::time::SystemTime::UNIX_EPOCH));
1470 let broker = InProcessBroker::with_clock(
1471 make_catalog("dev.traverse.pollprune", LifecycleStatus::Active),
1472 BrokerConfig {
1473 retention_window: Duration::from_secs(5),
1474 max_queue_len: 64,
1475 },
1476 clock.clone(),
1477 )
1478 .expect("broker must be created");
1479
1480 let sub = broker
1481 .subscribe("dev.traverse.pollprune", "0")
1482 .expect("subscribe must succeed");
1483
1484 broker
1485 .publish(sample_event("dev.traverse.pollprune", "evt-001"))
1486 .expect("publish must succeed");
1487 clock.advance(Duration::from_secs(4));
1488 broker
1489 .publish(sample_event("dev.traverse.pollprune", "evt-002"))
1490 .expect("publish must succeed");
1491
1492 clock.advance(Duration::from_secs(3));
1494
1495 let poll = broker
1496 .poll(&sub.subscription_id, 10)
1497 .expect("poll must succeed");
1498 assert_eq!(poll.events.len(), 1);
1499 assert_eq!(poll.events[0].event.id, "evt-002");
1500 }
1501
1502 #[test]
1503 fn cancel_unknown_subscription_returns_not_found() {
1504 let broker = InProcessBroker::new(make_catalog(
1505 "dev.traverse.cancel-miss",
1506 LifecycleStatus::Active,
1507 ))
1508 .expect("broker must be created");
1509 let err = broker.cancel("sub-missing").expect_err("cancel must fail");
1510 assert!(matches!(err, EventError::SubscriptionNotFound(_)));
1511 }
1512
1513 #[test]
1514 fn publish_tolerates_a_stale_event_type_index_entry() {
1515 let event_type = "dev.traverse.stale-index";
1516 let broker = InProcessBroker::new(make_catalog(event_type, LifecycleStatus::Active))
1517 .expect("broker must be created");
1518 let subscription = broker
1519 .subscribe(event_type, "0")
1520 .expect("subscribe must succeed");
1521
1522 broker
1523 .state
1524 .lock()
1525 .expect("broker lock must be available")
1526 .subscriptions
1527 .remove(&subscription.subscription_id);
1528
1529 broker
1530 .publish(sample_event(event_type, "evt-stale-index"))
1531 .expect("stale index entry must not prevent publication");
1532 }
1533
1534 #[test]
1535 fn cancel_removes_an_empty_event_type_index() {
1536 let event_type = "dev.traverse.cancel-index";
1537 let broker = InProcessBroker::new(make_catalog(event_type, LifecycleStatus::Active))
1538 .expect("broker must be created");
1539 let subscription = broker
1540 .subscribe(event_type, "0")
1541 .expect("subscribe must succeed");
1542
1543 broker
1544 .cancel(&subscription.subscription_id)
1545 .expect("cancel must succeed");
1546
1547 let state = broker.state.lock().expect("broker lock must be available");
1548 assert!(!state.subscriptions_by_event_type.contains_key(event_type));
1549 }
1550
1551 #[test]
1552 fn cancel_tolerates_a_missing_event_type_index_entry() {
1553 let event_type = "dev.traverse.cancel-missing-index";
1554 let broker = InProcessBroker::new(make_catalog(event_type, LifecycleStatus::Active))
1555 .expect("broker must be created");
1556 let subscription = broker
1557 .subscribe(event_type, "0")
1558 .expect("subscribe must succeed");
1559
1560 broker
1561 .state
1562 .lock()
1563 .expect("broker lock must be available")
1564 .subscriptions_by_event_type
1565 .remove(event_type);
1566
1567 broker
1568 .cancel(&subscription.subscription_id)
1569 .expect("missing index entry must not prevent cancellation");
1570 }
1571}