Saltar al contenido
Development

Enseñarle a un agente a probar mi app de Tauri a clics

Por Victor Da Luz
taurirusttestingdev-loggreenhouse

Cada cambio de interfaz en Greenhouse chocaba con la misma pared. Se cambiaba un diálogo, los tests del backend pasaban, el verificador de tipos quedaba conforme, y después la verificación se reducía a levantar el build de desarrollo y hacer clic por toda la app en persona. El agente no podía hacer esa última parte, así que se detenía y devolvía el recorrido a clics. Aceptable una vez. Molesto a la décima.

Lo que los tests no cubren es la costura: ¿hacer clic en “Capture” realmente llama al comando capture_idea y escribe una fila, o solo se probó que la función funciona cuando un test la llama directamente? Eso necesita un clic real y una vuelta real de IPC en el mismo movimiento. Así que se buscó darle al agente una forma de manejar la app en ejecución.

macOS es la parte difícil

Tauri tiene una historia oficial de WebDriver, pero en escritorio es solo para Windows y Linux. macOS no tiene ahí un driver de WKWebView, así que el camino documentado simplemente no aplica en esta máquina. Por un tiempo la sabiduría aceptada era “capturar la capa web con IPC simulado y darlo por hecho”, lo cual verifica el layout pero nunca toca el comando real de Rust.

Lo que sí funciona en macOS hoy es un plugin de la comunidad, tauri-plugin-webdriver-automation. Corre un pequeño servidor HTTP de WebDriver dentro de la app en builds de debug, y una CLI complementaria lo expone con el protocolo estándar W3C en localhost. La app compila contra Tauri v2 sin quejas. Buena señal.

El desvío de seguridad

Antes de conectarlo de verdad se leyó el código fuente del plugin, y fue una buena decisión. Dos cosas resaltaron. El servidor de WebDriver no tiene autenticación y expone ejecución arbitraria de JavaScript e IPC arbitrario, lo cual es inherente a cualquier driver pero vale la pena decirlo con claridad. Y el plugin trae consigo la feature dynamic-acl de Tauri.

Ese segundo punto causó un problema de forma inesperada. El primer intento agregó el plugin como una dependencia plana de target de macOS. La unificación de features de Cargo entonces activó en silencio dynamic-acl también en el Tauri de release, y compiló todo el servidor de WebDriver dentro de los binarios de release, aunque nunca corriera ahí. Una herramienta de pruebas filtrando una feature que debilita capacidades hacia producción es exactamente el tipo de cosa que no se nota hasta mucho después.

La solución fue convertirlo en un opt-in real. El plugin ahora es una dependencia opcional detrás de una feature de cargo llamada automation, registrada solo bajo #[cfg(all(debug_assertions, feature = "automation"))]. Un build normal no trae nada de eso. Se verificó con cargo tree: sin la feature, el crate ni siquiera aparece en el grafo.

Dos inconvenientes que se comieron una tarde

Primero: un binario de debug corrido directamente carga su frontend desde la URL de desarrollo de Vite, no desde recursos empaquetados. La app se lanzó bajo el driver, la ventana apareció en blanco, y cada llamada de “buscar elemento” quedaba colgada para siempre sin error. La sesión se creaba bien, y después nada. Tomó más tiempo del esperado darse cuenta de que el webview era simplemente una página en blanco esperando un servidor de Vite que no estaba corriendo. Arrancar Vite primero y todo funciona.

Segundo: no se puede manejar el propio onboarding. El flujo de primer arranque de Greenhouse usa un diálogo nativo de selección de carpeta, y un WebDriver no puede tocar un diálogo del sistema operativo. Así que se agregó un override de entorno, solo para debug, que hace que la app pase directo el onboarding hacia una bóveda descartable. Pequeño, contenido, y significa que el driver puede empezar cada corrida desde un estado conocido.

Funciona

El resultado es un script corto que el agente corre por su cuenta. Lanza la app contra una bóveda temporal, hace clic en “Capture an idea”, escribe un nombre, envía, y después revisa los resultados reales: el diálogo de confirmación muestra la ruta de la carpeta nueva, la barra superior cambia a “Captured today” a partir de una lectura fresca de la base de datos, y en disco hay una fila real de SQLite y un archivo markdown. Ningún mock en toda esa cadena.

Se descartó el envoltorio de MCP en lenguaje natural que algunos agregan encima. Quiere vivir en cada sesión de cada proyecto, lo cual es más privilegio permanente del que se quiere para una herramienta que solo hace falta acá. Un script de driver confirmado en el repositorio da el mismo resultado con una superficie mucho más chica.

Lo que gusta de cómo terminó esto: el agente ahora puede cerrar su propio ciclo en un cambio de interfaz en vez de dejarlo pendiente, y la única capacidad que podía haberse filtrado a producción queda detrás de una bandera de feature que está apagada por defecto. La lección que va a quedar es la aburrida. Leer el código fuente de la dependencia antes de confiar en ella, y vigilar lo que la unificación de features de Cargo hace por atrás.

Apéndice: el nivel barato, y el panorama de tres niveles

El driver de WebDriver es lo real, pero es pesado: compilar la app, arrancar Vite, abrir una ventana, manejarla. No es lo que se quiere corriendo en cada push. Así que se agregó el nivel barato debajo, tests de componentes con vitest que renderizan el componente real de Svelte, disparan un clic real, y simulan lo único que no puede correr en CI: la llamada a Rust.

El truco es simular en el límite, no en el módulo. El frontend de Tauri le habla a Rust a través de una sola función invoke, y @tauri-apps/api trae un helper mockIPC que la intercepta. Así que un test queda así: renderizar el diálogo de contacto, escribir una nota, hacer clic en “Log touch”, y después afirmar dos cosas, que invoke se llamó con record_touch y los argumentos correctos, y que el DOM reaccionó. Clic real, lógica real del componente, actualización real del DOM, backend simulado. Corre sin interfaz en unos dos segundos.

Dos cosas dieron problemas. Primero, jsdom y el elemento nativo <dialog>. Ambos diálogos llaman a showModal() apenas se montan, y según la versión de jsdom ese método puede no existir, así que el componente lanza un error antes de que corra una sola aserción. Un pequeño polyfill protegido en el setup de tests lo resuelve. Segundo, el verificador de tipos empezó a fallar, ahora también revisaba los archivos de test y no sabía qué eran expect o describe. Una línea en tsconfig para traer los globales de vitest y jest-dom, y quedó conforme otra vez.

Lo que gusta es la forma en la que terminó todo. Ahora hay tres niveles de verificación de frontend, del más barato al más caro: tests de componentes con IPC simulado que corren en cada push y detectan “el botón dejó de llamar al comando”; una pasada de capturas con datos simulados para “¿el layout llena la ventana?”; y el driver completo de WebDriver para “¿hacer clic acá realmente escribe una fila?”. Cada uno cubre lo que el más barato no puede, y se recurre al más caro solo cuando hace falta. Ese pareció el lugar correcto para detenerse.

Lecturas relacionadas

Development

La carpeta que se quedó donde estaba

Una revisión de código sacó a la luz un invariante que la función de adoptar/importar de Greenhouse rompía en silencio, y la corrección que la volvió aburrida otra vez.

Leer
Development

Dos notas que nadie vio jamás

Greenhouse guardaba fielmente una nota de captura y una nota de traspaso, y no mostraba ninguna de las dos: una no tenía campo en el tipo de wire, la otra tenía un comando funcional que nadie llamaba.

Leer
Development

El escaneo que se ofreció a importarse a sí mismo

Un escaneo de importación que encontró la propia estructura interna de la app, y luego volvió a ofrecer una carpeta que ya había importado - dos versiones de la misma conversación faltante entre capas.

Leer