wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
use super::*;

fn project(owner: &str, repo: &str) -> ProjectId {
    ProjectId {
        owner: owner.to_string(),
        repo: repo.to_string(),
    }
}

/// TEST20109: every kind of ref is written and read back the same, and a ref
/// that does not say exactly one thing is refused.
///
/// A ref is committed and read on every machine; one read as something else
/// than it was written would link two other things, and nobody would see it.
#[test]
fn test20109_refs_round_trip_and_ambiguous_ones_are_refused() {
    let every = [
        "board:jowharshamshiri/13",
        "card:machinefabric/7/PVTI_lADODw7C",
        "issue:jowharshamshiri/wsb#12",
        "repo:jowharshamshiri/wsb",
        "repo:machine-fabric/wsb_core.rs",
        "project:jowharshamshiri/names",
        "worktree:jowharshamshiri/wsb/model",
        "path:jowharshamshiri/wsb/wsb/src/lib.rs",
        "machine:pve",
        "note:20261024-213000-a1b2",
        "session:claude/5b0c9a8e-1d2f",
        "message:claude/5b0c9a8e-1d2f/0f1e2d3c",
        "url:https://example.com/a?b=c",
    ];
    for text in every {
        let parsed: Ref = text.parse().unwrap_or_else(|e| panic!("{text}: {e:#}"));
        assert_eq!(parsed.to_string(), text, "{text} did not come back as written");
        let json = serde_json::to_string(&parsed).unwrap();
        assert_eq!(serde_json::from_str::<Ref>(&json).unwrap(), parsed);
    }
    assert_eq!(
        "path:o/r/a/b/c.rs".parse::<Ref>().unwrap(),
        Ref::Path { project: project("o", "r"), path: "a/b/c.rs".to_string() }
    );
    assert_eq!(
        "project:o/r".parse::<Ref>().unwrap().project(),
        Some(&project("o", "r")),
        "a project ref is in its own project"
    );

    for bad in [
        "board:owner",
        "board:owner/x",
        "card:o/1",
        "issue:o/r12",
        "project:o",
        "project:o/r/x",
        "worktree:o/r/-bad",
        "project:--all/r",
        "project:-o/r",
        "project:o-/r",
        "project:o--p/r",
        "repo:o/-r",
        "repo:o/..",
        "issue:o_p/r#1",
        "board:own.er/1",
        "path:o/r",
        "path:o/r/../secret",
        "path:o/r/a//b",
        "machine:two words",
        "note:Not_An_Id",
        "url:ftp://x",
        "url:https://a b",
        "colour:red",
        "nokind",
    ] {
        assert!(bad.parse::<Ref>().is_err(), "{bad} was accepted");
    }
}

/// TEST20110: a note is written as a file people read and read back exactly,
/// and a header wsb did not write is refused rather than half understood.
#[test]
fn test20110_notes_round_trip_and_a_strange_header_is_refused() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    let note = Note::new("Why: the bridge retries", "Because peers sleep.\n\nTwice.", "pve").unwrap();
    assert!(note.id.len() == "20261024-213000-a1b2".len(), "{}", note.id);
    let path = note.write(root).unwrap();
    assert_eq!(path, root.join(".wsb/notes").join(format!("{}.md", note.id)));
    let text = std::fs::read_to_string(&path).unwrap();
    assert!(text.starts_with("---\ntitle: Why: the bridge retries\n"), "{text}");
    assert!(text.ends_with("Twice.\n"), "{text:?}");
    let read = Note::read(root, &note.id).unwrap().expect("the note is there");
    assert_eq!(read.body, "Because peers sleep.\n\nTwice.\n");
    assert_eq!((read.title.as_str(), read.machine.as_str()), ("Why: the bridge retries", "pve"));
    assert_eq!(Note::all(root).unwrap().len(), 1);

    // A note with no body is a note too.
    let empty = Note::parse("20261024-213000-0000", "---\ntitle: t\ncreated: 2026-10-24T21:30:00Z\nmachine: m\n---\n");
    assert_eq!(empty.unwrap().body, "");

    let header = |lines: &str| format!("---\n{lines}\n---\nbody\n");
    for (text, why) in [
        (header("title: t\ncreated: 2026-10-24T21:30:00Z"), "no machine"),
        (header("title: t\ncreated: yesterday\nmachine: m"), "a time that is not one"),
        (header("title: t\ntitle: u\ncreated: 2026-10-24T21:30:00Z\nmachine: m"), "a key twice"),
        (header("title: t\ncreated: 2026-10-24T21:30:00Z\nmachine: m\ncolour: red"), "an unknown key"),
        (header("title:  \ncreated: 2026-10-24T21:30:00Z\nmachine: m"), "an empty title"),
        ("title: t\n".to_string(), "no header"),
    ] {
        assert!(Note::parse("20261024-213000-0000", &text).is_err(), "{why} was accepted: {text:?}");
    }
    assert!(Note::new("a\ntwo-line title", "", "pve").is_err());
    assert!(Note::read(root, "../escape").is_err(), "an id that is a path was read");
}

/// TEST20111: the same link made twice is one file, an edited one is refused,
/// and the workshop's index holds each link once with every worktree that
/// holds it, each version of a note apart, and says what it could not read.
#[test]
fn test20111_links_are_one_file_per_link_and_the_index_merges_worktrees() {
    let workshop = tempfile::tempdir().unwrap();
    let main = workshop.path().join("main");
    let fix = workshop.path().join("fix");
    let broken = workshop.path().join("broken");
    for dir in [&main, &fix, &broken] {
        std::fs::create_dir_all(dir).unwrap();
    }
    let card: Ref = "card:jowharshamshiri/13/PVTI_x".parse().unwrap();
    let worktree: Ref = "worktree:jowharshamshiri/wsb/fix".parse().unwrap();
    let link = Link::new(card.clone(), worktree.clone(), None).unwrap();
    let first = link.write(&main).unwrap();
    assert_eq!(link.write(&main).unwrap(), first, "the same link made twice is a second file");
    assert_eq!(std::fs::read_dir(main.join(".wsb/links")).unwrap().count(), 1);
    link.write(&fix).unwrap();
    assert!(Link::new(card.clone(), card.clone(), None).is_err(), "a link from a thing to itself");
    assert!(Link::new(card.clone(), worktree.clone(), Some("Not A Label".into())).is_err());
    assert_ne!(
        Link::new(card.clone(), worktree.clone(), Some("implements".into())).unwrap().id(),
        link.id(),
        "a label is part of what a link says"
    );

    // One note, the same in two worktrees, then edited in one of them.
    let note = Note::new("Plan", "first\n", "pve").unwrap();
    note.write(&main).unwrap();
    note.write(&fix).unwrap();
    let mut edited = note.clone();
    edited.body = "second\n".to_string();
    edited.write(&fix).unwrap();

    // A link file edited by hand: its name no longer says what it holds.
    let other = Link::new(card.clone(), "machine:pve".parse().unwrap(), None).unwrap();
    let path = other.write(&broken).unwrap();
    std::fs::write(&path, serde_json::to_string(&link).unwrap()).unwrap();
    assert!(Link::all(&broken).is_err(), "a hand-edited link was read");

    let place = |root: &Path, name: &str| Place {
        workspace: "wsb".to_string(),
        worktree: name.to_string(),
        root: root.to_path_buf(),
    };
    let index = Index::of(vec![place(&main, "main"), place(&fix, "fix"), place(&broken, "broken")]);
    assert_eq!(index.links.len(), 1, "one link held twice is one link");
    assert_eq!(
        index.links[0].places.iter().map(|p| p.worktree.as_str()).collect::<Vec<_>>(),
        vec!["main", "fix"]
    );
    assert_eq!(index.links_at(&worktree).count(), 1);
    assert_eq!(index.links_at(&"machine:other".parse().unwrap()).count(), 0);
    let versions = index.note(&note.id);
    assert_eq!(versions.len(), 2, "two versions of a note were merged into one");
    assert!(versions.iter().any(|v| v.note.body == "first\n" && v.places[0].worktree == "main"));
    assert!(versions.iter().any(|v| v.note.body == "second\n" && v.places[0].worktree == "fix"));
    assert_eq!(index.problems.len(), 1, "{:?}", index.problems);
    assert!(index.problems[0].starts_with("wsb/broken:"), "{:?}", index.problems);
    assert_eq!(
        index.named_projects().into_iter().collect::<Vec<_>>(),
        vec![project("jowharshamshiri", "wsb")]
    );

    // Removal takes this checkout's copy, and refuses one it does not hold.
    remove_link(&main, &link).unwrap();
    assert!(remove_link(&main, &link).is_err(), "a link removed twice");
    remove_note(&main, &note.id).unwrap();
    assert!(Note::read(&main, &note.id).unwrap().is_none());
}