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 a machine answers a peer, where the answer decides whether files move.
//!
//! These are about the questions asked BEFORE anything is written or deleted.
//! A wrong answer here is not a wrong display: it is a file that goes, on a
//! machine whose owner never asked for it to.

use super::{answer, Machine};
use crate::peer::store::MachineStore;
use wsb::peer::converge::{Baseline, Entry, EntryKind};
use wsb::peer::identity::{Fingerprint, PairedPeer};
use crate::peer::coordinator::LocalWorkspace;
use wsb::peer::model::{PeerId, RepositoryId, WorkspaceId};
use wsb::peer::protocol::{baseline_digest, RefusalCode, Request, Response, WorkspaceSummary};
use std::sync::Arc;

/// A machine that holds one workspace and a real store.
struct Held {
    store: Arc<MachineStore>,
    workspace: Arc<LocalWorkspace>,
    staging: std::path::PathBuf,
}

impl Machine for Held {
    fn workspaces(&self) -> Vec<WorkspaceSummary> {
        Vec::new()
    }
    fn workspace(&self, id: &WorkspaceId) -> Result<Arc<LocalWorkspace>, String> {
        (*id == self.workspace.id)
            .then(|| Arc::clone(&self.workspace))
            .ok_or_else(|| "this machine does not have that worktree open".to_string())
    }
    fn store(&self) -> Arc<MachineStore> {
        Arc::clone(&self.store)
    }
    fn staging_root(&self) -> std::path::PathBuf {
        self.staging.clone()
    }
    fn plugins(&self) -> Arc<crate::plugin::PluginRegistry> {
        // No plugin is enabled in a directory nobody wrote a list into.
        Arc::new(crate::plugin::PluginRegistry::new(self.staging.clone()))
    }
}

fn entry(bytes: &[u8]) -> Entry {
    Entry {
        kind: EntryKind::File,
        hash: wsb::peer::converge::hash_bytes(bytes),
        size: bytes.len() as u64,
        executable: false,
    }
}

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

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

fn held(baseline: &Baseline) -> (tempfile::TempDir, Held) {
    let dir = tempfile::tempdir().expect("a directory");
    let store = MachineStore::open(dir.path()).expect("a machine store");
    store
        .add_peer(&PairedPeer {
            id: peer_id(),
            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 pairs");
    store
        .put_baseline(&peer_id(), &workspace_id(), &RepositoryId::root(), baseline)
        .expect("the baseline is stored");

    let workspace = Arc::new(LocalWorkspace {
        id: workspace_id(),
        repositories: Vec::new(),
        policy: wsb::peer::scan::Policy::default(),
    });
    let machine = Held {
        store: Arc::new(store),
        workspace,
        staging: dir.path().join("staging"),
    };
    (dir, machine)
}

fn confirm(digest: &str) -> Request {
    Request::Confirm {
        workspace: workspace_id(),
        repository: RepositoryId::root(),
        baseline_digest: digest.to_string(),
    }
}

/// TEST11587: a machine confirms the baseline it actually holds.
///
/// The question the asker puts before it changes its own tree. Answering yes
/// to any digest would make the question decorative, and the deletions it
/// guards would go through on nobody's authority.
#[test]
fn test11587_a_matching_baseline_is_confirmed() {
    let mut baseline = Baseline::new();
    baseline.insert("carried.txt".to_string(), entry(b"carried\n"));
    let (_dir, machine) = held(&baseline);

    let response = answer(&machine, &peer_id(), confirm(&baseline_digest(&baseline)));
    assert!(
        matches!(response, Response::Acknowledged),
        "a machine would not confirm the baseline it holds: {response:?}"
    );
}

/// TEST11588: a baseline that has drifted is refused, and says which kind.
///
/// This is the refusal that stops the data loss. The asker has planned
/// deletions from its own copy of the baseline; if this machine's copy differs,
/// those deletions were decided from a state this machine never agreed to, and
/// applying them removes files this machine never asked about.
///
/// `BaselineMismatch` specifically, because that is the code the asker turns
/// into a reseed. Any other refusal leaves it to try again against the same
/// disagreeing copy, for ever.
#[test]
fn test11588_a_drifted_baseline_is_refused_so_the_asker_reseeds() {
    let mut baseline = Baseline::new();
    baseline.insert("carried.txt".to_string(), entry(b"carried\n"));
    let (_dir, machine) = held(&baseline);

    // What the asker holds: one more path than this machine ever agreed to.
    // Exactly the shape of the drift that deleted a file from the machine that
    // created it.
    let mut asker = baseline.clone();
    asker.insert("after-commit.txt".to_string(), entry(b"carried again\n"));

    let response = answer(&machine, &peer_id(), confirm(&baseline_digest(&asker)));
    match response {
        Response::Refused { code, .. } => assert_eq!(
            code,
            RefusalCode::BaselineMismatch,
            "refused with a code that does not make the asker reseed"
        ),
        other => panic!("a drifted baseline was confirmed: {other:?}"),
    }
}

/// TEST11589: a machine confirms nothing about a workspace it does not hold.
///
/// An empty baseline and an absent workspace have the same digest, so a
/// machine that skipped this check would confirm a plan for a workspace it has
/// never seen — and the asker would then delete against that confirmation.
#[test]
fn test11589_an_unknown_workspace_is_not_confirmed() {
    let (_dir, machine) = held(&Baseline::new());
    let other = WorkspaceId::parse("dddd4444@main").expect("another workspace id");

    let response = answer(
        &machine,
        &peer_id(),
        Request::Confirm {
            workspace: other,
            repository: RepositoryId::root(),
            baseline_digest: baseline_digest(&Baseline::new()),
        },
    );
    match response {
        Response::Refused { code, .. } => assert_eq!(code, RefusalCode::UnknownWorkspace),
        other => panic!("a workspace this machine does not have was confirmed: {other:?}"),
    }
}

/// TEST20043: a machine that has not enabled continuity gives no files away.
///
/// It already refused to take them. Giving them was unguarded, so a peer
/// whose pass began before this machine paused still fetched a file written
/// after the pause, and the file crossed while the relationship was paused.
#[test]
fn test20043_files_are_given_only_while_continuity_is_enabled() {
    let (_dir, machine) = held(&Baseline::new());
    let fetch = || Request::Fetch {
        workspace: workspace_id(),
        repository: RepositoryId::root(),
        epoch: wsb::peer::compatibility::Epoch {
            repository: RepositoryId::root(),
            root_commit: "0".repeat(40),
            branch: Some("main".to_string()),
            head: Some("0".repeat(40)),
            git_state: wsb::peer::compatibility::GitState::Settled,
        },
        paths: vec!["notes.md".to_string()],
    };
    let refusal = |response: Response| match response {
        Response::Refused { code, .. } => code,
        other => panic!("a fetch was answered: {other:?}"),
    };

    for desired in [wsb::peer::model::Desired::Paused, wsb::peer::model::Desired::Disabled] {
        machine.store.set_desired(&peer_id(), &workspace_id(), desired, 2).expect("set");
        assert_eq!(refusal(answer(&machine, &peer_id(), fetch())), RefusalCode::NotEnabled, "{desired:?}");
    }
    // Enabled, it goes on to the repository — which this machine does not
    // have, so that is the refusal now.
    machine
        .store
        .set_desired(&peer_id(), &workspace_id(), wsb::peer::model::Desired::Enabled, 3)
        .expect("set");
    assert_eq!(refusal(answer(&machine, &peer_id(), fetch())), RefusalCode::UnknownRepository);
}

/// A machine holding one real repository, committed with `kept.txt`, with
/// continuity enabled, and the epoch a peer would have learned from it.
fn repository(baseline: &Baseline) -> (tempfile::TempDir, Held, wsb::peer::compatibility::Epoch) {
    let (dir, mut machine) = held(baseline);
    let workdir = dir.path().join("workspace");
    std::fs::create_dir_all(&workdir).expect("a working tree");
    let git = |args: &[&str]| {
        let status = std::process::Command::new("git")
            .args(["-c", "user.name=t", "-c", "user.email=t@t", "-c", "init.defaultBranch=main"])
            .args(args)
            .current_dir(&workdir)
            .status()
            .expect("git runs");
        assert!(status.success(), "git {args:?}");
    };
    git(&["init", "-q"]);
    std::fs::write(workdir.join("kept.txt"), "base\n").expect("write");
    git(&["add", "kept.txt"]);
    git(&["commit", "-q", "-m", "base"]);

    let git_dir = workdir.join(".git");
    let epoch = wsb::peer::compatibility::RepositoryFacts::read(RepositoryId::root(), &workdir, &git_dir)
        .expect("the repository reads")
        .epoch();
    machine.workspace = Arc::new(LocalWorkspace {
        id: workspace_id(),
        repositories: vec![crate::peer::coordinator::LocalRepository {
            id: RepositoryId::root(),
            workdir,
            git_dir,
        }],
        policy: wsb::peer::scan::Policy::default(),
    });
    machine
        .store
        .set_desired(&peer_id(), &workspace_id(), wsb::peer::model::Desired::Enabled, 2)
        .expect("continuity is enabled");
    (dir, machine, epoch)
}

fn write_of(path: &str, bytes: &[u8]) -> wsb::peer::protocol::Write {
    wsb::peer::protocol::Write {
        path: path.to_string(),
        entry: entry(bytes),
        content: wsb::peer::identity::base64_encode(bytes),
    }
}

fn applied(response: Response) -> Vec<String> {
    match response {
        Response::Applied { applied } => applied,
        other => panic!("the transfer was not applied: {other:?}"),
    }
}

/// TEST20047: a file edited after the peer read it is not replaced.
///
/// The peer planned from this machine's report, which said `kept.txt` held the
/// agreed version. Somebody saved it again before the transfer arrived. The
/// epoch is git's state and had not moved, so the peer's version went over the
/// edit, and the two machines then agreed on it: the edit was gone and no
/// conflict was ever recorded. A path that still holds what the plan found
/// lands, in the same transfer.
#[test]
fn test20047_a_file_edited_after_the_plan_read_it_is_not_replaced() {
    let mut baseline = Baseline::new();
    baseline.insert("kept.txt".to_string(), entry(b"base\n"));
    let (_dir, machine, epoch) = repository(&baseline);
    let workdir = machine.workspace.repositories[0].workdir.clone();
    std::fs::write(workdir.join("kept.txt"), "laptop\n").expect("the edit after the report");

    let response = answer(
        &machine,
        &peer_id(),
        Request::Apply {
            workspace: workspace_id(),
            repository: RepositoryId::root(),
            epoch,
            writes: vec![write_of("kept.txt", b"desktop\n"), write_of("fresh.txt", b"new\n")],
            deletions: Vec::new(),
            // What this machine reported: the agreed `kept.txt`, no `fresh.txt`.
            found: [("kept.txt".to_string(), entry(b"base\n"))].into_iter().collect(),
            baseline_digest: baseline_digest(&baseline),
        },
    );

    assert_eq!(applied(response), vec!["fresh.txt".to_string()], "only the untouched path is reported");
    assert_eq!(
        std::fs::read_to_string(workdir.join("kept.txt")).expect("read"),
        "laptop\n",
        "an edit made after the report was replaced by the peer's version"
    );
    assert_eq!(std::fs::read_to_string(workdir.join("fresh.txt")).expect("read"), "new\n");
}

/// TEST20048: a file edited after the peer read it is not deleted.
///
/// The peer deleted the version both machines had agreed on. The one here is
/// newer than that, and deleting it would take work nobody on either machine
/// has seen. A deletion of a file that still holds the agreed version goes.
#[test]
fn test20048_a_file_edited_after_the_plan_read_it_is_not_deleted() {
    let mut baseline = Baseline::new();
    baseline.insert("kept.txt".to_string(), entry(b"base\n"));
    baseline.insert("gone.txt".to_string(), entry(b"agreed\n"));
    let (_dir, machine, epoch) = repository(&baseline);
    let workdir = machine.workspace.repositories[0].workdir.clone();
    std::fs::write(workdir.join("kept.txt"), "edited\n").expect("the edit after the report");
    std::fs::write(workdir.join("gone.txt"), "agreed\n").expect("an untouched file");

    let response = answer(
        &machine,
        &peer_id(),
        Request::Apply {
            workspace: workspace_id(),
            repository: RepositoryId::root(),
            epoch,
            writes: Vec::new(),
            deletions: vec!["kept.txt".to_string(), "gone.txt".to_string()],
            found: [
                ("kept.txt".to_string(), entry(b"base\n")),
                ("gone.txt".to_string(), entry(b"agreed\n")),
            ]
            .into_iter()
            .collect(),
            baseline_digest: baseline_digest(&baseline),
        },
    );

    assert_eq!(applied(response), vec!["gone.txt".to_string()]);
    assert_eq!(
        std::fs::read_to_string(workdir.join("kept.txt")).expect("an edited file was deleted"),
        "edited\n"
    );
    assert!(!workdir.join("gone.txt").exists(), "the agreed file was not deleted");
}