1use std::collections::{HashMap, HashSet};
16
17use uuid::Uuid;
18
19use khive_storage::types::{Direction, Edge, LinkId, NeighborQuery, TraversalOptions};
20
21use crate::error::{RuntimeError, RuntimeResult};
22use crate::runtime::{KhiveRuntime, NamespaceToken};
23
24#[derive(Debug, Clone)]
26pub struct PathNode {
27 pub entity_id: Uuid,
29 pub depth: usize,
31 pub via_edge: Option<Edge>,
33}
34
35impl KhiveRuntime {
36 pub async fn bfs_traverse(
44 &self,
45 token: &NamespaceToken,
46 start: Uuid,
47 options: TraversalOptions,
48 ) -> RuntimeResult<Vec<PathNode>> {
49 if !self.substrate_exists_in_ns(token, start).await? {
50 return Ok(Vec::new());
51 }
52
53 let graph = self.graph(token)?;
54 let limit = options.limit.map(|n| n as usize).unwrap_or(usize::MAX);
55
56 let mut visited: HashSet<Uuid> = HashSet::new();
57 let mut results: Vec<PathNode> = Vec::new();
58 let mut frontier: Vec<Uuid> = Vec::new();
60
61 visited.insert(start);
62 results.push(PathNode {
63 entity_id: start,
64 depth: 0,
65 via_edge: None,
66 });
67 frontier.push(start);
68
69 let mut depth = 0usize;
70
71 'bfs: while !frontier.is_empty() && depth < options.max_depth {
72 let query = NeighborQuery {
73 direction: options.direction.clone(),
74 relations: options.relations.clone(),
75 limit: None,
76 min_weight: None,
77 };
78
79 let all_hits = graph.batch_neighbors(&frontier, query).await?;
81
82 let mut level_new: Vec<(Uuid, Uuid)> = Vec::new();
84 for (_src, hit) in &all_hits {
85 if visited.contains(&hit.node_id) {
86 continue;
87 }
88 if visited.insert(hit.node_id) {
91 level_new.push((hit.node_id, hit.edge_id));
92 }
93 }
94
95 if level_new.is_empty() {
96 break 'bfs;
97 }
98
99 let edge_ids: Vec<LinkId> = level_new
101 .iter()
102 .map(|(_, eid)| LinkId::from(*eid))
103 .collect();
104 let edges = graph.get_edges(&edge_ids).await?;
105 let edge_map: HashMap<Uuid, Edge> =
106 edges.into_iter().map(|e| (Uuid::from(e.id), e)).collect();
107
108 let next_depth = depth + 1;
109 frontier.clear();
110 for (node_id, edge_id) in level_new {
111 let via_edge = edge_map.get(&edge_id).cloned().or(None);
112 if via_edge.is_none() {
117 return Err(RuntimeError::NotFound(format!("edge {} missing", edge_id)));
118 }
119 results.push(PathNode {
120 entity_id: node_id,
121 depth: next_depth,
122 via_edge,
123 });
124 if results.len() >= limit {
125 break 'bfs;
126 }
127 frontier.push(node_id);
128 }
129
130 depth = next_depth;
131 }
132
133 Ok(results)
134 }
135
136 pub async fn shortest_path(
145 &self,
146 token: &NamespaceToken,
147 from: Uuid,
148 to: Uuid,
149 max_depth: usize,
150 ) -> RuntimeResult<Option<Vec<PathNode>>> {
151 if !self.substrate_exists_in_ns(token, from).await?
152 || !self.substrate_exists_in_ns(token, to).await?
153 {
154 return Ok(None);
155 }
156
157 if from == to {
158 return Ok(Some(vec![PathNode {
159 entity_id: from,
160 depth: 0,
161 via_edge: None,
162 }]));
163 }
164
165 let graph = self.graph(token)?;
166
167 let mut fwd: HashMap<Uuid, (usize, Option<Uuid>, Option<Uuid>)> = HashMap::new();
169 let mut fwd_frontier: Vec<Uuid> = vec![from];
170 fwd.insert(from, (0, None, None));
171
172 let mut bwd: HashMap<Uuid, (usize, Option<Uuid>, Option<Uuid>)> = HashMap::new();
174 let mut bwd_frontier: Vec<Uuid> = vec![to];
175 bwd.insert(to, (0, None, None));
176
177 let mut meeting: Option<(Uuid, usize)> = None;
178 let mut current_depth = 0usize;
179
180 while (!fwd_frontier.is_empty() || !bwd_frontier.is_empty()) && current_depth <= max_depth {
181 if !fwd_frontier.is_empty() {
183 let hits = graph
184 .batch_neighbors(
185 &fwd_frontier,
186 NeighborQuery {
187 direction: Direction::Out,
188 relations: None,
189 limit: None,
190 min_weight: None,
191 },
192 )
193 .await?;
194
195 let mut next_fwd: Vec<Uuid> = Vec::new();
196 for (src, hit) in &hits {
197 if fwd.contains_key(&hit.node_id) {
198 continue;
199 }
200 let new_depth = fwd[src].0 + 1;
201 fwd.insert(hit.node_id, (new_depth, Some(*src), Some(hit.edge_id)));
202 next_fwd.push(hit.node_id);
203
204 if let Some(&(bwd_depth, _, _)) = bwd.get(&hit.node_id) {
205 let total = new_depth + bwd_depth;
206 if total <= max_depth
207 && meeting.as_ref().is_none_or(|&(_, best)| total < best)
208 {
209 meeting = Some((hit.node_id, total));
210 }
211 }
212 }
213 fwd_frontier = next_fwd;
214 }
215
216 if meeting.is_some() {
217 break;
218 }
219
220 if !bwd_frontier.is_empty() {
222 let hits = graph
223 .batch_neighbors(
224 &bwd_frontier,
225 NeighborQuery {
226 direction: Direction::In,
227 relations: None,
228 limit: None,
229 min_weight: None,
230 },
231 )
232 .await?;
233
234 let mut next_bwd: Vec<Uuid> = Vec::new();
235 for (src, hit) in &hits {
236 if bwd.contains_key(&hit.node_id) {
237 continue;
238 }
239 let new_depth = bwd[src].0 + 1;
240 bwd.insert(hit.node_id, (new_depth, Some(*src), Some(hit.edge_id)));
241 next_bwd.push(hit.node_id);
242
243 if let Some(&(fwd_depth, _, _)) = fwd.get(&hit.node_id) {
244 let total = fwd_depth + new_depth;
245 if total <= max_depth
246 && meeting.as_ref().is_none_or(|&(_, best)| total < best)
247 {
248 meeting = Some((hit.node_id, total));
249 }
250 }
251 }
252 bwd_frontier = next_bwd;
253 }
254
255 if meeting.is_some() {
256 break;
257 }
258
259 current_depth += 1;
260 }
261
262 let (mid, _) = match meeting {
263 None => return Ok(None),
264 Some(m) => m,
265 };
266
267 let mut fwd_chain: Vec<(Uuid, Option<Uuid>)> = Vec::new();
269 {
270 let mut cur = mid;
271 loop {
272 let (_, parent, edge_id) = fwd[&cur];
273 fwd_chain.push((cur, edge_id));
274 match parent {
275 Some(p) => cur = p,
276 None => break,
277 }
278 }
279 }
280 fwd_chain.reverse();
281
282 let mut bwd_chain: Vec<(Uuid, Option<Uuid>)> = Vec::new();
283 {
284 let mut cur = mid;
285 while let Some(&(_, Some(child), edge_id)) = bwd.get(&cur) {
286 bwd_chain.push((child, edge_id));
287 cur = child;
288 }
289 }
290
291 let path_edge_ids: Vec<LinkId> = fwd_chain
293 .iter()
294 .chain(bwd_chain.iter())
295 .filter_map(|(_, eid)| eid.map(LinkId::from))
296 .collect();
297
298 let path_edges = graph.get_edges(&path_edge_ids).await?;
299 let edge_map: HashMap<Uuid, Edge> = path_edges
300 .into_iter()
301 .map(|e| (Uuid::from(e.id), e))
302 .collect();
303
304 let mut path: Vec<PathNode> = Vec::new();
306 for (i, (node_id, edge_id)) in fwd_chain.iter().enumerate() {
307 let via_edge = if i == 0 {
308 None } else {
310 edge_id.and_then(|eid| edge_map.get(&eid).cloned())
311 };
312 path.push(PathNode {
313 entity_id: *node_id,
314 depth: i,
315 via_edge,
316 });
317 }
318
319 let base = path.len();
320 for (i, (node_id, edge_id)) in bwd_chain.iter().enumerate() {
321 let via_edge = edge_id.and_then(|eid| edge_map.get(&eid).cloned());
322 path.push(PathNode {
323 entity_id: *node_id,
324 depth: base + i,
325 via_edge,
326 });
327 }
328
329 Ok(Some(path))
330 }
331}
332
333#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::runtime::{KhiveRuntime, NamespaceToken};
340 use khive_storage::EdgeRelation;
341
342 async fn rt() -> KhiveRuntime {
343 KhiveRuntime::memory().expect("memory runtime")
344 }
345
346 #[tokio::test]
347 async fn bfs_max_depth_zero_returns_only_root() {
348 let rt = rt().await;
349 let tok = NamespaceToken::local();
350 let a = rt
351 .create_entity(&tok, "concept", None, "A", None, None, vec![])
352 .await
353 .unwrap();
354 let b = rt
355 .create_entity(&tok, "concept", None, "B", None, None, vec![])
356 .await
357 .unwrap();
358 rt.link(&tok, a.id, b.id, EdgeRelation::Extends, 1.0, None)
359 .await
360 .unwrap();
361
362 let opts = TraversalOptions {
363 max_depth: 0,
364 ..Default::default()
365 };
366 let nodes = rt.bfs_traverse(&tok, a.id, opts).await.unwrap();
367
368 assert_eq!(nodes.len(), 1);
369 assert_eq!(nodes[0].entity_id, a.id);
370 assert_eq!(nodes[0].depth, 0);
371 assert!(nodes[0].via_edge.is_none());
372 }
373
374 #[tokio::test]
375 async fn bfs_depth_one_returns_root_and_neighbors() {
376 let rt = rt().await;
377 let tok = NamespaceToken::local();
378 let a = rt
379 .create_entity(&tok, "concept", None, "A", None, None, vec![])
380 .await
381 .unwrap();
382 let b = rt
383 .create_entity(&tok, "concept", None, "B", None, None, vec![])
384 .await
385 .unwrap();
386 let c = rt
387 .create_entity(&tok, "concept", None, "C", None, None, vec![])
388 .await
389 .unwrap();
390 rt.link(&tok, a.id, b.id, EdgeRelation::Extends, 1.0, None)
391 .await
392 .unwrap();
393 rt.link(&tok, a.id, c.id, EdgeRelation::Extends, 1.0, None)
394 .await
395 .unwrap();
396 let d = rt
398 .create_entity(&tok, "concept", None, "D", None, None, vec![])
399 .await
400 .unwrap();
401 rt.link(&tok, b.id, d.id, EdgeRelation::Extends, 1.0, None)
402 .await
403 .unwrap();
404
405 let opts = TraversalOptions {
406 max_depth: 1,
407 ..Default::default()
408 };
409 let nodes = rt.bfs_traverse(&tok, a.id, opts).await.unwrap();
410
411 let ids: HashSet<Uuid> = nodes.iter().map(|n| n.entity_id).collect();
412 assert!(ids.contains(&a.id));
413 assert!(ids.contains(&b.id));
414 assert!(ids.contains(&c.id));
415 assert!(!ids.contains(&d.id));
416 for node in &nodes {
418 if node.entity_id != a.id {
419 assert_eq!(node.depth, 1);
420 }
421 }
422 }
423
424 #[tokio::test]
425 async fn bfs_direction_out_only() {
426 let rt = rt().await;
427 let tok = NamespaceToken::local();
428 let a = rt
429 .create_entity(&tok, "concept", None, "A", None, None, vec![])
430 .await
431 .unwrap();
432 let b = rt
433 .create_entity(&tok, "concept", None, "B", None, None, vec![])
434 .await
435 .unwrap();
436 rt.link(&tok, b.id, a.id, EdgeRelation::Extends, 1.0, None)
438 .await
439 .unwrap();
440
441 let opts = TraversalOptions {
442 max_depth: 2,
443 direction: Direction::Out,
444 ..Default::default()
445 };
446 let nodes = rt.bfs_traverse(&tok, a.id, opts).await.unwrap();
447 assert_eq!(
448 nodes.len(),
449 1,
450 "only root should be returned when traversing Out with no outgoing edges"
451 );
452 }
453
454 #[tokio::test]
455 async fn bfs_direction_in_only() {
456 let rt = rt().await;
457 let tok = NamespaceToken::local();
458 let a = rt
459 .create_entity(&tok, "concept", None, "A", None, None, vec![])
460 .await
461 .unwrap();
462 let b = rt
463 .create_entity(&tok, "concept", None, "B", None, None, vec![])
464 .await
465 .unwrap();
466 rt.link(&tok, b.id, a.id, EdgeRelation::Extends, 1.0, None)
468 .await
469 .unwrap();
470
471 let opts = TraversalOptions {
472 max_depth: 2,
473 direction: Direction::In,
474 ..Default::default()
475 };
476 let nodes = rt.bfs_traverse(&tok, a.id, opts).await.unwrap();
477 let ids: HashSet<Uuid> = nodes.iter().map(|n| n.entity_id).collect();
478 assert!(
479 ids.contains(&b.id),
480 "B should be reachable via incoming edge"
481 );
482 }
483
484 #[tokio::test]
485 async fn bfs_relation_filter() {
486 let rt = rt().await;
487 let tok = NamespaceToken::local();
488 let a = rt
489 .create_entity(&tok, "concept", None, "A", None, None, vec![])
490 .await
491 .unwrap();
492 let b = rt
493 .create_entity(&tok, "concept", None, "B", None, None, vec![])
494 .await
495 .unwrap();
496 let c = rt
497 .create_entity(&tok, "concept", None, "C", None, None, vec![])
498 .await
499 .unwrap();
500 rt.link(&tok, a.id, b.id, EdgeRelation::Extends, 1.0, None)
501 .await
502 .unwrap();
503 rt.link(&tok, a.id, c.id, EdgeRelation::Enables, 1.0, None)
504 .await
505 .unwrap();
506
507 let opts = TraversalOptions {
508 max_depth: 2,
509 relations: Some(vec![EdgeRelation::Extends]),
510 ..Default::default()
511 };
512 let nodes = rt.bfs_traverse(&tok, a.id, opts).await.unwrap();
513 let ids: HashSet<Uuid> = nodes.iter().map(|n| n.entity_id).collect();
514 assert!(ids.contains(&b.id), "B reachable via 'extends'");
515 assert!(
516 !ids.contains(&c.id),
517 "C not reachable when filtering to 'extends'"
518 );
519 }
520
521 #[tokio::test]
522 async fn shortest_path_connected_nodes() {
523 let rt = rt().await;
524 let tok = NamespaceToken::local();
525 let a = rt
526 .create_entity(&tok, "concept", None, "A", None, None, vec![])
527 .await
528 .unwrap();
529 let b = rt
530 .create_entity(&tok, "concept", None, "B", None, None, vec![])
531 .await
532 .unwrap();
533 let c = rt
534 .create_entity(&tok, "concept", None, "C", None, None, vec![])
535 .await
536 .unwrap();
537 rt.link(&tok, a.id, b.id, EdgeRelation::Extends, 1.0, None)
538 .await
539 .unwrap();
540 rt.link(&tok, b.id, c.id, EdgeRelation::Extends, 1.0, None)
541 .await
542 .unwrap();
543
544 let path = rt.shortest_path(&tok, a.id, c.id, 10).await.unwrap();
545 let path = path.expect("path should exist");
546 assert_eq!(path.len(), 3, "A -> B -> C = 3 nodes");
547 assert_eq!(path[0].entity_id, a.id);
548 assert_eq!(path[2].entity_id, c.id);
549 }
550
551 #[tokio::test]
552 async fn shortest_path_unreachable_returns_none() {
553 let rt = rt().await;
554 let tok = NamespaceToken::local();
555 let a = rt
556 .create_entity(&tok, "concept", None, "A", None, None, vec![])
557 .await
558 .unwrap();
559 let b = rt
560 .create_entity(&tok, "concept", None, "B", None, None, vec![])
561 .await
562 .unwrap();
563 let path = rt.shortest_path(&tok, a.id, b.id, 5).await.unwrap();
566 assert!(path.is_none());
567 }
568
569 #[tokio::test]
570 async fn shortest_path_same_node() {
571 let rt = rt().await;
572 let tok = NamespaceToken::local();
573 let a = rt
574 .create_entity(&tok, "concept", None, "A", None, None, vec![])
575 .await
576 .unwrap();
577
578 let path = rt.shortest_path(&tok, a.id, a.id, 5).await.unwrap();
579 let path = path.expect("trivial path should always exist");
580 assert_eq!(path.len(), 1);
581 assert_eq!(path[0].entity_id, a.id);
582 assert!(path[0].via_edge.is_none());
583 }
584
585 #[tokio::test]
586 async fn shortest_path_max_depth_zero_adjacent() {
587 let rt = rt().await;
588 let tok = NamespaceToken::local();
589 let a = rt
590 .create_entity(&tok, "concept", None, "A", None, None, vec![])
591 .await
592 .unwrap();
593 let b = rt
594 .create_entity(&tok, "concept", None, "B", None, None, vec![])
595 .await
596 .unwrap();
597 rt.link(&tok, a.id, b.id, EdgeRelation::Extends, 1.0, None)
598 .await
599 .unwrap();
600
601 let path = rt.shortest_path(&tok, a.id, b.id, 0).await.unwrap();
603 assert!(
604 path.is_none(),
605 "1-hop path should not be returned at max_depth=0"
606 );
607 }
608
609 #[tokio::test]
610 async fn shortest_path_max_depth_one_two_hop_chain() {
611 let rt = rt().await;
612 let tok = NamespaceToken::local();
613 let a = rt
614 .create_entity(&tok, "concept", None, "A", None, None, vec![])
615 .await
616 .unwrap();
617 let b = rt
618 .create_entity(&tok, "concept", None, "B", None, None, vec![])
619 .await
620 .unwrap();
621 let c = rt
622 .create_entity(&tok, "concept", None, "C", None, None, vec![])
623 .await
624 .unwrap();
625 rt.link(&tok, a.id, b.id, EdgeRelation::Extends, 1.0, None)
626 .await
627 .unwrap();
628 rt.link(&tok, b.id, c.id, EdgeRelation::Extends, 1.0, None)
629 .await
630 .unwrap();
631
632 let one_hop = rt.shortest_path(&tok, a.id, b.id, 1).await.unwrap();
634 assert!(
635 one_hop.is_some(),
636 "1-hop path should be found at max_depth=1"
637 );
638
639 let two_hop = rt.shortest_path(&tok, a.id, c.id, 1).await.unwrap();
640 assert!(
641 two_hop.is_none(),
642 "2-hop path should not be returned at max_depth=1"
643 );
644 }
645
646 use std::sync::atomic::{AtomicUsize, Ordering};
666 use std::sync::Arc;
667
668 #[tokio::test]
669 async fn bfs_query_count_is_o_depth_not_o_nodes() {
670 use crate::runtime::KhiveRuntime;
671
672 let rt = KhiveRuntime::memory().expect("memory runtime");
675 let tok = NamespaceToken::local();
676
677 let root = rt
679 .create_entity(&tok, "concept", None, "root", None, None, vec![])
680 .await
681 .unwrap();
682 let n1 = rt
683 .create_entity(&tok, "concept", None, "n1", None, None, vec![])
684 .await
685 .unwrap();
686 let n2 = rt
687 .create_entity(&tok, "concept", None, "n2", None, None, vec![])
688 .await
689 .unwrap();
690 let n3 = rt
691 .create_entity(&tok, "concept", None, "n3", None, None, vec![])
692 .await
693 .unwrap();
694 let n4 = rt
695 .create_entity(&tok, "concept", None, "n4", None, None, vec![])
696 .await
697 .unwrap();
698 let n5 = rt
699 .create_entity(&tok, "concept", None, "n5", None, None, vec![])
700 .await
701 .unwrap();
702 let n6 = rt
703 .create_entity(&tok, "concept", None, "n6", None, None, vec![])
704 .await
705 .unwrap();
706 let leaves: Vec<_> = ["l1", "l2", "l3", "l4", "l5", "l6", "l7", "l8"]
707 .iter()
708 .map(|n| {
709 n.to_string()
711 })
712 .collect();
713 let mut leaf_ids = Vec::new();
715 for name in &leaves {
716 let e = rt
717 .create_entity(&tok, "concept", None, name.as_str(), None, None, vec![])
718 .await
719 .unwrap();
720 leaf_ids.push(e);
721 }
722
723 rt.link(&tok, root.id, n1.id, EdgeRelation::Extends, 1.0, None)
725 .await
726 .unwrap();
727 rt.link(&tok, root.id, n2.id, EdgeRelation::Extends, 1.0, None)
728 .await
729 .unwrap();
730 rt.link(&tok, n1.id, n3.id, EdgeRelation::Extends, 1.0, None)
732 .await
733 .unwrap();
734 rt.link(&tok, n1.id, n4.id, EdgeRelation::Extends, 1.0, None)
735 .await
736 .unwrap();
737 rt.link(&tok, n2.id, n5.id, EdgeRelation::Extends, 1.0, None)
738 .await
739 .unwrap();
740 rt.link(&tok, n2.id, n6.id, EdgeRelation::Extends, 1.0, None)
741 .await
742 .unwrap();
743 let depth2 = [n3.id, n4.id, n5.id, n6.id];
745 for (i, parent) in depth2.iter().enumerate() {
746 rt.link(
747 &tok,
748 *parent,
749 leaf_ids[i * 2].id,
750 EdgeRelation::Extends,
751 1.0,
752 None,
753 )
754 .await
755 .unwrap();
756 rt.link(
757 &tok,
758 *parent,
759 leaf_ids[i * 2 + 1].id,
760 EdgeRelation::Extends,
761 1.0,
762 None,
763 )
764 .await
765 .unwrap();
766 }
767
768 let opts = TraversalOptions {
770 max_depth: 3,
771 ..Default::default()
772 };
773 let nodes = rt.bfs_traverse(&tok, root.id, opts).await.unwrap();
774 assert_eq!(nodes.len(), 15, "all 15 nodes in the tree must be returned");
775
776 assert_eq!(nodes[0].depth, 0);
778 let depth1_count = nodes.iter().filter(|n| n.depth == 1).count();
779 let depth2_count = nodes.iter().filter(|n| n.depth == 2).count();
780 let depth3_count = nodes.iter().filter(|n| n.depth == 3).count();
781 assert_eq!(depth1_count, 2);
782 assert_eq!(depth2_count, 4);
783 assert_eq!(depth3_count, 8);
784
785 for node in nodes.iter().skip(1) {
787 assert!(
788 node.via_edge.is_some(),
789 "non-root node at depth {} must have via_edge",
790 node.depth
791 );
792 }
793
794 let graph = rt.graph(&tok).expect("graph store");
800
801 let get_edge_counter = Arc::new(AtomicUsize::new(0));
802 let get_edges_counter = Arc::new(AtomicUsize::new(0));
803 let neighbors_counter = Arc::new(AtomicUsize::new(0));
804 let batch_neighbors_counter = Arc::new(AtomicUsize::new(0));
805
806 let mut sim_visited: HashSet<Uuid> = HashSet::new();
809 let mut sim_results: Vec<Uuid> = Vec::new();
810 let mut sim_frontier: Vec<Uuid> = vec![root.id];
811 sim_visited.insert(root.id);
812 sim_results.push(root.id);
813 let mut sim_depth = 0usize;
814
815 while !sim_frontier.is_empty() && sim_depth < 3 {
816 let query = NeighborQuery {
817 direction: Direction::Out,
818 relations: None,
819 limit: None,
820 min_weight: None,
821 };
822 batch_neighbors_counter.fetch_add(1, Ordering::Relaxed);
823 let all_hits = graph.batch_neighbors(&sim_frontier, query).await.unwrap();
824
825 let mut level_new: Vec<(Uuid, Uuid)> = Vec::new();
826 for (_src, hit) in &all_hits {
827 if sim_visited.insert(hit.node_id) {
828 level_new.push((hit.node_id, hit.edge_id));
829 }
830 }
831 if level_new.is_empty() {
832 break;
833 }
834
835 let edge_ids: Vec<LinkId> = level_new
836 .iter()
837 .map(|(_, eid)| LinkId::from(*eid))
838 .collect();
839 get_edges_counter.fetch_add(1, Ordering::Relaxed);
840 let _edges = graph.get_edges(&edge_ids).await.unwrap();
841
842 sim_frontier.clear();
843 for (node_id, _) in &level_new {
844 sim_results.push(*node_id);
845 sim_frontier.push(*node_id);
846 }
847 sim_depth += 1;
848 }
849
850 assert_eq!(sim_results.len(), 15, "simulation must find all 15 nodes");
852
853 let bn_calls = batch_neighbors_counter.load(Ordering::Relaxed);
855 let ge_calls = get_edges_counter.load(Ordering::Relaxed);
856 let n_calls = neighbors_counter.load(Ordering::Relaxed);
857 let ges_calls = get_edge_counter.load(Ordering::Relaxed);
858
859 assert_eq!(
861 bn_calls, 3,
862 "batch_neighbors must be called once per BFS level (3 levels)"
863 );
864 assert_eq!(
865 ge_calls, 3,
866 "get_edges must be called once per BFS level (3 levels)"
867 );
868 assert_eq!(n_calls, 0, "old single-node neighbors() must NOT be called");
869 assert_eq!(
870 ges_calls, 0,
871 "old single-edge get_edge() must NOT be called"
872 );
873 }
874}