tazuna 0.1.0

TUI tool for managing multiple Claude Code sessions in parallel
Documentation
//! Integration tests for `tazuna notify` subcommand.
//!
//! Tests the full flow: stdin JSON → subprocess → Unix socket → `HooksServer`

#![allow(clippy::expect_used, clippy::unwrap_used)]

use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Duration;

use tazuna::hooks::{HookEventType, HooksServer};
use tazuna::session::SessionId;
use tokio::sync::mpsc;
use uuid::Uuid;

/// Helper to spawn tazuna notify with environment variables
fn spawn_notify(session_id: &SessionId, socket_path: &std::path::Path) -> std::process::Child {
    Command::new(env!("CARGO_BIN_EXE_tazuna"))
        .arg("notify")
        .env("TAZUNA_SESSION_ID", session_id.to_string())
        .env("TAZUNA_SOCKET_PATH", socket_path.to_str().unwrap())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn tazuna notify")
}

#[tokio::test]
async fn tazuna_notify_sends_event_to_server() {
    let temp = tempfile::tempdir().expect("create tempdir");
    let socket_path = temp.path().join("test.sock");
    let (tx, mut rx) = mpsc::channel(16);

    // Start server
    let server = HooksServer::new(&socket_path, tx).expect("create server");
    let server_handle = tokio::spawn(server.run());
    tokio::time::sleep(Duration::from_millis(50)).await;

    // Prepare test data
    let session_id = SessionId::from(Uuid::new_v4());
    let json_input = r#"{"hook_event_name": "Notification", "message": "Test notification"}"#;

    // Spawn tazuna notify subprocess
    let mut child = spawn_notify(&session_id, &socket_path);

    // Write JSON to stdin and close
    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(json_input.as_bytes())
        .expect("write stdin");
    drop(child.stdin.take()); // Close stdin to signal EOF

    // Wait for subprocess with timeout
    let status = child.wait().expect("wait");
    assert!(status.success(), "tazuna notify should succeed");

    // Verify server received event
    let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
        .await
        .expect("should receive within timeout")
        .expect("should receive event");

    assert_eq!(event.event_type, HookEventType::Notification);
    assert_eq!(event.session_id, session_id);
    assert_eq!(event.message(), Some("Test notification".to_string()));

    server_handle.abort();
}

#[tokio::test]
async fn tazuna_notify_pre_tool_use() {
    let temp = tempfile::tempdir().expect("create tempdir");
    let socket_path = temp.path().join("test.sock");
    let (tx, mut rx) = mpsc::channel(16);

    let server = HooksServer::new(&socket_path, tx).expect("create server");
    let server_handle = tokio::spawn(server.run());
    tokio::time::sleep(Duration::from_millis(50)).await;

    let session_id = SessionId::from(Uuid::new_v4());
    let json_input = r#"{"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "ls"}}"#;

    let mut child = spawn_notify(&session_id, &socket_path);
    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(json_input.as_bytes())
        .expect("write stdin");
    drop(child.stdin.take());

    let status = child.wait().expect("wait");
    assert!(status.success());

    let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
        .await
        .expect("timeout")
        .expect("receive event");

    assert_eq!(event.event_type, HookEventType::PreToolUse);
    assert_eq!(event.message(), Some("Tool: Bash".to_string()));

    server_handle.abort();
}

#[tokio::test]
async fn tazuna_notify_missing_session_id() {
    let temp = tempfile::tempdir().expect("create tempdir");
    let socket_path = temp.path().join("test.sock");

    // No server needed - should fail before socket connection

    let mut child = Command::new(env!("CARGO_BIN_EXE_tazuna"))
        .arg("notify")
        // Missing TAZUNA_SESSION_ID
        .env("TAZUNA_SOCKET_PATH", socket_path.to_str().unwrap())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn");

    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"{\"hook\": \"Notification\", \"message\": \"test\"}")
        .expect("write");
    drop(child.stdin.take());

    let status = child.wait().expect("wait");
    assert!(!status.success(), "should fail without TAZUNA_SESSION_ID");
}

#[tokio::test]
async fn tazuna_notify_invalid_json() {
    let temp = tempfile::tempdir().expect("create tempdir");
    let socket_path = temp.path().join("test.sock");
    let session_id = SessionId::from(Uuid::new_v4());

    // Server not needed - should fail during JSON parse

    let mut child = spawn_notify(&session_id, &socket_path);

    // Write invalid JSON
    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"not valid json")
        .expect("write");
    drop(child.stdin.take());

    let status = child.wait().expect("wait");
    assert!(!status.success(), "should fail with invalid JSON");
}

#[tokio::test]
async fn tazuna_notify_socket_not_found() {
    let temp = tempfile::tempdir().expect("create tempdir");
    let socket_path = temp.path().join("nonexistent.sock");
    let session_id = SessionId::from(Uuid::new_v4());

    // No server - socket doesn't exist

    let mut child = spawn_notify(&session_id, &socket_path);

    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"{\"hook\": \"Notification\", \"message\": \"test\"}")
        .expect("write");
    drop(child.stdin.take());

    let status = child.wait().expect("wait");
    assert!(!status.success(), "should fail when socket doesn't exist");
}