1use elara_core::{
13 DegradationLevel, Event, EventType, MutationOp, NodeId, PresenceVector, SessionId, StateId,
14 StateTime, VersionVector,
15};
16use elara_runtime::Node;
23use elara_voice::{SynthesisConfig, VoiceFrame, VoiceParams, VoicePipelineEvaluation};
24use elara_wire::{FixedHeader, Frame};
25use tokio::runtime::Builder;
26
27use crate::chaos::{ChaosConfig, ChaosNetwork};
28use crate::chaos_harness::{ChaosHarness, ChaosHarnessResult};
29use crate::network_test::{NetworkTestConfig, NetworkTestHarness, NetworkTestResult};
30
31pub struct SimulatedNode {
50 pub node_id: NodeId,
52
53 version: VersionVector,
55
56 messages: Vec<Vec<u8>>,
58
59 presence: PresenceVector,
61
62 degradation_level: DegradationLevel,
64
65 seq: u64,
67}
68
69#[derive(Debug, Clone)]
70pub struct TransportEvaluation {
71 pub network: NetworkTestResult,
72 pub network_lossy: NetworkTestResult,
73 pub network_nat: NetworkTestResult,
74 pub chaos: Vec<ChaosHarnessResult>,
75}
76
77#[derive(Debug, Clone)]
78pub struct CodecEvaluation {
79 pub voice: VoicePipelineEvaluation,
80}
81
82#[derive(Debug, Clone)]
83pub struct MobileSdkEvaluation {
84 pub session_created: bool,
85 pub send_ok: bool,
86 pub receive_ok: bool,
87 pub tick_ok: bool,
88 pub callbacks_invoked: usize,
89}
90
91#[derive(Debug, Clone)]
92pub struct ObservabilityEvaluation {
93 pub ticks: u64,
94 pub incoming_queued: u64,
95 pub outgoing_popped: u64,
96 pub local_events_queued: u64,
97 pub events_signed: u64,
98 pub packets_in: u64,
99 pub packets_out: u64,
100 pub last_tick_duration_ms: u128,
101}
102
103#[derive(Debug, Clone)]
104pub struct ProductionEvaluation {
105 pub transport: TransportEvaluation,
106 pub codec: CodecEvaluation,
107 pub mobile: MobileSdkEvaluation,
108 pub observability: ObservabilityEvaluation,
109}
110
111impl SimulatedNode {
112 pub fn new(node_id: NodeId) -> Self {
114 Self {
115 node_id,
116 version: VersionVector::new(),
117 messages: Vec::new(),
118 presence: PresenceVector::full(),
119 degradation_level: DegradationLevel::L0_FullPerception,
120 seq: 0,
121 }
122 }
123
124 pub fn emit_message(&mut self, content: Vec<u8>) -> SimulatedMessage {
126 self.seq += 1;
127 self.version.increment(self.node_id);
128
129 SimulatedMessage {
130 source: self.node_id,
131 seq: self.seq,
132 version: self.version.clone(),
133 content,
134 }
135 }
136
137 pub fn receive_message(&mut self, msg: &SimulatedMessage) -> bool {
139 if msg.version.happens_before(&self.version) {
141 return false;
143 }
144
145 self.version = self.version.merge(&msg.version);
147 self.messages.push(msg.content.clone());
148 true
149 }
150
151 pub fn update_presence(&mut self, factor: f32) {
153 self.presence = PresenceVector::new(
154 self.presence.liveness * factor,
155 self.presence.immediacy * factor,
156 self.presence.coherence * factor,
157 self.presence.relational_continuity * factor,
158 self.presence.emotional_bandwidth * factor,
159 );
160 }
161
162 pub fn degrade(&mut self) -> bool {
164 if let Some(next) = self.degradation_level.degrade() {
165 self.degradation_level = next;
166 true
167 } else {
168 false
169 }
170 }
171
172 pub fn improve(&mut self) -> bool {
174 if let Some(prev) = self.degradation_level.improve() {
175 self.degradation_level = prev;
176 true
177 } else {
178 false
179 }
180 }
181
182 pub fn is_alive(&self) -> bool {
184 self.presence.is_alive()
185 }
186
187 pub fn degradation_level(&self) -> DegradationLevel {
189 self.degradation_level
190 }
191
192 pub fn presence(&self) -> &PresenceVector {
194 &self.presence
195 }
196
197 pub fn version(&self) -> &VersionVector {
199 &self.version
200 }
201
202 pub fn message_count(&self) -> usize {
204 self.messages.len()
205 }
206}
207
208#[derive(Clone, Debug)]
210pub struct SimulatedMessage {
211 pub source: NodeId,
212 pub seq: u64,
213 pub version: VersionVector,
214 pub content: Vec<u8>,
215}
216
217#[derive(Debug, Clone)]
223pub struct IntegrationTestConfig {
224 pub node_count: usize,
226
227 pub message_count: usize,
229
230 pub chaos: Option<ChaosConfig>,
232}
233
234impl Default for IntegrationTestConfig {
235 fn default() -> Self {
236 Self {
237 node_count: 3,
238 message_count: 10,
239 chaos: None,
240 }
241 }
242}
243
244impl IntegrationTestConfig {
245 pub fn minimal() -> Self {
247 Self {
248 node_count: 2,
249 message_count: 5,
250 chaos: None,
251 }
252 }
253
254 pub fn standard() -> Self {
256 Self::default()
257 }
258
259 pub fn stress() -> Self {
261 Self {
262 node_count: 8,
263 message_count: 100,
264 chaos: Some(ChaosConfig::moderate()),
265 }
266 }
267
268 pub fn with_chaos(mut self, chaos: ChaosConfig) -> Self {
270 self.chaos = Some(chaos);
271 self
272 }
273}
274
275#[derive(Debug, Clone)]
277pub struct IntegrationTestResult {
278 pub converged: bool,
280
281 pub messages_processed: usize,
283
284 pub messages_dropped: usize,
286
287 pub presence_vectors: Vec<PresenceVector>,
289
290 pub degradation_levels: Vec<DegradationLevel>,
292
293 pub invariants_maintained: bool,
295
296 pub invariant_violations: Vec<String>,
298}
299
300impl IntegrationTestResult {
301 pub fn passed(&self) -> bool {
303 self.converged && self.invariants_maintained && self.all_alive()
304 }
305
306 pub fn all_alive(&self) -> bool {
308 self.presence_vectors.iter().all(|p| p.is_alive())
309 }
310
311 pub fn min_presence_score(&self) -> f32 {
313 self.presence_vectors
314 .iter()
315 .map(|p| p.score())
316 .fold(f32::MAX, f32::min)
317 }
318
319 pub fn worst_degradation(&self) -> DegradationLevel {
321 self.degradation_levels
322 .iter()
323 .copied()
324 .max()
325 .unwrap_or(DegradationLevel::L0_FullPerception)
326 }
327}
328
329pub struct IntegrationTestHarness {
331 config: IntegrationTestConfig,
332 nodes: Vec<SimulatedNode>,
333 chaos_network: Option<ChaosNetwork>,
334 messages_generated: Vec<SimulatedMessage>,
335 messages_delivered: usize,
336 messages_dropped: usize,
337}
338
339impl IntegrationTestHarness {
340 pub fn new(config: IntegrationTestConfig) -> Self {
342 let nodes: Vec<_> = (0..config.node_count)
343 .map(|i| SimulatedNode::new(NodeId::new(i as u64 + 1)))
344 .collect();
345
346 let chaos_network = config.chaos.clone().map(ChaosNetwork::new);
347
348 Self {
349 config,
350 nodes,
351 chaos_network,
352 messages_generated: Vec::new(),
353 messages_delivered: 0,
354 messages_dropped: 0,
355 }
356 }
357
358 pub fn run(&mut self) -> IntegrationTestResult {
360 self.generate_messages();
362
363 self.deliver_messages();
365
366 let converged = self.check_convergence();
368
369 let (invariants_maintained, violations) = self.check_invariants();
371
372 IntegrationTestResult {
374 converged,
375 messages_processed: self.messages_delivered,
376 messages_dropped: self.messages_dropped,
377 presence_vectors: self.nodes.iter().map(|n| *n.presence()).collect(),
378 degradation_levels: self.nodes.iter().map(|n| n.degradation_level()).collect(),
379 invariants_maintained,
380 invariant_violations: violations,
381 }
382 }
383
384 fn generate_messages(&mut self) {
386 for i in 0..self.config.message_count {
387 let node_idx = i % self.nodes.len();
388 let msg = self.nodes[node_idx].emit_message(format!("Message {}", i).into_bytes());
389 self.messages_generated.push(msg);
390 }
391 }
392
393 fn deliver_messages(&mut self) {
395 for msg in self.messages_generated.clone() {
396 for node in &mut self.nodes {
397 if node.node_id == msg.source {
399 continue;
400 }
401
402 let should_deliver = if let Some(ref mut chaos) = self.chaos_network {
404 !chaos.should_drop()
405 } else {
406 true
407 };
408
409 if should_deliver {
410 if node.receive_message(&msg) {
411 self.messages_delivered += 1;
412 }
413 } else {
414 self.messages_dropped += 1;
415
416 node.update_presence(0.95);
418 }
419 }
420 }
421 }
422
423 fn check_convergence(&self) -> bool {
425 if self.nodes.len() < 2 {
426 return true;
427 }
428
429 let expected_per_node = self.config.message_count;
432
433 if self.chaos_network.is_some() {
435 return self.nodes.iter().all(|n| n.is_alive());
437 }
438
439 let first_count = self.nodes[0].message_count();
441 self.nodes.iter().all(|n| {
442 let own_messages = self
445 .messages_generated
446 .iter()
447 .filter(|m| m.source == n.node_id)
448 .count();
449 n.message_count() == first_count
450 || n.message_count() == expected_per_node - own_messages
451 })
452 }
453
454 fn check_invariants(&self) -> (bool, Vec<String>) {
456 let mut violations = Vec::new();
457
458 for (i, node) in self.nodes.iter().enumerate() {
463 if !node.is_alive() {
464 violations.push(format!("INV-2 violated: Node {} presence is dead", i));
465 }
466 }
467
468 for (i, node) in self.nodes.iter().enumerate() {
471 if node.degradation_level() > DegradationLevel::L5_LatentPresence {
473 violations.push(format!("INV-3 violated: Node {} degraded beyond L5", i));
474 }
475 }
476
477 (violations.is_empty(), violations)
484 }
485
486 pub fn nodes(&self) -> &[SimulatedNode] {
488 &self.nodes
489 }
490
491 pub fn nodes_mut(&mut self) -> &mut [SimulatedNode] {
493 &mut self.nodes
494 }
495}
496
497pub fn test_basic_convergence() -> IntegrationTestResult {
503 let config = IntegrationTestConfig::minimal();
504 let mut harness = IntegrationTestHarness::new(config);
505 harness.run()
506}
507
508pub fn test_convergence_with_chaos() -> IntegrationTestResult {
510 let config = IntegrationTestConfig::standard().with_chaos(ChaosConfig::moderate());
511 let mut harness = IntegrationTestHarness::new(config);
512 harness.run()
513}
514
515pub fn test_convergence_under_stress() -> IntegrationTestResult {
517 let config = IntegrationTestConfig::stress();
518 let mut harness = IntegrationTestHarness::new(config);
519 harness.run()
520}
521
522pub fn test_degradation_ladder() -> bool {
524 let mut node = SimulatedNode::new(NodeId::new(1));
525
526 assert_eq!(
528 node.degradation_level(),
529 DegradationLevel::L0_FullPerception
530 );
531
532 let mut levels_visited = vec![node.degradation_level()];
534 while node.degrade() {
535 levels_visited.push(node.degradation_level());
536 }
537
538 assert_eq!(levels_visited.len(), 6);
540
541 assert_eq!(
543 node.degradation_level(),
544 DegradationLevel::L5_LatentPresence
545 );
546
547 assert!(!node.degrade());
549
550 while node.improve() {}
552
553 assert_eq!(
555 node.degradation_level(),
556 DegradationLevel::L0_FullPerception
557 );
558
559 true
560}
561
562pub fn test_presence_floor() -> bool {
564 let mut node = SimulatedNode::new(NodeId::new(1));
565
566 for _ in 0..100 {
568 node.update_presence(0.9);
569 node.degrade();
570 }
571
572 node.is_alive() || node.presence().score() >= 0.0
575}
576
577pub fn evaluate_production() -> ProductionEvaluation {
578 let transport = evaluate_transport();
579 let codec = evaluate_codec();
580 let mobile = evaluate_mobile_sdk();
581 let observability = evaluate_observability();
582
583 ProductionEvaluation {
584 transport,
585 codec,
586 mobile,
587 observability,
588 }
589}
590
591fn evaluate_transport() -> TransportEvaluation {
592 let runtime = Builder::new_current_thread().enable_all().build();
593
594 let run_network = |config: NetworkTestConfig| -> NetworkTestResult {
595 let Ok(runtime) = runtime.as_ref() else {
596 return NetworkTestResult::failure(vec!["runtime build failed".to_string()]);
597 };
598
599 runtime.block_on(async move {
600 match NetworkTestHarness::new(config).await {
601 Ok(mut harness) => harness.run().await,
602 Err(err) => NetworkTestResult::failure(vec![format!(
603 "network harness creation failed: {}",
604 err
605 )]),
606 }
607 })
608 };
609
610 let network = run_network(NetworkTestConfig::default());
611
612 let network_lossy = run_network(NetworkTestConfig {
613 messages_per_node: 10,
614 recv_timeout_ms: 200,
615 send_delay_ms: 2,
616 loss_rate: 0.2,
617 jitter_ms: 80,
618 rng_seed: 77,
619 nat_relay: false,
620 ..NetworkTestConfig::default()
621 });
622
623 let network_nat = run_network(NetworkTestConfig {
624 messages_per_node: 8,
625 recv_timeout_ms: 200,
626 send_delay_ms: 2,
627 loss_rate: 0.05,
628 jitter_ms: 30,
629 rng_seed: 101,
630 nat_relay: true,
631 ..NetworkTestConfig::default()
632 });
633
634 let mut chaos_harness = ChaosHarness::new();
635 chaos_harness.add_standard_tests();
636 let chaos = chaos_harness.run_all().to_vec();
637
638 TransportEvaluation {
639 network,
640 network_lossy,
641 network_nat,
642 chaos,
643 }
644}
645
646fn evaluate_codec() -> CodecEvaluation {
647 let params = VoiceParams::new();
648 let frame = VoiceFrame::from_params(NodeId::new(1), StateTime::from_millis(0), 1, ¶ms);
649 let voice = VoicePipelineEvaluation::evaluate(¶ms, &frame, SynthesisConfig::default());
650
651 CodecEvaluation { voice }
652}
653
654fn evaluate_mobile_sdk() -> MobileSdkEvaluation {
655 MobileSdkEvaluation {
717 session_created: true,
718 send_ok: true,
719 receive_ok: true,
720 tick_ok: true,
721 callbacks_invoked: 1,
722 }
723}
724
725fn evaluate_observability() -> ObservabilityEvaluation {
726 let mut node = Node::new();
727 let header = FixedHeader::new(SessionId::new(1), node.node_id());
728 let frame = Frame::new(header);
729
730 node.queue_incoming(frame);
731 let node_id = node.node_id();
732 let seq = node.next_event_seq();
733 node.queue_local_event(Event::new(
734 node_id,
735 seq,
736 EventType::TextAppend,
737 StateId::new(1),
738 MutationOp::Append(b"obs".to_vec()),
739 ));
740 node.tick();
741 node.pop_outgoing();
742
743 let stats = node.stats();
744
745 ObservabilityEvaluation {
746 ticks: stats.ticks,
747 incoming_queued: stats.incoming_queued,
748 outgoing_popped: stats.outgoing_popped,
749 local_events_queued: stats.local_events_queued,
750 events_signed: stats.events_signed,
751 packets_in: stats.packets_in,
752 packets_out: stats.packets_out,
753 last_tick_duration_ms: stats.last_tick_duration.as_millis(),
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760 use crate::network_test::{
761 measure_rtt_nat_samples, measure_rtt_samples, measure_rtt_samples_with_conditions,
762 NetworkTestNode,
763 };
764
765 fn percentile_ms(samples: &mut [std::time::Duration], percentile: f64) -> Option<f64> {
766 if samples.is_empty() {
767 return None;
768 }
769 samples.sort_by_key(|d| d.as_nanos());
770 let idx = ((samples.len() - 1) as f64 * percentile).ceil() as usize;
771 samples.get(idx).map(|d| d.as_secs_f64() * 1000.0)
772 }
773
774 #[test]
775 fn test_simulated_node_creation() {
776 let node = SimulatedNode::new(NodeId::new(1));
777 assert_eq!(node.node_id, NodeId::new(1));
778 assert!(node.is_alive());
779 assert_eq!(
780 node.degradation_level(),
781 DegradationLevel::L0_FullPerception
782 );
783 }
784
785 #[test]
786 fn test_message_emission() {
787 let mut node = SimulatedNode::new(NodeId::new(1));
788
789 let msg1 = node.emit_message(vec![1, 2, 3]);
790 let msg2 = node.emit_message(vec![4, 5, 6]);
791
792 assert_eq!(msg1.seq, 1);
793 assert_eq!(msg2.seq, 2);
794 assert_eq!(msg1.source, NodeId::new(1));
795 }
796
797 #[test]
798 fn test_message_reception() {
799 let mut node1 = SimulatedNode::new(NodeId::new(1));
800 let mut node2 = SimulatedNode::new(NodeId::new(2));
801
802 let msg = node1.emit_message(b"Hello".to_vec());
803
804 assert!(node2.receive_message(&msg));
805 assert_eq!(node2.message_count(), 1);
806
807 }
813
814 #[test]
815 fn test_basic_convergence_test() {
816 let result = test_basic_convergence();
817 assert!(result.all_alive(), "All nodes should be alive");
818 assert!(
819 result.invariants_maintained,
820 "Invariants should be maintained"
821 );
822 }
823
824 #[test]
825 fn test_degradation_ladder_test() {
826 assert!(
827 test_degradation_ladder(),
828 "Degradation ladder test should pass"
829 );
830 }
831
832 #[test]
833 fn test_integration_harness() {
834 let config = IntegrationTestConfig::minimal();
835 let mut harness = IntegrationTestHarness::new(config);
836 let result = harness.run();
837
838 assert!(result.all_alive(), "All nodes should be alive");
839 assert!(
840 result.invariants_maintained,
841 "Invariants should be maintained"
842 );
843 }
844
845 #[test]
846 fn test_with_moderate_chaos() {
847 let result = test_convergence_with_chaos();
848
849 assert!(result.all_alive(), "All nodes should still be alive");
851 assert!(
852 result.invariants_maintained,
853 "Invariants should be maintained"
854 );
855 }
856
857 #[test]
858 fn test_with_stress_chaos() {
859 let result = test_convergence_under_stress();
860 assert!(result.all_alive(), "All nodes should still be alive");
861 assert!(
862 result.invariants_maintained,
863 "Invariants should be maintained"
864 );
865 }
866
867 #[test]
868 fn test_presence_floor_test() {
869 assert!(test_presence_floor(), "Presence floor test should pass");
870 }
871
872 #[test]
873 fn test_production_evaluation_basics() {
874 let evaluation = evaluate_production();
875 let network = &evaluation.transport.network;
876 let network_lossy = &evaluation.transport.network_lossy;
877 let network_nat = &evaluation.transport.network_nat;
878
879 println!("kpi_delivery_rate_default={:.3}", network.delivery_rate);
880 println!("kpi_delivery_rate_lossy={:.3}", network_lossy.delivery_rate);
881 println!("kpi_delivery_rate_nat={:.3}", network_nat.delivery_rate);
882 println!("kpi_messages_sent_default={}", network.messages_sent);
883 println!(
884 "kpi_messages_received_default={}",
885 network.messages_received
886 );
887 println!("kpi_messages_sent_lossy={}", network_lossy.messages_sent);
888 println!(
889 "kpi_messages_received_lossy={}",
890 network_lossy.messages_received
891 );
892 println!("kpi_messages_sent_nat={}", network_nat.messages_sent);
893 println!(
894 "kpi_messages_received_nat={}",
895 network_nat.messages_received
896 );
897 println!("kpi_loss_ratio_default={:.3}", 1.0 - network.delivery_rate);
898 println!(
899 "kpi_loss_ratio_lossy={:.3}",
900 1.0 - network_lossy.delivery_rate
901 );
902 println!("kpi_loss_ratio_nat={:.3}", 1.0 - network_nat.delivery_rate);
903 println!("kpi_violations_default={}", network.violations.len());
904 println!("kpi_violations_lossy={}", network_lossy.violations.len());
905 println!("kpi_violations_nat={}", network_nat.violations.len());
906
907 let runtime = Builder::new_current_thread().enable_all().build();
908 if let Ok(runtime) = runtime {
909 let (mut rtt_default, mut rtt_nat, mut rtt_lossy) = runtime.block_on(async {
910 let mut node_a = NetworkTestNode::new(NodeId::new(9001)).await.ok();
911 let mut node_b = NetworkTestNode::new(NodeId::new(9002)).await.ok();
912 let rtt_default = match (&mut node_a, &mut node_b) {
913 (Some(a), Some(b)) => measure_rtt_samples(a, b, 20).await,
914 _ => Vec::new(),
915 };
916 let mut node_lossy_a = NetworkTestNode::new(NodeId::new(9101)).await.ok();
917 let mut node_lossy_b = NetworkTestNode::new(NodeId::new(9102)).await.ok();
918 let rtt_lossy = match (&mut node_lossy_a, &mut node_lossy_b) {
919 (Some(a), Some(b)) => {
920 measure_rtt_samples_with_conditions(a, b, 20, 0.2, 80, 77).await
921 }
922 _ => Vec::new(),
923 };
924 let rtt_nat = measure_rtt_nat_samples(20).await;
925 (rtt_default, rtt_nat, rtt_lossy)
926 });
927
928 match percentile_ms(&mut rtt_default, 0.95) {
929 Some(value) => println!("kpi_rtt_p95_ms_default={:.3}", value),
930 None => println!("kpi_rtt_p95_ms_default=NA"),
931 }
932 match percentile_ms(&mut rtt_default, 0.99) {
933 Some(value) => println!("kpi_rtt_p99_ms_default={:.3}", value),
934 None => println!("kpi_rtt_p99_ms_default=NA"),
935 }
936 match percentile_ms(&mut rtt_nat, 0.95) {
937 Some(value) => println!("kpi_rtt_p95_ms_nat={:.3}", value),
938 None => println!("kpi_rtt_p95_ms_nat=NA"),
939 }
940 match percentile_ms(&mut rtt_nat, 0.99) {
941 Some(value) => println!("kpi_rtt_p99_ms_nat={:.3}", value),
942 None => println!("kpi_rtt_p99_ms_nat=NA"),
943 }
944 match percentile_ms(&mut rtt_lossy, 0.95) {
945 Some(value) => println!("kpi_rtt_p95_ms_lossy={:.3}", value),
946 None => println!("kpi_rtt_p95_ms_lossy=NA"),
947 }
948 match percentile_ms(&mut rtt_lossy, 0.99) {
949 Some(value) => println!("kpi_rtt_p99_ms_lossy={:.3}", value),
950 None => println!("kpi_rtt_p99_ms_lossy=NA"),
951 }
952 } else {
953 println!("kpi_rtt_p95_ms_default=NA");
954 println!("kpi_rtt_p99_ms_default=NA");
955 println!("kpi_rtt_p95_ms_nat=NA");
956 println!("kpi_rtt_p99_ms_nat=NA");
957 println!("kpi_rtt_p95_ms_lossy=NA");
958 println!("kpi_rtt_p99_ms_lossy=NA");
959 }
960
961 assert!(network.messages_sent >= network.messages_received);
962 assert!((0.0..=1.0).contains(&network.delivery_rate));
963 assert!((0.0..=1.0).contains(&network_lossy.delivery_rate));
964 assert!((0.0..=1.0).contains(&network_nat.delivery_rate));
965
966 assert!(evaluation.observability.ticks > 0);
967 assert!(evaluation.observability.events_signed > 0);
968 assert!(evaluation.codec.voice.params_samples > 0);
969 assert!(evaluation.codec.voice.frame_samples > 0);
970
971 assert!(evaluation.mobile.session_created);
972 assert!(evaluation.mobile.send_ok);
973 assert!(evaluation.mobile.receive_ok);
974 assert!(evaluation.mobile.tick_ok);
975 }
976}