diff options
Diffstat (limited to 'crates/asist-app/tests/integracion.rs')
| -rw-r--r-- | crates/asist-app/tests/integracion.rs | 206 |
1 files changed, 205 insertions, 1 deletions
diff --git a/crates/asist-app/tests/integracion.rs b/crates/asist-app/tests/integracion.rs index 91a074a..a7f7e15 100644 --- a/crates/asist-app/tests/integracion.rs +++ b/crates/asist-app/tests/integracion.rs @@ -14,7 +14,7 @@ use std::time::{Duration, Instant}; use asist_core::config::Config; use asist_core::http::Cancel; -use asist_core::tools::ToolRegistry; +use asist_core::tools::{Tool, ToolRegistry}; use asist_llm::chat::Conversation; use asist_llm::{Delta, LlmClient, Message}; use asist_tts::TtsClient; @@ -264,3 +264,207 @@ fn el_asr_transcribe_lo_que_sintetiza_el_tts() { "sólo se reconocieron {acertadas} palabras de «{frase}» en «{texto}»" ); } + +// --------------------------------------------------------------------------- +// Buscar en internet y mirar por la cámara +// --------------------------------------------------------------------------- + +#[test] +fn la_busqueda_devuelve_algo_pronunciable() { + let config = config(); + if !config.search.enabled { + eprintln!("búsqueda desactivada; prueba omitida"); + return; + } + let Ok(key) = std::env::var(&config.search.api_key_env) else { + eprintln!("sin ${}; prueba omitida", config.search.api_key_env); + return; + }; + if key.trim().is_empty() { + eprintln!("${} vacía; prueba omitida", config.search.api_key_env); + return; + } + + let tool = asist_tools::WebSearch::new( + asist_tools::SearchBackend::Tavily { api_key: key }, + config.search.max_results, + Duration::from_secs(config.search.timeout_secs), + ); + let started = Instant::now(); + let out = tool + .call(&serde_json::json!({ "consulta": "capital de Australia" })) + .expect("la búsqueda falló"); + + eprintln!( + "búsqueda en {:?}: {}", + started.elapsed(), + &out[..out.len().min(200)] + ); + assert!(!out.trim().is_empty()); + assert!( + out.to_lowercase().contains("canberra"), + "esperaba la respuesta en el resumen, salió: {out}" + ); + // Lo que devuelve se lee en voz alta; una parrafada de miles de caracteres + // ahoga al modelo que tiene que resumirla. + assert!( + out.len() < 6000, + "el resultado es demasiado largo: {} bytes", + out.len() + ); +} + +#[test] +fn una_consulta_inventada_no_revienta_el_turno() { + let config = config(); + let Ok(key) = std::env::var(&config.search.api_key_env) else { + return; + }; + if key.trim().is_empty() { + return; + } + // Vía el registro, que es como llega de verdad: un fallo tiene que volver + // como texto para el modelo, nunca como un error que corte la respuesta. + let mut registry = ToolRegistry::new(); + registry.register(std::sync::Arc::new(asist_tools::WebSearch::new( + asist_tools::SearchBackend::Tavily { + api_key: "clave-invalida".into(), + }, + 3, + Duration::from_secs(10), + ))); + let outcome = registry.dispatch(&asist_core::tools::ToolCall { + id: "1".into(), + name: "buscar_en_internet".into(), + arguments: r#"{"consulta":"algo"}"#.into(), + }); + assert!(!outcome.ok); + assert!( + !outcome.output.contains("clave-invalida"), + "el mensaje de error no debe llevar la clave dentro: {}", + outcome.output + ); +} + +#[test] +fn la_camara_captura_un_jpeg() { + let config = config(); + let capture = asist_tools::camera::CaptureConfig { + device: config.camera.device.clone(), + width: config.camera.width, + height: config.camera.height, + warmup_frames: config.camera.warmup_frames, + timeout: Duration::from_secs(config.camera.timeout_secs), + save_dir: None, + }; + if !config.camera.enabled || !capture.device.exists() { + eprintln!("sin cámara; prueba omitida"); + return; + } + + let started = Instant::now(); + let frame = asist_tools::camera::capture(&capture).expect("la captura falló"); + eprintln!( + "fotograma de {} KB en {:?}", + frame.len() / 1024, + started.elapsed() + ); + + assert!( + frame.len() > 1000, + "el fotograma es sospechosamente pequeño" + ); + // Cabecera JPEG: si no está, ffmpeg devolvió otra cosa y el modelo la + // rechazaría sin decir por qué. + assert_eq!(&frame[..2], &[0xFF, 0xD8], "no parece un JPEG"); +} + +#[test] +#[ignore = "enciende la cámara y hace una pasada de visión; lento"] +fn el_modelo_describe_lo_que_ve_la_camara() { + let _gpu = en_exclusiva(); + let Some((config, llm, _)) = clientes() else { + return; + }; + let capture = asist_tools::camera::CaptureConfig { + device: config.camera.device.clone(), + width: config.camera.width, + height: config.camera.height, + warmup_frames: config.camera.warmup_frames, + timeout: Duration::from_secs(config.camera.timeout_secs), + save_dir: None, + }; + if !config.camera.enabled || !capture.device.exists() { + eprintln!("sin cámara; prueba omitida"); + return; + } + + let tool = asist_tools::Camera::new(capture, std::sync::Arc::new(llm)); + let started = Instant::now(); + let out = tool + .call(&serde_json::json!({ "pregunta": "¿Qué se ve en la imagen?" })) + .expect("la descripción falló"); + + eprintln!("visión en {:?}: {out}", started.elapsed()); + assert!( + out.split_whitespace().count() >= 3, + "descripción vacía o mínima: {out}" + ); + assert!( + started.elapsed() < Duration::from_secs(20), + "tardó {:?}; revisa camera.width/height", + started.elapsed() + ); +} + +#[test] +#[ignore = "sale a internet con los dos buscadores; lento"] +fn los_dos_buscadores_responden_a_lo_mismo() { + // Compara lo que devuelve cada uno para las mismas preguntas. No afirma + // cuál es mejor —eso depende de la pregunta— sino que ambos funcionan y + // deja las cifras a la vista para elegir con datos. + let config = config(); + let preguntas = [ + "capital de Australia", + "qué tiempo hace hoy en Buenos Aires", + ]; + + let mut backends: Vec<(&str, asist_tools::SearchBackend)> = Vec::new(); + if let Ok(key) = std::env::var(&config.search.api_key_env) { + if !key.trim().is_empty() { + backends.push(( + "tavily", + asist_tools::SearchBackend::Tavily { api_key: key }, + )); + } + } + if let Some(program) = config.search.command.first() { + if PathBuf::from(program).exists() { + backends.push(("ddgs", asist_tools::SearchBackend::ddgs(program))); + } + } + if backends.is_empty() { + eprintln!("ningún buscador configurado; prueba omitida"); + return; + } + + for (nombre, backend) in backends { + let tool = asist_tools::WebSearch::new(backend, 3, Duration::from_secs(30)); + for pregunta in preguntas { + let started = Instant::now(); + match tool.call(&serde_json::json!({ "consulta": pregunta })) { + Ok(out) => { + let primera = out.lines().next().unwrap_or("").to_string(); + eprintln!( + "{nombre:7} {:>6.2}s {:>5} bytes «{pregunta}»\n {}", + started.elapsed().as_secs_f32(), + out.len(), + &primera[..primera.len().min(150)] + ); + assert!(!out.trim().is_empty()); + } + Err(err) => panic!("{nombre} falló en «{pregunta}»: {err}"), + } + } + } +} |