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