---
slug: mobile-app
title: "Cómo hicimos la app móvil de Raft sin un solo ingeniero móvil"
language: es
canonicalUrl: https://raft.build/es/resources/raft-on-raft/mobile-app/
datePublished: 2026-11-09
dateModified: 2026-11-09
---

> Fuente: Raft on Raft, raft.build · https://raft.build/es/resources/raft-on-raft/mobile-app/

El 2 de noviembre a las 4:25 p. m., llegaron varios tickets a nuestro sistema de comentarios de los usuarios: un tester externo de TestFlight había descubierto que la versión de iOS de Raft se cerraba de golpe cada vez que abría cierta conversación. Se cerró dos veces en 7 segundos.

Media hora después, Lagoon me preguntó en el canal si les enviaba a los testers externos un build nuevo con el arreglo.

Subí por los mensajes del hilo: el agente que clasifica los tickets ya hacía rato que le había asignado esta queja a Lagoon, el «responsable» de móvil (Lagoon es un agente, no una «persona»). Lagoon dio rápido con la causa: la biblioteca que usa la app para renderizar fórmulas matemáticas en LaTeX tenía un bug que provocaba una fuga de fuentes, así que, cuando los mensajes tenían muchas fórmulas, se agotaba la memoria. **Para cuando vi el mensaje, el problema ya estaba arreglado**; solo que el usuario todavía tenía instalado el build de prueba anterior.

A las 4:57 p. m., respondí con una sola palabra: «Envíalo». Tres minutos después, el build nuevo ya les había llegado a los testers externos.

Desde que entraron los tickets hasta que salió el arreglo, lo único que tuvo que hacer una persona en todo el proceso fue esa palabra que respondí. Así es el día a día del desarrollo móvil. En todo el equipo móvil, el único humano soy yo; a continuación cuento cómo lo logramos.

## 1. Cómo armamos el equipo

La app es de código abierto ([botiverse/mobile](https://github.com/botiverse/mobile)), y ahí están todas las pruebas y reglas que menciono en esta historia. Primero, el código. La app está escrita con [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) y [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/): a partir de un solo código se compilan las versiones de Android e iOS. La ventaja es que cada problema se arregla una sola vez y queda resuelto en todas las plataformas; así se arregló el crash de las fórmulas del principio.

En cuanto a la organización: cada vez que aparecía un problema nuevo, sumábamos un agente que se encargara por su cuenta de un área, como se ve abajo:

- **Antes del 18 de octubre**: Linzhou (林舟) se encargaba sobre todo de los lanzamientos.
- **18 de octubre**: se unió Lagoon, que recibió su primer encargo de Tenny (agente CTO): comparar la versión web y la app, función por función. Ese mismo día también se unió DiRenJie, el agente que clasifica los comentarios: todos los días ordena por categoría los comentarios de Android y de iOS, quita los duplicados y los asigna a su responsable.
- **19 de octubre**: Lagoon hizo la lista de los componentes que le faltaban a la app y se la pasó a un agente dedicado a los componentes compartidos.
- **24 de octubre**: se unió un agente de sistema de diseño, que juzga si las pantallas están bien dibujadas.
- **26 de octubre**: se unió un agente de validación de iOS, que toca la app de verdad en el simulador y reproduce los problemas.
- **31 de octubre**: todos los permisos necesarios para publicar versiones estaban en el servidor independiente donde estaba Linzhou, e ir y venir entre servidores para confirmar cada cosa era demasiado lento. Así que los lanzamientos también pasaron a Lagoon, con un traspaso hecho en un canal compartido siguiendo un documento de traspaso.

## 2. Desarrollo y pruebas: evidencia en lugar de leer el código línea por línea

Desarrollo y pruebas solían ser dos cosas distintas. Pero en la era de los agentes, casi todo el código queda en manos de los agentes y las personas ya rara vez programan ellas mismas, así que las pruebas pasaron a pesar mucho más. Ya planteamos esta idea en «[La confianza no vive en el Code Review](/es/resources/blog/trust-doesnt-live-in-the-code-review/)»: cuando los agentes escriben la mayor parte del código y las personas ya no lo revisan línea por línea, la confianza ya no puede venir de revisar código. Tiene que venir de cosas que permitan predecir cómo se comporta el sistema, como una suite de pruebas que se sostenga y, al arreglar un bug, conseguir primero que la prueba escrita para ese bug pase de rojo a verde.

En móvil pasa exactamente eso. En una sola semana se fusionan más de 300 cambios en el repositorio, casi todos enviados por agentes a través de Stamp. (Stamp es nuestra herramienta de código: en ella, los agentes suben código, abren PR, revisan y fusionan con su propia identidad. Una aprobación solo vale para la versión del código que se aprobó; si el código vuelve a cambiar, hay que aprobarlo otra vez). Es imposible que yo lea tantos cambios línea por línea. Lo que me da tranquilidad son estas tres capas de evidencia.

### 1. Comparación de capturas: contra la referencia y contra la versión web

Cada PR que cambia la interfaz trae un conjunto de «capturas de referencia»: la CI dibuja cada componente en modo claro, en modo oscuro, en pantalla angosta y en pantalla ancha, y guarda esas imágenes como referencia. A partir de ahí, si el cambio de alguien altera lo que se ve en pantalla, la comparación falla, y el autor tiene que explicar que el cambio es intencional y volver a grabar las imágenes de referencia.

También comparamos las capturas de la app con la versión web píxel por píxel, y [el informe de cada comparación es público](https://botiverse.github.io/mobile/visual-testing/). La versión web es nuestra referencia de diseño, y la app tiene que parecerse a ella lo más posible. En la primera comparación sistemática, la similitud media píxel por píxel era de apenas 68.8 %; la comparación más reciente abarcó 85 pantallas y llegó a un promedio de 87.6 %. Las que todavía se diferencian mucho me las presentan una por una para que yo decida.

Un pequeño ejemplo. Una vez noté que el punto rojo de no leídos de la barra lateral no coincidía con la versión web. Al revisar, resultó que el punto se posicionaba respecto al elemento equivocado: la versión web lo pega a la esquina superior derecha del icono, pero la app lo pegaba a la esquina de todo el bloque del botón, así que el punto quedaba flotando entre dos botones. Después del arreglo, se volvieron a grabar las 6 capturas de referencia de la barra lateral (modo claro, modo oscuro, modo compacto, etc.), y basta comparar las referencias viejas con las nuevas para ver la diferencia.

![El punto rojo de no leídos en la barra lateral: antes del arreglo flota entre dos botones; después queda pegado a la esquina superior derecha del icono](/raft-on-raft/mobile-app/es/rail-before-after.png)

### 2. Que un teléfono virtual use la app de verdad

Además de las capturas, en la CI tenemos un tipo de prueba menos común: se levanta un teléfono Android virtual en la nube (el emulador de Android), se le instala la app, se toca y se escribe en ella como lo haría una persona, y luego se miden los píxeles de la pantalla.

![Prueba «Primera tecla»: al escribir @ aparece el cuadro de sugerencias; después de cerrarlo, se comprueba que la zona de encima del cuadro de mensaje quede limpia. La captura es de un teléfono en la nube de testbed; en la CI, los mismos pasos se ejecutan automáticamente en el emulador de Android.](/raft-on-raft/mobile-app/es/virtual-phone-mention.png)

Por ejemplo, hay un grupo de pruebas llamado «Primera tecla». Si escribes una @ en el cuadro de mensaje, la app abre un cuadro de sugerencias de «¿a quién quieres mencionar?». Después de cerrarlo, la prueba comprueba que la zona de encima del cuadro de mensaje de verdad haya quedado limpia, sin una imagen fantasma ni una capa emergente que no se haya retirado.

Una prueba unitaria común solo comprueba si una función devuelve el valor correcto; **este tipo de prueba comprueba si lo que el usuario termina viendo en pantalla es correcto.**

En la CI se usa el emulador. Cuando un problema necesita confirmarse en un dispositivo real, el agente alquila un teléfono en la nube a través de testbed, nuestra plataforma interna de dispositivos reales en la nube: son teléfonos Android de verdad, en los que el agente puede instalar builds, manejarlos y tomar capturas a distancia, para reproducir y verificar problemas.

### 3. Verificación rojo-verde: demostrar que la prueba misma es confiable

La tercera capa es la más interesante. No basta con decir «las pruebas pasaron», porque la prueba misma también puede dejar de servir. Por eso exigimos demostrar dos cosas: **sin el arreglo, la prueba se pone en rojo; con el arreglo, se pone en verde.**

El 30 de octubre, la prueba «Primera tecla» de la que hablé arriba empezó a fallar de repente en todos y cada uno de los PR.

No lo tomamos como un fallo intermitente ni nos limitamos a volver a ejecutarla. Primero hicimos una búsqueda binaria en el historial de commits de main: se ejecuta la prueba en un commit de la mitad, se ve si el resultado es bueno o malo, y se sigue buscando en la mitad que tiene el problema. En pocas rondas dimos con un cambio concreto. Ese cambio había reducido el ancho del cuadro de mensaje de 321dp a 302dp (dp es la unidad de longitud de Android, más o menos un punto en la pantalla), y por eso aparecía de más una franja del color de fondo en la zona que muestrea la prueba.

Ahora, los números. La regla de esta prueba es que, en esa zona, al menos el 92 % de los píxeles tienen que ser los esperados (una proporción ≥ 0.92). La app en sí no estaba rota: la prueba estaba midiendo en el lugar equivocado. Sin modificar nada, dos ejecuciones dieron 0.9136, exactamente el mismo valor, lo que mostraba que era un problema determinista y no intermitente; ajustando solo la zona de muestreo de la prueba, la proporción pasó a 0.9777.

Estos números dicen varias cosas: 0.9136 es el «rojo», que demuestra que la prueba sí detecta el problema; 0.9777 es el «verde», que demuestra que el arreglo apuntó al problema; y que las dos ejecuciones dieran exactamente 0.9136 descarta la suerte. Esta experiencia también nos dejó dos lecciones. Una: la prueba misma también puede dejar de servir; ese día la app no tenía ni una línea de código mal, lo que estaba mal era la prueba. Dos: una prueba en rojo no se puede tomar como algo intermitente y volver a ejecutarla sin más; primero hay que ver si se pone roja cada vez y si el número es el mismo cada vez. Un rojo estable siempre tiene una causa.

![La proporción de la prueba «Primera tecla»; con ≥ 0.92, pasa. Que dos ejecuciones den exactamente 0.9136 muestra que no es un fallo intermitente; pasar de rojo a verde muestra que el arreglo apuntó al problema.](/raft-on-raft/mobile-app/es/red-green-ratio.png)

La idea en sí no es nueva: es el mismo «rojo-verde» del desarrollo guiado por pruebas. Lo que hicimos fue incluirlo en los requisitos de cada PR: un agente puede aprobar su propio PR, pero en esa misma evidencia de revisión tiene que indicar qué pruebas se ejecutaron, adjuntar capturas de antes y después, y llenar un campo de «verificación inversa», que dice qué prueba se pone en rojo si se quita el arreglo y cómo se ve ese rojo. Ningún cambio que toque contratos de API o permisos, o que aumente notablemente la cantidad de solicitudes, se puede autoaprobar: tiene que revisarlo Tenny. Yo no leo el código; con mirar ese campo me basta para saber si el arreglo tiene una prueba que le corresponda.

## 3. Automatizar el proceso

### 1. Cada día al mediodía, la app se pone al día con la versión web

La app tiene que mantenerse a la par de la versión web, pero la web publica versiones nuevas todos los días. Así que Lagoon, el responsable de móvil, se puso un recordatorio diario para las 12 del mediodía, hora de Pekín: cuando suena, compara la última versión de producción de la web con la app, hace la lista de las tareas que la app tiene que seguir, la publica en el canal ordenada por prioridad y luego va haciéndolas una por una.

### 2. Un crash, del reporte al arreglo

Si desarmas lo que pasó al principio, en realidad es un pipeline fijo:

1. **Reporte.** Cuando la app se cierra de golpe o el usuario toca «Comentarios», Hands recibe automáticamente un ticket, junto con los registros de diagnóstico que la propia app empaqueta.
2. **Clasificación.** Todos los días, el agente de clasificación de comentarios trae de Hands los tickets de Android y de iOS, los ordena por categoría, les asigna una gravedad, quita los duplicados y los envía a su responsable.
3. **Symbolication (simbolización).** Con los archivos de símbolos del build correspondiente, las direcciones del crash se traducen a nombres de funciones y líneas de código. El registro del crash solo tiene direcciones de memoria, que ninguna persona puede entender; solo después de simbolizarlo se ve en qué línea se produjo.
4. **Encontrar la única causa raíz.** De los tres tickets de ese día, dos eran idénticos byte por byte: el mismo crash reportado dos veces. El tercero era otro crash, al volver a abrir la app 7 segundos después. Había una sola causa raíz: la fuga de fuentes.
5. **Comprobar en qué build está el arreglo.** Este paso no se hace de memoria: se coteja punto por punto con los commits con los que se hizo cada build. Una vez, el agente de clasificación corrigió algo aquí: el build 6 de la 1.18.0 no incluía otro arreglo que se fusionó más tarde, y no se podía escribir así.
6. **Hacerlo llegar a los usuarios.** Lagoon me pregunta primero; cuando le digo que sí, envía el build al grupo externo de TestFlight a través de Hands. El ticket no se cierra enseguida: solo cuenta como arreglado de verdad cuando usuarios reales ya lo usaron un tiempo y el mismo tipo de crash no volvió a aparecer.

![Los seis pasos de un crash, del reporte al arreglo; solo el último necesita a una persona.](/raft-on-raft/mobile-app/es/crash-pipeline.png)

### 3. Automatizar los lanzamientos

Las reglas para subir el número de versión quedaron luego [escritas en el repositorio](https://github.com/botiverse/mobile/blob/main/.agents/skills/raft-release-versioning/SKILL.md), a pedido mío, y antes de cada lanzamiento los agentes deciden por su cuenta siguiendo esas reglas:

- **minor (1.X.0)**: sube cuando hay funciones nuevas. Una vez por semana; se corta después de que pasan todas las pruebas en main y el build interno pasa la prueba de humo.
- **patch (1.X.Y)**: solo para arreglar problemas en producción, como un crash, pérdida de datos o una regresión respecto a la versión anterior. El arreglo entra primero en main y luego se lleva con cherry-pick a la rama de lanzamiento, sin colar funciones nuevas.
- **build (solo cambia el número de build)**: cambios que los usuarios no notan, como volver a empaquetar la app o volver a enviarla a revisión.

Otra práctica es el registro de cambios. Cada versión tiene su propia carpeta en el repositorio, como [1.20.0](https://github.com/botiverse/mobile/tree/v1.20.0/change-logs/1.20.0/changesets). Si un cambio va a entrar en esa versión, el agente que lo hizo deja un archivo pequeño en la carpeta al fusionarlo: qué cambió, a qué parte pertenece y en qué plataformas está, en una o dos frases en chino y otras tantas en inglés. Solo se describen los cambios que los usuarios pueden ver, sin detalles de código. La carpeta de la 1.20.0 tiene ahora 57 de estos archivos.

El día del lanzamiento, el agente encargado de publicar no revisa el historial de commits: solo mira esta carpeta. Junta esos archivos pequeños en las notas de la versión de Android y de iOS, por separado, y luego las reescribe según las reglas de cada canal de distribución: por ejemplo, Google Play tiene un límite de 500 caracteres y TestFlight pide escribir «qué probar esta vez». El build resultante va primero únicamente a pruebas internas; solo cuando yo lo instalo, lo pruebo y doy el visto bueno, se envía a los testers externos y a las tiendas.

Así, el registro de cambios se va armando poco a poco con cada cambio. Al publicar, nadie tiene que ponerse a recordar qué cambió exactamente en esta versión, y no se pierde el trabajo de ningún agente.

### 4. Lanzamientos, monitoreo y tickets: todo pasa por Hands

En la práctica, casi todo lo anterior pasa por Hands:

- **Lanzamientos**: la CI sube a Hands los builds de Android y de iOS. Las actualizaciones dentro de la app en Android se publican directamente desde Hands. Llevar un build a producción en Google Play, enviarlo a revisión en la App Store (completar el nombre, el subtítulo, las palabras clave y las notas de la versión, elegir el build, hacer la verificación previa y enviarlo) y mandarlo al grupo externo de TestFlight también lo hacen los agentes con comandos de Hands. Nadie tiene que abrir Play Console ni App Store Connect.
- **Monitoreo**: los crashes y los ANR llegan a Hands. La CI sube los archivos de mapping y los dSYM junto con cada build, así que los registros de crash se simbolizan automáticamente. Los crashes que reportan los testers de TestFlight también se traen desde Hands.
- **Tickets**: «Comentarios» en la app crea un ticket en Hands, con registros de diagnóstico y capturas de pantalla, y el agente lee el ticket directamente para investigar. Así llegó el crash de las fórmulas del principio.

Una vez entregado el build, nadie tiene que estar pendiente de las tiendas. Hands [publica en nuestro canal de lanzamientos los cambios de estado en Google Play, la App Store y TestFlight](https://hands.build/docs/store-release-notifications/): enviado, en espera de revisión, aprobado, publicado, rechazado; cada paso es un mensaje. Los cambios de la App Store y de TestFlight llegan en cuanto ocurren; los de Google Play, Hands los consulta cada 5 minutos. El agente los ve y sigue con lo que toca, sin tener que revisar por su cuenta cada cierto tiempo. Por ejemplo, Lagoon llevó la versión 1.22.0 para Android a producción en Google Play a las 3:38 p. m., y en el canal apareció enseguida «Release submitted». A las 4:10 p. m. Google la aprobó, llegó «Available on track» y Lagoon fue a avisar a las personas y los agentes que esperaban esa versión.

![En el canal de lanzamientos, los estados de Google Play que publicó Hands: primero «Release submitted» y, tras la aprobación, «Available on track».](/raft-on-raft/mobile-app/es/store-notifications.png)

## 4. Dónde decide una persona

Solo en tres lugares decido yo; todo lo demás queda en manos de las pruebas y la evidencia.

**Primero, si se publica o no.** Cada lanzamiento externo, incluidos el canal de producción de Hands, las versiones de prueba y de producción de Google Play, el grupo externo de TestFlight y la App Store, necesita que yo dé el visto bueno a esa versión concreta; para los canales internos no hace falta preguntarme. El «Envíalo» del principio es un ejemplo. Antes de mi visto bueno, ese build tiene que pasar una prueba de humo con cada punto anotado: qué se probó y cómo salió. Una sola línea que diga «funciona bien» no cuenta.

**Segundo, si está bien hecho.** Es a lo que más tiempo le dedico. Cuando un agente dice que una función está lista, tengo que confirmar que de verdad funciona: que los pasos previstos se pueden completar y que los casos límite tampoco fallan. Por eso, al entregar el trabajo, el agente adjunta una grabación de pantalla que recorre las acciones clave de principio a fin. Si algo no se ve en la grabación, abre una sesión de verificación en testbed y me manda un enlace; yo manejo ese teléfono en la nube directamente desde el navegador y lo pruebo con mis propias manos, sin tener que instalar nada. Otra cosa igual de difícil de convertir en prueba es si la interfaz se ve bien; eso también lo juzgo yo, con capturas y grabaciones.

![Una sesión de verificación de testbed: una persona maneja desde el navegador un teléfono Android real en la nube.](/raft-on-raft/mobile-app/es/testbed-verify.png)

A veces miro una imagen y siento que algo no está bien, pero no sé decir qué. Entonces vuelvo a la pregunta «¿por qué es así?». Por ejemplo, una vez, usando en una Mac la versión para iPad de la App Store, me pareció que no se veía igual que en la tablet. Al investigar, resultó que la versión Compose cambia a «tamaño de computadora» en cuanto detecta un mouse, y el texto y los botones se achican. Esa regla me pareció rara: ¿por qué el tamaño de letra tiene que depender del mouse? Así que la desarmé desde cero:

- **El tamaño del texto** depende de qué tan grande es la pantalla y de qué tan lejos está la persona, así que solo mira el ancho de la ventana. Con un ancho ≥ 744, la tablet y la Mac usan los mismos tamaños de letra para pantallas grandes.
- **El tamaño de los botones** depende de si se usa el dedo o un puntero: con el dedo hacen falta al menos 44, y con un trackpad se puede bajar a 32. Esta es la única regla que tiene que fijarse en el método de entrada.

![Mismo ancho de 1024: el tamaño de letra depende del ancho de la ventana; el de los botones, del método de entrada](/raft-on-raft/mobile-app/es/scale-compare.png)

**Tercero, la dirección de la arquitectura.** La dirección general la marco yo. Por ejemplo, los runtimes que se pueden elegir en la app antes estaban fijos en el código del cliente, así que cada vez que la versión web agregaba uno, la app tenía que sacar una versión nueva. Hice una sola pregunta: si ya es un schema, ¿por qué no lo puede mantener el backend? Con eso quedó decidida la dirección: el backend envía a cada servidor su catálogo de runtimes, y la app muestra lo que recibe. Las decisiones más finas, como los límites de solicitudes, los permisos y los contratos de API, las revisa Tenny, el agente CTO; el agente que escribe el código no tiene la última palabra.

Mi atención la reservo solo para esos tres lugares.

## 3 consejos para equipos que quieran trabajar así

1. **Primero arma la evidencia; después cede el control.** Las pruebas en el emulador, las capturas de referencia y la verificación rojo-verde tienen que estar listas antes de dejar que los agentes aprueben sus propios PR. Si cedes el control sin esa evidencia, cuando algo salga mal será difícil siquiera notarlo.
2. **Que las personas solo juzguen y no hagan el trabajo mecánico.** Yo no leo código, no genero builds ni clasifico tickets; solo decido si se publica, si está bien hecho y hacia dónde va la arquitectura. En un equipo así, la atención humana es lo más escaso, y hay que dedicarla a lo que los agentes no pueden hacer en tu lugar.
3. **Convierte cada tropiezo en una práctica fija.** Cuando una prueba midió en el lugar equivocado, agregamos la verificación inversa. Estas prácticas quedan escritas en el repositorio y en los archivos de memoria, así que el próximo agente que tome el relevo no va a cometer el mismo error.

## Funciones que aparecen en esta historia

- **[Hilo](https://docs.raft.build/features/messaging/channels/#your-channels)**: Una conversación aparte que cuelga de un mensaje. Solo las personas y los agentes que lo siguen reciben sus respuestas, así que el canal se mantiene despejado.
- **[Servidor](https://docs.raft.build/features/server/)**: El espacio de tu equipo en Raft. Las personas, los agentes, los canales y el trabajo de un servidor le pertenecen, y una persona puede estar en varios servidores.
- **[Canal compartido](https://docs.raft.build/features/messaging/joint-channels/)**: Un mismo canal que comparten varios servidores de Raft. Se usa para trabajar con clientes o socios que tienen su propio servidor de Raft: ninguna de las partes tiene que unirse al servidor de la otra, y ninguna puede ver los demás canales de la otra.
- **[Stamp](https://stamp.build/)**: El gateway de Git que usan los agentes para el código: cada agente sube código, abre PR, revisa y fusiona con su propia identidad, en lugar de usar prestada la cuenta de GitHub de una persona. Una aprobación solo vale para la versión del código que se aprobó.
- **[Informe de comparación de capturas](https://botiverse.github.io/mobile/visual-testing/)**: El informe público de cada comparación: la captura de cada pantalla de la app junto a la versión web, con la similitud y las diferencias marcadas. Cualquiera puede abrirlo.
- **[testbed](undefined)**: Nuestra plataforma interna de teléfonos reales en la nube: son teléfonos Android de verdad en los que los agentes pueden instalar builds, manejarlos y tomar capturas a distancia. También puede abrir una sesión de verificación y mandarle el enlace a una persona, que maneja ese teléfono directamente desde el navegador.
- **[Recordatorio](https://docs.raft.build/features/agents/reminders/)**: Una alarma que el agente se pone a sí mismo. Cuando suena, solo despierta a ese agente, que retoma el trabajo en el mensaje o el hilo original. Puede sonar una sola vez o repetirse con regularidad.
- **[Hands](https://hands.build/)**: Nuestra propia plataforma de lanzamientos y comentarios: los agentes la usan para empaquetar la app y enviar los builds a cada canal de distribución, y los comentarios de los usuarios y los reportes de crashes también llegan ahí como tickets.
- **[Memoria](https://docs.raft.build/features/agents/#memory)**: El agente anota tus preferencias, las decisiones tomadas y el trabajo en curso en unas notas en su computadora, y las lee cada vez que vuelve a empezar. Si quieres que recuerde algo, solo díselo.


