tazuna 0.1.0

TUI tool for managing multiple Claude Code sessions in parallel
Documentation
//! Error types for tazuna.
//!
//! Domain-specific errors using thiserror.

use std::path::PathBuf;
use thiserror::Error;

/// Session-related errors
#[derive(Debug, Error)]
pub enum SessionError {
    /// PTY creation failure
    #[error("failed to create PTY: {0}")]
    PtyCreation(#[source] std::io::Error),

    /// Session limit exceeded
    #[error("session limit reached (max: {max})")]
    LimitReached {
        /// Maximum allowed sessions
        max: usize,
    },

    /// Session not found
    #[error("session not found: {id}")]
    NotFound {
        /// Session ID that was not found
        id: String,
    },

    /// Process spawn failure
    #[error("failed to spawn process: {0}")]
    SpawnFailed(#[source] std::io::Error),

    /// Invalid session ID format
    #[error("invalid session ID: {id}")]
    InvalidSessionId {
        /// Invalid session ID string
        id: String,
    },

    /// Session already exists
    #[error("session already exists: {id}")]
    AlreadyExists {
        /// Duplicate session ID
        id: String,
    },

    /// Worktree not found at path
    #[error("worktree not found at: {}", path.display())]
    WorktreeNotFound {
        /// Path that was not found
        path: PathBuf,
    },

    /// Worktree manager not configured
    #[error("worktree manager not configured")]
    WorktreeNotConfigured,

    /// Worktree operation failed
    #[error("worktree operation failed: {0}")]
    Worktree(#[from] WorktreeError),
}

/// Configuration-related errors
#[derive(Debug, Error)]
pub enum ConfigError {
    /// File read failure
    #[error("failed to read config file at {path}: {source}")]
    ReadFailed {
        /// Path to the config file
        path: PathBuf,
        /// Underlying IO error
        #[source]
        source: std::io::Error,
    },

    /// Parse failure
    #[error("failed to parse config: {0}")]
    ParseFailed(#[source] toml::de::Error),

    /// Invalid value
    #[error("invalid config value: {field} - {message}")]
    InvalidValue {
        /// Field name with invalid value
        field: String,
        /// Description of why the value is invalid
        message: String,
    },
}

/// Worktree-related errors
#[derive(Debug, Error)]
pub enum WorktreeError {
    /// Git command failure
    #[error("git worktree command failed: {0}")]
    GitFailed(String),

    /// Path error
    #[error("invalid worktree path: {0}")]
    InvalidPath(PathBuf),

    /// Not a git repository
    #[error("not a git repository: {0}")]
    NotGitRepo(PathBuf),

    /// Branch already exists
    #[error("branch already exists: {0}")]
    BranchExists(String),

    /// Worktree already exists at path
    #[error("worktree already exists at: {}", path.display())]
    WorktreeExists {
        /// Path where worktree exists
        path: PathBuf,
    },

    /// Worktree not found at path
    #[error("worktree not found at: {}", path.display())]
    WorktreeNotFound {
        /// Path that was not found
        path: PathBuf,
    },

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

/// Hooks-related errors
#[derive(Debug, Error)]
pub enum HooksError {
    /// IPC failure
    #[error("IPC communication failed: {0}")]
    IpcFailed(#[source] std::io::Error),

    /// Script generation failure
    #[error("failed to generate hooks script: {0}")]
    ScriptGenFailed(#[source] std::io::Error),

    /// JSON parse failure
    #[error("failed to parse hook event: {0}")]
    ParseFailed(String),

    /// Missing environment variable
    #[error("missing environment variable: {0}")]
    MissingEnv(String),

    /// Socket not found
    #[error("socket not found: {}", .0.display())]
    SocketNotFound(PathBuf),

    /// Socket in use by another tazuna instance
    #[error("socket in use by another tazuna instance: {}", .0.display())]
    SocketInUse(PathBuf),

    /// Invalid session ID
    #[error("invalid session ID: {0}")]
    InvalidSessionId(String),
}

/// Notification-related errors
#[derive(Debug, Error)]
pub enum NotificationError {
    /// OS notification failure
    #[error("OS notification failed: {0}")]
    OsFailed(String),

    /// Webhook failure
    #[error("webhook request failed: {0}")]
    WebhookFailed(#[source] reqwest::Error),
}

/// GitHub-related errors
#[derive(Debug, Error)]
pub enum GitHubError {
    /// Command execution failure
    #[error("{command} failed: {source}")]
    CommandFailed {
        /// Command that failed
        command: String,
        /// Underlying IO error
        #[source]
        source: std::io::Error,
    },

    /// Parse failure
    #[error("failed to parse {context}: {message}")]
    ParseFailed {
        /// Context of what was being parsed
        context: String,
        /// Error message
        message: String,
    },
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn session_error_limit_reached_display() {
        let err = SessionError::LimitReached { max: 10 };
        assert_eq!(err.to_string(), "session limit reached (max: 10)");
    }

    #[test]
    fn session_error_not_found_display() {
        let err = SessionError::NotFound {
            id: "abc123".to_string(),
        };
        assert_eq!(err.to_string(), "session not found: abc123");
    }

    #[test]
    fn config_error_invalid_value_display() {
        let err = ConfigError::InvalidValue {
            field: "max_sessions".to_string(),
            message: "must be positive".to_string(),
        };
        assert!(err.to_string().contains("max_sessions"));
        assert!(err.to_string().contains("must be positive"));
    }

    #[test]
    fn worktree_error_git_failed_display() {
        let err = WorktreeError::GitFailed("branch already exists".to_string());
        assert!(err.to_string().contains("branch already exists"));
    }

    #[test]
    fn worktree_error_invalid_path_display() {
        let err = WorktreeError::InvalidPath(PathBuf::from("/invalid/path"));
        assert!(err.to_string().contains("/invalid/path"));
    }

    #[test]
    fn worktree_error_not_git_repo_display() {
        let err = WorktreeError::NotGitRepo(PathBuf::from("/not/a/repo"));
        assert!(err.to_string().contains("not a git repository"));
        assert!(err.to_string().contains("/not/a/repo"));
    }

    #[test]
    fn worktree_error_branch_exists_display() {
        let err = WorktreeError::BranchExists("tazuna/session-123".to_string());
        assert!(err.to_string().contains("branch already exists"));
        assert!(err.to_string().contains("tazuna/session-123"));
    }

    #[test]
    fn worktree_error_worktree_exists_display() {
        let err = WorktreeError::WorktreeExists {
            path: PathBuf::from("/some/worktree"),
        };
        assert!(err.to_string().contains("worktree already exists"));
        assert!(err.to_string().contains("/some/worktree"));
    }

    #[test]
    fn worktree_error_worktree_not_found_display() {
        let err = WorktreeError::WorktreeNotFound {
            path: PathBuf::from("/some/path"),
        };
        assert!(err.to_string().contains("worktree not found"));
        assert!(err.to_string().contains("/some/path"));
    }

    #[test]
    fn worktree_error_io_display() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
        let err = WorktreeError::Io(io_err);
        assert!(err.to_string().contains("IO error"));
    }

    #[test]
    fn notification_error_os_failed_display() {
        let err = NotificationError::OsFailed("dbus error".to_string());
        assert!(err.to_string().contains("dbus error"));
    }

    #[test]
    fn session_error_invalid_session_id_display() {
        let err = SessionError::InvalidSessionId {
            id: "not-a-uuid".to_string(),
        };
        assert!(err.to_string().contains("invalid session ID"));
        assert!(err.to_string().contains("not-a-uuid"));
    }

    #[test]
    fn session_error_already_exists_display() {
        let err = SessionError::AlreadyExists {
            id: "abc-123".to_string(),
        };
        assert!(err.to_string().contains("session already exists"));
        assert!(err.to_string().contains("abc-123"));
    }

    #[test]
    fn session_error_worktree_not_found_display() {
        let err = SessionError::WorktreeNotFound {
            path: PathBuf::from("/some/path"),
        };
        assert!(err.to_string().contains("worktree not found"));
        assert!(err.to_string().contains("/some/path"));
    }

    #[test]
    fn session_error_worktree_not_configured_display() {
        let err = SessionError::WorktreeNotConfigured;
        assert!(err.to_string().contains("worktree manager not configured"));
    }

    #[test]
    fn session_error_worktree_display() {
        let wt_err = WorktreeError::WorktreeNotFound {
            path: PathBuf::from("/test/path"),
        };
        let err = SessionError::Worktree(wt_err);
        assert!(err.to_string().contains("worktree operation failed"));
    }

    #[test]
    fn hooks_error_socket_in_use_display() {
        let err = HooksError::SocketInUse(PathBuf::from("/tmp/1234.sock"));
        assert!(
            err.to_string()
                .contains("socket in use by another tazuna instance")
        );
        assert!(err.to_string().contains("/tmp/1234.sock"));
    }

    #[test]
    fn github_error_command_failed_display() {
        let err = GitHubError::CommandFailed {
            command: "gh issue list".to_string(),
            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
        };
        assert!(err.to_string().contains("gh issue list"));
        assert!(err.to_string().contains("failed"));
    }

    #[test]
    fn github_error_parse_failed_display() {
        let err = GitHubError::ParseFailed {
            context: "issue #22".to_string(),
            message: "invalid JSON".to_string(),
        };
        assert!(err.to_string().contains("issue #22"));
        assert!(err.to_string().contains("invalid JSON"));
    }
}