Skip to main content

capsule_runtime/
wit_versions.rs

1//! The one version of each `murmur:*` WIT package this host serves, and the WIT levels that
2//! number those sets.
3
4use std::sync::OnceLock;
5
6use murmur_artifact::{parse_wit_levels, WitLevel};
7
8/// Every `murmur:*` WIT package declared under `crates/capsule-runtime/wit/`, paired with the one
9/// version of it this host links and resolves, sorted by package name.
10///
11/// These are the only versions the host accepts. Each interface is resolved or provided under
12/// exactly one instance name, with no fallback to any other version (see `wit/VERSIONING.md`), so
13/// a component naming any other version of one of these packages fails to instantiate.
14/// It is the package set of [`served_wit_level`]; fit is decided against that level, never against
15/// this table directly.
16///
17/// The tests below hold this table equal to the `package` declarations in the WIT tree, to every
18/// interface name the host resolves, and to the newest level in [`WIT_LEVELS_FILE`], so it moves
19/// with a version bump or fails the build.
20pub const SERVED_WIT_PACKAGES: &[(&str, &str)] = &[
21    ("murmur:artifact-manager", "0.1.0"),
22    ("murmur:capsule", "0.1.0"),
23    ("murmur:conversation", "0.2.0"),
24    ("murmur:driver", "0.3.0"),
25    ("murmur:hook", "0.9.0"),
26    ("murmur:host", "0.1.0"),
27    ("murmur:message", "0.1.0"),
28    ("murmur:runtime", "0.4.0"),
29    ("murmur:runtime-guest", "0.1.0"),
30    ("murmur:shell", "0.1.0"),
31    ("murmur:stream", "0.1.0"),
32    ("murmur:task", "0.1.0"),
33    ("murmur:task-io", "0.1.0"),
34    ("murmur:tool", "0.1.0"),
35    ("murmur:tool-registry", "0.1.0"),
36];
37
38/// The repository path of the committed WIT level table, for messages that name it.
39pub const WIT_LEVELS_FILE: &str = "crates/capsule-runtime/wit/levels.txt";
40
41/// Every committed WIT level, oldest first, parsed once from [`WIT_LEVELS_FILE`].
42///
43/// The file is compiled in, and `wit_levels_file_parses` parses the same bytes, so a malformed
44/// table fails the test suite before it can reach this `expect`.
45pub fn wit_levels() -> &'static [WitLevel] {
46    static LEVELS: OnceLock<Vec<WitLevel>> = OnceLock::new();
47    LEVELS.get_or_init(|| {
48        parse_wit_levels(include_str!("../wit/levels.txt"))
49            .unwrap_or_else(|error| panic!("{WIT_LEVELS_FILE} is not a valid level table: {error}"))
50    })
51}
52
53/// The WIT level this host serves: the newest level in [`WIT_LEVELS_FILE`].
54pub fn served_wit_level() -> &'static WitLevel {
55    wit_levels()
56        .last()
57        .expect("parse_wit_levels refuses a table with no level")
58}
59
60#[cfg(test)]
61mod tests {
62    use std::collections::BTreeSet;
63    use std::path::Path;
64
65    use super::{served_wit_level, wit_levels, SERVED_WIT_PACKAGES, WIT_LEVELS_FILE};
66
67    fn served_set() -> BTreeSet<(String, String)> {
68        SERVED_WIT_PACKAGES
69            .iter()
70            .map(|(package, version)| (package.to_string(), version.to_string()))
71            .collect()
72    }
73
74    /// The table names each package once, and exactly the package versions the WIT tree declares.
75    /// The tree is what `wit-bindgen` generates the host's bindings from, so a version bump in it
76    /// that the table does not follow fails here.
77    #[test]
78    fn wit_versions_table_matches_the_wit_tree() {
79        let declared = murmur_artifact::wit_package_declarations(Path::new(concat!(
80            env!("CARGO_MANIFEST_DIR"),
81            "/wit"
82        )))
83        .unwrap();
84        assert!(!declared.is_empty(), "no murmur package declarations found");
85
86        let mut packages: Vec<&str> = SERVED_WIT_PACKAGES.iter().map(|(p, _)| *p).collect();
87        packages.sort_unstable();
88        packages.dedup();
89        assert_eq!(
90            packages.len(),
91            SERVED_WIT_PACKAGES.len(),
92            "a package appears twice in SERVED_WIT_PACKAGES"
93        );
94
95        assert_eq!(served_set(), declared);
96    }
97
98    /// Every instance name the host resolves or provides is at its package's served version. A
99    /// resolver constant moved without the table, or the table without the constant, fails here.
100    #[test]
101    fn wit_versions_table_covers_every_resolved_interface() {
102        let resolved = [
103            crate::hooks::LIFECYCLE_IFACE,
104            crate::process_driver::PROCESS_DRIVER_IFACE,
105            crate::runtime::WIT_CAPSULE_IFACE_VERSIONED,
106            crate::runtime::WIT_TOOL_IFACE_VERSIONED,
107            crate::runtime::WIT_TOOL_REGISTRY_IFACE,
108            crate::runtime::WIT_STREAM_EVENTS_IFACE,
109            crate::runtime::WIT_TASK_IFACE,
110            crate::conversation_import::CONVERSATION_IFACE_VERSIONED,
111            crate::inference_import::INFERENCE_IFACE_VERSIONED,
112            crate::task_io_import::TASK_IO_IFACE_VERSIONED,
113            crate::tokens_import::TOKENS_IFACE_VERSIONED,
114        ];
115        let served = served_set();
116        for interface in resolved {
117            let (package, rest) = interface
118                .split_once('/')
119                .unwrap_or_else(|| panic!("{interface} names no interface"));
120            let (_, version) = rest
121                .rsplit_once('@')
122                .unwrap_or_else(|| panic!("{interface} carries no version"));
123            assert!(
124                served.contains(&(package.to_string(), version.to_string())),
125                "{interface} is not at a version SERVED_WIT_PACKAGES lists for {package}"
126            );
127        }
128    }
129
130    /// Doctor compares versions as exact strings, while wasmtime's linker treats two names that
131    /// differ only in a non-zero patch field as semver-compatible and links them. The two answers
132    /// agree only while murmur never ships a package at a non-zero patch.
133    #[test]
134    fn wit_versions_every_patch_field_is_zero() {
135        for (package, version) in SERVED_WIT_PACKAGES {
136            let patch = version
137                .split('.')
138                .nth(2)
139                .unwrap_or_else(|| panic!("{package}@{version} has no patch field"));
140            assert_eq!(patch, "0", "{package}@{version} has a non-zero patch field");
141        }
142    }
143
144    #[test]
145    fn wit_levels_file_parses() {
146        murmur_artifact::parse_wit_levels(include_str!("../wit/levels.txt"))
147            .unwrap_or_else(|error| panic!("{WIT_LEVELS_FILE}: {error}"));
148        assert!(!wit_levels().is_empty());
149    }
150
151    #[test]
152    fn wit_levels_served_level_is_level_1() {
153        assert_eq!(served_wit_level().level, 1);
154    }
155
156    /// The served table is the newest level's set. A version moved in one but not the other
157    /// fails here with the instruction for the bump: append the next level.
158    #[test]
159    fn wit_levels_newest_level_is_the_served_table() {
160        let newest = served_wit_level();
161        let level: BTreeSet<(String, String)> = newest.packages.iter().cloned().collect();
162        let served = served_set();
163        if level == served {
164            return;
165        }
166        let packages: BTreeSet<&str> = level
167            .iter()
168            .chain(served.iter())
169            .map(|(package, _)| package.as_str())
170            .collect();
171        let differences: Vec<String> = packages
172            .into_iter()
173            .filter_map(|package| {
174                let at_level = newest.version_of(package);
175                let in_table = SERVED_WIT_PACKAGES
176                    .iter()
177                    .find(|(p, _)| *p == package)
178                    .map(|(_, v)| *v);
179                (at_level != in_table).then(|| {
180                    format!(
181                        "  {package}: {} in level {} → {} in SERVED_WIT_PACKAGES",
182                        at_level.unwrap_or("absent"),
183                        newest.level,
184                        in_table.unwrap_or("absent")
185                    )
186                })
187            })
188            .collect();
189        let next = newest.level + 1;
190        panic!(
191            "SERVED_WIT_PACKAGES no longer equals WIT level {level} in {WIT_LEVELS_FILE}:\n{}\n\
192             append level {next} to {WIT_LEVELS_FILE}, listing every package at its new version \
193             (one `{next} <package>@<version>` line per package); never edit, renumber or reuse \
194             level {level}",
195            differences.join("\n"),
196            level = newest.level,
197        );
198    }
199}