Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: Refactor commands.rs into sub modules
Merged ade opened 1 month ago

The huge file was a sight for sore eyes, so refactored into sub modules.

The grouping may not be perfect, but its working towards more structure across the tests. Ideally we’d also restructure the markdown files too, but thats left for another patch.

18 files changed +2982 -2799 5aaf978f 1b986af0
modified crates/radicle-cli/tests/commands.rs
@@ -1,35 +1,38 @@
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::node::{Alias, Handle as _};
+
use radicle::prelude::RepoId;
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::environment::Environment;
use util::formula::formula;

+
mod commands {
+
    mod checkout;
+
    mod clone;
+
    mod cob;
+
    mod git;
+
    mod id;
+
    mod inbox;
+
    mod init;
+
    mod issue;
+
    mod jj;
+
    mod node;
+
    mod patch;
+
    mod policy;
+
    mod remote;
+
    mod sync;
+
    mod utility;
+
    mod watch;
+
    mod workflow;
+
}
+

/// Run a CLI test file.
pub(crate) fn test<'a>(
    test: impl AsRef<Path>,
@@ -88,2807 +91,50 @@ fn program_reports_version(program: &str) -> bool {
}

#[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;
-
    }
-

+
fn rad_remote() {
    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();
-

+
    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.
-
    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_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),
+
        Some(&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();
+
    let mut eve = eve.spawn();
    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()
+
        .follow(bob.id, Some(Alias::new("bob")))
        .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),
-
        )
+
    alice
+
        .handle
+
        .follow(eve.id, Some(Alias::new("eve")))
        .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();
+
    bob.connect(&alice);
+
    bob.routes_to(&[(rid, alice.id)]);
+
    bob.fork(rid, bob.home.path()).unwrap();
+
    alice.has_remote_of(&rid, &bob.id);

-
    environment.repository(&alice);
+
    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-init.md",
+
        "examples/rad-remote.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),
+
        Some(&home),
        [],
    )
    .unwrap();
added crates/radicle-cli/tests/commands/checkout.rs
@@ -0,0 +1,38 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+

+
#[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();
+
    }
+
}
added crates/radicle-cli/tests/commands/clone.rs
@@ -0,0 +1,297 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use radicle::node;
+
use radicle::node::address::Store as _;
+
use radicle::node::policy::Scope;
+
use radicle::node::routing::Store as _;
+
use radicle::node::UserAgent;
+
use radicle::node::{Alias, Handle as _};
+
use radicle::prelude::{NodeId, RepoId};
+
use radicle::storage::ReadStorage as _;
+
use radicle_localtime::LocalTime;
+
use radicle_node::PROTOCOL_VERSION;
+
use std::net;
+
use std::str::FromStr;
+

+
#[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_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]
+
// 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();
+
}
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/inbox.rs
@@ -0,0 +1,36 @@
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+
use radicle::test::fixtures;
+

+
#[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();
+
}
added crates/radicle-cli/tests/commands/init.rs
@@ -0,0 +1,342 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+
use radicle::git;
+
use radicle::node::config::DefaultSeedingPolicy;
+
use radicle::node::{Alias, Handle as _};
+
use radicle::profile;
+
use radicle_node::test::node::Node;
+

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

+
#[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_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_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();
+
}
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/jj.rs
@@ -0,0 +1,45 @@
+
use crate::util::environment::Environment;
+
use crate::{program_reports_version, test};
+
use radicle::git;
+

+
#[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"])
+
}
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,388 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+
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_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_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();
+
}
+

+
#[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();
+
}
added crates/radicle-cli/tests/commands/policy.rs
@@ -0,0 +1,97 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+
use radicle::node;
+
use radicle::node::config::DefaultSeedingPolicy;
+
use radicle::node::Alias;
+

+
#[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_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/remote.rs
@@ -0,0 +1,57 @@
+
use std::str::FromStr as _;
+

+
use radicle::node::{Alias, Handle as _};
+
use radicle::prelude::RepoId;
+

+
use crate::test;
+
use crate::util::environment::Environment;
+

+
#[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();
+
}
added crates/radicle-cli/tests/commands/sync.rs
@@ -0,0 +1,197 @@
+
use std::str::FromStr as _;
+

+
use radicle::node::config::DefaultSeedingPolicy;
+
use radicle::node::policy::Scope;
+
use radicle::node::Handle as _;
+
use radicle::prelude::RepoId;
+
use radicle::storage::{ReadStorage as _, RemoteRepository as _};
+

+
use crate::test;
+
use crate::util::{environment::Environment, formula::formula};
+

+
#[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_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_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();
+
}
added crates/radicle-cli/tests/commands/utility.rs
@@ -0,0 +1,226 @@
+
use std::str::FromStr as _;
+

+
use radicle::node::policy::Scope;
+
use radicle::node::DEFAULT_TIMEOUT;
+
use radicle::node::{Alias, Handle as _};
+
use radicle::prelude::RepoId;
+
use radicle::profile;
+

+
use crate::test;
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+

+
#[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_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_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_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() {
+
    use radicle::test::fixtures;
+

+
    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 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]
+
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();
+
}
added crates/radicle-cli/tests/commands/watch.rs
@@ -0,0 +1,32 @@
+
use crate::util::environment::Environment;
+
use crate::util::formula::formula;
+

+
#[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();
+
}
added crates/radicle-cli/tests/commands/workflow.rs
@@ -0,0 +1,52 @@
+
use crate::test;
+
use crate::util::environment::Environment;
+

+
#[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();
+
}