1pub mod mcp;
7pub mod search;
8
9use std::collections::{HashMap, HashSet, VecDeque};
10use std::path::Path;
11
12use graphify_core::graph::KnowledgeGraph;
13use serde_json::Value;
14use thiserror::Error;
15
16#[derive(Debug, Error)]
18pub enum ServeError {
19 #[error("IO error: {0}")]
20 Io(#[from] std::io::Error),
21
22 #[error("graph load error: {0}")]
23 GraphLoad(String),
24
25 #[error("serialization error: {0}")]
26 Serialization(#[from] serde_json::Error),
27}
28
29pub fn score_nodes(graph: &KnowledgeGraph, terms: &[String]) -> Vec<(f64, String)> {
35 let index = search::SearchIndex::build(graph);
36 index.search(terms)
37}
38
39pub fn bfs(
43 graph: &KnowledgeGraph,
44 start: &[String],
45 depth: usize,
46) -> (Vec<String>, Vec<(String, String)>) {
47 let mut visited: HashSet<String> = HashSet::new();
48 let mut edges: Vec<(String, String)> = Vec::new();
49 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
50
51 for s in start {
52 if graph.get_node(s).is_some() {
53 visited.insert(s.clone());
54 queue.push_back((s.clone(), 0));
55 }
56 }
57
58 while let Some((current, current_depth)) = queue.pop_front() {
59 if current_depth >= depth {
60 continue;
61 }
62
63 for neighbor_id in graph.neighbor_ids(¤t) {
64 edges.push((current.clone(), neighbor_id.clone()));
65
66 if !visited.contains(&neighbor_id) {
67 visited.insert(neighbor_id.clone());
68 queue.push_back((neighbor_id, current_depth + 1));
69 }
70 }
71 }
72
73 let visited_vec: Vec<String> = visited.into_iter().collect();
74 (visited_vec, edges)
75}
76
77pub fn dfs(
81 graph: &KnowledgeGraph,
82 start: &[String],
83 depth: usize,
84) -> (Vec<String>, Vec<(String, String)>) {
85 let mut visited: HashSet<String> = HashSet::new();
86 let mut edges: Vec<(String, String)> = Vec::new();
87 let mut stack: Vec<(String, usize)> = Vec::new();
88
89 for s in start {
90 if graph.get_node(s).is_some() {
91 visited.insert(s.clone());
92 stack.push((s.clone(), 0));
93 }
94 }
95
96 while let Some((current, current_depth)) = stack.pop() {
97 if current_depth >= depth {
98 continue;
99 }
100
101 for neighbor_id in graph.neighbor_ids(¤t) {
102 edges.push((current.clone(), neighbor_id.clone()));
103
104 if !visited.contains(&neighbor_id) {
105 visited.insert(neighbor_id.clone());
106 stack.push((neighbor_id, current_depth + 1));
107 }
108 }
109 }
110
111 let visited_vec: Vec<String> = visited.into_iter().collect();
112 (visited_vec, edges)
113}
114
115pub fn subgraph_to_text(
120 graph: &KnowledgeGraph,
121 nodes: &[String],
122 edges: &[(String, String)],
123 token_budget: usize,
124) -> String {
125 let char_budget = token_budget * 4;
126 let mut output = String::with_capacity(char_budget.min(64 * 1024));
127
128 output.push_str(&format!(
129 "=== Knowledge Graph Context ({} nodes, {} edges) ===\n\n",
130 nodes.len(),
131 edges.len()
132 ));
133
134 output.push_str("## Nodes\n\n");
135 for node_id in nodes {
136 if output.len() >= char_budget {
137 output.push_str("\n... (truncated due to token budget)\n");
138 break;
139 }
140
141 if let Some(node) = graph.get_node(node_id) {
142 output.push_str(&format!(
143 "- **{}** [{}] (type: {:?}",
144 node.label, node.id, node.node_type
145 ));
146 if let Some(community) = node.community {
147 output.push_str(&format!(", community: {}", community));
148 }
149 output.push_str(&format!(", file: {})\n", node.source_file));
150 }
151 }
152
153 if output.len() < char_budget {
154 output.push_str("\n## Relationships\n\n");
155
156 let mut seen: HashSet<(&str, &str)> = HashSet::new();
157 for (src, tgt) in edges {
158 if output.len() >= char_budget {
159 output.push_str("\n... (truncated due to token budget)\n");
160 break;
161 }
162
163 if seen.insert((src.as_str(), tgt.as_str())) {
164 let src_label = graph.get_node(src).map(|n| n.label.as_str()).unwrap_or(src);
165 let tgt_label = graph.get_node(tgt).map(|n| n.label.as_str()).unwrap_or(tgt);
166 output.push_str(&format!("- {} -> {}\n", src_label, tgt_label));
167 }
168 }
169 }
170
171 output
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum SummaryLevel {
177 Detailed,
179 Community,
181 Architecture,
183}
184
185pub fn smart_summary(
191 graph: &KnowledgeGraph,
192 communities: &HashMap<usize, Vec<String>>,
193 level: SummaryLevel,
194 token_budget: usize,
195) -> String {
196 match level {
197 SummaryLevel::Detailed => {
198 let all_nodes = graph.node_ids();
199 let all_edges: Vec<(String, String)> = graph
200 .edges_with_endpoints()
201 .iter()
202 .map(|(s, t, _)| (s.to_string(), t.to_string()))
203 .collect();
204 subgraph_to_text(graph, &all_nodes, &all_edges, token_budget)
205 }
206 SummaryLevel::Community => community_level_summary(graph, communities, token_budget),
207 SummaryLevel::Architecture => architecture_level_summary(graph, token_budget),
208 }
209}
210
211fn community_level_summary(
213 graph: &KnowledgeGraph,
214 communities: &HashMap<usize, Vec<String>>,
215 token_budget: usize,
216) -> String {
217 let char_budget = token_budget * 4;
218 let mut output = String::with_capacity(char_budget.min(64 * 1024));
219
220 let mut representatives: HashMap<usize, (&str, usize)> = HashMap::new();
221 for (&cid, members) in communities {
222 let best = members
223 .iter()
224 .map(|id| (id.as_str(), graph.degree(id)))
225 .max_by_key(|(_, d)| *d)
226 .unwrap_or(("", 0));
227 representatives.insert(cid, best);
228 }
229
230 output.push_str(&format!(
231 "=== Architecture Summary ({} communities, {} total nodes) ===\n\n",
232 communities.len(),
233 graph.node_count()
234 ));
235
236 let mut node_cid: HashMap<&str, usize> = HashMap::new();
237 for (&cid, members) in communities {
238 for m in members {
239 node_cid.insert(m.as_str(), cid);
240 }
241 }
242
243 output.push_str("## Communities\n\n");
244 let mut sorted_cids: Vec<usize> = communities.keys().copied().collect();
245 sorted_cids.sort();
246 for cid in &sorted_cids {
247 if output.len() >= char_budget {
248 output.push_str("\n... (truncated)\n");
249 break;
250 }
251 let members = &communities[cid];
252 let (rep_id, rep_deg) = representatives[cid];
253 let rep_label = graph
254 .get_node(rep_id)
255 .map(|n| n.label.as_str())
256 .unwrap_or(rep_id);
257 output.push_str(&format!(
258 "- Community {} ({} nodes): representative **{}** (degree {})\n",
259 cid,
260 members.len(),
261 rep_label,
262 rep_deg
263 ));
264 }
265
266 output.push_str("\n## Cross-Community Dependencies\n\n");
267 let mut cross_edges: HashMap<(usize, usize), usize> = HashMap::new();
268 for (src, tgt, _) in graph.edges_with_endpoints() {
269 let sc = node_cid.get(src).copied().unwrap_or(usize::MAX);
270 let tc = node_cid.get(tgt).copied().unwrap_or(usize::MAX);
271 if sc != tc && sc != usize::MAX && tc != usize::MAX {
272 let key = if sc < tc { (sc, tc) } else { (tc, sc) };
273 *cross_edges.entry(key).or_default() += 1;
274 }
275 }
276 let mut sorted_cross: Vec<_> = cross_edges.into_iter().collect();
277 sorted_cross.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
278 for ((c1, c2), count) in sorted_cross.iter().take(20) {
279 if output.len() >= char_budget {
280 break;
281 }
282 output.push_str(&format!(
283 "- Community {} ↔ Community {}: {} edges\n",
284 c1, c2, count
285 ));
286 }
287
288 output
289}
290
291fn architecture_level_summary(graph: &KnowledgeGraph, token_budget: usize) -> String {
293 let char_budget = token_budget * 4;
294 let mut output = String::with_capacity(char_budget.min(64 * 1024));
295
296 let mut dir_nodes: HashMap<String, Vec<&str>> = HashMap::new();
297 for node in graph.nodes() {
298 let dir = std::path::Path::new(&node.source_file)
299 .parent()
300 .and_then(|p| p.to_str())
301 .unwrap_or(".")
302 .to_string();
303 dir_nodes.entry(dir).or_default().push(&node.id);
304 }
305
306 let mut node_dir: HashMap<&str, &str> = HashMap::new();
307 for (dir, nodes) in &dir_nodes {
308 for &nid in nodes {
309 node_dir.insert(nid, dir.as_str());
310 }
311 }
312
313 output.push_str(&format!(
314 "=== Architecture Overview ({} packages/directories) ===\n\n",
315 dir_nodes.len()
316 ));
317
318 output.push_str("## Packages\n\n");
319 let mut sorted_dirs: Vec<_> = dir_nodes.iter().collect();
320 sorted_dirs.sort_by_key(|(_, nodes)| std::cmp::Reverse(nodes.len()));
321 for (dir, nodes) in sorted_dirs.iter().take(30) {
322 if output.len() >= char_budget {
323 output.push_str("\n... (truncated)\n");
324 break;
325 }
326 output.push_str(&format!("- **{}** ({} entities)\n", dir, nodes.len()));
327 }
328
329 output.push_str("\n## Dependencies\n\n");
330 let mut dir_edges: HashMap<(&str, &str), usize> = HashMap::new();
331 for (src, tgt, _) in graph.edges_with_endpoints() {
332 let sd = node_dir.get(src).copied().unwrap_or("?");
333 let td = node_dir.get(tgt).copied().unwrap_or("?");
334 if sd != td {
335 let key = if sd < td { (sd, td) } else { (td, sd) };
336 *dir_edges.entry(key).or_default() += 1;
337 }
338 }
339 let mut sorted_deps: Vec<_> = dir_edges.into_iter().collect();
340 sorted_deps.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
341 for ((d1, d2), count) in sorted_deps.iter().take(20) {
342 if output.len() >= char_budget {
343 break;
344 }
345 output.push_str(&format!("- {} → {}: {} edges\n", d1, d2, count));
346 }
347
348 output
349}
350
351pub fn load_graph(graph_path: &Path) -> Result<KnowledgeGraph, ServeError> {
353 let content = std::fs::read_to_string(graph_path)?;
354 let value: Value = serde_json::from_str(&content)?;
355 KnowledgeGraph::from_node_link_json(&value).map_err(|e| ServeError::GraphLoad(e.to_string()))
356}
357
358pub fn graph_stats(graph: &KnowledgeGraph) -> HashMap<String, Value> {
360 let mut stats = HashMap::new();
361 stats.insert("node_count".to_string(), Value::from(graph.node_count()));
362 stats.insert("edge_count".to_string(), Value::from(graph.edge_count()));
363 stats.insert(
364 "community_count".to_string(),
365 Value::from(graph.communities.len()),
366 );
367
368 let node_ids = graph.node_ids();
369 if !node_ids.is_empty() {
370 let degrees: Vec<usize> = node_ids.iter().map(|id| graph.degree(id)).collect();
371 let max_degree = degrees.iter().copied().max().unwrap_or(0);
372 let avg_degree = degrees.iter().sum::<usize>() as f64 / degrees.len() as f64;
373 stats.insert("max_degree".to_string(), Value::from(max_degree));
374 stats.insert(
375 "avg_degree".to_string(),
376 Value::from(format!("{:.2}", avg_degree)),
377 );
378 }
379
380 stats
381}
382
383pub async fn start_server(graph_path: &Path) -> Result<(), ServeError> {
388 let path = graph_path.to_path_buf();
389 tokio::task::spawn_blocking(move || mcp::run_mcp_server(&path))
390 .await
391 .map_err(|e| ServeError::Io(std::io::Error::other(e)))??;
392 Ok(())
393}
394
395pub fn all_simple_paths(
400 graph: &KnowledgeGraph,
401 source: &str,
402 target: &str,
403 max_length: usize,
404) -> Vec<Vec<String>> {
405 const MAX_PATHS: usize = 50;
406 let mut result: Vec<Vec<String>> = Vec::new();
407 let mut stack: Vec<(String, Vec<String>)> = Vec::new();
408
409 if graph.get_node(source).is_none() || graph.get_node(target).is_none() {
410 return result;
411 }
412
413 stack.push((source.to_string(), vec![source.to_string()]));
414
415 while let Some((current, path)) = stack.pop() {
416 if result.len() >= MAX_PATHS {
417 break;
418 }
419 if current == target && path.len() > 1 {
420 result.push(path);
421 continue;
422 }
423 if path.len() > max_length + 1 {
424 continue;
425 }
426
427 for neighbor_id in graph.neighbor_ids(¤t) {
428 if !path.contains(&neighbor_id) {
429 let mut new_path = path.clone();
430 new_path.push(neighbor_id.clone());
431 stack.push((neighbor_id, new_path));
432 }
433 }
434 }
435
436 result.sort_by_key(|p| p.len());
437 result
438}
439
440pub type EdgeDetail = (String, String, f64, String);
442
443pub fn dijkstra_path(
449 graph: &KnowledgeGraph,
450 source: &str,
451 target: &str,
452 min_confidence: f64,
453) -> Option<(Vec<String>, f64, Vec<EdgeDetail>)> {
454 use std::cmp::Ordering;
455 use std::collections::BinaryHeap;
456
457 if graph.get_node(source).is_none() || graph.get_node(target).is_none() {
458 return None;
459 }
460 if source == target {
461 return Some((vec![source.to_string()], 0.0, Vec::new()));
462 }
463
464 let mut adj: HashMap<String, Vec<(String, f64, String)>> = HashMap::new();
465 for (src, tgt, edge) in graph.edges_with_endpoints() {
466 if edge.confidence_score < min_confidence {
467 continue;
468 }
469 let cost = if edge.weight > 0.0 {
470 1.0 / edge.weight
471 } else {
472 f64::MAX
473 };
474 adj.entry(src.to_string()).or_default().push((
475 tgt.to_string(),
476 cost,
477 edge.relation.clone(),
478 ));
479 adj.entry(tgt.to_string()).or_default().push((
480 src.to_string(),
481 cost,
482 edge.relation.clone(),
483 ));
484 }
485
486 #[derive(PartialEq)]
487 struct State {
488 cost: f64,
489 node: String,
490 }
491 impl Eq for State {}
492 impl PartialOrd for State {
493 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
494 Some(self.cmp(other))
495 }
496 }
497 impl Ord for State {
498 fn cmp(&self, other: &Self) -> Ordering {
499 other
500 .cost
501 .partial_cmp(&self.cost)
502 .unwrap_or(Ordering::Equal)
503 }
504 }
505
506 let mut dist: HashMap<String, f64> = HashMap::new();
507 let mut prev: HashMap<String, (String, f64, String)> = HashMap::new();
508 let mut heap = BinaryHeap::new();
509
510 dist.insert(source.to_string(), 0.0);
511 heap.push(State {
512 cost: 0.0,
513 node: source.to_string(),
514 });
515
516 while let Some(State { cost, node }) = heap.pop() {
517 if node == target {
518 break;
519 }
520 if cost > *dist.get(&node).unwrap_or(&f64::MAX) {
521 continue;
522 }
523 if let Some(neighbors) = adj.get(&node) {
524 for (next, edge_cost, relation) in neighbors {
525 let new_cost = cost + edge_cost;
526 if new_cost < *dist.get(next).unwrap_or(&f64::MAX) {
527 dist.insert(next.clone(), new_cost);
528 prev.insert(next.clone(), (node.clone(), *edge_cost, relation.clone()));
529 heap.push(State {
530 cost: new_cost,
531 node: next.clone(),
532 });
533 }
534 }
535 }
536 }
537
538 if !prev.contains_key(target) {
539 return None;
540 }
541
542 let mut path = vec![target.to_string()];
543 let mut edge_details: Vec<(String, String, f64, String)> = Vec::new();
544 let mut current = target.to_string();
545 while let Some((from, cost, relation)) = prev.get(¤t) {
546 edge_details.push((from.clone(), current.clone(), *cost, relation.clone()));
547 path.push(from.clone());
548 current = from.clone();
549 }
550 path.reverse();
551 edge_details.reverse();
552
553 let total_cost = *dist.get(target).unwrap_or(&f64::MAX);
554 Some((path, total_cost, edge_details))
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use graphify_core::confidence::Confidence;
561 use graphify_core::model::{GraphEdge, GraphNode, NodeType};
562
563 fn make_node(id: &str, label: &str) -> GraphNode {
564 GraphNode {
565 id: id.into(),
566 label: label.into(),
567 source_file: "test.rs".into(),
568 source_location: None,
569 node_type: NodeType::Class,
570 community: None,
571 extra: HashMap::new(),
572 }
573 }
574
575 fn make_edge(src: &str, tgt: &str) -> GraphEdge {
576 GraphEdge {
577 source: src.into(),
578 target: tgt.into(),
579 relation: "calls".into(),
580 confidence: Confidence::Extracted,
581 confidence_score: 1.0,
582 source_file: "test.rs".into(),
583 source_location: None,
584 weight: 1.0,
585 provenance: None,
586 extra: HashMap::new(),
587 }
588 }
589
590 fn make_test_graph() -> KnowledgeGraph {
591 let mut g = KnowledgeGraph::new();
592 g.add_node(make_node("auth", "AuthService")).unwrap();
593 g.add_node(make_node("user", "UserManager")).unwrap();
594 g.add_node(make_node("db", "Database")).unwrap();
595 g.add_node(make_node("cache", "CacheLayer")).unwrap();
596 g.add_edge(make_edge("auth", "user")).unwrap();
597 g.add_edge(make_edge("auth", "db")).unwrap();
598 g.add_edge(make_edge("user", "db")).unwrap();
599 g.add_edge(make_edge("user", "cache")).unwrap();
600 g
601 }
602
603 #[test]
604 fn test_score_nodes_basic() {
605 let g = make_test_graph();
606 let results = score_nodes(&g, &["auth".to_string()]);
607 assert!(!results.is_empty());
608 let top_id = &results[0].1;
609 assert_eq!(top_id, "auth");
610 }
611
612 #[test]
613 fn test_score_nodes_no_match() {
614 let g = make_test_graph();
615 let results = score_nodes(&g, &["nonexistent".to_string()]);
616 assert!(results.is_empty());
617 }
618
619 #[test]
620 fn test_score_nodes_multiple_terms() {
621 let g = make_test_graph();
622 let results = score_nodes(&g, &["user".to_string(), "manager".to_string()]);
623 assert!(!results.is_empty());
624 assert!(results.iter().any(|(_, id)| id == "user"));
625 }
626
627 #[test]
628 fn test_bfs_depth_0() {
629 let g = make_test_graph();
630 let (nodes, edges) = bfs(&g, &["auth".to_string()], 0);
631 assert_eq!(nodes.len(), 1);
632 assert!(edges.is_empty());
633 }
634
635 #[test]
636 fn test_bfs_depth_1() {
637 let g = make_test_graph();
638 let (nodes, edges) = bfs(&g, &["auth".to_string()], 1);
639 assert!(nodes.len() >= 3); assert!(!edges.is_empty());
641 }
642
643 #[test]
644 fn test_bfs_depth_2() {
645 let g = make_test_graph();
646 let (nodes, _edges) = bfs(&g, &["auth".to_string()], 2);
647 assert_eq!(nodes.len(), 4);
648 }
649
650 #[test]
651 fn test_dfs_depth_1() {
652 let g = make_test_graph();
653 let (nodes, edges) = dfs(&g, &["auth".to_string()], 1);
654 assert!(nodes.len() >= 3);
655 assert!(!edges.is_empty());
656 }
657
658 #[test]
659 fn test_bfs_nonexistent_start() {
660 let g = make_test_graph();
661 let (nodes, edges) = bfs(&g, &["nonexistent".to_string()], 3);
662 assert!(nodes.is_empty());
663 assert!(edges.is_empty());
664 }
665
666 #[test]
667 fn test_subgraph_to_text() {
668 let g = make_test_graph();
669 let nodes = vec!["auth".to_string(), "user".to_string()];
670 let edges = vec![("auth".to_string(), "user".to_string())];
671 let text = subgraph_to_text(&g, &nodes, &edges, 1000);
672
673 assert!(text.contains("Knowledge Graph Context"));
674 assert!(text.contains("AuthService"));
675 assert!(text.contains("UserManager"));
676 assert!(text.contains("Relationships"));
677 }
678
679 #[test]
680 fn test_subgraph_to_text_budget() {
681 let g = make_test_graph();
682 let nodes: Vec<String> = g.node_ids();
683 let edges = vec![
684 ("auth".to_string(), "user".to_string()),
685 ("auth".to_string(), "db".to_string()),
686 ];
687 let text = subgraph_to_text(&g, &nodes, &edges, 10);
688 assert!(text.contains("truncated") || text.len() < 200);
689 }
690
691 #[test]
692 fn test_graph_stats() {
693 let g = make_test_graph();
694 let stats = graph_stats(&g);
695 assert_eq!(stats["node_count"], 4);
696 assert_eq!(stats["edge_count"], 4);
697 }
698
699 #[test]
700 fn test_bfs_multiple_starts() {
701 let g = make_test_graph();
702 let (nodes, _) = bfs(&g, &["auth".to_string(), "cache".to_string()], 1);
703 assert!(nodes.len() >= 4);
704 }
705
706 #[test]
707 fn test_all_simple_paths_direct() {
708 let g = make_test_graph();
709 let paths = all_simple_paths(&g, "auth", "user", 4);
710 assert!(!paths.is_empty());
711 assert!(paths.iter().any(|p| p.len() == 2));
712 }
713
714 #[test]
715 fn test_all_simple_paths_indirect() {
716 let g = make_test_graph();
717 let paths = all_simple_paths(&g, "auth", "db", 4);
718 assert!(
719 paths.len() >= 2,
720 "should find multiple paths, got {}",
721 paths.len()
722 );
723 }
724
725 #[test]
726 fn test_all_simple_paths_no_path() {
727 let mut g = KnowledgeGraph::new();
728 g.add_node(make_node("a", "A")).unwrap();
729 g.add_node(make_node("b", "B")).unwrap();
730 let paths = all_simple_paths(&g, "a", "b", 4);
731 assert!(paths.is_empty());
732 }
733
734 #[test]
735 fn test_all_simple_paths_nonexistent_node() {
736 let g = make_test_graph();
737 let paths = all_simple_paths(&g, "auth", "nonexistent", 4);
738 assert!(paths.is_empty());
739 }
740
741 #[test]
742 fn test_all_simple_paths_sorted_by_length() {
743 let g = make_test_graph();
744 let paths = all_simple_paths(&g, "auth", "cache", 5);
745 for w in paths.windows(2) {
746 assert!(w[0].len() <= w[1].len(), "paths should be sorted by length");
747 }
748 }
749
750 #[test]
751 fn test_dijkstra_direct_path() {
752 let g = make_test_graph();
753 let result = dijkstra_path(&g, "auth", "user", 0.0);
754 assert!(result.is_some());
755 let (path, cost, edges) = result.unwrap();
756 assert_eq!(path.first().unwrap(), "auth");
757 assert_eq!(path.last().unwrap(), "user");
758 assert!(cost > 0.0);
759 assert!(!edges.is_empty());
760 }
761
762 #[test]
763 fn test_dijkstra_same_node() {
764 let g = make_test_graph();
765 let result = dijkstra_path(&g, "auth", "auth", 0.0);
766 assert!(result.is_some());
767 let (path, cost, _) = result.unwrap();
768 assert_eq!(path.len(), 1);
769 assert!((cost - 0.0).abs() < f64::EPSILON);
770 }
771
772 #[test]
773 fn test_dijkstra_no_path() {
774 let mut g = KnowledgeGraph::new();
775 g.add_node(make_node("a", "A")).unwrap();
776 g.add_node(make_node("b", "B")).unwrap();
777 let result = dijkstra_path(&g, "a", "b", 0.0);
778 assert!(result.is_none());
779 }
780
781 #[test]
782 fn test_dijkstra_nonexistent_node() {
783 let g = make_test_graph();
784 assert!(dijkstra_path(&g, "auth", "nonexistent", 0.0).is_none());
785 }
786
787 #[test]
788 fn test_dijkstra_min_confidence_filter() {
789 let mut g = KnowledgeGraph::new();
790 g.add_node(make_node("a", "A")).unwrap();
791 g.add_node(make_node("b", "B")).unwrap();
792 g.add_node(make_node("c", "C")).unwrap();
793
794 let mut low_edge = make_edge("a", "b");
795 low_edge.confidence_score = 0.3;
796 g.add_edge(low_edge).unwrap();
797
798 let mut high1 = make_edge("a", "c");
799 high1.confidence_score = 1.0;
800 g.add_edge(high1).unwrap();
801
802 let mut high2 = make_edge("c", "b");
803 high2.confidence_score = 1.0;
804 g.add_edge(high2).unwrap();
805
806 let result = dijkstra_path(&g, "a", "b", 0.5);
807 assert!(result.is_some());
808 let (path, _, _) = result.unwrap();
809 assert_eq!(path.len(), 3, "should go through c, got path: {path:?}");
810 }
811}