aura-gui 0.2.2

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Cliente ACP contra un agente falso en Python: permiso, lectura de archivo del cliente y texto en streaming.

use aura_ui::ai::acp::{AcpClient, AcpEvent};
use aura_ui::ai::mcp::McpConfig;
use serde_json::json;
use std::sync::mpsc;
use std::time::Duration;

fn fake_config() -> McpConfig {
    let script = format!("{}/tests/acp_fake.py", env!("CARGO_MANIFEST_DIR"));
    McpConfig { command: "python3".into(), args: vec![script], ..Default::default() }
}

/// Atiende al agente como lo haría la interfaz: elige `option` en el permiso y entrega el archivo pedido.
fn conversation(option: Option<&str>) -> (String, Vec<AcpEvent>) {
    let (sender, receiver) = mpsc::channel();
    let client = AcpClient::start(&fake_config(), &std::env::temp_dir(), move |event| {
        sender.send(event).expect("canal");
    })
    .expect("arranca el agente");
    client.prompt("lee a.txt").expect("prompt");
    let (mut text, mut seen) = (String::new(), Vec::new());
    loop {
        let event = receiver.recv_timeout(Duration::from_secs(10)).expect("evento a tiempo");
        match &event {
            AcpEvent::Text(chunk) => text.push_str(chunk),
            AcpEvent::Permission { request, .. } => client.answer_permission(request, option).expect("respuesta"),
            AcpEvent::ReadFile { request, path, .. } => {
                assert_eq!(path, "a.txt");
                client.answer(request, Ok(json!({ "content": "hola desde el cliente" }))).expect("archivo");
            }
            AcpEvent::Done(_) | AcpEvent::Error(_) => {
                seen.push(event);
                return (text, seen);
            }
            _ => {}
        }
        seen.push(event);
    }
}

#[test]
fn permiso_concedido_el_agente_lee_el_archivo_del_cliente() {
    let (text, events) = conversation(Some("si"));
    assert_eq!(text, "Voy a leer. Contenido: hola desde el cliente");
    assert!(matches!(events.last(), Some(AcpEvent::Done(reason)) if reason == "end_turn"));
    let permission = events.iter().find_map(|event| match event {
        AcpEvent::Permission { title, options, .. } => Some((title.clone(), options.len())),
        _ => None,
    });
    assert_eq!(permission, Some(("Leer a.txt".to_owned(), 2)));
}

#[test]
fn permiso_rechazado_el_agente_no_lee_nada() {
    let (text, events) = conversation(None);
    assert_eq!(text, "Voy a leer. No me dejaron.");
    assert!(!events.iter().any(|event| matches!(event, AcpEvent::ReadFile { .. })));
}

#[test]
fn iniciar_sesion_llama_a_authenticate_con_el_metodo_pedido() {
    let dir = std::env::temp_dir();
    AcpClient::authenticate(&fake_config(), &dir, "chatgpt").expect("sesión iniciada");
    let error = AcpClient::authenticate(&fake_config(), &dir, "otro").expect_err("método rechazado");
    assert!(error.to_string().contains("método desconocido"), "{error}");
}

#[test]
fn retoma_la_sesion_guardada_y_si_falla_abre_una_nueva() {
    let dir = std::env::temp_dir();
    let start = |resume: Option<&str>| {
        AcpClient::start_resuming(&fake_config(), &dir, resume, |_| {})
            .expect("arranca el agente")
            .session_id()
            .to_owned()
    };
    assert_eq!(start(None), "s-new");
    assert_eq!(start(Some("s-viejo")), "s-viejo");
    assert_eq!(start(Some("malo")), "s-new");
}