git-paw 0.2.0

Parallel AI Worktrees — orchestrate multiple AI coding CLI sessions across git worktrees
Documentation
//! Session state persistence integration tests.
//!
//! Tests save/load round-trips, session lookup by repo path, deletion, and
//! effective status computation. All isolated via `tempfile`.

use std::path::PathBuf;
use std::time::SystemTime;

use tempfile::TempDir;

use git_paw::session::{
    Session, SessionStatus, WorktreeEntry, delete_session_in, find_session_for_repo_in,
    load_session_from, save_session_in,
};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn make_session(suffix: &str) -> Session {
    Session {
        session_name: format!("paw-test-{suffix}"),
        repo_path: PathBuf::from(format!("/tmp/fake-repo-{suffix}")),
        project_name: format!("test-project-{suffix}"),
        created_at: SystemTime::now(),
        status: SessionStatus::Active,
        worktrees: vec![
            WorktreeEntry {
                branch: "feature/auth".to_string(),
                worktree_path: PathBuf::from(format!("/tmp/wt-{suffix}-auth")),
                cli: "claude".to_string(),
            },
            WorktreeEntry {
                branch: "fix/db".to_string(),
                worktree_path: PathBuf::from(format!("/tmp/wt-{suffix}-db")),
                cli: "gemini".to_string(),
            },
        ],
    }
}

// ---------------------------------------------------------------------------
// Round-trip
// ---------------------------------------------------------------------------

#[test]
fn save_and_load_round_trip() {
    let dir = TempDir::new().expect("create temp dir");
    let session = make_session("roundtrip");

    save_session_in(&session, dir.path()).expect("save");

    let loaded = load_session_from(&session.session_name, dir.path())
        .expect("load")
        .expect("session should exist");

    assert_eq!(loaded.session_name, session.session_name);
    assert_eq!(loaded.repo_path, session.repo_path);
    assert_eq!(loaded.project_name, session.project_name);
    assert_eq!(loaded.status, SessionStatus::Active);
    assert_eq!(loaded.worktrees.len(), 2);
    assert_eq!(loaded.worktrees[0].branch, "feature/auth");
    assert_eq!(loaded.worktrees[0].cli, "claude");
    assert_eq!(loaded.worktrees[1].branch, "fix/db");
    assert_eq!(loaded.worktrees[1].cli, "gemini");
}

// ---------------------------------------------------------------------------
// Find by repo path
// ---------------------------------------------------------------------------

#[test]
fn find_session_by_repo_path() {
    let dir = TempDir::new().expect("create temp dir");
    let session = make_session("find-repo");

    save_session_in(&session, dir.path()).expect("save");

    let found = find_session_for_repo_in(&session.repo_path, dir.path())
        .expect("find")
        .expect("session should be found");
    assert_eq!(found.session_name, session.session_name);
}

#[test]
fn find_session_returns_none_for_unknown_repo() {
    let dir = TempDir::new().expect("create temp dir");
    let result = find_session_for_repo_in(&PathBuf::from("/nonexistent/repo"), dir.path())
        .expect("find should not error");
    assert!(result.is_none());
}

#[test]
fn find_correct_session_among_multiple() {
    let dir = TempDir::new().expect("create temp dir");

    let session_a = make_session("multi-a");
    let session_b = make_session("multi-b");

    save_session_in(&session_a, dir.path()).expect("save a");
    save_session_in(&session_b, dir.path()).expect("save b");

    let found = find_session_for_repo_in(&session_b.repo_path, dir.path())
        .expect("find")
        .expect("should find session b");
    assert_eq!(found.session_name, session_b.session_name);
}

// ---------------------------------------------------------------------------
// Delete
// ---------------------------------------------------------------------------

#[test]
fn delete_removes_session() {
    let dir = TempDir::new().expect("create temp dir");
    let session = make_session("delete");

    save_session_in(&session, dir.path()).expect("save");
    delete_session_in(&session.session_name, dir.path()).expect("delete");

    let loaded = load_session_from(&session.session_name, dir.path()).expect("load");
    assert!(loaded.is_none(), "session should be gone after delete");
}

#[test]
fn delete_nonexistent_is_idempotent() {
    let dir = TempDir::new().expect("create temp dir");
    let result = delete_session_in("nonexistent", dir.path());
    assert!(
        result.is_ok(),
        "deleting nonexistent session should succeed"
    );
}

// ---------------------------------------------------------------------------
// Load missing
// ---------------------------------------------------------------------------

#[test]
fn load_nonexistent_returns_none() {
    let dir = TempDir::new().expect("create temp dir");
    let loaded = load_session_from("does-not-exist", dir.path()).expect("load should not error");
    assert!(loaded.is_none());
}

// ---------------------------------------------------------------------------
// Overwrite
// ---------------------------------------------------------------------------

#[test]
fn saving_again_replaces_previous_state() {
    let dir = TempDir::new().expect("create temp dir");
    let mut session = make_session("overwrite");

    save_session_in(&session, dir.path()).expect("save 1");

    session.status = SessionStatus::Stopped;
    save_session_in(&session, dir.path()).expect("save 2");

    let loaded = load_session_from(&session.session_name, dir.path())
        .expect("load")
        .expect("session should exist");
    assert_eq!(loaded.status, SessionStatus::Stopped);
}

// ---------------------------------------------------------------------------
// Effective status
// ---------------------------------------------------------------------------

#[test]
fn effective_status_active_when_tmux_alive() {
    let session = make_session("eff-active");
    let status = session.effective_status(|_| true);
    assert_eq!(status, SessionStatus::Active);
}

#[test]
fn effective_status_stopped_when_tmux_dead() {
    let session = make_session("eff-stopped");
    let status = session.effective_status(|_| false);
    assert_eq!(status, SessionStatus::Stopped);
}

#[test]
fn effective_status_stopped_stays_stopped() {
    let mut session = make_session("eff-stay-stopped");
    session.status = SessionStatus::Stopped;
    let status = session.effective_status(|_| true);
    assert_eq!(status, SessionStatus::Stopped);
}

// ---------------------------------------------------------------------------
// Recovery data completeness
// ---------------------------------------------------------------------------

#[test]
fn saved_session_has_all_recovery_fields() {
    let dir = TempDir::new().expect("create temp dir");
    let session = make_session("recovery");

    save_session_in(&session, dir.path()).expect("save");

    let loaded = load_session_from(&session.session_name, dir.path())
        .expect("load")
        .expect("session should exist");

    // All fields needed for recovery must be present
    assert!(!loaded.session_name.is_empty());
    assert!(!loaded.repo_path.as_os_str().is_empty());
    assert!(!loaded.project_name.is_empty());
    assert!(!loaded.worktrees.is_empty());

    for wt in &loaded.worktrees {
        assert!(!wt.branch.is_empty(), "branch must be set for recovery");
        assert!(
            !wt.worktree_path.as_os_str().is_empty(),
            "worktree path must be set"
        );
        assert!(!wt.cli.is_empty(), "CLI must be set for recovery");
    }
}