midenup 1.1.0

The Miden toolchain manager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
use std::{ffi::OsString, fs::OpenOptions};

use clap::Parser;
use midenup::{
    channel::UserChannel, commands::Midenup, manifest::ComponentKind, miden_wrapper, version,
};

mod common;

use common::*;

/// Tries to install the "stable" toolchain from the present manifest.
///
/// This differs from the test present in the .github directory which tries to install the
/// stable toolchain from published manifest.
#[test]
fn integration_install_stable() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_install_stable");

    // Offline fixture: this test asserts on recorded state and symlink layout, none of which
    // needs a real toolchain. See `OfflineFixture` for why that matters.
    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
    let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);

    Midenup::try_parse_from(["midenup", "install", "stable"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to install stable");

    let state_file = test_env.midenup_home.join("state").with_extension("json");
    assert!(state_file.exists(), "install must write local state");

    let mainnet_dir = test_env.midenup_home.join("toolchains").join("mainnet");
    assert!(mainnet_dir.exists());
    assert!(mainnet_dir.is_symlink());

    // Which channel a network names is not persisted in local state -- it is a property of the
    // upstream manifest and a derived symlink on disk. Assert on the symlink, and that state
    // records the version it names.
    let mainnet_version = config
        .upstream_manifest()
        .unwrap()
        .network_version("mainnet")
        .expect("upstream must declare a mainnet network")
        .clone();
    assert_eq!(
        std::fs::read_link(&mainnet_dir).unwrap().file_name().unwrap(),
        std::ffi::OsStr::new(&mainnet_version.to_string()),
        "the mainnet link must point at the channel upstream says mainnet names"
    );

    // Re-read from disk to confirm it was persisted, not merely held in memory.
    let reloaded = midenup::state::LocalState::load(&state_file).expect("state must reload");
    assert!(reloaded.get(&mainnet_version).is_some());
}

/// A fresh install must actually place every artifact kind where it belongs.
///
/// What decides whether an artifact still has to be acquired is the artifact file itself, not the
/// directory it lands in -- those directories are pre-created, so testing one would report every
/// package as already installed.
#[test]
fn integration_install_places_every_artifact_kind() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_install_places_every_artifact_kind");

    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
    let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);

    Midenup::try_parse_from(["midenup", "install", "stable", "--profile", "complete"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to install stable");

    let root = test_env.midenup_home.join("toolchains").join("mainnet");

    assert!(
        root.join("bin").join("miden-vm").exists(),
        "executable -> bin/<installed-executable>"
    );
    assert!(root.join("lib").join("core.masp").exists(), "package -> lib/<artifact-id>");
    assert!(
        root.join("etc").join("assets").join("config.yml").exists(),
        "asset -> etc/<component>/<artifact-id>"
    );

    // The package must be the real content, not a zero-length placeholder left by a partial write.
    assert_eq!(std::fs::read(root.join("lib").join("core.masp")).unwrap(), b"fixture-package");
}

/// Executable components must get their `opt/` shims.
///
/// `opt/` serves two purposes: the clap `argv[0]` trick, so help renders as `miden vm ...`; and
/// PATH discoverability, since `opt/` is the only toolchain directory on `PATH`.
///
/// Every callable executable gets one, whether or not it declares `symlink-name`: the declaration
/// chooses the shim's name, it does not decide whether there is one.
#[test]
fn integration_install_creates_default_symlinks() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_install_creates_default_symlinks");

    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
    let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);

    Midenup::try_parse_from(["midenup", "install", "stable"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to install stable");

    let opt = test_env.midenup_home.join("toolchains").join("mainnet").join("opt");
    assert!(
        opt.join("miden vm").symlink_metadata().is_ok(),
        "missing default shim for 'vm' in {}",
        opt.display()
    );
}

/// An artifact published inside a tarball installs exactly like a bare one: same destination, mode
/// and receipt, since nothing downstream of acquisition can tell the difference.
///
/// The tarball nests its file under a directory, so this also shows the directory entry is not
/// mistaken for the artifact and that nothing but the file itself reaches `lib/`.
#[test]
fn integration_install_unpacks_archived_artifacts() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_install_unpacks_archived_artifacts");

    let fixture = common::harness::OfflineFixture::new(test_env.tmp_dir.path())
        .with_channel("0.15.0")
        .with_archived_component()
        .build();
    let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);

    Midenup::try_parse_from(["midenup", "install", "0.15.0"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to install");

    let lib = test_env.midenup_home.join("toolchains").join("0.15.0").join("lib");
    assert_eq!(
        std::fs::read(lib.join("archived.masp")).unwrap(),
        b"archived-package",
        "the archived file must land under its artifact id"
    );

    let installed: Vec<String> = std::fs::read_dir(&lib)
        .unwrap()
        .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
        .collect();
    assert!(
        !installed
            .iter()
            .any(|name| name.contains("packages-1.0") || name.ends_with(".tar.gz")),
        "neither the container nor its directory may be installed: {installed:?}"
    );

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        assert_eq!(
            std::fs::metadata(lib.join("archived.masp")).unwrap().permissions().mode() & 0o777,
            0o644,
            "the mode is the planned one for a package, not the archive entry's"
        );
    }

    let midenup::state::PublicationRef::Managed { id, .. } = &state
        .get(&semver::Version::new(0, 15, 0))
        .expect("the install must be recorded")
        .publication
    else {
        panic!("a fresh install must produce a managed publication");
    };
    let publication = midenup::paths::publication_dir(
        &test_env.midenup_home,
        &semver::Version::new(0, 15, 0),
        id,
    );
    let receipt = midenup::publish::read_receipt(&publication).expect("a receipt must be written");
    assert!(
        receipt
            .outputs
            .iter()
            .any(|o| o.path == std::path::Path::new("lib/archived.masp")
                && o.owner == "packages"
                && o.realized == midenup::state::RealizedMethod::Prebuilt),
        "an archived artifact is recorded like any other prebuilt one: {:?}",
        receipt.outputs
    );
}

/// An installation is published into `publications/<channel>-<publication-id>`, described by a
/// receipt, and reached only through the `toolchains/<channel>` symlink.
///
/// The id is opaque: nothing may infer identity from the directory name, because equal plan keys
/// are not evidence of equal bytes and a name derived from one would invite reusing the other's
/// content.
#[test]
fn integration_install_publishes_into_an_opaque_publication_with_a_receipt() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_install_publishes");

    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
    let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);

    Midenup::try_parse_from(["midenup", "install", "0.15.0"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to install");

    let channel = semver::Version::new(0, 15, 0);
    let installed = state.get(&channel).expect("the install must be recorded");
    let midenup::state::PublicationRef::Managed { id, plan_key, .. } = &installed.publication
    else {
        panic!("a fresh install must produce a managed publication");
    };

    let publication = midenup::paths::publication_dir(&test_env.midenup_home, &channel, id);
    assert!(publication.is_dir(), "{} must exist", publication.display());
    assert!(
        !publication.to_string_lossy().contains(&plan_key.to_string()[4..12]),
        "the publication must not be named after the plan key"
    );

    // The toolchain link is the only stable name; everything else reaches the publication through
    // it, which is what lets the publication behind it be replaced atomically.
    let link = test_env.midenup_home.join("toolchains").join("0.15.0");
    assert_eq!(
        std::fs::canonicalize(&link).unwrap(),
        std::fs::canonicalize(&publication).unwrap()
    );

    let receipt = midenup::publish::read_receipt(&publication).expect("a receipt must be written");
    assert_eq!(receipt.publication_id, *id);
    assert_eq!(&receipt.plan_key, plan_key);
    assert!(
        receipt.outputs.iter().any(|o| o.path == std::path::Path::new("bin/miden-vm")
            && o.owner == "vm"
            && o.realized == midenup::state::RealizedMethod::Prebuilt),
        "the receipt must record every installed file and how it was obtained: {:?}",
        receipt.outputs
    );
}

/// Adding a component publishes a *new* publication, seeded from the old one's receipt. The
/// previous publication is never modified in place, and is not deleted either: another process may
/// still be executing out of it, so it is left unreferenced for `midenup gc`.
#[test]
fn integration_install_republishes_rather_than_mutating() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_install_republishes");

    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
    let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
    let channel = semver::Version::new(0, 15, 0);

    let publication_of =
        |state: &LocalManifest| match &state.get(&channel).expect("installed").publication {
            midenup::state::PublicationRef::Managed { id, .. } => {
                midenup::paths::publication_dir(&test_env.midenup_home, &channel, id)
            },
            other => panic!("expected a managed publication, got {other:?}"),
        };

    Midenup::try_parse_from(["midenup", "install", "0.15.0"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to install");
    let first = publication_of(&state);

    // The `complete` profile adds `assets`, which the minimal install did not have.
    Midenup::try_parse_from(["midenup", "install", "0.15.0", "--profile", "complete"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to reinstall");
    let second = publication_of(&state);

    assert_ne!(first, second, "a changed installed set must produce a new publication");
    assert!(
        first.is_dir(),
        "the publication it replaced must be left intact for gc, not deleted underneath whatever \
         may still be running from it"
    );
    assert!(
        second.join("lib").join("core.masp").exists(),
        "unchanged files must be seeded from the previous publication"
    );
    assert!(
        second.join("etc").join("assets").join("config.yml").exists(),
        "the added component must be installed"
    );
}

/// `%var(data)` holds the Miden client's database, so it lives outside the publication: a
/// publication is replaced wholesale on every change, and anything inside one goes with it.
#[test]
fn integration_var_survives_update_and_republication() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_var_survives");

    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
    let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
    let channel = semver::Version::new(0, 15, 0);

    Midenup::try_parse_from(["midenup", "install", "0.15.0"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to install");

    // Stand in for whatever the client would have written under `%var(data)`.
    let var =
        midenup::paths::var_dir(&test_env.midenup_home, &UserChannel::Version(channel.clone()));
    std::fs::create_dir_all(&var).unwrap();
    std::fs::write(var.join("data"), b"user-database").unwrap();

    // Republish with a different component set, which produces a whole new publication.
    Midenup::try_parse_from(["midenup", "install", "0.15.0", "--profile", "complete"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to republish");

    assert_eq!(
        std::fs::read(var.join("data")).unwrap(),
        b"user-database",
        "user data must survive republication"
    );

    // And it must live outside the publication, which is *why* it survives.
    let midenup::state::PublicationRef::Managed { id, .. } =
        &state.get(&channel).unwrap().publication
    else {
        panic!("expected a managed publication");
    };
    let publication = midenup::paths::publication_dir(&test_env.midenup_home, &channel, id);
    assert!(!publication.join("var").exists(), "no publication may contain var/");
}

/// Spec section 9.3: when a `prebuilt-with-cargo-fallback` component's artifact cannot be
/// acquired, midenup builds it from source instead, and the receipt records which path was really
/// taken -- uninstall has to match the method that was actually used.
#[test]
fn integration_install_falls_back_to_cargo_when_an_artifact_is_unavailable() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_install_fallback");

    let sources = common::harness::SourceFixture::build(test_env.tmp_dir.path());
    let manifest = serde_json::json!({
        "manifest_version": "3.0.0",
        "date": 1735689600,
        "channels": [{
            "name": "0.15.0",
            "components": [{
                "name": "vm",
                "version": {"kind": "path", "path": sources.path_crate.to_str().unwrap()},
                "kind": "executable",
                "installation_method": {
                    "kind": "prebuilt-with-cargo-fallback",
                    "crate_name": "fixture-vm"
                },
                "installed-executable": "miden-vm",
                "profiles": ["minimal"],
                // Declared for this target, and absent from the filesystem: available at planning
                // time, unavailable at execution time, which is precisely the case the fallback
                // exists for.
                "artifacts": {"miden-vm": {"uri": "file:///nonexistent/miden-vm"}}
            }]
        }]
    });
    let manifest_path = test_env.tmp_dir.path().join("fallback-manifest.json");
    std::fs::write(&manifest_path, serde_json::to_string_pretty(&manifest).unwrap()).unwrap();

    let (mut state, config) = test_setup(&test_env, &format!("file://{}", manifest_path.display()));

    Midenup::try_parse_from(["midenup", "install", "0.15.0"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("a failed transfer with a declared fallback must not fail the install");

    let channel = semver::Version::new(0, 15, 0);
    let midenup::state::PublicationRef::Managed { id, .. } =
        &state.get(&channel).expect("installed").publication
    else {
        panic!("expected a managed publication");
    };
    let publication = midenup::paths::publication_dir(&test_env.midenup_home, &channel, id);

    assert!(
        publication.join("bin").join("miden-vm").exists(),
        "the fallback must install it"
    );

    let receipt = midenup::publish::read_receipt(&publication).unwrap();
    let vm = receipt
        .outputs
        .iter()
        .find(|o| o.owner == "vm")
        .expect("the receipt must record the binary");
    assert_eq!(
        vm.realized,
        midenup::state::RealizedMethod::Cargo,
        "the receipt must record the method actually taken, not the one declared"
    );
}

/// `path` and `git` authorities must be recorded at install time and re-checked on update.
///
/// The behaviour under test is update *detection*: midenup records a path's modification time and
/// a git revision, then reinstalls when either changes. The sources are trivial local crates
/// rather than real components -- cloning a component repository and building it proves nothing
/// extra here and costs minutes.
#[test]
fn integration_install_from_non_cargo() {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_install_from_non_cargo");

    let fixture = common::harness::SourceFixture::build(test_env.tmp_dir.path());
    let manifest_dir = test_env.tmp_dir.path();

    // Reads the recorded path mtime and git revision out of local state.
    let recorded = |state: &LocalManifest| {
        let named = std::fs::read_link(test_env.midenup_home.join("toolchains").join("mainnet"))
            .expect("the mainnet symlink must exist");
        let version = semver::Version::parse(named.file_name().unwrap().to_str().unwrap()).unwrap();
        let channel = state
            .get(&version)
            .expect("state must record the channel mainnet names")
            .as_channel();

        let last_modification = match channel.get_component("vm").unwrap().version {
            version::Authority::Path { last_modification, .. } => last_modification
                .expect("a path authority must record the tree's modification time"),
            ref authority => panic!("expected 'vm' to have a path authority, got {authority}"),
        };

        let revision = match &channel.get_component("client").unwrap().version {
            version::Authority::Git {
                target: version::GitTarget::Revision { hash },
                ..
            } => hash.clone(),
            authority => panic!("expected 'client' to have a git authority, got {authority}"),
        };

        (last_modification, revision)
    };

    let first = common::harness::write_source_manifest(
        manifest_dir,
        "manifest-1.json",
        &fixture,
        &fixture.revisions[0],
    );
    let (mut state, config) = test_setup(&test_env, &first);

    Midenup::try_parse_from(["midenup", "install", "stable"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to install stable");

    let (time_when_installed, hash_when_installed) = recorded(&state);
    assert_eq!(
        hash_when_installed, fixture.revisions[0],
        "the installed revision must be the one the manifest named"
    );

    // Nothing has changed, so an update must be a no-op for both authorities.
    Midenup::try_parse_from(["midenup", "update"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to update");

    let (unchanged_time, unchanged_revision) = recorded(&state);
    assert_eq!(unchanged_time, time_when_installed, "an unchanged path must not be reinstalled");
    assert_eq!(
        unchanged_revision, hash_when_installed,
        "an unchanged revision must not be reinstalled"
    );

    // Now change both: point the manifest at the second commit, and touch the path source.
    let second = common::harness::write_source_manifest(
        manifest_dir,
        "manifest-2.json",
        &fixture,
        &fixture.revisions[1],
    );
    let (_, config) = test_setup(&test_env, &second);
    OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(false)
        .open(fixture.path_crate.join("trigger-update"))
        .unwrap();

    Midenup::try_parse_from(["midenup", "update", "--path-update=all"])
        .unwrap()
        .execute_with_state(&config, &mut state)
        .expect("failed to update");

    let (new_time, new_revision) = recorded(&state);
    assert!(new_time > time_when_installed, "a touched path source must be reinstalled");
    assert_eq!(
        new_revision, fixture.revisions[1],
        "a changed revision must be reinstalled at the new commit"
    );
}

/// Pre-release check: every component in the real stable toolchain is actually executable.
///
/// This relies on every component respecting the `--help` flag, an assumption `miden_wrapper`
/// already makes because clap generates help automatically.
///
/// Deliberately kept on the real manifest and a real install -- it is the one test that proves the
/// whole pipeline produces binaries that run, which means it downloads and builds real components
/// and takes minutes. Everything asserting only on layout or recorded state uses the offline
/// fixture instead.
///
/// The `prerelease` marker in the name excludes it from `make integration-test`; run it with
/// `make prerelease-test`. See the Makefile.
///
/// [See here for details](https://docs.rs/clap/latest/clap/struct.Command.html#method.disable_help_flag)
#[test]
fn integration_prerelease_components_are_runnable() {
    let _guard = common::harness::mutating_test_guard();
    let test_name = "integration_test_components";
    let test_env = environment_setup(test_name);

    const FILE: &str = full_path_manifest!("manifest/channel-manifest.json");
    let (mut local_manifest, config) = test_setup(&test_env, FILE);

    // Install the latest stable toolchain
    let command =
        Midenup::try_parse_from(["midenup", "install", "stable", "--profile", "complete"]).unwrap();
    command
        .execute_with_state(&config, &mut local_manifest)
        .expect("failed to install stable");

    let named = std::fs::read_link(test_env.midenup_home.join("toolchains").join("mainnet"))
        .expect("the mainnet symlink must exist");
    let version = semver::Version::parse(named.file_name().unwrap().to_str().unwrap()).unwrap();
    let stable_channel = local_manifest
        .get(&version)
        .expect("state must record the channel mainnet names")
        .as_channel();

    println!("Installed: {}", stable_channel);

    // Verify each executable component is accessible and runnable
    for component in &stable_channel.components {
        match component.kind() {
            ComponentKind::Executable { installation_method, spec }
            | ComponentKind::CargoExtension { installation_method, spec }
                if !spec.is_hidden() =>
            {
                let argv: Vec<OsString> =
                    vec!["miden".into(), "help".into(), component.name.as_ref().into()];

                miden_wrapper::miden_wrapper(&argv, &config, &mut local_manifest).unwrap_or_else(
                    |err| {
                        panic!(
                            "Component '{}' is not runnable through the 'miden' interface: {}",
                            component.name, err
                        )
                    },
                );
            },
            // Skip executables that aren't meant to be executed directly
            ComponentKind::Executable { .. } | ComponentKind::CargoExtension { .. } => (),
            // Skip non-executable components, or command aliases
            ComponentKind::Asset
            | ComponentKind::Command { .. }
            | ComponentKind::Package
            | ComponentKind::LegacyPackage { .. } => (),
            // The checked-in manifest declares no unknown kinds; if one appears, the manifest and
            // this build have diverged and the test should say so rather than skipping quietly.
            ComponentKind::Unsupported { tag, .. } => {
                panic!("component '{}' has unsupported kind '{tag}'", component.name)
            },
        }
    }
}

/// Spec section 9.2: a `path` source that changes *while* it is being built produces an
/// installation matching neither the tree that was pinned nor the one on disk, so it is refused
/// before anything is published.
#[test]
fn integration_a_path_source_that_moves_during_the_build_is_refused() {
    use std::sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    };

    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup("integration_path_moved");

    let sources = common::harness::SourceFixture::build(test_env.tmp_dir.path());
    let manifest = serde_json::json!({
        "manifest_version": "3.0.0",
        "date": 1735689600,
        "channels": [{
            "name": "0.15.0",
            "components": [{
                "name": "vm",
                "version": {"kind": "path", "path": sources.path_crate.to_str().unwrap()},
                "kind": "executable",
                "installation_method": {"kind": "cargo", "crate_name": "fixture-vm"},
                "installed-executable": "miden-vm",
                "profiles": ["minimal"]
            }]
        }]
    });
    let manifest_path = test_env.tmp_dir.path().join("moving-source.json");
    std::fs::write(&manifest_path, serde_json::to_string_pretty(&manifest).unwrap()).unwrap();

    let (mut state, config) = test_setup(&test_env, &format!("file://{}", manifest_path.display()));

    // Stand in for an editor saving into the source tree while the build runs. It writes
    // continuously rather than once, so that a write is guaranteed to land after the plan pinned
    // the tree and before the post-build check reads it -- otherwise the test would be a race
    // against how long a trivial `cargo install` happens to take.
    let editing = Arc::new(AtomicBool::new(true));
    let editor = {
        let editing = Arc::clone(&editing);
        let file = sources.path_crate.join("edited-during-the-build");
        std::thread::spawn(move || {
            while editing.load(Ordering::Relaxed) {
                let _ = std::fs::write(&file, format!("{:?}", std::time::SystemTime::now()));
                std::thread::sleep(std::time::Duration::from_millis(25));
            }
        })
    };

    let result = Midenup::try_parse_from(["midenup", "install", "0.15.0"])
        .unwrap()
        .execute_with_state(&config, &mut state);

    editing.store(false, Ordering::Relaxed);
    editor.join().expect("the editing thread must not panic");

    let err = result.expect_err("a source that moved during the build must not be published");
    let message = format!("{err:#}");
    assert!(
        message.contains("changed") || message.contains("retry"),
        "the diagnostic must say what happened: {message}"
    );

    assert!(
        state.get(&semver::Version::new(0, 15, 0)).is_none(),
        "and nothing may be recorded as installed"
    );
}

/// A direct install over a patched installation must re-acquire the component rather than seed the
/// patched build into the new publication.
#[test]
fn integration_install_after_a_patch_does_not_keep_the_patched_build() {
    assert_unpatched_by("integration_install_after_patch", &["midenup", "install", "0.15.0"]);
}

/// An update drops the patches, so it must re-acquire a patched component even when the path-update
/// policy would otherwise keep it.
#[test]
fn integration_update_after_a_patch_does_not_keep_the_patched_build() {
    assert_unpatched_by(
        "integration_update_after_patch",
        &["midenup", "update", "--path-update=off", "0.15.0"],
    );
}

/// Installs `vm` patched from a path through a project, runs `args` outside it, and asserts the
/// upstream `vm` is what ends up installed and recorded.
fn assert_unpatched_by(test_name: &str, args: &[&str]) {
    let _guard = common::harness::mutating_test_guard();
    let test_env = environment_setup(test_name);

    let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
    let sources = common::harness::SourceFixture::build(test_env.tmp_dir.path());
    let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);

    let channel = semver::Version::new(0, 15, 0);
    let vm = midenup::paths::toolchain_link(&test_env.midenup_home, &channel)
        .join("bin")
        .join("miden-vm");
    let upstream_vm = std::fs::read(fixture.dir.join("0.15.0").join("miden-vm")).unwrap();

    let project = test_env.tmp_dir.path().join("patched-project");
    std::fs::create_dir_all(&project).unwrap();
    std::fs::write(
        project.join("miden-toolchain.toml"),
        format!(
            "[toolchain]\nchannel = \"0.15.0\"\ncomponents = [\"vm\"]\n\n[patches.vm]\ncrate_name \
             = \"fixture-vm\"\nversion = {{ kind = \"path\", path = \"{}\" }}\n",
            sources.path_crate.display()
        ),
    )
    .unwrap();
    let project_config = midenup::config::Config::init(
        project,
        test_env.midenup_home.clone(),
        test_env.cargo_home.clone(),
        &fixture.manifest_uri,
        true,
    )
    .unwrap();

    Midenup::try_parse_from(["miden", "help", "vm"])
        .unwrap()
        .execute_with_state(&project_config, &mut state)
        .expect("failed to activate the patched toolchain");
    assert!(std::fs::read(&vm).unwrap() != upstream_vm, "the patch must be installed");

    Midenup::try_parse_from(args)
        .unwrap()
        .execute_with_state(&config, &mut state)
        .unwrap_or_else(|err| panic!("{args:?} failed: {err:#}"));

    let installation = state.get(&channel).unwrap();
    assert!(installation.patches.is_empty());
    let recorded_vm = installation.components.iter().find(|c| c.name == "vm").unwrap();
    assert!(
        matches!(recorded_vm.version, version::Authority::Registry { .. }),
        "the upstream vm must be recorded, not the patch: {:?}",
        recorded_vm.version
    );
    assert!(
        std::fs::read(&vm).unwrap() == upstream_vm,
        "the patched build must not be carried forward"
    );
}