//! Integration tests for the daemon server lifecycle. //! //! Each test spins up a real gRPC server on a temporary unix socket, //! connects a client, and exercises the daemon RPCs. #[cfg(unix)] mod unix { use std::time::Duration; use atuin_client::database::Sqlite; use atuin_client::record::sqlite_store::SqliteStore; use atuin_client::settings::{Settings, init_meta_config_for_testing}; use atuin_daemon::client::HistoryClient; use atuin_daemon::components::HistoryComponent; use atuin_daemon::{Daemon, DaemonHandle}; use tempfile::TempDir; use tokio::net::UnixListener; use tokio_stream::wrappers::UnixListenerStream; use tonic::transport::Server; /// Spins up a daemon server on a temp socket and returns a connected client, /// the daemon handle (for shutdown), and the temp dir (must be held to keep paths alive). async fn start_test_daemon() -> (HistoryClient, DaemonHandle, TempDir) { let tmp = tempfile::tempdir().unwrap(); let db_path = tmp.path().join("history.db"); let record_path = tmp.path().join("records.db"); let key_path = tmp.path().join("key"); let socket_path = tmp.path().join("test.sock"); let meta_path = tmp.path().join("meta.db"); // Initialize the meta store config for testing (required for Settings::host_id()) init_meta_config_for_testing(meta_path.to_str().unwrap(), 5.0); // Build settings with test paths let settings: Settings = Settings::builder() .expect("could not build settings builder") .set_override("db_path", db_path.to_str().unwrap()) .expect("failed to set db_path") .set_override("record_store_path", record_path.to_str().unwrap()) .expect("failed to set record_store_path") .set_override("key_path", key_path.to_str().unwrap()) .expect("failed to set key_path") .set_override("daemon.socket_path", socket_path.to_str().unwrap()) .expect("failed to set socket_path") .set_override("meta.db_path", meta_path.to_str().unwrap()) .expect("failed to set meta.db_path") .build() .expect("could not build settings") .try_deserialize() .expect("could not deserialize settings"); // Create databases let history_db = Sqlite::new(&db_path, 5.0).await.unwrap(); let store = SqliteStore::new(&record_path, 5.0).await.unwrap(); // Create the history component and get its gRPC service let history_component = HistoryComponent::new(); let history_service = history_component.grpc_service(); // Build and start the daemon let mut daemon = Daemon::builder(settings) .store(store) .history_db(history_db) .component(history_component) .build() .await .unwrap(); let handle = daemon.handle(); // Start components (this initializes the history component with the handle) daemon.start_components().await.unwrap(); // Start the gRPC server let uds = UnixListener::bind(&socket_path).unwrap(); let stream = UnixListenerStream::new(uds); let server_handle = handle.clone(); tokio::spawn(async move { let mut rx = server_handle.subscribe(); Server::builder() .add_service(history_service) .serve_with_incoming_shutdown(stream, async move { loop { match rx.recv().await { Ok(atuin_daemon::DaemonEvent::ShutdownRequested) => break, Ok(_) => continue, Err(_) => break, } } }) .await .unwrap(); }); // Spawn the daemon event loop in the background tokio::spawn(async move { daemon.run_event_loop().await.unwrap(); }); // Give the server a moment to bind. tokio::time::sleep(Duration::from_millis(50)).await; let client = HistoryClient::new(socket_path.to_string_lossy().to_string()) .await .unwrap(); (client, handle, tmp) } #[tokio::test] async fn test_status() { let (mut client, _handle, _tmp) = start_test_daemon().await; let status = client.status().await.unwrap(); assert!(status.healthy); assert_eq!(status.version, env!("CARGO_PKG_VERSION")); assert_eq!(status.protocol, 1); assert!(status.pid > 0); } #[tokio::test] async fn test_start_end_history() { use atuin_client::history::History; let (mut client, _handle, _tmp) = start_test_daemon().await; let history = History::daemon() .timestamp(time::OffsetDateTime::now_utc()) .command("echo hello".to_string()) .cwd("/tmp".to_string()) .session("test-session".to_string()) .hostname("test-host".to_string()) .build() .into(); let start_reply = client.start_history(history).await.unwrap(); assert!(!start_reply.id.is_empty()); let end_reply = client .end_history(start_reply.id, 1_000_000, 0) .await .unwrap(); assert!(!end_reply.id.is_empty()); } #[tokio::test] async fn test_tail_history_streams_started_and_ended_events() { use atuin_client::history::History; use atuin_daemon::history::HistoryEventKind; let (mut client, _handle, _tmp) = start_test_daemon().await; let mut stream = client.tail_history().await.unwrap(); let history = History::daemon() .timestamp(time::OffsetDateTime::now_utc()) .command("git status".to_string()) .cwd("/tmp/repo".to_string()) .session("tail-session".to_string()) .hostname("test-host:ellie".to_string()) .author("claude".to_string()) .intent("inspect repository state".to_string()) .build() .into(); let start_reply = client.start_history(history).await.unwrap(); let started = stream.message().await.unwrap().unwrap(); assert_eq!( HistoryEventKind::try_from(started.kind).unwrap(), HistoryEventKind::Started ); let started_history = started.history.unwrap(); assert_eq!(started_history.id, start_reply.id); assert_eq!(started_history.command, "git status"); assert_eq!(started_history.cwd, "/tmp/repo"); assert_eq!(started_history.hostname, "test-host:ellie"); assert_eq!(started_history.author, "claude"); assert_eq!(started_history.intent, "inspect repository state"); client .end_history(start_reply.id.clone(), 1_000_000, 0) .await .unwrap(); let ended = stream.message().await.unwrap().unwrap(); assert_eq!( HistoryEventKind::try_from(ended.kind).unwrap(), HistoryEventKind::Ended ); let ended_history = ended.history.unwrap(); assert_eq!(ended_history.id, start_reply.id); assert_eq!(ended_history.exit, 0); assert_eq!(ended_history.duration, 1_000_000); } #[tokio::test] async fn test_end_unknown_history_fails() { let (mut client, _handle, _tmp) = start_test_daemon().await; let result = client .end_history("nonexistent-id".to_string(), 1000, 0) .await; assert!(result.is_err()); } #[tokio::test] async fn test_shutdown() { let (mut client, _handle, _tmp) = start_test_daemon().await; let accepted = client.shutdown().await.unwrap(); assert!(accepted); // Give server time to shut down. tokio::time::sleep(Duration::from_millis(100)).await; // Subsequent calls should fail since the server is gone. let result = client.status().await; assert!(result.is_err()); } }