1use std::sync::OnceLock;
5
6use murmur_artifact::{parse_wit_levels, WitLevel};
7
8pub 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.4.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.2.0"),
35 ("murmur:tool-registry", "0.2.0"),
36];
37
38pub const WIT_LEVELS_FILE: &str = "crates/capsule-runtime/wit/levels.txt";
40
41pub 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
53pub 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 #[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 #[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 #[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_3() {
153 assert_eq!(served_wit_level().level, 3);
154 }
155
156 #[test]
160 fn wit_levels_level_3_moves_the_tool_packages() {
161 use murmur_artifact::{
162 current_platform, version_fit, FitSubject, VersionFit, WitContracts,
163 };
164 let levels = wit_levels();
165 let (two, three) = (&levels[1], &levels[2]);
166 let moved: Vec<_> = two
167 .packages
168 .iter()
169 .zip(&three.packages)
170 .filter(|(before, after)| before != after)
171 .collect();
172 let package = |name: &str, version: &str| (name.to_string(), version.to_string());
173 assert_eq!(
174 moved,
175 [
176 (
177 &package("murmur:tool", "0.1.0"),
178 &package("murmur:tool", "0.2.0")
179 ),
180 (
181 &package("murmur:tool-registry", "0.1.0"),
182 &package("murmur:tool-registry", "0.2.0")
183 ),
184 ]
185 );
186
187 let contracts = |exports: &[&str], imports: &[&str]| WitContracts {
188 exports: exports.iter().map(|e| e.to_string()).collect(),
189 imports: imports.iter().map(|i| i.to_string()).collect(),
190 };
191 let fits = |contracts: WitContracts, level| {
192 version_fit(&FitSubject::component(contracts), level, current_platform())
193 == VersionFit::Fits
194 };
195 let old_tool = || contracts(&["murmur:tool/run@0.1.0"], &["murmur:stream/events@0.1.0"]);
196 assert!(fits(old_tool(), two));
197 assert!(!fits(old_tool(), three));
198 let tool = contracts(&["murmur:tool/run@0.2.0"], &["murmur:stream/events@0.1.0"]);
199 assert!(fits(tool, three));
200 let hook = || {
201 contracts(
202 &["murmur:hook/lifecycle@0.9.0"],
203 &["murmur:task-io/read@0.1.0"],
204 )
205 };
206 assert!(fits(hook(), two));
207 assert!(fits(hook(), three));
208 }
209
210 #[test]
213 fn wit_levels_level_2_moves_only_the_driver_package() {
214 use murmur_artifact::{
215 current_platform, version_fit, FitSubject, VersionFit, WitContracts,
216 };
217 let levels = wit_levels();
218 let (one, two) = (&levels[0], &levels[1]);
219 let moved: Vec<_> = one
220 .packages
221 .iter()
222 .zip(&two.packages)
223 .filter(|(before, after)| before != after)
224 .collect();
225 let driver = |version: &str| ("murmur:driver".to_string(), version.to_string());
226 assert_eq!(moved, [(&driver("0.3.0"), &driver("0.4.0"))]);
227
228 let contracts = |exports: &[&str], imports: &[&str]| WitContracts {
229 exports: exports.iter().map(|e| e.to_string()).collect(),
230 imports: imports.iter().map(|i| i.to_string()).collect(),
231 };
232 let fits = |contracts: WitContracts, level| {
233 version_fit(&FitSubject::component(contracts), level, current_platform())
234 == VersionFit::Fits
235 };
236 let hook = || {
237 contracts(
238 &["murmur:hook/lifecycle@0.9.0"],
239 &[
240 "murmur:runtime/inference@0.4.0",
241 "murmur:task-io/read@0.1.0",
242 ],
243 )
244 };
245 let tool = || contracts(&["murmur:tool/run@0.1.0"], &["murmur:stream/events@0.1.0"]);
246 for (what, built) in [("hook", hook()), ("tool", tool())] {
247 assert!(fits(built.clone(), one), "{what} at level 1");
248 assert!(fits(built, two), "{what} at level 2");
249 }
250 let old_driver = || contracts(&["murmur:driver/process@0.3.0"], &[]);
251 assert!(fits(old_driver(), one));
252 assert!(!fits(old_driver(), two));
253 }
254
255 #[test]
258 fn wit_levels_newest_level_is_the_served_table() {
259 let newest = served_wit_level();
260 let level: BTreeSet<(String, String)> = newest.packages.iter().cloned().collect();
261 let served = served_set();
262 if level == served {
263 return;
264 }
265 let packages: BTreeSet<&str> = level
266 .iter()
267 .chain(served.iter())
268 .map(|(package, _)| package.as_str())
269 .collect();
270 let differences: Vec<String> = packages
271 .into_iter()
272 .filter_map(|package| {
273 let at_level = newest.version_of(package);
274 let in_table = SERVED_WIT_PACKAGES
275 .iter()
276 .find(|(p, _)| *p == package)
277 .map(|(_, v)| *v);
278 (at_level != in_table).then(|| {
279 format!(
280 " {package}: {} in level {} → {} in SERVED_WIT_PACKAGES",
281 at_level.unwrap_or("absent"),
282 newest.level,
283 in_table.unwrap_or("absent")
284 )
285 })
286 })
287 .collect();
288 let next = newest.level + 1;
289 panic!(
290 "SERVED_WIT_PACKAGES no longer equals WIT level {level} in {WIT_LEVELS_FILE}:\n{}\n\
291 append level {next} to {WIT_LEVELS_FILE}, listing every package at its new version \
292 (one `{next} <package>@<version>` line per package); never edit, renumber or reuse \
293 level {level}",
294 differences.join("\n"),
295 level = newest.level,
296 );
297 }
298}