Skip to main content

origin_mcp/
version_check.rs

1use semver::Version;
2
3#[derive(Debug, PartialEq)]
4pub enum VersionStatus {
5    Compatible,
6    McpOutdated { mcp: Version, daemon: Version },
7}
8
9/// Compare origin-mcp's compiled version against the daemon's reported version.
10/// Treats minor/major drift as `McpOutdated`. Patch differences are ignored
11/// (release-please bumps patches frequently and they're API-compatible).
12/// Unparseable daemon versions are treated as Compatible (handshake never blocks).
13pub fn compare(mcp_version: &str, daemon_version: &str) -> VersionStatus {
14    // Defensive on both sides: if either version fails to parse, fall back to
15    // Compatible. Never panic at startup over a malformed version string.
16    let mcp = match Version::parse(mcp_version) {
17        Ok(v) => v,
18        Err(_) => return VersionStatus::Compatible,
19    };
20    let daemon = match Version::parse(daemon_version) {
21        Ok(v) => v,
22        Err(_) => return VersionStatus::Compatible,
23    };
24    if daemon.major > mcp.major || (daemon.major == mcp.major && daemon.minor > mcp.minor) {
25        VersionStatus::McpOutdated { mcp, daemon }
26    } else {
27        VersionStatus::Compatible
28    }
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn equal_versions_compatible() {
37        assert_eq!(compare("0.1.2", "0.1.2"), VersionStatus::Compatible);
38    }
39
40    #[test]
41    fn mcp_ahead_compatible() {
42        assert_eq!(compare("0.2.0", "0.1.5"), VersionStatus::Compatible);
43    }
44
45    #[test]
46    fn daemon_minor_ahead_outdated() {
47        assert!(matches!(
48            compare("0.1.2", "0.2.0"),
49            VersionStatus::McpOutdated { .. }
50        ));
51    }
52
53    #[test]
54    fn daemon_major_ahead_outdated() {
55        assert!(matches!(
56            compare("0.1.2", "1.0.0"),
57            VersionStatus::McpOutdated { .. }
58        ));
59    }
60
61    #[test]
62    fn patch_drift_compatible() {
63        assert_eq!(compare("0.1.2", "0.1.5"), VersionStatus::Compatible);
64    }
65
66    #[test]
67    fn unparseable_daemon_version_compatible() {
68        assert_eq!(compare("0.1.2", "garbage"), VersionStatus::Compatible);
69    }
70
71    #[test]
72    fn build_metadata_ignored_in_ordering() {
73        // SemVer 2.0.0: build metadata after `+` is ignored when ordering versions.
74        assert_eq!(compare("0.1.2", "0.1.2+abc"), VersionStatus::Compatible);
75    }
76
77    #[test]
78    fn prerelease_daemon_same_minor_compatible() {
79        // Daemon on a 0.2.0 pre-release while MCP is on stable 0.2.0:
80        // same major+minor → Compatible. Our gate is major/minor-only;
81        // semver pre-release ordering is irrelevant at that granularity.
82        assert_eq!(compare("0.2.0", "0.2.0-beta.1"), VersionStatus::Compatible);
83    }
84}