1use std::path::PathBuf;
14
15use crate::pin::Pin;
16use crate::store::{InstalledLayer, Store, StoreError};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Resolved {
21 pub layer: InstalledLayer,
22 pub tools: Vec<(String, PathBuf)>,
26 pub qualified: Vec<(crate::compose::ToolProvider, PathBuf)>,
31 pub runners: std::collections::BTreeMap<String, RunnerContract>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct RunnerContract {
38 pub tool: String,
39 pub args: Vec<String>,
40 pub arg_prefix: Option<String>,
41}
42
43#[derive(Debug, thiserror::Error)]
45pub enum ResolveError {
46 #[error("layer {layer} is not installed — run `varve install` in this project to lay it down")]
47 NotInstalled { layer: String },
48 #[error(
49 "pin digest {pinned} is not installed — run `varve install` in this project to lay it down"
50 )]
51 DigestNotInstalled { pinned: String },
52 #[error(
53 "pin names layer {named} but pins digest {pinned}, which is layer {found} — the name is a label, the digest is the artifact; refusing to guess. Fix the pin."
54 )]
55 NameDigestMismatch {
56 named: String,
57 pinned: String,
58 found: String,
59 },
60 #[error(
61 "layer {layer} is installed more than once under different digests ({count} entries) and the pin carries no digest to disambiguate — add `digest = \"sha256:…\"` to the pin"
62 )]
63 Ambiguous { layer: String, count: usize },
64 #[error(
65 "layer {layer} is installed but incomplete: missing {missing:?} — run `varve install` to repair it; refusing to fall back to PATH"
66 )]
67 PartialLayer { layer: String, missing: Vec<String> },
68 #[error(
69 "this project's pin restricts `tools` to {missing:?}, which layer {layer} does not \
70 contain. It exposes: {available}. Re-installing cannot help — the layer is complete, \
71 the pin asks for something that was never in it. Fix the `tools` list in varve.toml, \
72 or drop it to expose everything the layer carries."
73 )]
74 PinNamesUnknownTool {
75 layer: String,
76 missing: Vec<String>,
77 available: String,
78 },
79 #[error(transparent)]
80 Store(#[from] StoreError),
81 #[error(
82 "layer {layer} is installed on channel '{installed}', but this project's pin selects \
83 '{pinned}' — refusing. A qualified line carries a support window and qualification \
84 evidence; a rolling one carries neither. Install the {pinned} layer, or change the \
85 pin deliberately."
86 )]
87 ChannelMismatch {
88 layer: String,
89 installed: String,
90 pinned: String,
91 },
92 #[error(transparent)]
93 Compose(#[from] crate::compose::ComposeError),
94 #[error(
95 "layer {layer} composes layer {missing}{realm}, which is not installed — \
96 `varve install` it, then retry"
97 )]
98 IncludeNotInstalled {
99 layer: String,
100 missing: String,
101 realm: String,
102 },
103}
104
105pub fn resolve(pin: &Pin, store: &Store) -> Result<Resolved, ResolveError> {
108 let layer = match &pin.digest {
109 Some(digest) => {
110 let entry = store
111 .get(digest)?
112 .ok_or_else(|| ResolveError::DigestNotInstalled {
113 pinned: digest.clone(),
114 })?;
115 if entry.layer != pin.layer {
116 return Err(ResolveError::NameDigestMismatch {
117 named: pin.layer.to_string(),
118 pinned: digest.clone(),
119 found: entry.layer.to_string(),
120 });
121 }
122 entry
123 }
124 None => {
125 let matching: Vec<InstalledLayer> = store
126 .list()?
127 .into_iter()
128 .filter(|entry| entry.layer == pin.layer)
129 .collect();
130 match matching.len() {
131 0 => {
132 return Err(ResolveError::NotInstalled {
133 layer: pin.layer.to_string(),
134 });
135 }
136 1 => matching.into_iter().next().expect("len checked"),
137 count => {
138 return Err(ResolveError::Ambiguous {
139 layer: pin.layer.to_string(),
140 count,
141 });
142 }
143 }
144 }
145 };
146
147 if !layer.channel.is_empty() && layer.channel != pin.channel.as_str() {
154 return Err(ResolveError::ChannelMismatch {
155 layer: layer.layer.to_string(),
156 installed: layer.channel.clone(),
157 pinned: pin.channel.as_str().to_string(),
158 });
159 }
160
161 let offers: Vec<Offer> = composition_offers(pin, &layer, store)?;
166
167 let (tool_names, chosen) = exposed_and_chosen(pin, &offers);
170
171 let exposed: Vec<crate::compose::ToolProvider> = offers
176 .iter()
177 .filter(|o| tool_names.contains(&o.provider.tool))
178 .map(|o| o.provider.clone())
179 .collect();
180 let dispatch = crate::compose::select_tools(&exposed, &chosen)?;
181
182 let mut tools = Vec::new();
183 let mut missing = Vec::new();
184 for name in &tool_names {
185 match dispatch
186 .get(name)
187 .and_then(|p| offers.iter().find(|o| o.provider == *p))
188 .and_then(|o| o.path.clone())
189 {
190 Some(path) => tools.push((name.clone(), path)),
191 None => missing.push(name.clone()),
192 }
193 }
194 if !missing.is_empty() {
195 if pin.tools.is_some() {
201 let mut available: Vec<String> = store
202 .manifest_tool_names(&layer)
203 .unwrap_or_default()
204 .into_iter()
205 .chain(offers.iter().map(|o| o.provider.tool.clone()))
206 .collect();
207 available.sort();
208 available.dedup();
209 let unknown: Vec<String> = missing
210 .iter()
211 .filter(|m| !available.contains(m))
212 .cloned()
213 .collect();
214 if !unknown.is_empty() {
215 return Err(ResolveError::PinNamesUnknownTool {
216 layer: layer.layer.to_string(),
217 missing: unknown,
218 available: if available.is_empty() {
219 "(nothing)".into()
220 } else {
221 available.join(", ")
222 },
223 });
224 }
225 }
226 return Err(ResolveError::PartialLayer {
227 layer: layer.layer.to_string(),
228 missing,
229 });
230 }
231
232 let qualified: Vec<(crate::compose::ToolProvider, PathBuf)> = offers
236 .iter()
237 .filter(|o| tool_names.contains(&o.provider.tool))
238 .filter_map(|o| o.path.clone().map(|p| (o.provider.clone(), p)))
239 .collect();
240
241 let mut runners = std::collections::BTreeMap::new();
244 if let Ok(bytes) = std::fs::read(layer.root.join("layer.json"))
245 && let Ok(json) = serde_json::from_slice::<serde_json::Value>(&bytes)
246 && let Some(entries) = json["manifests"].as_array()
247 {
248 for entry in entries {
249 let ann = &entry["annotations"];
250 if let (Some(tool), Some(runner)) = (
251 ann["eu.pulseengine.tool"].as_str(),
252 ann[crate::bazel::ANN_RUNNER].as_str(),
253 ) {
254 runners.insert(
255 tool.to_string(),
256 RunnerContract {
257 tool: runner.to_string(),
258 args: ann[crate::bazel::ANN_RUNNER_ARGS]
259 .as_str()
260 .map(|a| a.split_whitespace().map(str::to_string).collect())
261 .unwrap_or_default(),
262 arg_prefix: ann[crate::bazel::ANN_RUNNER_ARG_PREFIX]
263 .as_str()
264 .map(str::to_string),
265 },
266 );
267 }
268 }
269 }
270 Ok(Resolved {
271 layer,
272 tools,
273 qualified,
274 runners,
275 })
276}
277
278fn exposed_and_chosen(
286 pin: &Pin,
287 offers: &[Offer],
288) -> (Vec<String>, std::collections::BTreeMap<String, String>) {
289 let Some(subset) = &pin.tools else {
290 let mut names: Vec<String> = offers.iter().map(|o| o.provider.tool.clone()).collect();
291 names.sort();
292 names.dedup();
293 return (names, std::collections::BTreeMap::new());
294 };
295 let mut names = Vec::with_capacity(subset.len());
296 let mut chosen = std::collections::BTreeMap::new();
297 for selector in subset {
298 names.push(selector.name.clone());
299 if let Some(realm) = &selector.realm {
300 chosen.insert(selector.name.clone(), realm.clone());
301 }
302 }
303 (names, chosen)
304}
305
306#[derive(Debug, Clone)]
310struct Offer {
311 provider: crate::compose::ToolProvider,
312 path: Option<PathBuf>,
313}
314
315fn composition_offers(
335 pin: &Pin,
336 layer: &InstalledLayer,
337 store: &Store,
338) -> Result<Vec<Offer>, ResolveError> {
339 let root_realm = pin.realm.clone().unwrap_or_default();
340 let path = layer.root.join("layer.json");
341 let root_view = match std::fs::read(&path) {
342 Ok(bytes) => crate::compose::view(&bytes)?,
346 Err(_) => crate::compose::LayerView::default(),
349 };
350 for inc in &root_view.includes {
353 if store.find_anywhere(&inc.digest)?.is_none() {
356 return Err(ResolveError::IncludeNotInstalled {
357 layer: layer.layer.to_string(),
358 missing: inc.layer.clone().unwrap_or_else(|| inc.digest.clone()),
359 realm: inc
360 .realm
361 .as_ref()
362 .map(|r| format!(" from realm '{r}'"))
363 .unwrap_or_default(),
364 });
365 }
366 }
367 let walked = crate::compose::walk(&layer.digest, &root_realm, &root_view, |digest| {
368 let (_, entry) = store.find_anywhere(digest).ok().flatten()?;
369 let bytes = std::fs::read(entry.root.join("layer.json")).ok()?;
370 crate::compose::view(&bytes).ok()
371 })?;
372
373 let mut out = Vec::new();
374 for step in &walked {
375 let (owner, entry) = if step.digest == layer.digest {
378 (store.clone(), layer.clone())
379 } else {
380 match store.find_anywhere(&step.digest)? {
381 Some(found) => found,
382 None => continue,
383 }
384 };
385 let mut names: Vec<String> = step.view.tools.clone();
386 if let Ok(rd) = std::fs::read_dir(entry.root.join("bin")) {
387 names.extend(
388 rd.filter_map(|e| e.ok())
389 .filter(|e| e.path().is_file())
390 .map(|e| e.file_name().to_string_lossy().into_owned()),
391 );
392 }
393 names.sort();
394 names.dedup();
395 for name in names {
396 out.push(Offer {
397 path: owner.tool_path(&entry, &name),
398 provider: crate::compose::ToolProvider {
399 tool: name,
400 realm: step.realm.clone(),
401 layer: entry.layer.to_string(),
402 digest: step.digest.clone(),
403 },
404 });
405 }
406 }
407 let on_disk: std::collections::BTreeSet<String> = out
412 .iter()
413 .filter(|o| o.path.is_some())
414 .map(|o| o.provider.tool.clone())
415 .collect();
416 out.retain(|o| on_disk.contains(&o.provider.tool));
417 Ok(out)
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::pin::Pin;
424 use crate::store::{Store, fixtures, manifest_digest};
425
426 fn pin(toml: &str) -> Pin {
427 Pin::parse(toml, "varve.toml").unwrap()
428 }
429
430 fn qualified_pin(layer: &str) -> Pin {
431 pin(&format!(
432 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"{layer}\"\n"
433 ))
434 }
435
436 fn store() -> (tempfile::TempDir, Store) {
437 let tmp = tempfile::tempdir().unwrap();
438 let store = Store::at(tmp.path().join("varve-root"));
439 (tmp, store)
440 }
441
442 fn manifest_composing(layer: &str, tools: &[&str], include: &str) -> Vec<u8> {
444 let mut entries: Vec<String> = tools
445 .iter()
446 .map(|t| {
447 format!(
448 r#"{{"digest":"sha256:{t}","annotations":{{"eu.pulseengine.tool":"{t}"}}}}"#
449 )
450 })
451 .collect();
452 entries.push(format!(
453 r#"{{"digest":"{include}","annotations":{{"eu.pulseengine.varve.kind":"layer"}}}}"#
454 ));
455 format!(
456 r#"{{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","artifactType":"application/vnd.pulseengine.varve.layer.v1+json","annotations":{{"eu.pulseengine.varve.layer":"{layer}","eu.pulseengine.varve.channel":"qualified"}},"manifests":[{}]}}"#,
457 entries.join(",")
458 )
459 .into_bytes()
460 }
461
462 #[test]
464 fn a_pin_selecting_qualified_refuses_an_installed_rolling_layer() {
465 let (_tmp, store) = store();
472 store
473 .lay_down(
474 &fixtures::manifest("2026.07.0", "rolling"),
475 &[("synth", b"s")],
476 )
477 .unwrap();
478 let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
479 match err {
480 ResolveError::ChannelMismatch {
481 installed, pinned, ..
482 } => {
483 assert_eq!(installed, "rolling");
484 assert_eq!(pinned, "qualified");
485 }
486 other => panic!("expected ChannelMismatch, got {other}"),
487 }
488 }
489
490 #[test]
492 fn a_matching_channel_still_resolves() {
493 let (_tmp, store) = store();
495 store
496 .lay_down(
497 &fixtures::manifest("2026.07.0", "qualified"),
498 &[("synth", b"s")],
499 )
500 .unwrap();
501 assert!(resolve(&qualified_pin("2026.07.0"), &store).is_ok());
502 }
503
504 #[test]
506 fn the_channel_refusal_names_both_channels_and_what_they_cost() {
507 let (_tmp, store) = store();
513 store
514 .lay_down(
515 &fixtures::manifest("2026.07.0", "rolling"),
516 &[("synth", b"s")],
517 )
518 .unwrap();
519 let msg = resolve(&qualified_pin("2026.07.0"), &store)
520 .unwrap_err()
521 .to_string();
522 assert!(msg.contains("rolling"), "names what is installed: {msg}");
523 assert!(msg.contains("qualified"), "names what is pinned: {msg}");
524 assert!(
526 msg.contains("support window"),
527 "says what qualified carries: {msg}"
528 );
529 assert!(
530 msg.contains("qualification evidence"),
531 "says what rolling lacks: {msg}"
532 );
533 }
534
535 #[test]
537 fn a_layer_predating_channel_annotations_is_not_refused() {
538 let (_tmp, store) = store();
543 store
544 .lay_down(
545 &fixtures::manifest_without_channel("2026.07.0"),
546 &[("synth", b"s")],
547 )
548 .unwrap();
549 let resolved = resolve(&qualified_pin("2026.07.0"), &store);
550 assert!(
551 resolved.is_ok(),
552 "a layer that states no channel contradicts no pin: {:?}",
553 resolved.err()
554 );
555 }
556
557 #[test]
559 fn resolve_returns_the_composed_layers_tools() {
560 let (_tmp, store) = store();
565 let up = store
566 .lay_down(
567 &fixtures::manifest("2026.08.0", "qualified"),
568 &[("wasm-tools", b"w")],
569 )
570 .unwrap();
571 store
572 .lay_down(
573 &manifest_composing("2026.07.0", &["rivet"], &up),
574 &[("rivet", b"r")],
575 )
576 .unwrap();
577 let resolved = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
578 let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
579 assert!(names.contains(&"rivet"), "own tool missing: {names:?}");
580 assert!(
581 names.contains(&"wasm-tools"),
582 "composed tool missing — the composition resolved to nothing: {names:?}"
583 );
584 }
585
586 #[test]
588 fn resolve_refuses_a_tool_exposed_by_both_layers() {
589 let (_tmp, store) = store();
592 let up = store
593 .lay_down(
594 &fixtures::manifest("2026.08.0", "qualified"),
595 &[("wasm-tools", b"u")],
596 )
597 .unwrap();
598 store
599 .lay_down(
600 &manifest_composing("2026.07.0", &["wasm-tools"], &up),
601 &[("wasm-tools", b"r")],
602 )
603 .unwrap();
604 let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
605 assert!(
606 matches!(err, ResolveError::Compose(_)),
607 "a tool in two layers must be refused, got {err}"
608 );
609 }
610
611 #[test]
613 fn a_pin_restricting_tools_also_restricts_the_composition() {
614 let (_tmp, store) = store();
617 let up = store
618 .lay_down(
619 &fixtures::manifest("2026.08.0", "qualified"),
620 &[("wasm-tools", b"w"), ("wkg", b"k")],
621 )
622 .unwrap();
623 store
624 .lay_down(
625 &manifest_composing("2026.07.0", &["rivet"], &up),
626 &[("rivet", b"r")],
627 )
628 .unwrap();
629 let pinned = pin(
630 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"rivet\", \"wasm-tools\"]\n",
631 );
632 let resolved = resolve(&pinned, &store).unwrap();
633 let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
634 assert!(
635 names.contains(&"wasm-tools"),
636 "selected composed tool: {names:?}"
637 );
638 assert!(
639 !names.contains(&"wkg"),
640 "the pin did not select wkg, so the composition must not add it: {names:?}"
641 );
642 }
643
644 #[test]
646 fn resolves_the_pinned_layer_by_name() {
647 let (_tmp, store) = store();
648 store
649 .lay_down(
650 &fixtures::manifest("2026.07.0", "qualified"),
651 &[("synth", b"s"), ("rivet", b"r")],
652 )
653 .unwrap();
654 let resolved = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
655 assert_eq!(resolved.layer.layer.to_string(), "2026.07.0");
656 let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
657 assert_eq!(names, ["rivet", "synth"], "all tools, stable order");
658 }
659
660 #[test]
662 fn missing_layer_fails_with_the_corrective_command() {
663 let (_tmp, store) = store();
664 let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
665 assert!(matches!(&err, ResolveError::NotInstalled { layer } if layer == "2026.07.0"));
666 assert!(
667 err.to_string().contains("varve install"),
668 "error must carry the fix: {err}"
669 );
670 }
671
672 #[test]
674 fn partial_layer_is_an_error_not_a_fallback() {
675 let (_tmp, store) = store();
680 store
681 .lay_down(
682 &crate::manifest::fixtures::manifest_with_tools(
683 "2026.07.0",
684 "qualified",
685 1,
686 "2026-07-01T00:00:00Z",
687 &[("rivet", "sha256:aa"), ("synth", "sha256:bb")],
688 ),
689 &[("rivet", b"r")],
690 )
691 .unwrap();
692 let p = pin(
693 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"rivet\", \"synth\"]\n",
694 );
695 match resolve(&p, &store).unwrap_err() {
696 ResolveError::PartialLayer { missing, .. } => {
697 assert_eq!(missing, vec!["synth".to_string()]);
698 }
699 other => panic!("expected PartialLayer, got: {other}"),
700 }
701 }
702
703 #[test]
705 fn a_pin_naming_a_tool_the_layer_never_had_is_not_told_to_reinstall() {
706 let (_tmp, store) = store();
712 store
713 .lay_down(
714 &crate::manifest::fixtures::manifest_with_tools(
715 "2026.07.0",
716 "qualified",
717 1,
718 "2026-07-01T00:00:00Z",
719 &[("rivet", "sha256:aa")],
720 ),
721 &[("rivet", b"r")],
722 )
723 .unwrap();
724 let p = pin(
725 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"notathing\"]\n",
726 );
727 match resolve(&p, &store).unwrap_err() {
728 ResolveError::PinNamesUnknownTool {
729 missing, available, ..
730 } => {
731 assert_eq!(missing, vec!["notathing".to_string()]);
732 assert!(
733 available.contains("rivet"),
734 "names what IS there: {available}"
735 );
736 }
737 other => panic!("expected PinNamesUnknownTool, got: {other}"),
738 }
739 }
740
741 #[test]
743 fn pinned_digest_wins_and_a_mismatching_name_is_a_hard_failure() {
744 let (_tmp, store) = store();
745 let july = fixtures::manifest("2026.07.0", "qualified");
746 store.lay_down(&july, &[("synth", b"s")]).unwrap();
747 let d_july = manifest_digest(&july);
748
749 let hex = d_july.strip_prefix("sha256:").unwrap();
751 let p = pin(&format!(
752 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\ndigest = \"sha256:{hex}\"\n"
753 ));
754 let err = resolve(&p, &store).unwrap_err();
755 assert!(
756 matches!(&err, ResolveError::NameDigestMismatch { named, found, .. }
757 if named == "2026.08.0" && found == "2026.07.0"),
758 "got: {err}"
759 );
760 }
761
762 #[test]
764 fn matching_digest_pin_resolves() {
765 let (_tmp, store) = store();
766 let july = fixtures::manifest("2026.07.0", "qualified");
767 store.lay_down(&july, &[("synth", b"s")]).unwrap();
768 let hex = manifest_digest(&july)
769 .strip_prefix("sha256:")
770 .unwrap()
771 .to_string();
772 let p = pin(&format!(
773 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ndigest = \"sha256:{hex}\"\n"
774 ));
775 let resolved = resolve(&p, &store).unwrap();
776 assert_eq!(resolved.layer.layer.to_string(), "2026.07.0");
777 }
778
779 #[test]
781 fn a_newer_layer_in_the_core_cannot_change_what_a_pin_resolves_to() {
782 let (_tmp, store) = store();
783 store
784 .lay_down(
785 &fixtures::manifest("2026.07.0", "qualified"),
786 &[("synth", b"july")],
787 )
788 .unwrap();
789 let p = qualified_pin("2026.07.0");
790 let before = resolve(&p, &store).unwrap();
791
792 store
794 .lay_down(
795 &fixtures::manifest("2026.08.0", "qualified"),
796 &[("synth", b"august")],
797 )
798 .unwrap();
799 let after = resolve(&p, &store).unwrap();
800 assert_eq!(
801 before, after,
802 "resolution is a pure function of (pin, store entry)"
803 );
804 assert_eq!(after.layer.layer.to_string(), "2026.07.0");
805 }
806
807 #[test]
809 fn ambiguous_name_fails_closed_instead_of_choosing() {
810 let (_tmp, store) = store();
811 let a = fixtures::manifest("2026.07.0", "qualified");
814 let mut b = a.clone();
815 b.extend_from_slice(b"\n");
816 store.lay_down(&a, &[]).unwrap();
817 store.lay_down(&b, &[]).unwrap();
818 let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
819 assert!(
820 matches!(&err, ResolveError::Ambiguous { count: 2, .. }),
821 "got: {err}"
822 );
823 }
824
825 #[test]
827 fn resolution_and_listing_never_write_to_the_core() {
828 fn tree_snapshot(root: &std::path::Path) -> Vec<(String, Vec<u8>)> {
829 let mut out = Vec::new();
830 if !root.exists() {
831 return out;
832 }
833 let mut stack = vec![root.to_path_buf()];
834 while let Some(dir) = stack.pop() {
835 let mut entries: Vec<_> = std::fs::read_dir(&dir)
836 .unwrap()
837 .map(|e| e.unwrap().path())
838 .collect();
839 entries.sort();
840 for path in entries {
841 if path.is_dir() {
842 stack.push(path);
843 } else {
844 out.push((path.display().to_string(), std::fs::read(&path).unwrap()));
845 }
846 }
847 }
848 out.sort();
849 out
850 }
851
852 let (_tmp, store) = store();
853 store
854 .lay_down(
855 &fixtures::manifest("2026.07.0", "qualified"),
856 &[("synth", b"s")],
857 )
858 .unwrap();
859 let before = tree_snapshot(store.root());
860 let _ = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
861 let _ = store.list().unwrap();
862 let _ = resolve(&qualified_pin("2026.09.0"), &store).unwrap_err();
863 let after = tree_snapshot(store.root());
864 assert_eq!(
865 before, after,
866 "select/verify/report must never mutate the core"
867 );
868 }
869
870 #[test]
872 fn a_tool_the_manifest_declares_for_other_platforms_is_not_exposed_here() {
873 let (_tmp, store) = store();
880 let manifest = format!(
881 r#"{{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","artifactType":"application/vnd.pulseengine.varve.layer.v1+json","annotations":{{"eu.pulseengine.varve.layer":"2026.07.0","eu.pulseengine.varve.channel":"qualified"}},"manifests":[
882{{"digest":"sha256:aa","annotations":{{"eu.pulseengine.tool":"rivet","eu.pulseengine.platform":"{here}"}}}},
883{{"digest":"sha256:bb","annotations":{{"eu.pulseengine.tool":"loom","eu.pulseengine.platform":"some-other-triple"}}}}]}}"#,
884 here = crate::platform::host_platform()
885 );
886 store
888 .lay_down(manifest.as_bytes(), &[("rivet", b"r")])
889 .unwrap();
890 let resolved = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
891 let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
892 assert_eq!(names, ["rivet"], "loom is declared, not laid down here");
893 }
894
895 #[test]
897 fn a_declared_tool_whose_bytes_are_missing_still_collides_with_a_composed_one() {
898 let (_tmp, store) = store();
905 let up = manifest_composing("2026.08.0", &["wasm-tools"], "sha256:none");
906 let up = String::from_utf8(up).unwrap().replace(
908 r#",{"digest":"sha256:none","annotations":{"eu.pulseengine.varve.kind":"layer"}}"#,
909 "",
910 );
911 let up_digest = store
912 .lay_down(up.as_bytes(), &[("wasm-tools", b"upstream")])
913 .unwrap();
914 let root = manifest_composing("2026.07.0", &["wasm-tools", "rivet"], &up_digest);
916 store.lay_down(&root, &[("rivet", b"r")]).unwrap();
917
918 let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
919 let msg = err.to_string();
920 assert!(
921 msg.contains("provided by more than one layer"),
922 "a half-installed root must not silently yield the name: {msg}"
923 );
924 }
925}