worktrunk 0.42.0

A CLI for Git worktree management, designed for parallel AI agent workflows
Documentation
use crate::common::{TestRepo, list_snapshots, repo, setup_snapshot_settings};
use insta::Settings;
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::process::Command;

fn snapshot_list(test_name: &str, repo: &TestRepo) {
    run_snapshot(
        setup_snapshot_settings(repo),
        test_name,
        list_snapshots::command(repo, repo.root_path()),
    );
}

fn snapshot_list_with_width(test_name: &str, repo: &TestRepo, width: usize) {
    run_snapshot(
        setup_snapshot_settings(repo),
        test_name,
        list_snapshots::command_with_width(repo, width),
    );
}

fn run_snapshot(settings: Settings, test_name: &str, mut cmd: Command) {
    settings.bind(|| {
        assert_cmd_snapshot!(test_name, cmd);
    });
}

#[rstest]
fn test_short_branch_names_shorter_than_header(mut repo: TestRepo) {
    // Create worktrees with very short branch names (shorter than "Branch" header)
    repo.add_worktree("a");
    repo.add_worktree("bb");
    repo.add_worktree("ccc");

    snapshot_list("short_branch_names", &repo);
}

#[rstest]
fn test_long_branch_names_longer_than_header(mut repo: TestRepo) {
    // Create worktrees with very long branch names
    repo.add_worktree("very-long-feature-branch-name");
    repo.add_worktree("another-extremely-long-name-here");
    repo.add_worktree("short");

    snapshot_list("long_branch_names", &repo);
}

#[rstest]
fn test_unicode_branch_names_width_calculation(mut repo: TestRepo) {
    // Create worktrees with unicode characters that have different visual widths
    // Note: Git may have restrictions on branch names, so use valid characters
    repo.add_worktree("cafe");
    repo.add_worktree("naive");
    repo.add_worktree("resume");

    snapshot_list("unicode_branch_names", &repo);
}

#[rstest]
fn test_mixed_length_branch_names(mut repo: TestRepo) {
    // Mix of very short, medium, and very long branch names
    repo.add_worktree("x");
    repo.add_worktree("medium");
    repo.add_worktree("extremely-long-branch-name-that-might-cause-layout-issues");

    snapshot_list("mixed_length_branch_names", &repo);
}

// Column alignment tests with varying diff sizes
// (Merged from column_alignment.rs)

#[rstest]
fn test_column_alignment_varying_diff_widths(mut repo: TestRepo) {
    // Create worktrees with varying diff sizes to test alignment
    repo.add_worktree("feature-small");
    repo.add_worktree("feature-medium");
    repo.add_worktree("feature-large");

    // Add files to create diffs with different digit counts
    let small_path = repo.worktrees.get("feature-small").unwrap();
    for i in 0..5 {
        std::fs::write(small_path.join(format!("file{}.txt", i)), "content").unwrap();
    }

    let medium_path = repo.worktrees.get("feature-medium").unwrap();
    for i in 0..50 {
        std::fs::write(medium_path.join(format!("file{}.txt", i)), "content").unwrap();
    }

    let large_path = repo.worktrees.get("feature-large").unwrap();
    for i in 0..500 {
        std::fs::write(large_path.join(format!("file{}.txt", i)), "content").unwrap();
    }

    // Test at a width where Dirty column is visible
    snapshot_list_with_width("alignment_varying_diffs", &repo, 180);
}

#[rstest]
fn test_column_alignment_with_empty_diffs(mut repo: TestRepo) {
    // Mix of worktrees with and without diffs
    repo.add_worktree("no-changes");

    repo.add_worktree("with-changes");
    let changes_path = repo.worktrees.get("with-changes").unwrap();
    std::fs::write(changes_path.join("file.txt"), "content").unwrap();

    repo.add_worktree("also-no-changes");

    // Path column should align even when some rows have diffs and others don't
    snapshot_list_with_width("alignment_empty_diffs", &repo, 180);
}

#[rstest]
fn test_column_alignment_extreme_diff_sizes(mut repo: TestRepo) {
    // Create worktrees with extreme diff size differences
    repo.add_worktree("tiny");
    repo.add_worktree("huge");

    let tiny_path = repo.worktrees.get("tiny").unwrap();
    std::fs::write(tiny_path.join("file.txt"), "x").unwrap();

    let huge_path = repo.worktrees.get("huge").unwrap();
    // 200 files is enough to test alignment with 3-digit diff counts
    for i in 0..200 {
        std::fs::write(huge_path.join(format!("file{}.txt", i)), "content").unwrap();
    }

    snapshot_list_with_width("alignment_extreme_diffs", &repo, 180);
}

// Width survey: visualize column layout at various terminal widths.
// These snapshots show what columns are visible and how branch names render
// at each size. The picker in Right layout gets terminal_width/2, so
// "picker at 80 cols" = list at 40 cols.
//
// Context: https://github.com/max-sixty/worktrunk/pull/1564

#[rstest]
fn test_width_survey_typical_branches(mut repo: TestRepo) {
    // Realistic mix: short default branch + medium feature branches
    repo.add_worktree("feature/auth-redesign");
    repo.add_worktree("fix/login-timeout");
    repo.add_worktree("chore/update-deps");

    // Add some dirty files so status columns have data
    let auth_path = repo.worktrees.get("feature/auth-redesign").unwrap();
    std::fs::write(auth_path.join("auth.rs"), "changed").unwrap();
    std::fs::write(auth_path.join("tests.rs"), "changed").unwrap();

    let fix_path = repo.worktrees.get("fix/login-timeout").unwrap();
    std::fs::write(fix_path.join("timeout.rs"), "changed").unwrap();

    for width in [30, 40, 50, 60, 80, 100, 120] {
        snapshot_list_with_width(&format!("width_survey_typical_{width}"), &repo, width);
    }
}

#[rstest]
fn test_width_survey_long_branches(mut repo: TestRepo) {
    // Long branch names that stress the allocator at narrow widths
    repo.add_worktree("feature/implement-oauth2-social-login");
    repo.add_worktree("fix/database-connection-pool-exhaustion");
    repo.add_worktree("short");

    let long_path = repo
        .worktrees
        .get("feature/implement-oauth2-social-login")
        .unwrap();
    std::fs::write(long_path.join("oauth.rs"), "changed").unwrap();

    for width in [30, 40, 50, 60, 80, 100, 120] {
        snapshot_list_with_width(&format!("width_survey_long_{width}"), &repo, width);
    }
}

#[rstest]
fn test_width_survey_picker_equivalent(mut repo: TestRepo) {
    // Simulate what the picker sees in Right layout: half the terminal width.
    // "picker_right_80" = picker list pane when terminal is 80 cols (list gets 40).
    repo.add_worktree("feature/auth-redesign");
    repo.add_worktree("fix/login-timeout");
    repo.add_worktree("chore/update-deps");

    let auth_path = repo.worktrees.get("feature/auth-redesign").unwrap();
    std::fs::write(auth_path.join("auth.rs"), "changed").unwrap();

    // Right layout: list gets terminal_width / 2
    for terminal_width in [60, 80, 100, 120, 160] {
        let list_width = terminal_width / 2;
        snapshot_list_with_width(
            &format!("width_survey_picker_right_{terminal_width}"),
            &repo,
            list_width,
        );
    }
}