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