1use anyhow::Result;
8use semver::Version;
9use std::collections::{HashMap, HashSet};
10use std::fmt;
11
12use crate::core::AgpmError;
13use crate::lockfile::ResourceId;
14
15#[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 pub required_by: String,
26 pub requirement: String,
28 pub resolved_sha: String,
30 pub resolved_version: Option<Version>,
32 pub parent_version_constraint: Option<String>,
34 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 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
60pub struct ConflictDetector {
62 requirements: HashMap<ResourceId, Vec<ConflictingRequirement>>, }
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 pub fn requirements(&self) -> &HashMap<ResourceId, Vec<ConflictingRequirement>> {
83 &self.requirements
84 }
85
86 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 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 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; }
132
133 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 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 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 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 fn find_best_version(
187 &self,
188 available: &[Version],
189 requirements: &[ConflictingRequirement],
190 ) -> Result<Version> {
191 let mut candidates = available.to_vec();
192
193 for req in requirements {
195 let req_str = &req.requirement;
196 if req_str == "latest" || req_str == "*" {
197 continue; }
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 candidates.sort_by(|a, b| b.cmp(a));
214 Ok(candidates[0].clone())
215 }
216}
217
218pub 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 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 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 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 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 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); 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(); 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"); 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}