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