Skip to main content

mockforge_data/
persona_graph.rs

1//! Persona Graph & Relationship Management
2//!
3//! This module provides graph-based relationship management for personas,
4//! enabling coherent persona switching across related entities (user → orders → payments → support tickets).
5
6use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::sync::Arc;
10
11/// Represents a node in the persona graph
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PersonaNode {
14    /// Persona ID
15    pub persona_id: String,
16    /// Entity type (e.g., "user", "order", "payment", "support_ticket")
17    pub entity_type: String,
18    /// Relationships from this persona to others
19    /// Key: relationship type (e.g., "has_orders", "has_payments")
20    /// Value: List of related persona IDs
21    pub relationships: HashMap<String, Vec<String>>,
22    /// Additional metadata for the node
23    #[serde(default)]
24    pub metadata: HashMap<String, serde_json::Value>,
25}
26
27impl PersonaNode {
28    /// Create a new persona node
29    pub fn new(persona_id: String, entity_type: String) -> Self {
30        Self {
31            persona_id,
32            entity_type,
33            relationships: HashMap::new(),
34            metadata: HashMap::new(),
35        }
36    }
37
38    /// Add a relationship to another persona
39    pub fn add_relationship(&mut self, relationship_type: String, related_persona_id: String) {
40        self.relationships
41            .entry(relationship_type)
42            .or_default()
43            .push(related_persona_id);
44    }
45
46    /// Get all related personas for a relationship type
47    pub fn get_related(&self, relationship_type: &str) -> Vec<String> {
48        self.relationships.get(relationship_type).cloned().unwrap_or_default()
49    }
50
51    /// Get all relationship types for this node
52    pub fn get_relationship_types(&self) -> Vec<String> {
53        self.relationships.keys().cloned().collect()
54    }
55}
56
57/// Edge in the persona graph
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Edge {
60    /// Source persona ID
61    pub from: String,
62    /// Target persona ID
63    pub to: String,
64    /// Relationship type
65    pub relationship_type: String,
66    /// Edge weight (for weighted traversals, default 1.0)
67    #[serde(default = "default_edge_weight")]
68    pub weight: f64,
69}
70
71fn default_edge_weight() -> f64 {
72    1.0
73}
74
75/// Persona graph for managing entity relationships
76///
77/// Maintains a graph structure of personas and their relationships,
78/// enabling coherent persona switching across related entities.
79#[derive(Debug, Clone)]
80pub struct PersonaGraph {
81    /// Graph nodes indexed by persona ID
82    nodes: Arc<RwLock<HashMap<String, PersonaNode>>>,
83    /// Graph edges indexed by source persona ID
84    edges: Arc<RwLock<HashMap<String, Vec<Edge>>>>,
85    /// Reverse edges for efficient backward traversal
86    reverse_edges: Arc<RwLock<HashMap<String, Vec<Edge>>>>,
87}
88
89impl PersonaGraph {
90    /// Create a new empty persona graph
91    pub fn new() -> Self {
92        Self {
93            nodes: Arc::new(RwLock::new(HashMap::new())),
94            edges: Arc::new(RwLock::new(HashMap::new())),
95            reverse_edges: Arc::new(RwLock::new(HashMap::new())),
96        }
97    }
98
99    /// Add a persona node to the graph
100    pub fn add_node(&self, node: PersonaNode) {
101        let mut nodes = self.nodes.write();
102        nodes.insert(node.persona_id.clone(), node);
103    }
104
105    /// Get a node by persona ID
106    pub fn get_node(&self, persona_id: &str) -> Option<PersonaNode> {
107        let nodes = self.nodes.read();
108        nodes.get(persona_id).cloned()
109    }
110
111    /// Add an edge between two personas
112    pub fn add_edge(&self, from: String, to: String, relationship_type: String) {
113        let to_clone = to.clone();
114        let edge = Edge {
115            from: from.clone(),
116            to: to_clone.clone(),
117            relationship_type: relationship_type.clone(),
118            weight: 1.0,
119        };
120
121        // Add forward edge
122        let mut edges = self.edges.write();
123        edges.entry(from.clone()).or_default().push(edge.clone());
124
125        // Add reverse edge
126        let mut reverse_edges = self.reverse_edges.write();
127        reverse_edges.entry(to_clone.clone()).or_default().push(edge);
128
129        // Update node relationships
130        if let Some(node) = self.get_node(&from) {
131            let mut updated_node = node;
132            updated_node.add_relationship(relationship_type, to_clone);
133            self.add_node(updated_node);
134        }
135    }
136
137    /// Get all edges from a persona
138    pub fn get_edges_from(&self, persona_id: &str) -> Vec<Edge> {
139        let edges = self.edges.read();
140        edges.get(persona_id).cloned().unwrap_or_default()
141    }
142
143    /// Get all edges to a persona
144    pub fn get_edges_to(&self, persona_id: &str) -> Vec<Edge> {
145        let reverse_edges = self.reverse_edges.read();
146        reverse_edges.get(persona_id).cloned().unwrap_or_default()
147    }
148
149    /// Find all related personas using BFS traversal
150    ///
151    /// Traverses the graph starting from the given persona ID,
152    /// following relationships of the specified types.
153    ///
154    /// # Arguments
155    /// * `start_persona_id` - Starting persona ID
156    /// * `relationship_types` - Optional filter for relationship types to follow
157    /// * `max_depth` - Maximum traversal depth (None = unlimited)
158    ///
159    /// # Returns
160    /// Vector of persona IDs reachable from the start persona
161    pub fn find_related_bfs(
162        &self,
163        start_persona_id: &str,
164        relationship_types: Option<&[String]>,
165        max_depth: Option<usize>,
166    ) -> Vec<String> {
167        let mut visited = HashSet::new();
168        let mut queue = VecDeque::new();
169        let mut result = Vec::new();
170
171        queue.push_back((start_persona_id.to_string(), 0));
172        visited.insert(start_persona_id.to_string());
173
174        while let Some((current_id, depth)) = queue.pop_front() {
175            if let Some(max) = max_depth {
176                if depth >= max {
177                    continue;
178                }
179            }
180
181            let edges = self.get_edges_from(&current_id);
182            for edge in edges {
183                // Filter by relationship type if specified
184                if let Some(types) = relationship_types {
185                    if !types.contains(&edge.relationship_type) {
186                        continue;
187                    }
188                }
189
190                if !visited.contains(&edge.to) {
191                    visited.insert(edge.to.clone());
192                    result.push(edge.to.clone());
193                    queue.push_back((edge.to.clone(), depth + 1));
194                }
195            }
196        }
197
198        result
199    }
200
201    /// Find all related personas using DFS traversal
202    ///
203    /// Traverses the graph starting from the given persona ID,
204    /// following relationships of the specified types.
205    ///
206    /// # Arguments
207    /// * `start_persona_id` - Starting persona ID
208    /// * `relationship_types` - Optional filter for relationship types to follow
209    /// * `max_depth` - Maximum traversal depth (None = unlimited)
210    ///
211    /// # Returns
212    /// Vector of persona IDs reachable from the start persona
213    pub fn find_related_dfs(
214        &self,
215        start_persona_id: &str,
216        relationship_types: Option<&[String]>,
217        max_depth: Option<usize>,
218    ) -> Vec<String> {
219        let mut visited = HashSet::new();
220        let mut result = Vec::new();
221
222        self.dfs_recursive(
223            start_persona_id,
224            relationship_types,
225            max_depth,
226            0,
227            &mut visited,
228            &mut result,
229        );
230
231        result
232    }
233
234    /// Recursive helper for DFS traversal
235    fn dfs_recursive(
236        &self,
237        current_id: &str,
238        relationship_types: Option<&[String]>,
239        max_depth: Option<usize>,
240        current_depth: usize,
241        visited: &mut HashSet<String>,
242        result: &mut Vec<String>,
243    ) {
244        if visited.contains(current_id) {
245            return;
246        }
247
248        if let Some(max) = max_depth {
249            if current_depth >= max {
250                return;
251            }
252        }
253
254        visited.insert(current_id.to_string());
255        if current_depth > 0 {
256            // Don't include the start node in results
257            result.push(current_id.to_string());
258        }
259
260        let edges = self.get_edges_from(current_id);
261        for edge in edges {
262            // Filter by relationship type if specified
263            if let Some(types) = relationship_types {
264                if !types.contains(&edge.relationship_type) {
265                    continue;
266                }
267            }
268
269            self.dfs_recursive(
270                &edge.to,
271                relationship_types,
272                max_depth,
273                current_depth + 1,
274                visited,
275                result,
276            );
277        }
278    }
279
280    /// Get the entire subgraph starting from a persona
281    ///
282    /// Returns all nodes and edges reachable from the start persona.
283    pub fn get_subgraph(&self, start_persona_id: &str) -> (Vec<PersonaNode>, Vec<Edge>) {
284        let related_ids = self.find_related_bfs(start_persona_id, None, None);
285        let mut all_ids = vec![start_persona_id.to_string()];
286        all_ids.extend(related_ids);
287
288        let nodes = self.nodes.read();
289        let edges = self.edges.read();
290
291        let subgraph_nodes: Vec<PersonaNode> =
292            all_ids.iter().filter_map(|id| nodes.get(id).cloned()).collect();
293
294        let subgraph_edges: Vec<Edge> = all_ids
295            .iter()
296            .flat_map(|id| edges.get(id).cloned().unwrap_or_default())
297            .filter(|edge| all_ids.contains(&edge.to))
298            .collect();
299
300        (subgraph_nodes, subgraph_edges)
301    }
302
303    /// Get all nodes in the graph
304    pub fn get_all_nodes(&self) -> Vec<PersonaNode> {
305        let nodes = self.nodes.read();
306        nodes.values().cloned().collect()
307    }
308
309    /// Remove a node and all its edges
310    pub fn remove_node(&self, persona_id: &str) {
311        let mut nodes = self.nodes.write();
312        nodes.remove(persona_id);
313
314        // Remove forward edges
315        let mut edges = self.edges.write();
316        edges.remove(persona_id);
317
318        // Remove reverse edges
319        let mut reverse_edges = self.reverse_edges.write();
320        reverse_edges.remove(persona_id);
321
322        // Remove edges pointing to this node
323        for edges_list in edges.values_mut() {
324            edges_list.retain(|e| e.to != persona_id);
325        }
326        for edges_list in reverse_edges.values_mut() {
327            edges_list.retain(|e| e.from != persona_id);
328        }
329    }
330
331    /// Clear the entire graph
332    pub fn clear(&self) {
333        let mut nodes = self.nodes.write();
334        nodes.clear();
335
336        let mut edges = self.edges.write();
337        edges.clear();
338
339        let mut reverse_edges = self.reverse_edges.write();
340        reverse_edges.clear();
341    }
342
343    /// Get graph statistics
344    pub fn get_stats(&self) -> GraphStats {
345        let nodes = self.nodes.read();
346        let edges = self.edges.read();
347
348        let mut relationship_type_counts = HashMap::new();
349        for edges_list in edges.values() {
350            for edge in edges_list {
351                *relationship_type_counts.entry(edge.relationship_type.clone()).or_insert(0) += 1;
352            }
353        }
354
355        GraphStats {
356            node_count: nodes.len(),
357            edge_count: edges.values().map(|e| e.len()).sum(),
358            relationship_types: relationship_type_counts,
359        }
360    }
361
362    /// Link personas across entity types automatically
363    ///
364    /// Creates relationships between personas based on common entity type patterns:
365    /// - user → has_orders → order
366    /// - user → has_accounts → account
367    /// - order → has_payments → payment
368    /// - user → has_webhooks → webhook
369    /// - user → has_tcp_messages → tcp_message
370    ///
371    /// # Arguments
372    /// * `from_persona_id` - Source persona ID
373    /// * `from_entity_type` - Source entity type (e.g., "user", "order")
374    /// * `to_persona_id` - Target persona ID
375    /// * `to_entity_type` - Target entity type (e.g., "order", "payment")
376    pub fn link_entity_types(
377        &self,
378        from_persona_id: &str,
379        from_entity_type: &str,
380        to_persona_id: &str,
381        to_entity_type: &str,
382    ) {
383        // Determine relationship type based on entity types
384        let relationship_type: String = match (from_entity_type, to_entity_type) {
385            ("user", "order") | ("user", "orders") => "has_orders".to_string(),
386            ("user", "account") | ("user", "accounts") => "has_accounts".to_string(),
387            ("user", "webhook") | ("user", "webhooks") => "has_webhooks".to_string(),
388            ("user", "tcp_message") | ("user", "tcp_messages") => "has_tcp_messages".to_string(),
389            ("order", "payment") | ("order", "payments") => "has_payments".to_string(),
390            ("account", "order") | ("account", "orders") => "has_orders".to_string(),
391            ("account", "payment") | ("account", "payments") => "has_payments".to_string(),
392            _ => {
393                // Generic relationship: from_entity_type -> to_entity_type
394                format!("has_{}", to_entity_type.to_lowercase().trim_end_matches('s'))
395            }
396        };
397
398        // Ensure both nodes exist
399        if self.get_node(from_persona_id).is_none() {
400            let node = PersonaNode::new(from_persona_id.to_string(), from_entity_type.to_string());
401            self.add_node(node);
402        }
403
404        if self.get_node(to_persona_id).is_none() {
405            let node = PersonaNode::new(to_persona_id.to_string(), to_entity_type.to_string());
406            self.add_node(node);
407        }
408
409        // Add the edge
410        self.add_edge(
411            from_persona_id.to_string(),
412            to_persona_id.to_string(),
413            relationship_type.to_string(),
414        );
415    }
416
417    /// Find all related personas of a specific entity type
418    ///
419    /// Traverses the graph to find all personas of the specified entity type
420    /// that are related to the starting persona.
421    ///
422    /// # Arguments
423    /// * `start_persona_id` - Starting persona ID
424    /// * `target_entity_type` - Entity type to find (e.g., "order", "payment")
425    /// * `relationship_type` - Optional relationship type filter (e.g., "has_orders")
426    ///
427    /// # Returns
428    /// Vector of persona IDs matching the criteria
429    pub fn find_related_by_entity_type(
430        &self,
431        start_persona_id: &str,
432        target_entity_type: &str,
433        relationship_type: Option<&str>,
434    ) -> Vec<String> {
435        let related_ids = if let Some(rel_type) = relationship_type {
436            let rel_types = vec![rel_type.to_string()];
437            self.find_related_bfs(start_persona_id, Some(&rel_types), Some(2))
438        } else {
439            self.find_related_bfs(start_persona_id, None, Some(2))
440        };
441
442        // Filter by entity type
443        related_ids
444            .into_iter()
445            .filter_map(|persona_id| {
446                if let Some(node) = self.get_node(&persona_id) {
447                    if node.entity_type.to_lowercase() == target_entity_type.to_lowercase() {
448                        Some(persona_id)
449                    } else {
450                        None
451                    }
452                } else {
453                    None
454                }
455            })
456            .collect()
457    }
458
459    /// Get or create a persona node and link it to related entities
460    ///
461    /// This is a convenience method that creates a persona node if it doesn't exist
462    /// and automatically establishes relationships based on entity type patterns.
463    ///
464    /// # Arguments
465    /// * `persona_id` - Persona ID
466    /// * `entity_type` - Entity type (e.g., "user", "order", "payment")
467    /// * `related_entity_id` - Optional related entity ID to link to
468    /// * `related_entity_type` - Optional related entity type
469    pub fn get_or_create_node_with_links(
470        &self,
471        persona_id: &str,
472        entity_type: &str,
473        related_entity_id: Option<&str>,
474        related_entity_type: Option<&str>,
475    ) -> PersonaNode {
476        // Get or create the node
477        let node = if let Some(existing) = self.get_node(persona_id) {
478            existing
479        } else {
480            let new_node = PersonaNode::new(persona_id.to_string(), entity_type.to_string());
481            self.add_node(new_node.clone());
482            new_node
483        };
484
485        // Link to related entity if provided
486        if let (Some(related_id), Some(related_type)) = (related_entity_id, related_entity_type) {
487            self.link_entity_types(persona_id, entity_type, related_id, related_type);
488        }
489
490        node
491    }
492}
493
494impl Default for PersonaGraph {
495    fn default() -> Self {
496        Self::new()
497    }
498}
499
500/// Graph statistics
501#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct GraphStats {
503    /// Number of nodes in the graph
504    pub node_count: usize,
505    /// Number of edges in the graph
506    pub edge_count: usize,
507    /// Count of edges by relationship type
508    pub relationship_types: HashMap<String, usize>,
509}
510
511/// Graph visualization data structure
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct GraphVisualization {
514    /// Nodes in the graph
515    pub nodes: Vec<VisualizationNode>,
516    /// Edges in the graph
517    pub edges: Vec<VisualizationEdge>,
518}
519
520/// Node for visualization
521#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct VisualizationNode {
523    /// Persona ID
524    pub id: String,
525    /// Entity type
526    pub entity_type: String,
527    /// Display label
528    pub label: String,
529    /// Node position (for layout algorithms)
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub position: Option<(f64, f64)>,
532}
533
534/// Edge for visualization
535#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct VisualizationEdge {
537    /// Source persona ID
538    pub from: String,
539    /// Target persona ID
540    pub to: String,
541    /// Relationship type
542    pub relationship_type: String,
543    /// Display label
544    pub label: String,
545}
546
547impl PersonaGraph {
548    /// Generate visualization data for the graph
549    pub fn to_visualization(&self) -> GraphVisualization {
550        let nodes = self.get_all_nodes();
551        let edges = self.edges.read();
552
553        let vis_nodes: Vec<VisualizationNode> = nodes
554            .iter()
555            .map(|node| VisualizationNode {
556                id: node.persona_id.clone(),
557                entity_type: node.entity_type.clone(),
558                label: format!("{} ({})", node.persona_id, node.entity_type),
559                position: None,
560            })
561            .collect();
562
563        let vis_edges: Vec<VisualizationEdge> = edges
564            .values()
565            .flatten()
566            .map(|edge| VisualizationEdge {
567                from: edge.from.clone(),
568                to: edge.to.clone(),
569                relationship_type: edge.relationship_type.clone(),
570                label: edge.relationship_type.clone(),
571            })
572            .collect();
573
574        GraphVisualization {
575            nodes: vis_nodes,
576            edges: vis_edges,
577        }
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    /// #759: parking_lot::RwLock does not poison. If a thread panics while holding
586    /// the write lock, subsequent operations on the graph must still succeed (the old
587    /// std::sync::RwLock would have poisoned and made every `.read()/.write().unwrap()`
588    /// panic-cascade).
589    #[test]
590    fn test_persona_graph_lock_not_poisoned_after_panic() {
591        let graph = PersonaGraph::new();
592        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
593
594        let graph_clone = graph.clone();
595        let handle = std::thread::spawn(move || {
596            // Acquire the write lock and then panic while holding it.
597            let _guard = graph_clone.nodes.write();
598            panic!("intentional panic while holding the write lock");
599        });
600        assert!(handle.join().is_err(), "spawned thread should have panicked");
601
602        // With parking_lot the lock is NOT poisoned: these still work.
603        assert!(graph.get_node("user-1").is_some());
604        graph.add_node(PersonaNode::new("user-2".to_string(), "user".to_string()));
605        assert!(graph.get_node("user-2").is_some());
606    }
607
608    // =========================================================================
609    // PersonaNode tests
610    // =========================================================================
611
612    #[test]
613    fn test_persona_node_new() {
614        let node = PersonaNode::new("user-123".to_string(), "user".to_string());
615        assert_eq!(node.persona_id, "user-123");
616        assert_eq!(node.entity_type, "user");
617        assert!(node.relationships.is_empty());
618        assert!(node.metadata.is_empty());
619    }
620
621    #[test]
622    fn test_persona_node_add_relationship() {
623        let mut node = PersonaNode::new("user-123".to_string(), "user".to_string());
624        node.add_relationship("has_orders".to_string(), "order-1".to_string());
625        node.add_relationship("has_orders".to_string(), "order-2".to_string());
626
627        let related = node.get_related("has_orders");
628        assert_eq!(related.len(), 2);
629        assert!(related.contains(&"order-1".to_string()));
630        assert!(related.contains(&"order-2".to_string()));
631    }
632
633    #[test]
634    fn test_persona_node_get_related_empty() {
635        let node = PersonaNode::new("user-123".to_string(), "user".to_string());
636        let related = node.get_related("has_orders");
637        assert!(related.is_empty());
638    }
639
640    #[test]
641    fn test_persona_node_get_relationship_types() {
642        let mut node = PersonaNode::new("user-123".to_string(), "user".to_string());
643        node.add_relationship("has_orders".to_string(), "order-1".to_string());
644        node.add_relationship("has_payments".to_string(), "payment-1".to_string());
645
646        let types = node.get_relationship_types();
647        assert_eq!(types.len(), 2);
648        assert!(types.contains(&"has_orders".to_string()));
649        assert!(types.contains(&"has_payments".to_string()));
650    }
651
652    #[test]
653    fn test_persona_node_clone() {
654        let mut node = PersonaNode::new("user-123".to_string(), "user".to_string());
655        node.add_relationship("has_orders".to_string(), "order-1".to_string());
656
657        let cloned = node.clone();
658        assert_eq!(cloned.persona_id, node.persona_id);
659        assert_eq!(cloned.entity_type, node.entity_type);
660        assert_eq!(cloned.relationships, node.relationships);
661    }
662
663    #[test]
664    fn test_persona_node_debug() {
665        let node = PersonaNode::new("user-123".to_string(), "user".to_string());
666        let debug_str = format!("{:?}", node);
667        assert!(debug_str.contains("user-123"));
668        assert!(debug_str.contains("user"));
669    }
670
671    #[test]
672    fn test_persona_node_serialize_deserialize() {
673        let mut node = PersonaNode::new("user-123".to_string(), "user".to_string());
674        node.add_relationship("has_orders".to_string(), "order-1".to_string());
675
676        let json = serde_json::to_string(&node).unwrap();
677        let deserialized: PersonaNode = serde_json::from_str(&json).unwrap();
678
679        assert_eq!(deserialized.persona_id, "user-123");
680        assert_eq!(deserialized.entity_type, "user");
681    }
682
683    // =========================================================================
684    // Edge tests
685    // =========================================================================
686
687    #[test]
688    fn test_edge_creation() {
689        let edge = Edge {
690            from: "user-123".to_string(),
691            to: "order-456".to_string(),
692            relationship_type: "has_orders".to_string(),
693            weight: 1.0,
694        };
695        assert_eq!(edge.from, "user-123");
696        assert_eq!(edge.to, "order-456");
697        assert_eq!(edge.relationship_type, "has_orders");
698        assert!((edge.weight - 1.0).abs() < f64::EPSILON);
699    }
700
701    #[test]
702    fn test_edge_clone() {
703        let edge = Edge {
704            from: "a".to_string(),
705            to: "b".to_string(),
706            relationship_type: "rel".to_string(),
707            weight: 2.5,
708        };
709        let cloned = edge.clone();
710        assert_eq!(cloned.from, edge.from);
711        assert!((cloned.weight - 2.5).abs() < f64::EPSILON);
712    }
713
714    #[test]
715    fn test_edge_debug() {
716        let edge = Edge {
717            from: "a".to_string(),
718            to: "b".to_string(),
719            relationship_type: "rel".to_string(),
720            weight: 1.0,
721        };
722        let debug_str = format!("{:?}", edge);
723        assert!(debug_str.contains("from"));
724        assert!(debug_str.contains("to"));
725    }
726
727    #[test]
728    fn test_edge_serialize_default_weight() {
729        let edge = Edge {
730            from: "a".to_string(),
731            to: "b".to_string(),
732            relationship_type: "rel".to_string(),
733            weight: 1.0,
734        };
735        let json = serde_json::to_string(&edge).unwrap();
736        let deserialized: Edge = serde_json::from_str(&json).unwrap();
737        assert!((deserialized.weight - 1.0).abs() < f64::EPSILON);
738    }
739
740    // =========================================================================
741    // PersonaGraph tests
742    // =========================================================================
743
744    #[test]
745    fn test_persona_graph_new() {
746        let graph = PersonaGraph::new();
747        let stats = graph.get_stats();
748        assert_eq!(stats.node_count, 0);
749        assert_eq!(stats.edge_count, 0);
750    }
751
752    #[test]
753    fn test_persona_graph_default() {
754        let graph = PersonaGraph::default();
755        assert_eq!(graph.get_stats().node_count, 0);
756    }
757
758    #[test]
759    fn test_persona_graph_add_node() {
760        let graph = PersonaGraph::new();
761        let node = PersonaNode::new("user-123".to_string(), "user".to_string());
762        graph.add_node(node);
763
764        let stats = graph.get_stats();
765        assert_eq!(stats.node_count, 1);
766    }
767
768    #[test]
769    fn test_persona_graph_get_node() {
770        let graph = PersonaGraph::new();
771        let node = PersonaNode::new("user-123".to_string(), "user".to_string());
772        graph.add_node(node);
773
774        let retrieved = graph.get_node("user-123");
775        assert!(retrieved.is_some());
776        assert_eq!(retrieved.unwrap().persona_id, "user-123");
777    }
778
779    #[test]
780    fn test_persona_graph_get_node_not_found() {
781        let graph = PersonaGraph::new();
782        assert!(graph.get_node("nonexistent").is_none());
783    }
784
785    #[test]
786    fn test_persona_graph_add_edge() {
787        let graph = PersonaGraph::new();
788        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
789        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
790
791        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
792
793        let stats = graph.get_stats();
794        assert_eq!(stats.edge_count, 1);
795    }
796
797    #[test]
798    fn test_persona_graph_get_edges_from() {
799        let graph = PersonaGraph::new();
800        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
801        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
802        graph.add_node(PersonaNode::new("order-2".to_string(), "order".to_string()));
803
804        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
805        graph.add_edge("user-1".to_string(), "order-2".to_string(), "has_orders".to_string());
806
807        let edges = graph.get_edges_from("user-1");
808        assert_eq!(edges.len(), 2);
809    }
810
811    #[test]
812    fn test_persona_graph_get_edges_to() {
813        let graph = PersonaGraph::new();
814        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
815        graph.add_node(PersonaNode::new("user-2".to_string(), "user".to_string()));
816        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
817
818        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
819        graph.add_edge("user-2".to_string(), "order-1".to_string(), "has_orders".to_string());
820
821        let edges = graph.get_edges_to("order-1");
822        assert_eq!(edges.len(), 2);
823    }
824
825    #[test]
826    fn test_persona_graph_find_related_bfs() {
827        let graph = PersonaGraph::new();
828        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
829        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
830        graph.add_node(PersonaNode::new("payment-1".to_string(), "payment".to_string()));
831
832        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
833        graph.add_edge("order-1".to_string(), "payment-1".to_string(), "has_payments".to_string());
834
835        let related = graph.find_related_bfs("user-1", None, None);
836        assert_eq!(related.len(), 2);
837        assert!(related.contains(&"order-1".to_string()));
838        assert!(related.contains(&"payment-1".to_string()));
839    }
840
841    #[test]
842    fn test_persona_graph_find_related_bfs_with_depth_limit() {
843        let graph = PersonaGraph::new();
844        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
845        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
846        graph.add_node(PersonaNode::new("payment-1".to_string(), "payment".to_string()));
847
848        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
849        graph.add_edge("order-1".to_string(), "payment-1".to_string(), "has_payments".to_string());
850
851        let related = graph.find_related_bfs("user-1", None, Some(1));
852        assert_eq!(related.len(), 1);
853        assert!(related.contains(&"order-1".to_string()));
854    }
855
856    #[test]
857    fn test_persona_graph_find_related_bfs_with_type_filter() {
858        let graph = PersonaGraph::new();
859        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
860        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
861        graph.add_node(PersonaNode::new("account-1".to_string(), "account".to_string()));
862
863        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
864        graph.add_edge("user-1".to_string(), "account-1".to_string(), "has_accounts".to_string());
865
866        let filter = vec!["has_orders".to_string()];
867        let related = graph.find_related_bfs("user-1", Some(&filter), None);
868        assert_eq!(related.len(), 1);
869        assert!(related.contains(&"order-1".to_string()));
870    }
871
872    #[test]
873    fn test_persona_graph_find_related_dfs() {
874        let graph = PersonaGraph::new();
875        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
876        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
877        graph.add_node(PersonaNode::new("payment-1".to_string(), "payment".to_string()));
878
879        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
880        graph.add_edge("order-1".to_string(), "payment-1".to_string(), "has_payments".to_string());
881
882        let related = graph.find_related_dfs("user-1", None, None);
883        assert_eq!(related.len(), 2);
884    }
885
886    #[test]
887    fn test_persona_graph_find_related_dfs_with_depth_limit() {
888        let graph = PersonaGraph::new();
889        graph.add_node(PersonaNode::new("a".to_string(), "node".to_string()));
890        graph.add_node(PersonaNode::new("b".to_string(), "node".to_string()));
891        graph.add_node(PersonaNode::new("c".to_string(), "node".to_string()));
892        graph.add_node(PersonaNode::new("d".to_string(), "node".to_string()));
893
894        graph.add_edge("a".to_string(), "b".to_string(), "linked".to_string());
895        graph.add_edge("b".to_string(), "c".to_string(), "linked".to_string());
896        graph.add_edge("c".to_string(), "d".to_string(), "linked".to_string());
897
898        // DFS implementation: max_depth=2 means we can go 0->1->2, but depth 2 is the cutoff
899        // So we get nodes at depth 1 only (b), not c at depth 2
900        let related = graph.find_related_dfs("a", None, Some(2));
901        assert_eq!(related.len(), 1); // only b, depth 2 is the cutoff
902        assert!(related.contains(&"b".to_string()));
903    }
904
905    #[test]
906    fn test_persona_graph_get_subgraph() {
907        let graph = PersonaGraph::new();
908        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
909        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
910        graph.add_node(PersonaNode::new("isolated".to_string(), "node".to_string()));
911
912        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
913
914        let (nodes, edges) = graph.get_subgraph("user-1");
915        assert_eq!(nodes.len(), 2); // user-1 and order-1
916        assert_eq!(edges.len(), 1);
917    }
918
919    #[test]
920    fn test_persona_graph_get_all_nodes() {
921        let graph = PersonaGraph::new();
922        graph.add_node(PersonaNode::new("a".to_string(), "node".to_string()));
923        graph.add_node(PersonaNode::new("b".to_string(), "node".to_string()));
924        graph.add_node(PersonaNode::new("c".to_string(), "node".to_string()));
925
926        let nodes = graph.get_all_nodes();
927        assert_eq!(nodes.len(), 3);
928    }
929
930    #[test]
931    fn test_persona_graph_remove_node() {
932        let graph = PersonaGraph::new();
933        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
934        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
935
936        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
937
938        graph.remove_node("order-1");
939
940        assert!(graph.get_node("order-1").is_none());
941        assert_eq!(graph.get_edges_from("user-1").len(), 0);
942    }
943
944    #[test]
945    fn test_persona_graph_clear() {
946        let graph = PersonaGraph::new();
947        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
948        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
949        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
950
951        graph.clear();
952
953        let stats = graph.get_stats();
954        assert_eq!(stats.node_count, 0);
955        assert_eq!(stats.edge_count, 0);
956    }
957
958    #[test]
959    fn test_persona_graph_get_stats() {
960        let graph = PersonaGraph::new();
961        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
962        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
963        graph.add_node(PersonaNode::new("order-2".to_string(), "order".to_string()));
964
965        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
966        graph.add_edge("user-1".to_string(), "order-2".to_string(), "has_orders".to_string());
967
968        let stats = graph.get_stats();
969        assert_eq!(stats.node_count, 3);
970        assert_eq!(stats.edge_count, 2);
971        assert_eq!(*stats.relationship_types.get("has_orders").unwrap(), 2);
972    }
973
974    // =========================================================================
975    // Link entity types tests
976    // =========================================================================
977
978    #[test]
979    fn test_persona_graph_link_entity_types_user_order() {
980        let graph = PersonaGraph::new();
981        graph.link_entity_types("user-1", "user", "order-1", "order");
982
983        let node = graph.get_node("user-1").unwrap();
984        assert_eq!(node.entity_type, "user");
985
986        let related = node.get_related("has_orders");
987        assert!(related.contains(&"order-1".to_string()));
988    }
989
990    #[test]
991    fn test_persona_graph_link_entity_types_order_payment() {
992        let graph = PersonaGraph::new();
993        graph.link_entity_types("order-1", "order", "payment-1", "payment");
994
995        let related = graph.get_node("order-1").unwrap().get_related("has_payments");
996        assert!(related.contains(&"payment-1".to_string()));
997    }
998
999    #[test]
1000    fn test_persona_graph_link_entity_types_generic() {
1001        let graph = PersonaGraph::new();
1002        graph.link_entity_types("foo-1", "foo", "bar-1", "bars");
1003
1004        let node = graph.get_node("foo-1").unwrap();
1005        // Generic relationship: has_bar (from "bars" -> "bar")
1006        let related = node.get_related("has_bar");
1007        assert!(related.contains(&"bar-1".to_string()));
1008    }
1009
1010    #[test]
1011    fn test_persona_graph_link_entity_types_creates_nodes() {
1012        let graph = PersonaGraph::new();
1013        graph.link_entity_types("new-user", "user", "new-order", "order");
1014
1015        assert!(graph.get_node("new-user").is_some());
1016        assert!(graph.get_node("new-order").is_some());
1017    }
1018
1019    // =========================================================================
1020    // Find related by entity type tests
1021    // =========================================================================
1022
1023    #[test]
1024    fn test_persona_graph_find_related_by_entity_type() {
1025        let graph = PersonaGraph::new();
1026        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
1027        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
1028        graph.add_node(PersonaNode::new("order-2".to_string(), "order".to_string()));
1029        graph.add_node(PersonaNode::new("payment-1".to_string(), "payment".to_string()));
1030
1031        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
1032        graph.add_edge("user-1".to_string(), "order-2".to_string(), "has_orders".to_string());
1033        graph.add_edge("user-1".to_string(), "payment-1".to_string(), "has_payments".to_string());
1034
1035        let orders = graph.find_related_by_entity_type("user-1", "order", None);
1036        assert_eq!(orders.len(), 2);
1037    }
1038
1039    #[test]
1040    fn test_persona_graph_find_related_by_entity_type_with_filter() {
1041        let graph = PersonaGraph::new();
1042        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
1043        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
1044
1045        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
1046
1047        let orders = graph.find_related_by_entity_type("user-1", "order", Some("has_orders"));
1048        assert_eq!(orders.len(), 1);
1049    }
1050
1051    // =========================================================================
1052    // Get or create node with links tests
1053    // =========================================================================
1054
1055    #[test]
1056    fn test_persona_graph_get_or_create_node_new() {
1057        let graph = PersonaGraph::new();
1058        let node = graph.get_or_create_node_with_links("user-new", "user", None, None);
1059        assert_eq!(node.persona_id, "user-new");
1060        assert!(graph.get_node("user-new").is_some());
1061    }
1062
1063    #[test]
1064    fn test_persona_graph_get_or_create_node_existing() {
1065        let graph = PersonaGraph::new();
1066        let node1 = PersonaNode::new("user-existing".to_string(), "user".to_string());
1067        graph.add_node(node1);
1068
1069        let node2 = graph.get_or_create_node_with_links("user-existing", "user", None, None);
1070        assert_eq!(node2.persona_id, "user-existing");
1071    }
1072
1073    #[test]
1074    fn test_persona_graph_get_or_create_node_with_link() {
1075        let graph = PersonaGraph::new();
1076        let _node = graph.get_or_create_node_with_links(
1077            "user-link",
1078            "user",
1079            Some("order-link"),
1080            Some("order"),
1081        );
1082
1083        assert!(graph.get_node("user-link").is_some());
1084        assert!(graph.get_node("order-link").is_some());
1085        assert_eq!(graph.get_edges_from("user-link").len(), 1);
1086    }
1087
1088    // =========================================================================
1089    // GraphStats tests
1090    // =========================================================================
1091
1092    #[test]
1093    fn test_graph_stats_clone() {
1094        let stats = GraphStats {
1095            node_count: 5,
1096            edge_count: 10,
1097            relationship_types: {
1098                let mut map = HashMap::new();
1099                map.insert("has_orders".to_string(), 5);
1100                map
1101            },
1102        };
1103        let cloned = stats.clone();
1104        assert_eq!(cloned.node_count, 5);
1105        assert_eq!(cloned.edge_count, 10);
1106    }
1107
1108    #[test]
1109    fn test_graph_stats_debug() {
1110        let stats = GraphStats {
1111            node_count: 3,
1112            edge_count: 2,
1113            relationship_types: HashMap::new(),
1114        };
1115        let debug_str = format!("{:?}", stats);
1116        assert!(debug_str.contains("node_count"));
1117        assert!(debug_str.contains("edge_count"));
1118    }
1119
1120    #[test]
1121    fn test_graph_stats_serialize() {
1122        let stats = GraphStats {
1123            node_count: 1,
1124            edge_count: 2,
1125            relationship_types: HashMap::new(),
1126        };
1127        let json = serde_json::to_string(&stats).unwrap();
1128        assert!(json.contains("node_count"));
1129    }
1130
1131    // =========================================================================
1132    // Visualization tests
1133    // =========================================================================
1134
1135    #[test]
1136    fn test_visualization_node_creation() {
1137        let node = VisualizationNode {
1138            id: "user-1".to_string(),
1139            entity_type: "user".to_string(),
1140            label: "User 1".to_string(),
1141            position: Some((0.0, 0.0)),
1142        };
1143        assert_eq!(node.id, "user-1");
1144    }
1145
1146    #[test]
1147    fn test_visualization_edge_creation() {
1148        let edge = VisualizationEdge {
1149            from: "a".to_string(),
1150            to: "b".to_string(),
1151            relationship_type: "linked".to_string(),
1152            label: "Linked".to_string(),
1153        };
1154        assert_eq!(edge.from, "a");
1155    }
1156
1157    #[test]
1158    fn test_persona_graph_to_visualization() {
1159        let graph = PersonaGraph::new();
1160        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
1161        graph.add_node(PersonaNode::new("order-1".to_string(), "order".to_string()));
1162        graph.add_edge("user-1".to_string(), "order-1".to_string(), "has_orders".to_string());
1163
1164        let viz = graph.to_visualization();
1165        assert_eq!(viz.nodes.len(), 2);
1166        assert_eq!(viz.edges.len(), 1);
1167    }
1168
1169    #[test]
1170    fn test_visualization_serialize() {
1171        let viz = GraphVisualization {
1172            nodes: vec![VisualizationNode {
1173                id: "test".to_string(),
1174                entity_type: "node".to_string(),
1175                label: "Test".to_string(),
1176                position: None,
1177            }],
1178            edges: vec![],
1179        };
1180        let json = serde_json::to_string(&viz).unwrap();
1181        assert!(json.contains("test"));
1182    }
1183
1184    // =========================================================================
1185    // Cycle detection tests
1186    // =========================================================================
1187
1188    #[test]
1189    fn test_persona_graph_handles_cycles() {
1190        let graph = PersonaGraph::new();
1191        graph.add_node(PersonaNode::new("a".to_string(), "node".to_string()));
1192        graph.add_node(PersonaNode::new("b".to_string(), "node".to_string()));
1193        graph.add_node(PersonaNode::new("c".to_string(), "node".to_string()));
1194
1195        // Create a cycle: a -> b -> c -> a
1196        graph.add_edge("a".to_string(), "b".to_string(), "linked".to_string());
1197        graph.add_edge("b".to_string(), "c".to_string(), "linked".to_string());
1198        graph.add_edge("c".to_string(), "a".to_string(), "linked".to_string());
1199
1200        // BFS should not loop infinitely
1201        let related = graph.find_related_bfs("a", None, None);
1202        assert_eq!(related.len(), 2); // b and c (not a again)
1203
1204        // DFS should not loop infinitely
1205        let related_dfs = graph.find_related_dfs("a", None, None);
1206        assert_eq!(related_dfs.len(), 2);
1207    }
1208
1209    // =========================================================================
1210    // Clone tests
1211    // =========================================================================
1212
1213    #[test]
1214    fn test_persona_graph_clone() {
1215        let graph = PersonaGraph::new();
1216        graph.add_node(PersonaNode::new("user-1".to_string(), "user".to_string()));
1217
1218        let cloned = graph.clone();
1219        // Both graphs share the same underlying data via Arc
1220        assert!(cloned.get_node("user-1").is_some());
1221    }
1222}