1use crate::error::Result;
2use crate::graph::GraphStore;
3use crate::types::{Edge, Node, NodeId, QueryResult};
4use std::collections::VecDeque;
5use std::collections::{HashMap, HashSet};
6
7pub struct QueryEngine {
8 store: Box<dyn GraphStore>,
9 max_nodes: usize,
10}
11
12impl QueryEngine {
13 pub fn new(store: Box<dyn GraphStore>) -> Self {
14 QueryEngine {
15 store,
16 max_nodes: 10_000,
17 }
18 }
19
20 pub fn with_max_nodes(store: Box<dyn GraphStore>, max_nodes: usize) -> Self {
21 QueryEngine { store, max_nodes }
22 }
23
24 pub fn store_ref(&self) -> &dyn GraphStore {
25 self.store.as_ref()
26 }
27
28 pub fn query_text(
29 &self,
30 query: &str,
31 mode: &str,
32 depth: usize,
33 token_budget: Option<usize>,
34 context_filter: Option<&str>,
35 ) -> Result<QueryResult> {
36 let seeds = self.store.search(query, 10)?;
37 let seed_nodes: Vec<Node> = seeds.into_iter().map(|(_, n)| n).collect();
38
39 if seed_nodes.is_empty() {
40 return Ok(QueryResult {
41 seed_nodes: vec![],
42 neighborhood: vec![],
43 edges: vec![],
44 truncated: false,
45 });
46 }
47
48 let node_count = self.store.node_count()?;
49 if node_count > self.max_nodes {
50 return Ok(QueryResult {
51 seed_nodes: vec![],
52 neighborhood: vec![],
53 edges: vec![],
54 truncated: true,
55 });
56 }
57
58 match mode {
59 "bfs" => self.traverse_bfs(&seed_nodes, depth, context_filter, token_budget),
60 "dfs" => self.traverse_dfs(&seed_nodes, depth, context_filter, token_budget),
61 _ => self.traverse_bfs(&seed_nodes, depth, context_filter, token_budget),
62 }
63 }
64
65 fn traverse_bfs(
66 &self,
67 seed_nodes: &[Node],
68 depth: usize,
69 context_filter: Option<&str>,
70 token_budget: Option<usize>,
71 ) -> Result<QueryResult> {
72 let mut visited: HashSet<NodeId> = HashSet::new();
73 let mut neighborhood: Vec<Node> = Vec::new();
74 let mut edges: Vec<Edge> = Vec::new();
75
76 for seed in seed_nodes {
77 visited.insert(seed.id.clone());
78 neighborhood.push(seed.clone());
79 }
80
81 if depth > 0 {
82 let mut frontier: Vec<NodeId> = seed_nodes.iter().map(|n| n.id.clone()).collect();
83 for _ in 0..depth {
84 let mut next_frontier = Vec::new();
85 for node_id in &frontier {
86 if let Ok(neighbors) = self.store.neighbors(node_id, context_filter) {
87 for (neighbor, edge) in &neighbors {
88 if visited.insert(neighbor.id.clone()) {
89 neighborhood.push(neighbor.clone());
90 next_frontier.push(neighbor.id.clone());
91 }
92 edges.push(edge.clone());
93 }
94 }
95 }
96 frontier = next_frontier;
97 if frontier.is_empty() {
98 break;
99 }
100 if let Some(budget) = token_budget {
101 let token_count: usize =
102 neighborhood.iter().map(|n| n.label.len()).sum::<usize>()
103 + edges.iter().map(|e| e.relation.len()).sum::<usize>();
104 if token_count >= budget {
105 return Ok(QueryResult {
106 seed_nodes: seed_nodes.to_vec(),
107 neighborhood,
108 edges,
109 truncated: true,
110 });
111 }
112 }
113 }
114 }
115
116 Ok(QueryResult {
117 seed_nodes: seed_nodes.to_vec(),
118 neighborhood,
119 edges,
120 truncated: false,
121 })
122 }
123
124 fn traverse_dfs(
125 &self,
126 seed_nodes: &[Node],
127 depth: usize,
128 context_filter: Option<&str>,
129 token_budget: Option<usize>,
130 ) -> Result<QueryResult> {
131 let mut visited: HashSet<NodeId> = HashSet::new();
132 let mut neighborhood: Vec<Node> = Vec::new();
133 let mut edges: Vec<Edge> = Vec::new();
134
135 for seed in seed_nodes {
136 visited.insert(seed.id.clone());
137 neighborhood.push(seed.clone());
138 }
139
140 if depth > 0 {
141 let mut stack: Vec<(NodeId, usize)> =
142 seed_nodes.iter().map(|n| (n.id.clone(), 0)).collect();
143
144 while let Some((node_id, current_depth)) = stack.pop() {
145 if current_depth >= depth {
146 continue;
147 }
148
149 if let Ok(neighbors) = self.store.neighbors(&node_id, context_filter) {
150 for (neighbor, edge) in &neighbors {
151 if visited.insert(neighbor.id.clone()) {
152 neighborhood.push(neighbor.clone());
153 stack.push((neighbor.id.clone(), current_depth + 1));
154 }
155 edges.push(edge.clone());
156 }
157 }
158
159 if let Some(budget) = token_budget {
160 let token_count: usize =
161 neighborhood.iter().map(|n| n.label.len()).sum::<usize>()
162 + edges.iter().map(|e| e.relation.len()).sum::<usize>();
163 if token_count >= budget {
164 return Ok(QueryResult {
165 seed_nodes: seed_nodes.to_vec(),
166 neighborhood,
167 edges,
168 truncated: true,
169 });
170 }
171 }
172 }
173 }
174
175 Ok(QueryResult {
176 seed_nodes: seed_nodes.to_vec(),
177 neighborhood,
178 edges,
179 truncated: false,
180 })
181 }
182
183 pub fn dijkstra_path(&self, src: &str, tgt: &str) -> Result<Option<(Vec<Node>, f64)>> {
184 if src == tgt {
185 return Ok(self.store.get_node(src)?.map(|n| (vec![n], 0.0)));
186 }
187
188 let all_nodes = self.store.get_all_nodes()?;
189 let all_edges = self.store.get_all_edges()?;
190
191 let mut dist: HashMap<&str, f64> = HashMap::new();
192 let mut prev: HashMap<&str, String> = HashMap::new();
193 let mut unvisited: HashSet<&str> = HashSet::new();
194
195 for node in &all_nodes {
196 dist.insert(node.id.as_str(), f64::INFINITY);
197 unvisited.insert(node.id.as_str());
198 }
199 dist.insert(src, 0.0);
200
201 while !unvisited.is_empty() {
202 let current = unvisited
203 .iter()
204 .min_by(|a, b| {
205 dist.get(*a)
206 .unwrap_or(&f64::INFINITY)
207 .partial_cmp(dist.get(*b).unwrap_or(&f64::INFINITY))
208 .unwrap_or(std::cmp::Ordering::Equal)
209 })
210 .cloned();
211
212 let current = match current {
213 Some(c) => c,
214 None => break,
215 };
216
217 if current == tgt {
218 break;
219 }
220
221 unvisited.remove(current);
222
223 let current_dist = *dist.get(current).unwrap_or(&f64::INFINITY);
224 if current_dist == f64::INFINITY {
225 break;
226 }
227
228 for edge in &all_edges {
229 let neighbor = if edge.source == current {
230 Some(edge.target.as_str())
231 } else if edge.target == current {
232 Some(edge.source.as_str())
233 } else {
234 None
235 };
236
237 if let Some(neighbor) = neighbor {
238 if unvisited.contains(neighbor) {
239 let alt = current_dist + edge.weight;
240 if alt < *dist.get(neighbor).unwrap_or(&f64::INFINITY) {
241 dist.insert(neighbor, alt);
242 prev.insert(neighbor, current.to_string());
243 }
244 }
245 }
246 }
247 }
248
249 if prev.contains_key(tgt) || src == tgt {
250 let mut path_nodes = Vec::new();
251 let mut current = tgt.to_string();
252 while current != src {
253 if let Some(node) = self.store.get_node(¤t)? {
254 path_nodes.push(node);
255 }
256 current = prev.get(current.as_str()).cloned().unwrap_or_default();
257 }
258 if let Some(node) = self.store.get_node(src)? {
259 path_nodes.push(node);
260 }
261 path_nodes.reverse();
262
263 let total_weight = *dist.get(tgt).unwrap_or(&f64::INFINITY);
264 Ok(Some((path_nodes, total_weight)))
265 } else {
266 Ok(None)
267 }
268 }
269
270 pub fn dfs(&self, src: &str, depth: usize, relation_filter: Option<&str>) -> Result<Vec<Node>> {
271 let mut visited: HashSet<NodeId> = HashSet::new();
272 let mut result: Vec<Node> = Vec::new();
273 let mut stack: Vec<(NodeId, usize)> = vec![(src.to_string(), 0)];
274
275 while let Some((node_id, d)) = stack.pop() {
276 if d > depth {
277 continue;
278 }
279 if !visited.insert(node_id.clone()) {
280 continue;
281 }
282 if let Some(node) = self.store.get_node(&node_id)? {
283 result.push(node);
284 }
285 if d < depth {
286 if let Ok(neighbors) = self.store.neighbors(&node_id, relation_filter) {
287 for (neighbor, _) in &neighbors {
288 if !visited.contains(&neighbor.id) {
289 stack.push((neighbor.id.clone(), d + 1));
290 }
291 }
292 }
293 }
294 }
295
296 Ok(result)
297 }
298
299 pub fn get_node_by_label(&self, label: &str) -> Result<Option<Node>> {
300 let nodes = self.store.search(label, 1)?;
301 Ok(nodes.into_iter().next().map(|(_, n)| n))
302 }
303
304 pub fn get_node_by_id(&self, id: &str) -> Result<Option<Node>> {
305 self.store.get_node(id)
306 }
307
308 pub fn resolve_node(&self, id_or_label: &str) -> Result<Option<Node>> {
309 if let Some(node) = self.store.get_node(id_or_label)? {
310 return Ok(Some(node));
311 }
312 self.get_node_by_label(id_or_label)
313 }
314
315 pub fn get_neighbors(
316 &self,
317 id: &str,
318 relation_filter: Option<&str>,
319 ) -> Result<Vec<(Node, Edge)>> {
320 self.store.neighbors(id, relation_filter)
321 }
322
323 pub fn shortest_path(&self, src: &str, tgt: &str) -> Result<Option<Vec<Node>>> {
324 self.store.shortest_path(src, tgt)
325 }
326
327 pub fn graph_stats(&self) -> Result<String> {
328 let node_count = self.store.node_count()?;
329 let edge_count = self.store.edge_count()?;
330 Ok(format!("Nodes: {}, Edges: {}", node_count, edge_count))
331 }
332
333 pub fn get_community(&self, community_id: usize) -> Result<Vec<Node>> {
334 let nodes = self.store.get_all_nodes()?;
335 Ok(nodes
336 .into_iter()
337 .filter(|n| n.community == Some(community_id))
338 .collect())
339 }
340
341 pub fn god_nodes(&self, top_n: usize) -> Result<Vec<Node>> {
342 let edges = self.store.get_all_edges()?;
343 let all_nodes = self.store.get_all_nodes()?;
344 let mut degree: HashMap<NodeId, usize> = HashMap::new();
345 for edge in &edges {
346 *degree.entry(edge.source.clone()).or_insert(0) += 1;
347 *degree.entry(edge.target.clone()).or_insert(0) += 1;
348 }
349 let mut ranked: Vec<(usize, NodeId)> = degree.into_iter().map(|(k, v)| (v, k)).collect();
350 ranked.sort_by_key(|k| std::cmp::Reverse(k.0));
351 ranked.truncate(top_n);
352
353 let id_set: HashSet<&str> = ranked.iter().map(|(_, id)| id.as_str()).collect();
354 let mut result: Vec<Node> = all_nodes
355 .into_iter()
356 .filter(|n| id_set.contains(n.id.as_str()))
357 .collect();
358 result.sort_by(|a, b| {
359 let da = ranked
360 .iter()
361 .find(|(_, id)| id == &a.id)
362 .map(|(d, _)| *d)
363 .unwrap_or(0);
364 let db = ranked
365 .iter()
366 .find(|(_, id)| id == &b.id)
367 .map(|(d, _)| *d)
368 .unwrap_or(0);
369 db.cmp(&da)
370 });
371 Ok(result)
372 }
373
374 pub fn shortest_path_with_max_hops(
375 &self,
376 src: &str,
377 tgt: &str,
378 max_hops: usize,
379 ) -> Result<Option<Vec<Node>>> {
380 if src == tgt {
381 return self.store.get_node(src).map(|n| n.map(|n| vec![n]));
382 }
383
384 let mut visited: HashSet<NodeId> = HashSet::new();
385 let mut queue: VecDeque<(NodeId, usize)> = VecDeque::new();
386 let mut parent: HashMap<NodeId, NodeId> = HashMap::new();
387
388 visited.insert(src.to_string());
389 queue.push_back((src.to_string(), 0));
390
391 while let Some((current, depth)) = queue.pop_front() {
392 if depth >= max_hops {
393 continue;
394 }
395
396 let neighbors = self.store.neighbors(¤t, None)?;
397 for (neighbor, _) in &neighbors {
398 if visited.insert(neighbor.id.clone()) {
399 parent.insert(neighbor.id.clone(), current.clone());
400 if neighbor.id == tgt {
401 let mut path = Vec::new();
402 let mut cur = tgt.to_string();
403 while cur != src {
404 if let Some(n) = self.store.get_node(&cur)? {
405 path.push(n);
406 }
407 cur = parent.get(&cur).cloned().unwrap_or_default();
408 }
409 if let Some(n) = self.store.get_node(src)? {
410 path.push(n);
411 }
412 path.reverse();
413 return Ok(Some(path));
414 }
415 queue.push_back((neighbor.id.clone(), depth + 1));
416 }
417 }
418 }
419
420 Ok(None)
421 }
422
423 pub fn infer_context_filter(question: &str) -> Option<String> {
424 const CONTEXT_HINTS: &[(&str, &[&str])] = &[
425 (
426 "call",
427 &["call", "calls", "called", "invoke", "invokes", "invoked"],
428 ),
429 (
430 "import",
431 &["import", "imports", "imported", "module", "modules"],
432 ),
433 (
434 "field",
435 &[
436 "field",
437 "fields",
438 "member",
439 "members",
440 "property",
441 "properties",
442 ],
443 ),
444 (
445 "parameter_type",
446 &[
447 "parameter",
448 "parameters",
449 "param",
450 "params",
451 "argument",
452 "arguments",
453 ],
454 ),
455 ("return_type", &["return", "returns", "returned"]),
456 (
457 "generic_arg",
458 &["generic", "generics", "template", "templates"],
459 ),
460 ];
461 let tokens: std::collections::HashSet<String> = question
462 .split(|c: char| !c.is_alphanumeric())
463 .filter(|s| !s.is_empty())
464 .map(|s| s.to_lowercase())
465 .collect();
466 for (context, hints) in CONTEXT_HINTS {
467 if hints.iter().any(|h| tokens.contains(*h)) {
468 return Some(context.to_string());
469 }
470 }
471 None
472 }
473
474 pub fn resolve_context_filter(question: &str, explicit: Option<&str>) -> Option<String> {
475 const ALIASES: &[(&str, &str)] = &[
476 ("param", "parameter_type"),
477 ("params", "parameter_type"),
478 ("parameter", "parameter_type"),
479 ("parameters", "parameter_type"),
480 ("argument", "parameter_type"),
481 ("arguments", "parameter_type"),
482 ("arg", "parameter_type"),
483 ("args", "parameter_type"),
484 ("return", "return_type"),
485 ("returns", "return_type"),
486 ("returned", "return_type"),
487 ("generic", "generic_arg"),
488 ("generics", "generic_arg"),
489 ("template", "generic_arg"),
490 ("templates", "generic_arg"),
491 ("calls", "call"),
492 ("called", "call"),
493 ("invoke", "call"),
494 ("invocation", "call"),
495 ("fields", "field"),
496 ("property", "field"),
497 ("properties", "field"),
498 ("member", "field"),
499 ("members", "field"),
500 ("imports", "import"),
501 ("imported", "import"),
502 ("module", "import"),
503 ("modules", "import"),
504 ("exports", "export"),
505 ("exported", "export"),
506 ];
507 if let Some(exp) = explicit {
508 let key = exp.trim().to_lowercase();
509 let normalized = ALIASES
510 .iter()
511 .find(|(k, _)| *k == key)
512 .map(|(_, v)| v.to_string())
513 .unwrap_or(key);
514 return Some(normalized);
515 }
516 Self::infer_context_filter(question)
517 }
518
519 pub fn detailed_stats(&self) -> Result<String> {
520 let nodes = self.store.get_all_nodes()?;
521 let edges = self.store.get_all_edges()?;
522
523 let mut file_types: HashMap<&str, usize> = HashMap::new();
524 for node in &nodes {
525 let ext = node.source_file.rsplit('.').next().unwrap_or("unknown");
526 *file_types.entry(ext).or_insert(0) += 1;
527 }
528
529 let mut rel_types: HashMap<&str, usize> = HashMap::new();
530 for edge in &edges {
531 *rel_types.entry(edge.relation.as_str()).or_insert(0) += 1;
532 }
533
534 let mut stats = String::new();
535 stats.push_str(&format!("Nodes: {}, Edges: {}\n", nodes.len(), edges.len()));
536 stats.push_str(&format!("File types: {:?}\n", file_types));
537 stats.push_str(&format!("Relation types: {:?}\n", rel_types));
538 Ok(stats)
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use crate::types::Node;
546 use std::collections::HashMap;
547 use std::sync::Mutex;
548
549 struct MockStore {
550 nodes: Mutex<HashMap<NodeId, Node>>,
551 edges: Mutex<Vec<Edge>>,
552 }
553
554 impl MockStore {
555 fn new() -> Self {
556 MockStore {
557 nodes: Mutex::new(HashMap::new()),
558 edges: Mutex::new(Vec::new()),
559 }
560 }
561 }
562
563 impl GraphStore for MockStore {
564 fn dijkstra_shortest_path(&self, _src: &str, _tgt: &str) -> Result<Option<Vec<Node>>> {
565 Ok(None)
566 }
567
568 fn add_node(&self, node: Node) -> Result<()> {
569 self.nodes.lock().unwrap().insert(node.id.clone(), node);
570 Ok(())
571 }
572
573 fn add_edge(&self, edge: Edge) -> Result<()> {
574 self.edges.lock().unwrap().push(edge);
575 Ok(())
576 }
577
578 fn get_node(&self, id: &str) -> Result<Option<Node>> {
579 Ok(self.nodes.lock().unwrap().get(id).cloned())
580 }
581
582 fn get_all_nodes(&self) -> Result<Vec<Node>> {
583 Ok(self.nodes.lock().unwrap().values().cloned().collect())
584 }
585
586 fn get_all_edges(&self) -> Result<Vec<Edge>> {
587 Ok(self.edges.lock().unwrap().clone())
588 }
589
590 fn neighbors(&self, id: &str, filter: Option<&str>) -> Result<Vec<(Node, Edge)>> {
591 let edges = self.edges.lock().unwrap();
592 let nodes = self.nodes.lock().unwrap();
593 let mut result = Vec::new();
594 for edge in edges.iter() {
595 if let Some(f) = filter {
596 if edge.relation != f {
597 continue;
598 }
599 }
600 if edge.source == id {
601 if let Some(node) = nodes.get(&edge.target) {
602 result.push((node.clone(), edge.clone()));
603 }
604 } else if edge.target == id {
605 if let Some(node) = nodes.get(&edge.source) {
606 result.push((node.clone(), edge.clone()));
607 }
608 }
609 }
610 Ok(result)
611 }
612
613 fn search(&self, query: &str, _top_k: usize) -> Result<Vec<(f64, Node)>> {
614 let nodes = self.nodes.lock().unwrap();
615 let q = query.to_lowercase();
616 let mut results = Vec::new();
617 for node in nodes.values() {
618 if node.label.to_lowercase().contains(&q) || node.id.to_lowercase().contains(&q) {
619 results.push((1.0, node.clone()));
620 }
621 }
622 Ok(results)
623 }
624
625 fn shortest_path(&self, src: &str, tgt: &str) -> Result<Option<Vec<Node>>> {
626 if src == tgt {
627 return Ok(self
628 .nodes
629 .lock()
630 .unwrap()
631 .get(src)
632 .cloned()
633 .map(|n| vec![n]));
634 }
635
636 let nodes = self.nodes.lock().unwrap();
637 let edges = self.edges.lock().unwrap();
638
639 let mut visited = HashSet::new();
640 let mut queue = VecDeque::new();
641 let mut parent: HashMap<String, String> = HashMap::new();
642
643 visited.insert(src.to_string());
644 queue.push_back(src.to_string());
645
646 while let Some(current) = queue.pop_front() {
647 if current == tgt {
648 let mut path = Vec::new();
649 let mut cur = tgt.to_string();
650 while cur != src {
651 if let Some(n) = nodes.get(&cur) {
652 path.push(n.clone());
653 }
654 cur = parent.get(&cur).cloned().unwrap_or_default();
655 }
656 if let Some(n) = nodes.get(src) {
657 path.push(n.clone());
658 }
659 path.reverse();
660 return Ok(Some(path));
661 }
662
663 for edge in edges.iter() {
664 let neighbor = if edge.source == current {
665 Some(edge.target.clone())
666 } else if edge.target == current {
667 Some(edge.source.clone())
668 } else {
669 None
670 };
671 if let Some(neighbor) = neighbor {
672 if visited.insert(neighbor.clone()) {
673 parent.insert(neighbor.clone(), current.clone());
674 queue.push_back(neighbor);
675 }
676 }
677 }
678 }
679
680 Ok(None)
681 }
682
683 fn subgraph(&self, _node_ids: &[&str]) -> Result<(Vec<Node>, Vec<Edge>)> {
684 Ok((vec![], vec![]))
685 }
686
687 fn node_count(&self) -> Result<usize> {
688 Ok(self.nodes.lock().unwrap().len())
689 }
690
691 fn edge_count(&self) -> Result<usize> {
692 Ok(self.edges.lock().unwrap().len())
693 }
694
695 fn remove_node(&self, id: &str) -> Result<()> {
696 self.nodes.lock().unwrap().remove(id);
697 Ok(())
698 }
699
700 fn remove_edge(&self, source: &str, target: &str, relation: &str) -> Result<()> {
701 let mut edges = self.edges.lock().unwrap();
702 edges.retain(|e| !(e.source == source && e.target == target && e.relation == relation));
703 Ok(())
704 }
705
706 fn clear(&self) -> Result<()> {
707 self.nodes.lock().unwrap().clear();
708 self.edges.lock().unwrap().clear();
709 Ok(())
710 }
711 }
712
713 fn make_node(id: &str, label: &str) -> Node {
714 Node {
715 id: id.to_string(),
716 label: label.to_string(),
717 file_type: "code".to_string(),
718 source_file: "test.py".to_string(),
719 source_location: None,
720 community: None,
721 rationale: None,
722 docstring: None,
723 metadata: HashMap::new(),
724 }
725 }
726
727 fn make_edge(src: &str, tgt: &str, relation: &str, weight: f64) -> Edge {
728 Edge {
729 source: src.to_string(),
730 target: tgt.to_string(),
731 relation: relation.to_string(),
732 confidence: "EXTRACTED".to_string(),
733 source_file: Some("test.py".to_string()),
734 weight,
735 context: None,
736 }
737 }
738
739 #[test]
740 fn test_get_node_by_id() {
741 let store = MockStore::new();
742 store.add_node(make_node("auth", "AuthService")).unwrap();
743 let engine = QueryEngine::new(Box::new(store));
744 let node = engine.get_node_by_id("auth").unwrap();
745 assert!(node.is_some());
746 assert_eq!(node.unwrap().label, "AuthService");
747 }
748
749 #[test]
750 fn test_get_node_not_found() {
751 let store = MockStore::new();
752 let engine = QueryEngine::new(Box::new(store));
753 let node = engine.get_node_by_id("nonexistent").unwrap();
754 assert!(node.is_none());
755 }
756
757 #[test]
758 fn test_graph_stats() {
759 let store = MockStore::new();
760 store.add_node(make_node("a", "A")).unwrap();
761 store.add_node(make_node("b", "B")).unwrap();
762 let engine = QueryEngine::new(Box::new(store));
763 let stats = engine.graph_stats().unwrap();
764 assert!(stats.contains("Nodes: 2"));
765 }
766
767 #[test]
768 fn test_detailed_stats() {
769 let store = MockStore::new();
770 store.add_node(make_node("a", "A")).unwrap();
771 store.add_node(make_node("b", "B")).unwrap();
772 store
773 .add_edge(make_edge("a", "b", "connects", 1.0))
774 .unwrap();
775 let engine = QueryEngine::new(Box::new(store));
776 let stats = engine.detailed_stats().unwrap();
777 assert!(stats.contains("Nodes: 2"));
778 assert!(stats.contains("Edges: 1"));
779 }
780
781 #[test]
782 fn test_bfs_traversal() {
783 let store = MockStore::new();
784 store.add_node(make_node("a", "A")).unwrap();
785 store.add_node(make_node("b", "B")).unwrap();
786 store.add_node(make_node("c", "C")).unwrap();
787 store
788 .add_edge(make_edge("a", "b", "connects", 1.0))
789 .unwrap();
790 store
791 .add_edge(make_edge("b", "c", "connects", 1.0))
792 .unwrap();
793
794 let engine = QueryEngine::new(Box::new(store));
795 let result = engine.query_text("a", "bfs", 2, None, None).unwrap();
796 assert_eq!(result.seed_nodes.len(), 1);
797 assert_eq!(result.seed_nodes[0].id, "a");
798 assert!(result.neighborhood.len() >= 2);
799 }
800
801 #[test]
802 fn test_dfs_traversal() {
803 let store = MockStore::new();
804 store.add_node(make_node("a", "A")).unwrap();
805 store.add_node(make_node("b", "B")).unwrap();
806 store.add_node(make_node("c", "C")).unwrap();
807 store
808 .add_edge(make_edge("a", "b", "connects", 1.0))
809 .unwrap();
810 store
811 .add_edge(make_edge("b", "c", "connects", 1.0))
812 .unwrap();
813
814 let engine = QueryEngine::new(Box::new(store));
815 let result = engine.query_text("a", "dfs", 2, None, None).unwrap();
816 assert_eq!(result.seed_nodes.len(), 1);
817 assert!(result.neighborhood.len() >= 2);
818 }
819
820 #[test]
821 fn test_dfs_method() {
822 let store = MockStore::new();
823 store.add_node(make_node("a", "A")).unwrap();
824 store.add_node(make_node("b", "B")).unwrap();
825 store.add_node(make_node("c", "C")).unwrap();
826 store
827 .add_edge(make_edge("a", "b", "connects", 1.0))
828 .unwrap();
829 store
830 .add_edge(make_edge("b", "c", "connects", 1.0))
831 .unwrap();
832
833 let engine = QueryEngine::new(Box::new(store));
834 let nodes = engine.dfs("a", 3, None).unwrap();
835 assert_eq!(nodes.len(), 3);
836 }
837
838 #[test]
839 fn test_dijkstra_path() {
840 let store = MockStore::new();
841 store.add_node(make_node("a", "A")).unwrap();
842 store.add_node(make_node("b", "B")).unwrap();
843 store.add_node(make_node("c", "C")).unwrap();
844 store
845 .add_edge(make_edge("a", "b", "connects", 1.5))
846 .unwrap();
847 store
848 .add_edge(make_edge("b", "c", "connects", 2.5))
849 .unwrap();
850
851 let engine = QueryEngine::new(Box::new(store));
852 let result = engine.dijkstra_path("a", "c").unwrap();
853 assert!(result.is_some());
854 let (path, weight) = result.unwrap();
855 assert_eq!(path.len(), 3);
856 assert!((weight - 4.0).abs() < 0.01);
857 }
858
859 #[test]
860 fn test_dijkstra_path_same_node() {
861 let store = MockStore::new();
862 store.add_node(make_node("a", "A")).unwrap();
863 let engine = QueryEngine::new(Box::new(store));
864 let result = engine.dijkstra_path("a", "a").unwrap();
865 assert!(result.is_some());
866 let (path, weight) = result.unwrap();
867 assert_eq!(path.len(), 1);
868 assert!((weight - 0.0).abs() < 0.01);
869 }
870
871 #[test]
872 fn test_dijkstra_path_no_path() {
873 let store = MockStore::new();
874 store.add_node(make_node("a", "A")).unwrap();
875 store.add_node(make_node("b", "B")).unwrap();
876 let engine = QueryEngine::new(Box::new(store));
877 let result = engine.dijkstra_path("a", "b").unwrap();
878 assert!(result.is_none());
879 }
880
881 #[test]
882 fn test_context_filter() {
883 let store = MockStore::new();
884 store.add_node(make_node("a", "A")).unwrap();
885 store.add_node(make_node("b", "B")).unwrap();
886 store.add_node(make_node("c", "C")).unwrap();
887 store.add_edge(make_edge("a", "b", "imports", 1.0)).unwrap();
888 store.add_edge(make_edge("a", "c", "calls", 1.0)).unwrap();
889
890 let engine = QueryEngine::new(Box::new(store));
891 let neighbors = engine.get_neighbors("a", Some("imports")).unwrap();
892 assert_eq!(neighbors.len(), 1);
893 assert_eq!(neighbors[0].0.label, "B");
894 }
895
896 #[test]
897 fn test_token_budget() {
898 let store = MockStore::new();
899 store.add_node(make_node("a", "A")).unwrap();
900 let engine = QueryEngine::new(Box::new(store));
901 let result = engine.query_text("a", "bfs", 1, Some(5), None).unwrap();
902 assert_eq!(result.seed_nodes.len(), 1);
903 }
904
905 #[test]
908 fn test_query_exact_match() {
909 let store = MockStore::new();
910 store.add_node(make_node("a1", "AuthService")).unwrap();
911 store.add_node(make_node("u1", "UserService")).unwrap();
912 let engine = QueryEngine::new(Box::new(store));
913 let result = engine
914 .query_text("AuthService", "bfs", 0, None, None)
915 .unwrap();
916 assert_eq!(result.seed_nodes.len(), 1);
917 assert_eq!(result.seed_nodes[0].label, "AuthService");
918 }
919
920 #[test]
923 fn test_query_prefix_match() {
924 let store = MockStore::new();
925 store.add_node(make_node("a1", "AuthService")).unwrap();
926 store.add_node(make_node("a2", "AuthMiddleware")).unwrap();
927 store.add_node(make_node("u1", "UserService")).unwrap();
928 let engine = QueryEngine::new(Box::new(store));
929 let result = engine.query_text("Auth", "bfs", 0, None, None).unwrap();
930 assert_eq!(result.seed_nodes.len(), 2);
931 for n in &result.seed_nodes {
932 assert!(n.label.starts_with("Auth"));
933 }
934 }
935
936 #[test]
939 fn test_query_substring_match() {
940 let store = MockStore::new();
941 store.add_node(make_node("a1", "AuthService")).unwrap();
942 store.add_node(make_node("u1", "UserService")).unwrap();
943 store.add_node(make_node("r1", "RateLimiter")).unwrap();
944 let engine = QueryEngine::new(Box::new(store));
945 let result = engine.query_text("Service", "bfs", 0, None, None).unwrap();
946 assert_eq!(result.seed_nodes.len(), 2);
947 for n in &result.seed_nodes {
948 assert!(n.label.contains("Service"));
949 }
950 }
951
952 #[test]
955 fn test_query_bfs_depth_1() {
956 let store = MockStore::new();
957 store.add_node(make_node("a", "A")).unwrap();
958 store.add_node(make_node("b", "B")).unwrap();
959 store.add_node(make_node("c", "C")).unwrap();
960 store
961 .add_edge(make_edge("a", "b", "connects", 1.0))
962 .unwrap();
963 store
964 .add_edge(make_edge("b", "c", "connects", 1.0))
965 .unwrap();
966 let engine = QueryEngine::new(Box::new(store));
967 let result = engine.query_text("a", "bfs", 1, None, None).unwrap();
968 assert_eq!(
969 result.neighborhood.len(),
970 2,
971 "depth 1: seed + immediate neighbor"
972 );
973 assert!(result.neighborhood.iter().any(|n| n.id == "b"));
974 assert!(
975 !result.neighborhood.iter().any(|n| n.id == "c"),
976 "C should not be reachable at depth 1"
977 );
978 }
979
980 #[test]
981 fn test_query_bfs_depth_3() {
982 let store = MockStore::new();
983 store.add_node(make_node("a", "A")).unwrap();
984 store.add_node(make_node("b", "B")).unwrap();
985 store.add_node(make_node("c", "C")).unwrap();
986 store
987 .add_edge(make_edge("a", "b", "connects", 1.0))
988 .unwrap();
989 store
990 .add_edge(make_edge("b", "c", "connects", 1.0))
991 .unwrap();
992 let engine = QueryEngine::new(Box::new(store));
993 let result = engine.query_text("a", "bfs", 3, None, None).unwrap();
994 assert_eq!(result.neighborhood.len(), 3, "depth 3: all nodes reachable");
995 assert!(
996 result.neighborhood.iter().any(|n| n.id == "c"),
997 "C should be reachable at depth 3"
998 );
999 }
1000
1001 #[test]
1004 fn test_query_context_filter_call() {
1005 let store = MockStore::new();
1006 store.add_node(make_node("a", "A")).unwrap();
1007 store.add_node(make_node("b", "B")).unwrap();
1008 store.add_node(make_node("c", "C")).unwrap();
1009 store.add_edge(make_edge("a", "b", "imports", 1.0)).unwrap();
1010 store.add_edge(make_edge("a", "c", "calls", 1.0)).unwrap();
1011 let engine = QueryEngine::new(Box::new(store));
1012 let result = engine
1013 .query_text("a", "bfs", 1, None, Some("calls"))
1014 .unwrap();
1015 assert_eq!(result.seed_nodes.len(), 1);
1016 let neighbor_ids: Vec<&str> = result.neighborhood.iter().map(|n| n.id.as_str()).collect();
1017 assert!(
1018 neighbor_ids.contains(&"c"),
1019 "call target should be in neighborhood"
1020 );
1021 assert!(
1022 !neighbor_ids.contains(&"b"),
1023 "import target should be filtered out"
1024 );
1025 }
1026
1027 #[test]
1030 fn test_query_context_filter_inferred() {
1031 assert_eq!(
1032 QueryEngine::infer_context_filter("who calls AuthService"),
1033 Some("call".to_string())
1034 );
1035 assert_eq!(
1036 QueryEngine::infer_context_filter("what imports UserModule"),
1037 Some("import".to_string())
1038 );
1039 assert_eq!(
1040 QueryEngine::infer_context_filter("list fields of Foo"),
1041 Some("field".to_string())
1042 );
1043 assert_eq!(
1044 QueryEngine::infer_context_filter("show me AuthService"),
1045 None
1046 );
1047 }
1048
1049 #[test]
1052 fn test_query_context_filter_explicit() {
1053 assert_eq!(
1054 QueryEngine::resolve_context_filter("who calls AuthService", Some("imports")),
1055 Some("import".to_string())
1056 );
1057 assert_eq!(
1058 QueryEngine::resolve_context_filter("who calls AuthService", None),
1059 Some("call".to_string())
1060 );
1061 assert_eq!(
1062 QueryEngine::resolve_context_filter("show me AuthService", None),
1063 None
1064 );
1065 }
1066
1067 #[test]
1070 fn test_get_node_by_label() {
1071 let store = MockStore::new();
1072 store.add_node(make_node("a1", "AuthService")).unwrap();
1073 let engine = QueryEngine::new(Box::new(store));
1074 let node = engine.get_node_by_label("AuthService").unwrap();
1075 assert!(node.is_some());
1076 assert_eq!(node.unwrap().label, "AuthService");
1077 }
1078
1079 #[test]
1082 fn test_get_neighbors_basic() {
1083 let store = MockStore::new();
1084 store.add_node(make_node("a", "A")).unwrap();
1085 store.add_node(make_node("b", "B")).unwrap();
1086 store.add_node(make_node("c", "C")).unwrap();
1087 store
1088 .add_edge(make_edge("a", "b", "connects", 1.0))
1089 .unwrap();
1090 store.add_edge(make_edge("a", "c", "calls", 1.0)).unwrap();
1091 let engine = QueryEngine::new(Box::new(store));
1092 let neighbors = engine.get_neighbors("a", None).unwrap();
1093 assert_eq!(neighbors.len(), 2);
1094 }
1095
1096 #[test]
1099 fn test_get_community() {
1100 let store = MockStore::new();
1101 let mut n1 = make_node("a", "A");
1102 n1.community = Some(1);
1103 let mut n2 = make_node("b", "B");
1104 n2.community = Some(1);
1105 let n3 = make_node("c", "C");
1106 store.add_node(n1).unwrap();
1107 store.add_node(n2).unwrap();
1108 store.add_node(n3).unwrap();
1109 let engine = QueryEngine::new(Box::new(store));
1110 let members = engine.get_community(1).unwrap();
1111 assert_eq!(members.len(), 2);
1112 for n in &members {
1113 assert_eq!(n.community, Some(1));
1114 }
1115 }
1116
1117 #[test]
1120 fn test_god_nodes() {
1121 let store = MockStore::new();
1122 store.add_node(make_node("a", "A")).unwrap();
1123 store.add_node(make_node("b", "B")).unwrap();
1124 store.add_node(make_node("c", "C")).unwrap();
1125 store
1126 .add_edge(make_edge("a", "b", "connects", 1.0))
1127 .unwrap();
1128 store
1129 .add_edge(make_edge("a", "c", "connects", 1.0))
1130 .unwrap();
1131 store
1132 .add_edge(make_edge("b", "c", "connects", 1.0))
1133 .unwrap();
1134 let engine = QueryEngine::new(Box::new(store));
1135 let gods = engine.god_nodes(2).unwrap();
1136 assert_eq!(gods.len(), 2);
1137 }
1138
1139 #[test]
1142 fn test_shortest_path_found() {
1143 let store = MockStore::new();
1144 store.add_node(make_node("a", "A")).unwrap();
1145 store.add_node(make_node("b", "B")).unwrap();
1146 store.add_node(make_node("c", "C")).unwrap();
1147 store
1148 .add_edge(make_edge("a", "b", "connects", 1.0))
1149 .unwrap();
1150 store
1151 .add_edge(make_edge("b", "c", "connects", 1.0))
1152 .unwrap();
1153 let engine = QueryEngine::new(Box::new(store));
1154 let path = engine.shortest_path("a", "c").unwrap();
1155 assert!(path.is_some());
1156 assert_eq!(path.unwrap().len(), 3);
1157 }
1158
1159 #[test]
1162 fn test_shortest_path_max_hops() {
1163 let store = MockStore::new();
1164 store.add_node(make_node("a", "A")).unwrap();
1165 store.add_node(make_node("b", "B")).unwrap();
1166 store.add_node(make_node("c", "C")).unwrap();
1167 store.add_node(make_node("d", "D")).unwrap();
1168 store
1169 .add_edge(make_edge("a", "b", "connects", 1.0))
1170 .unwrap();
1171 store
1172 .add_edge(make_edge("b", "c", "connects", 1.0))
1173 .unwrap();
1174 store
1175 .add_edge(make_edge("c", "d", "connects", 1.0))
1176 .unwrap();
1177
1178 let engine = QueryEngine::new(Box::new(store));
1179
1180 let limited = engine.shortest_path_with_max_hops("a", "d", 2).unwrap();
1182 assert!(limited.is_none(), "path exceeds max_hops");
1183
1184 let found = engine.shortest_path_with_max_hops("a", "d", 3).unwrap();
1186 assert!(found.is_some());
1187 assert_eq!(found.unwrap().len(), 4);
1188 }
1189
1190 #[test]
1191 fn test_resolve_node_by_label_when_id_missing() {
1192 let store = MockStore::new();
1193 store
1194 .add_node(make_node("api_authservice", "AuthService"))
1195 .unwrap();
1196 let engine = QueryEngine::new(Box::new(store));
1197
1198 let node = engine.resolve_node("AuthService").unwrap();
1200 assert!(node.is_some(), "resolve_node must find node by label");
1201 assert_eq!(node.unwrap().id, "api_authservice");
1202 }
1203
1204 #[test]
1205 fn test_resolve_node_by_exact_id() {
1206 let store = MockStore::new();
1207 store
1208 .add_node(make_node("api_authservice", "AuthService"))
1209 .unwrap();
1210 let engine = QueryEngine::new(Box::new(store));
1211
1212 let node = engine.resolve_node("api_authservice").unwrap();
1213 assert!(node.is_some());
1214 assert_eq!(node.unwrap().id, "api_authservice");
1215 }
1216
1217 #[test]
1218 fn test_resolve_node_not_found() {
1219 let store = MockStore::new();
1220 let engine = QueryEngine::new(Box::new(store));
1221 let node = engine.resolve_node("ghost").unwrap();
1222 assert!(node.is_none());
1223 }
1224}