aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-app/tests/integracion.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/asist-app/tests/integracion.rs')
-rw-r--r--crates/asist-app/tests/integracion.rs206
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}"),
+ }
+ }
+ }
+}