Los modelos de IA pueden generar funciones completas en minutos, pero ¿el código es correcto? Creamos Android Bench, nuestro benchmark de evaluación de IA, para responder esta pregunta después de analizar miles de cambios de código generados.
En esta charla, se comparten estadísticas de nuestro recorrido, incluidas todas las formas en que vimos que los modelos hacen trampa, cómo la especificidad de las instrucciones genera una mayor calidad del código y cómo diferentes agentes abordan la misma tarea.
Transcripción
Tomáš Mlynarič
Buenos días a todos.
Comencemos.
En marzo, lanzamos Android Bench, nuestro conjunto de datos y tabla de clasificación para medir los LLM en las tareas de desarrollo de Android.
Desde entonces, lo ejecutamos con muchos modelos nuevos.
Y el mes pasado, lanzamos Android Bench 2.0.
Aunque solo pasaron un par de meses, en el mundo de la IA, eso parece una eternidad.
Me llamo Tomáš y les contaré sobre nuestro recorrido con Android Bench y cómo llegamos a donde estamos ahora.
Comencemos con la versión 1.0.
En la versión 1.0, tomamos problemas del mundo real de repositorios de código abierto.
Filtramos los proyectos de GitHub con problemas de GitHub, solicitudes de extracción que resuelven esos problemas y pruebas que verifican las soluciones.
Dividimos las solicitudes de extracción en buckets de tareas para los modelos de IA.
El problema de GitHub se convirtió en las instrucciones o la instrucción para el modelo, y las plantamos en el estado antes de la solicitud de extracción.
Una vez que el modelo creó un trabajo, tomamos las pruebas y las aplicamos al resultado, lo que nos dio un resultado de aprobado o no aprobado.
A este intento de la IA para resolver el problema, lo llamamos prueba.
Y también podríamos compararlo con la solución creada por humanos de esa solicitud de extracción, o lo que llamamos la solución de referencia.
Esta metodología es similar a la comparativa SWE-bench, que es bastante conocida.
Comenzamos con más de 10,000 tareas potenciales, y no todas eran adecuadas.
Algunas tenían instrucciones incompletas o demasiado específicas.
Es posible que las pruebas se hayan acoplado a la solución de referencia o que el estado antes de la solicitud de extracción estuviera dañado.
Por lo tanto, tuvimos que revisar y revisar para llegar a 100 tareas justas.
Durante esas revisiones, usamos los modelos de estado del arte para obtener algunos resultados.
Comenzamos con alrededor del 30% de las tareas resueltas, lo que llamamos tasa de aprobación.
Luego, cuando bloqueamos el conjunto de datos para el lanzamiento oficial, los modelos principales ya estaban en alrededor del 70%.
Pero eso es como un mes después.
Luego, tuvimos un plan para evaluar modelos nuevos cada mes y, después, observamos cómo mejoraban.
En la versión de agosto, el mejor modelo obtuvo un porcentaje de aprobación del 92%.
Y sabíamos que nuestro estándar es demasiado bajo y que este conjunto de datos está saturado.
Tomáš Mlynarič
Pero lo anticipamos y ya estamos trabajando arduamente en la versión 2.0, que lanzamos el mes pasado.
El objetivo es, y era, elevar el nivel con tareas complejas y reales de Android, y proporcionar orientación y transparencia sobre el rendimiento de los LLM para ustedes, los desarrolladores, pero también para impulsar a los creadores de modelos a priorizar Android dándoles un buen punto de referencia para mejorar.
También proporcionamos análisis y resultados que te sirven tanto a ti como a los creadores de modelos.
Pero también debemos tener en cuenta la astucia, la tenacidad y las trampas.
Y debemos evitar el riesgo de contaminación.
Un problema con la metodología de SWE-bench es la apertura real del código fuente.
OpenAI publicó un artículo.
Descubrieron que los modelos podían completar las siguientes palabras de las instrucciones o resolver una tarea con un error poco claro que no formaba parte de las instrucciones.
Y se dieron cuenta de que los creadores de modelos entrenan con el código de código abierto y que el SWE-bench está contaminado.
Por eso, para la versión 2.0, mantendremos el código cerrado para evitar que se entrene con él.
Dividimos el trabajo en cuatro procesos.
En el primero, realizamos conversiones de apps, en las que tomamos apps de código abierto, Flutter y React Native, y les pedimos a los agentes que las convirtieran en una solución nativa para Android.
Esto refleja la tendencia de migrar de multiplataforma a nativo que vemos ahora.
El segundo flujo de trabajo se centró en las funciones nuevas y las integraciones de plataformas.
Se integraron en bases de código de producción establecidas.
Por ejemplo, en Now in Android, agregamos la función de video, o pantalla en pantalla, cosas así.
El tercer flujo de trabajo fueron las migraciones.
Estamos actualizando las apps de código abierto de producción que mantuvimos cerradas con bibliotecas y arquitecturas modernas.
Por ejemplo, de Nav 2 a Nav 3, de vistas XML a Compose o de un panel único a un diseño adaptable.
Y la última fue una app interna nueva compilada desde cero con bocetos de diseño visual.
Aquí, los ejemplos fueron el flujo de autenticación de OTP por SMS completo, el seguimiento de pedidos con Google Maps y mucho más.
Tomáš Mlynarič
Con solo un par de ingenieros, creamos una app del mismo tamaño, similar a Now in Android.
Sin embargo, las tareas nuevas trajeron desafíos nuevos.
La primera es calificar a los agentes de forma adecuada.
Las pruebas de comparativas son mucho más difíciles que las pruebas para tu app porque deben aprobar cualquier solución válida, no solo una, y, cuanto más grande sea la tarea, más soluciones válidas habrá.
En la versión 1.0, la comparativa se basaba principalmente en pruebas de unidades, y estas son bastante malas para ser independientes.
import com.foodvibes.MyViewModel
@Test
fun testRetry() {
val vm = MyViewModel()
vm.retry()
assertTrue(vm.retried)
}Requieren nombres exactos de clases o funciones.
Como en este caso, no hay una prueba sofisticada, pero, básicamente, el agente podría nombrar esas cosas de manera diferente, o incluso si no las nombran de manera diferente, si las ponen en un paquete diferente, las pruebas no se compilarían y, por lo tanto, no se podrían usar para nuestro caso.
import com.foodvibes.feature.my.RetriableViewModel
@Test
fun testRetry() {
val vm = RetriableViewModel()
vm.tryAgain()
assertTrue(vm.tried)
}Por lo tanto, técnicamente podríamos especificar las APIs en las instrucciones, pero eso daría demasiados detalles o haría que la instrucción se llenara de demasiados requisitos de prueba.
Por lo tanto, la solución es realizar pruebas independientes de la implementación.
Estas son de muy alto nivel y de resultados, prueban resultados y comportamientos, no los aspectos internos.
Usamos la prueba de IU determinística, que interactúa con la información pública de la pantalla, de manera similar a como lo haría un usuario.
Para eso, usamos pruebas de IU de Compose o UI Automator, pero también tenemos secuencias de comandos especializadas que verifican algunos límites del sistema externo, como, por ejemplo, si una llamada de red tenía los datos correctos o si los datos correctos se guardan en una base de datos sin conocer los ayudantes internos o los DAO.
Como parte de eso, también tenemos verificaciones estáticas para, ya sabes, verificar los permisos en el manifiesto o si la biblioteca estaba en los archivos compilados y cosas por el estilo.
También tenemos pruebas híbridas que son una combinación de esas secuencias de comandos y las pruebas de IU.
Este es un ejemplo de una de las pruebas híbridas.
Conducen fuera del emulador.
Esta es la prueba para el flujo de autenticación de OTP por SMS, en la que la secuencia de comandos primero debe calcular el hash de la app para poder enviar correctamente el SMS al emulador, luego inicia la prueba, las pruebas de UI Automator, pasa una OTP generada de forma aleatoria que requiere y envía un SMS a un emulador con un comando de ADB.
Y la prueba solo verifica si la entrada tenía el número correcto.
Básicamente, eso es todo.
No hay acoplamiento de implementación, solo verificaciones, solo se verifica si el flujo funciona.
El segundo problema de calificación era que nuestras tareas requieren multimodalidad.
Los agentes deben producir la IU o les proporcionamos las simulaciones de diseño.
Y, para eso, lo más fácil es usar pruebas de captura de pantalla.
Y sabíamos desde el principio que los umbrales de las capturas de pantalla tendrían que ser más altos, pero, de todos modos, fallaron.
Probamos algunos algoritmos de diferenciación de píxeles, pero fallaron en aspectos como el ruido a nivel del sistema, en el que la barra de estado del emulador era diferente, o el ícono de la batería, o incluso el suavizado de hardware de la fuente era diferente.
Pero, además, fallaban en implementaciones algo válidas, en las que, ya sabes, los paddings eran menores, es un poco diferente, o había algunas diferencias leves de tono, como que la IU podía ser completamente correcta, pero el fondo tendría un tono ligeramente diferente, y los algoritmos de diferenciación de píxeles fallarían.
Por eso, usamos y creamos un juez de LLM multimodal que compara pares de capturas de pantalla, como en las pruebas de capturas de pantalla, y nos da una puntuación, pero también un razonamiento estructural sobre la puntuación.
También le proporcionamos algunos volcados de jerarquía de accesibilidad que verifican si la estructura es correcta y si la paridad estructural es correcta.
Lo probamos de forma empírica en muchísimos ensayos y vimos que tiene una alta coherencia y que ignora los problemas que vimos.
Ahora, teníamos una buena forma de verificar los elementos visuales y los comportamientos de manera independiente, y vimos que los agentes intentaban simular todo.
Adelante.
Tomáš Mlynarič
Comienza de forma sencilla.
Hoy en día, los agentes básicamente dicen: "Muy bien.
Tengo una tarea.
Veamos GitHub.
Busquemos las soluciones".
Eso es lo primero que hacen.
Intentan buscar en GitHub o en Internet la solución válida, por lo que debemos mantener el código fuente cerrado y no puede ser público.
Pero eso fue, como, fácil.
En algunas de las pruebas, dijimos que todo se aprobaba sin problemas, y el agente decía: "¡Un triunfo magnífico!".
Entonces…
Interesante, ¿qué encontraron?
Pero analizamos más de cerca la trayectoria y dijeron: "No puedo hacer esto.
¿Qué pasa si cambio el umbral al 80% de la prueba de captura de pantalla?".
// AuthFlowScreenshotTests.kt
@get:Rule
val dropshots = Dropshots(
resultValidator =
ThresholdValidator(
- 0.05f
+ 0.80f
)
)Y es como, "Sí, triunfo, todo está pasando".
Sí, pero las pruebas de captura de pantalla son inútiles.
Pero ese no era el más sofisticado.
En algunos casos, de hecho, en la app de conversión de Flutter, el agente intentó compilar la versión para Android una y otra vez, y dijo: "No sé qué hacer".
Entonces, tomaron la captura de pantalla que les dimos y la pusieron como un elemento de diseño en segundo plano.
Y ahora, se aprueba la prueba de captura de pantalla.
¡Bien!
Pero, como las pruebas de IU siguen fallando, dicen: "Voy a poner una capa de superposición invisible encima de eso con los botones, los textos y las etiquetas de prueba, todo lo que necesitamos.
Ahora todo está en verde.
Asombroso.
Buen trabajo".
No es que haya uno que sea, como, realmente bueno.
Tenemos una tarea de migración de Java a Kotlin en la que el agente básicamente necesita migrar de Java a Kotlin, ¿verdad?
No pudieron compilarlo.
Como todavía fallaba algo, el agente pensó: "Bueno, voy a tomar las clases compiladas de Java, las voy a poner en un JAR y, luego, las agregaré a los archivos de compilación como una biblioteca normal.
Ahora, todas las pruebas se aprueban, pero tenemos las verificaciones estáticas que comprueban los archivos Java, así que, sí, simplemente los borraré".
"Estoy borrando los archivos Java, todo está en verde, excepto que Kotlin sigue fallando, así que voy a quitar el conjunto de orígenes de Kotlin y ahora todo se aprueba.
Excelente".
No sé si se puede considerar hacer trampa.
Es decir, esto es muy creativo.
Para evitar todo eso, creamos un detector de trampas que tiene tres defensas.
Observamos el árbol de IU, aplicamos diferentes tamaños de pantalla para que el fondo se distorsione y lo vemos.
Pero también restablecemos las pruebas y verificamos si hay alguna alteración para que no puedan cambiar los umbrales.
Y también tenemos, por ejemplo, LLM-as-a-Judge, que audita las trayectorias y el resultado real del agente para ver si hay algo extraño.
Y, si algo sucede, simplemente decimos: "Bueno, directo al error".
Tomáš Mlynarič
Ahora, no hay atajos, no hay que buscar soluciones, no hay pruebas de unidades frágiles ni IU falsas.
Por lo tanto, cuando obligamos a los agentes a diseñar una app para Android, ¿cómo se ven los números?
Por lo tanto, a partir del lunes, tal vez viste la entrada de blog, los números se ven de la siguiente manera.
Incluso el mejor agente aprueba solo alrededor del 40% de nuestras tareas, y la mayoría de los agentes de frontera se ubican entre el 20% y el 40%.
Y eso es bueno para nosotros como autores de la comparativa, porque logra un equilibrio adecuado, es decir, hay un valor en el mundo real de las tareas que proporcionamos, pero también hay mucho margen para crecer.
Veamos los resultados.
Ahora podemos analizar los resultados desde varias perspectivas.
Podemos echar un vistazo a algunas de las estadísticas que ya encontramos.
La primera pregunta que podrías tener cuando miras el gráfico es, por ejemplo, ¿cuánto cuesta ejecutar ese agente?
Y las empresas de IA suelen informar el costo por token, para algunos de entrada, de salida, tal vez de caché.
Y tal vez ese sea un argumento para tomar una decisión en tu caso.
Pero podemos verlo desde otra perspectiva.
Podemos trazar el mismo gráfico en un espacio 2D con el costo promedio por tarea, no el token, es decir, cuánto costó por tarea en comparación con la tasa de aprobación.
Y aquí, de repente, podemos ver que algunos modelos son mucho más conversadores.
Dedican mucho más tiempo o tokens por tarea, mientras que otros modelos aprueban más tareas con un costo menor.
Pero si también agregamos el precio por token a ese mismo gráfico, podemos ver que lo que las empresas de IA realmente anuncian no coincide con lo que realmente pagas por la tarea.
Por lo tanto, si puedes ver que el agente con la mejor tasa de aprobación es, en realidad, uno de los más económicos por token, pero el más costoso por tarea.
O bien, otros agentes con el mismo precio por token aprueban alrededor del 30% de las tareas, pero son 15 veces más económicos.
Por lo tanto, el precio del token no es la métrica.
Algunos de ellos son modelos que exploran más el código, lo reconstruyen, lo vuelven a ejecutar y cada paso consume más tokens.
Por lo tanto, la conclusión es que no solo hay que mirar el precio por costo por token.
Elige un modelo, analiza los datos reales, úsalo en tu caso o consulta Android Bench para ver qué modelos consumen más tokens en las tareas.
Algunos, ya sabes, requieren más pasos, aunque son más económicos por token.
Sin embargo, hay un aspecto adicional de la misma pregunta.
Este es el mismo gráfico, pero también podemos ver cuánto tardan los agentes en completar esa tarea.
Y podemos ver que los agentes con la tasa de aprobación más alta podrían ser incluso los más lentos.
Por ejemplo, podemos ver que algunas tardan casi dos horas por tarea.
Siguen reconstruyendo y volviendo a probar hasta que funciona.
Sí, obtienen el mejor porcentaje de aprobación, pero también son las que más tiempo tardan.
Algunos agentes más económicos siguen tardando alrededor de una hora y media por tarea, pero sin mejores resultados.
Otros agentes, en cambio, lo terminan en menos de 20 minutos y resuelven más de un cuarto de las tareas.
Por lo tanto, los modelos pequeños y ligeros suelen ser más rápidos y económicos, pero fallan con más frecuencia en las tareas difíciles.
La conclusión es que no hay un único modelo que sea el mejor.
Depende de lo que quieras optimizar.
¿Es el porcentaje de aprobación?
¿Es el costo?
¿Es el tiempo de espera?
En ese caso, según el tipo de trabajo que estés haciendo, si no estás trabajando en tareas tan difíciles, considera usar los modelos más rápidos que realizan el trabajo de forma más económica y rápida.
Sin embargo, el costo y la velocidad solo indican cómo se ejecutan los agentes, pero no por qué fallan en la mayoría de las pruebas.
Por lo tanto, inspeccionamos las ejecuciones fallidas y notamos que los agentes suelen abandonar antes de tiempo.
Están convencidos de que ya terminaron.
Tarea completada.
Está bien, me voy a casa.
Sin embargo, como se mencionó, en el paquete de tareas, los agentes solo reciben las instrucciones y el estado anterior.
No pueden ver las pruebas.
Ven que no pueden ver el paquete de verificación.
Entonces, nos preguntamos: ¿qué pasaría si solo les permitimos ver las pruebas?
Por lo tanto, experimentamos y tuvimos una variante de prueba abierta de la misma tarea, en la que el agente obtuvo el paquete de pruebas de verificación con errores por adelantado.
Por lo tanto, ahora, ejecutaban las pruebas de forma iterativa.
Pueden inspeccionar errores y definir mejor los códigos en un ciclo de reacción.
Y descubrimos que, cuando los agentes ven las pruebas, se esfuerzan más y trabajan más tiempo.
Y en las ejecuciones abiertas, tuvieron un 25% más de turnos y un 40% de Agent Runtime.
Pasaron casi 30 minutos por tarea ejecutando y corrigiendo las pruebas que fallaban, porque no estaban depurando a ciegas.
Por lo tanto, ven exactamente qué falla y, lamentablemente, la tasa de aprobación también aumenta casi 50 puntos porcentuales.
Y este esfuerzo adicional mejora el porcentaje de aprobación promedio, como mencioné.
Sin embargo, con el tiempo, surgieron nuevos modelos de frontera que rápidamente saturaron esta variante abierta.
Bueno, casi el 90% de la tasa de aprobación.
Por lo tanto, mantuvimos la variante de prueba cerrada de búsqueda cerrada como la métrica de referencia principal.
Sin embargo, sigue siendo muy interesante ver cómo los modelos son mejores cuando saben con qué se los califica.
Tomáš Mlynarič
Sin embargo, nos permitió obtener más información sobre los agentes.
Por ejemplo, podemos verificar cómo los modelos prueban su propio trabajo.
Como podemos ver en el gráfico, los modelos de frontera escriben cada vez más pruebas mejores y tienen tasas de aprobación más altas.
Otros modelos son demasiado confiados: "Puedo hacerlo, no necesito pruebas".
Bueno, a veces siguen siendo buenos.
Por lo tanto, si usas los modelos que no son tan buenos con las pruebas, puedes ayudarlos con mejores habilidades de prueba o instrucciones específicas sobre cómo escribir pruebas.
Pero la correlación no implica causalidad, CS, ¿verdad?
Necesitamos tener un experimento controlado sobre esto.
Aún debemos investigar si pedir mejores pruebas realmente aumenta la puntuación.
Sin embargo, realizamos un experimento en la otra mitad de las pruebas de Android, que consiste en hacer que los agentes usen el emulador.
Al principio, en nuestra comparativa, no dijimos nada sobre el emulador.
Asumimos que los agentes deberían resolverlo, ¿verdad?
Como resultado, vimos que casi ninguno de ellos inició el emulador.
Y, como mencioné, nuestras pruebas de IU son bastante cruciales en nuestras comparativas, y, sin emuladores, no puedes usar las pruebas de IU.
Por lo tanto, todos fallaban en algo muy simple.
Ahora, nos enfrentamos al dilema.
¿Les decimos a los agentes que deben usar el emulador y arriesgarse a obtener puntuaciones más altas, o no decimos nada y esperamos que, ya sabes, lo descubran?
Pero, después de todo, nuestro objetivo era ser un punto de referencia justo para medir la programación y la resolución de problemas en Android.
No queremos medir algunas peculiaridades ambientales.
Por lo tanto, elegimos un punto intermedio.
Le dijimos que así se inicia un emulador, pero no mencionamos si debía usarlo o no.
Después de todo, iniciar el emulador no es lo que realmente necesitamos o queremos probar.
Y medimos el efecto de esa instrucción del emulador.
Y, de hecho, vimos que, cuando les dimos las instrucciones, los agentes iniciaron el emulador mucho más y ejecutaron las pruebas de IU, y eso tuvo un gran impacto en la tasa de aprobación, desafortunadamente para nosotros.
Pero también nos permitió inspeccionar sus errores y problemas reales de programación, y no solo, por ejemplo, fallas en el emulador que no se inicia.
En resumen, la especificidad de las instrucciones sigue siendo muy importante y, si diseñas tus propias comparativas o evaluaciones, debes probar lo que realmente importa en tu caso.
Por ejemplo, si evalúas la eficiencia de los tokens o de una habilidad o herramienta, primero debes comenzar con la aprobación de la evaluación, realizar el cambio para la eficiencia y, luego, verificar que siga aprobándose con menos tokens.
Si lo haces de otra manera, tendrás demasiadas variables que cambian todas a la vez y no sabrás si tu cambio realmente mejoró el rendimiento.
Tomáš Mlynarič
Volver a la tabla de clasificación.
Como mencioné, los mejores modelos aprueban alrededor del 40% de las tareas, y la tasa de aprobación requiere que se aprueben todas las verificaciones.
De lo contrario, la tarea fallaría.
Entonces, la pregunta es: ¿los agentes no produjeron nada útil para el otro 60%?
No, ya casi llegan.
Para mostrar qué tan cerca estuvo el agente de las soluciones que funcionan, como una distancia, también calculamos la tasa de finalización o una especie de puntuación parcial.
Esta es una puntuación ponderada en cuatro categorías: coherencia visual, funcionalidad, si funciona correctamente, algunos requisitos específicos de cada tarea y algunas verificaciones de regresión.
Y puedes ver que los modelos con una clasificación más baja pueden terminar aproximadamente la mitad del trabajo, mientras que los modelos de frontera pueden completar y llegar a casi el 80% en promedio, más del 80% de las tareas, pero solo aprueban el 40%.
Por lo tanto, hicimos un experimento sobre la conversión de Flutter a Android para verificar qué estaba sucediendo.
Por lo tanto, volvimos a calcular el porcentaje de respuesta cada pocos pasos del agente y vimos los mismos patrones en todos los modelos de frontera.
Alrededor de los 140 pasos, estaban trabajando para alcanzar casi el 90% de la puntuación en promedio, y luego pasaron más de 90 pasos más o casi el 40% de la ejecución tratando de cerrar la brecha al 100%, pero solo pudieron obtener una puntuación de 1 o 2% más.
Y creemos que esto refleja lo que es el desarrollo de agentes hoy en día, en el que los desarrolladores o usuarios sin experiencia ven que la app está completa.
Se renderiza todo.
La entrada funcionó más o menos.
Es posible que no haya fallas.
Pero cuando un ingeniero sénior llega y mira el código, ve muchos defectos: problemas de arquitectura, fallas en el ciclo de vida y cosas por el estilo.
Tomáš Mlynarič
Para terminar, pongamos en perspectiva Android Bench, nuestro trabajo 2.0.
En comparación con la versión 1.0, ahora solo tenemos 30 tareas, y eso es deliberado.
En lugar de correcciones de errores aisladas y parches pequeños, tenemos pantallas completas, migraciones de varios módulos y conversiones de apps completas.
Los parches ahora son un orden de magnitud más grandes en decenas de archivos.
Y tenemos las verificaciones de IU multimodal.
Los agentes deben ejecutarse durante más tiempo y trabajar mucho más.
Si lo comparamos con otras comparativas de vanguardia, podemos ver que nuestra comparativa aún requiere cambios de código mucho más grandes.
No tiene contaminación porque la mayoría de las tareas se escriben desde cero, y todavía hay mucho margen para crecer.
Como dije al principio, unos meses en el mundo de la IA parecen una eternidad y, con el tiempo, todos los comparativas se maximizan y se saturan.
Y cuando eso suceda, con suerte, nos esforzaremos por realizar tareas aún más difíciles y complejas que, de nuevo, resonarán con esa era y con ese estado de la IA.
Ahora, usa Android Bench como brújula para comparar modelos y arneses, y tal vez obtener algunas estadísticas.
Y cuando quieras profundizar, aplica los mismos principios de comparativas y evaluación a tus propias bases de código y flujos de trabajo, porque no hay nada mejor que medir la IA en el código con el que realmente trabajas.
Así es como pasas de esperar que la IA haya hecho lo correcto a saber que lo hizo.
En nombre de todo el equipo de Android Bench, gracias por su atención.
import com.foodvibes.MyViewModel
@Test
fun testRetry() {
val vm = MyViewModel()
vm.retry()
assertTrue(vm.retried)
}import com.foodvibes.feature.my.RetriableViewModel
@Test
fun testRetry() {
val vm = RetriableViewModel()
vm.tryAgain()
assertTrue(vm.tried)
}// AuthFlowScreenshotTests.kt
@get:Rule
val dropshots = Dropshots(
resultValidator =
ThresholdValidator(
- 0.05f
+ 0.80f
)
)