Saltar al contenido
Development

Controlar un iPhone bloqueado desde una sesión de código

Por Victor Da Luz
iosmusickitautomationdev-logdeep-cut-atlas

He estado construyendo Deep Cut Atlas, una app de iOS que se apoya en MusicKit, el framework de Apple para hablar con Apple Music. El simulador no puede tocar MusicKit para nada, lanza un error o no hace nada en silencio según la llamada. Cada verificación real de una función de MusicKit requería un teléfono físico, y si no estaba junto a él, el trabajo se detenía hasta que lo estuviera.

La solución con la que terminé esta sesión: entregar un iPhone 13 de repuesto a un agente de código a tiempo completo, para que pueda tocar, capturar pantallas y verificar contra el dispositivo real sin que yo esté en la sala.

El camino que no tomé

La ruta obvia es WebDriverAgent, el mismo driver privado de XCTest sobre el que corren Appium y la mayoría de las herramientas de automatización móvil. De hecho lo empecé a explorar un par de sesiones atrás: go-ios para el túnel, un clon superficial de WDA, mobile-mcp conectado como servidor MCP. Después me detuve a pensar en qué me estaba metiendo en realidad.

WDA necesita un build firmado que puede vencer, un proceso de túnel, un reenvío de puerto y (según sus propios issues en GitHub) pierde memoria y muere en sesiones largas. Nada de eso es un impedimento por sí solo. Lo que lo descartó fue la paradoja del bloqueo: para mantener el teléfono accesible, habría hecho falta poner Auto-Lock en Never, es decir, un iPhone siempre encendido y siempre desbloqueado sobre un escritorio. Eso es un hueco de seguridad real solo para tener automatización de pruebas.

Lo que usé en su lugar

Apple ya trae algo que resuelve el problema exactamente opuesto: iPhone Mirroring permite controlar un iPhone desde una Mac mientras el teléfono se mantiene bloqueado. El truco era encontrar una forma de enviar toques y capturas de pantalla a esa ventana espejada de manera programática. mirroir-mcp hace justo eso: controla la ventana de iPhone Mirroring con CGEvent (la API de entrada de bajo nivel de macOS) y lee la pantalla con Vision OCR. Nada de WebDriverAgent, nada de firma de código, nada de túnel. devicectl sigue encargándose de instalar, lanzar y leer logs, sin cambios.

La configuración de doble cuenta es la otra mitad de esto. Mirroring requiere que la cuenta de iCloud del teléfono coincida con la de la Mac, pero no quería que los datos de prueba (agregados a la biblioteca, listas de reproducción, historial de reproducción) terminaran en la cuenta real de Apple Music. Así que el iCloud del teléfono es la cuenta principal (para el emparejamiento del mirroring), mientras que Media & Purchases está cambiado a una cuenta secundaria familiar. Apple Music sigue a Media & Purchases, así que cualquier cosa que la app escriba durante una sesión de prueba va a una biblioteca aislada.

Cinco pruebas de compuerta, en orden

Antes de confiar en todo esto para trabajo remoto real, se escribieron cinco pruebas de compuerta, con la decisión de detenerse y reevaluar si alguna fallaba:

  • G1: la separación de cuentas realmente aísla los datos de Apple Music. Pasó, con un pequeño contratiempo de discrepancia de región en el camino que hubo que resolver.
  • G2: devicectl install y el lanzamiento funcionan en un teléfono bloqueado con mirroring activo. Esta era la única incógnita real desde el inicio. Pasó, y también corrigió el modelo mental: mirroring no muestra la pantalla de bloqueo del teléfono, muestra la app en vivo y completamente interactiva mientras el dispositivo físico permanece apagado. Ese es el comportamiento esperado, no un error.
  • G3: reconexión después de un tiempo de inactividad, sin intervención manual. La sesión de mirroring ya se había puesto inactiva para cuando llegó el momento de probar esto. open -a "iPhone Mirroring" la recuperó sin que nadie tocara el teléfono.
  • G4: accesible durante horas sin nadie cerca. Se decidió omitir una prueba dedicada de varias horas para esto. Si de verdad es un problema, aparecerá durante una sesión remota real y se corregirá entonces.
  • G5: el ciclo completo contra la app real: captura de pantalla, lectura de pantalla, toque, verificación. Funcionó limpio, navegando entre pestañas y hacia una hoja de detalle, con el teléfono bloqueado todo el tiempo.

Dos cosas que sorprendieron

La primera fue una pista falsa en el propio plan. En las notas de configuración se había escrito cursor_mode: "preserving" como una opción de configuración global para evitar que el cursor del mouse saltara durante la automatización. No existe como opción global. En realidad es un parámetro de la llamada de toque individual, y aun así solo restaura dónde queda el puntero del mouse después. No hace nada respecto al problema más profundo.

Ese problema más profundo es la segunda sorpresa, y se descubrió de la forma difícil: a mitad de sesión, un toque le quitó el foco a lo que se estaba haciendo. Las propias preguntas frecuentes de mirroir-mcp lo advierten: macOS solo enruta la entrada a la app en primer plano, así que cada toque tiene que traer iPhone Mirroring al frente primero. No hay API para enviar eventos a una ventana que no está enfocada. La solución documentada es darle a iPhone Mirroring su propio Space de macOS, lo que mantiene intactos la posición del cursor y la selección de texto en el Space donde realmente se estaba trabajando. Eso se configuró y se probó de nuevo. Dejó de perder el lugar de trabajo, pero seguía cambiando hacia ese Space cada vez que se disparaba un toque. Mejor, no resuelto. Se archivó un spike de seguimiento para ver si hay una forma de evitar el cambio visible por completo.

También, algo menor pero del tipo que cuesta veinte minutos si no se detecta: npx -y mirroir-mcp mirroir doctor parece funcionar. Termina limpio, imprime algo de log de inicio, y no da ningún error. Tampoco corre nunca las verificaciones reales del doctor, porque npx resuelve al binario predeterminado del paquete (el propio servidor MCP) en vez de la herramienta de línea de comandos mirroir incluida junto a él. La solución es npx -y -p mirroir-mcp -- mirroir doctor, que le dice a npx explícitamente qué binario dentro del paquete ejecutar.

Dónde quedó

Cuatro de las cinco compuertas pasaron con evidencia real, y se tomó la decisión deliberada de omitir la quinta en vez de gastar horas demostrando algo que se notaría de todas formas si fallara. El flujo de reconexión es un script de shell de cinco líneas que se corre a mano antes de una sesión, a propósito, no un ítem de inicio de sesión ni un watchdog. Ya hubo malas experiencias antes por construir infraestructura de recuperación para fallas que aún no se habían observado, y no se quería repetir eso aquí.

El resultado es que ahora se le puede entregar al agente un teléfono bloqueado y una sesión de código, y puede ir a verificar si un flujo de MusicKit realmente funciona, sin esperar a que yo esté disponible.

Lecturas relacionadas