wsb-daemon 0.258.6441

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
//! What the machine database keeps about services, modes and dismissals.
//!
//! Every one of these is a fact that decides whether files move. A default
//! that silently read the wrong way, or a column an upgrade never added, would
//! not fail anything visibly — it would carry a working tree somebody had
//! switched off, or refuse to carry one they had switched on.

use super::*;
use wsb::peer::identity::{Fingerprint, PairedPeer};

fn store() -> (tempfile::TempDir, MachineStore) {
    let dir = tempfile::tempdir().expect("tempdir");
    let store = MachineStore::open(dir.path()).expect("a machine store");
    (dir, store)
}

fn peer_id() -> PeerId {
    PeerId::parse("aaaa1111bbbb2222").expect("a peer id")
}

fn workspace_id() -> WorkspaceId {
    WorkspaceId::parse("cccc3333@main").expect("a workspace id")
}

fn pair(store: &MachineStore) -> PeerId {
    let id = peer_id();
    store
        .add_peer(&PairedPeer {
            id: id.clone(),
            name: "laptop".to_string(),
            public_key: "key".to_string(),
            fingerprint: Fingerprint::from_stored("ff".to_string()),
            address: "127.0.0.1:7420".to_string(),
            paired_at: 1,
        })
        .expect("the peer is paired");
    id
}

/// TEST10147: a machine somebody paired can do everything until they say
/// otherwise, and a switch moves both ways.
#[test]
fn test10147_services_default_on_and_switch_both_ways() {
    let (_dir, store) = store();
    let peer = pair(&store);

    // No rows at all — which is what pairing writes — reads as on.
    for service in Service::BUILT_IN {
        assert_eq!(
            store.service(&peer, service.clone()).expect("a switch"),
            Switch::On,
            "{service} must be available to a machine somebody paired"
        );
    }

    store
        .set_service(&peer, Service::Files, Switch::Off, 10)
        .expect("files off");
    assert_eq!(store.service(&peer, Service::Files).expect("files"), Switch::Off);

    // And back, which is the half that a one-way switch would fail at.
    store
        .set_service(&peer, Service::Files, Switch::On, 11)
        .expect("files on");
    assert_eq!(store.service(&peer, Service::Files).expect("files"), Switch::On);
}

/// TEST10148: a relationship carries nothing by itself until somebody asks it
/// to, and the mode survives a restart.
///
/// The default is the whole point of the mode. It is read from a row that may
/// not exist, from a column that may not have existed when the row was
/// written, and both must come back manual.
#[test]
fn test10148_the_mode_defaults_to_manual_and_persists() {
    let (dir, store) = store();
    let peer = pair(&store);
    let workspace = workspace_id();

    assert_eq!(
        store.mode(&peer, &workspace).expect("a mode"),
        SyncMode::Manual,
        "a relationship nobody has configured must not carry files by itself"
    );

    // Enabling the workspace does not change how it is carried: the two are
    // separate decisions, and this is the one that was never asked.
    store
        .set_desired(&peer, &workspace, Desired::Enabled, 10)
        .expect("enabled");
    assert_eq!(store.mode(&peer, &workspace).expect("a mode"), SyncMode::Manual);

    store
        .set_mode(&peer, &workspace, SyncMode::Auto, 11)
        .expect("auto");
    assert_eq!(store.mode(&peer, &workspace).expect("a mode"), SyncMode::Auto);
    assert_eq!(
        store.desired(&peer, &workspace).expect("desired"),
        Desired::Enabled,
        "setting the mode must not disturb whether the workspace is carried"
    );

    // Reopened: a mode kept in memory would pass everything above.
    drop(store);
    let store = MachineStore::open(dir.path()).expect("reopened");
    assert_eq!(store.mode(&peer, &workspace).expect("a mode"), SyncMode::Auto);
}

/// TEST10149: choosing a mode before choosing to carry the workspace works.
///
/// The workbench has two controls and no imposed order. Refusing the mode
/// until the relationship existed would make one of them fail depending on
/// which was touched first — and the failure would be a silent no-op.
#[test]
fn test10149_a_mode_can_be_set_before_the_workspace_is_carried() {
    let (_dir, store) = store();
    let peer = pair(&store);
    let workspace = workspace_id();

    store
        .set_mode(&peer, &workspace, SyncMode::Auto, 10)
        .expect("auto before enabled");
    assert_eq!(store.mode(&peer, &workspace).expect("a mode"), SyncMode::Auto);
    assert_eq!(
        store.desired(&peer, &workspace).expect("desired"),
        Desired::Disabled,
        "choosing how it would be carried must not start carrying it"
    );

    store
        .set_desired(&peer, &workspace, Desired::Enabled, 11)
        .expect("enabled");
    assert_eq!(
        store.mode(&peer, &workspace).expect("a mode"),
        SyncMode::Auto,
        "enabling the workspace must keep the mode already chosen"
    );
}

/// TEST10150: a dismissal is about two versions, not about a path.
///
/// It is stored with the hashes it was made at so the coordinator can tell
/// "this is the difference I was shown" from "this path differs again". The
/// store's job is to hand back exactly what was recorded, including the absent
/// side of a file only one machine has.
#[test]
fn test10150_a_dismissal_records_the_versions_it_was_about() {
    let (_dir, store) = store();
    let peer = pair(&store);
    let workspace = workspace_id();
    let repository = RepositoryId::root();

    store
        .dismiss(&peer, &workspace, &repository, "notes.md", Some("aaa"), None, 10)
        .expect("dismissed");

    let held = store
        .dismissals(&peer, &workspace, &repository)
        .expect("dismissals");
    assert_eq!(
        held.get("notes.md"),
        Some(&(Some("aaa".to_string()), None)),
        "the absent side must come back absent rather than as an empty string"
    );

    // Dismissing again at different versions replaces the first: it is one
    // statement about one path, made afresh each time.
    store
        .dismiss(&peer, &workspace, &repository, "notes.md", Some("bbb"), Some("ccc"), 11)
        .expect("dismissed again");
    let held = store
        .dismissals(&peer, &workspace, &repository)
        .expect("dismissals");
    assert_eq!(
        held.get("notes.md"),
        Some(&(Some("bbb".to_string()), Some("ccc".to_string())))
    );

    store
        .clear_dismissal(&peer, &workspace, &repository, "notes.md")
        .expect("cleared");
    assert!(store
        .dismissals(&peer, &workspace, &repository)
        .expect("dismissals")
        .is_empty());
}

/// TEST10151: forgetting a peer takes its services with it.
///
/// A re-pairing under the same name must start from the defaults. Leaving a
/// switched-off service behind would mean a machine somebody re-paired
/// deliberately sat there doing nothing, with nothing on screen to say why.
#[test]
fn test10151_forgetting_a_peer_forgets_what_it_was_allowed_to_do() {
    let (_dir, store) = store();
    let peer = pair(&store);

    store
        .set_service(&peer, Service::Files, Switch::Off, 10)
        .expect("files off");
    assert!(store.remove_peer(&peer).expect("removed"));

    let peer = pair(&store);
    assert_eq!(
        store.service(&peer, Service::Files).expect("files"),
        Switch::On,
        "a re-paired machine must start from the defaults"
    );
}

/// TEST10152: a database written before the mode existed keeps its peers and
/// reads as manual.
///
/// This database holds the pinned public key of every machine this one trusts.
/// `CREATE TABLE IF NOT EXISTS` does nothing for a table that is already
/// there, so adding a column to `relationships` had to be an ALTER — and the
/// alternative somebody reaches for, recreating the table, would end every
/// trust relationship on the machine.
///
/// Manual for the rows that predate the column, deliberately: a machine that
/// upgrades must not start moving files while nobody is looking.
#[test]
fn test10152_a_database_from_before_the_mode_upgrades_in_place() {
    let dir = tempfile::tempdir().expect("tempdir");

    // The schema as it was: relationships with no mode column.
    {
        let connection =
            rusqlite::Connection::open(dir.path().join("machine.db")).expect("the old database");
        connection
            .execute_batch(
                r#"
                CREATE TABLE peers (
                    peer_id     TEXT PRIMARY KEY NOT NULL,
                    name        TEXT NOT NULL,
                    public_key  TEXT NOT NULL,
                    fingerprint TEXT NOT NULL,
                    address     TEXT NOT NULL,
                    paired_at   INTEGER NOT NULL,
                    last_seen   INTEGER
                );
                CREATE TABLE relationships (
                    peer_id      TEXT NOT NULL REFERENCES peers(peer_id) ON DELETE CASCADE,
                    workspace_id TEXT NOT NULL,
                    desired      TEXT NOT NULL,
                    enabled_at   INTEGER NOT NULL,
                    PRIMARY KEY (peer_id, workspace_id)
                );
                INSERT INTO peers VALUES
                    ('aaaa1111bbbb2222', 'laptop', 'key', 'ff', '127.0.0.1:7420', 1, NULL);
                INSERT INTO relationships VALUES
                    ('aaaa1111bbbb2222', 'cccc3333@main', 'enabled', 2);
                "#,
            )
            .expect("the old schema");
    }

    let store = MachineStore::open(dir.path()).expect("the store opens an older database");
    let peer = peer_id();
    let workspace = workspace_id();

    let peers = store.peers().expect("peers");
    assert_eq!(peers.len(), 1, "the upgrade must not lose a pinned identity");
    assert_eq!(peers[0].public_key, "key", "the pinned key must survive");

    assert_eq!(
        store.desired(&peer, &workspace).expect("desired"),
        Desired::Enabled,
        "what the operator asked for must survive the upgrade"
    );
    assert_eq!(
        store.mode(&peer, &workspace).expect("a mode"),
        SyncMode::Manual,
        "a relationship that predates the column must not start carrying files by itself"
    );

    // And the new column is writable, rather than merely readable.
    store
        .set_mode(&peer, &workspace, SyncMode::Auto, 10)
        .expect("auto");
    assert_eq!(store.mode(&peer, &workspace).expect("a mode"), SyncMode::Auto);

    // Opening it again is a no-op rather than a second ALTER.
    drop(store);
    let store = MachineStore::open(dir.path()).expect("reopened");
    assert_eq!(store.mode(&peer, &workspace).expect("a mode"), SyncMode::Auto);
}