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    DaemonOutdated { mcp: Version, daemon: Version },
8}
9
10/// Compare origin-mcp's compiled version against the daemon's reported version.
11/// Treats the daemon being minor/major AHEAD as `McpOutdated` (a patch-ahead
12/// daemon is ignored: release-please bumps patches frequently and they're
13/// API-compatible). Flags `DaemonOutdated` when the running daemon is strictly
14/// OLDER than origin-mcp at any level, including patch (it was not restarted
15/// after an upgrade). Unparseable daemon versions are treated as Compatible
16/// (handshake never blocks).
17pub fn compare(mcp_version: &str, daemon_version: &str) -> VersionStatus {
18    // Defensive on both sides: if either version fails to parse, fall back to
19    // Compatible. Never panic at startup over a malformed version string.
20    let mcp = match Version::parse(mcp_version) {
21        Ok(v) => v,
22        Err(_) => return VersionStatus::Compatible,
23    };
24    let daemon = match Version::parse(daemon_version) {
25        Ok(v) => v,
26        Err(_) => return VersionStatus::Compatible,
27    };
28    if daemon.major > mcp.major || (daemon.major == mcp.major && daemon.minor > mcp.minor) {
29        VersionStatus::McpOutdated { mcp, daemon }
30    } else if mcp > daemon {
31        // mcp strictly newer than the running daemon (any level, incl. patch):
32        // the daemon binary on disk may already be new, but the running PROCESS
33        // is stale — it was not restarted after an upgrade.
34        VersionStatus::DaemonOutdated { mcp, daemon }
35    } else {
36        VersionStatus::Compatible
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    #[test]
45    fn mcp_minor_ahead_daemon_outdated() {
46        // Was previously "Compatible"; now the daemon is flagged as stale.
47        assert!(matches!(
48            compare("0.2.0", "0.1.5"),
49            VersionStatus::DaemonOutdated { .. }
50        ));
51    }
52
53    #[test]
54    fn daemon_patch_behind_outdated() {
55        // The common post-upgrade case: new mcp, daemon not restarted.
56        assert!(matches!(
57            compare("0.8.3", "0.8.2"),
58            VersionStatus::DaemonOutdated { .. }
59        ));
60    }
61
62    #[test]
63    fn daemon_patch_ahead_compatible() {
64        // mcp slightly behind daemon by patch is fine (unchanged).
65        assert_eq!(compare("0.8.2", "0.8.3"), VersionStatus::Compatible);
66    }
67
68    #[test]
69    fn equal_versions_compatible() {
70        assert_eq!(compare("0.8.3", "0.8.3"), VersionStatus::Compatible);
71    }
72
73    #[test]
74    fn daemon_minor_ahead_outdated() {
75        assert!(matches!(
76            compare("0.1.2", "0.2.0"),
77            VersionStatus::McpOutdated { .. }
78        ));
79    }
80
81    #[test]
82    fn daemon_major_ahead_outdated() {
83        assert!(matches!(
84            compare("0.1.2", "1.0.0"),
85            VersionStatus::McpOutdated { .. }
86        ));
87    }
88
89    #[test]
90    fn patch_drift_compatible() {
91        assert_eq!(compare("0.1.2", "0.1.5"), VersionStatus::Compatible);
92    }
93
94    #[test]
95    fn unparseable_daemon_version_compatible() {
96        assert_eq!(compare("0.1.2", "garbage"), VersionStatus::Compatible);
97    }
98
99    #[test]
100    fn build_metadata_ignored_in_ordering() {
101        // SemVer 2.0.0: build metadata after `+` is ignored when ordering versions.
102        assert_eq!(compare("0.1.2", "0.1.2+abc"), VersionStatus::Compatible);
103    }
104
105    #[test]
106    fn prerelease_daemon_same_minor_daemon_outdated() {
107        // Daemon on a 0.2.0 pre-release while MCP is on stable 0.2.0:
108        // semver ordering: 0.2.0 > 0.2.0-beta.1 (pre-releases rank lower),
109        // so mcp > daemon triggers DaemonOutdated — the daemon binary is stale.
110        assert!(matches!(
111            compare("0.2.0", "0.2.0-beta.1"),
112            VersionStatus::DaemonOutdated { .. }
113        ));
114    }
115}