Skip to main content

mars_agents/resolve/
compat.rs

1use super::VersionConstraint;
2use semver::Version;
3
4/// Result of comparing two version constraints.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CompatibilityResult {
7    /// Both constraints resolve to the same version.
8    Compatible,
9    /// Constraints might resolve differently (Latest vs pinned).
10    PotentiallyConflicting,
11    /// Constraints cannot resolve to the same version.
12    Conflicting,
13}
14
15impl VersionConstraint {
16    /// Check if this constraint is compatible with another.
17    ///
18    /// Matrix:
19    /// - Latest + Latest => Compatible
20    /// - Semver(same) + Semver(same) => Compatible
21    /// - RefPin(same) + RefPin(same) => Compatible
22    /// - Latest + Semver/RefPin => PotentiallyConflicting
23    /// - Different Semver/RefPin => Conflicting
24    /// - Semver + RefPin => Conflicting
25    pub fn compatible_with(&self, other: &VersionConstraint) -> CompatibilityResult {
26        use CompatibilityResult::{Compatible, Conflicting, PotentiallyConflicting};
27        use VersionConstraint::{Latest, RefPin, Semver};
28
29        match (self, other) {
30            (Latest, Latest) => Compatible,
31            (Latest, Semver(_) | RefPin(_)) | (Semver(_) | RefPin(_), Latest) => {
32                PotentiallyConflicting
33            }
34            (Semver(lhs), Semver(rhs)) => {
35                if lhs == rhs {
36                    Compatible
37                } else {
38                    Conflicting
39                }
40            }
41            (RefPin(lhs), RefPin(rhs)) => {
42                if lhs == rhs {
43                    Compatible
44                } else {
45                    Conflicting
46                }
47            }
48            (Semver(_), RefPin(_)) | (RefPin(_), Semver(_)) => Conflicting,
49        }
50    }
51
52    /// Check compatibility against a concrete resolved version.
53    ///
54    /// This is stricter than pure syntactic comparison for semver constraints:
55    /// two different semver expressions are compatible when both accept the
56    /// already-resolved concrete version.
57    pub fn compatible_with_resolved(
58        &self,
59        other: &VersionConstraint,
60        resolved_version: Option<&Version>,
61    ) -> CompatibilityResult {
62        use CompatibilityResult::{Compatible, Conflicting, PotentiallyConflicting};
63        use VersionConstraint::{Latest, Semver};
64
65        match (self, other) {
66            (Semver(lhs), Semver(rhs)) => {
67                if lhs == rhs {
68                    Compatible
69                } else if let Some(version) = resolved_version {
70                    if lhs.matches(version) && rhs.matches(version) {
71                        Compatible
72                    } else {
73                        Conflicting
74                    }
75                } else {
76                    Conflicting
77                }
78            }
79            // Latest vs Semver: if the resolved version satisfies the semver
80            // constraint, they agree on the same concrete version — no drift.
81            (Latest, Semver(req)) | (Semver(req), Latest) => {
82                if let Some(version) = resolved_version {
83                    if req.matches(version) {
84                        Compatible
85                    } else {
86                        PotentiallyConflicting
87                    }
88                } else {
89                    PotentiallyConflicting
90                }
91            }
92            _ => self.compatible_with(other),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::CompatibilityResult;
100    use crate::resolve::VersionConstraint;
101    use semver::Version;
102
103    fn semver(req: &str) -> VersionConstraint {
104        VersionConstraint::Semver(req.parse().expect("valid semver requirement"))
105    }
106
107    #[test]
108    fn latest_with_latest_is_compatible() {
109        assert_eq!(
110            VersionConstraint::Latest.compatible_with(&VersionConstraint::Latest),
111            CompatibilityResult::Compatible
112        );
113    }
114
115    #[test]
116    fn same_semver_is_compatible() {
117        assert_eq!(
118            semver("^1.2").compatible_with(&semver("^1.2")),
119            CompatibilityResult::Compatible
120        );
121    }
122
123    #[test]
124    fn same_ref_pin_is_compatible() {
125        assert_eq!(
126            VersionConstraint::RefPin("main".into())
127                .compatible_with(&VersionConstraint::RefPin("main".into())),
128            CompatibilityResult::Compatible
129        );
130    }
131
132    #[test]
133    fn latest_with_semver_is_potentially_conflicting() {
134        assert_eq!(
135            VersionConstraint::Latest.compatible_with(&semver(">=1.0.0")),
136            CompatibilityResult::PotentiallyConflicting
137        );
138    }
139
140    #[test]
141    fn latest_with_ref_pin_is_potentially_conflicting() {
142        assert_eq!(
143            VersionConstraint::Latest.compatible_with(&VersionConstraint::RefPin("main".into())),
144            CompatibilityResult::PotentiallyConflicting
145        );
146    }
147
148    #[test]
149    fn different_semver_is_conflicting() {
150        assert_eq!(
151            semver("^1.0").compatible_with(&semver("^2.0")),
152            CompatibilityResult::Conflicting
153        );
154    }
155
156    #[test]
157    fn different_ref_pin_is_conflicting() {
158        assert_eq!(
159            VersionConstraint::RefPin("main".into())
160                .compatible_with(&VersionConstraint::RefPin("release".into())),
161            CompatibilityResult::Conflicting
162        );
163    }
164
165    #[test]
166    fn semver_with_ref_pin_is_conflicting() {
167        assert_eq!(
168            semver("^1.0").compatible_with(&VersionConstraint::RefPin("main".into())),
169            CompatibilityResult::Conflicting
170        );
171    }
172
173    #[test]
174    fn equivalent_semver_syntax_is_compatible_for_resolved_version() {
175        let resolved = Version::new(1, 4, 2);
176        assert_eq!(
177            semver("^1.0").compatible_with_resolved(&semver(">=1.0.0, <2.0.0"), Some(&resolved)),
178            CompatibilityResult::Compatible
179        );
180    }
181
182    #[test]
183    fn incompatible_semver_syntax_is_conflicting_for_resolved_version() {
184        let resolved = Version::new(2, 0, 0);
185        assert_eq!(
186            semver("^1.0").compatible_with_resolved(&semver(">=1.0.0, <2.0.0"), Some(&resolved)),
187            CompatibilityResult::Conflicting
188        );
189    }
190
191    #[test]
192    fn latest_with_semver_compatible_when_resolved_matches() {
193        let resolved = Version::new(0, 2, 1);
194        assert_eq!(
195            VersionConstraint::Latest.compatible_with_resolved(&semver("=0.2.1"), Some(&resolved)),
196            CompatibilityResult::Compatible
197        );
198        // Symmetric
199        assert_eq!(
200            semver("=0.2.1").compatible_with_resolved(&VersionConstraint::Latest, Some(&resolved)),
201            CompatibilityResult::Compatible
202        );
203    }
204
205    #[test]
206    fn latest_with_semver_potentially_conflicting_when_resolved_mismatches() {
207        let resolved = Version::new(0, 3, 0);
208        assert_eq!(
209            VersionConstraint::Latest.compatible_with_resolved(&semver("=0.2.1"), Some(&resolved)),
210            CompatibilityResult::PotentiallyConflicting
211        );
212    }
213
214    #[test]
215    fn latest_with_semver_potentially_conflicting_without_resolved() {
216        assert_eq!(
217            VersionConstraint::Latest.compatible_with_resolved(&semver("=0.2.1"), None),
218            CompatibilityResult::PotentiallyConflicting
219        );
220    }
221}