//! sandokan — el orquestador del ecosistema brahman (umbrella). //! //! `sandokan` es una **library horizontal embebible**, no un daemon //! supremo. Cualquier binario lo embebe y elige cómo correrlo: //! //! - [`LocalEngine`] — orquesta in-process (encarna Cards localmente). //! - [`DaemonEngine`] — delega a otro proceso vía Unix socket. //! - `RemoteEngine` — delega a otro host vía SSH (crate `sandokan-remote`). //! //! [`auto`] implementa el patrón "el primero que arranca gana": prueba //! si hay un daemon escuchando y, si lo hay, se le suma como //! `DaemonEngine`; si no, corre su propio `LocalEngine`. pub use sandokan_core::{ Engine, EngineError, ExecContext, ExecHandle, Intent, IsolationLevel, LifecycleEvent, TelemetryFrame, }; pub use sandokan_daemon::{serve, DaemonEngine, DaemonRequest, DaemonResponse}; pub use sandokan_local::LocalEngine; pub use sandokan_remote::RemoteEngine; /// Re-export de las primitivas de lifecycle. pub use sandokan_lifecycle as lifecycle; use std::path::{Path, PathBuf}; /// Path por defecto del socket del daemon sandokan. /// /// `$XDG_RUNTIME_DIR/sandokan.sock` si la variable está; si no, /// `/run/brahman/sandokan.sock`. pub fn default_socket_path() -> PathBuf { match std::env::var_os("XDG_RUNTIME_DIR") { Some(rt) => PathBuf::from(rt).join("sandokan.sock"), None => PathBuf::from("/run/brahman/sandokan.sock"), } } /// Elige el engine según el entorno: si hay un daemon escuchando en /// `socket_path`, devuelve un [`DaemonEngine`] (delega); si no, un /// [`LocalEngine`] (orquesta in-process). pub async fn auto(socket_path: &Path) -> Box { let daemon = DaemonEngine::new(socket_path); if daemon.is_reachable().await { Box::new(daemon) } else { Box::new(LocalEngine::new()) } } /// [`auto`] con [`default_socket_path`]. pub async fn auto_default() -> Box { auto(&default_socket_path()).await } #[cfg(test)] mod tests { use super::*; use std::sync::Arc; use std::time::Duration; #[tokio::test] async fn auto_falls_back_to_local_without_daemon() { // Socket inexistente → debe caer a LocalEngine (list() vacío). let dir = tempfile::tempdir().unwrap(); let sock = dir.path().join("nope.sock"); let engine = auto(&sock).await; assert!(engine.list().await.unwrap().is_empty()); } #[tokio::test] async fn auto_picks_daemon_when_listening() { let dir = tempfile::tempdir().unwrap(); let sock = dir.path().join("sandokan.sock"); let served = Arc::new(LocalEngine::new()); let sock_srv = sock.clone(); let srv = tokio::spawn(async move { let _ = serve(served, &sock_srv).await; }); for _ in 0..50 { if sock.exists() { break; } tokio::time::sleep(Duration::from_millis(10)).await; } // Con el daemon escuchando, auto() debe conectar y operar vía wire. let engine = auto(&sock).await; assert!(engine.list().await.unwrap().is_empty()); srv.abort(); } #[test] fn default_socket_path_is_absolute() { assert!(default_socket_path().is_absolute()); } }