use rand::{Rng, SeedableRng}; use rand::rngs::StdRng; use crate::models::{GraphData, GraphEdge, GraphNode, Task}; /// Build graph data from a list of tasks. /// Nodes = tasks, edges = ~30% of all pairs, deterministic seed from task IDs. pub fn build_graph(tasks: &[Task]) -> GraphData { let nodes: Vec = tasks .iter() .map(|t| GraphNode { id: t.id.clone(), label: t.title.clone(), completed: t.completed, }) .collect(); let mut edges = Vec::new(); if tasks.len() < 2 { return GraphData { nodes, edges }; } // Derive a deterministic seed from all task IDs concatenated let seed_str: String = tasks.iter().map(|t| t.id.as_str()).collect::>().join(""); let seed: u64 = seed_str .bytes() .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64)); let mut rng = StdRng::seed_from_u64(seed); for i in 0..tasks.len() { for j in (i + 1)..tasks.len() { // ~30% chance of an edge if rng.gen::() < 0.30 { let weight = rng.gen_range(0.1f32..1.0f32); edges.push(GraphEdge { source: tasks[i].id.clone(), target: tasks[j].id.clone(), weight, }); } } } GraphData { nodes, edges } } #[cfg(test)] mod tests { use super::*; fn make_tasks(n: usize) -> Vec { (0..n) .map(|i| Task { id: format!("task-{i:04}"), title: format!("Task {i}"), description: None, completed: i % 2 == 0, created_at: i as i64, }) .collect() } #[test] fn test_graph_node_count_matches_tasks() { let tasks = make_tasks(10); let graph = build_graph(&tasks); assert_eq!(graph.nodes.len(), 10); } #[test] fn test_graph_edge_count_reasonable() { let tasks = make_tasks(10); let graph = build_graph(&tasks); // 10 tasks => 45 possible pairs. ~30% => roughly 5..20 edges assert!(graph.edges.len() <= 45); // At least some edges should exist with 10 tasks // (statistically overwhelmingly likely, but we just check <= max) } #[test] fn test_graph_deterministic() { let tasks = make_tasks(8); let g1 = build_graph(&tasks); let g2 = build_graph(&tasks); assert_eq!(g1.edges.len(), g2.edges.len()); for (e1, e2) in g1.edges.iter().zip(g2.edges.iter()) { assert_eq!(e1.source, e2.source); assert_eq!(e1.target, e2.target); } } #[test] fn test_empty_tasks() { let graph = build_graph(&[]); assert!(graph.nodes.is_empty()); assert!(graph.edges.is_empty()); } #[test] fn test_single_task_no_edges() { let tasks = make_tasks(1); let graph = build_graph(&tasks); assert_eq!(graph.nodes.len(), 1); assert!(graph.edges.is_empty()); } #[test] fn test_edge_weights_in_range() { let tasks = make_tasks(10); let graph = build_graph(&tasks); for edge in &graph.edges { assert!(edge.weight >= 0.1 && edge.weight < 1.0); } } }