Files
sergio 649ca02d4d feat(verbo): verbo-daemon — embeddings compartidos entre procesos
Daemon que carga un Provider una vez y lo sirve sobre socket Unix;
DaemonClient lo consume desde otro proceso implementando el trait
Provider (indistinguible de un backend local). Multi-instancia: un
daemon por modelo, cada uno en su socket. Frames postcard con
prefijo de largo. 8 tests (wire + integración real sobre socket).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:25:56 +00:00

104 lines
3.5 KiB
Rust

//! Pruebas de integración: un daemon real sobre socket Unix + clientes.
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use verbo_core::Provider;
use verbo_daemon::{Daemon, DaemonClient};
use verbo_mock::MockProvider;
/// Ruta de socket única por test — evita choques entre tests paralelos.
fn unique_socket() -> std::path::PathBuf {
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("verbo-d-{}-{n}.sock", std::process::id()))
}
/// Levanta un daemon sirviendo un `MockProvider` y devuelve su ruta + el
/// handle de la task (para abortarla al final).
fn spawn_daemon(dim: usize) -> (std::path::PathBuf, tokio::task::JoinHandle<()>) {
let path = unique_socket();
let daemon = Daemon::bind(&path).expect("bind");
let provider = Arc::new(MockProvider::new(dim));
let handle = tokio::spawn(async move {
let _ = daemon.serve(provider).await;
});
(path, handle)
}
#[tokio::test]
async fn client_embed_matches_direct_provider() {
let (path, handle) = spawn_daemon(32);
let client = DaemonClient::connect(&path).await.expect("connect");
let over_socket = client.embed("texto de prueba").await.unwrap();
let direct = MockProvider::new(32).embed("texto de prueba").await.unwrap();
// El daemon no debe alterar el vector: byte a byte igual al directo.
assert_eq!(over_socket.values, direct.values);
assert_eq!(over_socket.model, direct.model);
handle.abort();
}
#[tokio::test]
async fn handshake_reports_model_id() {
let (path, handle) = spawn_daemon(384);
let client = DaemonClient::connect(&path).await.expect("connect");
assert_eq!(client.model_id().dimension, 384);
handle.abort();
}
#[tokio::test]
async fn batch_over_socket_matches_individual() {
let (path, handle) = spawn_daemon(16);
let client = DaemonClient::connect(&path).await.expect("connect");
let texts = vec!["uno".to_string(), "dos".to_string(), "tres".to_string()];
let batch = client.embed_batch(&texts).await.unwrap();
assert_eq!(batch.len(), 3);
let single = client.embed("dos").await.unwrap();
assert_eq!(batch[1].values, single.values);
handle.abort();
}
#[tokio::test]
async fn many_requests_on_one_client() {
// El cliente hace round-trip por llamada: varias llamadas seguidas
// sobre el mismo cliente deben funcionar sin estado corrupto.
let (path, handle) = spawn_daemon(8);
let client = DaemonClient::connect(&path).await.expect("connect");
for word in ["a", "bb", "ccc", "a"] {
let v = client.embed(word).await.unwrap();
assert_eq!(v.values.len(), 8);
}
// Mismo texto → mismo vector incluso tras otras llamadas.
let first = client.embed("a").await.unwrap();
let again = client.embed("a").await.unwrap();
assert_eq!(first.values, again.values);
handle.abort();
}
#[tokio::test]
async fn two_clients_share_one_daemon() {
let (path, handle) = spawn_daemon(24);
let a = DaemonClient::connect(&path).await.expect("connect a");
let b = DaemonClient::connect(&path).await.expect("connect b");
let va = a.embed("compartido").await.unwrap();
let vb = b.embed("compartido").await.unwrap();
// Dos procesos, un modelo: el mismo texto da el mismo vector.
assert_eq!(va.values, vb.values);
handle.abort();
}
#[tokio::test]
async fn connect_to_missing_daemon_errors() {
let path = unique_socket(); // nunca se bindeó
let result = DaemonClient::connect(&path).await;
assert!(result.is_err());
}