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.6.0"),
25    ("murmur:hook", "0.9.0"),
26    ("murmur:host", "0.1.0"),
27    ("murmur:message", "0.1.0"),
28    ("murmur:process", "0.1.0"),
29    ("murmur:runtime", "0.4.0"),
30    ("murmur:runtime-guest", "0.1.0"),
31    ("murmur:shell", "0.1.0"),
32    ("murmur:stream", "0.1.0"),
33    ("murmur:task", "0.1.0"),
34    ("murmur:task-io", "0.1.0"),
35    ("murmur:tool", "0.2.0"),
36    ("murmur:tool-registry", "0.2.0"),
37];
38
39/// The repository path of the committed WIT level table, for messages that name it.
40pub const WIT_LEVELS_FILE: &str = "crates/capsule-runtime/wit/levels.txt";
41
42/// Every committed WIT level, oldest first, parsed once from [`WIT_LEVELS_FILE`].
43///
44/// The file is compiled in, and `wit_levels_file_parses` parses the same bytes, so a malformed
45/// table fails the test suite before it can reach this `expect`.
46pub fn wit_levels() -> &'static [WitLevel] {
47    static LEVELS: OnceLock<Vec<WitLevel>> = OnceLock::new();
48    LEVELS.get_or_init(|| {
49        parse_wit_levels(include_str!("../wit/levels.txt"))
50            .unwrap_or_else(|error| panic!("{WIT_LEVELS_FILE} is not a valid level table: {error}"))
51    })
52}
53
54/// The WIT level this host serves: the newest level in [`WIT_LEVELS_FILE`].
55pub fn served_wit_level() -> &'static WitLevel {
56    wit_levels()
57        .last()
58        .expect("parse_wit_levels refuses a table with no level")
59}
60
61#[cfg(test)]
62mod tests {
63    use std::collections::BTreeSet;
64    use std::path::Path;
65
66    use super::{served_wit_level, wit_levels, SERVED_WIT_PACKAGES, WIT_LEVELS_FILE};
67
68    fn served_set() -> BTreeSet<(String, String)> {
69        SERVED_WIT_PACKAGES
70            .iter()
71            .map(|(package, version)| (package.to_string(), version.to_string()))
72            .collect()
73    }
74
75    /// The table names each package once, and exactly the package versions the WIT tree declares.
76    /// The tree is what `wit-bindgen` generates the host's bindings from, so a version bump in it
77    /// that the table does not follow fails here.
78    #[test]
79    fn wit_versions_table_matches_the_wit_tree() {
80        let declared = murmur_artifact::wit_package_declarations(Path::new(concat!(
81            env!("CARGO_MANIFEST_DIR"),
82            "/wit"
83        )))
84        .unwrap();
85        assert!(!declared.is_empty(), "no murmur package declarations found");
86
87        let mut packages: Vec<&str> = SERVED_WIT_PACKAGES.iter().map(|(p, _)| *p).collect();
88        packages.sort_unstable();
89        packages.dedup();
90        assert_eq!(
91            packages.len(),
92            SERVED_WIT_PACKAGES.len(),
93            "a package appears twice in SERVED_WIT_PACKAGES"
94        );
95
96        assert_eq!(served_set(), declared);
97    }
98
99    /// Every instance name the host resolves or provides is at its package's served version. A
100    /// resolver constant moved without the table, or the table without the constant, fails here.
101    #[test]
102    fn wit_versions_table_covers_every_resolved_interface() {
103        let resolved = [
104            crate::hooks::LIFECYCLE_IFACE,
105            crate::process_driver::PROCESS_DRIVER_IFACE,
106            crate::runtime::WIT_CAPSULE_IFACE_VERSIONED,
107            crate::runtime::WIT_TOOL_IFACE_VERSIONED,
108            crate::runtime::WIT_TOOL_REGISTRY_IFACE,
109            crate::runtime::WIT_STREAM_EVENTS_IFACE,
110            crate::runtime::WIT_TASK_IFACE,
111            crate::conversation_import::CONVERSATION_IFACE_VERSIONED,
112            crate::inference_import::INFERENCE_IFACE_VERSIONED,
113            crate::task_io_import::TASK_IO_IFACE_VERSIONED,
114            crate::tokens_import::TOKENS_IFACE_VERSIONED,
115            crate::session_process::PROCESS_IFACE,
116        ];
117        let served = served_set();
118        for interface in resolved {
119            let (package, rest) = interface
120                .split_once('/')
121                .unwrap_or_else(|| panic!("{interface} names no interface"));
122            let (_, version) = rest
123                .rsplit_once('@')
124                .unwrap_or_else(|| panic!("{interface} carries no version"));
125            assert!(
126                served.contains(&(package.to_string(), version.to_string())),
127                "{interface} is not at a version SERVED_WIT_PACKAGES lists for {package}"
128            );
129        }
130    }
131
132    /// Doctor compares versions as exact strings, while wasmtime's linker treats two names that
133    /// differ only in a non-zero patch field as semver-compatible and links them. The two answers
134    /// agree only while murmur never ships a package at a non-zero patch.
135    #[test]
136    fn wit_versions_every_patch_field_is_zero() {
137        for (package, version) in SERVED_WIT_PACKAGES {
138            let patch = version
139                .split('.')
140                .nth(2)
141                .unwrap_or_else(|| panic!("{package}@{version} has no patch field"));
142            assert_eq!(patch, "0", "{package}@{version} has a non-zero patch field");
143        }
144    }
145
146    #[test]
147    fn wit_levels_file_parses() {
148        murmur_artifact::parse_wit_levels(include_str!("../wit/levels.txt"))
149            .unwrap_or_else(|error| panic!("{WIT_LEVELS_FILE}: {error}"));
150        assert!(!wit_levels().is_empty());
151    }
152
153    #[test]
154    fn wit_levels_served_level_is_level_6() {
155        assert_eq!(served_wit_level().level, 6);
156    }
157
158    /// Level 6 moved `murmur:driver` alone, for the harness's declared needs, so an artifact
159    /// built at level 5 whose interfaces name no `murmur:driver` package fits level 6 unchanged,
160    /// and a level-5 process driver does not.
161    #[test]
162    fn wit_levels_level_6_moves_only_the_driver_package() {
163        use murmur_artifact::{version_fit, FitSubject, VersionFit, WitContracts};
164        let levels = wit_levels();
165        let (five, six) = (&levels[4], &levels[5]);
166        assert_eq!(five.packages.len(), six.packages.len());
167        let moved: Vec<_> = five
168            .packages
169            .iter()
170            .zip(&six.packages)
171            .filter(|(before, after)| before != after)
172            .collect();
173        let driver = |version: &str| ("murmur:driver".to_string(), version.to_string());
174        assert_eq!(moved, [(&driver("0.5.0"), &driver("0.6.0"))]);
175
176        let contracts = |exports: &[&str], imports: &[&str]| WitContracts {
177            exports: exports.iter().map(|e| e.to_string()).collect(),
178            imports: imports.iter().map(|i| i.to_string()).collect(),
179        };
180        let fits = |contracts: WitContracts, level| {
181            version_fit(&FitSubject::component(contracts), level) == VersionFit::Fits
182        };
183        let driver_at =
184            |version: &str| contracts(&[&format!("murmur:driver/process@{version}")], &[]);
185        assert!(fits(driver_at("0.5.0"), five));
186        assert!(!fits(driver_at("0.5.0"), six));
187        assert!(fits(driver_at("0.6.0"), six));
188    }
189
190    /// Level 5 moved `murmur:driver` alone, for the login check, so an artifact built at level 4
191    /// whose interfaces name no `murmur:driver` package fits level 5 unchanged, and a level-4
192    /// process driver does not.
193    #[test]
194    fn wit_levels_level_5_moves_only_the_driver_package() {
195        use murmur_artifact::{version_fit, FitSubject, VersionFit, WitContracts};
196        let levels = wit_levels();
197        let (four, five) = (&levels[3], &levels[4]);
198        assert_eq!(four.packages.len(), five.packages.len());
199        let moved: Vec<_> = four
200            .packages
201            .iter()
202            .zip(&five.packages)
203            .filter(|(before, after)| before != after)
204            .collect();
205        let driver = |version: &str| ("murmur:driver".to_string(), version.to_string());
206        assert_eq!(moved, [(&driver("0.4.0"), &driver("0.5.0"))]);
207
208        let contracts = |exports: &[&str], imports: &[&str]| WitContracts {
209            exports: exports.iter().map(|e| e.to_string()).collect(),
210            imports: imports.iter().map(|i| i.to_string()).collect(),
211        };
212        let fits = |contracts: WitContracts, level| {
213            version_fit(&FitSubject::component(contracts), level) == VersionFit::Fits
214        };
215        let tool = || {
216            contracts(
217                &["murmur:tool/run@0.2.0"],
218                &["murmur:stream/events@0.1.0", "murmur:process/process@0.1.0"],
219            )
220        };
221        assert!(fits(tool(), four));
222        assert!(fits(tool(), five));
223        let driver_at =
224            |version: &str| contracts(&[&format!("murmur:driver/process@{version}")], &[]);
225        assert!(fits(driver_at("0.4.0"), four));
226        assert!(!fits(driver_at("0.4.0"), five));
227        assert!(fits(driver_at("0.5.0"), five));
228    }
229
230    /// Level 4 adds `murmur:process` as a new package and moves nothing else, so an artifact fits
231    /// level 4 exactly when it fits level 3, unless it imports the new package.
232    #[test]
233    fn wit_levels_level_4_only_adds_the_process_package() {
234        use murmur_artifact::{version_fit, FitSubject, VersionFit, WitContracts};
235        let levels = wit_levels();
236        let (three, four) = (&levels[2], &levels[3]);
237        let added: Vec<_> = four
238            .packages
239            .iter()
240            .filter(|package| !three.packages.contains(package))
241            .collect();
242        assert_eq!(
243            added,
244            [&("murmur:process".to_string(), "0.1.0".to_string())]
245        );
246        assert_eq!(four.packages.len(), three.packages.len() + 1);
247
248        let contracts = |imports: &[&str]| WitContracts {
249            exports: vec!["murmur:tool/run@0.2.0".to_string()],
250            imports: imports.iter().map(|i| i.to_string()).collect(),
251        };
252        let fits = |contracts: WitContracts, level| {
253            version_fit(&FitSubject::component(contracts), level) == VersionFit::Fits
254        };
255        let tool = || contracts(&["murmur:stream/events@0.1.0", "murmur:task/task@0.1.0"]);
256        assert!(fits(tool(), three));
257        assert!(fits(tool(), four));
258        let process_tool = || contracts(&["murmur:process/process@0.1.0"]);
259        assert!(!fits(process_tool(), three));
260        assert!(fits(process_tool(), four));
261    }
262
263    /// Level 3 moved `murmur:tool` and `murmur:tool-registry` together, for `tool-result`'s
264    /// `attachments` field. A level-2 tool or driver exporting `murmur:tool/run@0.1.0` does not
265    /// fit level 3, and a hook, which names neither package, fits both levels unchanged.
266    #[test]
267    fn wit_levels_level_3_moves_the_tool_packages() {
268        use murmur_artifact::{version_fit, FitSubject, VersionFit, WitContracts};
269        let levels = wit_levels();
270        let (two, three) = (&levels[1], &levels[2]);
271        let moved: Vec<_> = two
272            .packages
273            .iter()
274            .zip(&three.packages)
275            .filter(|(before, after)| before != after)
276            .collect();
277        let package = |name: &str, version: &str| (name.to_string(), version.to_string());
278        assert_eq!(
279            moved,
280            [
281                (
282                    &package("murmur:tool", "0.1.0"),
283                    &package("murmur:tool", "0.2.0")
284                ),
285                (
286                    &package("murmur:tool-registry", "0.1.0"),
287                    &package("murmur:tool-registry", "0.2.0")
288                ),
289            ]
290        );
291
292        let contracts = |exports: &[&str], imports: &[&str]| WitContracts {
293            exports: exports.iter().map(|e| e.to_string()).collect(),
294            imports: imports.iter().map(|i| i.to_string()).collect(),
295        };
296        let fits = |contracts: WitContracts, level| {
297            version_fit(&FitSubject::component(contracts), level) == VersionFit::Fits
298        };
299        let old_tool = || contracts(&["murmur:tool/run@0.1.0"], &["murmur:stream/events@0.1.0"]);
300        assert!(fits(old_tool(), two));
301        assert!(!fits(old_tool(), three));
302        let tool = contracts(&["murmur:tool/run@0.2.0"], &["murmur:stream/events@0.1.0"]);
303        assert!(fits(tool, three));
304        let hook = || {
305            contracts(
306                &["murmur:hook/lifecycle@0.9.0"],
307                &["murmur:task-io/read@0.1.0"],
308            )
309        };
310        assert!(fits(hook(), two));
311        assert!(fits(hook(), three));
312    }
313
314    /// Level 2 moved `murmur:driver` alone, so an artifact built at level 1 whose interfaces name
315    /// no `murmur:driver` package fits level 2 unchanged, and a level-1 process driver does not.
316    #[test]
317    fn wit_levels_level_2_moves_only_the_driver_package() {
318        use murmur_artifact::{version_fit, FitSubject, VersionFit, WitContracts};
319        let levels = wit_levels();
320        let (one, two) = (&levels[0], &levels[1]);
321        let moved: Vec<_> = one
322            .packages
323            .iter()
324            .zip(&two.packages)
325            .filter(|(before, after)| before != after)
326            .collect();
327        let driver = |version: &str| ("murmur:driver".to_string(), version.to_string());
328        assert_eq!(moved, [(&driver("0.3.0"), &driver("0.4.0"))]);
329
330        let contracts = |exports: &[&str], imports: &[&str]| WitContracts {
331            exports: exports.iter().map(|e| e.to_string()).collect(),
332            imports: imports.iter().map(|i| i.to_string()).collect(),
333        };
334        let fits = |contracts: WitContracts, level| {
335            version_fit(&FitSubject::component(contracts), level) == VersionFit::Fits
336        };
337        let hook = || {
338            contracts(
339                &["murmur:hook/lifecycle@0.9.0"],
340                &[
341                    "murmur:runtime/inference@0.4.0",
342                    "murmur:task-io/read@0.1.0",
343                ],
344            )
345        };
346        let tool = || contracts(&["murmur:tool/run@0.1.0"], &["murmur:stream/events@0.1.0"]);
347        for (what, built) in [("hook", hook()), ("tool", tool())] {
348            assert!(fits(built.clone(), one), "{what} at level 1");
349            assert!(fits(built, two), "{what} at level 2");
350        }
351        let old_driver = || contracts(&["murmur:driver/process@0.3.0"], &[]);
352        assert!(fits(old_driver(), one));
353        assert!(!fits(old_driver(), two));
354    }
355
356    /// The served table is the newest level's set. A version moved in one but not the other
357    /// fails here with the instruction for the bump: append the next level.
358    #[test]
359    fn wit_levels_newest_level_is_the_served_table() {
360        let newest = served_wit_level();
361        let level: BTreeSet<(String, String)> = newest.packages.iter().cloned().collect();
362        let served = served_set();
363        if level == served {
364            return;
365        }
366        let packages: BTreeSet<&str> = level
367            .iter()
368            .chain(served.iter())
369            .map(|(package, _)| package.as_str())
370            .collect();
371        let differences: Vec<String> = packages
372            .into_iter()
373            .filter_map(|package| {
374                let at_level = newest.version_of(package);
375                let in_table = SERVED_WIT_PACKAGES
376                    .iter()
377                    .find(|(p, _)| *p == package)
378                    .map(|(_, v)| *v);
379                (at_level != in_table).then(|| {
380                    format!(
381                        "  {package}: {} in level {} → {} in SERVED_WIT_PACKAGES",
382                        at_level.unwrap_or("absent"),
383                        newest.level,
384                        in_table.unwrap_or("absent")
385                    )
386                })
387            })
388            .collect();
389        let next = newest.level + 1;
390        panic!(
391            "SERVED_WIT_PACKAGES no longer equals WIT level {level} in {WIT_LEVELS_FILE}:\n{}\n\
392             append level {next} to {WIT_LEVELS_FILE}, listing every package at its new version \
393             (one `{next} <package>@<version>` line per package); never edit, renumber or reuse \
394             level {level}",
395            differences.join("\n"),
396            level = newest.level,
397        );
398    }
399}