wsb-daemon 0.258.6441

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
use super::*;

/// TEST20032: a worktree's git state is read from what `git status` says, and
/// nothing is assumed.
///
/// The machine home shows every worktree's branch, whether it has work in it
/// and how far it is from its upstream. Each of those has a state git names
/// specially — a detached HEAD, a repository with no commit, a branch with no
/// upstream — and each must read as that state, not as a branch called
/// "(detached)" or a commit called "(initial)".
#[test]
fn test20032_git_state_is_read_from_porcelain_status() {
    let clean = parse_status(
        "# branch.oid 1234abcd\n# branch.head card/fix\n# branch.upstream origin/card/fix\n# branch.ab +2 -3\n",
    )
    .expect("read");
    assert_eq!(
        clean,
        GitState {
            branch: Some("card/fix".into()),
            head: Some("1234abcd".into()),
            dirty: 0,
            upstream: Some("origin/card/fix".into()),
            ahead: Some(2),
            behind: Some(3),
            last_commit_at: None,
        }
    );

    let detached = parse_status("# branch.oid 1234abcd\n# branch.head (detached)\n").expect("read");
    assert_eq!(detached.branch, None, "a detached HEAD is no branch");
    assert_eq!(detached.upstream, None);
    assert_eq!((detached.ahead, detached.behind), (None, None), "no upstream, no distance");

    let unborn = parse_status("# branch.oid (initial)\n# branch.head main\n").expect("read");
    assert_eq!(unborn.head, None, "no commit yet");

    for change in [
        "1 .M N... 100644 100644 100644 aaaa bbbb src/lib.rs",
        "? notes.md",
        "u UU N... 100644 100644 100644 100644 aaaa bbbb cccc both.rs",
        "1 .M S.M. 160000 160000 160000 aaaa bbbb sub",
    ] {
        let state = parse_status(&format!("# branch.oid aaaa\n# branch.head main\n{change}\n"))
            .expect("read");
        assert_eq!(state.dirty, 1, "{change:?} is one path of uncommitted work");
    }
    let two = parse_status("# branch.oid aaaa\n# branch.head main\n? a\n? b\n").expect("read");
    assert_eq!(two.dirty, 2, "each changed path is counted");

    assert!(parse_status("# branch.ab +x -1\n").is_err(), "a malformed distance is refused");
}

/// TEST20033: `prj`'s slots are filed under the worktree they run in, and a
/// listing that does not read is an error.
///
/// The slots shown on a worktree's row are the ones `prj` runs there; a slot
/// filed under the wrong worktree is a session somebody resumes in the wrong
/// checkout. A listing that cannot be read is said so beside the worktrees —
/// an empty list would say no session is running anywhere.
///
/// The listing is in the shape `prj ls --json` prints (toolset's
/// `platforms/common/prj.py`, `cmd_ls`): an object holding the slots and the
/// untracked sessions, each row with every field prj writes.
#[test]
fn test20033_slots_are_filed_by_worktree() {
    let listing = r#"{
      "slots": [
        {"group": "main", "position": 1, "name": "fixer", "key": "k1", "project": "alpha",
         "worktree": "fix", "target": "alpha/fix", "status": "running", "detail": null,
         "session_id": "s1", "activity": "now", "activity_at": 1700000000,
         "last_exit": {"at": "2026-10-18T08:42:42+00:00", "code": 143, "reason": "signal"}},
        {"group": "main", "position": 2, "name": "main", "key": "k2", "project": "alpha",
         "worktree": "main", "target": "alpha", "status": "parked", "detail": "stopped",
         "session_id": null, "activity": "-", "activity_at": null, "last_exit": null},
        {"group": "main", "position": 3, "name": "other", "key": "k3", "project": "alpha",
         "worktree": "fix", "target": "alpha/fix", "status": "closed", "detail": null,
         "session_id": "s3", "activity": "3d", "activity_at": 1, "last_exit": null}
      ],
      "untracked": [
        {"pid": 4242, "session_id": "s9", "cwd": "/prj/alpha/main-worktree", "status": "idle"}
      ]
    }"#;
    let slots = parse_slots(listing).expect("read");
    let fix: Vec<&str> = slots[&("alpha".to_string(), "fix".to_string())]
        .iter()
        .map(|slot| slot.name.as_str())
        .collect();
    assert_eq!(fix, ["fixer", "other"]);
    let main = &slots[&("alpha".to_string(), "main".to_string())];
    assert_eq!(main.len(), 1);
    assert_eq!(main[0].status, "parked");
    assert_eq!(main[0].activity_at, None);
    assert_eq!(main[0].detail.as_deref(), Some("stopped"));

    assert_eq!(
        slots.values().map(Vec::len).sum::<usize>(),
        3,
        "an untracked session is no slot"
    );

    assert!(parse_slots("not json").is_err());
    assert!(
        parse_slots(r#"{"slots": [{"name": "x"}], "untracked": []}"#).is_err(),
        "a row that does not say where it runs is refused, not filed nowhere"
    );
    assert!(
        parse_slots(r#"{"untracked": []}"#).is_err(),
        "a listing without slots is refused, not read as none running"
    );
}

/// TEST20034: what a page asks `prj` to act on is checked before it becomes
/// an argument.
///
/// Every action runs `prj` with words from a request. A name that is an option
/// (`--all`), carries a separator or is empty would make `prj` do something
/// nobody clicked.
#[test]
fn test20034_prj_arguments_are_checked() {
    for good in ["alpha", "alpha/fix", "a.b/c_d-2"] {
        assert_eq!(target(good).expect(good), good);
    }
    for bad in ["", "--all", "alpha/", "/fix", "alpha/fix/x", "alpha fix"] {
        assert!(target(bad).is_err(), "{bad:?} was taken as a target");
    }
    for good in ["main", "origin/main", "v1.2.3", "1234abcd"] {
        assert_eq!(git_ref(good).expect(good), good);
    }
    for bad in ["", "-b", "--force", "a b", "a\nb"] {
        assert!(git_ref(bad).is_err(), "{bad:?} was taken as a ref");
    }
    assert!(slot_name("--all").is_err());
    assert!(slot_name("fixer").is_ok());
}

/// TEST20044: pages hear about the projects only when the listing changes.
///
/// The listing is read again every half minute and on every event that might
/// move it, and most readings find nothing new. Each page that heard about
/// every reading would refetch the whole machine for nothing; one that heard
/// about none would never see a worktree appear.
#[tokio::test]
async fn test20044_a_listing_is_announced_only_when_it_changes() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let machine = MachineState::new(
        dir.path().to_path_buf(),
        std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
        0,
        std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
        0,
    )
    .expect("a machine");
    let listing = Listing::new(None);
    let mut heard = listing.changed.subscribe();

    let first = listing.read(&machine).await;
    assert!(first.as_ref().as_ref().expect("read").projects.is_empty());
    assert!(heard.try_recv().is_ok(), "the first reading is news");
    listing.read(&machine).await;
    assert!(heard.try_recv().is_err(), "the same reading again is not");

    let elsewhere = tempfile::tempdir().expect("a checkout");
    machine
        .store
        .register_worktree(&wsb::daemon::RegisteredWorktree {
            id: wsb::daemon::WorktreeId::parse("~0123456789abcdef").expect("an id"),
            project: None,
            name: "notes".to_string(),
            workspace: None,
            super_root: elsewhere.path().to_path_buf(),
            root_commit: None,
            last_seen: 0,
            available: true,
        })
        .expect("recorded");
    let after = listing.read(&machine).await;
    let standalone = &after.as_ref().as_ref().expect("read").standalone;
    assert_eq!(standalone.len(), 1);
    assert_eq!(standalone[0].name, "notes");
    assert!(
        standalone[0].error.is_some(),
        "a folder that is not a repository says so rather than reading as clean"
    );
    assert!(heard.try_recv().is_ok(), "a worktree appearing is news");
    assert_eq!(*listing.get(&machine).await, *after, "a page is served the reading held");
}