Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Refactor commands.rs into sub tests
Adrian Duke committed 2 months ago
commit 383a4d439bd2f3133122d78026e40c3875678340
parent 94f6f11ff86ab5022a88cc035e2746aab907c286
9 files changed +2910 -2838
modified crates/radicle-cli/tests/commands.rs
@@ -1,35 +1,21 @@
-
use core::panic;
use std::path::Path;
-
use std::str::FromStr;
-
use std::{fs, net, thread, time};

-
use radicle::cob;
-
use radicle::git;
-
use radicle::node;
-
use radicle::node::address::Store as _;
-
use radicle::node::config::seeds::RADICLE_NODE_BOOTSTRAP_IRIS;
-
use radicle::node::config::DefaultSeedingPolicy;
-
use radicle::node::events::Event;
-
use radicle::node::policy::Scope;
-
use radicle::node::routing::Store as _;
-
use radicle::node::UserAgent;
-
use radicle::node::{Address, Alias, Config, Handle as _, DEFAULT_TIMEOUT};
-
use radicle::prelude::{NodeId, RepoId};
-
use radicle::profile;
use radicle::profile::Home;
-
use radicle::storage::{ReadStorage, RefUpdate, RemoteRepository};
-
use radicle::test::fixtures;
-

-
use radicle_localtime::LocalTime;
-
#[allow(unused_imports)]
-
use radicle_node::test::logger;
-
use radicle_node::test::node::Node;
-
use radicle_node::PROTOCOL_VERSION;

mod util;
-
use util::environment::{config, Environment};
use util::formula::formula;

+
mod commands {
+
    mod cob;
+
    mod git;
+
    mod id;
+
    mod init;
+
    mod issue;
+
    mod misc;
+
    mod node;
+
    mod patch;
+
}
+

/// Run a CLI test file.
pub(crate) fn test<'a>(
    test: impl AsRef<Path>,
@@ -68,7 +54,7 @@ pub(crate) fn test<'a>(
///
/// If there is an error executing the program other than the program not being
/// found, or the program does not exit successfully.
-
fn program_reports_version(program: &str) -> bool {
+
pub(crate) fn program_reports_version(program: &str) -> bool {
    use std::io::ErrorKind;
    use std::process::{Command, Stdio};

@@ -86,2815 +72,3 @@ fn program_reports_version(program: &str) -> bool {
        Ok(status) => panic!("executing `{program}` resulted in status {status}"),
    }
}
-

-
#[test]
-
fn rad_help() {
-
    Environment::alice(["rad-help"]);
-
}
-

-
#[test]
-
fn rad_auth() {
-
    test("examples/rad-auth.md", Path::new("."), None, []).unwrap();
-
}
-

-
#[test]
-
fn rad_key_mismatch() {
-
    let mut environment = Environment::new();
-
    let alice = environment.profile("alice");
-
    environment.repository(&alice);
-

-
    environment.test("rad-init", &alice).unwrap();
-

-
    // Replace the public key with one that does not match the secret key anymore.
-
    fs::write(alice.home.path().join("keys").join("radicle.pub"), "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE6Ul/D+P0I/Hl1JVOWGS8Z589us9FqKQXWv8OMOpKCh snakeoil\n").unwrap();
-

-
    environment.test("rad-key-mismatch", &alice).unwrap();
-
}
-

-
#[test]
-
fn rad_auth_errors() {
-
    test("examples/rad-auth-errors.md", Path::new("."), None, []).unwrap();
-
}
-

-
#[test]
-
fn rad_issue() {
-
    Environment::alice(["rad-init", "rad-issue"]);
-
}
-

-
#[test]
-
fn rad_issue_list() {
-
    Environment::alice(["rad-init", "rad-issue", "rad-issue-list"]);
-
}
-

-
#[test]
-
fn rad_cob_update() {
-
    Environment::alice(["rad-init", "rad-cob-log"]);
-
}
-

-
#[test]
-
fn rad_cob_update_identity() {
-
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-
    let working = environment.tempdir().join("working");
-
    let home = &profile.home;
-

-
    let base = Path::new(env!("CARGO_MANIFEST_DIR"));
-

-
    std::fs::create_dir_all(base).unwrap();
-
    std::fs::create_dir_all(working.clone()).unwrap();
-

-
    // Setup a test repository.
-
    fixtures::repository(&working);
-

-
    test("examples/rad-init.md", &working, Some(home), []).unwrap();
-
    test(
-
        "examples/rad-cob-update-identity.md",
-
        &working,
-
        Some(home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_cob_multiset() {
-
    // `rad-cob-multiset` is a `jq` script, which requires `jq` to be installed.
-
    // We test whether `jq` is installed, and have this test succeed if it is not.
-
    // Programmatic skipping of tests is not supported as of 2024-08.
-
    if !program_reports_version("jq") {
-
        return;
-
    }
-

-
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-
    let home = &profile.home;
-
    let working = environment.tempdir().join("working");
-

-
    let base = Path::new(env!("CARGO_MANIFEST_DIR"));
-
    std::fs::create_dir_all(base).unwrap();
-
    std::fs::create_dir_all(working.clone()).unwrap();
-

-
    // Copy over the script that implements the multiset COB.
-
    std::fs::copy(
-
        base.join("examples").join("rad-cob-multiset"),
-
        working.join("rad-cob-multiset"),
-
    )
-
    .unwrap();
-

-
    // Setup a test repository.
-
    fixtures::repository(&working);
-

-
    test("examples/rad-init.md", &working, Some(home), []).unwrap();
-
    test("examples/rad-cob-multiset.md", &working, Some(home), []).unwrap();
-
}
-

-
#[test]
-
fn rad_cob_log() {
-
    Environment::alice(["rad-init", "rad-cob-log"]);
-
}
-

-
#[test]
-
fn rad_cob_show() {
-
    Environment::alice(["rad-init", "rad-cob-show"]);
-
}
-

-
#[test]
-
fn rad_cob_migrate() {
-
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-
    let home = &profile.home;
-

-
    home.cobs_db_mut()
-
        .unwrap()
-
        .raw_query(|conn| conn.execute("PRAGMA user_version = 0"))
-
        .unwrap();
-

-
    environment.repository(&profile);
-

-
    environment
-
        .tests(["rad-init", "rad-cob-migrate"], &profile)
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_cob_operations() {
-
    Environment::alice(["rad-init", "rad-cob-operations"]);
-
}
-

-
#[test]
-
#[ignore = "part of many other tests"]
-
fn rad_init() {
-
    Environment::alice(["rad-init"]);
-
}
-

-
#[test]
-
fn rad_init_no_announce() {
-
    Environment::alice(["rad-init-no-announce"]);
-
}
-

-
#[test]
-
fn rad_init_bare() {
-
    let mut env = Environment::new();
-
    let alice = env.profile("alice");
-
    radicle::test::fixtures::bare_repository(env.work(&alice).as_path());
-
    env.tests(["git/git-is-bare-repository", "rad-init"], &alice)
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_init_existing() {
-
    let mut environment = Environment::new();
-
    let mut profile = environment.node("alice");
-
    let working = tempfile::tempdir().unwrap();
-
    let rid = profile.project("heartwood", "Radicle Heartwood Protocol & Stack");
-

-
    test(
-
        "examples/rad-init-existing.md",
-
        working.path(),
-
        Some(&profile.home),
-
        [(
-
            "URL",
-
            git::url::File::new(profile.storage.path())
-
                .rid(rid)
-
                .to_string()
-
                .as_str(),
-
        )],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_init_existing_bare() {
-
    let mut environment = Environment::new();
-
    let mut profile = environment.node("alice");
-
    let working = tempfile::tempdir().unwrap();
-
    let rid = profile.project("heartwood", "Radicle Heartwood Protocol & Stack");
-

-
    test(
-
        "examples/rad-init-existing-bare.md",
-
        working.path(),
-
        Some(&profile.home),
-
        [(
-
            "URL",
-
            git::url::File::new(profile.storage.path())
-
                .rid(rid)
-
                .to_string()
-
                .as_str(),
-
        )],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_init_no_seed() {
-
    Environment::alice(["rad-init-no-seed"]);
-
}
-

-
#[test]
-
fn rad_init_with_existing_remote() {
-
    Environment::alice(["rad-init-with-existing-remote"]);
-
}
-

-
#[test]
-
fn rad_init_no_git() {
-
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-

-
    // NOTE: There is no repository set up here.
-

-
    environment.test("rad-init-no-git", &profile).unwrap();
-
}
-

-
#[test]
-
fn rad_init_detached_head() {
-
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-

-
    // NOTE: There is no repository set up here.
-

-
    environment
-
        .test("rad-init-detached-head", &profile)
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_inspect() {
-
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-

-
    environment.repository(&profile);
-

-
    environment
-
        .tests(["rad-init", "rad-inspect"], &profile)
-
        .unwrap();
-

-
    // NOTE: The next test runs without $RAD_HOME set.
-
    test(
-
        "examples/rad-inspect-noauth.md",
-
        environment.work(&profile),
-
        None,
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_config() {
-
    let mut environment = Environment::new();
-
    let alias = Alias::new("alice");
-
    let profile = environment.profile_with(profile::Config {
-
        preferred_seeds: vec![RADICLE_NODE_BOOTSTRAP_IRIS.clone().first().unwrap().clone()],
-
        ..profile::Config::new(alias)
-
    });
-
    let working = tempfile::tempdir().unwrap();
-

-
    test(
-
        "examples/rad-config.md",
-
        working.path(),
-
        Some(&profile.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_warn_old_nodes() {
-
    Environment::alice(["rad-warn-old-nodes"]);
-
}
-

-
#[test]
-
fn rad_checkout() {
-
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-
    let copy = tempfile::tempdir().unwrap();
-

-
    environment.repository(&profile);
-

-
    environment.test("rad-init", &profile).unwrap();
-
    test(
-
        "examples/rad-checkout.md",
-
        copy.path(),
-
        Some(&profile.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    if cfg!(target_os = "linux") {
-
        test(
-
            "examples/rad-checkout-repo-config-linux.md",
-
            copy.path(),
-
            Some(&profile.home),
-
            [],
-
        )
-
        .unwrap();
-
    } else if cfg!(target_os = "macos") {
-
        test(
-
            "examples/rad-checkout-repo-config-macos.md",
-
            copy.path(),
-
            Some(&profile.home),
-
            [],
-
        )
-
        .unwrap();
-
    }
-
}
-

-
#[test]
-
fn rad_id() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let bob = bob.spawn();
-

-
    alice.handle.seed(acme, Scope::All).unwrap();
-
    alice.connect(&bob).converge([&bob]);
-

-
    let events = alice.handle.events();
-
    bob.fork(acme, bob.home.path()).unwrap();
-
    bob.announce(acme, 2, bob.home.path()).unwrap();
-
    alice.has_remote_of(&acme, &bob.id);
-

-
    // Alice must have Bob to try add them as a delegate
-
    events
-
        .wait(
-
            |e| matches!(e, Event::RefsFetched { .. }).then_some(()),
-
            time::Duration::from_secs(6),
-
        )
-
        .unwrap();
-

-
    test(
-
        "examples/rad-id.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_id_threshold() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let seed = environment.node("seed");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut seed = seed.spawn();
-
    let mut bob = bob.spawn();
-

-
    seed.handle.seed(acme, Scope::All).unwrap();
-
    alice.handle.seed(acme, Scope::Followed).unwrap();
-
    alice
-
        .handle
-
        .follow(seed.id, Some(Alias::new("seed")))
-
        .unwrap();
-

-
    alice.connect(&seed).connect(&bob);
-
    bob.connect(&seed);
-
    alice.routes_to(&[(acme, seed.id)]);
-
    seed.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
-

-
    formula(&environment.tempdir(), "examples/rad-id-threshold.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .home(
-
            "seed",
-
            environment.work(&seed),
-
            [("RAD_HOME", seed.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_id_threshold_soft_fork() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    let events = bob.handle.events();
-
    bob.handle.seed(acme, Scope::All).unwrap();
-
    alice.connect(&bob).converge([&bob]);
-

-
    events
-
        .wait(
-
            |e| matches!(e, Event::RefsFetched { .. }).then_some(()),
-
            time::Duration::from_secs(6),
-
        )
-
        .unwrap();
-

-
    formula(
-
        &environment.tempdir(),
-
        "examples/rad-id-threshold-soft-fork.md",
-
    )
-
    .unwrap()
-
    .home(
-
        "alice",
-
        environment.work(&alice),
-
        [("RAD_HOME", alice.home.path().display())],
-
    )
-
    .home(
-
        "bob",
-
        environment.work(&bob),
-
        [("RAD_HOME", bob.home.path().display())],
-
    )
-
    .run()
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_id_update_delete_field() {
-
    Environment::alice(["rad-init", "rad-id-update-delete-field"]);
-
}
-

-
#[test]
-
fn rad_id_multi_delegate() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let eve = environment.node("eve");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let mut eve = eve.spawn();
-

-
    alice.handle.seed(acme, Scope::All).unwrap();
-
    bob.handle.follow(eve.id, None).unwrap();
-
    eve.handle.follow(bob.id, None).unwrap();
-
    alice.connect(&bob).converge([&bob]);
-
    eve.connect(&alice).converge([&alice]);
-

-
    bob.fork(acme, environment.work(&bob)).unwrap();
-
    bob.has_remote_of(&acme, &alice.id);
-
    alice.has_remote_of(&acme, &bob.id);
-

-
    eve.fork(acme, environment.work(&eve)).unwrap();
-
    eve.has_remote_of(&acme, &bob.id);
-
    alice.has_remote_of(&acme, &eve.id);
-
    alice.is_synced_with(&acme, &eve.id);
-
    alice.is_synced_with(&acme, &bob.id);
-

-
    // TODO: Have formula with two connected nodes and a tracked project.
-
    formula(&environment.tempdir(), "examples/rad-id-multi-delegate.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_id_unauthorized_delegate() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    // Alice sets up the seed
-
    alice.handle.seed(acme, Scope::Followed).unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.rad(
-
        "clone",
-
        &[acme.to_string().as_str()],
-
        environment.work(&bob),
-
    )
-
    .unwrap();
-

-
    formula(
-
        &environment.tempdir(),
-
        "examples/rad-id-unauthorized-delegate.md",
-
    )
-
    .unwrap()
-
    .home(
-
        "alice",
-
        environment.work(&alice),
-
        [("RAD_HOME", alice.home.path().display())],
-
    )
-
    .home(
-
        "bob",
-
        environment.work(&bob),
-
        [("RAD_HOME", bob.home.path().display())],
-
    )
-
    .run()
-
    .unwrap();
-
}
-

-
#[test]
-
#[ignore = "slow"]
-
fn rad_id_collaboration() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let eve = environment.node("eve");
-
    let seed = environment.seed("seed");
-
    let distrustful = environment.seed("distrustful");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let mut eve = eve.spawn();
-
    let mut seed = seed.spawn();
-
    let mut distrustful = distrustful.spawn();
-

-
    // Alice sets up the seed and follows Bob and Eve via the CLI
-
    alice.handle.seed(acme, Scope::Followed).unwrap();
-
    alice
-
        .handle
-
        .follow(seed.id, Some(Alias::new("seed")))
-
        .unwrap();
-

-
    // The seed is trustful and will fetch from anyone
-
    seed.handle.seed(acme, Scope::All).unwrap();
-

-
    // The distrustful seed will only interact with Alice and Bob
-
    distrustful.handle.seed(acme, Scope::Followed).unwrap();
-
    distrustful.handle.follow(alice.id, None).unwrap();
-
    distrustful.handle.follow(bob.id, None).unwrap();
-

-
    alice
-
        .connect(&seed)
-
        .connect(&distrustful)
-
        .converge([&seed, &distrustful]);
-
    bob.connect(&seed)
-
        .connect(&distrustful)
-
        .converge([&seed, &distrustful]);
-
    eve.connect(&seed)
-
        .connect(&distrustful)
-
        .converge([&seed, &distrustful]);
-

-
    seed.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
-
    distrustful
-
        .handle
-
        .fetch(acme, alice.id, DEFAULT_TIMEOUT)
-
        .unwrap();
-

-
    formula(&environment.tempdir(), "examples/rad-id-collaboration.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .home(
-
            "eve",
-
            environment.work(&eve),
-
            [("RAD_HOME", eve.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-

-
    // Ensure the seeds have fetched all nodes.
-
    let repo = seed.storage.repository(acme).unwrap();
-
    let mut remotes = repo
-
        .remote_ids()
-
        .unwrap()
-
        .collect::<Result<Vec<_>, _>>()
-
        .unwrap();
-
    let mut expected = vec![alice.id, bob.id, eve.id];
-
    remotes.sort();
-
    expected.sort();
-
    assert_eq!(remotes, expected);
-

-
    let repo = distrustful.storage.repository(acme).unwrap();
-
    let mut remotes = repo
-
        .remote_ids()
-
        .unwrap()
-
        .collect::<Result<Vec<_>, _>>()
-
        .unwrap();
-
    let mut expected = vec![alice.id, bob.id, eve.id];
-
    remotes.sort();
-
    expected.sort();
-
    assert_eq!(remotes, expected);
-
}
-

-
#[test]
-
fn rad_id_conflict() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let bob = bob.spawn();
-

-
    alice.connect(&bob).converge([&bob]);
-

-
    bob.fork(acme, environment.work(&bob)).unwrap();
-
    bob.announce(acme, 2, bob.home.path()).unwrap();
-
    alice.has_remote_of(&acme, &bob.id);
-

-
    formula(&environment.tempdir(), "examples/rad-id-conflict.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_id_unknown_field() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-

-
    environment.repository(&alice);
-
    environment.test("rad-init", &alice).unwrap();
-

-
    let alice = alice.spawn();
-
    environment.test("rad-id-unknown-field", &alice).unwrap();
-
}
-

-
#[test]
-
fn rad_id_private() {
-
    Environment::alice(["rad-init-private", "rad-id-private"]);
-
}
-

-
#[test]
-
fn rad_node_connect() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let working = tempfile::tempdir().unwrap();
-
    let alice = alice.spawn();
-
    let bob = bob.spawn();
-

-
    alice
-
        .rad(
-
            "node",
-
            &["connect", format!("{}@{}", bob.id, bob.addr).as_str()],
-
            working.path(),
-
        )
-
        .unwrap();
-

-
    let sessions = alice.handle.sessions().unwrap();
-
    let session = sessions.first().unwrap();
-

-
    assert_eq!(session.nid, bob.id);
-
    assert_eq!(session.addr, bob.addr.into());
-
    assert!(session.state.is_connected());
-
}
-

-
#[test]
-
fn rad_node_connect_without_address() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let working = tempfile::tempdir().unwrap();
-
    let bob = bob.spawn();
-

-
    alice
-
        .db
-
        .addresses_mut()
-
        .insert(
-
            &bob.id,
-
            PROTOCOL_VERSION,
-
            node::Features::SEED,
-
            &Alias::new("bob"),
-
            0,
-
            &UserAgent::default(),
-
            LocalTime::now().into(),
-
            [node::KnownAddress::new(
-
                node::Address::from(bob.addr),
-
                node::address::Source::Imported,
-
            )],
-
        )
-
        .unwrap();
-
    let alice = alice.spawn();
-
    alice
-
        .rad(
-
            "node",
-
            &["connect", format!("{}", bob.id).as_str()],
-
            working.path(),
-
        )
-
        .unwrap();
-

-
    let sessions = alice.handle.sessions().unwrap();
-
    let session = sessions.first().unwrap();
-

-
    assert_eq!(session.nid, bob.id);
-
    assert_eq!(session.addr, bob.addr.into());
-
    assert!(session.state.is_connected());
-
}
-

-
#[test]
-
fn rad_node() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node_with(Config {
-
        external_addresses: vec![
-
            Address::from(net::SocketAddr::from(([41, 12, 98, 112], 8776))),
-
            Address::from_str("seed.cloudhead.io:8776").unwrap(),
-
        ],
-
        seeding_policy: DefaultSeedingPolicy::Block,
-
        ..Config::test(Alias::new("alice"))
-
    });
-
    let working = tempfile::tempdir().unwrap();
-
    let alice = alice.spawn();
-

-
    fixtures::repository(working.path().join("alice"));
-

-
    test(
-
        "examples/rad-init-sync-not-connected.md",
-
        working.path().join("alice"),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    test(
-
        "examples/rad-node.md",
-
        working.path().join("alice"),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_patch() {
-
    Environment::alice(["rad-init", "rad-patch"]);
-
}
-

-
#[test]
-
fn rad_jj_bare() {
-
    // We test whether `jj` is installed, and have this test succeed if it is not.
-
    // Programmatic skipping of tests is not supported as of 2024-08.
-
    if !program_reports_version("jj") {
-
        return;
-
    }
-

-
    let mut environment = Environment::new();
-
    let mut profile = environment.node("alice");
-
    let rid = profile.project("heartwood", "Radicle Heartwood Protocol & Stack");
-

-
    test(
-
        "examples/rad-init-existing-bare.md",
-
        environment.work(&profile),
-
        Some(&profile.home),
-
        [(
-
            "URL",
-
            git::url::File::new(profile.storage.path())
-
                .rid(rid)
-
                .to_string()
-
                .as_str(),
-
        )],
-
    )
-
    .unwrap();
-

-
    environment
-
        .tests(["jj-config", "jj-init-bare"], &profile)
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_jj_colocated_patch() {
-
    // We test whether `jj` is installed, and have this test succeed if it is not.
-
    // Programmatic skipping of tests is not supported as of 2024-08.
-
    if !program_reports_version("jj") {
-
        return;
-
    }
-

-
    Environment::alice(["rad-init", "jj-config", "jj-init-colocate", "rad-patch-jj"])
-
}
-

-
#[test]
-
fn rad_patch_diff() {
-
    Environment::alice(["rad-init", "rad-patch-diff"]);
-
}
-

-
#[test]
-
fn rad_patch_edit() {
-
    Environment::alice(["rad-init", "rad-patch-edit"]);
-
}
-

-
#[test]
-
fn rad_patch_checkout() {
-
    Environment::alice(["rad-init", "rad-patch-checkout"]);
-
}
-

-
#[test]
-
fn rad_patch_checkout_revision() {
-
    Environment::alice([
-
        "rad-init",
-
        "rad-patch-checkout",
-
        "rad-patch-checkout-revision",
-
    ]);
-
}
-

-
#[test]
-
fn rad_patch_checkout_force() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.handle.seed(acme, Scope::All).unwrap();
-
    alice.connect(&bob).converge([&bob]);
-

-
    bob.rad(
-
        "clone",
-
        &[acme.to_string().as_str()],
-
        environment.work(&bob),
-
    )
-
    .unwrap();
-

-
    formula(
-
        &environment.tempdir(),
-
        "examples/rad-patch-checkout-force.md",
-
    )
-
    .unwrap()
-
    .home(
-
        "alice",
-
        environment.work(&alice),
-
        [("RAD_HOME", alice.home.path().display())],
-
    )
-
    .home(
-
        "bob",
-
        environment.work(&bob),
-
        [("RAD_HOME", bob.home.path().display())],
-
    )
-
    .run()
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_patch_update() {
-
    Environment::alice(["rad-init", "rad-patch-update"]);
-
}
-

-
#[test]
-
#[cfg(not(target_os = "macos"))]
-
fn rad_patch_ahead_behind() {
-
    let mut environment = Environment::new();
-
    let profile = environment.profile("alice");
-

-
    environment.repository(&profile);
-

-
    std::fs::write(
-
        environment.work(&profile).join("CONTRIBUTORS"),
-
        "Alice Jones\n",
-
    )
-
    .unwrap();
-

-
    environment
-
        .tests(["rad-init", "rad-patch-ahead-behind"], &profile)
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_patch_change_base() {
-
    Environment::alice(["rad-init", "rad-patch-change-base"]);
-
}
-

-
#[test]
-
fn rad_patch_draft() {
-
    Environment::alice(["rad-init", "rad-patch-draft"]);
-
}
-

-
#[test]
-
fn rad_patch_via_push() {
-
    Environment::alice(["rad-init", "rad-patch-via-push"]);
-
}
-

-
#[test]
-
fn rad_patch_detached_head() {
-
    Environment::alice(["rad-init", "rad-patch-detached-head"]);
-
}
-

-
#[test]
-
fn rad_patch_merge_draft() {
-
    Environment::alice(["rad-init", "rad-patch-merge-draft"]);
-
}
-

-
#[test]
-
fn rad_patch_revert_merge() {
-
    Environment::alice(["rad-init", "rad-patch-revert-merge"]);
-
}
-

-
#[test]
-
#[cfg(not(target_os = "macos"))]
-
fn rad_review_by_hunk() {
-
    Environment::alice(["rad-init", "rad-review-by-hunk"]);
-
}
-

-
#[test]
-
fn rad_patch_delete() {
-
    let mut environment = Environment::new();
-
    let alice = environment.relay("alice");
-
    let bob = environment.relay("bob");
-
    let seed = environment.relay("seed");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let mut seed = seed.spawn();
-

-
    bob.handle.seed(acme, Scope::All).unwrap();
-
    seed.handle.seed(acme, Scope::All).unwrap();
-
    alice.connect(&bob).connect(&seed).converge([&bob, &seed]);
-
    bob.connect(&seed).converge([&seed]);
-
    bob.routes_to(&[(acme, seed.id)]);
-

-
    bob.rad(
-
        "clone",
-
        &[acme.to_string().as_str()],
-
        environment.work(&bob),
-
    )
-
    .unwrap();
-

-
    formula(&environment.tempdir(), "examples/rad-patch-delete.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .home(
-
            "seed",
-
            environment.work(&seed),
-
            [("RAD_HOME", seed.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_clean() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let eve = environment.node("eve");
-
    let working = environment.tempdir().join("working");
-

-
    // Setup a test project.
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-
    fixtures::repository(working.join("acme"));
-
    test(
-
        "examples/rad-init.md",
-
        working.join("acme"),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let mut eve = eve.spawn();
-
    alice.handle.seed(acme, Scope::All).unwrap();
-
    eve.handle.seed(acme, Scope::Followed).unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-
    eve.connect(&alice).converge([&alice]);
-

-
    eve.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
-

-
    bob.fork(acme, bob.home.path()).unwrap();
-
    bob.announce(acme, 1, bob.home.path()).unwrap();
-
    bob.has_remote_of(&acme, &alice.id);
-
    alice.has_remote_of(&acme, &bob.id);
-
    eve.has_remote_of(&acme, &alice.id);
-

-
    formula(&environment.tempdir(), "examples/rad-clean.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            working.join("acme"),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            working.join("bob"),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .home(
-
            "eve",
-
            working.join("eve"),
-
            [("RAD_HOME", eve.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_seed_and_follow() {
-
    Environment::alice(["rad-seed-and-follow"]);
-
}
-

-
#[test]
-
fn rad_seed_many() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let mut bob = environment.node("bob");
-
    // Bob creates two projects that Alice seeds in the test
-
    let _ = bob.project("heartwood", "Radicle Heartwood Protocol & Stack");
-
    let _ = bob.project("nixpkgs", "Home for Nix Packages");
-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    test(
-
        "examples/rad-seed-many.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_unseed() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let working = tempfile::tempdir().unwrap();
-

-
    // Setup a test project.
-
    alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-
    let alice = alice.spawn();
-

-
    test("examples/rad-unseed.md", working, Some(&alice.home), []).unwrap();
-
}
-

-
#[test]
-
fn rad_unseed_many() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-

-
    // Setup a test project.
-
    alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-
    alice.project("nixpkgs", "Home for Nix Packages");
-
    let alice = alice.spawn();
-

-
    test(
-
        "examples/rad-unseed-many.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_block() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node_with(Config {
-
        seeding_policy: DefaultSeedingPolicy::permissive(),
-
        ..Config::test(Alias::new("alice"))
-
    });
-
    let working = tempfile::tempdir().unwrap();
-

-
    test("examples/rad-block.md", working, Some(&alice.home), []).unwrap();
-
}
-

-
#[test]
-
fn rad_clone() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let working = environment.tempdir().join("working");
-

-
    // Setup a test project.
-
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
-
    alice.handle.seed(acme, Scope::Followed).unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    test("examples/rad-clone.md", working, Some(&bob.home), []).unwrap();
-
}
-

-
#[test]
-
fn rad_clone_bare() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let working = environment.tempdir().join("working");
-

-
    // Setup a test project.
-
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
-
    alice.handle.seed(acme, Scope::Followed).unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    test("examples/rad-clone-bare.md", working, Some(&bob.home), []).unwrap();
-
}
-

-
#[test]
-
fn rad_clone_directory() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let working = environment.tempdir().join("working");
-

-
    // Setup a test project.
-
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
-
    alice.handle.seed(acme, Scope::Followed).unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    test(
-
        "examples/rad-clone-directory.md",
-
        working,
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_clone_all() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let eve = environment.node("eve");
-

-
    // Setup a test project.
-
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let mut eve = eve.spawn();
-

-
    alice.handle.seed(acme, Scope::All).unwrap();
-
    bob.connect(&alice).converge([&alice]);
-
    eve.connect(&alice).converge([&alice]);
-

-
    // Fork and sync repo.
-
    bob.fork(acme, bob.home.path()).unwrap();
-
    bob.announce(acme, 2, bob.home.path()).unwrap();
-
    bob.has_remote_of(&acme, &alice.id);
-
    alice.has_remote_of(&acme, &bob.id);
-

-
    test(
-
        "examples/rad-clone-all.md",
-
        environment.work(&eve),
-
        Some(&eve.home),
-
        [],
-
    )
-
    .unwrap();
-
    eve.has_remote_of(&acme, &bob.id);
-
}
-

-
#[test]
-
fn rad_clone_partial_fail() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let mut eve = environment.node("eve");
-
    let carol = NodeId::from_str("z6MksFqXN3Yhqk8pTJdUGLwBTkRfQvwZXPqR2qMEhbS9wzpT").unwrap();
-

-
    // Setup a test project.
-
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    // Make Even think she knows about a seed called "carol" that has the repo.
-
    eve.db
-
        .addresses_mut()
-
        .insert(
-
            &carol,
-
            PROTOCOL_VERSION,
-
            node::Features::SEED,
-
            &Alias::new("carol"),
-
            0,
-
            &UserAgent::default(),
-
            LocalTime::now().into(),
-
            [node::KnownAddress::new(
-
                // Eve will fail to connect to this address.
-
                node::Address::from(net::SocketAddr::from(([0, 0, 0, 0], 19873))),
-
                node::address::Source::Imported,
-
            )],
-
        )
-
        .unwrap();
-
    eve.db
-
        .routing_mut()
-
        .add_inventory([&acme], carol, LocalTime::now().into())
-
        .unwrap();
-
    eve.config.peers = node::config::PeerConfig::Static;
-

-
    let mut eve = eve.spawn();
-

-
    alice.handle.seed(acme, Scope::All).unwrap();
-
    bob.handle.seed(acme, Scope::All).unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-
    eve.connect(&alice);
-
    eve.connect(&bob);
-
    eve.routes_to(&[(acme, carol), (acme, bob.id), (acme, alice.id)]);
-
    bob.storage.repository(acme).unwrap().remove().unwrap(); // Cause the fetch from Bob to fail.
-
    bob.storage.temporary_repository(acme).ok(); // Prevent repo from being re-fetched.
-

-
    test(
-
        "examples/rad-clone-partial-fail.md",
-
        environment.work(&eve),
-
        Some(&eve.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_clone_connect() {
-
    let mut environment = Environment::new();
-
    let working = environment.tempdir().join("working");
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let mut eve = environment.node("eve");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-
    let ua = UserAgent::default();
-
    let now = LocalTime::now().into();
-

-
    fixtures::repository(working.join("acme"));
-

-
    test(
-
        "examples/rad-init.md",
-
        working.join("acme"),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    // Let Eve know about Alice and Bob having the repo.
-
    eve.db
-
        .addresses_mut()
-
        .insert(
-
            &alice.id,
-
            PROTOCOL_VERSION,
-
            node::Features::SEED,
-
            &Alias::new("alice"),
-
            0,
-
            &ua,
-
            now,
-
            [node::KnownAddress::new(
-
                node::Address::from(alice.addr),
-
                node::address::Source::Imported,
-
            )],
-
        )
-
        .unwrap();
-
    eve.db
-
        .addresses_mut()
-
        .insert(
-
            &bob.id,
-
            PROTOCOL_VERSION,
-
            node::Features::SEED,
-
            &Alias::new("bob"),
-
            0,
-
            &ua,
-
            now,
-
            [node::KnownAddress::new(
-
                node::Address::from(bob.addr),
-
                node::address::Source::Imported,
-
            )],
-
        )
-
        .unwrap();
-
    eve.db
-
        .routing_mut()
-
        .add_inventory([&acme], alice.id, now)
-
        .unwrap();
-
    eve.db
-
        .routing_mut()
-
        .add_inventory([&acme], bob.id, now)
-
        .unwrap();
-
    eve.config.peers = node::config::PeerConfig::Static;
-

-
    let eve = eve.spawn();
-

-
    alice.handle.seed(acme, Scope::Followed).unwrap();
-
    bob.handle.seed(acme, Scope::Followed).unwrap();
-
    alice.connect(&bob);
-
    bob.routes_to(&[(acme, alice.id)]);
-
    eve.routes_to(&[(acme, alice.id), (acme, bob.id)]);
-
    alice.routes_to(&[(acme, alice.id), (acme, bob.id)]);
-

-
    test(
-
        "examples/rad-clone-connect.md",
-
        working.join("acme"),
-
        Some(&eve.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_sync_without_node() {
-
    let mut environment = Environment::new();
-
    let alice = environment.seed("alice");
-
    let bob = environment.seed("bob");
-
    let mut eve = environment.seed("eve");
-

-
    let rid = RepoId::from_urn("rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5").unwrap();
-
    eve.policies.seed(&rid, Scope::All).unwrap();
-

-
    formula(&environment.tempdir(), "examples/rad-sync-without-node.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            alice.home.path(),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            bob.home.path(),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .home(
-
            "eve",
-
            eve.home.path(),
-
            [("RAD_HOME", eve.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_self() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node_with(Config {
-
        external_addresses: vec!["seed.alice.acme:8776".parse().unwrap()],
-
        ..Config::test(Alias::new("alice"))
-
    });
-
    let working = environment.tempdir().join("working");
-

-
    test("examples/rad-self.md", working, Some(&alice.home), []).unwrap();
-
}
-

-
#[test]
-
fn rad_clone_unknown() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let working = environment.tempdir().join("working");
-

-
    let alice = alice.spawn();
-

-
    test(
-
        "examples/rad-clone-unknown.md",
-
        working,
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_init_sync_not_connected() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let working = tempfile::tempdir().unwrap();
-
    let alice = alice.spawn();
-

-
    fixtures::repository(working.path().join("alice"));
-

-
    test(
-
        "examples/rad-init-sync-not-connected.md",
-
        working.path().join("alice"),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_init_sync_preferred() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment
-
        .node_with(Config {
-
            seeding_policy: DefaultSeedingPolicy::permissive(),
-
            ..Config::test(Alias::new("alice"))
-
        })
-
        .spawn();
-

-
    let bob = environment.profile_with(profile::Config {
-
        preferred_seeds: vec![alice.address()],
-
        ..environment.config("bob")
-
    });
-
    let mut bob = Node::new(bob).spawn();
-

-
    bob.connect(&alice);
-
    alice.handle.follow(bob.id, None).unwrap();
-

-
    environment.repository(&bob);
-

-
    // Bob initializes a repo after her node has started, and after bob has connected to it.
-
    test(
-
        "examples/rad-init-sync-preferred.md",
-
        environment.work(&bob),
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_init_sync_timeout() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment
-
        .node_with(Config {
-
            seeding_policy: DefaultSeedingPolicy::Block,
-
            ..Config::test(Alias::new("alice"))
-
        })
-
        .spawn();
-

-
    let bob = environment.profile_with(profile::Config {
-
        preferred_seeds: vec![alice.address()],
-
        ..environment.config("bob")
-
    });
-
    let mut bob = Node::new(bob).spawn();
-

-
    bob.connect(&alice);
-
    alice.handle.follow(bob.id, None).unwrap();
-

-
    environment.repository(&bob);
-

-
    // Bob initializes a repo after her node has started, and after bob has connected to it.
-
    test(
-
        "examples/rad-init-sync-timeout.md",
-
        environment.work(&bob),
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_init_sync_and_clone() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice);
-

-
    environment.repository(&alice);
-

-
    // Alice initializes a repo after her node has started, and after bob has connected to it.
-
    test(
-
        "examples/rad-init-sync.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    // Wait for bob to get any updates to the routing table.
-
    bob.converge([&alice]);
-

-
    test(
-
        "examples/rad-clone.md",
-
        environment.work(&bob),
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_fetch() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    let mut alice = alice.spawn();
-
    let bob = bob.spawn();
-

-
    alice.connect(&bob);
-
    environment.repository(&alice);
-

-
    // Alice initializes a repo after her node has started, and after bob has connected to it.
-
    environment.test("rad-init-sync", &alice).unwrap();
-

-
    // Wait for bob to get any updates to the routing table.
-
    bob.converge([&alice]);
-

-
    environment.test("rad-fetch", &bob).unwrap();
-
}
-

-
#[test]
-
fn rad_fork() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    let mut alice = alice.spawn();
-
    let bob = bob.spawn();
-

-
    alice.connect(&bob);
-
    environment.repository(&alice);
-

-
    // Alice initializes a repo after her node has started, and after bob has connected to it.
-
    environment.test("rad-init-sync", &alice).unwrap();
-

-
    // Wait for bob to get any updates to the routing table.
-
    bob.converge([&alice]);
-

-
    environment.tests(["rad-fetch", "rad-fork"], &bob).unwrap();
-
}
-

-
#[cfg(unix)]
-
#[test]
-
fn rad_diff() {
-
    if std::env::consts::OS == "macos" {
-
        // macOS's `sed` requires an argument for `-i`, which we don't provide
-
        // in the example. Providing it makes the test fail on Linux.
-
        // Since this command is deprecated anyway, we just skip macOS.
-
        return;
-
    }
-

-
    let tmp = tempfile::tempdir().unwrap();
-

-
    fixtures::repository(&tmp);
-

-
    test("examples/rad-diff.md", tmp, None, []).unwrap();
-
}
-

-
#[test]
-
// User tries to clone; no seeds are available, but user has the repo locally.
-
fn test_clone_without_seeds() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let working = environment.tempdir().join("working");
-
    let rid = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
-
    let mut alice = alice.spawn();
-
    let seeds = alice.handle.seeds_for(rid, [alice.id]).unwrap();
-
    let connected = seeds.connected().collect::<Vec<_>>();
-

-
    assert!(connected.is_empty());
-

-
    alice
-
        .rad("clone", &[rid.to_string().as_str()], working.as_path())
-
        .unwrap();
-

-
    alice
-
        .rad("inspect", &[], working.join("heartwood").as_path())
-
        .unwrap();
-
}
-

-
#[test]
-
fn test_cob_replication() {
-
    let mut environment = Environment::new();
-
    let working = tempfile::tempdir().unwrap();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    let rid = alice.project("heartwood", "");
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let events = alice.handle.events();
-

-
    alice.handle.follow(bob.id, None).unwrap();
-
    alice.connect(&bob);
-

-
    bob.routes_to(&[(rid, alice.id)]);
-
    bob.fork(rid, working.path()).unwrap();
-

-
    // Wait for Alice to fetch the clone refs.
-
    events
-
        .wait(
-
            |e| {
-
                matches!(
-
                    e,
-
                    Event::RefsFetched { updated, .. }
-
                    if updated.iter().any(|u| matches!(u, RefUpdate::Created { .. }))
-
                )
-
                .then_some(())
-
            },
-
            time::Duration::from_secs(6),
-
        )
-
        .unwrap();
-

-
    let bob_repo = bob.storage.repository(rid).unwrap();
-
    let mut bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
-
    let mut bob_cache = radicle::cob::cache::InMemory::default();
-
    let issue = bob_issues
-
        .create(
-
            cob::Title::new("Something's fishy").unwrap(),
-
            "I don't know what it is",
-
            &[],
-
            &[],
-
            [],
-
            &mut bob_cache,
-
            &bob.signer,
-
        )
-
        .unwrap();
-
    log::debug!(target: "test", "Issue {} created", issue.id());
-

-
    // Make sure that Bob's issue refs announcement has a different timestamp than his fork's
-
    // announcement, otherwise Alice will consider it stale.
-
    thread::sleep(time::Duration::from_millis(3));
-

-
    bob.handle.announce_refs_for(rid, [bob.id]).unwrap();
-

-
    // Wait for Alice to fetch the issue refs.
-
    events
-
        .iter()
-
        .find(|e| matches!(e, Event::RefsFetched { .. }))
-
        .unwrap();
-

-
    let alice_repo = alice.storage.repository(rid).unwrap();
-
    let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
-
    let alice_issue = alice_issues.get(issue.id()).unwrap().unwrap();
-

-
    assert_eq!(alice_issue.title(), "Something's fishy");
-
}
-

-
#[test]
-
fn test_cob_deletion() {
-
    let mut environment = Environment::new();
-
    let working = tempfile::tempdir().unwrap();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    let rid = alice.project("heartwood", "");
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    alice.handle.seed(rid, Scope::All).unwrap();
-
    bob.handle.seed(rid, Scope::All).unwrap();
-
    alice.connect(&bob);
-
    bob.routes_to(&[(rid, alice.id)]);
-

-
    let alice_repo = alice.storage.repository(rid).unwrap();
-
    let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap();
-
    let issue = alice_issues
-
        .create(
-
            cob::Title::new("Something's fishy").unwrap(),
-
            "I don't know what it is",
-
            &[],
-
            &[],
-
            [],
-
            &alice.signer,
-
        )
-
        .unwrap();
-
    let issue_id = issue.id();
-
    log::debug!(target: "test", "Issue {issue_id} created");
-

-
    bob.rad("clone", &[rid.to_string().as_str()], working.path())
-
        .unwrap();
-

-
    let bob_repo = bob.storage.repository(rid).unwrap();
-
    let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
-
    assert!(bob_issues.get(issue_id).unwrap().is_some());
-

-
    let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap();
-
    alice_issues.remove(issue_id, &alice.signer).unwrap();
-

-
    log::debug!(target: "test", "Removing issue..");
-

-
    radicle::assert_matches!(
-
        bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(),
-
        radicle::node::FetchResult::Success { .. }
-
    );
-
    let bob_repo = bob.storage.repository(rid).unwrap();
-
    let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
-
    assert!(bob_issues.get(issue_id).unwrap().is_none());
-
}
-

-
#[test]
-
fn rad_sync() {
-
    let mut environment = Environment::new();
-
    let working = environment.tempdir().join("working");
-
    let alice = environment.seed("alice");
-
    let bob = environment.seed("bob");
-
    let eve = environment.seed("eve");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    fixtures::repository(working.join("acme"));
-

-
    test(
-
        "examples/rad-init.md",
-
        working.join("acme"),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let mut eve = eve.spawn();
-

-
    bob.handle.seed(acme, Scope::All).unwrap();
-
    eve.handle.seed(acme, Scope::All).unwrap();
-

-
    alice.connect(&bob);
-
    eve.connect(&alice);
-

-
    bob.routes_to(&[(acme, alice.id)]);
-
    eve.routes_to(&[(acme, alice.id)]);
-
    alice.routes_to(&[(acme, alice.id), (acme, eve.id), (acme, bob.id)]);
-
    alice.is_synced_with(&acme, &eve.id);
-
    alice.is_synced_with(&acme, &bob.id);
-

-
    test(
-
        "examples/rad-sync.md",
-
        working.join("acme"),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
//
-
//     alice -- seed -- bob
-
//
-
fn test_replication_via_seed() {
-
    let mut environment = Environment::new();
-
    let alice = environment.relay("alice");
-
    let bob = environment.relay("bob");
-
    let seed = environment.node_with(Config {
-
        seeding_policy: DefaultSeedingPolicy::permissive(),
-
        ..config::relay("seed")
-
    });
-
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let seed = seed.spawn();
-

-
    alice.connect(&seed);
-
    bob.connect(&seed);
-

-
    // Enough time for the next inventory from Seed to not be considered stale by Bob.
-
    thread::sleep(time::Duration::from_millis(3));
-

-
    alice.routes_to(&[]);
-
    seed.routes_to(&[]);
-
    bob.routes_to(&[]);
-

-
    // Initialize a repo as Alice.
-
    environment.repository(&alice);
-
    alice
-
        .rad(
-
            "init",
-
            &[
-
                "--name",
-
                "heartwood",
-
                "--description",
-
                "Radicle Heartwood Protocol & Stack",
-
                "--default-branch",
-
                "master",
-
                "--public",
-
            ],
-
            environment.work(&alice),
-
        )
-
        .unwrap();
-

-
    alice
-
        .rad("follow", &[&bob.id.to_human()], environment.work(&alice))
-
        .unwrap();
-

-
    alice.routes_to(&[(rid, alice.id), (rid, seed.id)]);
-
    seed.routes_to(&[(rid, alice.id), (rid, seed.id)]);
-
    bob.routes_to(&[(rid, alice.id), (rid, seed.id)]);
-

-
    let seed_events = seed.handle.events();
-
    let alice_events = alice.handle.events();
-

-
    bob.fork(rid, environment.work(&bob)).unwrap();
-

-
    alice.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
-
    seed.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
-
    bob.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
-

-
    seed_events.iter().any(|e| {
-
        matches!(
-
            e, Event::RefsFetched { updated, remote, .. }
-
            if remote == bob.id && updated.iter().any(|u| u.is_created())
-
        )
-
    });
-
    alice_events.iter().any(|e| {
-
        matches!(
-
            e, Event::RefsFetched { updated, remote, .. }
-
            if remote == seed.id && updated.iter().any(|u| u.is_created())
-
        )
-
    });
-

-
    seed.storage
-
        .repository(rid)
-
        .unwrap()
-
        .remote(&bob.id)
-
        .unwrap();
-

-
    // Seed should send Bob's ref announcement to Alice, after the fetch.
-
    alice
-
        .storage
-
        .repository(rid)
-
        .unwrap()
-
        .remote(&bob.id)
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_remote() {
-
    let mut environment = Environment::new();
-
    let alice = environment.relay("alice");
-
    let bob = environment.relay("bob");
-
    let eve = environment.relay("eve");
-
    let home = alice.home.clone();
-
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-
    // Setup a test repository.
-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let mut alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let mut eve = eve.spawn();
-
    alice
-
        .handle
-
        .follow(bob.id, Some(Alias::new("bob")))
-
        .unwrap();
-
    alice
-
        .handle
-
        .follow(eve.id, Some(Alias::new("eve")))
-
        .unwrap();
-

-
    bob.connect(&alice);
-
    bob.routes_to(&[(rid, alice.id)]);
-
    bob.fork(rid, bob.home.path()).unwrap();
-
    alice.has_remote_of(&rid, &bob.id);
-

-
    eve.connect(&alice);
-
    eve.routes_to(&[(rid, alice.id)]);
-
    eve.fork(rid, eve.home.path()).unwrap();
-
    alice.has_remote_of(&rid, &eve.id);
-

-
    test(
-
        "examples/rad-remote.md",
-
        environment.work(&alice),
-
        Some(&home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_merge_via_push() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-

-
    environment.repository(&alice);
-

-
    environment.test("rad-init", &alice).unwrap();
-

-
    let alice = alice.spawn();
-

-
    environment.test("rad-merge-via-push", &alice).unwrap();
-
}
-

-
#[test]
-
fn rad_merge_after_update() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-

-
    environment.repository(&alice);
-

-
    environment.test("rad-init", &alice).unwrap();
-

-
    let alice = alice.spawn();
-

-
    environment.test("rad-merge-after-update", &alice).unwrap();
-
}
-

-
#[test]
-
fn rad_merge_no_ff() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-

-
    environment.repository(&alice);
-

-
    environment
-
        .tests(["rad-init", "rad-merge-no-ff"], &alice)
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_patch_pull_update() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    environment.repository(&alice);
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    formula(&environment.tempdir(), "examples/rad-patch-pull-update.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            bob.home.path(),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_patch_open_explore() {
-
    let mut environment = Environment::new();
-
    let seed = environment
-
        .node_with(Config {
-
            seeding_policy: DefaultSeedingPolicy::permissive(),
-
            ..config::seed("seed")
-
        })
-
        .spawn();
-

-
    let bob = environment.profile_with(profile::Config {
-
        preferred_seeds: vec![seed.address()],
-
        ..environment.config("bob")
-
    });
-
    let mut bob = Node::new(bob).spawn();
-
    let working = environment.tempdir().join("working");
-

-
    fixtures::repository(&working);
-

-
    bob.connect(&seed);
-
    bob.init("heartwood", "", &working).unwrap();
-
    bob.converge([&seed]);
-

-
    test(
-
        "examples/rad-patch-open-explore.md",
-
        &working,
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_init_private() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-

-
    environment.repository(&alice);
-

-
    environment.test("rad-init-private", &alice).unwrap();
-
}
-

-
#[test]
-
fn rad_init_private_no_seed() {
-
    Environment::alice(["rad-init-private-no-seed"]);
-
}
-

-
#[test]
-
fn rad_init_private_seed() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    environment.repository(&alice);
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    environment.test("rad-init-private", &alice).unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    formula(&environment.tempdir(), "examples/rad-init-private-seed.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            bob.home.path(),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_init_private_clone() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    environment.repository(&alice);
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    environment.test("rad-init-private", &alice).unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    formula(&environment.tempdir(), "examples/rad-init-private-clone.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            bob.home.path(),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_init_private_clone_seed() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    environment.repository(&alice);
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    test(
-
        "examples/rad-init-private.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    formula(
-
        &environment.tempdir(),
-
        "examples/rad-init-private-clone-seed.md",
-
    )
-
    .unwrap()
-
    .home(
-
        "alice",
-
        environment.work(&alice),
-
        [("RAD_HOME", alice.home.path().display())],
-
    )
-
    .home(
-
        "bob",
-
        bob.home.path(),
-
        [("RAD_HOME", bob.home.path().display())],
-
    )
-
    .run()
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_publish() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-

-
    environment.repository(&alice);
-

-
    environment
-
        .tests(["rad-init-private", "rad-publish"], &alice)
-
        .unwrap();
-
}
-

-
#[test]
-
fn framework_home() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    formula(&environment.tempdir(), "examples/framework/home.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            alice.home.path(),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            bob.home.path(),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn git_push_diverge() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.fork(acme, environment.work(&bob)).unwrap();
-
    alice.has_remote_of(&acme, &bob.id);
-

-
    formula(&environment.tempdir(), "examples/git/git-push-diverge.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob).join("heartwood"),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn git_push_converge() {
-
    use std::fs;
-

-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let eve = environment.node("eve");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-
    let mut eve = eve.spawn();
-

-
    bob.connect(&alice).connect(&eve).converge([&alice]);
-
    eve.connect(&alice).converge([&alice]);
-
    bob.fork(acme, environment.work(&bob)).unwrap();
-
    eve.fork(acme, environment.work(&eve)).unwrap();
-
    alice.has_remote_of(&acme, &bob.id);
-
    alice.has_remote_of(&acme, &eve.id);
-

-
    fs::write(
-
        environment.work(&bob).join("heartwood").join("README"),
-
        "Hello\n",
-
    )
-
    .unwrap();
-
    fs::write(
-
        environment.work(&eve).join("heartwood").join("README"),
-
        "Hello, world!\n",
-
    )
-
    .unwrap();
-

-
    formula(&environment.tempdir(), "examples/git/git-push-converge.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob).join("heartwood"),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .home(
-
            "eve",
-
            environment.work(&eve).join("heartwood"),
-
            [("RAD_HOME", eve.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn git_push_amend() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.fork(acme, environment.work(&bob)).unwrap();
-
    alice.has_remote_of(&acme, &bob.id);
-

-
    formula(&environment.tempdir(), "examples/git/git-push-amend.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob).join("heartwood"),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn git_push_force_with_lease() {
-
    Environment::alice(["rad-init", "git/git-push-force-with-lease"]);
-
}
-

-
#[test]
-
fn git_push_rollback() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.fork(acme, environment.work(&bob)).unwrap();
-
    alice.has_remote_of(&acme, &bob.id);
-

-
    formula(&environment.tempdir(), "examples/git/git-push-rollback.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob).join("heartwood"),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_push_and_pull_patches() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.fork(acme, environment.work(&bob)).unwrap();
-
    alice.has_remote_of(&acme, &bob.id);
-

-
    formula(
-
        &environment.tempdir(),
-
        "examples/rad-push-and-pull-patches.md",
-
    )
-
    .unwrap()
-
    .home(
-
        "alice",
-
        environment.work(&alice),
-
        [("RAD_HOME", alice.home.path().display())],
-
    )
-
    .home(
-
        "bob",
-
        environment.work(&bob).join("heartwood"),
-
        [("RAD_HOME", bob.home.path().display())],
-
    )
-
    .run()
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_patch_fetch_1() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let (repo, _) = environment.repository(&alice);
-
    let rid = alice.project_from("heartwood", "Radicle Heartwood Protocol & Stack", &repo);
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.clone(rid, environment.work(&bob)).unwrap();
-

-
    formula(&environment.tempdir(), "examples/rad-patch-fetch-1.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob).join("heartwood"),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_watch() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let (repo, _) = environment.repository(&alice);
-
    let rid = alice.project_from("heartwood", "Radicle Heartwood Protocol & Stack", &repo);
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.clone(rid, environment.work(&bob)).unwrap();
-

-
    formula(&environment.tempdir(), "examples/rad-watch.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob).join("heartwood"),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_inbox() {
-
    let mut environment = Environment::new();
-
    let mut alice = environment.node("alice");
-
    let bob = environment.node("bob");
-
    let (repo1, _) = fixtures::repository(environment.work(&alice).join("heartwood"));
-
    let (repo2, _) = fixtures::repository(environment.work(&alice).join("radicle-git"));
-
    let rid1 = alice.project_from("heartwood", "Radicle Heartwood Protocol & Stack", &repo1);
-
    let rid2 = alice.project_from("radicle-git", "Radicle Git", &repo2);
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.clone(rid1, environment.work(&bob)).unwrap();
-
    bob.clone(rid2, environment.work(&bob)).unwrap();
-

-
    formula(&environment.tempdir(), "examples/rad-inbox.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn rad_patch_fetch_2() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-

-
    environment.repository(&alice);
-

-
    environment
-
        .tests(["rad-init", "rad-patch-fetch-2"], &alice)
-
        .unwrap();
-
}
-

-
#[test]
-
fn git_push_and_fetch() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    environment.repository(&alice);
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    environment.test("rad-clone", &bob).unwrap();
-
    environment.test("git/git-push", &alice).unwrap();
-
    environment.test("git/git-fetch", &bob).unwrap();
-
    environment.test("git/git-push-delete", &alice).unwrap();
-
}
-

-
#[test]
-
fn git_tag() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    environment.repository(&alice);
-

-
    environment.test("rad-init", &alice).unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    test(
-
        "examples/rad-clone.md",
-
        environment.work(&bob),
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    formula(&environment.tempdir(), "examples/git/git-tag.md")
-
        .unwrap()
-
        .home(
-
            "alice",
-
            environment.work(&alice),
-
            [("RAD_HOME", alice.home.path().display())],
-
        )
-
        .home(
-
            "bob",
-
            environment.work(&bob),
-
            [("RAD_HOME", bob.home.path().display())],
-
        )
-
        .run()
-
        .unwrap();
-
}
-

-
#[test]
-
fn git_push_canonical_lightweight_tags() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    fixtures::repository(environment.work(&alice));
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.clone(rid, environment.work(&bob)).unwrap();
-
    formula(
-
        &environment.tempdir(),
-
        "examples/git/git-push-canonical-lightweight-tags.md",
-
    )
-
    .unwrap()
-
    .home(
-
        "alice",
-
        environment.work(&alice),
-
        [("RAD_HOME", alice.home.path().display())],
-
    )
-
    .home(
-
        "bob",
-
        environment.work(&bob),
-
        [("RAD_HOME", bob.home.path().display())],
-
    )
-
    .run()
-
    .unwrap();
-
}
-

-
#[test]
-
fn git_push_canonical_annotated_tags() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
-

-
    fixtures::repository(environment.work(&alice));
-

-
    test(
-
        "examples/rad-init.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-
    bob.clone(rid, environment.work(&bob)).unwrap();
-
    formula(
-
        &environment.tempdir(),
-
        "examples/git/git-push-canonical-annotated-tags.md",
-
    )
-
    .unwrap()
-
    .home(
-
        "alice",
-
        environment.work(&alice),
-
        [("RAD_HOME", alice.home.path().display())],
-
    )
-
    .home(
-
        "bob",
-
        environment.work(&bob),
-
        [("RAD_HOME", bob.home.path().display())],
-
    )
-
    .run()
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_workflow() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node("alice");
-
    let bob = environment.node("bob");
-

-
    environment.repository(&alice);
-

-
    environment.test("workflow/1-new-project", &alice).unwrap();
-

-
    let alice = alice.spawn();
-
    let mut bob = bob.spawn();
-

-
    bob.connect(&alice).converge([&alice]);
-

-
    environment.test("workflow/2-cloning", &bob).unwrap();
-

-
    test(
-
        "examples/workflow/3-issues.md",
-
        environment.work(&bob).join("heartwood"),
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    test(
-
        "examples/workflow/4-patching-contributor.md",
-
        environment.work(&bob).join("heartwood"),
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    test(
-
        "examples/workflow/5-patching-maintainer.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-

-
    test(
-
        "examples/workflow/6-pulling-contributor.md",
-
        environment.work(&bob).join("heartwood"),
-
        Some(&bob.home),
-
        [],
-
    )
-
    .unwrap();
-
}
-

-
#[test]
-
fn rad_seed_policy_allow_no_scope() {
-
    let mut environment = Environment::new();
-
    let alice = environment.node_with(Config {
-
        seeding_policy: DefaultSeedingPolicy::Allow {
-
            scope: node::config::Scope::implicit(),
-
        },
-
        ..Config::test(Alias::new("alice"))
-
    });
-

-
    let alice = alice.spawn();
-

-
    test(
-
        "examples/rad-seed-policy-allow-no-scope.md",
-
        environment.work(&alice),
-
        Some(&alice.home),
-
        [],
-
    )
-
    .unwrap();
-
}
added crates/radicle-cli/tests/commands/cob.rs
@@ -0,0 +1,230 @@
+
use std::path::Path;
+

+
use crate::util::environment::Environment;
+
use crate::{program_reports_version, test};
+
use radicle::node::Handle;
+
use radicle::test::fixtures;
+

+
#[test]
+
fn rad_cob_update() {
+
    Environment::alice(["rad-init", "rad-cob-log"]);
+
}
+

+
#[test]
+
fn rad_cob_update_identity() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+
    let working = environment.tempdir().join("working");
+
    let home = &profile.home;
+

+
    let base = Path::new(env!("CARGO_MANIFEST_DIR"));
+

+
    std::fs::create_dir_all(base).unwrap();
+
    std::fs::create_dir_all(working.clone()).unwrap();
+

+
    // Setup a test repository.
+
    fixtures::repository(&working);
+

+
    test("examples/rad-init.md", &working, Some(home), []).unwrap();
+
    test(
+
        "examples/rad-cob-update-identity.md",
+
        &working,
+
        Some(home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_cob_multiset() {
+
    // `rad-cob-multiset` is a `jq` script, which requires `jq` to be installed.
+
    // We test whether `jq` is installed, and have this test succeed if it is not.
+
    // Programmatic skipping of tests is not supported as of 2024-08.
+
    if !program_reports_version("jq") {
+
        return;
+
    }
+

+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+
    let home = &profile.home;
+
    let working = environment.tempdir().join("working");
+

+
    let base = Path::new(env!("CARGO_MANIFEST_DIR"));
+
    std::fs::create_dir_all(base).unwrap();
+
    std::fs::create_dir_all(working.clone()).unwrap();
+

+
    // Copy over the script that implements the multiset COB.
+
    std::fs::copy(
+
        base.join("examples").join("rad-cob-multiset"),
+
        working.join("rad-cob-multiset"),
+
    )
+
    .unwrap();
+

+
    // Setup a test repository.
+
    fixtures::repository(&working);
+

+
    test("examples/rad-init.md", &working, Some(home), []).unwrap();
+
    test("examples/rad-cob-multiset.md", &working, Some(home), []).unwrap();
+
}
+

+
#[test]
+
fn rad_cob_log() {
+
    Environment::alice(["rad-init", "rad-cob-log"]);
+
}
+

+
#[test]
+
fn rad_cob_show() {
+
    Environment::alice(["rad-init", "rad-cob-show"]);
+
}
+

+
#[test]
+
fn rad_cob_migrate() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+
    let home = &profile.home;
+

+
    home.cobs_db_mut()
+
        .unwrap()
+
        .raw_query(|conn| conn.execute("PRAGMA user_version = 0"))
+
        .unwrap();
+

+
    environment.repository(&profile);
+

+
    environment
+
        .tests(["rad-init", "rad-cob-migrate"], &profile)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_cob_operations() {
+
    Environment::alice(["rad-init", "rad-cob-operations"]);
+
}
+

+
#[test]
+
fn test_cob_replication() {
+
    let mut environment = Environment::new();
+
    let working = tempfile::tempdir().unwrap();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    let rid = alice.project("heartwood", "");
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let events = alice.handle.events();
+

+
    alice.handle.follow(bob.id, None).unwrap();
+
    alice.connect(&bob);
+

+
    bob.routes_to(&[(rid, alice.id)]);
+
    bob.fork(rid, working.path()).unwrap();
+

+
    // Wait for Alice to fetch the clone refs.
+
    events
+
        .wait(
+
            |e| {
+
                matches!(
+
                    e,
+
                    radicle::node::Event::RefsFetched { updated, .. }
+
                    if updated.iter().any(|u| matches!(u, radicle::storage::RefUpdate::Created { .. }))
+
                )
+
                .then_some(())
+
            },
+
            std::time::Duration::from_secs(6),
+
        )
+
        .unwrap();
+

+
    let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap();
+
    let mut bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
+
    let mut bob_cache = radicle::cob::cache::InMemory::default();
+
    let issue = bob_issues
+
        .create(
+
            radicle::cob::Title::new("Something's fishy").unwrap(),
+
            "I don't know what it is",
+
            &[],
+
            &[],
+
            [],
+
            &mut bob_cache,
+
            &bob.signer,
+
        )
+
        .unwrap();
+
    log::debug!(target: "test", "Issue {} created", issue.id());
+

+
    // Make sure that Bob's issue refs announcement has a different timestamp than his fork's
+
    // announcement, otherwise Alice will consider it stale.
+
    std::thread::sleep(std::time::Duration::from_millis(3));
+

+
    bob.handle.announce_refs_for(rid, [bob.id]).unwrap();
+

+
    // Wait for Alice to fetch the issue refs.
+
    events
+
        .iter()
+
        .find(|e| matches!(e, radicle::node::Event::RefsFetched { .. }))
+
        .unwrap();
+

+
    let alice_repo = radicle::storage::ReadStorage::repository(&alice.storage, rid).unwrap();
+
    let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
+
    let alice_issue = alice_issues.get(issue.id()).unwrap().unwrap();
+

+
    assert_eq!(alice_issue.title(), "Something's fishy");
+
}
+

+
#[test]
+
fn test_cob_deletion() {
+
    let mut environment = Environment::new();
+
    let working = tempfile::tempdir().unwrap();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    let rid = alice.project("heartwood", "");
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    alice
+
        .handle
+
        .seed(rid, radicle::node::policy::Scope::All)
+
        .unwrap();
+
    bob.handle
+
        .seed(rid, radicle::node::policy::Scope::All)
+
        .unwrap();
+
    alice.connect(&bob);
+
    bob.routes_to(&[(rid, alice.id)]);
+

+
    let alice_repo = radicle::storage::ReadStorage::repository(&alice.storage, rid).unwrap();
+
    let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap();
+
    let issue = alice_issues
+
        .create(
+
            radicle::cob::Title::new("Something's fishy").unwrap(),
+
            "I don't know what it is",
+
            &[],
+
            &[],
+
            [],
+
            &alice.signer,
+
        )
+
        .unwrap();
+
    let issue_id = issue.id();
+
    log::debug!(target: "test", "Issue {issue_id} created");
+

+
    bob.rad("clone", &[rid.to_string().as_str()], working.path())
+
        .unwrap();
+

+
    let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap();
+
    let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
+
    assert!(bob_issues.get(issue_id).unwrap().is_some());
+

+
    let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap();
+
    alice_issues.remove(issue_id, &alice.signer).unwrap();
+

+
    log::debug!(target: "test", "Removing issue..");
+

+
    radicle::assert_matches!(
+
        bob.handle
+
            .fetch(rid, alice.id, radicle::node::DEFAULT_TIMEOUT)
+
            .unwrap(),
+
        radicle::node::FetchResult::Success { .. }
+
    );
+
    let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap();
+
    let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
+
    assert!(bob_issues.get(issue_id).unwrap().is_none());
+
}
added crates/radicle-cli/tests/commands/git.rs
@@ -0,0 +1,344 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+
use radicle::prelude::RepoId;
+
use radicle::test::fixtures;
+
use std::str::FromStr;
+

+
#[test]
+
fn git_push_diverge() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.fork(acme, environment.work(&bob)).unwrap();
+
    alice.has_remote_of(&acme, &bob.id);
+

+
    formula(&environment.tempdir(), "examples/git/git-push-diverge.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob).join("heartwood"),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn git_push_converge() {
+
    use std::fs;
+

+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let eve = environment.node("eve");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+

+
    bob.connect(&alice).connect(&eve).converge([&alice]);
+
    eve.connect(&alice).converge([&alice]);
+
    bob.fork(acme, environment.work(&bob)).unwrap();
+
    eve.fork(acme, environment.work(&eve)).unwrap();
+
    alice.has_remote_of(&acme, &bob.id);
+
    alice.has_remote_of(&acme, &eve.id);
+

+
    fs::write(
+
        environment.work(&bob).join("heartwood").join("README"),
+
        "Hello\n",
+
    )
+
    .unwrap();
+
    fs::write(
+
        environment.work(&eve).join("heartwood").join("README"),
+
        "Hello, world!\n",
+
    )
+
    .unwrap();
+

+
    formula(&environment.tempdir(), "examples/git/git-push-converge.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob).join("heartwood"),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .home(
+
            "eve",
+
            environment.work(&eve).join("heartwood"),
+
            [("RAD_HOME", eve.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn git_push_amend() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.fork(acme, environment.work(&bob)).unwrap();
+
    alice.has_remote_of(&acme, &bob.id);
+

+
    formula(&environment.tempdir(), "examples/git/git-push-amend.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob).join("heartwood"),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn git_push_force_with_lease() {
+
    Environment::alice(["rad-init", "git/git-push-force-with-lease"]);
+
}
+

+
#[test]
+
fn git_push_rollback() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.fork(acme, environment.work(&bob)).unwrap();
+
    alice.has_remote_of(&acme, &bob.id);
+

+
    formula(&environment.tempdir(), "examples/git/git-push-rollback.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob).join("heartwood"),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn git_push_and_fetch() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    environment.test("rad-clone", &bob).unwrap();
+
    environment.test("git/git-push", &alice).unwrap();
+
    environment.test("git/git-fetch", &bob).unwrap();
+
    environment.test("git/git-push-delete", &alice).unwrap();
+
}
+

+
#[test]
+
fn git_tag() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    environment.repository(&alice);
+

+
    environment.test("rad-init", &alice).unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    test(
+
        "examples/rad-clone.md",
+
        environment.work(&bob),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    formula(&environment.tempdir(), "examples/git/git-tag.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn git_push_canonical_lightweight_tags() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    fixtures::repository(environment.work(&alice));
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.clone(rid, environment.work(&bob)).unwrap();
+
    formula(
+
        &environment.tempdir(),
+
        "examples/git/git-push-canonical-lightweight-tags.md",
+
    )
+
    .unwrap()
+
    .home(
+
        "alice",
+
        environment.work(&alice),
+
        [("RAD_HOME", alice.home.path().display())],
+
    )
+
    .home(
+
        "bob",
+
        environment.work(&bob),
+
        [("RAD_HOME", bob.home.path().display())],
+
    )
+
    .run()
+
    .unwrap();
+
}
+

+
#[test]
+
fn git_push_canonical_annotated_tags() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    fixtures::repository(environment.work(&alice));
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.clone(rid, environment.work(&bob)).unwrap();
+
    formula(
+
        &environment.tempdir(),
+
        "examples/git/git-push-canonical-annotated-tags.md",
+
    )
+
    .unwrap()
+
    .home(
+
        "alice",
+
        environment.work(&alice),
+
        [("RAD_HOME", alice.home.path().display())],
+
    )
+
    .home(
+
        "bob",
+
        environment.work(&bob),
+
        [("RAD_HOME", bob.home.path().display())],
+
    )
+
    .run()
+
    .unwrap();
+
}
added crates/radicle-cli/tests/commands/id.rs
@@ -0,0 +1,433 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+
use radicle::node::policy::Scope;
+
use radicle::node::Event;
+
use radicle::node::DEFAULT_TIMEOUT;
+
use radicle::node::{Alias, Handle as _};
+
use radicle::prelude::RepoId;
+
use radicle::storage::ReadStorage as _;
+
use std::str::FromStr;
+
use std::time;
+

+
#[test]
+
fn rad_id() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();
+

+
    alice.handle.seed(acme, Scope::All).unwrap();
+
    alice.connect(&bob).converge([&bob]);
+

+
    let events = alice.handle.events();
+
    bob.fork(acme, bob.home.path()).unwrap();
+
    bob.announce(acme, 2, bob.home.path()).unwrap();
+
    alice.has_remote_of(&acme, &bob.id);
+

+
    // Alice must have Bob to try add them as a delegate
+
    events
+
        .wait(
+
            |e| matches!(e, Event::RefsFetched { .. }).then_some(()),
+
            time::Duration::from_secs(6),
+
        )
+
        .unwrap();
+

+
    test(
+
        "examples/rad-id.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_id_threshold() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let seed = environment.node("seed");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut seed = seed.spawn();
+
    let mut bob = bob.spawn();
+

+
    seed.handle.seed(acme, Scope::All).unwrap();
+
    alice.handle.seed(acme, Scope::Followed).unwrap();
+
    alice
+
        .handle
+
        .follow(seed.id, Some(Alias::new("seed")))
+
        .unwrap();
+

+
    alice.connect(&seed).connect(&bob);
+
    bob.connect(&seed);
+
    alice.routes_to(&[(acme, seed.id)]);
+
    seed.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
+

+
    formula(&environment.tempdir(), "examples/rad-id-threshold.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .home(
+
            "seed",
+
            environment.work(&seed),
+
            [("RAD_HOME", seed.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_id_threshold_soft_fork() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    let events = bob.handle.events();
+
    bob.handle.seed(acme, Scope::All).unwrap();
+
    alice.connect(&bob).converge([&bob]);
+

+
    events
+
        .wait(
+
            |e| matches!(e, Event::RefsFetched { .. }).then_some(()),
+
            time::Duration::from_secs(6),
+
        )
+
        .unwrap();
+

+
    formula(
+
        &environment.tempdir(),
+
        "examples/rad-id-threshold-soft-fork.md",
+
    )
+
    .unwrap()
+
    .home(
+
        "alice",
+
        environment.work(&alice),
+
        [("RAD_HOME", alice.home.path().display())],
+
    )
+
    .home(
+
        "bob",
+
        environment.work(&bob),
+
        [("RAD_HOME", bob.home.path().display())],
+
    )
+
    .run()
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_id_update_delete_field() {
+
    Environment::alice(["rad-init", "rad-id-update-delete-field"]);
+
}
+

+
#[test]
+
fn rad_id_multi_delegate() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let eve = environment.node("eve");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+

+
    alice.handle.seed(acme, Scope::All).unwrap();
+
    bob.handle.follow(eve.id, None).unwrap();
+
    eve.handle.follow(bob.id, None).unwrap();
+
    alice.connect(&bob).converge([&bob]);
+
    eve.connect(&alice).converge([&alice]);
+

+
    bob.fork(acme, environment.work(&bob)).unwrap();
+
    bob.has_remote_of(&acme, &alice.id);
+
    alice.has_remote_of(&acme, &bob.id);
+

+
    eve.fork(acme, environment.work(&eve)).unwrap();
+
    eve.has_remote_of(&acme, &bob.id);
+
    alice.has_remote_of(&acme, &eve.id);
+
    alice.is_synced_with(&acme, &eve.id);
+
    alice.is_synced_with(&acme, &bob.id);
+

+
    // TODO: Have formula with two connected nodes and a tracked project.
+
    formula(&environment.tempdir(), "examples/rad-id-multi-delegate.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_id_unauthorized_delegate() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    // Alice sets up the seed
+
    alice.handle.seed(acme, Scope::Followed).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.rad(
+
        "clone",
+
        &[acme.to_string().as_str()],
+
        environment.work(&bob),
+
    )
+
    .unwrap();
+

+
    formula(
+
        &environment.tempdir(),
+
        "examples/rad-id-unauthorized-delegate.md",
+
    )
+
    .unwrap()
+
    .home(
+
        "alice",
+
        environment.work(&alice),
+
        [("RAD_HOME", alice.home.path().display())],
+
    )
+
    .home(
+
        "bob",
+
        environment.work(&bob),
+
        [("RAD_HOME", bob.home.path().display())],
+
    )
+
    .run()
+
    .unwrap();
+
}
+

+
#[test]
+
#[ignore = "slow"]
+
fn rad_id_collaboration() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let eve = environment.node("eve");
+
    let seed = environment.seed("seed");
+
    let distrustful = environment.seed("distrustful");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+
    let mut seed = seed.spawn();
+
    let mut distrustful = distrustful.spawn();
+

+
    // Alice sets up the seed and follows Bob and Eve via the CLI
+
    alice.handle.seed(acme, Scope::Followed).unwrap();
+
    alice
+
        .handle
+
        .follow(seed.id, Some(Alias::new("seed")))
+
        .unwrap();
+

+
    // The seed is trustful and will fetch from anyone
+
    seed.handle.seed(acme, Scope::All).unwrap();
+

+
    // The distrustful seed will only interact with Alice and Bob
+
    distrustful.handle.seed(acme, Scope::Followed).unwrap();
+
    distrustful.handle.follow(alice.id, None).unwrap();
+
    distrustful.handle.follow(bob.id, None).unwrap();
+

+
    alice
+
        .connect(&seed)
+
        .connect(&distrustful)
+
        .converge([&seed, &distrustful]);
+
    bob.connect(&seed)
+
        .connect(&distrustful)
+
        .converge([&seed, &distrustful]);
+
    eve.connect(&seed)
+
        .connect(&distrustful)
+
        .converge([&seed, &distrustful]);
+

+
    seed.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
+
    distrustful
+
        .handle
+
        .fetch(acme, alice.id, DEFAULT_TIMEOUT)
+
        .unwrap();
+

+
    formula(&environment.tempdir(), "examples/rad-id-collaboration.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .home(
+
            "eve",
+
            environment.work(&eve),
+
            [("RAD_HOME", eve.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+

+
    // Ensure the seeds have fetched all nodes.
+
    let repo = seed.storage.repository(acme).unwrap();
+
    let mut remotes = repo
+
        .remote_ids()
+
        .unwrap()
+
        .collect::<Result<Vec<_>, _>>()
+
        .unwrap();
+
    let mut expected = vec![alice.id, bob.id, eve.id];
+
    remotes.sort();
+
    expected.sort();
+
    assert_eq!(remotes, expected);
+

+
    let repo = distrustful.storage.repository(acme).unwrap();
+
    let mut remotes = repo
+
        .remote_ids()
+
        .unwrap()
+
        .collect::<Result<Vec<_>, _>>()
+
        .unwrap();
+
    let mut expected = vec![alice.id, bob.id, eve.id];
+
    remotes.sort();
+
    expected.sort();
+
    assert_eq!(remotes, expected);
+
}
+

+
#[test]
+
fn rad_id_conflict() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();
+

+
    alice.connect(&bob).converge([&bob]);
+

+
    bob.fork(acme, environment.work(&bob)).unwrap();
+
    bob.announce(acme, 2, bob.home.path()).unwrap();
+
    alice.has_remote_of(&acme, &bob.id);
+

+
    formula(&environment.tempdir(), "examples/rad-id-conflict.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_id_unknown_field() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+

+
    environment.repository(&alice);
+
    environment.test("rad-init", &alice).unwrap();
+

+
    let alice = alice.spawn();
+
    environment.test("rad-id-unknown-field", &alice).unwrap();
+
}
+

+
#[test]
+
fn rad_id_private() {
+
    Environment::alice(["rad-init-private", "rad-id-private"]);
+
}
added crates/radicle-cli/tests/commands/init.rs
@@ -0,0 +1,1186 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+
use radicle::git;
+
use radicle::node;
+
use radicle::node::address::Store as _;
+
use radicle::node::config::DefaultSeedingPolicy;
+
use radicle::node::policy::Scope;
+
use radicle::node::routing::Store as _;
+
use radicle::node::UserAgent;
+
use radicle::node::DEFAULT_TIMEOUT;
+
use radicle::node::{Alias, Handle as _};
+
use radicle::prelude::{NodeId, RepoId};
+
use radicle::profile;
+
use radicle::storage::{ReadStorage as _, RemoteRepository as _};
+
use radicle_localtime::LocalTime;
+
use radicle_node::test::node::Node;
+
use radicle_node::PROTOCOL_VERSION;
+
use std::net;
+
use std::str::FromStr;
+

+
#[test]
+
#[ignore = "part of many other tests"]
+
fn rad_init() {
+
    Environment::alice(["rad-init"]);
+
}
+

+
#[test]
+
fn rad_init_no_announce() {
+
    Environment::alice(["rad-init-no-announce"]);
+
}
+

+
#[test]
+
fn rad_init_bare() {
+
    let mut env = Environment::new();
+
    let alice = env.profile("alice");
+
    radicle::test::fixtures::bare_repository(env.work(&alice).as_path());
+
    env.tests(["git/git-is-bare-repository", "rad-init"], &alice)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_init_existing() {
+
    let mut environment = Environment::new();
+
    let mut profile = environment.node("alice");
+
    let working = tempfile::tempdir().unwrap();
+
    let rid = profile.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    test(
+
        "examples/rad-init-existing.md",
+
        working.path(),
+
        Some(&profile.home),
+
        [(
+
            "URL",
+
            git::url::File::new(profile.storage.path())
+
                .rid(rid)
+
                .to_string()
+
                .as_str(),
+
        )],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_init_existing_bare() {
+
    let mut environment = Environment::new();
+
    let mut profile = environment.node("alice");
+
    let working = tempfile::tempdir().unwrap();
+
    let rid = profile.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    test(
+
        "examples/rad-init-existing-bare.md",
+
        working.path(),
+
        Some(&profile.home),
+
        [(
+
            "URL",
+
            git::url::File::new(profile.storage.path())
+
                .rid(rid)
+
                .to_string()
+
                .as_str(),
+
        )],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_init_no_seed() {
+
    Environment::alice(["rad-init-no-seed"]);
+
}
+

+
#[test]
+
fn rad_init_with_existing_remote() {
+
    Environment::alice(["rad-init-with-existing-remote"]);
+
}
+

+
#[test]
+
fn rad_init_no_git() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+

+
    // NOTE: There is no repository set up here.
+

+
    environment.test("rad-init-no-git", &profile).unwrap();
+
}
+

+
#[test]
+
fn rad_init_detached_head() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+

+
    // NOTE: There is no repository set up here.
+

+
    environment
+
        .test("rad-init-detached-head", &profile)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_inspect() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+

+
    environment.repository(&profile);
+

+
    environment
+
        .tests(["rad-init", "rad-inspect"], &profile)
+
        .unwrap();
+

+
    // NOTE: The next test runs without $RAD_HOME set.
+
    test(
+
        "examples/rad-inspect-noauth.md",
+
        environment.work(&profile),
+
        None,
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_config() {
+
    let mut environment = Environment::new();
+
    let alias = Alias::new("alice");
+
    let profile = environment.profile_with(profile::Config {
+
        preferred_seeds: vec![radicle::node::config::seeds::RADICLE_NODE_BOOTSTRAP_IRIS
+
            .clone()
+
            .first()
+
            .unwrap()
+
            .clone()],
+
        ..profile::Config::new(alias)
+
    });
+
    let working = tempfile::tempdir().unwrap();
+

+
    test(
+
        "examples/rad-config.md",
+
        working.path(),
+
        Some(&profile.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_warn_old_nodes() {
+
    Environment::alice(["rad-warn-old-nodes"]);
+
}
+

+
#[test]
+
fn rad_checkout() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+
    let copy = tempfile::tempdir().unwrap();
+

+
    environment.repository(&profile);
+

+
    environment.test("rad-init", &profile).unwrap();
+
    test(
+
        "examples/rad-checkout.md",
+
        copy.path(),
+
        Some(&profile.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    if cfg!(target_os = "linux") {
+
        test(
+
            "examples/rad-checkout-repo-config-linux.md",
+
            copy.path(),
+
            Some(&profile.home),
+
            [],
+
        )
+
        .unwrap();
+
    } else if cfg!(target_os = "macos") {
+
        test(
+
            "examples/rad-checkout-repo-config-macos.md",
+
            copy.path(),
+
            Some(&profile.home),
+
            [],
+
        )
+
        .unwrap();
+
    }
+
}
+

+
#[test]
+
fn rad_clean() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let eve = environment.node("eve");
+
    let working = environment.tempdir().join("working");
+

+
    // Setup a test project.
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    radicle::test::fixtures::repository(working.join("acme"));
+
    test(
+
        "examples/rad-init.md",
+
        working.join("acme"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+
    alice.handle.seed(acme, Scope::All).unwrap();
+
    eve.handle.seed(acme, Scope::Followed).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+
    eve.connect(&alice).converge([&alice]);
+

+
    eve.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap();
+

+
    bob.fork(acme, bob.home.path()).unwrap();
+
    bob.announce(acme, 1, bob.home.path()).unwrap();
+
    bob.has_remote_of(&acme, &alice.id);
+
    alice.has_remote_of(&acme, &bob.id);
+
    eve.has_remote_of(&acme, &alice.id);
+

+
    formula(&environment.tempdir(), "examples/rad-clean.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            working.join("acme"),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            working.join("bob"),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .home(
+
            "eve",
+
            working.join("eve"),
+
            [("RAD_HOME", eve.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_seed_and_follow() {
+
    Environment::alice(["rad-seed-and-follow"]);
+
}
+

+
#[test]
+
fn rad_seed_many() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let mut bob = environment.node("bob");
+
    // Bob creates two projects that Alice seeds in the test
+
    let _ = bob.project("heartwood", "Radicle Heartwood Protocol & Stack");
+
    let _ = bob.project("nixpkgs", "Home for Nix Packages");
+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    test(
+
        "examples/rad-seed-many.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_unseed() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let working = tempfile::tempdir().unwrap();
+

+
    // Setup a test project.
+
    alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+
    let alice = alice.spawn();
+

+
    test("examples/rad-unseed.md", working, Some(&alice.home), []).unwrap();
+
}
+

+
#[test]
+
fn rad_unseed_many() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+

+
    // Setup a test project.
+
    alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+
    alice.project("nixpkgs", "Home for Nix Packages");
+
    let alice = alice.spawn();
+

+
    test(
+
        "examples/rad-unseed-many.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_block() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node_with(radicle::node::Config {
+
        seeding_policy: DefaultSeedingPolicy::permissive(),
+
        ..radicle::node::Config::test(Alias::new("alice"))
+
    });
+
    let working = tempfile::tempdir().unwrap();
+

+
    test("examples/rad-block.md", working, Some(&alice.home), []).unwrap();
+
}
+

+
#[test]
+
fn rad_clone() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let working = environment.tempdir().join("working");
+

+
    // Setup a test project.
+
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
+
    alice.handle.seed(acme, Scope::Followed).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    test("examples/rad-clone.md", working, Some(&bob.home), []).unwrap();
+
}
+

+
#[test]
+
fn rad_clone_bare() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let working = environment.tempdir().join("working");
+

+
    // Setup a test project.
+
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
+
    alice.handle.seed(acme, Scope::Followed).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    test("examples/rad-clone-bare.md", working, Some(&bob.home), []).unwrap();
+
}
+

+
#[test]
+
fn rad_clone_directory() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let working = environment.tempdir().join("working");
+

+
    // Setup a test project.
+
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    // Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
+
    alice.handle.seed(acme, Scope::Followed).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    test(
+
        "examples/rad-clone-directory.md",
+
        working,
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_clone_all() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let eve = environment.node("eve");
+

+
    // Setup a test project.
+
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+

+
    alice.handle.seed(acme, Scope::All).unwrap();
+
    bob.connect(&alice).converge([&alice]);
+
    eve.connect(&alice).converge([&alice]);
+

+
    // Fork and sync repo.
+
    bob.fork(acme, bob.home.path()).unwrap();
+
    bob.announce(acme, 2, bob.home.path()).unwrap();
+
    bob.has_remote_of(&acme, &alice.id);
+
    alice.has_remote_of(&acme, &bob.id);
+

+
    test(
+
        "examples/rad-clone-all.md",
+
        environment.work(&eve),
+
        Some(&eve.home),
+
        [],
+
    )
+
    .unwrap();
+
    eve.has_remote_of(&acme, &bob.id);
+
}
+

+
#[test]
+
fn rad_clone_partial_fail() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let mut eve = environment.node("eve");
+
    let carol = NodeId::from_str("z6MksFqXN3Yhqk8pTJdUGLwBTkRfQvwZXPqR2qMEhbS9wzpT").unwrap();
+

+
    // Setup a test project.
+
    let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    // Make Even think she knows about a seed called "carol" that has the repo.
+
    eve.db
+
        .addresses_mut()
+
        .insert(
+
            &carol,
+
            PROTOCOL_VERSION,
+
            node::Features::SEED,
+
            &Alias::new("carol"),
+
            0,
+
            &UserAgent::default(),
+
            LocalTime::now().into(),
+
            [node::KnownAddress::new(
+
                // Eve will fail to connect to this address.
+
                node::Address::from(net::SocketAddr::from(([0, 0, 0, 0], 19873))),
+
                node::address::Source::Imported,
+
            )],
+
        )
+
        .unwrap();
+
    eve.db
+
        .routing_mut()
+
        .add_inventory([&acme], carol, LocalTime::now().into())
+
        .unwrap();
+
    eve.config.peers = node::config::PeerConfig::Static;
+

+
    let mut eve = eve.spawn();
+

+
    alice.handle.seed(acme, Scope::All).unwrap();
+
    bob.handle.seed(acme, Scope::All).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+
    eve.connect(&alice);
+
    eve.connect(&bob);
+
    eve.routes_to(&[(acme, carol), (acme, bob.id), (acme, alice.id)]);
+
    bob.storage.repository(acme).unwrap().remove().unwrap(); // Cause the fetch from Bob to fail.
+
    bob.storage.temporary_repository(acme).ok(); // Prevent repo from being re-fetched.
+

+
    test(
+
        "examples/rad-clone-partial-fail.md",
+
        environment.work(&eve),
+
        Some(&eve.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_clone_connect() {
+
    let mut environment = Environment::new();
+
    let working = environment.tempdir().join("working");
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let mut eve = environment.node("eve");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let ua = UserAgent::default();
+
    let now = LocalTime::now().into();
+

+
    radicle::test::fixtures::repository(working.join("acme"));
+

+
    test(
+
        "examples/rad-init.md",
+
        working.join("acme"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    // Let Eve know about Alice and Bob having the repo.
+
    eve.db
+
        .addresses_mut()
+
        .insert(
+
            &alice.id,
+
            PROTOCOL_VERSION,
+
            node::Features::SEED,
+
            &Alias::new("alice"),
+
            0,
+
            &ua,
+
            now,
+
            [node::KnownAddress::new(
+
                node::Address::from(alice.addr),
+
                node::address::Source::Imported,
+
            )],
+
        )
+
        .unwrap();
+
    eve.db
+
        .addresses_mut()
+
        .insert(
+
            &bob.id,
+
            PROTOCOL_VERSION,
+
            node::Features::SEED,
+
            &Alias::new("bob"),
+
            0,
+
            &ua,
+
            now,
+
            [node::KnownAddress::new(
+
                node::Address::from(bob.addr),
+
                node::address::Source::Imported,
+
            )],
+
        )
+
        .unwrap();
+
    eve.db
+
        .routing_mut()
+
        .add_inventory([&acme], alice.id, now)
+
        .unwrap();
+
    eve.db
+
        .routing_mut()
+
        .add_inventory([&acme], bob.id, now)
+
        .unwrap();
+
    eve.config.peers = node::config::PeerConfig::Static;
+

+
    let eve = eve.spawn();
+

+
    alice.handle.seed(acme, Scope::Followed).unwrap();
+
    bob.handle.seed(acme, Scope::Followed).unwrap();
+
    alice.connect(&bob);
+
    bob.routes_to(&[(acme, alice.id)]);
+
    eve.routes_to(&[(acme, alice.id), (acme, bob.id)]);
+
    alice.routes_to(&[(acme, alice.id), (acme, bob.id)]);
+

+
    test(
+
        "examples/rad-clone-connect.md",
+
        working.join("acme"),
+
        Some(&eve.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_sync_without_node() {
+
    let mut environment = Environment::new();
+
    let alice = environment.seed("alice");
+
    let bob = environment.seed("bob");
+
    let mut eve = environment.seed("eve");
+

+
    let rid = RepoId::from_urn("rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5").unwrap();
+
    eve.policies.seed(&rid, Scope::All).unwrap();
+

+
    formula(&environment.tempdir(), "examples/rad-sync-without-node.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            alice.home.path(),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            bob.home.path(),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .home(
+
            "eve",
+
            eve.home.path(),
+
            [("RAD_HOME", eve.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_self() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node_with(radicle::node::Config {
+
        external_addresses: vec!["seed.alice.acme:8776".parse().unwrap()],
+
        ..radicle::node::Config::test(Alias::new("alice"))
+
    });
+
    let working = environment.tempdir().join("working");
+

+
    test("examples/rad-self.md", working, Some(&alice.home), []).unwrap();
+
}
+

+
#[test]
+
fn rad_clone_unknown() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let working = environment.tempdir().join("working");
+

+
    let alice = alice.spawn();
+

+
    test(
+
        "examples/rad-clone-unknown.md",
+
        working,
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_init_sync_not_connected() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let working = tempfile::tempdir().unwrap();
+
    let alice = alice.spawn();
+

+
    radicle::test::fixtures::repository(working.path().join("alice"));
+

+
    test(
+
        "examples/rad-init-sync-not-connected.md",
+
        working.path().join("alice"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_init_sync_preferred() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment
+
        .node_with(radicle::node::Config {
+
            seeding_policy: DefaultSeedingPolicy::permissive(),
+
            ..radicle::node::Config::test(Alias::new("alice"))
+
        })
+
        .spawn();
+

+
    let bob = environment.profile_with(profile::Config {
+
        preferred_seeds: vec![alice.address()],
+
        ..environment.config("bob")
+
    });
+
    let mut bob = Node::new(bob).spawn();
+

+
    bob.connect(&alice);
+
    alice.handle.follow(bob.id, None).unwrap();
+

+
    environment.repository(&bob);
+

+
    // Bob initializes a repo after her node has started, and after bob has connected to it.
+
    test(
+
        "examples/rad-init-sync-preferred.md",
+
        environment.work(&bob),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_init_sync_timeout() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment
+
        .node_with(radicle::node::Config {
+
            seeding_policy: DefaultSeedingPolicy::Block,
+
            ..radicle::node::Config::test(Alias::new("alice"))
+
        })
+
        .spawn();
+

+
    let bob = environment.profile_with(profile::Config {
+
        preferred_seeds: vec![alice.address()],
+
        ..environment.config("bob")
+
    });
+
    let mut bob = Node::new(bob).spawn();
+

+
    bob.connect(&alice);
+
    alice.handle.follow(bob.id, None).unwrap();
+

+
    environment.repository(&bob);
+

+
    // Bob initializes a repo after her node has started, and after bob has connected to it.
+
    test(
+
        "examples/rad-init-sync-timeout.md",
+
        environment.work(&bob),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_init_sync_and_clone() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice);
+

+
    environment.repository(&alice);
+

+
    // Alice initializes a repo after her node has started, and after bob has connected to it.
+
    test(
+
        "examples/rad-init-sync.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    // Wait for bob to get any updates to the routing table.
+
    bob.converge([&alice]);
+

+
    test(
+
        "examples/rad-clone.md",
+
        environment.work(&bob),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_fetch() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();
+

+
    alice.connect(&bob);
+
    environment.repository(&alice);
+

+
    // Alice initializes a repo after her node has started, and after bob has connected to it.
+
    environment.test("rad-init-sync", &alice).unwrap();
+

+
    // Wait for bob to get any updates to the routing table.
+
    bob.converge([&alice]);
+

+
    environment.test("rad-fetch", &bob).unwrap();
+
}
+

+
#[test]
+
fn rad_fork() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    let mut alice = alice.spawn();
+
    let bob = bob.spawn();
+

+
    alice.connect(&bob);
+
    environment.repository(&alice);
+

+
    // Alice initializes a repo after her node has started, and after bob has connected to it.
+
    environment.test("rad-init-sync", &alice).unwrap();
+

+
    // Wait for bob to get any updates to the routing table.
+
    bob.converge([&alice]);
+

+
    environment.tests(["rad-fetch", "rad-fork"], &bob).unwrap();
+
}
+

+
#[test]
+
// User tries to clone; no seeds are available, but user has the repo locally.
+
fn test_clone_without_seeds() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let working = environment.tempdir().join("working");
+
    let rid = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
+
    let mut alice = alice.spawn();
+
    let seeds = alice.handle.seeds_for(rid, [alice.id]).unwrap();
+
    let connected = seeds.connected().collect::<Vec<_>>();
+

+
    assert!(connected.is_empty());
+

+
    alice
+
        .rad("clone", &[rid.to_string().as_str()], working.as_path())
+
        .unwrap();
+

+
    alice
+
        .rad("inspect", &[], working.join("heartwood").as_path())
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_sync() {
+
    let mut environment = Environment::new();
+
    let working = environment.tempdir().join("working");
+
    let alice = environment.seed("alice");
+
    let bob = environment.seed("bob");
+
    let eve = environment.seed("eve");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    radicle::test::fixtures::repository(working.join("acme"));
+

+
    test(
+
        "examples/rad-init.md",
+
        working.join("acme"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+

+
    bob.handle.seed(acme, Scope::All).unwrap();
+
    eve.handle.seed(acme, Scope::All).unwrap();
+

+
    alice.connect(&bob);
+
    eve.connect(&alice);
+

+
    bob.routes_to(&[(acme, alice.id)]);
+
    eve.routes_to(&[(acme, alice.id)]);
+
    alice.routes_to(&[(acme, alice.id), (acme, eve.id), (acme, bob.id)]);
+
    alice.is_synced_with(&acme, &eve.id);
+
    alice.is_synced_with(&acme, &bob.id);
+

+
    test(
+
        "examples/rad-sync.md",
+
        working.join("acme"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
//
+
//     alice -- seed -- bob
+
//
+
fn test_replication_via_seed() {
+
    let mut environment = Environment::new();
+
    let alice = environment.relay("alice");
+
    let bob = environment.relay("bob");
+
    let seed = environment.node_with(radicle::node::Config {
+
        seeding_policy: DefaultSeedingPolicy::permissive(),
+
        ..crate::util::environment::config::relay("seed")
+
    });
+
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let seed = seed.spawn();
+

+
    alice.connect(&seed);
+
    bob.connect(&seed);
+

+
    // Enough time for the next inventory from Seed to not be considered stale by Bob.
+
    std::thread::sleep(std::time::Duration::from_millis(3));
+

+
    alice.routes_to(&[]);
+
    seed.routes_to(&[]);
+
    bob.routes_to(&[]);
+

+
    // Initialize a repo as Alice.
+
    environment.repository(&alice);
+
    alice
+
        .rad(
+
            "init",
+
            &[
+
                "--name",
+
                "heartwood",
+
                "--description",
+
                "Radicle Heartwood Protocol & Stack",
+
                "--default-branch",
+
                "master",
+
                "--public",
+
            ],
+
            environment.work(&alice),
+
        )
+
        .unwrap();
+

+
    alice
+
        .rad("follow", &[&bob.id.to_human()], environment.work(&alice))
+
        .unwrap();
+

+
    alice.routes_to(&[(rid, alice.id), (rid, seed.id)]);
+
    seed.routes_to(&[(rid, alice.id), (rid, seed.id)]);
+
    bob.routes_to(&[(rid, alice.id), (rid, seed.id)]);
+

+
    let seed_events = seed.handle.events();
+
    let alice_events = alice.handle.events();
+

+
    bob.fork(rid, environment.work(&bob)).unwrap();
+

+
    alice.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
+
    seed.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
+
    bob.routes_to(&[(rid, alice.id), (rid, seed.id), (rid, bob.id)]);
+

+
    seed_events.iter().any(|e| {
+
        matches!(
+
            e, radicle::node::Event::RefsFetched { updated, remote, .. }
+
            if remote == bob.id && updated.iter().any(|u| u.is_created())
+
        )
+
    });
+
    alice_events.iter().any(|e| {
+
        matches!(
+
            e, radicle::node::Event::RefsFetched { updated, remote, .. }
+
            if remote == seed.id && updated.iter().any(|u| u.is_created())
+
        )
+
    });
+

+
    seed.storage
+
        .repository(rid)
+
        .unwrap()
+
        .remote(&bob.id)
+
        .unwrap();
+

+
    // Seed should send Bob's ref announcement to Alice, after the fetch.
+
    alice
+
        .storage
+
        .repository(rid)
+
        .unwrap()
+
        .remote(&bob.id)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_remote() {
+
    let mut environment = Environment::new();
+
    let alice = environment.relay("alice");
+
    let bob = environment.relay("bob");
+
    let eve = environment.relay("eve");
+
    let home = alice.home.clone();
+
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    // Setup a test repository.
+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+
    alice
+
        .handle
+
        .follow(bob.id, Some(Alias::new("bob")))
+
        .unwrap();
+
    alice
+
        .handle
+
        .follow(eve.id, Some(Alias::new("eve")))
+
        .unwrap();
+

+
    bob.connect(&alice);
+
    bob.routes_to(&[(rid, alice.id)]);
+
    bob.fork(rid, bob.home.path()).unwrap();
+
    alice.has_remote_of(&rid, &bob.id);
+

+
    eve.connect(&alice);
+
    eve.routes_to(&[(rid, alice.id)]);
+
    eve.fork(rid, eve.home.path()).unwrap();
+
    alice.has_remote_of(&rid, &eve.id);
+

+
    test(
+
        "examples/rad-remote.md",
+
        environment.work(&alice),
+
        Some(&home),
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_merge_via_push() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+

+
    environment.repository(&alice);
+

+
    environment.test("rad-init", &alice).unwrap();
+

+
    let alice = alice.spawn();
+

+
    environment.test("rad-merge-via-push", &alice).unwrap();
+
}
+

+
#[test]
+
fn rad_merge_after_update() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+

+
    environment.repository(&alice);
+

+
    environment.test("rad-init", &alice).unwrap();
+

+
    let alice = alice.spawn();
+

+
    environment.test("rad-merge-after-update", &alice).unwrap();
+
}
+

+
#[test]
+
fn rad_merge_no_ff() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+

+
    environment.repository(&alice);
+

+
    environment
+
        .tests(["rad-init", "rad-merge-no-ff"], &alice)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_init_private() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+

+
    environment.repository(&alice);
+

+
    environment.test("rad-init-private", &alice).unwrap();
+
}
+

+
#[test]
+
fn rad_init_private_no_seed() {
+
    Environment::alice(["rad-init-private-no-seed"]);
+
}
+

+
#[test]
+
fn rad_init_private_seed() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    environment.repository(&alice);
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    environment.test("rad-init-private", &alice).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    formula(&environment.tempdir(), "examples/rad-init-private-seed.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            bob.home.path(),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_init_private_clone() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    environment.repository(&alice);
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    environment.test("rad-init-private", &alice).unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    formula(&environment.tempdir(), "examples/rad-init-private-clone.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            bob.home.path(),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_init_private_clone_seed() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    environment.repository(&alice);
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    test(
+
        "examples/rad-init-private.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    formula(
+
        &environment.tempdir(),
+
        "examples/rad-init-private-clone-seed.md",
+
    )
+
    .unwrap()
+
    .home(
+
        "alice",
+
        environment.work(&alice),
+
        [("RAD_HOME", alice.home.path().display())],
+
    )
+
    .home(
+
        "bob",
+
        bob.home.path(),
+
        [("RAD_HOME", bob.home.path().display())],
+
    )
+
    .run()
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_publish() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+

+
    environment.repository(&alice);
+

+
    environment
+
        .tests(["rad-init-private", "rad-publish"], &alice)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_seed_policy_allow_no_scope() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node_with(radicle::node::Config {
+
        seeding_policy: DefaultSeedingPolicy::Allow {
+
            scope: node::config::Scope::implicit(),
+
        },
+
        ..radicle::node::Config::test(Alias::new("alice"))
+
    });
+

+
    let alice = alice.spawn();
+

+
    test(
+
        "examples/rad-seed-policy-allow-no-scope.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
added crates/radicle-cli/tests/commands/issue.rs
@@ -0,0 +1,11 @@
+
use crate::util::environment::Environment;
+

+
#[test]
+
fn rad_issue() {
+
    Environment::alice(["rad-init", "rad-issue"]);
+
}
+

+
#[test]
+
fn rad_issue_list() {
+
    Environment::alice(["rad-init", "rad-issue", "rad-issue-list"]);
+
}
added crates/radicle-cli/tests/commands/misc.rs
@@ -0,0 +1,128 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+
use radicle::test::fixtures;
+

+
#[test]
+
fn rad_help() {
+
    Environment::alice(["rad-help"]);
+
}
+

+
#[test]
+
fn rad_auth() {
+
    test("examples/rad-auth.md", std::path::Path::new("."), None, []).unwrap();
+
}
+

+
#[test]
+
fn rad_key_mismatch() {
+
    let mut environment = Environment::new();
+
    let alice = environment.profile("alice");
+
    environment.repository(&alice);
+

+
    environment.test("rad-init", &alice).unwrap();
+

+
    // Replace the public key with one that does not match the secret key anymore.
+
    std::fs::write(alice.home.path().join("keys").join("radicle.pub"), "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE6Ul/D+P0I/Hl1JVOWGS8Z589us9FqKQXWv8OMOpKCh snakeoil\n").unwrap();
+

+
    environment.test("rad-key-mismatch", &alice).unwrap();
+
}
+

+
#[test]
+
fn rad_auth_errors() {
+
    test(
+
        "examples/rad-auth-errors.md",
+
        std::path::Path::new("."),
+
        None,
+
        [],
+
    )
+
    .unwrap();
+
}
+

+
#[cfg(unix)]
+
#[test]
+
fn rad_diff() {
+
    if std::env::consts::OS == "macos" {
+
        // macOS's `sed` requires an argument for `-i`, which we don't provide
+
        // in the example. Providing it makes the test fail on Linux.
+
        // Since this command is deprecated anyway, we just skip macOS.
+
        return;
+
    }
+

+
    let tmp = tempfile::tempdir().unwrap();
+

+
    fixtures::repository(&tmp);
+

+
    test("examples/rad-diff.md", tmp, None, []).unwrap();
+
}
+

+
#[test]
+
fn framework_home() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    formula(&environment.tempdir(), "examples/framework/home.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            alice.home.path(),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            bob.home.path(),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_workflow() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    environment.repository(&alice);
+

+
    environment.test("workflow/1-new-project", &alice).unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    environment.test("workflow/2-cloning", &bob).unwrap();
+

+
    test(
+
        "examples/workflow/3-issues.md",
+
        environment.work(&bob).join("heartwood"),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    test(
+
        "examples/workflow/4-patching-contributor.md",
+
        environment.work(&bob).join("heartwood"),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    test(
+
        "examples/workflow/5-patching-maintainer.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    test(
+
        "examples/workflow/6-pulling-contributor.md",
+
        environment.work(&bob).join("heartwood"),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+
}
added crates/radicle-cli/tests/commands/node.rs
@@ -0,0 +1,112 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use radicle::node::address::Store as _;
+
use radicle::node::config::DefaultSeedingPolicy;
+
use radicle::node::Address;
+
use radicle::node::UserAgent;
+
use radicle::node::{Alias, Handle as _};
+
use radicle::test::fixtures;
+
use radicle_localtime::LocalTime;
+
use radicle_node::PROTOCOL_VERSION;
+
use std::net;
+
use std::str::FromStr;
+

+
#[test]
+
fn rad_node_connect() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let working = tempfile::tempdir().unwrap();
+
    let alice = alice.spawn();
+
    let bob = bob.spawn();
+

+
    alice
+
        .rad(
+
            "node",
+
            &["connect", format!("{}@{}", bob.id, bob.addr).as_str()],
+
            working.path(),
+
        )
+
        .unwrap();
+

+
    let sessions = alice.handle.sessions().unwrap();
+
    let session = sessions.first().unwrap();
+

+
    assert_eq!(session.nid, bob.id);
+
    assert_eq!(session.addr, bob.addr.into());
+
    assert!(session.state.is_connected());
+
}
+

+
#[test]
+
fn rad_node_connect_without_address() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let working = tempfile::tempdir().unwrap();
+
    let bob = bob.spawn();
+

+
    alice
+
        .db
+
        .addresses_mut()
+
        .insert(
+
            &bob.id,
+
            PROTOCOL_VERSION,
+
            radicle::node::Features::SEED,
+
            &Alias::new("bob"),
+
            0,
+
            &UserAgent::default(),
+
            LocalTime::now().into(),
+
            [radicle::node::KnownAddress::new(
+
                radicle::node::Address::from(bob.addr),
+
                radicle::node::address::Source::Imported,
+
            )],
+
        )
+
        .unwrap();
+
    let alice = alice.spawn();
+
    alice
+
        .rad(
+
            "node",
+
            &["connect", format!("{}", bob.id).as_str()],
+
            working.path(),
+
        )
+
        .unwrap();
+

+
    let sessions = alice.handle.sessions().unwrap();
+
    let session = sessions.first().unwrap();
+

+
    assert_eq!(session.nid, bob.id);
+
    assert_eq!(session.addr, bob.addr.into());
+
    assert!(session.state.is_connected());
+
}
+

+
#[test]
+
fn rad_node() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node_with(radicle::node::Config {
+
        external_addresses: vec![
+
            Address::from(net::SocketAddr::from(([41, 12, 98, 112], 8776))),
+
            Address::from_str("seed.cloudhead.io:8776").unwrap(),
+
        ],
+
        seeding_policy: DefaultSeedingPolicy::Block,
+
        ..radicle::node::Config::test(Alias::new("alice"))
+
    });
+
    let working = tempfile::tempdir().unwrap();
+
    let alice = alice.spawn();
+

+
    fixtures::repository(working.path().join("alice"));
+

+
    test(
+
        "examples/rad-init-sync-not-connected.md",
+
        working.path().join("alice"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    test(
+
        "examples/rad-node.md",
+
        working.path().join("alice"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+
}
added crates/radicle-cli/tests/commands/patch.rs
@@ -0,0 +1,454 @@
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+
use crate::{program_reports_version, test};
+
use radicle::git;
+
use radicle::node::policy::Scope;
+
use radicle::node::Handle as _;
+
use radicle::prelude::RepoId;
+
use radicle::test::fixtures;
+
use std::str::FromStr;
+

+
#[test]
+
fn rad_patch() {
+
    Environment::alice(["rad-init", "rad-patch"]);
+
}
+

+
#[test]
+
fn rad_jj_bare() {
+
    // We test whether `jj` is installed, and have this test succeed if it is not.
+
    // Programmatic skipping of tests is not supported as of 2024-08.
+
    if !program_reports_version("jj") {
+
        return;
+
    }
+

+
    let mut environment = Environment::new();
+
    let mut profile = environment.node("alice");
+
    let rid = profile.project("heartwood", "Radicle Heartwood Protocol & Stack");
+

+
    test(
+
        "examples/rad-init-existing-bare.md",
+
        environment.work(&profile),
+
        Some(&profile.home),
+
        [(
+
            "URL",
+
            git::url::File::new(profile.storage.path())
+
                .rid(rid)
+
                .to_string()
+
                .as_str(),
+
        )],
+
    )
+
    .unwrap();
+

+
    environment
+
        .tests(["jj-config", "jj-init-bare"], &profile)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_jj_colocated_patch() {
+
    // We test whether `jj` is installed, and have this test succeed if it is not.
+
    // Programmatic skipping of tests is not supported as of 2024-08.
+
    if !program_reports_version("jj") {
+
        return;
+
    }
+

+
    Environment::alice(["rad-init", "jj-config", "jj-init-colocate", "rad-patch-jj"])
+
}
+

+
#[test]
+
fn rad_patch_diff() {
+
    Environment::alice(["rad-init", "rad-patch-diff"]);
+
}
+

+
#[test]
+
fn rad_patch_edit() {
+
    Environment::alice(["rad-init", "rad-patch-edit"]);
+
}
+

+
#[test]
+
fn rad_patch_checkout() {
+
    Environment::alice(["rad-init", "rad-patch-checkout"]);
+
}
+

+
#[test]
+
fn rad_patch_checkout_revision() {
+
    Environment::alice([
+
        "rad-init",
+
        "rad-patch-checkout",
+
        "rad-patch-checkout-revision",
+
    ]);
+
}
+

+
#[test]
+
fn rad_patch_checkout_force() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.handle.seed(acme, Scope::All).unwrap();
+
    alice.connect(&bob).converge([&bob]);
+

+
    bob.rad(
+
        "clone",
+
        &[acme.to_string().as_str()],
+
        environment.work(&bob),
+
    )
+
    .unwrap();
+

+
    formula(
+
        &environment.tempdir(),
+
        "examples/rad-patch-checkout-force.md",
+
    )
+
    .unwrap()
+
    .home(
+
        "alice",
+
        environment.work(&alice),
+
        [("RAD_HOME", alice.home.path().display())],
+
    )
+
    .home(
+
        "bob",
+
        environment.work(&bob),
+
        [("RAD_HOME", bob.home.path().display())],
+
    )
+
    .run()
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_patch_update() {
+
    Environment::alice(["rad-init", "rad-patch-update"]);
+
}
+

+
#[test]
+
#[cfg(not(target_os = "macos"))]
+
fn rad_patch_ahead_behind() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile("alice");
+

+
    environment.repository(&profile);
+

+
    std::fs::write(
+
        environment.work(&profile).join("CONTRIBUTORS"),
+
        "Alice Jones\n",
+
    )
+
    .unwrap();
+

+
    environment
+
        .tests(["rad-init", "rad-patch-ahead-behind"], &profile)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_patch_change_base() {
+
    Environment::alice(["rad-init", "rad-patch-change-base"]);
+
}
+

+
#[test]
+
fn rad_patch_draft() {
+
    Environment::alice(["rad-init", "rad-patch-draft"]);
+
}
+

+
#[test]
+
fn rad_patch_via_push() {
+
    Environment::alice(["rad-init", "rad-patch-via-push"]);
+
}
+

+
#[test]
+
fn rad_patch_detached_head() {
+
    Environment::alice(["rad-init", "rad-patch-detached-head"]);
+
}
+

+
#[test]
+
fn rad_patch_merge_draft() {
+
    Environment::alice(["rad-init", "rad-patch-merge-draft"]);
+
}
+

+
#[test]
+
fn rad_patch_revert_merge() {
+
    Environment::alice(["rad-init", "rad-patch-revert-merge"]);
+
}
+

+
#[test]
+
#[cfg(not(target_os = "macos"))]
+
fn rad_review_by_hunk() {
+
    Environment::alice(["rad-init", "rad-review-by-hunk"]);
+
}
+

+
#[test]
+
fn rad_patch_delete() {
+
    let mut environment = Environment::new();
+
    let alice = environment.relay("alice");
+
    let bob = environment.relay("bob");
+
    let seed = environment.relay("seed");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let mut alice = alice.spawn();
+
    let mut bob = bob.spawn();
+
    let mut seed = seed.spawn();
+

+
    bob.handle.seed(acme, Scope::All).unwrap();
+
    seed.handle.seed(acme, Scope::All).unwrap();
+
    alice.connect(&bob).connect(&seed).converge([&bob, &seed]);
+
    bob.connect(&seed).converge([&seed]);
+
    bob.routes_to(&[(acme, seed.id)]);
+

+
    bob.rad(
+
        "clone",
+
        &[acme.to_string().as_str()],
+
        environment.work(&bob),
+
    )
+
    .unwrap();
+

+
    formula(&environment.tempdir(), "examples/rad-patch-delete.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .home(
+
            "seed",
+
            environment.work(&seed),
+
            [("RAD_HOME", seed.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_push_and_pull_patches() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+

+
    environment.repository(&alice);
+

+
    test(
+
        "examples/rad-init.md",
+
        environment.work(&alice),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.fork(acme, environment.work(&bob)).unwrap();
+
    alice.has_remote_of(&acme, &bob.id);
+

+
    formula(
+
        &environment.tempdir(),
+
        "examples/rad-push-and-pull-patches.md",
+
    )
+
    .unwrap()
+
    .home(
+
        "alice",
+
        environment.work(&alice),
+
        [("RAD_HOME", alice.home.path().display())],
+
    )
+
    .home(
+
        "bob",
+
        environment.work(&bob).join("heartwood"),
+
        [("RAD_HOME", bob.home.path().display())],
+
    )
+
    .run()
+
    .unwrap();
+
}
+

+
#[test]
+
fn rad_patch_fetch_1() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let (repo, _) = environment.repository(&alice);
+
    let rid = alice.project_from("heartwood", "Radicle Heartwood Protocol & Stack", &repo);
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.clone(rid, environment.work(&bob)).unwrap();
+

+
    formula(&environment.tempdir(), "examples/rad-patch-fetch-1.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob).join("heartwood"),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_watch() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let (repo, _) = environment.repository(&alice);
+
    let rid = alice.project_from("heartwood", "Radicle Heartwood Protocol & Stack", &repo);
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.clone(rid, environment.work(&bob)).unwrap();
+

+
    formula(&environment.tempdir(), "examples/rad-watch.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob).join("heartwood"),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_inbox() {
+
    let mut environment = Environment::new();
+
    let mut alice = environment.node("alice");
+
    let bob = environment.node("bob");
+
    let (repo1, _) = fixtures::repository(environment.work(&alice).join("heartwood"));
+
    let (repo2, _) = fixtures::repository(environment.work(&alice).join("radicle-git"));
+
    let rid1 = alice.project_from("heartwood", "Radicle Heartwood Protocol & Stack", &repo1);
+
    let rid2 = alice.project_from("radicle-git", "Radicle Git", &repo2);
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+
    bob.clone(rid1, environment.work(&bob)).unwrap();
+
    bob.clone(rid2, environment.work(&bob)).unwrap();
+

+
    formula(&environment.tempdir(), "examples/rad-inbox.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            environment.work(&bob),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_patch_fetch_2() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+

+
    environment.repository(&alice);
+

+
    environment
+
        .tests(["rad-init", "rad-patch-fetch-2"], &alice)
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_patch_pull_update() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node("alice");
+
    let bob = environment.node("bob");
+

+
    environment.repository(&alice);
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    formula(&environment.tempdir(), "examples/rad-patch-pull-update.md")
+
        .unwrap()
+
        .home(
+
            "alice",
+
            environment.work(&alice),
+
            [("RAD_HOME", alice.home.path().display())],
+
        )
+
        .home(
+
            "bob",
+
            bob.home.path(),
+
            [("RAD_HOME", bob.home.path().display())],
+
        )
+
        .run()
+
        .unwrap();
+
}
+

+
#[test]
+
fn rad_patch_open_explore() {
+
    let mut environment = Environment::new();
+
    let seed = environment
+
        .node_with(radicle::node::Config {
+
            seeding_policy: radicle::node::config::DefaultSeedingPolicy::permissive(),
+
            ..crate::util::environment::config::seed("seed")
+
        })
+
        .spawn();
+

+
    let bob = environment.profile_with(radicle::profile::Config {
+
        preferred_seeds: vec![seed.address()],
+
        ..environment.config("bob")
+
    });
+
    let mut bob = radicle_node::test::node::Node::new(bob).spawn();
+
    let working = environment.tempdir().join("working");
+

+
    fixtures::repository(&working);
+

+
    bob.connect(&seed);
+
    bob.init("heartwood", "", &working).unwrap();
+
    bob.converge([&seed]);
+

+
    test(
+
        "examples/rad-patch-open-explore.md",
+
        &working,
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+
}