wsb 0.258.6441

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The command line's own guarantees: which verbs exist, and that the two
//! kinds of "sync" can never be confused.

use super::*;

fn args(words: &[&str]) -> Vec<String> {
    words.iter().map(|w| w.to_string()).collect()
}

/// TEST9984: peer continuity and the fleet's git sync are separate commands
/// with separate vocabularies.
///
/// They share a word in English and nothing else. `wsb sync` moves history
/// through git; `wsb peer sync` carries an unfinished working tree and never
/// touches git. A person who confuses them at the command line is one who
/// expects one and gets the other, so the usage says which is which.
#[test]
fn test9984_peer_sync_is_not_the_fleets_git_sync() {
    assert!(USAGE.contains("sync <peer>"), "the peer verb is named");
    assert!(
        USAGE.contains("`wsb sync`"),
        "the usage must say what the OTHER sync is: {USAGE}"
    );
    assert!(
        USAGE.contains("never pulls, pushes, merges, rebases, commits"),
        "the usage must state what continuity never does to git"
    );

    // An unknown verb is a usage mistake (2), not a failure (1) — a script
    // must be able to tell "you called this wrongly" from "it did not work".
    assert_eq!(dispatch(&args(&["wsb", "peer", "reticulate"])), 2);
    // Help is not a mistake.
    assert_eq!(dispatch(&args(&["wsb", "peer", "help"])), 0);
}

/// TEST9985: a peer is named by a person, and an ambiguous name is refused
/// rather than picked.
///
/// The commands take a peer by NAME because that is what an operator knows.
/// Two peers whose names both match is a state where guessing would act on
/// the wrong machine — pausing one relationship while the operator watched
/// the other.
#[test]
fn test9985_an_ambiguous_peer_name_is_refused() {
    let laptop = PeerView {
        id: "aaaa1111".into(),
        name: "laptop".into(),
        fingerprint: String::new(),
        address: String::new(),
        state: "connected".into(),
        detail: None,
        shared_workspaces: 0,
    };
    let other = PeerView {
        id: "aaaa2222".into(),
        name: "laptop-2".into(),
        ..laptop.clone()
    };
    let peers = vec![laptop.clone(), other.clone()];

    // An exact name wins even when it is a prefix of another id or name.
    let chosen = choose(&peers, "laptop").expect("an exact name is not ambiguous");
    assert_eq!(chosen.id, laptop.id);

    // A prefix that matches two ids is refused, naming both.
    let error = choose(&peers, "aaaa").expect_err("an ambiguous prefix must be refused");
    assert!(error.contains("laptop"), "{error}");
    assert!(error.contains("laptop-2"), "{error}");

    // A name nothing matches lists what there is, rather than saying "not
    // found" about a machine the operator can see in the workbench.
    let error = choose(&peers, "desktop").expect_err("an unknown name must be refused");
    assert!(error.contains("laptop"), "{error}");
}

/// The selection `find_peer` performs, against a list a test controls.
///
/// The command talks to a daemon for the list; this is the choosing, which is
/// the part with a rule in it.
fn choose(peers: &[PeerView], name: &str) -> Result<PeerView, String> {
    let matches: Vec<&PeerView> = peers
        .iter()
        .filter(|peer| peer.name.eq_ignore_ascii_case(name) || peer.id == name)
        .collect();
    if let [one] = matches.as_slice() {
        return Ok((*one).clone());
    }
    let prefixed: Vec<&PeerView> = peers
        .iter()
        .filter(|peer| peer.id.starts_with(name) || peer.name.starts_with(name))
        .collect();
    match prefixed.as_slice() {
        [one] => Ok((*one).clone()),
        [] => Err(format!(
            "no peer called '{name}'. This machine knows: {}",
            peers
                .iter()
                .map(|peer| peer.name.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        )),
        many => Err(format!(
            "'{name}' matches {} peers: {}. Name one exactly.",
            many.len(),
            many.iter().map(|peer| peer.name.as_str()).collect::<Vec<_>>().join(", ")
        )),
    }
}

/// TEST10145: every switch is reachable from a terminal, and the default is
/// stated.
///
/// The usage is the contract for somebody who has just been surprised, and
/// the surprise that was real is enabling a workspace and expecting files to
/// start moving. So the usage has to say that manual is the default.
#[test]
fn test10145_the_usage_states_the_two_things_that_surprise_people() {
    assert!(
        USAGE.contains("service <peer> <service> <on|off>")
            && USAGE.contains("files, or a plugin's <plugin>.<service>"),
        "the service must be switchable by name, and the names said: {USAGE}"
    );
    assert!(
        USAGE.contains("MANUAL mode"),
        "the default must be stated, not discovered: {USAGE}"
    );
    assert!(
        USAGE.contains("take") && USAGE.contains("keep") && USAGE.contains("leave"),
        "all three answers to a difference must be listed: {USAGE}"
    );

    // The verbs exist and are not usage mistakes. They fail because no daemon
    // is reachable in a unit test, which is exit 1 — not exit 2, which would
    // mean the verb itself was unknown.
    for verb in ["services", "service", "mode", "deltas", "take", "keep", "leave", "reconsider"] {
        assert_ne!(
            dispatch(&args(&["wsb", "peer", verb, "somepeer"])),
            2,
            "'{verb}' must be a verb this command knows"
        );
    }
    assert_eq!(dispatch(&args(&["wsb", "peer", "reticulate"])), 2);
}

/// TEST10146: the vocabulary the command line accepts is the model's.
///
/// These words travel: typed here, stored in the machine database, sent over
/// the wire, rendered in the workbench. A command that accepted `--auto` or
/// `true` would be one that stored something no other surface could read, so
/// the parse is the model's own.
#[test]
fn test10146_service_arguments_parse_through_the_model() {
    use crate::peer::model::{Service, Switch, SyncMode};

    assert_eq!(Service::parse("files"), Ok(Service::Files));
    assert!(Service::parse("secrets").is_err(), "a service wsb does not run is refused");
    assert!(Service::parse("file").is_err(), "no near-misses");

    assert_eq!(Switch::parse("off"), Ok(Switch::Off));
    assert!(Switch::parse("no").is_err());

    assert_eq!(SyncMode::parse("auto"), Ok(SyncMode::Auto));
    assert_eq!(SyncMode::parse("manual"), Ok(SyncMode::Manual));
    assert!(SyncMode::parse("automatic").is_err());
}