agpm_cli/version/
conflict.rs

1//! Version conflict detection and reporting.
2//!
3//! This module handles detection and reporting of version conflicts that can occur
4//! when multiple dependencies require incompatible versions of the same resource.
5//! It provides detailed conflict information to help users resolve dependency issues.
6
7use anyhow::Result;
8use semver::Version;
9use std::collections::{HashMap, HashSet};
10use std::fmt;
11
12use crate::core::AgpmError;
13use crate::lockfile::ResourceId;
14
15/// Represents a version conflict between dependencies
16#[derive(Debug, Clone)]
17pub struct VersionConflict {
18    pub resource: ResourceId,
19    pub conflicting_requirements: Vec<ConflictingRequirement>,
20}
21
22#[derive(Debug, Clone)]
23pub struct ConflictingRequirement {
24    /// The parent resource that requires this dependency (e.g., "agents/agent-a")
25    pub required_by: String,
26    /// The version constraint for the transitive dependency (e.g., "x-v1.0.0")
27    pub requirement: String,
28    /// The SHA that the transitive dependency resolved to
29    pub resolved_sha: String,
30    /// The semantic version if applicable
31    pub resolved_version: Option<Version>,
32    /// The version constraint of the parent resource (e.g., "^1.0.0")
33    pub parent_version_constraint: Option<String>,
34    /// The SHA that the parent resource resolved to
35    pub parent_resolved_sha: Option<String>,
36}
37
38impl fmt::Display for VersionConflict {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        writeln!(f, "Version conflict for {}:", self.resource)?;
41
42        // Group by SHA to show which requirements resolve to which commit
43        let mut sha_groups: HashMap<&str, Vec<&ConflictingRequirement>> = HashMap::new();
44        for req in &self.conflicting_requirements {
45            sha_groups.entry(&req.resolved_sha).or_default().push(req);
46        }
47
48        for (sha, reqs) in sha_groups {
49            let short_sha = &sha[..8.min(sha.len())];
50            writeln!(f, "  Commit {short_sha}:")?;
51            for req in reqs {
52                writeln!(f, "    - {} requires {}", req.required_by, req.requirement)?;
53            }
54        }
55
56        Ok(())
57    }
58}
59
60/// Detects and resolves version conflicts
61pub struct ConflictDetector {
62    requirements: HashMap<ResourceId, Vec<ConflictingRequirement>>, // resource -> [requirements]
63}
64
65impl Default for ConflictDetector {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl ConflictDetector {
72    pub fn new() -> Self {
73        Self {
74            requirements: HashMap::new(),
75        }
76    }
77
78    /// Get a reference to the internal requirements map.
79    ///
80    /// This is used by the backtracking resolver to populate its resource registry
81    /// for conflict detection during version changes.
82    pub fn requirements(&self) -> &HashMap<ResourceId, Vec<ConflictingRequirement>> {
83        &self.requirements
84    }
85
86    /// Add a dependency requirement with optional parent metadata
87    pub fn add_requirement(
88        &mut self,
89        resource: ResourceId,
90        required_by: &str,
91        version_constraint: &str,
92        resolved_sha: &str,
93    ) {
94        self.add_requirement_with_parent(
95            resource,
96            required_by,
97            version_constraint,
98            resolved_sha,
99            None,
100            None,
101        );
102    }
103
104    /// Add a dependency requirement with full parent metadata for backtracking
105    pub fn add_requirement_with_parent(
106        &mut self,
107        resource: ResourceId,
108        required_by: &str,
109        version_constraint: &str,
110        resolved_sha: &str,
111        parent_version_constraint: Option<String>,
112        parent_resolved_sha: Option<String>,
113    ) {
114        self.requirements.entry(resource).or_default().push(ConflictingRequirement {
115            required_by: required_by.to_string(),
116            requirement: version_constraint.to_string(),
117            resolved_sha: resolved_sha.to_string(),
118            resolved_version: None,
119            parent_version_constraint,
120            parent_resolved_sha,
121        });
122    }
123
124    /// Detect conflicts in the current requirements
125    pub fn detect_conflicts(&self) -> Vec<VersionConflict> {
126        let mut conflicts = Vec::new();
127
128        for (resource_id, requirements) in &self.requirements {
129            if requirements.len() <= 1 {
130                continue; // No conflict possible with single requirement
131            }
132
133            // Group by resolved SHA
134            let mut sha_groups: HashMap<&str, Vec<&ConflictingRequirement>> = HashMap::new();
135            for req in requirements {
136                sha_groups.entry(req.resolved_sha.as_str()).or_default().push(req);
137            }
138
139            // Conflict only if multiple different SHAs
140            if sha_groups.len() > 1 {
141                conflicts.push(VersionConflict {
142                    resource: resource_id.clone(),
143                    conflicting_requirements: requirements.clone(),
144                });
145            }
146        }
147
148        conflicts
149    }
150
151    /// Try to resolve conflicts by finding compatible versions
152    pub fn resolve_conflicts(
153        &self,
154        available_versions: &HashMap<ResourceId, Vec<Version>>,
155    ) -> Result<HashMap<ResourceId, Version>> {
156        let mut resolved = HashMap::new();
157        let conflicts = self.detect_conflicts();
158
159        if !conflicts.is_empty() {
160            let conflict_messages: Vec<String> =
161                conflicts.iter().map(std::string::ToString::to_string).collect();
162
163            return Err(AgpmError::Other {
164                message: format!(
165                    "Unable to resolve version conflicts:\n{}",
166                    conflict_messages.join("\n")
167                ),
168            }
169            .into());
170        }
171
172        // Resolve each resource to its best version
173        for (resource_id, requirements) in &self.requirements {
174            let versions = available_versions.get(resource_id).ok_or_else(|| AgpmError::Other {
175                message: format!("No versions available for resource: {resource_id}"),
176            })?;
177
178            let best_version = self.find_best_version(versions, requirements)?;
179            resolved.insert(resource_id.clone(), best_version);
180        }
181
182        Ok(resolved)
183    }
184
185    /// Find the best version that satisfies all requirements
186    fn find_best_version(
187        &self,
188        available: &[Version],
189        requirements: &[ConflictingRequirement],
190    ) -> Result<Version> {
191        let mut candidates = available.to_vec();
192
193        // Filter by each requirement
194        for req in requirements {
195            let req_str = &req.requirement;
196            if req_str == "latest" || req_str == "*" {
197                continue; // These match everything
198            }
199
200            if let Ok(req) = crate::version::parse_version_req(req_str) {
201                candidates.retain(|v| req.matches(v));
202            }
203        }
204
205        if candidates.is_empty() {
206            return Err(AgpmError::Other {
207                message: format!("No version satisfies all requirements: {requirements:?}"),
208            }
209            .into());
210        }
211
212        // Sort and return the highest version
213        candidates.sort_by(|a, b| b.cmp(a));
214        Ok(candidates[0].clone())
215    }
216}
217
218/// Analyzes dependency graphs for circular dependencies
219pub struct CircularDependencyDetector {
220    graph: HashMap<String, HashSet<String>>,
221}
222
223impl Default for CircularDependencyDetector {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229impl CircularDependencyDetector {
230    pub fn new() -> Self {
231        Self {
232            graph: HashMap::new(),
233        }
234    }
235
236    /// Add a dependency edge
237    pub fn add_dependency(&mut self, from: &str, to: &str) {
238        self.graph.entry(from.to_string()).or_default().insert(to.to_string());
239    }
240
241    /// Detect circular dependencies
242    pub fn detect_cycles(&self) -> Vec<Vec<String>> {
243        let mut cycles = Vec::new();
244        let mut visited = HashSet::new();
245        let mut rec_stack = HashSet::new();
246        let mut path = Vec::new();
247
248        for node in self.graph.keys() {
249            if !visited.contains(node) {
250                self.dfs_detect_cycle(node, &mut visited, &mut rec_stack, &mut path, &mut cycles);
251            }
252        }
253
254        cycles
255    }
256
257    fn dfs_detect_cycle(
258        &self,
259        node: &str,
260        visited: &mut HashSet<String>,
261        rec_stack: &mut HashSet<String>,
262        path: &mut Vec<String>,
263        cycles: &mut Vec<Vec<String>>,
264    ) {
265        visited.insert(node.to_string());
266        rec_stack.insert(node.to_string());
267        path.push(node.to_string());
268
269        if let Some(neighbors) = self.graph.get(node) {
270            for neighbor in neighbors {
271                if !visited.contains(neighbor) {
272                    self.dfs_detect_cycle(neighbor, visited, rec_stack, path, cycles);
273                } else if rec_stack.contains(neighbor) {
274                    // Found a cycle
275                    let cycle_start = path.iter().position(|n| n == neighbor).unwrap();
276                    let cycle = path[cycle_start..].to_vec();
277                    cycles.push(cycle);
278                }
279            }
280        }
281
282        path.pop();
283        rec_stack.remove(node);
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    // Test helper to create a ResourceId easily
292    fn test_resource_id(name: &str) -> ResourceId {
293        ResourceId::new(
294            name,
295            Some("test-source"),
296            Some("claude-code"),
297            crate::core::ResourceType::Agent,
298            crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string(),
299        )
300    }
301
302    #[test]
303    fn test_conflict_detection() {
304        let mut detector = ConflictDetector::new();
305
306        // Add compatible requirements (same SHA)
307        detector.add_requirement(test_resource_id("lib1"), "app1", "^1.0.0", "abc123def456");
308        detector.add_requirement(test_resource_id("lib1"), "app2", "^1.2.0", "abc123def456");
309
310        let conflicts = detector.detect_conflicts();
311        assert_eq!(conflicts.len(), 0); // These are compatible (same SHA)
312
313        // Add incompatible requirements (different SHAs)
314        detector.add_requirement(test_resource_id("lib2"), "app1", "^1.0.0", "111222333444");
315        detector.add_requirement(test_resource_id("lib2"), "app2", "^2.0.0", "555666777888");
316
317        let conflicts = detector.detect_conflicts();
318        assert_eq!(conflicts.len(), 1);
319        assert!(conflicts[0].resource.to_string().contains("lib2"));
320    }
321
322    #[test]
323    fn test_git_ref_compatibility() {
324        let mut detector = ConflictDetector::new();
325
326        // Same git ref resolving to same SHA - compatible
327        detector.add_requirement(test_resource_id("lib1"), "app1", "main", "abc123def456");
328        detector.add_requirement(test_resource_id("lib1"), "app2", "main", "abc123def456");
329
330        let conflicts = detector.detect_conflicts();
331        assert_eq!(conflicts.len(), 0);
332
333        // Different git refs resolving to different SHAs - incompatible
334        detector.add_requirement(test_resource_id("lib2"), "app1", "main", "abc123def456");
335        detector.add_requirement(test_resource_id("lib2"), "app2", "develop", "999888777666");
336
337        let conflicts = detector.detect_conflicts();
338        assert_eq!(conflicts.len(), 1);
339    }
340
341    #[test]
342    fn test_git_ref_case_insensitive() {
343        let mut detector = ConflictDetector::new();
344
345        // Git refs differing only by case should be treated as the same
346        // (important for case-insensitive filesystems like Windows and macOS)
347        detector.add_requirement(test_resource_id("lib1"), "app1", "main", "abc123def456");
348        detector.add_requirement(test_resource_id("lib1"), "app2", "Main", "abc123def456");
349        detector.add_requirement(test_resource_id("lib1"), "app3", "MAIN", "abc123def456");
350
351        let conflicts = detector.detect_conflicts();
352        assert_eq!(
353            conflicts.len(),
354            0,
355            "Git refs differing only by case should be compatible (case-insensitive filesystems)"
356        );
357
358        // Mixed case with different branch names should still conflict
359        let mut detector2 = ConflictDetector::new();
360        detector2.add_requirement(test_resource_id("lib2"), "app1", "Main", "abc123def456");
361        detector2.add_requirement(test_resource_id("lib2"), "app2", "Develop", "999888777666");
362
363        let conflicts2 = detector2.detect_conflicts();
364        assert_eq!(
365            conflicts2.len(),
366            1,
367            "Different branch names should conflict regardless of case"
368        );
369    }
370
371    #[test]
372    fn test_resolve_conflicts() {
373        let mut detector = ConflictDetector::new();
374        let lib1_id = test_resource_id("lib1");
375        detector.add_requirement(lib1_id.clone(), "app1", "^1.0.0", "abc123def456");
376        detector.add_requirement(lib1_id.clone(), "app2", "^1.2.0", "abc123def456");
377
378        let mut available = HashMap::new();
379        available.insert(
380            lib1_id.clone(),
381            vec![
382                Version::parse("1.0.0").unwrap(),
383                Version::parse("1.2.0").unwrap(),
384                Version::parse("1.5.0").unwrap(),
385                Version::parse("2.0.0").unwrap(),
386            ],
387        );
388
389        let resolved = detector.resolve_conflicts(&available).unwrap();
390        assert_eq!(resolved.get(&lib1_id), Some(&Version::parse("1.5.0").unwrap()));
391    }
392
393    #[test]
394    fn test_circular_dependency_detection() {
395        let mut detector = CircularDependencyDetector::new();
396
397        // Create a cycle: A -> B -> C -> A
398        detector.add_dependency("A", "B");
399        detector.add_dependency("B", "C");
400        detector.add_dependency("C", "A");
401
402        let cycles = detector.detect_cycles();
403        assert_eq!(cycles.len(), 1);
404        assert!(cycles[0].contains(&"A".to_string()));
405        assert!(cycles[0].contains(&"B".to_string()));
406        assert!(cycles[0].contains(&"C".to_string()));
407    }
408
409    #[test]
410    fn test_no_circular_dependencies() {
411        let mut detector = CircularDependencyDetector::new();
412
413        // Create a DAG: A -> B -> C
414        detector.add_dependency("A", "B");
415        detector.add_dependency("B", "C");
416        detector.add_dependency("A", "C");
417
418        let cycles = detector.detect_cycles();
419        assert_eq!(cycles.len(), 0);
420    }
421
422    #[test]
423    fn test_conflict_display() {
424        let conflict = VersionConflict {
425            resource: ResourceId::new(
426                "test-lib",
427                Some("test-source"),
428                Some("claude-code"),
429                crate::core::ResourceType::Agent,
430                crate::utils::EMPTY_VARIANT_INPUTS_HASH.to_string(),
431            ),
432            conflicting_requirements: vec![
433                ConflictingRequirement {
434                    required_by: "app1".to_string(),
435                    requirement: "^1.0.0".to_string(),
436                    resolved_sha: "abc123def456".to_string(),
437                    resolved_version: Some(Version::parse("1.5.0").unwrap()),
438                    parent_version_constraint: None,
439                    parent_resolved_sha: None,
440                },
441                ConflictingRequirement {
442                    required_by: "app2".to_string(),
443                    requirement: "^2.0.0".to_string(),
444                    resolved_sha: "999888777666".to_string(),
445                    resolved_version: None,
446                    parent_version_constraint: None,
447                    parent_resolved_sha: None,
448                },
449            ],
450        };
451
452        let display = format!("{}", conflict);
453        // Check that the conflict is displayed with SHA-based grouping
454        assert!(display.contains("test-lib"));
455        assert!(display.contains("app1"));
456        assert!(display.contains("app2"));
457        assert!(display.contains("^1.0.0"));
458        assert!(display.contains("^2.0.0"));
459        // Check that SHAs are displayed (first 8 chars)
460        assert!(display.contains("abc123de"));
461        assert!(display.contains("99988877"));
462    }
463
464    #[test]
465    fn test_head_with_specific_version_conflict() {
466        let mut detector = ConflictDetector::new();
467
468        // HEAD (unspecified) mixed with specific version should conflict
469        detector.add_requirement(test_resource_id("lib1"), "app1", "HEAD", "abc123def456");
470        detector.add_requirement(test_resource_id("lib1"), "app2", "^1.0.0", "999888777666");
471
472        let conflicts = detector.detect_conflicts();
473        assert_eq!(conflicts.len(), 1, "HEAD mixed with specific version should conflict");
474
475        // "*" with any specific range is compatible (intersection is non-empty)
476        let mut detector2 = ConflictDetector::new();
477        detector2.add_requirement(test_resource_id("lib2"), "app1", "*", "abc123def456");
478        detector2.add_requirement(test_resource_id("lib2"), "app2", "^1.0.0", "abc123def456");
479
480        let conflicts = detector2.detect_conflicts();
481        assert_eq!(
482            conflicts.len(),
483            0,
484            "* should be compatible with ^1.0.0 (intersection is [1.0.0, 2.0.0))"
485        );
486
487        // "*" with ~2.1.0 is also compatible (intersection is [2.1.0, 2.2.0))
488        let mut detector3 = ConflictDetector::new();
489        detector3.add_requirement(test_resource_id("lib3"), "app1", "*", "abc123def456");
490        detector3.add_requirement(test_resource_id("lib3"), "app2", "~2.1.0", "abc123def456");
491
492        let conflicts = detector3.detect_conflicts();
493        assert_eq!(
494            conflicts.len(),
495            0,
496            "* should be compatible with ~2.1.0 (intersection is [2.1.0, 2.2.0))"
497        );
498    }
499
500    #[test]
501    fn test_mixed_semver_and_git_refs() {
502        let mut detector = ConflictDetector::new();
503
504        // Mix of semver and git branch - should be incompatible
505        detector.add_requirement(test_resource_id("lib1"), "app1", "^1.0.0", "abc123def456");
506        detector.add_requirement(test_resource_id("lib1"), "app2", "main", "999888777666");
507
508        let conflicts = detector.detect_conflicts();
509        assert_eq!(conflicts.len(), 1, "Mixed semver and git ref should be detected as conflict");
510
511        // Test with exact version and git tag
512        let mut detector2 = ConflictDetector::new();
513        detector2.add_requirement(test_resource_id("lib2"), "app1", "v1.0.0", "abc123def456");
514        detector2.add_requirement(test_resource_id("lib2"), "app2", "develop", "999888777666");
515
516        let conflicts2 = detector2.detect_conflicts();
517        assert_eq!(conflicts2.len(), 1, "Exact version with git branch should conflict");
518    }
519
520    #[test]
521    fn test_duplicate_requirements_same_version() {
522        let mut detector = ConflictDetector::new();
523
524        // Multiple resources requiring the same exact version
525        detector.add_requirement(test_resource_id("lib1"), "app1", "v1.0.0", "abc123def456");
526        detector.add_requirement(test_resource_id("lib1"), "app2", "v1.0.0", "abc123def456");
527        detector.add_requirement(test_resource_id("lib1"), "app3", "v1.0.0", "abc123def456");
528
529        let conflicts = detector.detect_conflicts();
530        assert_eq!(conflicts.len(), 0, "Same version requirements should not conflict");
531    }
532
533    #[test]
534    fn test_exact_version_conflicts() {
535        let mut detector = ConflictDetector::new();
536
537        // Different exact versions - definitely incompatible
538        detector.add_requirement(test_resource_id("lib1"), "app1", "v1.0.0", "abc123def456");
539        detector.add_requirement(test_resource_id("lib1"), "app2", "v2.0.0", "999888777666");
540
541        let conflicts = detector.detect_conflicts();
542        assert_eq!(conflicts.len(), 1, "Different exact versions must conflict");
543        assert_eq!(conflicts[0].conflicting_requirements.len(), 2);
544    }
545
546    #[test]
547    fn test_resolve_conflicts_missing_resource() {
548        let mut detector = ConflictDetector::new();
549        detector.add_requirement(test_resource_id("lib1"), "app1", "^1.0.0", "abc123def456");
550
551        let available = HashMap::new(); // Empty - missing lib1
552
553        let result = detector.resolve_conflicts(&available);
554        assert!(result.is_err(), "Should error when resource not in available versions");
555        let err_msg = result.unwrap_err().to_string();
556        assert!(err_msg.contains("No versions available"), "Error should mention missing versions");
557    }
558
559    #[test]
560    fn test_resolve_conflicts_with_incompatible_ranges() {
561        let mut detector = ConflictDetector::new();
562        let lib1_id = test_resource_id("lib1");
563        detector.add_requirement(lib1_id.clone(), "app1", "^1.0.0", "abc123def456");
564        detector.add_requirement(lib1_id.clone(), "app2", "^2.0.0", "999888777666");
565
566        let mut available = HashMap::new();
567        available.insert(
568            lib1_id,
569            vec![Version::parse("1.5.0").unwrap(), Version::parse("2.3.0").unwrap()],
570        );
571
572        let result = detector.resolve_conflicts(&available);
573        assert!(result.is_err(), "Should error when requirements are incompatible");
574        let err_msg = result.unwrap_err().to_string();
575        assert!(
576            err_msg.contains("Unable to resolve version conflicts"),
577            "Error should mention conflict resolution failure"
578        );
579    }
580
581    #[test]
582    fn test_resolve_conflicts_no_matching_version() {
583        let mut detector = ConflictDetector::new();
584        let lib1_id = test_resource_id("lib1");
585        detector.add_requirement(lib1_id.clone(), "app1", "^3.0.0", "abc123def456"); // Requires 3.x
586
587        let mut available = HashMap::new();
588        available.insert(
589            lib1_id,
590            vec![Version::parse("1.0.0").unwrap(), Version::parse("2.0.0").unwrap()],
591        );
592
593        let result = detector.resolve_conflicts(&available);
594        assert!(result.is_err(), "Should error when no version satisfies requirement");
595        let err_msg = result.unwrap_err().to_string();
596        assert!(
597            err_msg.contains("No version satisfies"),
598            "Error should mention no matching version: {}",
599            err_msg
600        );
601    }
602
603    #[test]
604    fn test_conflict_aggregated_error_message() {
605        let mut detector = ConflictDetector::new();
606        detector.add_requirement(test_resource_id("lib1"), "app1", "^1.0.0", "abc123def456");
607        detector.add_requirement(test_resource_id("lib1"), "app2", "^2.0.0", "999888777666");
608        detector.add_requirement(test_resource_id("lib2"), "app1", "main", "111222333444");
609        detector.add_requirement(test_resource_id("lib2"), "app3", "develop", "555666777888");
610
611        let conflicts = detector.detect_conflicts();
612        assert_eq!(conflicts.len(), 2, "Should detect both conflicts");
613
614        // Verify the conflicts contain proper information
615        let lib1_conflict = conflicts.iter().find(|c| c.resource.to_string().contains("lib1"));
616        assert!(lib1_conflict.is_some(), "Should have lib1 conflict");
617        assert_eq!(
618            lib1_conflict.unwrap().conflicting_requirements.len(),
619            2,
620            "lib1 should have 2 conflicting requirements"
621        );
622
623        let lib2_conflict = conflicts.iter().find(|c| c.resource.to_string().contains("lib2"));
624        assert!(lib2_conflict.is_some(), "Should have lib2 conflict");
625        assert_eq!(
626            lib2_conflict.unwrap().conflicting_requirements.len(),
627            2,
628            "lib2 should have 2 conflicting requirements"
629        );
630    }
631
632    #[test]
633    fn test_multi_comparator_compatible() {
634        let mut detector = ConflictDetector::new();
635
636        // ">=5.0.0, <6.0.0" should be compatible with ">=5.5.0"
637        // Intersection is [5.5.0, 6.0.0)
638        detector.add_requirement(
639            test_resource_id("lib1"),
640            "app1",
641            ">=5.0.0, <6.0.0",
642            "abc123def456",
643        );
644        detector.add_requirement(test_resource_id("lib1"), "app2", ">=5.5.0", "abc123def456");
645
646        let conflicts = detector.detect_conflicts();
647        assert_eq!(
648            conflicts.len(),
649            0,
650            "Multi-comparator ranges with non-empty intersection should be compatible"
651        );
652    }
653
654    #[test]
655    fn test_multi_comparator_incompatible() {
656        let mut detector = ConflictDetector::new();
657
658        // ">=5.0.0, <6.0.0" should conflict with ">=7.0.0"
659        // Intersection is empty
660        detector.add_requirement(
661            test_resource_id("lib1"),
662            "app1",
663            ">=5.0.0, <6.0.0",
664            "abc123def456",
665        );
666        detector.add_requirement(test_resource_id("lib1"), "app2", ">=7.0.0", "999888777666");
667
668        let conflicts = detector.detect_conflicts();
669        assert_eq!(
670            conflicts.len(),
671            1,
672            "Multi-comparator ranges with empty intersection should conflict"
673        );
674    }
675
676    #[test]
677    fn test_tilde_operator_variants() {
678        let mut detector1 = ConflictDetector::new();
679
680        // ~1 means [1.0.0, 2.0.0) - should be compatible with ^1.5.0 [1.5.0, 2.0.0)
681        detector1.add_requirement(test_resource_id("lib1"), "app1", "~1", "abc123def456");
682        detector1.add_requirement(test_resource_id("lib1"), "app2", "^1.5.0", "abc123def456");
683
684        let conflicts1 = detector1.detect_conflicts();
685        assert_eq!(
686            conflicts1.len(),
687            0,
688            "~1 should be compatible with ^1.5.0 (intersection is [1.5.0, 2.0.0))"
689        );
690
691        let mut detector2 = ConflictDetector::new();
692
693        // ~1.2 means [1.2.0, 1.3.0) - should conflict with ^1.5.0 [1.5.0, 2.0.0)
694        detector2.add_requirement(test_resource_id("lib2"), "app1", "~1.2", "abc123def456");
695        detector2.add_requirement(test_resource_id("lib2"), "app2", "^1.5.0", "999888777666");
696
697        let conflicts2 = detector2.detect_conflicts();
698        assert_eq!(conflicts2.len(), 1, "~1.2 should conflict with ^1.5.0 (disjoint ranges)");
699
700        let mut detector3 = ConflictDetector::new();
701
702        // ~1.2.3 means [1.2.3, 1.3.0) - should be compatible with >=1.2.0
703        detector3.add_requirement(test_resource_id("lib3"), "app1", "~1.2.3", "abc123def456");
704        detector3.add_requirement(test_resource_id("lib3"), "app2", ">=1.2.0", "abc123def456");
705
706        let conflicts3 = detector3.detect_conflicts();
707        assert_eq!(conflicts3.len(), 0, "~1.2.3 should be compatible with >=1.2.0");
708    }
709
710    #[test]
711    fn test_caret_zero_zero_patch() {
712        let mut detector1 = ConflictDetector::new();
713
714        // ^0.0.3 means [0.0.3, 0.0.4) - should be compatible with >=0.0.3, <0.0.5
715        detector1.add_requirement(test_resource_id("lib1"), "app1", "^0.0.3", "abc123def456");
716        detector1.add_requirement(
717            test_resource_id("lib1"),
718            "app2",
719            ">=0.0.3, <0.0.5",
720            "abc123def456",
721        );
722
723        let conflicts1 = detector1.detect_conflicts();
724        assert_eq!(
725            conflicts1.len(),
726            0,
727            "^0.0.3 should be compatible with >=0.0.3, <0.0.5 (intersection is [0.0.3, 0.0.4))"
728        );
729
730        let mut detector2 = ConflictDetector::new();
731
732        // ^0.0.3 means [0.0.3, 0.0.4) - should conflict with ^0.0.5 [0.0.5, 0.0.6)
733        detector2.add_requirement(test_resource_id("lib2"), "app1", "^0.0.3", "abc123def456");
734        detector2.add_requirement(test_resource_id("lib2"), "app2", "^0.0.5", "999888777666");
735
736        let conflicts2 = detector2.detect_conflicts();
737        assert_eq!(conflicts2.len(), 1, "^0.0.3 should conflict with ^0.0.5 (disjoint ranges)");
738    }
739
740    #[test]
741    fn test_caret_zero_variants() {
742        let mut detector1 = ConflictDetector::new();
743
744        // ^0 means [0.0.0, 1.0.0) - should be compatible with ^0.5.0 [0.5.0, 0.6.0)
745        detector1.add_requirement(test_resource_id("lib1"), "app1", "^0", "abc123def456");
746        detector1.add_requirement(test_resource_id("lib1"), "app2", "^0.5.0", "abc123def456");
747
748        let conflicts1 = detector1.detect_conflicts();
749        assert_eq!(
750            conflicts1.len(),
751            0,
752            "^0 should be compatible with ^0.5.0 (intersection is [0.5.0, 0.6.0))"
753        );
754
755        let mut detector2 = ConflictDetector::new();
756
757        // ^0.0 means [0.0.0, 0.1.0) - should conflict with ^0.5.0 [0.5.0, 0.6.0)
758        detector2.add_requirement(test_resource_id("lib2"), "app1", "^0.0", "abc123def456");
759        detector2.add_requirement(test_resource_id("lib2"), "app2", "^0.5.0", "999888777666");
760
761        let conflicts2 = detector2.detect_conflicts();
762        assert_eq!(conflicts2.len(), 1, "^0.0 should conflict with ^0.5.0 (disjoint ranges)");
763    }
764
765    #[test]
766    fn test_prerelease_versions() {
767        let mut detector1 = ConflictDetector::new();
768
769        // =1.0.0-beta.1 should conflict with =1.0.0 (different versions)
770        detector1.add_requirement(
771            test_resource_id("lib1"),
772            "app1",
773            "=1.0.0-beta.1",
774            "abc123def456",
775        );
776        detector1.add_requirement(test_resource_id("lib1"), "app2", "=1.0.0", "999888777666");
777
778        let conflicts1 = detector1.detect_conflicts();
779        assert_eq!(
780            conflicts1.len(),
781            1,
782            "=1.0.0-beta.1 should conflict with =1.0.0 (different prerelease)"
783        );
784
785        let mut detector2 = ConflictDetector::new();
786
787        // =1.0.0-beta.1 should be compatible with itself
788        detector2.add_requirement(
789            test_resource_id("lib2"),
790            "app1",
791            "=1.0.0-beta.1",
792            "abc123def456",
793        );
794        detector2.add_requirement(
795            test_resource_id("lib2"),
796            "app2",
797            "=1.0.0-beta.1",
798            "abc123def456",
799        );
800
801        let conflicts2 = detector2.detect_conflicts();
802        assert_eq!(conflicts2.len(), 0, "Same prerelease version should be compatible");
803
804        let mut detector3 = ConflictDetector::new();
805
806        // >=1.0.0-beta should be compatible with >=1.0.0-alpha (intersection exists)
807        detector3.add_requirement(test_resource_id("lib3"), "app1", ">=1.0.0-beta", "abc123def456");
808        detector3.add_requirement(
809            test_resource_id("lib3"),
810            "app2",
811            ">=1.0.0-alpha",
812            "abc123def456",
813        );
814
815        let conflicts3 = detector3.detect_conflicts();
816        assert_eq!(conflicts3.len(), 0, ">=1.0.0-beta should be compatible with >=1.0.0-alpha");
817    }
818
819    #[test]
820    fn test_high_version_ranges() {
821        let mut detector = ConflictDetector::new();
822
823        // Test ranges well above typical test versions (>3.0.0)
824        detector.add_requirement(
825            test_resource_id("lib1"),
826            "app1",
827            ">=5.0.0, <10.0.0",
828            "abc123def456",
829        );
830        detector.add_requirement(test_resource_id("lib1"), "app2", "^7.5.0", "abc123def456");
831
832        let conflicts = detector.detect_conflicts();
833        assert_eq!(
834            conflicts.len(),
835            0,
836            "High version ranges should work correctly (intersection is [7.5.0, 8.0.0))"
837        );
838
839        let mut detector2 = ConflictDetector::new();
840
841        // Test conflicting high version ranges
842        detector2.add_requirement(test_resource_id("lib2"), "app1", ">=100.0.0", "abc123def456");
843        detector2.add_requirement(test_resource_id("lib2"), "app2", "<50.0.0", "999888777666");
844
845        let conflicts2 = detector2.detect_conflicts();
846        assert_eq!(conflicts2.len(), 1, "Disjoint high version ranges should conflict");
847    }
848
849    #[test]
850    fn test_cross_prefix_same_sha_no_conflict() {
851        let mut detector = ConflictDetector::new();
852
853        // Different version prefixes resolving to same SHA should NOT conflict
854        detector.add_requirement(test_resource_id("lib1"), "app1", "agents-v1.0.0", "abc123def456");
855        detector.add_requirement(
856            test_resource_id("lib1"),
857            "app2",
858            "snippets-v1.0.0",
859            "abc123def456",
860        );
861
862        let conflicts = detector.detect_conflicts();
863        assert_eq!(
864            conflicts.len(),
865            0,
866            "Different version prefixes resolving to same SHA should not conflict"
867        );
868    }
869
870    #[test]
871    fn test_cross_prefix_different_sha_conflicts() {
872        let mut detector = ConflictDetector::new();
873
874        // Different version prefixes resolving to different SHAs SHOULD conflict
875        detector.add_requirement(test_resource_id("lib1"), "app1", "agents-v1.0.0", "abc123def456");
876        detector.add_requirement(
877            test_resource_id("lib1"),
878            "app2",
879            "snippets-v1.0.0",
880            "999888777666",
881        );
882
883        let conflicts = detector.detect_conflicts();
884        assert_eq!(
885            conflicts.len(),
886            1,
887            "Different version prefixes resolving to different SHAs should conflict"
888        );
889    }
890
891    #[test]
892    fn test_many_requirements_same_sha_no_conflict() {
893        let mut detector = ConflictDetector::new();
894
895        // Multiple requirements with same SHA should NOT conflict
896        detector.add_requirement(test_resource_id("lib1"), "app1", "^1.0.0", "abc123def456");
897        detector.add_requirement(test_resource_id("lib1"), "app2", "^1.2.0", "abc123def456");
898        detector.add_requirement(test_resource_id("lib1"), "app3", "~1.5.0", "abc123def456");
899        detector.add_requirement(
900            test_resource_id("lib1"),
901            "app4",
902            ">=1.0.0, <2.0.0",
903            "abc123def456",
904        );
905        detector.add_requirement(test_resource_id("lib1"), "app5", "v1.8.0", "abc123def456");
906
907        let conflicts = detector.detect_conflicts();
908        assert_eq!(
909            conflicts.len(),
910            0,
911            "Multiple requirements with same SHA should not conflict, regardless of version constraints"
912        );
913    }
914}