1use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
19use std::sync::{Mutex, OnceLock};
20
21use serde::{Deserialize, Serialize};
22
23use crate::core::context_kernel::bounded::BoundedQueue;
24
25#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
29#[serde(tag = "type")]
30pub enum OclaEvent {
31 RequestCompleted {
33 model: String,
34 input_tokens: u64,
35 output_tokens: u64,
36 duration_ms: u64,
37 session_id: Option<String>,
38 },
39 FeedbackRecorded {
41 session_id: String,
42 outcome: FeedbackOutcome,
43 tool: Option<String>,
44 },
45 ThresholdShift {
47 language: String,
48 old_value: f64,
49 new_value: f64,
50 metric: ThresholdMetric,
51 },
52 CompressionApplied {
54 path: Option<String>,
55 before_tokens: u64,
56 after_tokens: u64,
57 strategy: String,
58 },
59 SavingsRecorded {
61 input_saved: u64,
62 output_saved: u64,
63 source: SavingsSource,
64 attribution_id: Option<String>,
65 evidence_class: Option<String>,
66 measurement_method: Option<String>,
67 },
68 IntentClassified {
71 tier: String,
72 confidence: f64,
73 reasoning: String,
74 },
75 OutcomeRecorded {
77 session_id: String,
78 accepted: bool,
79 implicit: bool,
80 },
81 ResponseOptimized {
83 cache_hit: bool,
84 is_duplicate: bool,
85 tokens_saved: u64,
86 },
87 ModelRouted {
90 requested_model: String,
91 routed_model: String,
92 tier: String,
93 model_changed: bool,
94 },
95 AgentChainEvent {
97 agent_id: String,
98 action: String,
99 parent_agent: Option<String>,
100 },
101 CrossAgentStubServed {
103 path: String,
104 tokens_saved: u64,
105 serving_agent: String,
106 original_agent: String,
107 },
108}
109
110#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
112#[serde(rename_all = "snake_case")]
113pub enum FeedbackOutcome {
114 Accept,
115 Reject,
116 Partial,
117}
118
119#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
121#[serde(rename_all = "snake_case")]
122pub enum ThresholdMetric {
123 Entropy,
124 Jaccard,
125}
126
127#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "snake_case")]
130pub enum SavingsSource {
131 Compression,
132 Cache,
133 Routing,
134 Verbosity,
135 ResponseCache,
136}
137
138#[derive(Clone, Debug, Serialize, Deserialize)]
142pub struct OclaBusRecord {
143 pub id: u64,
144 pub timestamp_ms: u64,
145 pub event: OclaEvent,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
150#[serde(rename_all = "snake_case")]
151pub enum OverflowPolicy {
152 DropOldest,
154 DropNewest,
156 Backpressure,
158}
159
160impl OverflowPolicy {
161 fn from_env() -> Self {
162 match std::env::var("LEANCTX_BUS_OVERFLOW").as_deref() {
163 Ok("drop_newest") => Self::DropNewest,
164 Ok("backpressure") => Self::Backpressure,
165 Ok("drop_oldest") | Err(_) => Self::DropOldest,
166 Ok(value) => {
167 tracing::warn!(
168 value,
169 "invalid LEANCTX_BUS_OVERFLOW value; using drop_oldest"
170 );
171 Self::DropOldest
172 }
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
179pub struct OverflowEvent {
180 pub policy: OverflowPolicy,
182 pub dropped_count: usize,
184 pub queue_capacity: usize,
186}
187
188pub struct OclaBus {
195 enabled: AtomicBool,
196 ring: Mutex<BoundedQueue<OclaBusRecord>>,
197 capacity: usize,
198 next_id: AtomicU64,
199 emitted_count: AtomicU64,
200 overflow_policy: OverflowPolicy,
201 overflow_count: AtomicUsize,
202}
203
204impl OclaBus {
205 fn new(capacity: usize) -> Self {
207 Self::new_with_policy(capacity, OverflowPolicy::from_env())
208 }
209
210 fn new_with_policy(capacity: usize, overflow_policy: OverflowPolicy) -> Self {
211 Self {
212 enabled: AtomicBool::new(false),
213 ring: Mutex::new(BoundedQueue::new(capacity)),
214 capacity,
215 next_id: AtomicU64::new(1),
216 emitted_count: AtomicU64::new(0),
217 overflow_policy,
218 overflow_count: AtomicUsize::new(0),
219 }
220 }
221
222 pub fn scoped(capacity: usize) -> Self {
224 let bus = Self::new_with_policy(capacity, OverflowPolicy::DropOldest);
225 bus.enabled.store(true, Ordering::Relaxed);
226 bus
227 }
228
229 #[cfg(test)]
230 fn scoped_with_policy(capacity: usize, overflow_policy: OverflowPolicy) -> Self {
231 let bus = Self::new_with_policy(capacity, overflow_policy);
232 bus.enabled.store(true, Ordering::Relaxed);
233 bus
234 }
235
236 pub fn enable(&self) {
238 self.enabled.store(true, Ordering::Release);
239 }
240
241 pub fn disable(&self) {
243 self.enabled.store(false, Ordering::Release);
244 }
245
246 #[inline]
248 pub fn is_enabled(&self) -> bool {
249 self.enabled.load(Ordering::Acquire)
250 }
251
252 #[inline]
254 pub fn emit_if_enabled(&self, event: OclaEvent) -> u64 {
255 if !self.is_enabled() {
256 return 0;
257 }
258 self.emit_unconditional(event)
259 }
260
261 fn emit_unconditional(&self, event: OclaEvent) -> u64 {
264 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
265 let record = OclaBusRecord {
266 id,
267 timestamp_ms: current_timestamp_ms(),
268 event,
269 };
270
271 let mut ring = self
272 .ring
273 .lock()
274 .unwrap_or_else(std::sync::PoisonError::into_inner);
275 self.push_record(&mut ring, record);
276 self.emitted_count.fetch_add(1, Ordering::Relaxed);
277 id
278 }
279
280 fn emit_durable_if_enabled(&self, event: OclaEvent) -> u64 {
281 if !self.is_enabled() {
282 return 0;
283 }
284
285 let mut id = self.next_id.load(Ordering::Relaxed);
290 loop {
291 let Some(next) = id.checked_add(1) else {
292 tracing::error!("OCLA event ID space exhausted");
293 return 0;
294 };
295 match self
296 .next_id
297 .compare_exchange_weak(id, next, Ordering::Relaxed, Ordering::Relaxed)
298 {
299 Ok(_) => break,
300 Err(current) => id = current,
301 }
302 }
303 let record = OclaBusRecord {
304 id,
305 timestamp_ms: current_timestamp_ms(),
306 event,
307 };
308 if let Err(error) = crate::core::context_os::try_runtime().and_then(|runtime| {
309 runtime.bus.append_ocla_observation(
310 crate::core::context_os::observation::producer_instance(),
311 &record,
312 )
313 }) {
314 tracing::warn!("canonical OCLA observation persistence failed: {error}");
315 return 0;
316 }
317
318 self.emitted_count.fetch_add(1, Ordering::Relaxed);
319 if let Ok(mut ring) = crate::core::context_os::observation::lock_observation(&self.ring) {
322 self.push_record(&mut ring, record);
323 } else {
324 tracing::warn!(
325 "OCLA projection unavailable; committed observation retained in ContextBus"
326 );
327 }
328 id
329 }
330
331 fn push_record(&self, ring: &mut BoundedQueue<OclaBusRecord>, record: OclaBusRecord) {
332 let _dropped = if ring.is_full() {
333 let dropped_count = self.overflow_count.fetch_add(1, Ordering::Relaxed) + 1;
334 let overflow = OverflowEvent {
335 policy: self.overflow_policy,
336 dropped_count,
337 queue_capacity: self.capacity,
338 };
339
340 match self.overflow_policy {
341 OverflowPolicy::DropOldest => ring.push(record),
342 OverflowPolicy::DropNewest => Some(record),
343 OverflowPolicy::Backpressure => {
344 tracing::warn!(
345 dropped_count = overflow.dropped_count,
346 queue_capacity = overflow.queue_capacity,
347 "OCLA bus backpressure requested; dropping newest event"
348 );
349 Some(record)
350 }
351 }
352 } else {
353 ring.push(record)
354 };
355 }
356
357 pub fn drain(&self) -> Vec<OclaBusRecord> {
359 let mut ring = self
360 .ring
361 .lock()
362 .unwrap_or_else(std::sync::PoisonError::into_inner);
363 let count = ring.len();
364 ring.drain_oldest(count)
365 }
366
367 pub fn events_since(&self, after_id: u64) -> Vec<OclaBusRecord> {
369 let ring = self
370 .ring
371 .lock()
372 .unwrap_or_else(std::sync::PoisonError::into_inner);
373 ring.iter().filter(|r| r.id > after_id).cloned().collect()
374 }
375
376 pub fn latest(&self, n: usize) -> Vec<OclaBusRecord> {
378 let ring = self
379 .ring
380 .lock()
381 .unwrap_or_else(std::sync::PoisonError::into_inner);
382 let start = ring.len().saturating_sub(n);
383 ring.iter().skip(start).cloned().collect()
384 }
385
386 pub fn len(&self) -> usize {
388 self.ring
389 .lock()
390 .unwrap_or_else(std::sync::PoisonError::into_inner)
391 .len()
392 }
393
394 pub fn is_empty(&self) -> bool {
396 self.len() == 0
397 }
398
399 pub fn total_emitted(&self) -> u64 {
401 self.emitted_count.load(Ordering::Relaxed)
402 }
403
404 pub fn overflow_count(&self) -> usize {
406 self.overflow_count.load(Ordering::Relaxed)
407 }
408
409 pub fn overflow_policy(&self) -> OverflowPolicy {
411 self.overflow_policy
412 }
413}
414
415impl std::fmt::Debug for OclaBus {
416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417 let ring_len = self.ring.lock().map_or(0, |r| r.len());
418 f.debug_struct("OclaBus")
419 .field("enabled", &self.is_enabled())
420 .field("ring", &format_args!("[{ring_len} events]"))
421 .field("capacity", &self.capacity)
422 .field("next_id", &self.next_id.load(Ordering::Relaxed))
423 .field("emitted_count", &self.total_emitted())
424 .field("overflow_policy", &self.overflow_policy)
425 .field("overflow_count", &self.overflow_count())
426 .finish()
427 }
428}
429
430const DEFAULT_CAPACITY: usize = 1000;
433
434fn global_bus() -> &'static OclaBus {
435 static INSTANCE: OnceLock<OclaBus> = OnceLock::new();
436 INSTANCE.get_or_init(|| OclaBus::new(DEFAULT_CAPACITY))
437}
438
439#[inline]
442pub fn emit(event: OclaEvent) -> u64 {
443 global_bus().emit_durable_if_enabled(event)
444}
445
446pub fn enable() {
448 global_bus().enable();
449}
450
451pub fn disable() {
453 global_bus().disable();
454}
455
456#[inline]
458pub fn is_enabled() -> bool {
459 global_bus().is_enabled()
460}
461
462pub fn events_since(after_id: u64) -> Vec<OclaBusRecord> {
464 global_bus().events_since(after_id)
465}
466
467pub fn latest(n: usize) -> Vec<OclaBusRecord> {
469 global_bus().latest(n)
470}
471
472pub fn total_emitted() -> u64 {
474 global_bus().total_emitted()
475}
476
477pub fn overflow_count() -> usize {
479 global_bus().overflow_count()
480}
481
482pub fn overflow_policy() -> OverflowPolicy {
484 global_bus().overflow_policy()
485}
486
487pub fn emit_and_bridge(event: OclaEvent) -> u64 {
493 emit(event)
494}
495
496fn current_timestamp_ms() -> u64 {
499 use std::time::{SystemTime, UNIX_EPOCH};
500 SystemTime::now()
501 .duration_since(UNIX_EPOCH)
502 .unwrap_or_default()
503 .as_millis() as u64
504}
505
506#[cfg(test)]
509pub mod tests {
510 use super::*;
511
512 fn savings_event(input_saved: u64) -> OclaEvent {
513 OclaEvent::SavingsRecorded {
514 input_saved,
515 output_saved: 0,
516 source: SavingsSource::Compression,
517 attribution_id: None,
518 evidence_class: None,
519 measurement_method: None,
520 }
521 }
522
523 #[test]
524 fn disabled_bus_returns_zero() {
525 let bus = OclaBus::new(16);
526 assert!(!bus.is_enabled());
527 let id = bus.emit_if_enabled(OclaEvent::RequestCompleted {
528 model: "gpt-4o".into(),
529 input_tokens: 100,
530 output_tokens: 50,
531 duration_ms: 200,
532 session_id: None,
533 });
534 assert_eq!(id, 0);
535 assert!(bus.is_empty());
536 }
537
538 #[test]
539 fn enabled_bus_records_events() {
540 let bus = OclaBus::scoped(16);
541 let id = bus.emit_if_enabled(OclaEvent::ModelRouted {
542 requested_model: "gpt-4o".into(),
543 routed_model: "gpt-4o-mini".into(),
544 tier: "fast".into(),
545 model_changed: true,
546 });
547 assert!(id > 0);
548 assert_eq!(bus.len(), 1);
549 }
550
551 #[test]
552 fn scoped_bus_is_isolated() {
553 let bus1 = OclaBus::scoped(8);
554 let bus2 = OclaBus::scoped(8);
555
556 bus1.emit_if_enabled(OclaEvent::ResponseOptimized {
557 cache_hit: true,
558 is_duplicate: false,
559 tokens_saved: 42,
560 });
561
562 assert_eq!(bus1.len(), 1);
563 assert_eq!(bus2.len(), 0, "scoped buses are isolated");
564 }
565
566 #[test]
567 fn test_bounded_queue_replaces_vecdeque() {
568 let bus = OclaBus::scoped(2);
569 bus.emit_if_enabled(savings_event(1));
570 bus.emit_if_enabled(savings_event(2));
571
572 assert_eq!(bus.len(), 2);
573 assert_eq!(bus.drain().len(), 2);
574 assert!(bus.is_empty());
575 }
576
577 #[test]
578 fn test_overflow_drop_oldest() {
579 let bus = OclaBus::scoped_with_policy(3, OverflowPolicy::DropOldest);
580 let id1 = bus.emit_if_enabled(OclaEvent::SavingsRecorded {
581 input_saved: 10,
582 output_saved: 5,
583 source: SavingsSource::Compression,
584 attribution_id: None,
585 evidence_class: None,
586 measurement_method: None,
587 });
588 bus.emit_if_enabled(OclaEvent::SavingsRecorded {
589 input_saved: 20,
590 output_saved: 10,
591 source: SavingsSource::Cache,
592 attribution_id: None,
593 evidence_class: None,
594 measurement_method: None,
595 });
596 bus.emit_if_enabled(OclaEvent::SavingsRecorded {
597 input_saved: 30,
598 output_saved: 15,
599 source: SavingsSource::Routing,
600 attribution_id: None,
601 evidence_class: None,
602 measurement_method: None,
603 });
604 bus.emit_if_enabled(OclaEvent::SavingsRecorded {
606 input_saved: 40,
607 output_saved: 20,
608 source: SavingsSource::Verbosity,
609 attribution_id: None,
610 evidence_class: None,
611 measurement_method: None,
612 });
613
614 assert_eq!(bus.len(), 3);
615 let events = bus.events_since(0);
616 assert!(events.iter().all(|r| r.id > id1), "oldest evicted");
617 }
618
619 #[test]
620 fn test_overflow_drop_newest() {
621 let bus = OclaBus::scoped_with_policy(2, OverflowPolicy::DropNewest);
622 bus.emit_if_enabled(savings_event(1));
623 bus.emit_if_enabled(savings_event(2));
624 let rejected_id = bus.emit_if_enabled(savings_event(3));
625
626 let retained_ids = bus
627 .events_since(0)
628 .into_iter()
629 .map(|record| record.id)
630 .collect::<Vec<_>>();
631 assert_eq!(retained_ids, vec![1, 2]);
632 assert_eq!(rejected_id, 3);
633 }
634
635 #[test]
636 fn test_overflow_metrics() {
637 let bus = OclaBus::scoped_with_policy(1, OverflowPolicy::DropNewest);
638 bus.emit_if_enabled(savings_event(1));
639 bus.emit_if_enabled(savings_event(2));
640 bus.emit_if_enabled(savings_event(3));
641
642 assert_eq!(bus.overflow_count(), 2);
643 assert_eq!(bus.overflow_policy(), OverflowPolicy::DropNewest);
644 }
645
646 #[test]
647 fn test_overflow_policy_from_env() {
648 let _env_lock = crate::core::data_dir::test_env_lock();
649 let previous = std::env::var_os("LEANCTX_BUS_OVERFLOW");
650 crate::test_env::set_var("LEANCTX_BUS_OVERFLOW", "backpressure");
651
652 let bus = OclaBus::new(1);
653 assert_eq!(bus.overflow_policy(), OverflowPolicy::Backpressure);
654
655 if let Some(value) = previous {
656 crate::test_env::set_var("LEANCTX_BUS_OVERFLOW", value);
657 } else {
658 crate::test_env::remove_var("LEANCTX_BUS_OVERFLOW");
659 }
660 }
661
662 #[test]
663 fn drain_consumes_all_events() {
664 let bus = OclaBus::scoped(16);
665 bus.emit_if_enabled(OclaEvent::IntentClassified {
666 tier: "fast".into(),
667 confidence: 0.9,
668 reasoning: "simple query".into(),
669 });
670 bus.emit_if_enabled(OclaEvent::IntentClassified {
671 tier: "premium".into(),
672 confidence: 0.8,
673 reasoning: "complex architecture".into(),
674 });
675
676 let drained = bus.drain();
677 assert_eq!(drained.len(), 2);
678 assert!(bus.is_empty(), "drain consumes events");
679 }
680
681 #[test]
682 fn events_since_filters_by_id() {
683 let bus = OclaBus::scoped(16);
684 let id1 = bus.emit_if_enabled(OclaEvent::FeedbackRecorded {
685 session_id: "s1".into(),
686 outcome: FeedbackOutcome::Accept,
687 tool: Some("ctx_read".into()),
688 });
689 let id2 = bus.emit_if_enabled(OclaEvent::FeedbackRecorded {
690 session_id: "s2".into(),
691 outcome: FeedbackOutcome::Reject,
692 tool: None,
693 });
694
695 let after = bus.events_since(id1);
696 assert_eq!(after.len(), 1);
697 assert_eq!(after[0].id, id2);
698 }
699
700 #[test]
701 fn latest_returns_tail() {
702 let bus = OclaBus::scoped(16);
703 for i in 0..5 {
704 bus.emit_if_enabled(OclaEvent::CompressionApplied {
705 path: Some(format!("file_{i}.rs")),
706 before_tokens: 100,
707 after_tokens: 50,
708 strategy: "treesitter".into(),
709 });
710 }
711
712 let tail = bus.latest(2);
713 assert_eq!(tail.len(), 2);
714 assert_eq!(tail[0].id, 4);
715 assert_eq!(tail[1].id, 5);
716 }
717
718 #[test]
719 fn enable_disable_toggle() {
720 let bus = OclaBus::new(16);
721 assert!(!bus.is_enabled());
722
723 bus.enable();
724 assert!(bus.is_enabled());
725 let id = bus.emit_if_enabled(OclaEvent::AgentChainEvent {
726 agent_id: "a1".into(),
727 action: "start".into(),
728 parent_agent: None,
729 });
730 assert!(id > 0);
731
732 bus.disable();
733 let id2 = bus.emit_if_enabled(OclaEvent::AgentChainEvent {
734 agent_id: "a2".into(),
735 action: "stop".into(),
736 parent_agent: Some("a1".into()),
737 });
738 assert_eq!(id2, 0);
739 assert_eq!(bus.len(), 1, "disabled emit is a no-op");
740 }
741
742 #[test]
743 fn total_emitted_counts_all() {
744 let bus = OclaBus::scoped(3);
745 bus.emit_if_enabled(OclaEvent::OutcomeRecorded {
746 session_id: "s1".into(),
747 accepted: true,
748 implicit: false,
749 });
750 bus.emit_if_enabled(OclaEvent::OutcomeRecorded {
751 session_id: "s2".into(),
752 accepted: false,
753 implicit: true,
754 });
755 bus.emit_if_enabled(OclaEvent::OutcomeRecorded {
756 session_id: "s3".into(),
757 accepted: true,
758 implicit: true,
759 });
760 bus.emit_if_enabled(OclaEvent::OutcomeRecorded {
762 session_id: "s4".into(),
763 accepted: true,
764 implicit: false,
765 });
766
767 assert_eq!(bus.total_emitted(), 4, "counts all, even evicted");
768 assert_eq!(bus.len(), 3, "ring only holds capacity");
769 }
770
771 #[test]
772 fn event_serialization_roundtrip() {
773 let event = OclaEvent::ModelRouted {
774 requested_model: "claude-sonnet-4-20250514".into(),
775 routed_model: "claude-haiku-3".into(),
776 tier: "fast".into(),
777 model_changed: true,
778 };
779 let json = serde_json::to_string(&event).unwrap();
780 let deserialized: OclaEvent = serde_json::from_str(&json).unwrap();
781 assert_eq!(event, deserialized);
782 }
783
784 #[test]
785 fn all_11_event_types_serialize() {
786 let events = vec![
787 OclaEvent::RequestCompleted {
788 model: "m".into(),
789 input_tokens: 1,
790 output_tokens: 1,
791 duration_ms: 1,
792 session_id: None,
793 },
794 OclaEvent::FeedbackRecorded {
795 session_id: "s".into(),
796 outcome: FeedbackOutcome::Partial,
797 tool: None,
798 },
799 OclaEvent::ThresholdShift {
800 language: "rust".into(),
801 old_value: 0.5,
802 new_value: 0.6,
803 metric: ThresholdMetric::Entropy,
804 },
805 OclaEvent::CompressionApplied {
806 path: None,
807 before_tokens: 100,
808 after_tokens: 50,
809 strategy: "s".into(),
810 },
811 OclaEvent::SavingsRecorded {
812 input_saved: 10,
813 output_saved: 5,
814 source: SavingsSource::ResponseCache,
815 attribution_id: None,
816 evidence_class: None,
817 measurement_method: None,
818 },
819 OclaEvent::IntentClassified {
820 tier: "standard".into(),
821 confidence: 0.7,
822 reasoning: "r".into(),
823 },
824 OclaEvent::OutcomeRecorded {
825 session_id: "s".into(),
826 accepted: true,
827 implicit: false,
828 },
829 OclaEvent::ResponseOptimized {
830 cache_hit: false,
831 is_duplicate: true,
832 tokens_saved: 0,
833 },
834 OclaEvent::ModelRouted {
835 requested_model: "a".into(),
836 routed_model: "b".into(),
837 tier: "premium".into(),
838 model_changed: true,
839 },
840 OclaEvent::AgentChainEvent {
841 agent_id: "x".into(),
842 action: "spawn".into(),
843 parent_agent: Some("y".into()),
844 },
845 OclaEvent::CrossAgentStubServed {
846 path: "src/main.rs".into(),
847 tokens_saved: 200,
848 serving_agent: "agent-a".into(),
849 original_agent: "agent-b".into(),
850 },
851 ];
852
853 for event in &events {
854 let json = serde_json::to_string(event).unwrap();
855 assert!(!json.is_empty(), "`json` is empty");
856 let _: OclaEvent = serde_json::from_str(&json).unwrap();
857 }
858 assert_eq!(events.len(), 11, "exactly 11 event types");
859 }
860
861 #[test]
862 fn savings_recorded_p5_fields_roundtrip() {
863 let event = OclaEvent::SavingsRecorded {
864 input_saved: 100,
865 output_saved: 25,
866 source: SavingsSource::Routing,
867 attribution_id: Some("attr-42".into()),
868 evidence_class: Some("measured".into()),
869 measurement_method: Some("provider_reconciled".into()),
870 };
871 let json = serde_json::to_string(&event).unwrap();
872 assert!(json.contains("attribution_id"));
873 assert!(json.contains("evidence_class"));
874 assert!(json.contains("measurement_method"));
875 let deserialized: OclaEvent = serde_json::from_str(&json).unwrap();
876 assert_eq!(event, deserialized);
877 }
878}