#[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;
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");
init_meta_config_for_testing(meta_path.to_str().unwrap(), 5.0);
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");
let history_db = Sqlite::new(&db_path, 5.0).await.unwrap();
let store = SqliteStore::new(&record_path, 5.0).await.unwrap();
let history_component = HistoryComponent::new();
let history_service = history_component.grpc_service();
let mut daemon = Daemon::builder(settings)
.store(store)
.history_db(history_db)
.component(history_component)
.build()
.await
.unwrap();
let handle = daemon.handle();
daemon.start_components().await.unwrap();
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();
});
tokio::spawn(async move {
daemon.run_event_loop().await.unwrap();
});
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);
tokio::time::sleep(Duration::from_millis(100)).await;
let result = client.status().await;
assert!(result.is_err());
}
}