Skip to main content

traverse_runtime/events/
broker.rs

1//! Synchronous in-process event broker.
2//!
3//! Governed by spec 026-event-broker and spec 036-event-subscription-replay.
4
5use 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
20/// Clock abstraction used by the broker for retention pruning.
21pub 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/// Broker runtime configuration.
35#[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    /// See [`EventBroker::seed_restart_floor`].
76    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/// Sanitized observation that a broker delivered one governed event.
85///
86/// This is runtime evidence, not a catalog declaration. It intentionally
87/// excludes the event payload and authenticated subject data.
88#[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/// Sanitized record written when enforcement rejects an event envelope.
100///
101/// The record is deliberately distinct from migration diagnostics: it models
102/// the governed quarantine stream without retaining the rejected payload.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct EventQuarantineRecord {
105    pub evidence: EventValidationEvidence,
106}
107
108/// Portable, OpenTelemetry-compatible event boundary evidence.
109///
110/// Host adapters can map this stable, payload-free shape to their chosen
111/// OpenTelemetry SDK. The in-process broker retains it for deterministic
112/// conformance tests and never exports an event payload or subject identity.
113#[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/// Counter snapshot for OpenTelemetry-compatible event runtime evidence.
131#[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
139/// Synchronous, in-memory implementation of [`EventBroker`].
140///
141/// The broker stores a bounded retention buffer per event type and maintains a
142/// bounded delivery queue per subscription. Subscribers poll for events using a
143/// broker-issued subscription id and a cursor.
144pub 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    /// Create a new broker backed by the given catalog.
160    ///
161    /// # Errors
162    ///
163    /// Returns [`EventError::InvalidRetentionWindow`] when the provided configuration is invalid.
164    pub fn new(catalog: Arc<EventCatalog>) -> Result<Self, EventError> {
165        Self::with_clock(catalog, BrokerConfig::default(), Arc::new(SystemClock))
166    }
167
168    /// Create a broker with explicit configuration and clock.
169    ///
170    /// # Errors
171    ///
172    /// Returns [`EventError::InvalidRetentionWindow`] when the provided configuration is invalid.
173    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    /// Create a broker with an explicit governed-event enforcement policy.
182    ///
183    /// Migration mode records violations without interrupting existing traffic;
184    /// enforcement mode rejects invalid envelopes.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`EventError::InvalidRetentionWindow`] when the configuration
189    /// cannot maintain replay and queue guarantees.
190    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    /// Returns sanitized validation and quarantine evidence. Payload data is
217    /// never retained through this interface.
218    #[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    /// Returns sanitized enforcement rejections prepared for governed quarantine.
227    #[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    /// Returns sanitized runtime delivery observations for catalog reconciliation.
236    #[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    /// Returns deterministic, sanitized boundary telemetry for host export.
245    #[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    /// Returns a deterministic counter snapshot for event runtime evidence.
254    #[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    /// Subscribe with the consuming capability identity needed for observed lineage.
325    ///
326    /// The identity is runtime evidence only; it does not declare a catalog relationship.
327    ///
328    /// # Errors
329    ///
330    /// Returns [`EventError::LifecycleViolation`] when `consumer_id` is empty,
331    /// plus the same errors as [`EventBroker::subscribe_for_subject`].
332    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    /// Shared implementation for `publish` and `publish_with_cursor`.
389    /// `assigned_cursor`, when given, is adopted as this event's cursor
390    /// instead of self-incrementing the per-type counter (spec 066 FR-007:
391    /// keeps live-delivery cursors consistent with the durable journal's
392    /// cursor space). The per-type counter still tracks the highest cursor
393    /// ever used so `validate_from_cursor`'s empty-buffer fallback remains
394    /// correct regardless of cursor source.
395    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            // Duplicate emissions are silently discarded.
442            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        // Buffer is empty after pruning; nothing to sync.
547        return;
548    };
549
550    // Sync per-subscription queues so they don't deliver events that are no longer retained.
551    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            // Cursor is now outside the retention window; keep it as-is so poll can surface cursor_expired.
564        }
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 the buffer is empty but we have published events before, treat cursors behind the last
597    // observed cursor as expired to avoid silent gaps.
598    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    /// Publish `event` to all registered subscribers.
636    ///
637    /// # Errors
638    ///
639    /// - [`EventError::UnregisteredEventType`] if the event type is not in the catalog.
640    /// - [`EventError::LifecycleViolation`] if the catalog entry is `Draft` or `Deprecated`.
641    fn publish(&self, event: TraverseEvent) -> Result<(), EventError> {
642        self.publish_internal(&event, None)
643    }
644
645    /// Publish `event`, adopting `cursor` as this broker's own cursor for it
646    /// instead of self-assigning the next per-type sequence value. Used by
647    /// [`DurableBroker`](super::durable::DurableBroker) so live-delivery
648    /// cursors stay numerically consistent with the durable journal's cursor
649    /// space (spec 066 FR-007): a cursor obtained during live polling
650    /// remains valid when a later `poll` falls back to durable replay.
651    ///
652    /// # Errors
653    ///
654    /// Returns [`EventError::InvalidCursor`] when `cursor` is not a base-10
655    /// unsigned integer, plus the same errors as [`Self::publish`].
656    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    /// Create a subscription for `event_type` starting from `from_cursor`.
662    ///
663    /// The event type must already be registered in the catalog.
664    ///
665    /// # Errors
666    ///
667    /// Returns [`EventError::UnregisteredEventType`] if the event type is not catalogued.
668    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    /// Poll a subscription for up to `max_events`.
673    ///
674    /// # Errors
675    ///
676    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    /// Cancel a subscription.
776    ///
777    /// # Errors
778    ///
779    /// Returns [`EventError::SubscriptionNotFound`] if the subscription id is unknown.
780    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        // The default `publish_with_cursor` ignores the supplied cursor and
983        // behaves exactly like `publish` (self-assigning cursor "1").
984        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        // The default `seed_restart_floor` is a no-op; exercise it alongside
997        // this wrapper's other pass-through trait methods.
998        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        // Move time backwards to force duration_since() to hit the error path.
1299        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        // Jump forward so evt-001 and evt-002 are outside retention; evt-003 is retained.
1358        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        // Also cover the non-cursor_expired branch in the extraction logic above.
1391        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        // Advance so only the last event remains within retention.
1445        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        // Advance so evt-001 is outside retention but evt-002 is retained.
1493        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}