1mod prompts;
33
34pub use prompts::{build_detection_prompt, format_entity_for_prompt, EVOLUTION_DETECTION_PROMPT};
35
36use crate::constants::{
37 EVOLUTION_CONFIDENCE_MAX, EVOLUTION_CONFIDENCE_MIN, EVOLUTION_CONFIDENCE_THRESHOLD_DEFAULT,
38 EVOLUTION_EXISTING_ENTITIES_COUNT_MAX, EVOLUTION_REASON_BYTES_MAX,
39};
40use crate::llm::{CompletionRequest, LLMProvider};
41use crate::storage::{Entity, EvolutionRelation, EvolutionType, StorageBackend};
42use std::marker::PhantomData;
43use thiserror::Error;
44
45#[derive(Debug, Error)]
51pub enum EvolutionError {
52 #[error("invalid options: {0}")]
54 InvalidOptions(String),
55}
56
57#[derive(Debug, Clone)]
65pub struct DetectionOptions {
66 pub min_confidence: f32,
68
69 pub max_comparisons: usize,
71}
72
73impl DetectionOptions {
74 #[must_use]
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 #[must_use]
85 pub fn with_min_confidence(mut self, confidence: f32) -> Self {
86 debug_assert!(
87 (EVOLUTION_CONFIDENCE_MIN as f32..=EVOLUTION_CONFIDENCE_MAX as f32)
88 .contains(&confidence),
89 "min_confidence must be {}-{}: got {}",
90 EVOLUTION_CONFIDENCE_MIN,
91 EVOLUTION_CONFIDENCE_MAX,
92 confidence
93 );
94 self.min_confidence = confidence;
95 self
96 }
97
98 #[must_use]
103 pub fn with_max_comparisons(mut self, max_comparisons: usize) -> Self {
104 debug_assert!(
105 max_comparisons > 0 && max_comparisons <= EVOLUTION_EXISTING_ENTITIES_COUNT_MAX,
106 "max_comparisons must be 1-{}: got {}",
107 EVOLUTION_EXISTING_ENTITIES_COUNT_MAX,
108 max_comparisons
109 );
110 self.max_comparisons = max_comparisons;
111 self
112 }
113}
114
115impl Default for DetectionOptions {
116 fn default() -> Self {
117 Self {
118 min_confidence: EVOLUTION_CONFIDENCE_THRESHOLD_DEFAULT as f32,
119 max_comparisons: EVOLUTION_EXISTING_ENTITIES_COUNT_MAX,
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
130pub struct DetectionResult {
131 pub relation: EvolutionRelation,
133
134 pub llm_used: bool,
136}
137
138impl DetectionResult {
139 #[must_use]
141 pub fn new(relation: EvolutionRelation, llm_used: bool) -> Self {
142 Self { relation, llm_used }
143 }
144
145 #[must_use]
147 pub fn evolution_type(&self) -> EvolutionType {
148 self.relation.evolution_type
149 }
150
151 #[must_use]
153 pub fn reason(&self) -> &str {
154 &self.relation.reason
155 }
156
157 #[must_use]
159 pub fn confidence(&self) -> f32 {
160 self.relation.confidence
161 }
162
163 #[must_use]
165 pub fn is_high_confidence(&self) -> bool {
166 self.relation.is_high_confidence()
167 }
168}
169
170pub struct EvolutionTracker<L: LLMProvider, S: StorageBackend> {
192 llm: L,
193 _storage: PhantomData<S>,
194}
195
196impl<L: LLMProvider, S: StorageBackend> EvolutionTracker<L, S> {
197 #[must_use]
202 pub fn new(llm: L) -> Self {
203 Self {
204 llm,
205 _storage: PhantomData,
206 }
207 }
208
209 pub async fn detect(
225 &self,
226 new_entity: &Entity,
227 existing_entities: &[Entity],
228 options: DetectionOptions,
229 ) -> Result<Option<DetectionResult>, EvolutionError> {
230 debug_assert!(!new_entity.id.is_empty(), "new_entity must have id");
232 debug_assert!(!new_entity.name.is_empty(), "new_entity must have name");
233
234 if existing_entities.is_empty() {
236 return Ok(None);
237 }
238
239 let limited_entities: Vec<&Entity> = existing_entities
241 .iter()
242 .take(options.max_comparisons)
243 .collect();
244
245 let new_content = format!("{}: {}", new_entity.name, new_entity.content);
247 let existing_list: String = limited_entities
248 .iter()
249 .map(|e| format_entity_for_prompt(&e.id, &e.name, &e.content))
250 .collect::<Vec<_>>()
251 .join("\n");
252
253 let prompt = build_detection_prompt(&new_content, &existing_list);
254
255 let response = match self.llm.complete(&CompletionRequest::new(&prompt)).await {
257 Ok(resp) => resp,
258 Err(_) => return Ok(None), };
260
261 let relation = match self.parse_response(&response, &new_entity.id) {
263 Some(r) => r,
264 None => return Ok(None),
265 };
266
267 if relation.confidence < options.min_confidence {
269 return Ok(None);
270 }
271
272 debug_assert!(
274 (EVOLUTION_CONFIDENCE_MIN as f32..=EVOLUTION_CONFIDENCE_MAX as f32)
275 .contains(&relation.confidence),
276 "confidence must be in valid range"
277 );
278
279 Ok(Some(DetectionResult::new(relation, true)))
280 }
281
282 fn parse_response(&self, response: &str, new_entity_id: &str) -> Option<EvolutionRelation> {
291 let data: serde_json::Value = serde_json::from_str(response).ok()?;
293
294 let type_str = data.get("type")?.as_str()?;
296
297 if type_str == "none" {
299 return None;
300 }
301
302 let evolution_type = EvolutionType::from_str(type_str)?;
304
305 let related_id = data.get("related_id")?.as_str()?;
307 if related_id.is_empty() || related_id == "null" {
308 return None;
309 }
310
311 let reason = data
313 .get("reason")
314 .and_then(|r| r.as_str())
315 .unwrap_or("")
316 .chars()
317 .take(EVOLUTION_REASON_BYTES_MAX)
318 .collect::<String>();
319
320 let confidence = data
322 .get("confidence")
323 .and_then(|c| c.as_f64())
324 .map(|c| c as f32)
325 .unwrap_or(0.5)
326 .clamp(
327 EVOLUTION_CONFIDENCE_MIN as f32,
328 EVOLUTION_CONFIDENCE_MAX as f32,
329 );
330
331 Some(
333 EvolutionRelation::builder(
334 related_id.to_string(),
335 new_entity_id.to_string(),
336 evolution_type,
337 )
338 .with_reason(reason)
339 .with_confidence(confidence)
340 .build(),
341 )
342 }
343}
344
345#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::llm::SimLLMProvider;
353 use crate::storage::{EntityType, SimStorageBackend};
354
355 fn create_tracker(seed: u64) -> EvolutionTracker<SimLLMProvider, SimStorageBackend> {
357 let llm = SimLLMProvider::with_seed(seed);
358 EvolutionTracker::new(llm)
359 }
360
361 fn create_entity(id: &str, name: &str, content: &str) -> Entity {
363 let mut entity = Entity::new(EntityType::Person, name.to_string(), content.to_string());
364 entity.id = id.to_string();
366 entity
367 }
368
369 #[test]
374 fn test_detection_options_default() {
375 let options = DetectionOptions::default();
376
377 assert!(
378 (options.min_confidence - EVOLUTION_CONFIDENCE_THRESHOLD_DEFAULT as f32).abs()
379 < f32::EPSILON
380 );
381 assert_eq!(
382 options.max_comparisons,
383 EVOLUTION_EXISTING_ENTITIES_COUNT_MAX
384 );
385 }
386
387 #[test]
388 fn test_detection_options_builder() {
389 let options = DetectionOptions::new()
390 .with_min_confidence(0.5)
391 .with_max_comparisons(5);
392
393 assert!((options.min_confidence - 0.5).abs() < f32::EPSILON);
394 assert_eq!(options.max_comparisons, 5);
395 }
396
397 #[test]
398 #[should_panic(expected = "min_confidence must be")]
399 fn test_detection_options_invalid_confidence_high() {
400 let _ = DetectionOptions::new().with_min_confidence(1.5);
401 }
402
403 #[test]
404 #[should_panic(expected = "min_confidence must be")]
405 fn test_detection_options_invalid_confidence_low() {
406 let _ = DetectionOptions::new().with_min_confidence(-0.1);
407 }
408
409 #[test]
410 #[should_panic(expected = "max_comparisons must be")]
411 fn test_detection_options_invalid_max_zero() {
412 let _ = DetectionOptions::new().with_max_comparisons(0);
413 }
414
415 #[test]
416 #[should_panic(expected = "max_comparisons must be")]
417 fn test_detection_options_invalid_max_too_large() {
418 let _ =
419 DetectionOptions::new().with_max_comparisons(EVOLUTION_EXISTING_ENTITIES_COUNT_MAX + 1);
420 }
421
422 #[test]
427 fn test_detection_result_accessors() {
428 let relation = EvolutionRelation::new(
429 "source-1".to_string(),
430 "target-1".to_string(),
431 EvolutionType::Update,
432 "Job changed".to_string(),
433 0.9,
434 );
435 let result = DetectionResult::new(relation, true);
436
437 assert_eq!(result.evolution_type(), EvolutionType::Update);
438 assert_eq!(result.reason(), "Job changed");
439 assert!(result.is_high_confidence());
440 assert!(result.llm_used);
441 }
442
443 #[test]
444 fn test_detection_result_low_confidence() {
445 let relation = EvolutionRelation::new(
446 "source-1".to_string(),
447 "target-1".to_string(),
448 EvolutionType::Extend,
449 "Maybe related".to_string(),
450 0.4,
451 );
452 let result = DetectionResult::new(relation, true);
453
454 assert!(!result.is_high_confidence());
455 }
456
457 #[test]
462 fn test_tracker_creation() {
463 let tracker = create_tracker(42);
464 let _ = tracker;
466 }
467
468 #[tokio::test]
473 async fn test_detect_empty_existing() {
474 let tracker = create_tracker(42);
475 let new_entity = create_entity("new-1", "Alice", "Joined StartupX");
476
477 let result = tracker
478 .detect(&new_entity, &[], DetectionOptions::default())
479 .await;
480
481 assert!(result.is_ok());
482 assert!(result.unwrap().is_none());
483 }
484
485 #[tokio::test]
486 async fn test_detect_with_existing_entities() {
487 let tracker = create_tracker(42);
488
489 let old_entity = create_entity("old-1", "Alice", "Works at Acme Corp");
490 let new_entity = create_entity("new-1", "Alice", "Left Acme, now at StartupX");
491
492 let result = tracker
493 .detect(&new_entity, &[old_entity], DetectionOptions::default())
494 .await;
495
496 assert!(result.is_ok());
497 }
500
501 #[tokio::test]
502 async fn test_detect_limits_comparisons() {
503 let tracker = create_tracker(42);
504
505 let new_entity = create_entity("new-1", "Test", "New content");
506 let existing: Vec<Entity> = (0..20)
507 .map(|i| create_entity(&format!("old-{}", i), "Test", &format!("Content {}", i)))
508 .collect();
509
510 let options = DetectionOptions::new().with_max_comparisons(3);
511 let result = tracker.detect(&new_entity, &existing, options).await;
512
513 assert!(result.is_ok());
515 }
516
517 #[tokio::test]
518 async fn test_detect_high_confidence_threshold() {
519 let tracker = create_tracker(42);
520
521 let old_entity = create_entity("old-1", "Test", "Old content");
522 let new_entity = create_entity("new-1", "Test", "New content");
523
524 let options = DetectionOptions::new().with_min_confidence(0.99);
526 let _result = tracker
527 .detect(&new_entity, &[old_entity], options)
528 .await
529 .unwrap();
530
531 }
534
535 #[tokio::test]
536 async fn test_detect_low_confidence_threshold() {
537 let tracker = create_tracker(42);
538
539 let old_entity = create_entity("old-1", "Test", "Old content");
540 let new_entity = create_entity("new-1", "Test", "New content");
541
542 let options = DetectionOptions::new().with_min_confidence(0.01);
544 let _result = tracker
545 .detect(&new_entity, &[old_entity], options)
546 .await
547 .unwrap();
548
549 }
551
552 #[test]
557 fn test_parse_response_valid_update() {
558 let tracker = create_tracker(42);
559
560 let response = r#"{"type": "update", "reason": "Job changed", "related_id": "old-1", "confidence": 0.9}"#;
561 let result = tracker.parse_response(response, "new-1");
562
563 assert!(result.is_some());
564 let relation = result.unwrap();
565 assert_eq!(relation.evolution_type, EvolutionType::Update);
566 assert_eq!(relation.source_id, "old-1");
567 assert_eq!(relation.target_id, "new-1");
568 assert_eq!(relation.reason, "Job changed");
569 assert!((relation.confidence - 0.9).abs() < f32::EPSILON);
570 }
571
572 #[test]
573 fn test_parse_response_valid_extend() {
574 let tracker = create_tracker(42);
575
576 let response = r#"{"type": "extend", "reason": "Added skill", "related_id": "old-1", "confidence": 0.8}"#;
577 let result = tracker.parse_response(response, "new-1");
578
579 assert!(result.is_some());
580 assert_eq!(result.unwrap().evolution_type, EvolutionType::Extend);
581 }
582
583 #[test]
584 fn test_parse_response_valid_contradict() {
585 let tracker = create_tracker(42);
586
587 let response = r#"{"type": "contradict", "reason": "Conflicting", "related_id": "old-1", "confidence": 0.95}"#;
588 let result = tracker.parse_response(response, "new-1");
589
590 assert!(result.is_some());
591 let relation = result.unwrap();
592 assert_eq!(relation.evolution_type, EvolutionType::Contradict);
593 assert!(relation.needs_resolution());
594 }
595
596 #[test]
597 fn test_parse_response_valid_derive() {
598 let tracker = create_tracker(42);
599
600 let response =
601 r#"{"type": "derive", "reason": "Inferred", "related_id": "old-1", "confidence": 0.7}"#;
602 let result = tracker.parse_response(response, "new-1");
603
604 assert!(result.is_some());
605 assert_eq!(result.unwrap().evolution_type, EvolutionType::Derive);
606 }
607
608 #[test]
609 fn test_parse_response_none_type() {
610 let tracker = create_tracker(42);
611
612 let response =
613 r#"{"type": "none", "reason": "No relation", "related_id": null, "confidence": 0.9}"#;
614 let result = tracker.parse_response(response, "new-1");
615
616 assert!(result.is_none());
617 }
618
619 #[test]
620 fn test_parse_response_invalid_json() {
621 let tracker = create_tracker(42);
622
623 let response = "not valid json";
624 let result = tracker.parse_response(response, "new-1");
625
626 assert!(result.is_none());
627 }
628
629 #[test]
630 fn test_parse_response_missing_type() {
631 let tracker = create_tracker(42);
632
633 let response = r#"{"reason": "something", "related_id": "old-1", "confidence": 0.9}"#;
634 let result = tracker.parse_response(response, "new-1");
635
636 assert!(result.is_none());
637 }
638
639 #[test]
640 fn test_parse_response_missing_related_id() {
641 let tracker = create_tracker(42);
642
643 let response = r#"{"type": "update", "reason": "something", "confidence": 0.9}"#;
644 let result = tracker.parse_response(response, "new-1");
645
646 assert!(result.is_none());
647 }
648
649 #[test]
650 fn test_parse_response_null_related_id() {
651 let tracker = create_tracker(42);
652
653 let response =
654 r#"{"type": "update", "reason": "something", "related_id": "null", "confidence": 0.9}"#;
655 let result = tracker.parse_response(response, "new-1");
656
657 assert!(result.is_none());
658 }
659
660 #[test]
661 fn test_parse_response_invalid_type() {
662 let tracker = create_tracker(42);
663
664 let response = r#"{"type": "invalid", "reason": "something", "related_id": "old-1", "confidence": 0.9}"#;
665 let result = tracker.parse_response(response, "new-1");
666
667 assert!(result.is_none());
668 }
669
670 #[test]
671 fn test_parse_response_clamps_confidence() {
672 let tracker = create_tracker(42);
673
674 let response =
676 r#"{"type": "update", "reason": "test", "related_id": "old-1", "confidence": 1.5}"#;
677 let result = tracker.parse_response(response, "new-1");
678 assert!(result.is_some());
679 assert!((result.unwrap().confidence - 1.0).abs() < f32::EPSILON);
680
681 let response =
683 r#"{"type": "update", "reason": "test", "related_id": "old-1", "confidence": -0.5}"#;
684 let result = tracker.parse_response(response, "new-1");
685 assert!(result.is_some());
686 assert!(result.unwrap().confidence >= 0.0);
687 }
688
689 #[test]
690 fn test_parse_response_default_confidence() {
691 let tracker = create_tracker(42);
692
693 let response = r#"{"type": "update", "reason": "test", "related_id": "old-1"}"#;
694 let result = tracker.parse_response(response, "new-1");
695
696 assert!(result.is_some());
697 assert!((result.unwrap().confidence - 0.5).abs() < f32::EPSILON);
698 }
699
700 #[test]
701 fn test_parse_response_truncates_long_reason() {
702 let tracker = create_tracker(42);
703
704 let long_reason = "a".repeat(2000);
705 let response = format!(
706 r#"{{"type": "update", "reason": "{}", "related_id": "old-1", "confidence": 0.9}}"#,
707 long_reason
708 );
709 let result = tracker.parse_response(&response, "new-1");
710
711 assert!(result.is_some());
712 assert!(result.unwrap().reason.len() <= EVOLUTION_REASON_BYTES_MAX);
713 }
714
715 #[tokio::test]
720 async fn test_detect_deterministic_same_seed() {
721 let old_entity = create_entity("old-1", "Alice", "Works at Acme");
722 let new_entity = create_entity("new-1", "Alice", "Left Acme, now at StartupX");
723
724 let tracker1 = create_tracker(42);
726 let result1 = tracker1
727 .detect(
728 &new_entity,
729 &[old_entity.clone()],
730 DetectionOptions::default(),
731 )
732 .await;
733
734 let tracker2 = create_tracker(42);
735 let result2 = tracker2
736 .detect(&new_entity, &[old_entity], DetectionOptions::default())
737 .await;
738
739 assert!(result1.is_ok());
741 assert!(result2.is_ok());
742
743 match (result1.unwrap(), result2.unwrap()) {
745 (Some(r1), Some(r2)) => {
746 assert_eq!(r1.relation.evolution_type, r2.relation.evolution_type);
747 assert_eq!(r1.relation.source_id, r2.relation.source_id);
748 assert_eq!(r1.relation.target_id, r2.relation.target_id);
749 }
750 (None, None) => (), _ => panic!("Determinism violated: one result is Some, other is None"),
752 }
753 }
754
755 #[tokio::test]
760 async fn test_scenario_employment_update() {
761 let tracker = create_tracker(42);
763
764 let old = create_entity("old-1", "Alice", "Works at Acme Corp as engineer");
765 let new = create_entity("new-1", "Alice", "Left Acme, now CTO at StartupX");
766
767 let result = tracker
768 .detect(&new, &[old], DetectionOptions::default())
769 .await;
770
771 assert!(result.is_ok());
773 }
774
775 #[tokio::test]
776 async fn test_scenario_skill_extension() {
777 let tracker = create_tracker(42);
778
779 let old = create_entity("old-1", "Bob", "Knows JavaScript and React");
780 let new = create_entity("new-1", "Bob", "Also learned TypeScript recently");
781
782 let result = tracker
783 .detect(&new, &[old], DetectionOptions::default())
784 .await;
785
786 assert!(result.is_ok());
787 }
788
789 #[tokio::test]
790 async fn test_scenario_preference_contradiction() {
791 let tracker = create_tracker(42);
792
793 let old = create_entity("old-1", "User", "Loves hiking and outdoor activities");
794 let new = create_entity("new-1", "User", "Hates hiking, prefers indoor activities");
795
796 let result = tracker
797 .detect(&new, &[old], DetectionOptions::default())
798 .await;
799
800 assert!(result.is_ok());
801 }
802
803 #[tokio::test]
804 async fn test_scenario_derived_insight() {
805 let tracker = create_tracker(42);
806
807 let old = create_entity("old-1", "User", "Works from home 5 days a week");
808 let new = create_entity("new-1", "User", "Prefers remote work over office");
809
810 let result = tracker
811 .detect(&new, &[old], DetectionOptions::default())
812 .await;
813
814 assert!(result.is_ok());
815 }
816}
817
818#[cfg(test)]
823mod dst_tests {
824 use super::*;
825 use crate::dst::{FaultConfig, FaultType, SimConfig, Simulation};
826 use crate::llm::SimLLMProvider;
827 use crate::storage::{EntityType, SimStorageBackend};
828
829 fn create_entity(id: &str, name: &str, content: &str) -> Entity {
831 let mut entity = Entity::new(EntityType::Person, name.to_string(), content.to_string());
832 entity.id = id.to_string();
833 entity
834 }
835
836 #[tokio::test]
841 async fn test_detect_with_llm_timeout() {
842 println!("\n=== EvolutionTracker DST: LLM Timeout ===");
843
844 let sim = Simulation::new(SimConfig::with_seed(42))
845 .with_fault(FaultConfig::new(FaultType::LlmTimeout, 1.0)); sim.run(|env| async move {
848 let llm = SimLLMProvider::with_faults(42, env.faults.clone());
849 let tracker: EvolutionTracker<SimLLMProvider, SimStorageBackend> =
850 EvolutionTracker::new(llm);
851
852 let old_entity = create_entity("old-1", "Alice", "Works at Acme Corp");
853 let new_entity = create_entity("new-1", "Alice", "Left Acme, now at StartupX");
854
855 let result = tracker
856 .detect(&new_entity, &[old_entity], DetectionOptions::default())
857 .await;
858
859 println!("Result: {:?}", result);
860
861 assert!(
863 result.is_ok(),
864 "BUG: Expected Ok(_), got Err. EvolutionTracker should gracefully degrade, not error!"
865 );
866
867 let detection = result.unwrap();
868 assert!(
869 detection.is_none(),
870 "BUG: Expected None (LLM failure → skip detection), got Some. Fault may not have fired!"
871 );
872
873 println!("✓ LLM timeout correctly returns Ok(None) - graceful degradation verified");
874
875 Ok::<_, anyhow::Error>(())
876 })
877 .await
878 .unwrap();
879 }
880
881 #[tokio::test]
886 async fn test_detect_with_llm_rate_limit() {
887 println!("\n=== EvolutionTracker DST: LLM Rate Limit ===");
888
889 let sim = Simulation::new(SimConfig::with_seed(42))
890 .with_fault(FaultConfig::new(FaultType::LlmRateLimit, 1.0)); sim.run(|env| async move {
893 let llm = SimLLMProvider::with_faults(42, env.faults.clone());
894 let tracker: EvolutionTracker<SimLLMProvider, SimStorageBackend> =
895 EvolutionTracker::new(llm);
896
897 let old_entity = create_entity("old-1", "Bob", "Knows JavaScript");
898 let new_entity = create_entity("new-1", "Bob", "Also learned TypeScript");
899
900 let result = tracker
901 .detect(&new_entity, &[old_entity], DetectionOptions::default())
902 .await;
903
904 println!("Result: {:?}", result);
905
906 assert!(
908 result.is_ok(),
909 "BUG: Rate limit should return Ok(None), not error!"
910 );
911
912 assert!(
913 result.unwrap().is_none(),
914 "BUG: Rate limit should skip detection (None), got Some. Fault didn't fire!"
915 );
916
917 println!("✓ Rate limit correctly returns Ok(None)");
918
919 Ok::<_, anyhow::Error>(())
920 })
921 .await
922 .unwrap();
923 }
924
925 #[tokio::test]
930 async fn test_detect_with_llm_invalid_response() {
931 println!("\n=== EvolutionTracker DST: LLM Invalid Response ===");
932
933 let sim = Simulation::new(SimConfig::with_seed(42))
934 .with_fault(FaultConfig::new(FaultType::LlmInvalidResponse, 1.0)); sim.run(|env| async move {
937 let llm = SimLLMProvider::with_faults(42, env.faults.clone());
938 let tracker: EvolutionTracker<SimLLMProvider, SimStorageBackend> =
939 EvolutionTracker::new(llm);
940
941 let old_entity = create_entity("old-1", "User", "Loves hiking");
942 let new_entity = create_entity("new-1", "User", "Hates hiking");
943
944 let result = tracker
945 .detect(&new_entity, &[old_entity], DetectionOptions::default())
946 .await;
947
948 println!("Result: {:?}", result);
949
950 assert!(
952 result.is_ok(),
953 "BUG: Invalid response should return Ok(None), not error!"
954 );
955
956 assert!(
957 result.unwrap().is_none(),
958 "BUG: Invalid response should return None (parse failure), got Some. Fault didn't fire or parse didn't fail!"
959 );
960
961 println!("✓ Invalid response correctly returns Ok(None) - parse failure handled gracefully");
962
963 Ok::<_, anyhow::Error>(())
964 })
965 .await
966 .unwrap();
967 }
968
969 #[tokio::test]
974 async fn test_detect_with_probabilistic_llm_failure() {
975 println!("\n=== EvolutionTracker DST: Probabilistic Failure (50%) ===");
976
977 let sim = Simulation::new(SimConfig::with_seed(42))
978 .with_fault(FaultConfig::new(FaultType::LlmTimeout, 0.5)); sim.run(|env| async move {
981 let llm = SimLLMProvider::with_faults(42, env.faults.clone());
982 let tracker: EvolutionTracker<SimLLMProvider, SimStorageBackend> =
983 EvolutionTracker::new(llm);
984
985 let old_entity = create_entity("old-1", "Test", "Original content");
986
987 let mut none_count = 0;
989 let mut some_count = 0;
990
991 for i in 0..10 {
992 let new_entity = create_entity(&format!("new-{}", i), "Test", &format!("Content {}", i));
993
994 let result = tracker
995 .detect(&new_entity, &[old_entity.clone()], DetectionOptions::default())
996 .await;
997
998 assert!(result.is_ok(), "Iteration {}: Expected Ok, got Err", i);
999
1000 match result.unwrap() {
1001 None => none_count += 1,
1002 Some(_) => some_count += 1,
1003 }
1004 }
1005
1006 println!("Results after 10 detections with 50% failure rate (seed 42):");
1007 println!(" - Skipped (None): {}", none_count);
1008 println!(" - Detected (Some): {}", some_count);
1009
1010 assert!(
1014 none_count > 0,
1015 "BUG: Expected some failures (None), got 0. Fault may not be firing!"
1016 );
1017
1018 let total = none_count + some_count;
1021 assert_eq!(total, 10, "BUG: Should have exactly 10 results");
1022
1023 println!(
1024 "✓ Probabilistic failure is deterministic: {} skipped, {} detected (seed 42)",
1025 none_count, some_count
1026 );
1027 println!(" (Deterministic: same seed always produces same sequence)");
1028
1029 Ok::<_, anyhow::Error>(())
1030 })
1031 .await
1032 .unwrap();
1033 }
1034
1035 #[tokio::test]
1040 async fn test_detect_with_llm_service_unavailable() {
1041 println!("\n=== EvolutionTracker DST: LLM Service Unavailable ===");
1042
1043 let sim = Simulation::new(SimConfig::with_seed(42))
1044 .with_fault(FaultConfig::new(FaultType::LlmServiceUnavailable, 1.0)); sim.run(|env| async move {
1047 let llm = SimLLMProvider::with_faults(42, env.faults.clone());
1048 let tracker: EvolutionTracker<SimLLMProvider, SimStorageBackend> =
1049 EvolutionTracker::new(llm);
1050
1051 let old_entity = create_entity("old-1", "User", "Works from home");
1052 let new_entity = create_entity("new-1", "User", "Prefers remote work");
1053
1054 let result = tracker
1055 .detect(&new_entity, &[old_entity], DetectionOptions::default())
1056 .await;
1057
1058 println!("Result: {:?}", result);
1059
1060 assert!(
1062 result.is_ok(),
1063 "BUG: Service unavailable should return Ok(None), not error!"
1064 );
1065
1066 assert!(
1067 result.unwrap().is_none(),
1068 "BUG: Service unavailable should skip detection (None), got Some. Fault didn't fire!"
1069 );
1070
1071 println!("✓ Service unavailable correctly returns Ok(None)");
1072
1073 Ok::<_, anyhow::Error>(())
1074 })
1075 .await
1076 .unwrap();
1077 }
1078
1079 #[tokio::test]
1084 async fn test_detect_with_multiple_entities_and_faults() {
1085 println!("\n=== EvolutionTracker DST: Multiple Entities + Faults ===");
1086
1087 let sim = Simulation::new(SimConfig::with_seed(42))
1088 .with_fault(FaultConfig::new(FaultType::LlmTimeout, 1.0));
1089
1090 sim.run(|env| async move {
1091 let llm = SimLLMProvider::with_faults(42, env.faults.clone());
1092 let tracker: EvolutionTracker<SimLLMProvider, SimStorageBackend> =
1093 EvolutionTracker::new(llm);
1094
1095 let existing: Vec<Entity> = (0..5)
1097 .map(|i| create_entity(&format!("old-{}", i), "Alice", &format!("Content {}", i)))
1098 .collect();
1099
1100 let new_entity = create_entity("new-1", "Alice", "New information");
1101
1102 let result = tracker
1103 .detect(&new_entity, &existing, DetectionOptions::default())
1104 .await;
1105
1106 println!("Result with {} existing entities: {:?}", existing.len(), result);
1107
1108 assert!(result.is_ok(), "BUG: Should return Ok even with faults");
1110 assert!(
1111 result.unwrap().is_none(),
1112 "BUG: LLM failure should skip detection even with multiple existing entities"
1113 );
1114
1115 println!("✓ Fault correctly handled with multiple existing entities");
1116
1117 Ok::<_, anyhow::Error>(())
1118 })
1119 .await
1120 .unwrap();
1121 }
1122}