1#![allow(unused)]
2
3pub mod activation;
4pub mod builder;
5pub mod counterfactual;
6pub mod domain;
7pub mod error;
8pub mod graph;
9pub mod plasticity;
10pub mod query;
11pub mod resonance;
12pub mod seed;
13pub mod semantic;
14pub mod snapshot;
15pub mod temporal;
16pub mod topology;
17pub mod types;
18pub mod xlr;
19
20#[cfg(test)]
21mod tests {
22 use crate::activation::*;
23 use crate::counterfactual::*;
24 use crate::error::*;
25 use crate::graph::*;
26 use crate::plasticity::*;
27 use crate::query::*;
28 use crate::resonance::*;
29 use crate::seed::*;
30 use crate::temporal::*;
31 use crate::topology::*;
32 use crate::types::*;
33 use crate::xlr::*;
34
35 #[test]
38 #[cfg(debug_assertions)]
39 #[should_panic(expected = "non-finite")]
40 fn finite_f32_rejects_nan() {
41 let _f = FiniteF32::new(f32::NAN);
43 }
44
45 #[test]
46 #[cfg(debug_assertions)]
47 #[should_panic(expected = "non-finite")]
48 fn finite_f32_rejects_inf() {
49 let _f = FiniteF32::new(f32::INFINITY);
51 }
52
53 #[test]
54 fn finite_f32_accepts_normal() {
55 let f = FiniteF32::new(1.0);
56 assert_eq!(f.get(), 1.0);
57 }
58
59 #[test]
60 fn finite_f32_total_order() {
61 let a = FiniteF32::new(0.5);
62 let b = FiniteF32::new(0.7);
63 assert!(a < b);
64 assert_eq!(a.cmp(&a), std::cmp::Ordering::Equal);
65 }
66
67 #[test]
68 fn pos_f32_rejects_zero() {
69 assert!(PosF32::new(0.0).is_none());
70 }
71
72 #[test]
73 fn pos_f32_rejects_negative() {
74 assert!(PosF32::new(-1.0).is_none());
75 }
76
77 #[test]
78 fn pos_f32_accepts_positive() {
79 assert!(PosF32::new(0.001).is_some());
80 }
81
82 #[test]
83 fn learning_rate_range() {
84 assert!(LearningRate::new(0.0).is_none());
85 assert!(LearningRate::new(1.1).is_none());
86 assert!(LearningRate::new(0.5).is_some());
87 assert!(LearningRate::new(1.0).is_some());
88 }
89
90 #[test]
91 fn decay_factor_range() {
92 assert!(DecayFactor::new(0.0).is_none());
93 assert!(DecayFactor::new(1.1).is_none());
94 assert!(DecayFactor::new(0.55).is_some());
95 }
96
97 #[test]
100 fn error_display_empty_graph() {
101 let e = M1ndError::EmptyGraph;
102 let msg = format!("{e}");
103 assert!(msg.contains("empty"), "Expected 'empty' in: {msg}");
104 }
105
106 #[test]
107 fn error_display_dangling_edge() {
108 let e = M1ndError::DanglingEdge {
109 edge: EdgeIdx::new(0),
110 node: NodeId::new(999),
111 };
112 let msg = format!("{e}");
113 assert!(msg.contains("dangling"));
114 }
115
116 fn build_test_graph() -> Graph {
119 let mut g = Graph::new();
120 g.add_node(
121 "mat_pe",
122 "Polietileno",
123 NodeType::Material,
124 &["plastico", "polimero"],
125 1000.0,
126 0.5,
127 )
128 .unwrap();
129 g.add_node(
130 "mat_pp",
131 "Polipropileno",
132 NodeType::Material,
133 &["plastico", "polimero"],
134 900.0,
135 0.3,
136 )
137 .unwrap();
138 g.add_node(
139 "mat_abs",
140 "ABS",
141 NodeType::Material,
142 &["plastico"],
143 800.0,
144 0.2,
145 )
146 .unwrap();
147 g.add_node(
148 "proc_inj",
149 "Injecao",
150 NodeType::Process,
151 &["processo"],
152 700.0,
153 0.4,
154 )
155 .unwrap();
156 g.add_node(
157 "proc_ext",
158 "Extrusao",
159 NodeType::Process,
160 &["processo"],
161 600.0,
162 0.1,
163 )
164 .unwrap();
165 g.add_node(
166 "prod_garrafa",
167 "Garrafa",
168 NodeType::Product,
169 &["produto"],
170 500.0,
171 0.6,
172 )
173 .unwrap();
174
175 g.add_edge(
176 NodeId::new(0),
177 NodeId::new(3),
178 "feeds_into",
179 FiniteF32::new(0.8),
180 EdgeDirection::Forward,
181 false,
182 FiniteF32::new(0.5),
183 )
184 .unwrap();
185 g.add_edge(
186 NodeId::new(1),
187 NodeId::new(3),
188 "feeds_into",
189 FiniteF32::new(0.7),
190 EdgeDirection::Forward,
191 false,
192 FiniteF32::new(0.3),
193 )
194 .unwrap();
195 g.add_edge(
196 NodeId::new(2),
197 NodeId::new(4),
198 "feeds_into",
199 FiniteF32::new(0.6),
200 EdgeDirection::Forward,
201 false,
202 FiniteF32::new(0.2),
203 )
204 .unwrap();
205 g.add_edge(
206 NodeId::new(3),
207 NodeId::new(5),
208 "produces",
209 FiniteF32::new(0.9),
210 EdgeDirection::Forward,
211 false,
212 FiniteF32::new(0.8),
213 )
214 .unwrap();
215 g.add_edge(
216 NodeId::new(0),
217 NodeId::new(1),
218 "similar_to",
219 FiniteF32::new(0.5),
220 EdgeDirection::Bidirectional,
221 false,
222 FiniteF32::ZERO,
223 )
224 .unwrap();
225
226 g.finalize().unwrap();
227 g
228 }
229
230 #[test]
231 fn graph_add_node_and_resolve() {
232 let mut g = Graph::new();
233 let n1 = g
234 .add_node("ext1", "Label1", NodeType::Module, &[], 0.0, 0.0)
235 .unwrap();
236 assert_eq!(n1, NodeId::new(0));
237 assert_eq!(g.num_nodes(), 1);
238 assert_eq!(g.resolve_id("ext1"), Some(NodeId::new(0)));
239 }
240
241 #[test]
242 fn graph_add_node_duplicate() {
243 let mut g = Graph::new();
244 g.add_node("ext1", "label1", NodeType::Module, &[], 0.0, 0.0)
245 .unwrap();
246 let n2 = g.add_node("ext1", "label2", NodeType::Module, &[], 0.0, 0.0);
247 assert!(matches!(n2, Err(M1ndError::DuplicateNode(_))));
248 }
249
250 #[test]
251 fn graph_add_edge_dangling() {
252 let mut g = Graph::new();
253 let n1 = g
254 .add_node("a", "A", NodeType::Module, &[], 0.0, 0.0)
255 .unwrap();
256 let bad = NodeId::new(999);
257 let e = g.add_edge(
258 n1,
259 bad,
260 "calls",
261 FiniteF32::ONE,
262 EdgeDirection::Forward,
263 false,
264 FiniteF32::ZERO,
265 );
266 assert!(matches!(e, Err(M1ndError::DanglingEdge { .. })));
267 }
268
269 #[test]
270 fn graph_finalize_builds_csr() {
271 let g = build_test_graph();
272 assert!(g.finalized);
273 assert_eq!(g.num_nodes(), 6);
274 assert!(g.num_edges() > 0);
275 assert!(g.pagerank_computed);
276 }
277
278 #[test]
279 fn graph_pagerank_computed() {
280 let g = build_test_graph();
281 let max_pr = (0..g.num_nodes() as usize)
283 .map(|i| g.nodes.pagerank[i].get())
284 .fold(0.0f32, f32::max);
285 assert!(max_pr > 0.0, "PageRank should have non-zero values");
286 }
287
288 #[test]
291 fn seed_finder_exact_match() {
292 let g = build_test_graph();
293 let seeds = SeedFinder::find_seeds(&g, "Polietileno", 200).unwrap();
294 assert!(!seeds.is_empty(), "Should find at least one seed");
295 assert_eq!(
296 seeds[0].1.get(),
297 1.0,
298 "Exact match should have relevance 1.0"
299 );
300 }
301
302 #[test]
303 fn seed_finder_tag_match() {
304 let g = build_test_graph();
305 let seeds = SeedFinder::find_seeds(&g, "plastico", 200).unwrap();
306 assert!(!seeds.is_empty(), "Should find seeds by tag");
307 }
308
309 #[test]
310 fn seed_finder_caps_at_max() {
311 let g = build_test_graph();
312 let seeds = SeedFinder::find_seeds(&g, "a", 2).unwrap();
313 assert!(seeds.len() <= 2);
314 }
315
316 #[test]
319 fn bloom_filter_basic() {
320 let mut bf = BloomFilter::with_capacity(1000, 0.01);
321 bf.insert(NodeId::new(42));
322 assert!(bf.probably_contains(NodeId::new(42)));
323 }
326
327 #[test]
328 fn wavefront_single_seed() {
329 let g = build_test_graph();
330 let engine = WavefrontEngine::new();
331 let config = PropagationConfig::default();
332 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
333 let result = engine.propagate(&g, &seeds, &config).unwrap();
334 assert!(
335 !result.scores.is_empty(),
336 "Wavefront should activate at least one node"
337 );
338 assert!(result.scores[0].1.get() > 0.0);
339 }
340
341 #[test]
342 fn heap_single_seed() {
343 let g = build_test_graph();
344 let engine = HeapEngine::new();
345 let config = PropagationConfig::default();
346 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
347 let result = engine.propagate(&g, &seeds, &config).unwrap();
348 assert!(
349 !result.scores.is_empty(),
350 "Heap should activate at least one node"
351 );
352 }
353
354 #[test]
355 fn hybrid_delegates_correctly() {
356 let g = build_test_graph();
357 let engine = HybridEngine::new();
358 let config = PropagationConfig::default();
359 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
360 let result = engine.propagate(&g, &seeds, &config).unwrap();
361 assert!(!result.scores.is_empty());
362 }
363
364 #[test]
365 fn activation_empty_seeds_returns_empty() {
366 let g = build_test_graph();
367 let engine = WavefrontEngine::new();
368 let config = PropagationConfig::default();
369 let result = engine.propagate(&g, &[], &config).unwrap();
370 assert!(result.scores.is_empty());
371 }
372
373 #[test]
374 fn merge_dimensions_resonance_bonus() {
375 let make_dim = |dim: Dimension, scores: Vec<(NodeId, FiniteF32)>| DimensionResult {
377 scores,
378 dimension: dim,
379 elapsed_ns: 0,
380 };
381
382 let node = NodeId::new(0);
383 let score = FiniteF32::new(0.5);
384 let results = [
385 make_dim(Dimension::Structural, vec![(node, score)]),
386 make_dim(Dimension::Semantic, vec![(node, score)]),
387 make_dim(Dimension::Temporal, vec![(node, score)]),
388 make_dim(Dimension::Causal, vec![(node, score)]),
389 ];
390
391 let merged = merge_dimensions(&results, 10).unwrap();
392 assert!(!merged.activated.is_empty());
393 let activated = &merged.activated[0];
394 assert_eq!(activated.active_dimension_count, 4);
395 let base = 0.5 * 0.35 + 0.5 * 0.25 + 0.5 * 0.15 + 0.5 * 0.25;
397 let expected = base * RESONANCE_BONUS_4DIM;
398 assert!(
399 (activated.activation.get() - expected).abs() < 0.01,
400 "Expected ~{expected}, got {}",
401 activated.activation.get()
402 );
403 }
404
405 #[test]
408 fn xlr_sigmoid_gate() {
409 let zero = AdaptiveXlrEngine::sigmoid_gate(FiniteF32::ZERO);
410 assert!((zero.get() - 0.5).abs() < 0.01, "sigmoid(0) should be ~0.5");
411
412 let positive = AdaptiveXlrEngine::sigmoid_gate(FiniteF32::new(1.0));
413 assert!(positive.get() > 0.5, "sigmoid(+) should be > 0.5");
414
415 let negative = AdaptiveXlrEngine::sigmoid_gate(FiniteF32::new(-1.0));
416 assert!(negative.get() < 0.5, "sigmoid(-) should be < 0.5");
417 }
418
419 #[test]
420 fn xlr_pick_anti_seeds() {
421 let g = build_test_graph();
422 let xlr = AdaptiveXlrEngine::with_defaults();
423 let seeds = vec![NodeId::new(0)];
424 let anti = xlr.pick_anti_seeds(&g, &seeds).unwrap();
425 assert!(!anti.contains(&NodeId::new(0)));
427 }
428
429 #[test]
430 fn xlr_immunity_bfs() {
431 let g = build_test_graph();
432 let xlr = AdaptiveXlrEngine::with_defaults();
433 let seeds = vec![NodeId::new(0)];
434 let immunity = xlr.compute_immunity(&g, &seeds).unwrap();
435 assert!(immunity[0], "Seed itself should be immune");
436 }
437
438 #[test]
441 fn temporal_decay_clamps_negative_age() {
442 let scorer = TemporalDecayScorer::new(PosF32::new(168.0).unwrap());
443 let result = scorer.score_one(-10.0, FiniteF32::ZERO, None);
444 assert!(
446 (result.raw_decay.get() - 1.0).abs() < 0.01,
447 "Negative age should clamp to decay=1.0, got {}",
448 result.raw_decay.get()
449 );
450 }
451
452 #[test]
453 fn temporal_decay_exponential() {
454 let scorer = TemporalDecayScorer::new(PosF32::new(168.0).unwrap());
455 let result = scorer.score_one(168.0, FiniteF32::ZERO, None);
456 assert!(
458 (result.raw_decay.get() - 0.5).abs() < 0.05,
459 "After one half-life, decay ~0.5, got {}",
460 result.raw_decay.get()
461 );
462 }
463
464 #[test]
465 fn causal_chain_budget_limits() {
466 let g = build_test_graph();
467 let detector = CausalChainDetector::new(6, FiniteF32::new(0.01), 100);
468 let chains = detector.detect(&g, NodeId::new(0)).unwrap();
469 assert!(!chains.is_empty() || g.num_edges() == 0);
472 }
473
474 #[test]
477 fn louvain_detects_communities() {
478 let g = build_test_graph();
479 let detector = CommunityDetector::with_defaults();
480 let result = detector.detect(&g).unwrap();
481 assert!(result.num_communities >= 1);
482 assert_eq!(result.assignments.len(), g.num_nodes() as usize);
483 }
484
485 #[test]
486 fn louvain_empty_graph_error() {
487 let g = Graph::new();
488 let detector = CommunityDetector::with_defaults();
489 let result = detector.detect(&g);
490 assert!(matches!(result, Err(M1ndError::EmptyGraph)));
491 }
492
493 #[test]
494 fn bridge_detection() {
495 let g = build_test_graph();
496 let detector = CommunityDetector::with_defaults();
497 let communities = detector.detect(&g).unwrap();
498 let bridges = BridgeDetector::detect(&g, &communities).unwrap();
499 let _ = bridges;
502 }
503
504 #[test]
505 fn spectral_gap_empty_graph() {
506 let g = Graph::new();
507 let analyzer = SpectralGapAnalyzer::with_defaults();
508 let result = analyzer.analyze(&g);
509 assert!(matches!(result, Err(M1ndError::EmptyGraph)));
510 }
511
512 #[test]
515 fn wave_accumulator_complex_interference() {
516 let mut acc = WaveAccumulator::default();
517 let pulse1 = WavePulse {
518 node: NodeId::new(0),
519 amplitude: FiniteF32::ONE,
520 phase: FiniteF32::ZERO,
521 frequency: PosF32::new(1.0).unwrap(),
522 wavelength: PosF32::new(4.0).unwrap(),
523 hops: 0,
524 prev_node: NodeId::new(0),
525 };
526 acc.accumulate(&pulse1);
527 let amp = acc.amplitude().get();
528 assert!(
529 (amp - 1.0).abs() < 0.01,
530 "Single pulse amplitude should be ~1.0"
531 );
532 }
533
534 #[test]
535 fn standing_wave_propagation() {
536 let g = build_test_graph();
537 let propagator = StandingWavePropagator::new(5, FiniteF32::new(0.01), 10_000);
538 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
539 let result = propagator
540 .propagate(
541 &g,
542 &seeds,
543 PosF32::new(1.0).unwrap(),
544 PosF32::new(4.0).unwrap(),
545 )
546 .unwrap();
547 assert!(result.pulses_processed > 0);
548 assert!(!result.antinodes.is_empty());
549 }
550
551 #[test]
554 fn query_memory_ring_buffer() {
555 let mut mem = QueryMemory::new(3, 10);
556 assert!(mem.is_empty());
557
558 for i in 0..5 {
559 mem.record(QueryRecord {
560 query_text: format!("query_{i}"),
561 seeds: vec![NodeId::new(i)],
562 activated_nodes: vec![NodeId::new(i), NodeId::new(i + 1)],
563 timestamp: i as f64,
564 });
565 }
566 assert_eq!(mem.len(), 3);
568 }
569
570 #[test]
571 fn plasticity_generation_check() {
572 let mut g = build_test_graph();
573 let engine = PlasticityEngine::new(&g, PlasticityConfig::default());
574 let _ = engine;
578 }
579
580 #[test]
583 fn removal_mask_basic() {
584 let g = build_test_graph();
585 let mut mask = RemovalMask::new(g.num_nodes(), g.num_edges());
586 assert!(!mask.is_node_removed(NodeId::new(0)));
587 mask.remove_node(&g, NodeId::new(0));
588 assert!(mask.is_node_removed(NodeId::new(0)));
589 mask.reset();
590 assert!(!mask.is_node_removed(NodeId::new(0)));
591 }
592
593 #[test]
596 fn query_orchestrator_builds() {
597 let g = build_test_graph();
598 let orch = QueryOrchestrator::build(&g);
599 assert!(orch.is_ok(), "Orchestrator should build from test graph");
600 }
601
602 #[test]
603 fn full_query_execution() {
604 let mut g = build_test_graph();
605 let mut orch = QueryOrchestrator::build(&g).unwrap();
606 let config = QueryConfig {
607 query: "plastico polimero".to_string(),
608 agent_id: "test".to_string(),
609 top_k: 10,
610 xlr_enabled: false, include_ghost_edges: true,
612 include_structural_holes: false,
613 ..QueryConfig::default()
614 };
615 let result = orch.query(&mut g, &config).unwrap();
616 assert!(result.elapsed_ms >= 0.0);
617 }
620
621 fn make_test_graph() -> Graph {
627 let mut g = Graph::new();
628 g.add_node(
630 "n0",
631 "Alpha",
632 NodeType::Material,
633 &["group_a", "core"],
634 1000.0,
635 0.9,
636 )
637 .unwrap();
638 g.add_node(
639 "n1",
640 "Beta",
641 NodeType::Material,
642 &["group_a", "core"],
643 900.0,
644 0.8,
645 )
646 .unwrap();
647 g.add_node("n2", "Gamma", NodeType::Process, &["group_b"], 800.0, 0.7)
648 .unwrap();
649 g.add_node("n3", "Delta", NodeType::Process, &["group_b"], 700.0, 0.6)
650 .unwrap();
651 g.add_node("n4", "Epsilon", NodeType::Product, &["group_c"], 600.0, 0.5)
652 .unwrap();
653 g.add_node("n5", "Zeta", NodeType::Product, &["group_c"], 500.0, 0.4)
654 .unwrap();
655 g.add_node("n6", "Eta", NodeType::Module, &["group_d"], 400.0, 0.3)
656 .unwrap();
657 g.add_node("n7", "Theta", NodeType::Module, &["group_d"], 300.0, 0.2)
658 .unwrap();
659 g.add_node("n8", "Iota", NodeType::Concept, &["group_e"], 200.0, 0.1)
660 .unwrap();
661 g.add_node("n9", "Kappa", NodeType::Concept, &["group_e"], 100.0, 0.05)
662 .unwrap();
663
664 g.add_edge(
666 NodeId::new(0),
667 NodeId::new(1),
668 "similar",
669 FiniteF32::new(0.9),
670 EdgeDirection::Bidirectional,
671 false,
672 FiniteF32::ZERO,
673 )
674 .unwrap();
675 g.add_edge(
676 NodeId::new(0),
677 NodeId::new(2),
678 "feeds",
679 FiniteF32::new(0.8),
680 EdgeDirection::Forward,
681 false,
682 FiniteF32::new(0.7),
683 )
684 .unwrap();
685 g.add_edge(
686 NodeId::new(0),
687 NodeId::new(3),
688 "feeds",
689 FiniteF32::new(0.7),
690 EdgeDirection::Forward,
691 false,
692 FiniteF32::new(0.6),
693 )
694 .unwrap();
695 g.add_edge(
696 NodeId::new(0),
697 NodeId::new(4),
698 "feeds",
699 FiniteF32::new(0.6),
700 EdgeDirection::Forward,
701 false,
702 FiniteF32::new(0.5),
703 )
704 .unwrap();
705 g.add_edge(
706 NodeId::new(0),
707 NodeId::new(5),
708 "feeds",
709 FiniteF32::new(0.5),
710 EdgeDirection::Forward,
711 false,
712 FiniteF32::new(0.4),
713 )
714 .unwrap();
715 g.add_edge(
716 NodeId::new(2),
717 NodeId::new(4),
718 "produces",
719 FiniteF32::new(0.8),
720 EdgeDirection::Forward,
721 false,
722 FiniteF32::new(0.8),
723 )
724 .unwrap();
725 g.add_edge(
726 NodeId::new(3),
727 NodeId::new(5),
728 "produces",
729 FiniteF32::new(0.7),
730 EdgeDirection::Forward,
731 false,
732 FiniteF32::new(0.7),
733 )
734 .unwrap();
735 g.add_edge(
736 NodeId::new(4),
737 NodeId::new(6),
738 "uses",
739 FiniteF32::new(0.6),
740 EdgeDirection::Forward,
741 false,
742 FiniteF32::new(0.3),
743 )
744 .unwrap();
745 g.add_edge(
746 NodeId::new(5),
747 NodeId::new(7),
748 "uses",
749 FiniteF32::new(0.5),
750 EdgeDirection::Forward,
751 false,
752 FiniteF32::new(0.2),
753 )
754 .unwrap();
755 g.add_edge(
756 NodeId::new(6),
757 NodeId::new(8),
758 "refs",
759 FiniteF32::new(0.4),
760 EdgeDirection::Forward,
761 false,
762 FiniteF32::new(0.1),
763 )
764 .unwrap();
765 g.add_edge(
766 NodeId::new(7),
767 NodeId::new(9),
768 "refs",
769 FiniteF32::new(0.3),
770 EdgeDirection::Forward,
771 false,
772 FiniteF32::ZERO,
773 )
774 .unwrap();
775 g.add_edge(
776 NodeId::new(1),
777 NodeId::new(3),
778 "feeds",
779 FiniteF32::new(0.6),
780 EdgeDirection::Forward,
781 false,
782 FiniteF32::new(0.5),
783 )
784 .unwrap();
785 g.add_edge(
786 NodeId::new(8),
787 NodeId::new(9),
788 "related",
789 FiniteF32::new(0.5),
790 EdgeDirection::Bidirectional,
791 false,
792 FiniteF32::ZERO,
793 )
794 .unwrap();
795 g.add_edge(
796 NodeId::new(6),
797 NodeId::new(7),
798 "related",
799 FiniteF32::new(0.4),
800 EdgeDirection::Bidirectional,
801 false,
802 FiniteF32::ZERO,
803 )
804 .unwrap();
805 g.add_edge(
806 NodeId::new(2),
807 NodeId::new(3),
808 "related",
809 FiniteF32::new(0.3),
810 EdgeDirection::Bidirectional,
811 false,
812 FiniteF32::ZERO,
813 )
814 .unwrap();
815
816 g.finalize().unwrap();
817 g
818 }
819
820 #[test]
823 fn test_semantic_cooccurrence_builds_from_graph() {
824 use crate::semantic::CoOccurrenceIndex;
825 let g = make_test_graph();
826 let idx = CoOccurrenceIndex::build(&g, 10, 20, 4).unwrap();
827 let _ = idx; }
830
831 #[test]
832 fn test_semantic_ppmi_positive_values() {
833 use crate::semantic::CoOccurrenceIndex;
834 let g = make_test_graph();
835 let idx = CoOccurrenceIndex::build(&g, 10, 20, 4).unwrap();
836 let results = idx.query_top_k(NodeId::new(0), 5);
838 for &(_, score) in &results {
840 assert!(
841 score.get() >= 0.0,
842 "PPMI score must be non-negative, got {}",
843 score.get()
844 );
845 }
846 }
847
848 #[test]
849 fn test_semantic_engine_finds_similar_nodes() {
850 use crate::semantic::SemanticEngine;
851 let g = make_test_graph();
852 let engine = SemanticEngine::build(&g, SemanticWeights::default()).unwrap();
853 let results = engine.query(&g, "Alpha", 5).unwrap();
854 assert!(
856 !results.is_empty(),
857 "Semantic query for exact label should return results"
858 );
859 }
860
861 #[test]
862 fn test_semantic_search_exact_label_match() {
863 use crate::semantic::CharNgramIndex;
864 let g = make_test_graph();
865 let idx = CharNgramIndex::build(&g, 3).unwrap();
866 let results = idx.query_top_k("Alpha", 10);
867 assert!(
869 !results.is_empty(),
870 "N-gram search for exact label should find matches"
871 );
872 assert_eq!(
874 results[0].0,
875 NodeId::new(0),
876 "Top result should be node 0 (Alpha)"
877 );
878 }
879
880 #[test]
881 fn test_semantic_search_empty_query_returns_empty() {
882 use crate::semantic::CharNgramIndex;
883 let g = make_test_graph();
884 let idx = CharNgramIndex::build(&g, 3).unwrap();
885 let results = idx.query_top_k("", 10);
886 let _ = results;
888 }
889
890 #[test]
891 fn test_semantic_cosine_similarity_identical_vectors() {
892 use crate::semantic::CharNgramIndex;
893 let g = make_test_graph();
894 let idx = CharNgramIndex::build(&g, 3).unwrap();
895 let qvec = idx.query_vector("Alpha");
896 let sim = CharNgramIndex::cosine_similarity(&qvec, &qvec);
897 assert!(
898 (sim.get() - 1.0).abs() < 0.01,
899 "Self-similarity should be ~1.0, got {}",
900 sim.get()
901 );
902 }
903
904 #[test]
905 fn test_semantic_cosine_similarity_empty_vectors() {
906 use crate::semantic::{CharNgramIndex, NgramVector};
907 let empty: NgramVector = std::collections::HashMap::new();
908 let sim = CharNgramIndex::cosine_similarity(&empty, &empty);
909 assert_eq!(
910 sim.get(),
911 0.0,
912 "Cosine similarity of empty vectors should be 0"
913 );
914 }
915
916 #[test]
917 fn test_semantic_synonym_expansion() {
918 use crate::semantic::SynonymExpander;
919 let expander = SynonymExpander::build_default().unwrap();
920 let expanded = expander.expand("plastico");
921 assert!(expanded.contains(&"plastico".to_string()));
922 assert!(expanded.contains(&"polimero".to_string()));
923 assert!(expanded.contains(&"resina".to_string()));
924 }
925
926 #[test]
927 fn test_semantic_synonym_are_synonyms() {
928 use crate::semantic::SynonymExpander;
929 let expander = SynonymExpander::build_default().unwrap();
930 assert!(expander.are_synonyms("plastico", "polimero"));
931 assert!(expander.are_synonyms("Plastico", "POLIMERO")); assert!(!expander.are_synonyms("plastico", "metal"));
933 }
934
935 #[test]
938 fn test_snapshot_roundtrip_graph() {
939 use crate::snapshot::{load_graph, save_graph};
940 let g = make_test_graph();
941 let path = std::path::PathBuf::from("/tmp/m1nd_test_snapshot_graph.json");
942 save_graph(&g, &path).unwrap();
943 let loaded = load_graph(&path).unwrap();
944 assert_eq!(
945 loaded.num_nodes(),
946 g.num_nodes(),
947 "Node count mismatch after roundtrip"
948 );
949 assert!(loaded.num_edges() > 0, "Loaded graph should have edges");
951 assert!(loaded.finalized, "Loaded graph should be finalized");
952 let _ = std::fs::remove_file(&path);
954 }
955
956 #[test]
957 fn test_snapshot_roundtrip_preserves_labels() {
958 use crate::snapshot::{load_graph, save_graph};
959 let g = make_test_graph();
960 let path = std::path::PathBuf::from("/tmp/m1nd_test_snapshot_labels.json");
961 save_graph(&g, &path).unwrap();
962 let loaded = load_graph(&path).unwrap();
963 assert!(
965 loaded.resolve_id("n0").is_some(),
966 "Should resolve 'n0' after roundtrip"
967 );
968 assert!(
969 loaded.resolve_id("n9").is_some(),
970 "Should resolve 'n9' after roundtrip"
971 );
972 let _ = std::fs::remove_file(&path);
973 }
974
975 #[test]
976 fn test_snapshot_roundtrip_preserves_provenance() {
977 use crate::graph::NodeProvenanceInput;
978 use crate::snapshot::{load_graph, save_graph};
979
980 let mut g = make_test_graph();
981 let node = g.resolve_id("n0").unwrap();
982 g.set_node_provenance(
983 node,
984 NodeProvenanceInput {
985 source_path: Some("memory/2026-03-13.md"),
986 line_start: Some(12),
987 line_end: Some(14),
988 excerpt: Some("Batman mode means peak build window."),
989 namespace: Some("memory"),
990 canonical: true,
991 },
992 );
993
994 let path = std::path::PathBuf::from("/tmp/m1nd_test_snapshot_provenance.json");
995 save_graph(&g, &path).unwrap();
996 let loaded = load_graph(&path).unwrap();
997 let provenance = loaded.resolve_node_provenance(loaded.resolve_id("n0").unwrap());
998
999 assert_eq!(
1000 provenance.source_path.as_deref(),
1001 Some("memory/2026-03-13.md")
1002 );
1003 assert_eq!(provenance.line_start, Some(12));
1004 assert_eq!(provenance.line_end, Some(14));
1005 assert_eq!(
1006 provenance.excerpt.as_deref(),
1007 Some("Batman mode means peak build window.")
1008 );
1009 assert_eq!(provenance.namespace.as_deref(), Some("memory"));
1010 assert!(provenance.canonical);
1011
1012 let _ = std::fs::remove_file(&path);
1013 }
1014
1015 #[test]
1016 fn test_snapshot_roundtrip_plasticity_state() {
1017 use crate::plasticity::SynapticState;
1018 use crate::snapshot::{load_plasticity_state, save_plasticity_state};
1019 let states = vec![
1020 SynapticState {
1021 source_label: "n0".to_string(),
1022 target_label: "n2".to_string(),
1023 relation: "feeds".to_string(),
1024 original_weight: 0.8,
1025 current_weight: 0.85,
1026 strengthen_count: 3,
1027 weaken_count: 0,
1028 ltp_applied: false,
1029 ltd_applied: false,
1030 },
1031 SynapticState {
1032 source_label: "n2".to_string(),
1033 target_label: "n4".to_string(),
1034 relation: "produces".to_string(),
1035 original_weight: 0.7,
1036 current_weight: 0.65,
1037 strengthen_count: 0,
1038 weaken_count: 2,
1039 ltp_applied: false,
1040 ltd_applied: false,
1041 },
1042 ];
1043 let path = std::path::PathBuf::from("/tmp/m1nd_test_plasticity_state.json");
1044 save_plasticity_state(&states, &path).unwrap();
1045 let loaded = load_plasticity_state(&path).unwrap();
1046 assert_eq!(loaded.len(), 2);
1047 assert_eq!(loaded[0].source_label, "n0");
1048 assert!((loaded[0].current_weight - 0.85).abs() < 1e-5);
1049 assert_eq!(loaded[1].weaken_count, 2);
1050 let _ = std::fs::remove_file(&path);
1051 }
1052
1053 #[test]
1054 fn test_snapshot_load_nonexistent_file_returns_error() {
1055 use crate::snapshot::load_graph;
1056 let path = std::path::Path::new("/tmp/m1nd_test_nonexistent_42.json");
1057 let result = load_graph(path);
1058 assert!(
1059 result.is_err(),
1060 "Loading nonexistent file should return error"
1061 );
1062 }
1063
1064 #[test]
1065 fn test_snapshot_load_corrupt_json_returns_error() {
1066 use crate::snapshot::load_graph;
1067 let path = std::path::PathBuf::from("/tmp/m1nd_test_corrupt.json");
1068 std::fs::write(&path, "{ this is not valid json !!!").unwrap();
1069 let result = load_graph(&path);
1070 assert!(result.is_err(), "Loading corrupt JSON should return error");
1071 let _ = std::fs::remove_file(&path);
1072 }
1073
1074 #[test]
1075 fn test_snapshot_saved_graph_is_valid_json() {
1076 use crate::snapshot::save_graph;
1077 let g = make_test_graph();
1078 let path = std::path::PathBuf::from("/tmp/m1nd_test_valid_json.json");
1079 save_graph(&g, &path).unwrap();
1080 let data = std::fs::read_to_string(&path).unwrap();
1081 let parsed: serde_json::Value = serde_json::from_str(&data).unwrap();
1082 assert!(parsed.is_object(), "Saved snapshot should be a JSON object");
1083 assert!(
1084 parsed.get("version").is_some(),
1085 "Snapshot should have version field"
1086 );
1087 assert!(
1088 parsed.get("nodes").is_some(),
1089 "Snapshot should have nodes field"
1090 );
1091 assert!(
1092 parsed.get("edges").is_some(),
1093 "Snapshot should have edges field"
1094 );
1095 let _ = std::fs::remove_file(&path);
1096 }
1097
1098 #[test]
1101 fn test_query_orchestrator_builds_from_finalized_graph() {
1102 let g = make_test_graph();
1103 let orch = QueryOrchestrator::build(&g);
1104 assert!(
1105 orch.is_ok(),
1106 "Orchestrator should build from finalized graph"
1107 );
1108 }
1109
1110 #[test]
1111 fn test_query_execution_returns_results_for_existing_node() {
1112 let mut g = make_test_graph();
1113 let mut orch = QueryOrchestrator::build(&g).unwrap();
1114 let config = QueryConfig {
1115 query: "Alpha".to_string(),
1116 agent_id: "test_agent".to_string(),
1117 top_k: 10,
1118 xlr_enabled: false,
1119 include_ghost_edges: false,
1120 include_structural_holes: false,
1121 ..QueryConfig::default()
1122 };
1123 let result = orch.query(&mut g, &config).unwrap();
1124 assert!(result.elapsed_ms >= 0.0);
1125 }
1126
1127 #[test]
1128 fn test_query_with_nonexistent_term_returns_gracefully() {
1129 let mut g = make_test_graph();
1130 let mut orch = QueryOrchestrator::build(&g).unwrap();
1131 let config = QueryConfig {
1132 query: "zzzznonexistent_xyz_987".to_string(),
1133 agent_id: "test".to_string(),
1134 top_k: 5,
1135 xlr_enabled: false,
1136 include_ghost_edges: false,
1137 include_structural_holes: false,
1138 ..QueryConfig::default()
1139 };
1140 let result = orch.query(&mut g, &config).unwrap();
1141 assert!(result.elapsed_ms >= 0.0);
1143 }
1144
1145 #[test]
1146 fn test_query_memory_deduplication() {
1147 let mut mem = QueryMemory::new(10, 20);
1148 let record1 = QueryRecord {
1149 query_text: "Alpha".to_string(),
1150 seeds: vec![NodeId::new(0)],
1151 activated_nodes: vec![NodeId::new(0), NodeId::new(1)],
1152 timestamp: 1.0,
1153 };
1154 let record2 = QueryRecord {
1155 query_text: "Alpha".to_string(),
1156 seeds: vec![NodeId::new(0)],
1157 activated_nodes: vec![NodeId::new(0), NodeId::new(1)],
1158 timestamp: 2.0,
1159 };
1160 mem.record(record1);
1161 mem.record(record2);
1162 assert_eq!(mem.len(), 2, "Both records should be stored");
1163 let priming = mem.get_priming_signal(&[NodeId::new(0)], FiniteF32::new(0.5));
1165 let has_node1 = priming.iter().any(|(n, _)| *n == NodeId::new(1));
1167 assert!(has_node1, "Node 1 should appear in priming signal");
1168 }
1169
1170 #[test]
1173 fn test_counterfactual_removal_of_leaf_node() {
1174 let g = make_test_graph();
1175 let engine = HybridEngine::new();
1176 let config = PropagationConfig::default();
1177 let cf = CounterfactualEngine::with_defaults();
1178 let result = cf
1180 .simulate_removal(&g, &engine, &config, &[NodeId::new(9)])
1181 .unwrap();
1182 assert!(
1183 result.total_impact.get() >= 0.0,
1184 "Impact should be non-negative"
1185 );
1186 }
1188
1189 #[test]
1190 fn test_counterfactual_removal_of_hub_node_has_positive_impact() {
1191 let g = make_test_graph();
1192 let engine = HybridEngine::new();
1193 let config = PropagationConfig::default();
1194 let cf = CounterfactualEngine::with_defaults();
1195 let result = cf
1197 .simulate_removal(&g, &engine, &config, &[NodeId::new(0)])
1198 .unwrap();
1199 assert!(
1200 result.total_impact.get() >= 0.0,
1201 "Hub removal should have non-negative impact"
1202 );
1203 }
1204
1205 #[test]
1206 fn test_counterfactual_empty_removal_returns_zero_impact() {
1207 let g = make_test_graph();
1208 let engine = HybridEngine::new();
1209 let config = PropagationConfig::default();
1210 let cf = CounterfactualEngine::with_defaults();
1211 let result = cf.simulate_removal(&g, &engine, &config, &[]).unwrap();
1212 assert!(
1213 (result.total_impact.get() - 0.0).abs() < 0.01,
1214 "Empty removal should have ~0% impact, got {}",
1215 result.total_impact.get()
1216 );
1217 }
1218
1219 #[test]
1220 fn test_counterfactual_multi_node_combined_ge_individual() {
1221 let g = make_test_graph();
1222 let engine = HybridEngine::new();
1223 let config = PropagationConfig::default();
1224 let cf = CounterfactualEngine::with_defaults();
1225 let synergy = cf
1226 .synergy_analysis(&g, &engine, &config, &[NodeId::new(0), NodeId::new(2)])
1227 .unwrap();
1228 let max_individual = synergy
1230 .individual_impacts
1231 .iter()
1232 .map(|(_, s)| s.get())
1233 .fold(0.0f32, f32::max);
1234 assert!(
1235 synergy.combined_impact.get() >= max_individual * 0.5,
1236 "Combined impact ({}) should be significant relative to individual ({})",
1237 synergy.combined_impact.get(),
1238 max_individual
1239 );
1240 }
1241
1242 #[test]
1243 fn test_counterfactual_cascade_analysis() {
1244 let g = make_test_graph();
1245 let engine = HybridEngine::new();
1246 let config = PropagationConfig::default();
1247 let cf = CounterfactualEngine::with_defaults();
1248 let cascade = cf
1249 .cascade_analysis(&g, &engine, &config, NodeId::new(0))
1250 .unwrap();
1251 assert!(
1253 cascade.total_affected > 0,
1254 "Hub removal should have downstream cascade"
1255 );
1256 assert!(
1257 cascade.cascade_depth > 0,
1258 "Cascade depth from hub should be > 0"
1259 );
1260 }
1261
1262 #[test]
1263 fn test_counterfactual_removal_mask_edge_removal() {
1264 let g = make_test_graph();
1265 let mut mask = RemovalMask::new(g.num_nodes(), g.num_edges());
1266 mask.remove_node(&g, NodeId::new(0));
1268 let out_range = g.csr.out_range(NodeId::new(0));
1269 for j in out_range {
1270 assert!(
1271 mask.is_edge_removed(EdgeIdx::new(j as u32)),
1272 "Edge {} from removed node should be marked removed",
1273 j
1274 );
1275 }
1276 }
1277
1278 #[test]
1281 fn test_plasticity_engine_creates_correct_entries() {
1282 let g = make_test_graph();
1283 let engine = PlasticityEngine::new(&g, PlasticityConfig::default());
1284 let _ = engine;
1286 }
1287
1288 #[test]
1289 fn test_plasticity_strengthen_edge_increases_weight() {
1290 let mut g = make_test_graph();
1291 let mut engine = PlasticityEngine::new(&g, PlasticityConfig::default());
1292 let edge0_before = g.csr.read_weight(EdgeIdx::new(0)).get();
1294 let activated = vec![
1296 (NodeId::new(0), FiniteF32::ONE),
1297 (NodeId::new(1), FiniteF32::ONE),
1298 (NodeId::new(2), FiniteF32::ONE),
1299 (NodeId::new(3), FiniteF32::ONE),
1300 (NodeId::new(4), FiniteF32::ONE),
1301 ];
1302 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1303 let result = engine
1304 .update(&mut g, &activated, &seeds, "test query")
1305 .unwrap();
1306 assert!(
1307 result.edges_strengthened > 0,
1308 "At least one edge should be strengthened"
1309 );
1310 }
1311
1312 #[test]
1313 fn test_plasticity_export_state_produces_real_labels() {
1314 let g = make_test_graph();
1315 let engine = PlasticityEngine::new(&g, PlasticityConfig::default());
1316 let states = engine.export_state(&g).unwrap();
1317 assert!(!states.is_empty(), "Export should produce states");
1318 for state in &states {
1320 assert!(
1321 !state.source_label.is_empty(),
1322 "Source label should not be empty"
1323 );
1324 assert!(
1325 !state.target_label.is_empty(),
1326 "Target label should not be empty"
1327 );
1328 assert!(
1329 !state.source_label.starts_with("node_"),
1330 "Source should use real label, not placeholder"
1331 );
1332 }
1333 }
1334
1335 #[test]
1336 fn test_plasticity_import_state_roundtrip() {
1337 let mut g = make_test_graph();
1338 let engine = PlasticityEngine::new(&g, PlasticityConfig::default());
1339 let states = engine.export_state(&g).unwrap();
1340 let mut engine2 = PlasticityEngine::new(&g, PlasticityConfig::default());
1342 let applied = engine2.import_state(&mut g, &states).unwrap();
1343 assert!(applied > 0, "Import should apply at least some states");
1344 }
1345
1346 #[test]
1347 fn test_plasticity_decay_reduces_weights() {
1348 let mut g = make_test_graph();
1349 let mut engine = PlasticityEngine::new(&g, PlasticityConfig::default());
1350 let edge_idx = EdgeIdx::new(
1352 g.csr.offsets[8] as u32, );
1354 let before = g.csr.read_weight(edge_idx).get();
1355 let activated = vec![(NodeId::new(0), FiniteF32::ONE)];
1357 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1358 let result = engine
1359 .update(&mut g, &activated, &seeds, "decay test")
1360 .unwrap();
1361 let after = g.csr.read_weight(edge_idx).get();
1362 assert!(
1364 after <= before,
1365 "Weight should decrease after decay: before={before}, after={after}"
1366 );
1367 assert!(
1368 result.edges_decayed > 0,
1369 "At least one edge should be decayed"
1370 );
1371 }
1372
1373 #[test]
1374 fn test_plasticity_ltp_threshold_triggering() {
1375 let mut g = make_test_graph();
1376 let ltp_thresh = 3u16; let config = PlasticityConfig {
1378 ltp_threshold: ltp_thresh,
1379 ..PlasticityConfig::default()
1380 };
1381 let mut engine = PlasticityEngine::new(&g, config);
1382 let activated = vec![
1383 (NodeId::new(0), FiniteF32::ONE),
1384 (NodeId::new(1), FiniteF32::ONE),
1385 (NodeId::new(2), FiniteF32::ONE),
1386 (NodeId::new(3), FiniteF32::ONE),
1387 ];
1388 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1389 let mut total_ltp = 0u32;
1391 for i in 0..6 {
1392 let result = engine
1393 .update(&mut g, &activated, &seeds, &format!("ltp_test_{i}"))
1394 .unwrap();
1395 total_ltp += result.ltp_events;
1396 }
1397 assert!(
1398 total_ltp > 0,
1399 "LTP should trigger after repeated strengthening"
1400 );
1401 }
1402
1403 #[test]
1404 fn test_plasticity_query_memory_priming_signal() {
1405 let mut mem = QueryMemory::new(100, 20);
1406 for i in 0..5 {
1408 mem.record(QueryRecord {
1409 query_text: format!("query_{i}"),
1410 seeds: vec![NodeId::new(0), NodeId::new(1)],
1411 activated_nodes: vec![
1412 NodeId::new(0),
1413 NodeId::new(1),
1414 NodeId::new(2),
1415 NodeId::new(3),
1416 ],
1417 timestamp: i as f64,
1418 });
1419 }
1420 let priming = mem.get_priming_signal(&[NodeId::new(0)], FiniteF32::new(0.5));
1422 assert!(
1424 !priming.is_empty(),
1425 "Priming signal should be non-empty after recording queries"
1426 );
1427 }
1428
1429 #[test]
1430 fn test_plasticity_homeostatic_normalization() {
1431 let mut g = make_test_graph();
1432 let edge_idx = EdgeIdx::new(0);
1434 let _ = g
1435 .csr
1436 .atomic_write_weight(edge_idx, FiniteF32::new(10.0), 64);
1437 let engine = PlasticityEngine::new(&g, PlasticityConfig::default());
1438 let states = engine.export_state(&g).unwrap();
1440 for state in &states {
1441 assert!(
1442 state.current_weight.is_finite(),
1443 "Exported weight should be finite"
1444 );
1445 }
1446 }
1447
1448 #[test]
1451 fn test_resonance_standing_wave_produces_waves_from_seeds() {
1452 let g = make_test_graph();
1453 let propagator = StandingWavePropagator::new(10, FiniteF32::new(0.001), 50_000);
1454 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1455 let result = propagator
1456 .propagate(
1457 &g,
1458 &seeds,
1459 PosF32::new(1.0).unwrap(),
1460 PosF32::new(4.0).unwrap(),
1461 )
1462 .unwrap();
1463 assert!(
1464 result.pulses_processed > 0,
1465 "Should process at least one pulse"
1466 );
1467 assert!(!result.antinodes.is_empty(), "Should produce antinodes");
1468 assert!(
1469 result.total_energy.get() > 0.0,
1470 "Total energy should be positive"
1471 );
1472 }
1473
1474 #[test]
1475 fn test_resonance_harmonic_analyzer_detects_fundamental() {
1476 let g = make_test_graph();
1477 let propagator = StandingWavePropagator::new(5, FiniteF32::new(0.01), 10_000);
1478 let analyzer = HarmonicAnalyzer::new(propagator, 3);
1479 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1480 let result = analyzer
1481 .analyze(
1482 &g,
1483 &seeds,
1484 PosF32::new(1.0).unwrap(),
1485 PosF32::new(4.0).unwrap(),
1486 )
1487 .unwrap();
1488 assert!(
1489 !result.harmonics.is_empty(),
1490 "Should detect at least one harmonic"
1491 );
1492 assert_eq!(
1494 result.harmonics[0].harmonic, 1,
1495 "First harmonic should be the fundamental"
1496 );
1497 assert!(
1498 result.harmonics[0].total_energy.get() > 0.0,
1499 "Fundamental should have energy"
1500 );
1501 }
1502
1503 #[test]
1504 fn test_resonance_sympathetic_detector_finds_pairs() {
1505 let g = make_test_graph();
1506 let propagator = StandingWavePropagator::new(10, FiniteF32::new(0.001), 50_000);
1507 let detector = SympatheticResonanceDetector::new(propagator, FiniteF32::new(0.01));
1508 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1509 let result = detector
1510 .detect(
1511 &g,
1512 &seeds,
1513 PosF32::new(1.0).unwrap(),
1514 PosF32::new(4.0).unwrap(),
1515 )
1516 .unwrap();
1517 assert!(
1518 result.checked_disconnected,
1519 "Should check disconnected components"
1520 );
1521 let _ = result.sympathetic_nodes;
1523 }
1524
1525 #[test]
1526 fn test_resonance_engine_analyze_returns_report() {
1527 let g = make_test_graph();
1528 let engine = ResonanceEngine::with_defaults();
1529 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1530 let report = engine.analyze(&g, &seeds).unwrap();
1531 assert!(report.standing_wave.pulses_processed > 0);
1532 assert!(!report.harmonics.harmonics.is_empty());
1533 }
1534
1535 #[test]
1536 fn test_resonance_wave_accumulator_destructive_interference() {
1537 let mut acc = WaveAccumulator::default();
1538 let pulse1 = WavePulse {
1540 node: NodeId::new(0),
1541 amplitude: FiniteF32::ONE,
1542 phase: FiniteF32::ZERO,
1543 frequency: PosF32::new(1.0).unwrap(),
1544 wavelength: PosF32::new(4.0).unwrap(),
1545 hops: 0,
1546 prev_node: NodeId::new(0),
1547 };
1548 let pulse2 = WavePulse {
1549 node: NodeId::new(0),
1550 amplitude: FiniteF32::ONE,
1551 phase: FiniteF32::new(std::f32::consts::PI),
1552 frequency: PosF32::new(1.0).unwrap(),
1553 wavelength: PosF32::new(4.0).unwrap(),
1554 hops: 0,
1555 prev_node: NodeId::new(0),
1556 };
1557 acc.accumulate(&pulse1);
1558 acc.accumulate(&pulse2);
1559 let amp = acc.amplitude().get();
1560 assert!(
1561 amp < 0.1,
1562 "Opposite-phase pulses should destructively interfere, got amp={amp}"
1563 );
1564 }
1565
1566 #[test]
1569 fn test_graph_finalize_empty_graph_succeeds() {
1570 let mut g = Graph::new();
1571 let result = g.finalize();
1573 assert!(result.is_ok(), "Finalizing empty graph should succeed");
1574 assert!(g.finalized);
1575 }
1576
1577 #[test]
1578 fn test_graph_finalize_computes_pagerank_top_node() {
1579 let g = make_test_graph();
1580 let max_idx = (0..g.num_nodes() as usize)
1582 .max_by(|&a, &b| g.nodes.pagerank[a].cmp(&g.nodes.pagerank[b]))
1583 .unwrap();
1584 let max_pr = g.nodes.pagerank[max_idx].get();
1585 assert!(
1586 (max_pr - 1.0).abs() < 0.01,
1587 "Highest PageRank should be normalized to ~1.0, got {max_pr}"
1588 );
1589 }
1590
1591 #[test]
1592 fn test_graph_csr_offsets_monotonically_increasing() {
1593 let g = make_test_graph();
1594 for i in 1..g.csr.offsets.len() {
1595 assert!(
1596 g.csr.offsets[i] >= g.csr.offsets[i - 1],
1597 "CSR offsets must be monotonically increasing: offsets[{}]={} < offsets[{}]={}",
1598 i,
1599 g.csr.offsets[i],
1600 i - 1,
1601 g.csr.offsets[i - 1]
1602 );
1603 }
1604 }
1605
1606 #[test]
1607 fn test_graph_bidirectional_edge_creates_two_csr_entries() {
1608 let mut g = Graph::new();
1610 g.add_node("a", "A", NodeType::Module, &[], 0.0, 0.0)
1611 .unwrap();
1612 g.add_node("b", "B", NodeType::Module, &[], 0.0, 0.0)
1613 .unwrap();
1614 g.add_edge(
1615 NodeId::new(0),
1616 NodeId::new(1),
1617 "bidir",
1618 FiniteF32::ONE,
1619 EdgeDirection::Bidirectional,
1620 false,
1621 FiniteF32::ZERO,
1622 )
1623 .unwrap();
1624 g.finalize().unwrap();
1625 assert_eq!(
1627 g.num_edges(),
1628 2,
1629 "Bidirectional edge should produce 2 CSR entries"
1630 );
1631 let out_0 = g.csr.out_range(NodeId::new(0));
1633 assert_eq!(
1634 out_0.end - out_0.start,
1635 1,
1636 "Node 0 should have 1 outgoing edge"
1637 );
1638 let out_1 = g.csr.out_range(NodeId::new(1));
1640 assert_eq!(
1641 out_1.end - out_1.start,
1642 1,
1643 "Node 1 should have 1 outgoing edge"
1644 );
1645 }
1646
1647 #[test]
1648 fn test_graph_edge_plasticity_matches_csr_count() {
1649 let g = make_test_graph();
1650 let num_csr = g.num_edges();
1651 assert_eq!(
1652 g.edge_plasticity.original_weight.len(),
1653 num_csr,
1654 "Plasticity original_weight length should match CSR edge count"
1655 );
1656 assert_eq!(
1657 g.edge_plasticity.current_weight.len(),
1658 num_csr,
1659 "Plasticity current_weight length should match CSR edge count"
1660 );
1661 assert_eq!(
1662 g.edge_plasticity.strengthen_count.len(),
1663 num_csr,
1664 "Plasticity strengthen_count length should match CSR edge count"
1665 );
1666 assert_eq!(
1667 g.edge_plasticity.ltp_applied.len(),
1668 num_csr,
1669 "Plasticity ltp_applied length should match CSR edge count"
1670 );
1671 }
1672
1673 #[test]
1674 fn test_graph_resolve_nonexistent_id_returns_none() {
1675 let g = make_test_graph();
1676 assert!(g.resolve_id("nonexistent_node_xyz").is_none());
1677 }
1678
1679 #[test]
1680 fn test_graph_avg_degree_positive_for_nonempty_graph() {
1681 let g = make_test_graph();
1682 assert!(
1683 g.avg_degree() > 0.0,
1684 "Average degree should be positive for non-empty graph"
1685 );
1686 }
1687
1688 #[test]
1689 fn test_graph_avg_degree_zero_for_empty_graph() {
1690 let g = Graph::new();
1691 assert_eq!(
1692 g.avg_degree(),
1693 0.0,
1694 "Average degree of empty graph should be 0"
1695 );
1696 }
1697
1698 #[test]
1701 fn test_counterfactual_keystone_analysis() {
1702 let g = make_test_graph();
1703 let engine = HybridEngine::new();
1704 let config = PropagationConfig::default();
1705 let cf = CounterfactualEngine::new(4, 5);
1706 let result = cf.find_keystones(&g, &engine, &config).unwrap();
1707 assert!(
1708 !result.keystones.is_empty(),
1709 "Should identify at least one keystone"
1710 );
1711 for i in 1..result.keystones.len() {
1713 assert!(
1714 result.keystones[i - 1].avg_impact.get() >= result.keystones[i].avg_impact.get(),
1715 "Keystones should be sorted by impact descending"
1716 );
1717 }
1718 }
1719
1720 #[test]
1721 fn test_counterfactual_redundancy_check() {
1722 let g = make_test_graph();
1723 let engine = HybridEngine::new();
1724 let config = PropagationConfig::default();
1725 let cf = CounterfactualEngine::with_defaults();
1726 let result = cf
1727 .check_redundancy(&g, &engine, &config, NodeId::new(9))
1728 .unwrap();
1729 assert!(
1731 result.redundancy_score.get() >= 0.0 && result.redundancy_score.get() <= 1.0,
1732 "Redundancy score should be in [0,1], got {}",
1733 result.redundancy_score.get()
1734 );
1735 }
1736
1737 #[test]
1738 fn test_counterfactual_reachability_tracked() {
1739 let g = make_test_graph();
1740 let engine = HybridEngine::new();
1741 let config = PropagationConfig::default();
1742 let cf = CounterfactualEngine::with_defaults();
1743 let result = cf
1744 .simulate_removal(&g, &engine, &config, &[NodeId::new(0)])
1745 .unwrap();
1746 assert!(
1747 result.reachability_before > 0,
1748 "Reachability before should be > 0"
1749 );
1750 assert!(
1752 result.reachability_after <= result.reachability_before,
1753 "Reachability should not increase after removal"
1754 );
1755 }
1756
1757 #[test]
1760 fn test_query_config_default_has_four_dimensions() {
1761 let config = QueryConfig::default();
1762 assert_eq!(config.dimensions.len(), 4);
1763 assert!(config.xlr_enabled);
1764 assert_eq!(config.top_k, 20);
1765 }
1766
1767 #[test]
1768 fn test_query_with_structural_holes_enabled() {
1769 let mut g = make_test_graph();
1770 let mut orch = QueryOrchestrator::build(&g).unwrap();
1771 let config = QueryConfig {
1772 query: "Alpha Beta".to_string(),
1773 agent_id: "test".to_string(),
1774 top_k: 10,
1775 xlr_enabled: false,
1776 include_ghost_edges: true,
1777 include_structural_holes: true,
1778 ..QueryConfig::default()
1779 };
1780 let result = orch.query(&mut g, &config).unwrap();
1781 assert!(result.elapsed_ms >= 0.0);
1783 }
1784
1785 #[test]
1788 fn test_merge_dimensions_three_dim_resonance_bonus() {
1789 let make_dim = |dim: Dimension, scores: Vec<(NodeId, FiniteF32)>| DimensionResult {
1790 scores,
1791 dimension: dim,
1792 elapsed_ns: 0,
1793 };
1794 let node = NodeId::new(0);
1795 let score = FiniteF32::new(0.5);
1796 let results = [
1797 make_dim(Dimension::Structural, vec![(node, score)]),
1798 make_dim(Dimension::Semantic, vec![(node, score)]),
1799 make_dim(Dimension::Temporal, vec![(node, score)]),
1800 make_dim(Dimension::Causal, vec![]), ];
1802 let merged = merge_dimensions(&results, 10).unwrap();
1803 assert!(!merged.activated.is_empty());
1804 let activated = &merged.activated[0];
1805 assert_eq!(activated.active_dimension_count, 3);
1806 let w_sum = 0.35 + 0.25 + 0.15; let base = 0.5 * (0.35 / w_sum) + 0.5 * (0.25 / w_sum) + 0.5 * (0.15 / w_sum);
1809 let expected = base * RESONANCE_BONUS_3DIM;
1810 assert!(
1811 (activated.activation.get() - expected).abs() < 0.02,
1812 "Expected ~{expected}, got {}",
1813 activated.activation.get()
1814 );
1815 }
1816
1817 #[test]
1818 fn test_merge_dimensions_single_dim_no_bonus() {
1819 let make_dim = |dim: Dimension, scores: Vec<(NodeId, FiniteF32)>| DimensionResult {
1820 scores,
1821 dimension: dim,
1822 elapsed_ns: 0,
1823 };
1824 let node = NodeId::new(0);
1825 let score = FiniteF32::new(0.5);
1826 let results = [
1827 make_dim(Dimension::Structural, vec![(node, score)]),
1828 make_dim(Dimension::Semantic, vec![]),
1829 make_dim(Dimension::Temporal, vec![]),
1830 make_dim(Dimension::Causal, vec![]),
1831 ];
1832 let merged = merge_dimensions(&results, 10).unwrap();
1833 assert!(!merged.activated.is_empty());
1834 let activated = &merged.activated[0];
1835 assert_eq!(activated.active_dimension_count, 1);
1836 let expected = 0.5 * 1.0; assert!(
1840 (activated.activation.get() - expected).abs() < 0.02,
1841 "Expected ~{expected} (no bonus), got {}",
1842 activated.activation.get()
1843 );
1844 }
1845
1846 #[test]
1849 fn test_bloom_filter_clear_resets_all() {
1850 let mut bf = BloomFilter::with_capacity(100, 0.01);
1851 bf.insert(NodeId::new(10));
1852 bf.insert(NodeId::new(20));
1853 assert!(bf.probably_contains(NodeId::new(10)));
1854 bf.clear();
1855 }
1859
1860 #[test]
1863 fn test_string_interner_idempotent() {
1864 let mut interner = StringInterner::new();
1865 let h1 = interner.get_or_intern("hello");
1866 let h2 = interner.get_or_intern("hello");
1867 assert_eq!(h1, h2, "Re-interning same string should return same handle");
1868 assert_eq!(interner.len(), 1, "Should only have one entry");
1869 }
1870
1871 #[test]
1872 fn test_string_interner_resolve() {
1873 let mut interner = StringInterner::new();
1874 let h = interner.get_or_intern("world");
1875 assert_eq!(interner.resolve(h), "world");
1876 }
1877
1878 #[test]
1881 fn test_temporal_activation_dimension() {
1882 let g = make_test_graph();
1883 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1884 let weights = TemporalWeights::default();
1885 let result = activate_temporal(&g, &seeds, &weights).unwrap();
1886 assert_eq!(result.dimension, Dimension::Temporal);
1887 assert!(
1889 !result.scores.is_empty(),
1890 "Temporal activation should produce scores for seeds"
1891 );
1892 }
1893
1894 #[test]
1895 fn test_causal_activation_dimension() {
1896 let g = make_test_graph();
1897 let seeds = vec![(NodeId::new(0), FiniteF32::ONE)];
1898 let config = PropagationConfig::default();
1899 let result = activate_causal(&g, &seeds, &config).unwrap();
1900 assert_eq!(result.dimension, Dimension::Causal);
1901 }
1902
1903 #[test]
1904 fn test_causal_activation_empty_graph() {
1905 let g = Graph::new();
1906 let seeds = vec![];
1907 let config = PropagationConfig::default();
1908 let result = activate_causal(&g, &seeds, &config).unwrap();
1909 assert!(result.scores.is_empty());
1910 }
1911
1912 #[test]
1915 fn test_removal_mask_remove_edge_directly() {
1916 let g = make_test_graph();
1917 let mut mask = RemovalMask::new(g.num_nodes(), g.num_edges());
1918 mask.remove_edge(EdgeIdx::new(0));
1919 assert!(mask.is_edge_removed(EdgeIdx::new(0)));
1920 assert!(!mask.is_edge_removed(EdgeIdx::new(1)));
1921 }
1922
1923 #[test]
1924 fn test_removal_mask_out_of_range_node_noop() {
1925 let g = make_test_graph();
1926 let mut mask = RemovalMask::new(g.num_nodes(), g.num_edges());
1927 mask.remove_node(&g, NodeId::new(999));
1929 for i in 0..g.num_nodes() {
1931 assert!(!mask.is_node_removed(NodeId::new(i)));
1932 }
1933 }
1934
1935 #[test]
1938 fn test_cooccurrence_cosine_empty_vectors() {
1939 use crate::semantic::CoOccurrenceIndex;
1940 let a: Vec<(NodeId, FiniteF32)> = vec![];
1941 let b: Vec<(NodeId, FiniteF32)> = vec![];
1942 let sim = CoOccurrenceIndex::cosine_similarity(&a, &b);
1943 assert_eq!(sim.get(), 0.0);
1944 }
1945
1946 #[test]
1947 fn test_cooccurrence_cosine_identical_vectors() {
1948 use crate::semantic::CoOccurrenceIndex;
1949 let a = vec![
1950 (NodeId::new(0), FiniteF32::new(0.5)),
1951 (NodeId::new(1), FiniteF32::new(0.3)),
1952 ];
1953 let sim = CoOccurrenceIndex::cosine_similarity(&a, &a);
1954 assert!(
1955 (sim.get() - 1.0).abs() < 0.01,
1956 "Cosine similarity of identical vectors should be ~1.0, got {}",
1957 sim.get()
1958 );
1959 }
1960
1961 #[test]
1968 fn test_co_change_record_increments_count() {
1969 let g = build_test_graph();
1970 let mut matrix = CoChangeMatrix::bootstrap(&g, 500_000).unwrap();
1971 let a = NodeId::new(0);
1972 let b = NodeId::new(1);
1973
1974 matrix.record_co_change(a, b, 1000.0).unwrap();
1976
1977 let predictions = matrix.predict(a, 10);
1979 assert!(
1980 predictions.iter().any(|e| e.target == b),
1981 "After recording co-change A->B, predict(A) should include B"
1982 );
1983 }
1984
1985 #[test]
1986 fn test_co_change_multiple_records_accumulate() {
1987 let g = build_test_graph();
1988 let mut matrix = CoChangeMatrix::bootstrap(&g, 500_000).unwrap();
1989 let a = NodeId::new(0);
1990 let b = NodeId::new(2);
1991
1992 let initial_strength = matrix
1994 .predict(a, 100)
1995 .iter()
1996 .find(|e| e.target == b)
1997 .map(|e| e.strength.get())
1998 .unwrap_or(0.0);
1999
2000 matrix.record_co_change(a, b, 1000.0).unwrap();
2002 matrix.record_co_change(a, b, 2000.0).unwrap();
2003 matrix.record_co_change(a, b, 3000.0).unwrap();
2004
2005 let final_strength = matrix
2006 .predict(a, 100)
2007 .iter()
2008 .find(|e| e.target == b)
2009 .map(|e| e.strength.get())
2010 .unwrap_or(0.0);
2011
2012 assert!(
2013 final_strength > initial_strength,
2014 "Multiple co-change records should accumulate: initial={}, final={}",
2015 initial_strength,
2016 final_strength
2017 );
2018 }
2019
2020 #[test]
2021 fn test_co_change_predict_returns_most_frequent() {
2022 let g = build_test_graph();
2023 let mut matrix = CoChangeMatrix::bootstrap(&g, 500_000).unwrap();
2024 let a = NodeId::new(0);
2025 let b = NodeId::new(1);
2026 let c = NodeId::new(2);
2027
2028 for _ in 0..5 {
2030 matrix.record_co_change(a, b, 1000.0).unwrap();
2031 }
2032 matrix.record_co_change(a, c, 1000.0).unwrap();
2033
2034 let predictions = matrix.predict(a, 10);
2035 let b_strength = predictions
2036 .iter()
2037 .find(|e| e.target == b)
2038 .map(|e| e.strength.get())
2039 .unwrap_or(0.0);
2040 let c_strength = predictions
2041 .iter()
2042 .find(|e| e.target == c)
2043 .map(|e| e.strength.get())
2044 .unwrap_or(0.0);
2045
2046 assert!(
2047 b_strength > c_strength,
2048 "More frequently co-changed node should have higher strength: B={}, C={}",
2049 b_strength,
2050 c_strength
2051 );
2052 }
2053
2054 #[test]
2055 fn test_co_change_empty_matrix_returns_empty() {
2056 let mut g = Graph::new();
2058 g.add_node("iso_a", "IsoA", NodeType::File, &[], 0.0, 0.1)
2059 .unwrap();
2060 g.add_node("iso_b", "IsoB", NodeType::File, &[], 0.0, 0.1)
2061 .unwrap();
2062 g.finalize().unwrap();
2063
2064 let matrix = CoChangeMatrix::bootstrap(&g, 500_000).unwrap();
2065
2066 let predictions = matrix.predict(NodeId::new(0), 10);
2068 assert!(
2069 predictions.is_empty(),
2070 "Empty matrix should return empty predictions"
2071 );
2072 }
2073
2074 #[test]
2075 fn test_co_change_predict_returns_both_partners() {
2076 let g = build_test_graph();
2077 let mut matrix = CoChangeMatrix::bootstrap(&g, 500_000).unwrap();
2078 let a = NodeId::new(0);
2079 let b = NodeId::new(3);
2080 let c = NodeId::new(4);
2081
2082 matrix.record_co_change(a, b, 1000.0).unwrap();
2084 matrix.record_co_change(a, c, 1000.0).unwrap();
2085
2086 let predictions = matrix.predict(a, 10);
2087 let has_b = predictions.iter().any(|e| e.target == b);
2088 let has_c = predictions.iter().any(|e| e.target == c);
2089
2090 assert!(has_b, "predict(A) should include B after recording A<->B");
2091 assert!(has_c, "predict(A) should include C after recording A<->C");
2092 }
2093
2094 #[test]
2097 fn test_velocity_zscore_zero_for_average() {
2098 let mut g = Graph::new();
2100 g.add_node("v0", "V0", NodeType::File, &[], 1000.0, 0.5)
2101 .unwrap();
2102 g.add_node("v1", "V1", NodeType::File, &[], 1000.0, 0.5)
2103 .unwrap();
2104 g.add_node("v2", "V2", NodeType::File, &[], 1000.0, 0.5)
2105 .unwrap();
2106 g.finalize().unwrap();
2107
2108 let score = VelocityScorer::score_one(&g, NodeId::new(0), 2000.0).unwrap();
2109 assert!(
2110 score.velocity.get().abs() < 0.01,
2111 "Node with average frequency should have z-score ~0, got {}",
2112 score.velocity.get()
2113 );
2114 assert_eq!(score.trend, VelocityTrend::Stable);
2115 }
2116
2117 #[test]
2118 fn test_velocity_high_frequency_positive_zscore() {
2119 let mut g = Graph::new();
2121 g.add_node("high", "High", NodeType::File, &[], 1000.0, 1.0)
2122 .unwrap();
2123 g.add_node("low1", "Low1", NodeType::File, &[], 1000.0, 0.1)
2124 .unwrap();
2125 g.add_node("low2", "Low2", NodeType::File, &[], 1000.0, 0.1)
2126 .unwrap();
2127 g.add_node("low3", "Low3", NodeType::File, &[], 1000.0, 0.1)
2128 .unwrap();
2129 g.finalize().unwrap();
2130
2131 let score = VelocityScorer::score_one(&g, NodeId::new(0), 2000.0).unwrap();
2132 assert!(
2133 score.velocity.get() > 0.0,
2134 "High-frequency node should have positive z-score, got {}",
2135 score.velocity.get()
2136 );
2137 }
2138
2139 #[test]
2140 fn test_velocity_low_frequency_negative_zscore() {
2141 let mut g = Graph::new();
2143 g.add_node("low", "Low", NodeType::File, &[], 1000.0, 0.05)
2144 .unwrap();
2145 g.add_node("high1", "High1", NodeType::File, &[], 1000.0, 0.9)
2146 .unwrap();
2147 g.add_node("high2", "High2", NodeType::File, &[], 1000.0, 0.9)
2148 .unwrap();
2149 g.add_node("high3", "High3", NodeType::File, &[], 1000.0, 0.9)
2150 .unwrap();
2151 g.finalize().unwrap();
2152
2153 let score = VelocityScorer::score_one(&g, NodeId::new(0), 2000.0).unwrap();
2154 assert!(
2155 score.velocity.get() < 0.0,
2156 "Low-frequency node should have negative z-score, got {}",
2157 score.velocity.get()
2158 );
2159 }
2160
2161 #[test]
2162 fn test_velocity_score_all_empty_graph() {
2163 let g = Graph::new();
2164 let scores = VelocityScorer::score_all(&g, 0.0).unwrap();
2165 assert!(
2166 scores.is_empty(),
2167 "score_all on empty graph should return empty"
2168 );
2169 }
2170
2171 #[test]
2174 fn test_decay_recent_file_near_one() {
2175 let scorer = TemporalDecayScorer::new(PosF32::new(168.0).unwrap());
2176 let result = scorer.score_one(0.0, FiniteF32::ZERO, None);
2178 assert!(
2179 (result.raw_decay.get() - 1.0).abs() < 0.01,
2180 "Recent file (age=0) should have decay ~1.0, got {}",
2181 result.raw_decay.get()
2182 );
2183 assert!(
2184 (result.final_score.get() - 1.0).abs() < 0.01,
2185 "Recent file final_score should be ~1.0, got {}",
2186 result.final_score.get()
2187 );
2188 }
2189
2190 #[test]
2191 fn test_decay_old_file_below_half() {
2192 let scorer = TemporalDecayScorer::new(PosF32::new(168.0).unwrap());
2193 let result = scorer.score_one(504.0, FiniteF32::ZERO, None);
2195 assert!(
2196 result.raw_decay.get() < 0.5,
2197 "Old file (3 half-lives) should have decay < 0.5, got {}",
2198 result.raw_decay.get()
2199 );
2200 }
2201
2202 #[test]
2203 fn test_decay_per_nodetype_function_faster_than_module() {
2204 let scorer = TemporalDecayScorer::new(PosF32::new(168.0).unwrap());
2205 let age_hours = 336.0; let func_decay =
2209 scorer.score_one_typed(age_hours, FiniteF32::ZERO, None, Some(NodeType::Function));
2210 let mod_decay =
2211 scorer.score_one_typed(age_hours, FiniteF32::ZERO, None, Some(NodeType::Module));
2212
2213 assert!(
2214 func_decay.raw_decay.get() < mod_decay.raw_decay.get(),
2215 "Function should decay faster than Module at same age: func={}, mod={}",
2216 func_decay.raw_decay.get(),
2217 mod_decay.raw_decay.get()
2218 );
2219 }
2220
2221 #[test]
2222 fn test_decay_score_all_in_unit_range() {
2223 let g = build_test_graph();
2224 let scorer = TemporalDecayScorer::new(PosF32::new(168.0).unwrap());
2225 let now_unix = 2000.0;
2227 let scores = scorer.score_all(&g, now_unix).unwrap();
2228
2229 assert_eq!(scores.len(), g.num_nodes() as usize);
2230 for ds in &scores {
2231 assert!(
2232 ds.final_score.get() >= 0.0 && ds.final_score.get() <= 1.0,
2233 "Decay score should be in [0,1], got {} for node {:?}",
2234 ds.final_score.get(),
2235 ds.node
2236 );
2237 assert!(
2238 ds.raw_decay.get() >= 0.0 && ds.raw_decay.get() <= 1.0,
2239 "Raw decay should be in [0,1], got {} for node {:?}",
2240 ds.raw_decay.get(),
2241 ds.node
2242 );
2243 }
2244 }
2245
2246 #[test]
2249 fn test_impact_isolated_node_limited_to_self() {
2250 let mut g = Graph::new();
2252 g.add_node("iso", "Isolated", NodeType::File, &[], 1000.0, 0.5)
2253 .unwrap();
2254 g.add_node("other", "Other", NodeType::File, &[], 1000.0, 0.5)
2255 .unwrap();
2256 g.finalize().unwrap();
2258
2259 let calc = ImpactRadiusCalculator::new(5, FiniteF32::new(0.01));
2260 let result = calc
2261 .compute(&g, NodeId::new(0), ImpactDirection::Both)
2262 .unwrap();
2263
2264 assert!(
2265 result.blast_radius.is_empty(),
2266 "Isolated node should have empty blast radius, got {} entries",
2267 result.blast_radius.len()
2268 );
2269 assert_eq!(result.source, NodeId::new(0));
2270 }
2271
2272 #[test]
2273 fn test_impact_propagates_through_edges() {
2274 let g = build_test_graph();
2275 let calc = ImpactRadiusCalculator::new(5, FiniteF32::new(0.01));
2276
2277 let result = calc
2279 .compute(&g, NodeId::new(0), ImpactDirection::Forward)
2280 .unwrap();
2281
2282 assert!(
2283 !result.blast_radius.is_empty(),
2284 "Connected node should have non-empty blast radius"
2285 );
2286
2287 let impacted_nodes: Vec<NodeId> = result.blast_radius.iter().map(|e| e.node).collect();
2289 assert!(
2290 impacted_nodes.contains(&NodeId::new(3)),
2291 "Direct forward neighbor (proc_inj, node 3) should be in blast radius. Got: {:?}",
2292 impacted_nodes
2293 );
2294
2295 for entry in &result.blast_radius {
2297 assert!(
2298 entry.signal_strength.get() > 0.0 && entry.signal_strength.get() <= 1.0,
2299 "Impact signal should be in (0,1], got {}",
2300 entry.signal_strength.get()
2301 );
2302 }
2303 }
2304
2305 #[test]
2306 fn test_impact_multi_hop_propagation() {
2307 let g = build_test_graph();
2308 let calc = ImpactRadiusCalculator::new(5, FiniteF32::new(0.001));
2309
2310 let result = calc
2312 .compute(&g, NodeId::new(0), ImpactDirection::Forward)
2313 .unwrap();
2314
2315 let node5_entry = result
2316 .blast_radius
2317 .iter()
2318 .find(|e| e.node == NodeId::new(5));
2319 assert!(
2320 node5_entry.is_some(),
2321 "Impact should propagate to 2-hop neighbor (node 5). Blast radius: {:?}",
2322 result
2323 .blast_radius
2324 .iter()
2325 .map(|e| e.node)
2326 .collect::<Vec<_>>()
2327 );
2328
2329 if let Some(entry) = node5_entry {
2330 assert!(
2331 entry.hop_distance >= 2,
2332 "Node 5 should be at hop distance >= 2, got {}",
2333 entry.hop_distance
2334 );
2335 }
2336 }
2337
2338 #[test]
2341 fn test_velocity_scorer_cache_works() {
2342 let mut g = Graph::new();
2343 g.add_node("c0", "C0", NodeType::File, &[], 1000.0, 0.9)
2344 .unwrap();
2345 g.add_node("c1", "C1", NodeType::File, &[], 1000.0, 0.1)
2346 .unwrap();
2347 g.add_node("c2", "C2", NodeType::File, &[], 1000.0, 0.5)
2348 .unwrap();
2349 g.finalize().unwrap();
2350
2351 let mut scorer = VelocityScorer::new();
2352
2353 let scores1 = scorer.score_all_cached(&g, 2000.0).unwrap();
2355 let scores2 = scorer.score_all_cached(&g, 2000.0).unwrap();
2357
2358 assert_eq!(scores1.len(), scores2.len(), "Cached results should match");
2359 for (s1, s2) in scores1.iter().zip(scores2.iter()) {
2360 assert_eq!(
2361 s1.velocity.get(),
2362 s2.velocity.get(),
2363 "Cached velocity scores should be identical"
2364 );
2365 }
2366
2367 scorer.invalidate_cache();
2369 let scores3 = scorer.score_all_cached(&g, 2000.0).unwrap();
2370 assert_eq!(
2371 scores1.len(),
2372 scores3.len(),
2373 "After invalidation, results should still match"
2374 );
2375 }
2376
2377 #[test]
2380 fn test_co_change_populate_from_commit_groups() {
2381 let mut g = Graph::new();
2383 g.add_node(
2384 "file::alpha.rs",
2385 "alpha.rs",
2386 NodeType::File,
2387 &[],
2388 1000.0,
2389 0.5,
2390 )
2391 .unwrap();
2392 g.add_node("file::beta.rs", "beta.rs", NodeType::File, &[], 1000.0, 0.3)
2393 .unwrap();
2394 g.add_node(
2395 "file::gamma.rs",
2396 "gamma.rs",
2397 NodeType::File,
2398 &[],
2399 1000.0,
2400 0.2,
2401 )
2402 .unwrap();
2403 g.finalize().unwrap();
2404
2405 let mut matrix = CoChangeMatrix::bootstrap(&g, 500_000).unwrap();
2406
2407 let groups = vec![
2409 vec!["alpha.rs".to_string(), "beta.rs".to_string()],
2410 vec!["alpha.rs".to_string(), "gamma.rs".to_string()],
2411 ];
2412
2413 matrix.populate_from_commit_groups(&g, &groups).unwrap();
2414
2415 let alpha_id = g.resolve_id("file::alpha.rs").unwrap();
2417 let beta_id = g.resolve_id("file::beta.rs").unwrap();
2418 let gamma_id = g.resolve_id("file::gamma.rs").unwrap();
2419
2420 let predictions = matrix.predict(alpha_id, 10);
2421 let has_beta = predictions.iter().any(|e| e.target == beta_id);
2422 let has_gamma = predictions.iter().any(|e| e.target == gamma_id);
2423
2424 assert!(
2425 has_beta,
2426 "After commit group [alpha, beta], alpha should predict beta"
2427 );
2428 assert!(
2429 has_gamma,
2430 "After commit group [alpha, gamma], alpha should predict gamma"
2431 );
2432 }
2433}