ricecoder_agents/domain/
coordinator.rs

1//! Multi-agent coordination for domain agents
2
3use crate::domain::error::{DomainError, DomainResult};
4use crate::domain::models::Recommendation;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// A request to be routed to domain agents
9///
10/// This struct represents a user request that needs to be routed
11/// to one or more domain agents for processing.
12///
13/// # Examples
14///
15/// ```ignore
16/// use ricecoder_agents::domain::DomainRequest;
17///
18/// let request = DomainRequest {
19///     id: "req-1".to_string(),
20///     domains: vec!["web".to_string(), "backend".to_string()],
21///     content: "Help me set up a full-stack application".to_string(),
22///     context: Default::default(),
23/// };
24/// ```
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DomainRequest {
27    /// Request identifier
28    pub id: String,
29    /// Target domains for this request
30    pub domains: Vec<String>,
31    /// Request content
32    pub content: String,
33    /// Request context
34    pub context: HashMap<String, serde_json::Value>,
35}
36
37/// Coordinates multi-agent workflows
38///
39/// This struct manages coordination between domain agents,
40/// including request routing, response aggregation, and operation sequencing.
41///
42/// # Examples
43///
44/// ```ignore
45/// use ricecoder_agents::domain::DomainCoordinator;
46///
47/// let coordinator = DomainCoordinator::new();
48/// let coordinated = coordinator.coordinate_responses(responses)?;
49/// ```
50#[derive(Debug, Clone)]
51pub struct DomainCoordinator;
52
53impl DomainCoordinator {
54    /// Create a new domain coordinator
55    pub fn new() -> Self {
56        Self
57    }
58
59    /// Route a request to appropriate domain agents
60    ///
61    /// # Arguments
62    ///
63    /// * `request` - The request to route
64    ///
65    /// # Returns
66    ///
67    /// Returns the target domains for this request
68    pub fn route_request(&self, request: &DomainRequest) -> DomainResult<Vec<String>> {
69        if request.domains.is_empty() {
70            return Err(DomainError::coordination_error(
71                "Request must specify at least one target domain",
72            ));
73        }
74
75        // Validate that all requested domains are known
76        let valid_domains = vec!["web", "backend", "devops"];
77        for domain in &request.domains {
78            if !valid_domains.contains(&domain.as_str()) && !domain.starts_with("custom-") {
79                return Err(DomainError::coordination_error(format!(
80                    "Unknown domain: {}",
81                    domain
82                )));
83            }
84        }
85
86        Ok(request.domains.clone())
87    }
88
89    /// Determine which agents should handle a request based on content
90    ///
91    /// # Arguments
92    ///
93    /// * `content` - The request content
94    ///
95    /// # Returns
96    ///
97    /// Returns the inferred domains for this request
98    pub fn infer_domains(&self, content: &str) -> DomainResult<Vec<String>> {
99        let mut domains = Vec::new();
100        let content_lower = content.to_lowercase();
101
102        // Infer web domain
103        if content_lower.contains("frontend")
104            || content_lower.contains("react")
105            || content_lower.contains("vue")
106            || content_lower.contains("angular")
107            || content_lower.contains("styling")
108            || content_lower.contains("ui")
109            || content_lower.contains("web")
110        {
111            domains.push("web".to_string());
112        }
113
114        // Infer backend domain
115        if content_lower.contains("backend")
116            || content_lower.contains("api")
117            || content_lower.contains("database")
118            || content_lower.contains("server")
119            || content_lower.contains("rest")
120            || content_lower.contains("graphql")
121            || content_lower.contains("microservice")
122        {
123            domains.push("backend".to_string());
124        }
125
126        // Infer devops domain
127        if content_lower.contains("devops")
128            || content_lower.contains("deployment")
129            || content_lower.contains("ci/cd")
130            || content_lower.contains("docker")
131            || content_lower.contains("kubernetes")
132            || content_lower.contains("infrastructure")
133            || content_lower.contains("terraform")
134        {
135            domains.push("devops".to_string());
136        }
137
138        // If no domains inferred, default to all
139        if domains.is_empty() {
140            domains = vec!["web".to_string(), "backend".to_string(), "devops".to_string()];
141        }
142
143        Ok(domains)
144    }
145
146    /// Coordinate responses from multiple agents
147    ///
148    /// # Arguments
149    ///
150    /// * `responses` - Responses from domain agents
151    ///
152    /// # Returns
153    ///
154    /// Returns coordinated response
155    pub fn coordinate_responses(
156        &self,
157        responses: Vec<Recommendation>,
158    ) -> DomainResult<CoordinatedResponse> {
159        // Group recommendations by domain
160        let mut by_domain: std::collections::HashMap<String, Vec<Recommendation>> =
161            std::collections::HashMap::new();
162
163        for response in responses {
164            by_domain
165                .entry(response.domain.clone())
166                .or_insert_with(Vec::new)
167                .push(response);
168        }
169
170        // Create coordinated response
171        let coordinated = CoordinatedResponse {
172            recommendations: by_domain.values().flatten().cloned().collect(),
173            domain_count: by_domain.len(),
174            total_recommendations: by_domain.values().map(|v| v.len()).sum(),
175        };
176
177        Ok(coordinated)
178    }
179
180    /// Sequence operations for cross-domain tasks
181    ///
182    /// # Arguments
183    ///
184    /// * `operations` - Operations to sequence
185    ///
186    /// # Returns
187    ///
188    /// Returns sequenced operations
189    pub fn sequence_operations(
190        &self,
191        operations: Vec<Operation>,
192    ) -> DomainResult<Vec<Operation>> {
193        // Sort operations by dependency order
194        let mut sequenced = operations;
195        sequenced.sort_by_key(|op| op.priority);
196
197        Ok(sequenced)
198    }
199
200    /// Validate consistency across domains
201    ///
202    /// # Arguments
203    ///
204    /// * `recommendations` - Recommendations to validate
205    ///
206    /// # Returns
207    ///
208    /// Returns true if recommendations are consistent
209    pub fn validate_consistency(&self, recommendations: &[Recommendation]) -> DomainResult<bool> {
210        // Check for basic consistency
211        if recommendations.is_empty() {
212            return Ok(true);
213        }
214
215        // Verify all recommendations have required fields
216        for rec in recommendations {
217            if rec.domain.is_empty() || rec.category.is_empty() {
218                return Err(DomainError::coordination_error(
219                    "Recommendation missing required fields",
220                ));
221            }
222        }
223
224        // Check for cross-domain consistency
225        let mut domains_present = std::collections::HashSet::new();
226        for rec in recommendations {
227            domains_present.insert(rec.domain.clone());
228        }
229
230        // Verify that recommendations from different domains are complementary
231        if domains_present.len() > 1 {
232            // Group recommendations by domain
233            let mut by_domain: HashMap<String, Vec<&Recommendation>> = HashMap::new();
234            for rec in recommendations {
235                by_domain
236                    .entry(rec.domain.clone())
237                    .or_insert_with(Vec::new)
238                    .push(rec);
239            }
240
241            // Verify each domain has recommendations
242            for domain_recs in by_domain.values() {
243                if domain_recs.is_empty() {
244                    return Err(DomainError::coordination_error(
245                        "Domain has no recommendations",
246                    ));
247                }
248            }
249        }
250
251        Ok(true)
252    }
253
254    /// Coordinate full-stack recommendations across domains
255    ///
256    /// # Arguments
257    ///
258    /// * `recommendations` - Recommendations from all domains
259    ///
260    /// # Returns
261    ///
262    /// Returns a full-stack coordination result
263    pub fn coordinate_full_stack(
264        &self,
265        recommendations: Vec<Recommendation>,
266    ) -> DomainResult<FullStackCoordination> {
267        // Validate consistency
268        self.validate_consistency(&recommendations)?;
269
270        // Group recommendations by domain
271        let mut by_domain: HashMap<String, Vec<Recommendation>> = HashMap::new();
272        for rec in recommendations {
273            by_domain
274                .entry(rec.domain.clone())
275                .or_insert_with(Vec::new)
276                .push(rec);
277        }
278
279        // Ensure all three domains are represented for full-stack
280        let has_web = by_domain.contains_key("web");
281        let has_backend = by_domain.contains_key("backend");
282        let has_devops = by_domain.contains_key("devops");
283
284        let is_full_stack = has_web && has_backend && has_devops;
285
286        Ok(FullStackCoordination {
287            web_recommendations: by_domain.get("web").cloned().unwrap_or_default(),
288            backend_recommendations: by_domain.get("backend").cloned().unwrap_or_default(),
289            devops_recommendations: by_domain.get("devops").cloned().unwrap_or_default(),
290            is_full_stack,
291            total_recommendations: by_domain.values().map(|v| v.len()).sum(),
292        })
293    }
294
295    /// Ensure consistency across full-stack domains
296    ///
297    /// # Arguments
298    ///
299    /// * `coordination` - The full-stack coordination to validate
300    ///
301    /// # Returns
302    ///
303    /// Returns true if all domains are consistent
304    pub fn ensure_full_stack_consistency(
305        &self,
306        coordination: &FullStackCoordination,
307    ) -> DomainResult<bool> {
308        if !coordination.is_full_stack {
309            return Ok(true);
310        }
311
312        // Verify each domain has recommendations
313        if coordination.web_recommendations.is_empty() {
314            return Err(DomainError::coordination_error(
315                "Web domain has no recommendations",
316            ));
317        }
318
319        if coordination.backend_recommendations.is_empty() {
320            return Err(DomainError::coordination_error(
321                "Backend domain has no recommendations",
322            ));
323        }
324
325        if coordination.devops_recommendations.is_empty() {
326            return Err(DomainError::coordination_error(
327                "DevOps domain has no recommendations",
328            ));
329        }
330
331        // Verify technology stack consistency
332        let mut all_techs = std::collections::HashSet::new();
333        for rec in &coordination.web_recommendations {
334            for tech in &rec.technologies {
335                all_techs.insert(tech.clone());
336            }
337        }
338        for rec in &coordination.backend_recommendations {
339            for tech in &rec.technologies {
340                all_techs.insert(tech.clone());
341            }
342        }
343        for rec in &coordination.devops_recommendations {
344            for tech in &rec.technologies {
345                all_techs.insert(tech.clone());
346            }
347        }
348
349        // Ensure we have a reasonable technology stack
350        if all_techs.is_empty() {
351            return Err(DomainError::coordination_error(
352                "No technologies recommended across domains",
353            ));
354        }
355
356        Ok(true)
357    }
358
359    /// Detect conflicts in full-stack recommendations
360    ///
361    /// # Arguments
362    ///
363    /// * `coordination` - The full-stack coordination to analyze
364    ///
365    /// # Returns
366    ///
367    /// Returns a vector of potential conflicts
368    pub fn detect_full_stack_conflicts(
369        &self,
370        coordination: &FullStackCoordination,
371    ) -> DomainResult<Vec<String>> {
372        let mut conflicts = Vec::new();
373
374        // Check for incompatible technology combinations
375        let web_techs: std::collections::HashSet<_> = coordination
376            .web_recommendations
377            .iter()
378            .flat_map(|r| r.technologies.clone())
379            .collect();
380
381        let backend_techs: std::collections::HashSet<_> = coordination
382            .backend_recommendations
383            .iter()
384            .flat_map(|r| r.technologies.clone())
385            .collect();
386
387        let devops_techs: std::collections::HashSet<_> = coordination
388            .devops_recommendations
389            .iter()
390            .flat_map(|r| r.technologies.clone())
391            .collect();
392
393        // Check for known incompatibilities
394        let incompatible_pairs = vec![
395            ("Webpack", "Vite"),
396            ("npm", "yarn"),
397            ("PostgreSQL", "MongoDB"),
398            ("REST", "GraphQL"),
399            ("Microservices", "Monolithic"),
400        ];
401
402        for (tech_a, tech_b) in incompatible_pairs {
403            let has_a_web = web_techs.contains(tech_a);
404            let has_b_web = web_techs.contains(tech_b);
405            let has_a_backend = backend_techs.contains(tech_a);
406            let has_b_backend = backend_techs.contains(tech_b);
407            let has_a_devops = devops_techs.contains(tech_a);
408            let has_b_devops = devops_techs.contains(tech_b);
409
410            if (has_a_web || has_a_backend || has_a_devops)
411                && (has_b_web || has_b_backend || has_b_devops)
412            {
413                conflicts.push(format!(
414                    "Incompatible technologies: {} and {} are recommended",
415                    tech_a, tech_b
416                ));
417            }
418        }
419
420        Ok(conflicts)
421    }
422}
423
424impl Default for DomainCoordinator {
425    fn default() -> Self {
426        Self::new()
427    }
428}
429
430/// Coordinated response from multiple agents
431#[derive(Debug, Clone)]
432pub struct CoordinatedResponse {
433    /// All recommendations
434    pub recommendations: Vec<Recommendation>,
435    /// Number of domains involved
436    pub domain_count: usize,
437    /// Total number of recommendations
438    pub total_recommendations: usize,
439}
440
441/// Full-stack coordination across web, backend, and DevOps domains
442///
443/// This struct represents coordinated recommendations across all three
444/// development domains for a complete full-stack application.
445///
446/// # Examples
447///
448/// ```ignore
449/// use ricecoder_agents::domain::FullStackCoordination;
450///
451/// let coordination = FullStackCoordination {
452///     web_recommendations: vec![],
453///     backend_recommendations: vec![],
454///     devops_recommendations: vec![],
455///     is_full_stack: true,
456///     total_recommendations: 0,
457/// };
458/// ```
459#[derive(Debug, Clone)]
460pub struct FullStackCoordination {
461    /// Recommendations from web domain
462    pub web_recommendations: Vec<Recommendation>,
463    /// Recommendations from backend domain
464    pub backend_recommendations: Vec<Recommendation>,
465    /// Recommendations from DevOps domain
466    pub devops_recommendations: Vec<Recommendation>,
467    /// Whether this is a complete full-stack coordination
468    pub is_full_stack: bool,
469    /// Total number of recommendations
470    pub total_recommendations: usize,
471}
472
473/// An operation to be sequenced
474#[derive(Debug, Clone)]
475pub struct Operation {
476    /// Operation identifier
477    pub id: String,
478    /// Operation name
479    pub name: String,
480    /// Priority (lower = earlier)
481    pub priority: u32,
482    /// Dependencies
483    pub dependencies: Vec<String>,
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use crate::domain::models::Recommendation;
490
491    fn create_test_recommendation(domain: &str) -> Recommendation {
492        Recommendation {
493            domain: domain.to_string(),
494            category: "test".to_string(),
495            content: "Test recommendation".to_string(),
496            technologies: vec!["Tech1".to_string()],
497            rationale: "Test rationale".to_string(),
498        }
499    }
500
501    #[test]
502    fn test_coordinator_creation() {
503        let coordinator = DomainCoordinator::new();
504        assert_eq!(std::mem::size_of_val(&coordinator), 0); // Zero-sized type
505    }
506
507    #[test]
508    fn test_coordinate_responses_single_domain() {
509        let coordinator = DomainCoordinator::new();
510        let responses = vec![create_test_recommendation("web")];
511
512        let coordinated = coordinator.coordinate_responses(responses).unwrap();
513
514        assert_eq!(coordinated.domain_count, 1);
515        assert_eq!(coordinated.total_recommendations, 1);
516    }
517
518    #[test]
519    fn test_coordinate_responses_multiple_domains() {
520        let coordinator = DomainCoordinator::new();
521        let responses = vec![
522            create_test_recommendation("web"),
523            create_test_recommendation("backend"),
524            create_test_recommendation("devops"),
525        ];
526
527        let coordinated = coordinator.coordinate_responses(responses).unwrap();
528
529        assert_eq!(coordinated.domain_count, 3);
530        assert_eq!(coordinated.total_recommendations, 3);
531    }
532
533    #[test]
534    fn test_coordinate_responses_empty() {
535        let coordinator = DomainCoordinator::new();
536        let responses = vec![];
537
538        let coordinated = coordinator.coordinate_responses(responses).unwrap();
539
540        assert_eq!(coordinated.domain_count, 0);
541        assert_eq!(coordinated.total_recommendations, 0);
542    }
543
544    #[test]
545    fn test_sequence_operations() {
546        let coordinator = DomainCoordinator::new();
547        let operations = vec![
548            Operation {
549                id: "op1".to_string(),
550                name: "Operation 1".to_string(),
551                priority: 3,
552                dependencies: vec![],
553            },
554            Operation {
555                id: "op2".to_string(),
556                name: "Operation 2".to_string(),
557                priority: 1,
558                dependencies: vec![],
559            },
560            Operation {
561                id: "op3".to_string(),
562                name: "Operation 3".to_string(),
563                priority: 2,
564                dependencies: vec![],
565            },
566        ];
567
568        let sequenced = coordinator.sequence_operations(operations).unwrap();
569
570        assert_eq!(sequenced[0].priority, 1);
571        assert_eq!(sequenced[1].priority, 2);
572        assert_eq!(sequenced[2].priority, 3);
573    }
574
575    #[test]
576    fn test_validate_consistency_valid() {
577        let coordinator = DomainCoordinator::new();
578        let recommendations = vec![create_test_recommendation("web")];
579
580        assert!(coordinator.validate_consistency(&recommendations).unwrap());
581    }
582
583    #[test]
584    fn test_validate_consistency_empty() {
585        let coordinator = DomainCoordinator::new();
586        let recommendations = vec![];
587
588        assert!(coordinator.validate_consistency(&recommendations).unwrap());
589    }
590
591    #[test]
592    fn test_validate_consistency_invalid_domain() {
593        let coordinator = DomainCoordinator::new();
594        let mut rec = create_test_recommendation("web");
595        rec.domain = String::new();
596
597        assert!(coordinator.validate_consistency(&[rec]).is_err());
598    }
599
600    #[test]
601    fn test_validate_consistency_invalid_category() {
602        let coordinator = DomainCoordinator::new();
603        let mut rec = create_test_recommendation("web");
604        rec.category = String::new();
605
606        assert!(coordinator.validate_consistency(&[rec]).is_err());
607    }
608
609    #[test]
610    fn test_default_coordinator() {
611        let coordinator = DomainCoordinator::default();
612        let responses = vec![create_test_recommendation("web")];
613
614        assert!(coordinator.coordinate_responses(responses).is_ok());
615    }
616
617    #[test]
618    fn test_coordinated_response_structure() {
619        let coordinator = DomainCoordinator::new();
620        let responses = vec![
621            create_test_recommendation("web"),
622            create_test_recommendation("web"),
623            create_test_recommendation("backend"),
624        ];
625
626        let coordinated = coordinator.coordinate_responses(responses).unwrap();
627
628        assert_eq!(coordinated.domain_count, 2);
629        assert_eq!(coordinated.total_recommendations, 3);
630        assert_eq!(coordinated.recommendations.len(), 3);
631    }
632
633    #[test]
634    fn test_operation_sequencing_with_dependencies() {
635        let coordinator = DomainCoordinator::new();
636        let operations = vec![
637            Operation {
638                id: "setup".to_string(),
639                name: "Setup".to_string(),
640                priority: 1,
641                dependencies: vec![],
642            },
643            Operation {
644                id: "deploy".to_string(),
645                name: "Deploy".to_string(),
646                priority: 2,
647                dependencies: vec!["setup".to_string()],
648            },
649        ];
650
651        let sequenced = coordinator.sequence_operations(operations).unwrap();
652
653        assert_eq!(sequenced[0].id, "setup");
654        assert_eq!(sequenced[1].id, "deploy");
655    }
656
657    #[test]
658    fn test_multiple_recommendations_same_domain() {
659        let coordinator = DomainCoordinator::new();
660        let responses = vec![
661            create_test_recommendation("web"),
662            create_test_recommendation("web"),
663            create_test_recommendation("web"),
664        ];
665
666        let coordinated = coordinator.coordinate_responses(responses).unwrap();
667
668        assert_eq!(coordinated.domain_count, 1);
669        assert_eq!(coordinated.total_recommendations, 3);
670    }
671
672    #[test]
673    fn test_route_request_valid() {
674        let coordinator = DomainCoordinator::new();
675        let request = DomainRequest {
676            id: "req-1".to_string(),
677            domains: vec!["web".to_string(), "backend".to_string()],
678            content: "Help me set up a full-stack app".to_string(),
679            context: std::collections::HashMap::new(),
680        };
681
682        let routed = coordinator.route_request(&request).unwrap();
683        assert_eq!(routed.len(), 2);
684        assert!(routed.contains(&"web".to_string()));
685        assert!(routed.contains(&"backend".to_string()));
686    }
687
688    #[test]
689    fn test_route_request_empty_domains() {
690        let coordinator = DomainCoordinator::new();
691        let request = DomainRequest {
692            id: "req-1".to_string(),
693            domains: vec![],
694            content: "Help me set up a full-stack app".to_string(),
695            context: std::collections::HashMap::new(),
696        };
697
698        assert!(coordinator.route_request(&request).is_err());
699    }
700
701    #[test]
702    fn test_route_request_invalid_domain() {
703        let coordinator = DomainCoordinator::new();
704        let request = DomainRequest {
705            id: "req-1".to_string(),
706            domains: vec!["invalid-domain".to_string()],
707            content: "Help me set up a full-stack app".to_string(),
708            context: std::collections::HashMap::new(),
709        };
710
711        assert!(coordinator.route_request(&request).is_err());
712    }
713
714    #[test]
715    fn test_route_request_custom_domain() {
716        let coordinator = DomainCoordinator::new();
717        let request = DomainRequest {
718            id: "req-1".to_string(),
719            domains: vec!["custom-mobile".to_string()],
720            content: "Help me set up a mobile app".to_string(),
721            context: std::collections::HashMap::new(),
722        };
723
724        let routed = coordinator.route_request(&request).unwrap();
725        assert_eq!(routed.len(), 1);
726        assert_eq!(routed[0], "custom-mobile");
727    }
728
729    #[test]
730    fn test_infer_domains_web() {
731        let coordinator = DomainCoordinator::new();
732        let domains = coordinator
733            .infer_domains("I need help with React and styling")
734            .unwrap();
735
736        assert!(domains.contains(&"web".to_string()));
737    }
738
739    #[test]
740    fn test_infer_domains_backend() {
741        let coordinator = DomainCoordinator::new();
742        let domains = coordinator
743            .infer_domains("I need help with REST API and database design")
744            .unwrap();
745
746        assert!(domains.contains(&"backend".to_string()));
747    }
748
749    #[test]
750    fn test_infer_domains_devops() {
751        let coordinator = DomainCoordinator::new();
752        let domains = coordinator
753            .infer_domains("I need help with Docker and Kubernetes")
754            .unwrap();
755
756        assert!(domains.contains(&"devops".to_string()));
757    }
758
759    #[test]
760    fn test_infer_domains_full_stack() {
761        let coordinator = DomainCoordinator::new();
762        let domains = coordinator
763            .infer_domains("I need help with React, Node.js API, and Docker deployment")
764            .unwrap();
765
766        assert!(domains.contains(&"web".to_string()));
767        assert!(domains.contains(&"backend".to_string()));
768        assert!(domains.contains(&"devops".to_string()));
769    }
770
771    #[test]
772    fn test_infer_domains_empty_defaults_to_all() {
773        let coordinator = DomainCoordinator::new();
774        let domains = coordinator.infer_domains("general help").unwrap();
775
776        assert_eq!(domains.len(), 3);
777        assert!(domains.contains(&"web".to_string()));
778        assert!(domains.contains(&"backend".to_string()));
779        assert!(domains.contains(&"devops".to_string()));
780    }
781
782    #[test]
783    fn test_coordinate_full_stack() {
784        let coordinator = DomainCoordinator::new();
785        let responses = vec![
786            create_test_recommendation("web"),
787            create_test_recommendation("backend"),
788            create_test_recommendation("devops"),
789        ];
790
791        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
792
793        assert!(coordination.is_full_stack);
794        assert_eq!(coordination.web_recommendations.len(), 1);
795        assert_eq!(coordination.backend_recommendations.len(), 1);
796        assert_eq!(coordination.devops_recommendations.len(), 1);
797        assert_eq!(coordination.total_recommendations, 3);
798    }
799
800    #[test]
801    fn test_coordinate_full_stack_partial() {
802        let coordinator = DomainCoordinator::new();
803        let responses = vec![
804            create_test_recommendation("web"),
805            create_test_recommendation("backend"),
806        ];
807
808        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
809
810        assert!(!coordination.is_full_stack);
811        assert_eq!(coordination.web_recommendations.len(), 1);
812        assert_eq!(coordination.backend_recommendations.len(), 1);
813        assert_eq!(coordination.devops_recommendations.len(), 0);
814        assert_eq!(coordination.total_recommendations, 2);
815    }
816
817    #[test]
818    fn test_validate_consistency_cross_domain() {
819        let coordinator = DomainCoordinator::new();
820        let recommendations = vec![
821            create_test_recommendation("web"),
822            create_test_recommendation("backend"),
823            create_test_recommendation("devops"),
824        ];
825
826        assert!(coordinator.validate_consistency(&recommendations).unwrap());
827    }
828
829    #[test]
830    fn test_domain_request_creation() {
831        let request = DomainRequest {
832            id: "req-1".to_string(),
833            domains: vec!["web".to_string()],
834            content: "Help me".to_string(),
835            context: std::collections::HashMap::new(),
836        };
837
838        assert_eq!(request.id, "req-1");
839        assert_eq!(request.domains.len(), 1);
840    }
841
842    #[test]
843    fn test_full_stack_coordination_structure() {
844        let coordinator = DomainCoordinator::new();
845        let responses = vec![
846            create_test_recommendation("web"),
847            create_test_recommendation("web"),
848            create_test_recommendation("backend"),
849            create_test_recommendation("devops"),
850        ];
851
852        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
853
854        assert!(coordination.is_full_stack);
855        assert_eq!(coordination.web_recommendations.len(), 2);
856        assert_eq!(coordination.backend_recommendations.len(), 1);
857        assert_eq!(coordination.devops_recommendations.len(), 1);
858        assert_eq!(coordination.total_recommendations, 4);
859    }
860
861    #[test]
862    fn test_ensure_full_stack_consistency_valid() {
863        let coordinator = DomainCoordinator::new();
864        let responses = vec![
865            create_test_recommendation("web"),
866            create_test_recommendation("backend"),
867            create_test_recommendation("devops"),
868        ];
869
870        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
871        assert!(coordinator
872            .ensure_full_stack_consistency(&coordination)
873            .unwrap());
874    }
875
876    #[test]
877    fn test_ensure_full_stack_consistency_partial() {
878        let coordinator = DomainCoordinator::new();
879        let responses = vec![
880            create_test_recommendation("web"),
881            create_test_recommendation("backend"),
882        ];
883
884        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
885        // Partial stack should still be valid
886        assert!(coordinator
887            .ensure_full_stack_consistency(&coordination)
888            .unwrap());
889    }
890
891    #[test]
892    fn test_ensure_full_stack_consistency_missing_web() {
893        let coordinator = DomainCoordinator::new();
894        let coordination = FullStackCoordination {
895            web_recommendations: vec![],
896            backend_recommendations: vec![create_test_recommendation("backend")],
897            devops_recommendations: vec![create_test_recommendation("devops")],
898            is_full_stack: true,
899            total_recommendations: 2,
900        };
901
902        assert!(coordinator
903            .ensure_full_stack_consistency(&coordination)
904            .is_err());
905    }
906
907    #[test]
908    fn test_detect_full_stack_conflicts_none() {
909        let coordinator = DomainCoordinator::new();
910        let responses = vec![
911            create_test_recommendation("web"),
912            create_test_recommendation("backend"),
913            create_test_recommendation("devops"),
914        ];
915
916        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
917        let conflicts = coordinator
918            .detect_full_stack_conflicts(&coordination)
919            .unwrap();
920
921        assert!(conflicts.is_empty());
922    }
923
924    #[test]
925    fn test_detect_full_stack_conflicts_incompatible_techs() {
926        let coordinator = DomainCoordinator::new();
927
928        let mut web_rec = create_test_recommendation("web");
929        web_rec.technologies = vec!["Webpack".to_string()];
930
931        let mut backend_rec = create_test_recommendation("backend");
932        backend_rec.technologies = vec!["Node.js".to_string()];
933
934        let mut devops_rec = create_test_recommendation("devops");
935        devops_rec.technologies = vec!["Vite".to_string()];
936
937        let coordination = FullStackCoordination {
938            web_recommendations: vec![web_rec],
939            backend_recommendations: vec![backend_rec],
940            devops_recommendations: vec![devops_rec],
941            is_full_stack: true,
942            total_recommendations: 3,
943        };
944
945        let conflicts = coordinator
946            .detect_full_stack_conflicts(&coordination)
947            .unwrap();
948
949        assert!(!conflicts.is_empty());
950        assert!(conflicts[0].contains("Incompatible"));
951    }
952
953    #[test]
954    fn test_full_stack_coordination_empty() {
955        let coordinator = DomainCoordinator::new();
956        let responses = vec![];
957
958        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
959
960        assert!(!coordination.is_full_stack);
961        assert_eq!(coordination.total_recommendations, 0);
962    }
963
964    #[test]
965    fn test_full_stack_coordination_single_domain() {
966        let coordinator = DomainCoordinator::new();
967        let responses = vec![create_test_recommendation("web")];
968
969        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
970
971        assert!(!coordination.is_full_stack);
972        assert_eq!(coordination.web_recommendations.len(), 1);
973        assert_eq!(coordination.backend_recommendations.len(), 0);
974        assert_eq!(coordination.devops_recommendations.len(), 0);
975    }
976
977    #[test]
978    fn test_full_stack_coordination_two_domains() {
979        let coordinator = DomainCoordinator::new();
980        let responses = vec![
981            create_test_recommendation("web"),
982            create_test_recommendation("backend"),
983        ];
984
985        let coordination = coordinator.coordinate_full_stack(responses).unwrap();
986
987        assert!(!coordination.is_full_stack);
988        assert_eq!(coordination.web_recommendations.len(), 1);
989        assert_eq!(coordination.backend_recommendations.len(), 1);
990        assert_eq!(coordination.devops_recommendations.len(), 0);
991    }
992}