Pular para o conteúdo
Development

Ensinando um agente a testar cliques no meu app Tauri

Por Victor Da Luz
taurirusttestingdev-loggreenhouse

Toda mudança de UI no Greenhouse esbarrava na mesma parede. Eu mudava um diálogo, os testes de backend passavam, o type-checker ficava satisfeito, e aí a verificação se resumia a eu mesmo abrir o build de desenvolvimento e clicar em tudo manualmente. O agente não conseguia fazer essa última parte, então ele parava e devolvia o clique-a-clique pra mim. Tranquilo na primeira vez. Irritante na décima.

A coisa que os testes não cobrem é a costura: clicar em “Capture” realmente chama o comando capture_idea e escreve uma linha, ou eu só provei que a função funciona quando um teste a chama diretamente? Isso exige um clique de verdade e uma ida e volta de IPC de verdade, no mesmo movimento. Então me propus a dar ao agente um jeito de controlar o app em execução.

O macOS é a parte difícil

O Tauri tem uma solução oficial de WebDriver, mas no desktop isso vale só para Windows e Linux. O macOS não tem um driver de WKWebView ali, então o caminho documentado simplesmente não se aplica à minha máquina. Por um tempo, o senso comum era “tirar screenshot da camada web com IPC mockado e dar como resolvido,” o que verifica o layout mas nunca toca o comando real em Rust.

O que de fato funciona no macOS hoje é um plugin da comunidade, tauri-plugin-webdriver-automation. Ele roda um pequeno servidor HTTP WebDriver dentro do app em builds de debug, e uma CLI complementar expõe isso com o protocolo W3C padrão em localhost. O app compila contra o Tauri v2 sem reclamar. Bom sinal.

O desvio de segurança

Antes de integrar o plugin de verdade, li o código-fonte dele, e ainda bem que li. Duas coisas chamaram atenção. O servidor WebDriver não tem autenticação e expõe execução arbitrária de JavaScript e de IPC, o que é inerente a qualquer driver, mas vale dizer isso claramente. E o plugin puxa a feature dynamic-acl do Tauri.

Essa segunda me pegou de um jeito que eu não esperava. Minha primeira tentativa adicionou o plugin como uma dependência simples de target macOS. A unificação de features do Cargo então habilitou silenciosamente o dynamic-acl também no meu Tauri de release, e compilou o servidor WebDriver inteiro dentro dos binários de release, mesmo que ele nunca rode ali. Uma ferramenta de teste vazando uma feature que enfraquece a segurança para produção é exatamente o tipo de coisa que você só percebe muito depois.

A correção foi tornar isso um opt-in de verdade. O plugin agora é uma dependência opcional atrás de uma feature automation do cargo, registrada só sob #[cfg(all(debug_assertions, feature = "automation"))]. Um build normal não puxa nada disso. Verifiquei com cargo tree: sem a feature, o crate nem aparece no grafo.

Duas pegadinhas que consumiram uma tarde

Primeiro: um binário de debug executado diretamente carrega seu frontend da URL de dev do Vite, não dos assets embutidos. Eu abri o app sob o driver, a janela apareceu em branco, e toda chamada de “find element” ficava travada para sempre sem erro nenhum. A sessão era criada normalmente, e depois nada. Demorei mais do que gostaria de admitir para perceber que a webview era só uma página em branco esperando um servidor Vite que não estava rodando. Basta iniciar o Vite primeiro que tudo funciona.

Segundo: você não consegue controlar o próprio onboarding. O fluxo de primeira execução do Greenhouse usa um diálogo nativo de seleção de pasta, e um WebDriver não consegue tocar num diálogo do sistema operacional. Então adicionei um override de ambiente só para debug que faz o app pular direto o onboarding e cair num vault descartável. Pequeno, contido, e significa que o driver consegue começar toda execução a partir de um estado conhecido.

Funciona

O resultado é um script curto que o próprio agente executa. Ele abre o app contra um vault temporário, clica em “Capture an idea,” digita um nome, envia, e então checa os resultados reais: o diálogo de confirmação mostra o novo caminho da pasta, a barra superior muda para “Captured today” com base numa leitura fresca do banco, e no disco existe uma linha real de SQLite e um arquivo markdown. Nenhum mock em nenhum ponto dessa cadeia.

Deixei de lado o wrapper MCP em linguagem natural que algumas pessoas colam por cima. Ele quer viver em toda sessão, em todo projeto, o que é mais privilégio permanente do que eu quero dar para uma ferramenta que só uso aqui. Um script de driver versionado no repositório me dá o mesmo resultado com uma superfície muito menor.

O que eu gosto de onde isso foi parar: o agente agora consegue fechar sozinho o próprio ciclo numa mudança de UI, em vez de me deixar isso pendurado, e a única capacidade que poderia ter vazado para produção fica atrás de uma feature flag desligada por padrão. A lição que eu vou guardar é a chata. Leia o código-fonte da dependência antes de confiar nela, e preste atenção no que a unificação de features do Cargo faz pelas suas costas.

Adendo: a camada barata, e o panorama de três camadas

O driver de WebDriver é a coisa real, mas é pesado: compilar o app, iniciar o Vite, abrir uma janela, controlá-la. Não é o que eu quero rodando a cada push. Então adicionei a camada barata por baixo dele - testes de componente com vitest que renderizam o componente Svelte de verdade, disparam um clique de verdade, e mockam a única coisa que não pode rodar em CI: a chamada para o Rust.

O truque é mockar na fronteira, não no módulo. O frontend do Tauri fala com o Rust através de uma única função invoke, e o @tauri-apps/api traz um helper mockIPC que a intercepta. Então um teste fica assim: renderiza o diálogo de toque, digita uma nota, clica em “Log touch,” e então verifica duas coisas - que invoke foi chamado com record_touch e os argumentos certos, e que o DOM reagiu. Clique real, lógica de componente real, atualização de DOM real, backend falso. Roda sem interface gráfica em cerca de dois segundos.

Duas coisas me pegaram. Primeiro, o jsdom e o elemento nativo <dialog>. Meus dois diálogos chamam showModal() no momento em que montam, e dependendo da versão do jsdom esse método pode nem existir, então o componente lança um erro antes de uma única assertion rodar. Um pequeno polyfill protegido no setup de testes resolve isso. Segundo, meu type-checker começou a falhar - ele passou a checar os arquivos de teste também e não sabia o que eram expect ou describe. Uma linha no tsconfig para trazer os globais do vitest e do jest-dom, e ele ficou satisfeito de novo.

O que eu gosto é do formato em que tudo se acomodou. Agora tenho três camadas de verificação de frontend, da mais barata para a mais cara: testes de componente com IPC mockado que rodam a cada push e pegam “o botão parou de chamar o comando”; uma passada de screenshot com dados mockados para “o layout preenche a janela”; e o driver completo de WebDriver para “clicar nisso realmente escreve uma linha.” Cada uma cobre o que a mais barata não consegue, e eu só recorro à mais cara quando preciso. Pareceu o lugar certo para parar.

Leitura relacionada

Development

A pasta que ficou parada

Uma revisão de código encontrou um invariante que a feature de adotar/importar do Greenhouse quebrava silenciosamente, e a correção que tornou tudo chato de novo.

Ler
Development

O scan que se ofereceu para importar a si mesmo

Um scanner de importação que encontrou a própria estrutura interna do app, e depois voltou a oferecer uma pasta que tinha acabado de importar, duas versões da mesma conversa que faltava entre camadas.

Ler

Você também pode achar útil

Proton

Proton Drive

Armazenamento em nuvem criptografado, da equipe por trás do Proton Mail.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
Proton

Proton VPN

VPN comercial com filtragem NetShield e interruptor de desligamento automático.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
Proton

Proton Pass

Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais