use std::path::Path;
use midenup::{
migrate_v1::v1_manifest_path,
paths,
state::{LocalState, PublicationRef},
};
mod common;
use common::*;
const UNREACHABLE_UPSTREAM: &str = "https://127.0.0.1:1/nope.json";
fn v1_manifest(version: &str, channel: &str, components: &[&str]) -> String {
let components: Vec<_> = components
.iter()
.map(|name| {
serde_json::json!({
"name": name,
"version": "0.1.0",
"installed_executable": format!("miden-{name}"),
})
})
.collect();
serde_json::json!({
"manifest_version": version,
"date": 1735689600,
"channels": [{"name": channel, "components": components}]
})
.to_string()
}
fn write_v1_manifest(env: &TestEnvironment, contents: &str) {
std::fs::create_dir_all(&env.midenup_home).unwrap();
std::fs::write(v1_manifest_path(&env.midenup_home), contents).unwrap();
}
fn state_of(home: &Path) -> LocalState {
LocalState::load(&paths::state_path(home)).expect("state.json must be readable")
}
#[test]
fn integration_a_v1_manifest_is_migrated_even_when_upstream_is_unreachable() {
let _guard = common::harness::mutating_test_guard();
let env = environment_setup("v1_migrate_offline");
write_v1_manifest(&env, &v1_manifest("1.0.1", "0.15.0", &["vm", "client"]));
let output = run_midenup(&env, UNREACHABLE_UPSTREAM, &["list"]);
assert!(
!output.status.success(),
"the command itself must fail on the unreachable upstream"
);
let state = state_of(&env.midenup_home);
assert_eq!(state.installations.len(), 1, "the installed channel must be carried forward");
let installation = state.get(&semver::Version::new(0, 15, 0)).expect("0.15.0 must be recorded");
assert!(matches!(installation.publication, PublicationRef::NeedsReinstall));
assert!(installation.intent.roots.contains("vm"));
assert!(installation.intent.roots.contains("client"));
assert!(
installation.intent.profiles.is_empty(),
"migrated intent is roots-only, so unrelated new profile members are not pulled in"
);
assert!(
!v1_manifest_path(&env.midenup_home).exists(),
"the v1 document is removed once the state document is committed"
);
}
#[test]
fn integration_a_document_below_the_migration_floor_is_rejected_and_preserved() {
let _guard = common::harness::mutating_test_guard();
let env = environment_setup("v1_too_old");
write_v1_manifest(&env, &v1_manifest("1.0.0", "0.15.0", &["vm"]));
let path = v1_manifest_path(&env.midenup_home);
let before = std::fs::read(&path).unwrap();
let output = run_midenup(&env, UNREACHABLE_UPSTREAM, &["list"]);
assert!(!output.status.success(), "must refuse");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("1.0.0"), "the diagnostic must name the version found: {stderr}");
assert_eq!(std::fs::read(&path).unwrap(), before, "the file must be untouched");
assert!(
!paths::state_path(&env.midenup_home).exists(),
"and must leave no partial state document"
);
}
#[test]
fn integration_re_running_startup_after_migration_changes_nothing() {
let _guard = common::harness::mutating_test_guard();
let env = environment_setup("v1_idempotent");
write_v1_manifest(&env, &v1_manifest("1.0.1", "0.15.0", &["vm"]));
run_midenup(&env, UNREACHABLE_UPSTREAM, &["list"]);
let after_first = std::fs::read(paths::state_path(&env.midenup_home)).unwrap();
run_midenup(&env, UNREACHABLE_UPSTREAM, &["list"]);
assert_eq!(std::fs::read(paths::state_path(&env.midenup_home)).unwrap(), after_first);
}
#[cfg(feature = "fault-injection")]
#[test]
fn integration_recovery_a_failure_before_the_migration_commit_preserves_the_v1_document() {
let _guard = common::harness::mutating_test_guard();
let env = environment_setup("v1_precommit_failure");
write_v1_manifest(&env, &v1_manifest("1.0.1", "0.15.0", &["vm"]));
let path = v1_manifest_path(&env.midenup_home);
let before = std::fs::read(&path).unwrap();
let output = midenup_command(env!("CARGO_BIN_EXE_midenup"), &env, UNREACHABLE_UPSTREAM)
.arg("list")
.env(midenup::fault::FAULT_POINT_ENV, "pre-migration-commit")
.output()
.expect("failed to run midenup");
assert!(!output.status.success());
assert_eq!(std::fs::read(&path).unwrap(), before, "the v1 document must be untouched");
assert!(
!paths::state_path(&env.midenup_home).exists(),
"and no partial state document may be left behind"
);
}
fn upstream(env: &TestEnvironment, file: &str, components: &[&str]) -> String {
let dir = env.tmp_dir.path().join("fixture");
std::fs::create_dir_all(&dir).unwrap();
let vm = dir.join("miden-vm");
std::fs::write(&vm, "#!/bin/sh\nexit 0\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&vm, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let mut declared = vec![serde_json::json!({
"name": "vm",
"version": {"kind": "registry", "version": "0.1.0"},
"kind": "executable",
"installation_method": {"kind": "prebuilt"},
"installed-executable": "miden-vm",
"profiles": ["minimal"],
"artifacts": {"miden-vm": {"uri": format!("file://{}", vm.display())}}
})];
for name in components.iter().filter(|name| **name != "vm") {
let artifact = dir.join(format!("{name}.txt"));
std::fs::write(&artifact, format!("{name}\n")).unwrap();
declared.push(serde_json::json!({
"name": name,
"version": {"kind": "registry", "version": "0.1.0"},
"kind": "asset",
"profiles": [],
"artifacts": {format!("{name}.txt"): {"uri": format!("file://{}", artifact.display())}}
}));
}
let manifest = serde_json::json!({
"manifest_version": "3.0.0",
"date": 1735689600,
"channels": [{"name": "0.15.0", "components": declared}]
});
let path = dir.join(file);
std::fs::write(&path, serde_json::to_string_pretty(&manifest).unwrap()).unwrap();
format!("file://{}", path.display())
}
#[test]
fn integration_a_migrated_installation_is_reinstalled_on_first_use() {
let _guard = common::harness::mutating_test_guard();
let env = environment_setup("needs_reinstall");
write_v1_manifest(&env, &v1_manifest("1.0.1", "0.15.0", &["vm"]));
let manifest = upstream(&env, "upstream.json", &["vm"]);
run_midenup(&env, &manifest, &["list"]);
assert!(
matches!(
state_of(&env.midenup_home).installations[0].publication,
PublicationRef::NeedsReinstall
),
"migration alone must not claim the toolchain is usable"
);
let output = midenup_command(env!("CARGO_BIN_EXE_midenup"), &env, &manifest)
.args(["install", "0.15.0", "--profile", "minimal"])
.output()
.expect("failed to run midenup");
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let installation = &state_of(&env.midenup_home).installations[0];
assert!(
matches!(installation.publication, PublicationRef::Managed { .. }),
"the reinstall must produce a publication midenup owns"
);
assert!(!installation.components.is_empty(), "and a component snapshot to dispatch from");
}
#[test]
fn integration_migrated_roots_missing_upstream_are_dropped_once_with_a_warning() {
let _guard = common::harness::mutating_test_guard();
let env = environment_setup("migrated_root_dropped");
write_v1_manifest(&env, &v1_manifest("1.0.1", "0.15.0", &["vm", "goneaway"]));
let manifest = upstream(&env, "upstream.json", &["vm"]);
run_midenup(&env, &manifest, &["list"]);
let output = run_midenup(&env, &manifest, &["install", "0.15.0", "--profile", "minimal"]);
assert!(
output.status.success(),
"a missing migrated root must not block: {}",
String::from_utf8_lossy(&output.stderr)
);
let reported = String::from_utf8_lossy(&output.stderr);
assert!(reported.contains("goneaway"), "the dropped root must be named: {reported}");
let installation = &state_of(&env.midenup_home).installations[0];
assert!(
!installation.intent.roots.contains("goneaway"),
"intent must be rewritten without it: {:?}",
installation.intent
);
assert!(matches!(installation.publication, PublicationRef::Managed { .. }));
}
#[test]
fn integration_after_the_first_operation_a_removed_root_blocks_as_normal() {
let _guard = common::harness::mutating_test_guard();
let env = environment_setup("relaxation_is_one_time");
write_v1_manifest(&env, &v1_manifest("1.0.1", "0.15.0", &["vm", "client"]));
let with_client = upstream(&env, "with-client.json", &["vm", "client"]);
run_midenup(&env, &with_client, &["list"]);
let output = run_midenup(&env, &with_client, &["install", "0.15.0", "--profile", "minimal"]);
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let installation = &state_of(&env.midenup_home).installations[0];
assert!(
installation.intent.roots.contains("client"),
"the root survived the first install"
);
let without_client = upstream(&env, "without-client.json", &["vm"]);
let output = run_midenup(&env, &without_client, &["update", "0.15.0"]);
assert!(!output.status.success(), "a chosen root that disappeared must block the update");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("client"), "the diagnostic must name it: {stderr}");
assert!(
state_of(&env.midenup_home).installations[0].intent.roots.contains("client"),
"and the installation must be preserved"
);
}
#[test]
fn integration_a_migrated_channel_absent_upstream_is_reported_not_deleted() {
let _guard = common::harness::mutating_test_guard();
let env = environment_setup("migrated_channel_gone");
write_v1_manifest(&env, &v1_manifest("1.0.1", "0.1.0", &["vm"]));
let manifest = upstream(&env, "upstream.json", &["vm"]);
let output = run_midenup(&env, &manifest, &["show", "list"]);
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
let reported = String::from_utf8_lossy(&output.stdout);
assert!(reported.contains("0.1.0"), "the channel must still be listed: {reported}");
assert!(reported.contains("unavailable"), "and marked unavailable: {reported}");
assert_eq!(
state_of(&env.midenup_home).installations.len(),
1,
"the record must be retained, not deleted"
);
}