<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Silviu Technology</title>
    <description>The latest articles on DEV Community by Silviu Technology (@silviutech).</description>
    <link>https://dev.to/silviutech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013497%2F7d1c82f4-cd78-412d-908d-981280990b57.png</url>
      <title>DEV Community: Silviu Technology</title>
      <link>https://dev.to/silviutech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/silviutech"/>
    <language>en</language>
    <item>
      <title>Agentes LLM: una cola de revisión para email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 15 Nov 2026 11:23:38 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-una-cola-de-revision-para-email-1ol7</link>
      <guid>https://dev.to/silviutech/agentes-llm-una-cola-de-revision-para-email-1ol7</guid>
      <description>&lt;p&gt;Un agente LLM puede preparar un email correcto y aun así tomar una mala decisión operativa: enviarlo antes de tener todos los datos, reabrir una tarea ya cerrada o aprobar un mensaje que pertenece a otra corrida. El problema no es solo la calidad del texto. Es que el agente suele mezclar intención, ejecución y revisión en una misma acción.&lt;/p&gt;

&lt;p&gt;Una cola de revisión pequeña crea una frontera más clara. El agente propone una operación; una capa determinista la coloca en estado &lt;code&gt;pending&lt;/code&gt;; después, una política decide si puede continuar, necesita una revisión o debe quedar bloqueada. Este patrón es util cuando una automatización toca cuentas, notificaciones o pruebas de correo.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: el agente decide demasiado pronto
&lt;/h2&gt;

&lt;p&gt;Un contrato como &lt;code&gt;send_email(to, subject, body)&lt;/code&gt; es comodo para una demostración, pero es demasiado abierto para un flujo que debe poder auditarse. No responde preguntas importantes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿La solicitud pertenece a la corrida actual?&lt;/li&gt;
&lt;li&gt;¿El destinatario es de staging o parece real?&lt;/li&gt;
&lt;li&gt;¿El contenido ya fue revisado por una regla o por una persona?&lt;/li&gt;
&lt;li&gt;¿Qué pasa si el agente cambia el mensaje después de una espera?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cuando todo se resuelve en una llamada, un timeout puede parecer un permiso para reintentar. Un dato ausente puede convertirse en una suposición. Y un resultado viejo puede volver a entrar en la conversación. La automatización sigue funcionando, pero nadie sabe bien por que.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseñar una cola de revisión pequeña
&lt;/h2&gt;

&lt;p&gt;Piensa en el flujo como un diagrama en palabras: el agente produce una intención, la cola conserva una copia inmutable, el evaluador aplica reglas y el adaptador ejecuta solo las operaciones aprobadas. La cola no tiene que ser un sistema distribuido enorme. Puede empezar como una tabla o un archivo por corrida.&lt;/p&gt;

&lt;p&gt;Cada elemento debería incluir un identificador, una acción y un estado explícito:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"run-2026-11-15-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"task_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"welcome-check-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"prepare_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"recipient_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"content_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"sha256:..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"pending"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La separación importante es entre &lt;code&gt;prepare_email&lt;/code&gt; y &lt;code&gt;send_email&lt;/code&gt;. El agente puede redactar o pedir una revisión sin obtener permiso implícito para enviar. Para una prueba de signup, por ejemplo, crear un correo temporal y comprobar su contenido son pasos distintos, con presupuestos y evidencias distintas.&lt;/p&gt;

&lt;h2&gt;
  
  
  La identidad por corrida evita mezclar estados
&lt;/h2&gt;

&lt;p&gt;El &lt;code&gt;run_id&lt;/code&gt; debe viajar con cada evento: creación, revisión, envío, lectura y limpieza. Si el agente se reinicia, no debe buscar “el último mensaje” de forma global. Debe consultar solo los mensajes de su corrida y de su &lt;code&gt;task_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Esto también ayuda cuando se prueban interfaces. Una señal de email puede llegar mientras la pantalla todavía muestra un estado pendiente; por eso conviene tratar el mensaje y la interfaz como dos fuentes que deben correlacionarse. La idea se parece a una &lt;a href="https://dev.to/silviutech/react-validacion-async-sin-saltos-visuales-41ja"&gt;validación async sin saltos visuales&lt;/a&gt;: el usuario o el test necesita saber que la señal corresponde a la acción visible.&lt;/p&gt;

&lt;p&gt;Guarda la huella del contenido, no necesariamente todo el cuerpo en cada evento. Así puedes demostrar que el agente no cambió la propuesta entre revisión y ejecución sin copiar datos sensibles por todo el sistema. La limpieza debe ser parte del contrato, aunque a veces se olvida cuando el flujo funciona en local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato mínimo para aprobar o devolver
&lt;/h2&gt;

&lt;p&gt;La salida del evaluador puede ser deliberadamente pequeña:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"approve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"staging_recipient_and_known_template"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"checked_run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"run-2026-11-15-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"checked_content_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"sha256:..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-15T11:35:00Z"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una aprobación debe caducar. Si pasan diez minutos y cambia el contenido, la decisión deja de ser válida. En cambio, &lt;code&gt;return_for_revision&lt;/code&gt; debería incluir una razón accionable, como “falta la clase del destinatario” o “el hash no coincide”. Un “no” sin explicación solo provoca otra llamada del agente.&lt;/p&gt;

&lt;p&gt;Para profundizar en la separación entre intención y evidencia, conviene revisar estos &lt;a href="https://dev.to/silviutech/llm-y-correo-contratos-que-si-se-pueden-probar"&gt;contratos de correo que sí se pueden probar&lt;/a&gt;. El punto no es agregar burocracia, sino hacer visible el limite que protege al sistema.&lt;/p&gt;

&lt;p&gt;Durante las pruebas pueden aparecer entradas como &lt;code&gt;temp gamil com&lt;/code&gt; o &lt;code&gt;tamp mail com&lt;/code&gt;. Déjalas como datos de prueba planos; nunca las conviertas en dominios permitidos ni en anclas de navegación. También es frecuente escribir “mas facil”, “deberia” o “rapida” en notas internas. Son pequeños detalles, pero no deben entrar en comandos, URLs o reglas de seguridad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿La revisión siempre tiene que ser humana?
&lt;/h3&gt;

&lt;p&gt;No. Puede ser una política determinista, un conjunto de tests o una persona para operaciones de mayor riesgo. Lo importante es que la decisión quede registrada y separada de la propuesta del modelo.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuándo basta una cola en memoria?
&lt;/h3&gt;

&lt;p&gt;Para una prueba local de una sola corrida. En CI o en una integración compartida, conviene persistir el estado mínimo para que un reinicio no convierta una tarea pendiente en un envío duplicado.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué debe hacer el agente cuando la tarea vuelve a revisión?
&lt;/h3&gt;

&lt;p&gt;Leer la razón, cambiar solo lo necesario y crear una nueva versión de la propuesta. No debería sobrescribir silenciosamente la versión anterior, por que entonces se pierde la evidencia de qué se revisó.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de implementación
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Separa preparar, revisar y enviar en acciones diferentes.&lt;/li&gt;
&lt;li&gt;Asigna &lt;code&gt;run_id&lt;/code&gt; y &lt;code&gt;task_id&lt;/code&gt; a cada evento.&lt;/li&gt;
&lt;li&gt;Guarda el hash del contenido aprobado.&lt;/li&gt;
&lt;li&gt;Expira las aprobaciones después de una ventana corta.&lt;/li&gt;
&lt;li&gt;Clasifica destinatarios de staging y producción de forma explícita.&lt;/li&gt;
&lt;li&gt;Devuelve razones accionables cuando una propuesta es rechazada.&lt;/li&gt;
&lt;li&gt;Prueba reinicio, timeout, duplicado y contenido cambiado.&lt;/li&gt;
&lt;li&gt;Elimina fixtures y buzones de prueba cuando termina la corrida.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Una cola de revisión no intenta hacer que el LLM sea perfecto. Limita el impacto de sus decisiones y conserva el contexto necesario para entenderlas. El agente puede proponer, corregir y volver a intentar; la capa de control decide qué versión tiene permiso para avanzar.&lt;/p&gt;

&lt;p&gt;Empieza con tres estados —&lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;approved&lt;/code&gt;, &lt;code&gt;blocked&lt;/code&gt;— y un &lt;code&gt;run_id&lt;/code&gt; obligatorio. Cuando el flujo sea estable, añade expiración, hashes y métricas de devoluciones. Es una arquitectura pequeña, pero hace la automatización más fácil de probar, explicar y detener.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Agentes LLM: un cortacircuito para email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 15 Nov 2026 08:23:18 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-un-cortacircuito-para-email-45fh</link>
      <guid>https://dev.to/silviutech/agentes-llm-un-cortacircuito-para-email-45fh</guid>
      <description>&lt;p&gt;Un agente LLM puede redactar una respuesta correcta y aun así causar un incidente pequeño: enviar el mismo email tres veces, seguir reintentando después de un error permanente o usar una corrida vieja para completar una tarea nueva. El problema no suele ser el modelo. Es la frontera entre el modelo y la herramienta de correo.&lt;/p&gt;

&lt;p&gt;En este artículo propongo un cortacircuito sencillo. La idea es tratar cada envío como una operación con presupuesto, identidad y evidencia. El agente decide &lt;strong&gt;qué&lt;/strong&gt; quiere hacer; una capa determinista decide &lt;strong&gt;si todavía puede hacerlo&lt;/strong&gt;. Esta separación hace la automatización más facil de probar y de explicar.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: una herramienta de email sin freno
&lt;/h2&gt;

&lt;p&gt;Una función como &lt;code&gt;send_email(to, subject, body)&lt;/code&gt; parece suficiente para una demo. En producción faltan varias respuestas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Cuántas veces puede reintentarse el mismo mensaje?&lt;/li&gt;
&lt;li&gt;¿Qué ocurre si el proveedor responde &lt;code&gt;400&lt;/code&gt; o &lt;code&gt;401&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;¿Cómo sabemos que el mensaje pertenece a esta corrida?&lt;/li&gt;
&lt;li&gt;¿Quién detiene al agente cuando la tarea ya no tiene sentido?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sin una frontera, un error temporal puede convertirse en una tormenta de reintentos. Un error de autenticación, por el contrario, no se arregla llamando a la misma herramienta otra vez. El agente necesita una señal clara: continuar, esperar o abrir el circuito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseñar el cortacircuito como frontera
&lt;/h2&gt;

&lt;p&gt;Imagina el flujo como un diagrama en palabras: el agente produce una intención, el guardia valida esa intención, el adaptador llama al proveedor y el ledger guarda el resultado. Solo el guardia puede pasar de una etapa a la siguiente.&lt;/p&gt;

&lt;p&gt;La frontera debe controlar cuatro cosas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Presupuesto:&lt;/strong&gt; máximo de intentos y tiempo total de la operación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identidad:&lt;/strong&gt; un &lt;code&gt;run_id&lt;/code&gt; y un &lt;code&gt;message_id&lt;/code&gt; que no se reutilizan entre corridas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clasificación:&lt;/strong&gt; distinguir fallos temporales de fallos permanentes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estado:&lt;/strong&gt; marcar &lt;code&gt;allowed&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;blocked&lt;/code&gt; o &lt;code&gt;sent&lt;/code&gt; de forma explícita.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este diseño complementa los &lt;a href="https://dev.to/silviutech/agentes-llm-contratos-de-evidencia-para-email-58ip"&gt;contratos de evidencia para email&lt;/a&gt;: la evidencia no es un log decorativo, es parte del contrato que permite auditar la decisión.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato pequeño para la herramienta
&lt;/h2&gt;

&lt;p&gt;No hace falta darle al LLM diez opciones. Un contrato estrecho reduce decisiones ambiguas y mantiene los controles en código:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"run-2026-11-15-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"welcome-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"max_attempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"send"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"recipient_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"body_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"sha256:..."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Antes de llamar al proveedor, el guardia valida que &lt;code&gt;attempt &amp;lt;= max_attempts&lt;/code&gt;, que la corrida esté activa y que el &lt;code&gt;message_id&lt;/code&gt; no tenga ya un resultado final. Si una comprobación falla, devuelve una razón legible al agente. No devuelve un error genérico que invite a probar de nuevo.&lt;/p&gt;

&lt;p&gt;Una política útil puede ser: &lt;code&gt;429&lt;/code&gt;, timeout y errores de red pasan a &lt;code&gt;waiting&lt;/code&gt;; &lt;code&gt;400&lt;/code&gt;, &lt;code&gt;401&lt;/code&gt; y un destinatario inválido pasan a &lt;code&gt;blocked&lt;/code&gt;. Los códigos exactos dependen del proveedor, pero la decisión debe ser estable. El modelo no deberia inventar una excepción nueva cada vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué registrar cuando se abre el circuito
&lt;/h2&gt;

&lt;p&gt;Cuando el cortacircuito bloquea la llamada, guarda el contexto mínimo: &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;message_id&lt;/code&gt;, intento, clasificación, timestamp, decisión y una referencia al error. No guardes el cuerpo completo si no es necesario. Así se reduce exposición y la revisión es mas rápida.&lt;/p&gt;

&lt;p&gt;También conviene separar el estado del agente del estado del proveedor. Un agente puede decir “enviar”, mientras que el proveedor puede responder “aceptado”, “rebotado” o “desconocido”. Mezclar ambos estados complica el diagnóstico por que una nueva corrida puede leer un resultado antiguo.&lt;/p&gt;

&lt;p&gt;Para evitarlo, cada consulta debe estar ligada a su corrida. Este patrón se parece a &lt;a href="https://dev.to/silviutech/llms-aprobar-correos-sin-mezclar-corridas-4ei"&gt;evitar mezclar corridas en un flujo de email&lt;/a&gt;: la identidad es una condición técnica, no una convención de nombres.&lt;/p&gt;

&lt;p&gt;Un detalle humano: durante pruebas alguien puede buscar frases como &lt;code&gt;fake e mail com&lt;/code&gt; o &lt;code&gt;tepm mail com&lt;/code&gt; en datos de ejemplo. Deben quedar como texto de prueba, nunca como reglas de enrutamiento ni como dominios permitidos. Parece obvio, pero estos casos se cuelan cuando el fixture crece deprisa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de implementación
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Define un límite de intentos por &lt;code&gt;message_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Usa un reloj de deadline, no solo un contador.&lt;/li&gt;
&lt;li&gt;Clasifica el error antes de decidir el reintento.&lt;/li&gt;
&lt;li&gt;Rechaza una corrida inactiva o desconocida.&lt;/li&gt;
&lt;li&gt;Haz idempotente la combinación &lt;code&gt;run_id + message_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Registra la razón de bloqueo y la referencia al proveedor.&lt;/li&gt;
&lt;li&gt;Prueba timeout, &lt;code&gt;429&lt;/code&gt;, &lt;code&gt;401&lt;/code&gt;, duplicado y respuesta desconocida.&lt;/li&gt;
&lt;li&gt;Revisa que los datos de staging no contengan destinatarios reales.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Un agente LLM es más confiable cuando no tiene permiso implícito para insistir. El cortacircuito convierte una herramienta abierta en una interfaz con límites visibles: presupuesto, identidad, clasificación y evidencia. El resultado no es menos automatización; es automatización que puede detenerse, reanudarse y revisarse sin adivinar que pasó.&lt;/p&gt;

&lt;p&gt;Empieza con una sola herramienta de email y un solo &lt;code&gt;run_id&lt;/code&gt;. Añade métricas después: porcentaje de llamadas bloqueadas, reintentos por causa y tiempo hasta la decisión final. Es un punto de control pequeño, pero cambia mucho la calidad operativa del sistema.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Playwright Email Fixtures Need Better Failure Evidence</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 15 Nov 2026 05:23:31 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-fixtures-need-better-failure-evidence-5bp6</link>
      <guid>https://dev.to/silviutech/playwright-email-fixtures-need-better-failure-evidence-5bp6</guid>
      <description>&lt;p&gt;Email tests rarely fail because an email is completely absent. More often, the test sees the wrong message, reads the inbox too early, or loses the evidence that would explain what happened. A &lt;code&gt;temp mail generator&lt;/code&gt; can provide a useful isolated fixture, but isolation alone does not make an end-to-end test diagnosable.&lt;/p&gt;

&lt;p&gt;This is the workflow I use when a Playwright email test is flaky: define the expected states, capture the first useful evidence, and make every retry explain what changed. The goal is not to add more waits. It is to make the failure tell us which boundary broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure is often a missing state, not a missing email
&lt;/h2&gt;

&lt;p&gt;Consider a signup test that requests a verification code. The test opens an inbox and waits for a message with the subject “Verify your account.” It passes locally, then fails in CI with “message not found.”&lt;/p&gt;

&lt;p&gt;That message hides several different failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The signup request never completed.&lt;/li&gt;
&lt;li&gt;The message was sent to a previous fixture address.&lt;/li&gt;
&lt;li&gt;The inbox API returned an older message first.&lt;/li&gt;
&lt;li&gt;The email arrived after the polling deadline.&lt;/li&gt;
&lt;li&gt;The code was present, but the selector matched the wrong element.&lt;/li&gt;
&lt;li&gt;The test found the message but failed while parsing its body.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each case needs a different fix. A longer timeout only makes some of them slower. The first diagnostic step is to record the boundary events: fixture created, address submitted, request accepted, message observed, code extracted, and verification completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the fixture as a state ledger
&lt;/h2&gt;

&lt;p&gt;I prefer a small state ledger over a single &lt;code&gt;waitForEmail()&lt;/code&gt; helper. The ledger gives each transition a timestamp and a compact detail value without storing the whole mailbox contents in every log line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;EmailState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"fixture-created"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"address-submitted"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"message-observed"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"code-extracted"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"verified"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"expired"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EmailState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EmailState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;states&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ledger should be attached to the test result when a step fails. Do not log the verification code itself; record its source message ID, subject, and age instead. This keeps the artifact useful without turning a CI report into a copy of the inbox.&lt;/p&gt;

&lt;p&gt;A state ledger also catches an easy-to-miss problem: the first clue are often in the order of events. If &lt;code&gt;message-observed&lt;/code&gt; occurs before &lt;code&gt;address-submitted&lt;/code&gt;, the test may be reading a stale message or sharing a fixture across workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a diagnostic Playwright helper
&lt;/h2&gt;

&lt;p&gt;The helper should poll for a message using a stable identity, not a broad text match. A good identity usually combines the fixture address, a test-run token, and a bounded age. Subject text can be part of the check, but it should not be the only check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readLatestMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runToken&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"message-observed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Message ${message.id} has no code`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"code-extracted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`token=${token}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Verification message did not arrive before the deadline"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the real helper, keep the polling interval and deadline configurable. Capture one sanitized response summary on timeout: message count, newest message timestamp, and the requested token. That is usually enough to tell whether delivery or matching failed.&lt;/p&gt;

&lt;p&gt;If a selector is involved, prefer a role or a data attribute that the application owns. Avoid a selector such as &lt;code&gt;text=123456&lt;/code&gt;; another message can contain the same number. Playwright is very good at reporting selector failures, but only when the test gives it a stable target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the mailbox boundary explicit
&lt;/h2&gt;

&lt;p&gt;The mailbox is an external dependency even when it is created by a test service. Give each worker a unique address or run token, and clean it up after the test. Also separate the browser timeout from the email delivery timeout. They measure different things and should produce different errors.&lt;/p&gt;

&lt;p&gt;When documenting fixtures, search terms can be messy. Someone may write &lt;code&gt;tepm mail com&lt;/code&gt; or &lt;code&gt;temp gamil com&lt;/code&gt; in a failure ticket. Keep those phrases in troubleshooting notes if they help search older incidents, but do not turn them into selectors or production validation rules. The phrase &lt;code&gt;temp mail mail&lt;/code&gt; can likewise appear in an imported test description without being a reliable fixture identifier.&lt;/p&gt;

&lt;p&gt;For privacy-sensitive flows, review what the fixture service retains and who can access it. These &lt;a href="https://dev.to/bitheirstake/privacy-notes-for-facebook-temp-mail-checks-1okg"&gt;privacy notes for email checks&lt;/a&gt; are a useful reminder that test data still deserves a boundary and a deletion policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable CI failure checklist
&lt;/h2&gt;

&lt;p&gt;When the test fails, check the evidence in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; Was the same address or run token used from setup through message lookup?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application event:&lt;/strong&gt; Did the signup request return the expected status before polling began?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery:&lt;/strong&gt; Did any message arrive within the deadline, even if it had an unexpected subject?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matching:&lt;/strong&gt; Did the message contain the current token, or was it stale?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parsing:&lt;/strong&gt; Was the message body format different from the local fixture?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser state:&lt;/strong&gt; Did the page navigate, reload, or lose the verification form while waiting?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact quality:&lt;/strong&gt; Can another engineer replay the relevant inputs from the CI artifact?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the failure is intermittent, save a small replay pack with the state ledger, sanitized message metadata, request correlation ID, and the final page screenshot. An &lt;a href="https://dev.to/mrdapperx/replay-packs-make-cron-jobs-easier-to-fix-4mg8"&gt;approach based on replay packs for easier debugging&lt;/a&gt; works well because it preserves the facts without asking a developer to guess which retry was meaningful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions that help classify the failure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I increase the email timeout first?
&lt;/h3&gt;

&lt;p&gt;No. First determine whether the message arrived late, arrived under a different identity, or was never requested. Increase the deadline only when delivery latency is the known cause, and keep the failure artifact so the new budget remains visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a temp mail generator suitable for every test?
&lt;/h3&gt;

&lt;p&gt;It is useful for isolated signup and verification scenarios, especially when each run gets a unique fixture. It is less suitable for tests that need long-term mailbox history, guaranteed retention, or sensitive production-like data. In those cases, a controlled fake mail adapter may be more predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes an email test reliable?
&lt;/h3&gt;

&lt;p&gt;Reliable tests have unique identity, explicit states, bounded polling, stable selectors, sanitized evidence, and cleanup. They also fail at the first broken boundary. That last part matters most: a test that says “verification failed” is hard to repair, while one that says “message arrived for the previous run token” points directly to fixture isolation.&lt;/p&gt;

&lt;p&gt;The practical rule is simple: treat email as a sequence of observable states, not a single asynchronous event. Once Playwright records that sequence, flaky failures become smaller QA problems with evidence instead of mysterious red builds.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Agentes LLM: un presupuesto de latencia para email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 14 Nov 2026 11:23:53 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-un-presupuesto-de-latencia-para-email-576e</link>
      <guid>https://dev.to/silviutech/agentes-llm-un-presupuesto-de-latencia-para-email-576e</guid>
      <description>&lt;p&gt;Los agentes LLM pueden llamar APIs, crear datos de prueba y decidir el siguiente paso de un flujo. Pero cuando el flujo incluye un email, aparece un problema menos vistoso: el agente puede quedarse esperando una bandeja que nunca confirma nada.&lt;/p&gt;

&lt;p&gt;La solución no es subir todos los timeouts. Es tratar el email de prueba como un recurso con un presupuesto de latencia. Así el agente sabe cuanto tiempo tiene, qué estados son válidos y cuándo debe entregar el control a un worker o a una persona.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un agente que espera demasiado
&lt;/h2&gt;

&lt;p&gt;Imaginemos un agente que valida un onboarding. Crea un usuario, solicita una verificación y espera un enlace. Si usa un bucle de “consultar hasta que aparezca”, tenemos tres riesgos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consume tokens y conexiones mientras no ocurre nada;&lt;/li&gt;
&lt;li&gt;confunde una bandeja vacía con un fallo de la aplicación;&lt;/li&gt;
&lt;li&gt;reintenta una operación que ya fue aceptada por el proveedor de correo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En un flujo pequeño, esto solo hace la prueba más lenta. En una cola de automatización, puede ocupar workers, multiplicar el trafico y ocultar el error real. Por eso conviene separar la decisión del agente de la espera del sistema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dibujar el presupuesto como un contrato
&lt;/h2&gt;

&lt;p&gt;Un diseño sencillo tiene tres piezas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agente LLM:&lt;/strong&gt; solicita un fixture de correo, inicia la acción y decide qué hacer con el resultado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker de email:&lt;/strong&gt; consulta la bandeja, normaliza eventos y aplica el timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ledger de ejecución:&lt;/strong&gt; guarda &lt;code&gt;run_id&lt;/code&gt;, estado, último intento y causa de salida.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En palabras, el recorrido es: agente → comando idempotente → worker → evento terminal → agente. El agente no debería consultar directamente cada pocos segundos. Su contrato puede ser algo así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deadline = ahora + 45 s
estado inicial = waiting_for_email
resultado terminal = received | expired | rejected
reintento máximo = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los 45 segundos no son una verdad universal. Son una decisión operativa que se puede medir. Para una prueba local quizá basten 15; para un proveedor externo, 60 puede ser más realista. Lo importante es que el límite exista y que esté visible en los logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estados y reintentos que no se pisan
&lt;/h2&gt;

&lt;p&gt;El worker debe distinguir entre “todavía no llegó”, “llegó un evento inválido” y “la fuente no responde”. Un modelo mínimo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nf"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;StrEnum&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StrEnum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="no"&gt;WAITING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"waiting_for_email"&lt;/span&gt;
    &lt;span class="no"&gt;RECEIVED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"received"&lt;/span&gt;
    &lt;span class="no"&gt;EXPIRED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"expired"&lt;/span&gt;
    &lt;span class="no"&gt;REJECTED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"rejected"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada consulta debe llevar una clave de idempotencia, por ejemplo &lt;code&gt;run_id:message_type&lt;/code&gt;. Si el agente reenvía el mismo comando, el worker recupera el resultado existente en vez de crear otra dirección. Esto es especialmente útil al usar un correo temporal gratis para aislar datos de una ejecución.&lt;/p&gt;

&lt;p&gt;También conviene aplicar backoff con un máximo estricto. Una secuencia de 2, 5 y 10 segundos es fácil de razonar. Si el tercer intento termina el presupuesto, el estado es &lt;code&gt;expired&lt;/code&gt;; no se debe reiniciar el reloj solo porque el modelo decidió preguntar de nuevo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una implementación pequeña
&lt;/h2&gt;

&lt;p&gt;La interfaz del worker puede ser deliberadamente aburrida:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"verification_link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"provider_error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"rejected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"deadline_exceeded"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El agente recibe un resultado estructurado, no una explicación ambigua. Puede entonces corregir el selector, repetir la acción original o marcar la prueba como no concluyente. En la practica, esta frontera reduce bastante los prompts que intentan “adivinar” si la bandeja ya se actualizó.&lt;/p&gt;

&lt;p&gt;Para pruebas manuales, una fuente de &lt;strong&gt;&lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;correo desechable gratis&lt;/a&gt;&lt;/strong&gt; puede ser suficiente si la política del proyecto permite datos efímeros. No la usaría para cuentas reales, secretos ni recuperación de acceso. El fixture debe tener una duración y un responsable claros.&lt;/p&gt;

&lt;h2&gt;
  
  
  Señales y límites operativos
&lt;/h2&gt;

&lt;p&gt;El ledger debe registrar al menos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt; y nombre del flujo;&lt;/li&gt;
&lt;li&gt;edad del mensaje y tiempo restante;&lt;/li&gt;
&lt;li&gt;número de consultas y reintentos;&lt;/li&gt;
&lt;li&gt;estado terminal y razón;&lt;/li&gt;
&lt;li&gt;hash o identificador del fixture, nunca su contenido sensible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una métrica útil es &lt;code&gt;email_wait_budget_exhausted_total&lt;/code&gt;. Otra es el p95 del tiempo entre la solicitud y el evento recibido. Si sube el p95 pero no aumenta el número de errores del proveedor, el problema puede ser la frecuencia de polling. Si aumentan ambos, hay que revisar la dependencia externa.&lt;/p&gt;

&lt;p&gt;En una interfaz humana, estos estados también deben ser comprensibles. El artículo sobre &lt;a href="https://dev.to/silviutech/react-estados-de-correo-sin-bloquear-la-interfaz-2j40"&gt;estados de correo en React&lt;/a&gt; muestra por qué una UI no debería quedarse congelada durante una espera. Para operaciones, las &lt;a href="https://dev.to/alexcarteruk/kubernetes-alertas-que-guian-durante-un-drain-4f16"&gt;alertas que guían el diagnóstico&lt;/a&gt; siguen la misma idea: indicar acción y contexto, no solo color rojo.&lt;/p&gt;

&lt;p&gt;A veces el dato no llegan antes del deadline y eso no significa que el producto esté roto. Significa que el contrato produjo una señal útil. Un circuit breaker puede abrirse después de varios &lt;code&gt;provider_error&lt;/code&gt;, mientras que los &lt;code&gt;expired&lt;/code&gt; deberían alimentar un análisis distinto. Mezclar ambos estados hace que el equipo persiga por que una prueba aislada falló.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de implementación
&lt;/h2&gt;

&lt;p&gt;Antes de poner este patrón en una automatización con LLM, revisaría:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿El agente emite una orden idempotente?&lt;/li&gt;
&lt;li&gt;¿Existe un deadline absoluto, independiente del modelo?&lt;/li&gt;
&lt;li&gt;¿El worker devuelve estados terminales y una causa?&lt;/li&gt;
&lt;li&gt;¿Los reintentos conservan el mismo &lt;code&gt;run_id&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;¿Se puede distinguir timeout, rechazo y bandeja vacía?&lt;/li&gt;
&lt;li&gt;¿Las métricas muestran p95 y presupuesto agotado?&lt;/li&gt;
&lt;li&gt;¿Se eliminan los fixtures al terminar su ciclo?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Las búsquedas humanas pueden escribir “tamp mail com” o “tem email”, pero la arquitectura no debe depender de esas variantes. Debe depender de contratos pequeños y observables. Un agente LLM es más confiable cuando sabe cuándo esperar, cuándo parar y qué evidencia entregar al siguiente componente.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>testing</category>
    </item>
    <item>
      <title>A Failure Budget for Playwright Email Tests</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 14 Nov 2026 05:24:32 +0000</pubDate>
      <link>https://dev.to/silviutech/a-failure-budget-for-playwright-email-tests-37ka</link>
      <guid>https://dev.to/silviutech/a-failure-budget-for-playwright-email-tests-37ka</guid>
      <description>&lt;p&gt;Email verification tests often look simple: submit a form, wait for a message, click a link, and confirm the account is active. In a real CI run, that short scenario crosses several systems. A browser submits the request, an API queues a message, a mail service accepts it, a mailbox exposes it, and Playwright reads the result.&lt;/p&gt;

&lt;p&gt;When the test fails, the red line rarely tells you which system was late or wrong. A longer timeout can make the dashboard greener while making the diagnosis worse.&lt;/p&gt;

&lt;p&gt;I use a failure budget instead. The idea is simple: define how much waiting and retrying each stage is allowed, then preserve evidence when that budget is spent. This gives a QA team a repeatable way to separate a slow email from a broken verification flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email tests need a budget
&lt;/h2&gt;

&lt;p&gt;An email test has at least four clocks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser clock: how long the UI waits for a response.&lt;/li&gt;
&lt;li&gt;The application clock: how long the API takes to create and queue the message.&lt;/li&gt;
&lt;li&gt;The delivery clock: how long the message takes to reach the test mailbox.&lt;/li&gt;
&lt;li&gt;The processing clock: how long the test takes to parse the message and follow its link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If these clocks are hidden inside one &lt;code&gt;expect(...).toBeVisible()&lt;/code&gt; call, a failure is hard to classify. A burner email address may be valid, but the fixture can still be stale. A message may exist, but contain a link for a different test run. Or the application may never have queued anything.&lt;/p&gt;

&lt;p&gt;The budget makes those possibilities visible. Give the API response, mailbox discovery, and link processing separate limits. The values are not universal; the useful part is assigning ownership to each wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the causal timeline
&lt;/h2&gt;

&lt;p&gt;Before writing assertions, record the identifiers that connect the browser action to the message:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;test run ID&lt;/li&gt;
&lt;li&gt;user or fixture ID&lt;/li&gt;
&lt;li&gt;message request ID, if the API returns one&lt;/li&gt;
&lt;li&gt;mailbox address&lt;/li&gt;
&lt;li&gt;verification token or message ID, when safe to expose in test logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then log milestones, not every polling attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;timeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;requestSentAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;messageFoundAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;verificationClickedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Email'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requestSentAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'button'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Create account'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the mailbox helper finds a message, add &lt;code&gt;messageFoundAt&lt;/code&gt;. After the browser completes verification, add &lt;code&gt;verificationClickedAt&lt;/code&gt;. If &lt;code&gt;messageFoundAt&lt;/code&gt; is zero, the failure is probably delivery or fixture isolation. If the message is found but the link is rejected, inspect token ownership and application state.&lt;/p&gt;

&lt;p&gt;The state model should be explicit too. A small union such as &lt;code&gt;requested&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;opened&lt;/code&gt;, &lt;code&gt;verified&lt;/code&gt;, and &lt;code&gt;expired&lt;/code&gt; prevents a test from treating every non-success response as the same thing. See &lt;a href="https://dev.to/ryanlee91/type-safe-signup-email-states-in-node-164a"&gt;type-safe signup email states&lt;/a&gt; for a useful pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify failures before retrying
&lt;/h2&gt;

&lt;p&gt;Retries are useful only for failures that are plausibly transient. I put failures into three groups:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application failure:&lt;/strong&gt; the signup request returns an error, the message is never queued, or the verification endpoint rejects a token that was just issued. Retrying can hide a product bug, so capture the response and stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delivery failure:&lt;/strong&gt; the request succeeded, but no matching message appears before the mailbox budget expires. Keep the mailbox query, timestamps, and correlation IDs. Do not silently switch to another inbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test-observer failure:&lt;/strong&gt; the message exists and the token is valid, but the helper parsed the wrong element or the page assertion raced the navigation. Fix the observer or assertion; adding more delivery time will not help.&lt;/p&gt;

&lt;p&gt;A retry should create a new fixture identity. Reusing the same address can pick up an earlier message, which makes the second attempt look successful for the wrong reason. This is where a test contract for email probes is useful: it can define matching rules, ownership, expiry, and what evidence a probe must return.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Playwright implementation
&lt;/h2&gt;

&lt;p&gt;Keep mailbox polling in a helper with a deadline rather than an unbounded loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;deadlineMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;deadlineMs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Verify'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Verification email not found for ${address}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper should return structured data, not only a URL. Include the message ID, received time, subject, and a sanitized parse-failure reason. Avoid dumping full message bodies into CI logs because verification links can contain sensitive tokens.&lt;/p&gt;

&lt;p&gt;Also assert the causal relationship. The message should belong to the current run, the link should target the expected environment, and the resulting account should match the fixture. A test that only checks that “some” verification link works is passing a weaker requirement than the product actually has.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CI review checklist
&lt;/h2&gt;

&lt;p&gt;When a Playwright email test fails, ask these questions in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did the browser request complete, and with which status?&lt;/li&gt;
&lt;li&gt;Was a message queued for this run and address?&lt;/li&gt;
&lt;li&gt;Did the mailbox return a matching message before the deadline?&lt;/li&gt;
&lt;li&gt;Was the link parsed from the expected message?&lt;/li&gt;
&lt;li&gt;Did the verification endpoint accept the token?&lt;/li&gt;
&lt;li&gt;Did the UI reach the expected state after navigation?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The wording in failure output matters. “Expected verified state” is vague. “No message for run &lt;code&gt;abc&lt;/code&gt; after 30 seconds; request completed at 2.1 seconds” gives the next engineer somewhere to start.&lt;/p&gt;

&lt;p&gt;Keep test data short-lived: a tem email or temp org mail fixture should be isolated and cleaned up after every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I retry a missing email?
&lt;/h3&gt;

&lt;p&gt;Only after recording that the application request succeeded and the message was queued. Use a bounded retry with a fresh fixture. If the queue event is absent, treat it as an application failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a longer Playwright timeout a solution?
&lt;/h3&gt;

&lt;p&gt;Usually not. Increase a stage-specific budget only when delivery measurements support it. A global timeout hides which clock is slow and makes unrelated UI failures take longer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the most valuable artifact after a failure?
&lt;/h3&gt;

&lt;p&gt;A causal timeline with the run ID, fixture address, request result, mailbox result, and final browser state. It lets someone reproduce the reasoning without rerunning the whole job.&lt;/p&gt;

&lt;p&gt;Reliable email testing is less about waiting longer and more about knowing what happened. Give every stage a budget, make state transitions explicit, and preserve enough evidence to tell a product failure from a delivery delay. That turns a flaky-looking Playwright test into a diagnostic tool for the whole verification system.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Agentes LLM: permisos mínimos para email de prueba</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 12 Nov 2026 20:23:39 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-permisos-minimos-para-email-de-prueba-jfm</link>
      <guid>https://dev.to/silviutech/agentes-llm-permisos-minimos-para-email-de-prueba-jfm</guid>
      <description>&lt;p&gt;Un agente LLM puede crear un buzón, provocar un registro y leer un código de verificación en pocos pasos. Esa capacidad resulta útil para probar productos, pero también crea una frontera de seguridad poco visible: el modelo tiene acceso a una herramienta que puede generar datos, consultar mensajes y mantener recursos vivos.&lt;/p&gt;

&lt;p&gt;La solución no es quitar la automatización. Es darle permisos mínimos, un tiempo de vida corto y respuestas que hagan evidente lo que acaba de ocurrir. Un &lt;code&gt;correo de usar y tirar&lt;/code&gt; debe ser un recurso de prueba acotado, no una identidad que el agente pueda reutilizar sin límite.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: una herramienta demasiado poderosa
&lt;/h2&gt;

&lt;p&gt;Cuando se expone una sola función como &lt;code&gt;email_tool&lt;/code&gt;, el modelo suele recibir demasiadas opciones: crear una dirección, listar mensajes, reenviar contenido, buscar por asunto y borrar recursos. Cada operación parece razonable por separado, pero juntas permiten que un fallo del prompt o una respuesta ambigua amplíe el alcance de la tarea.&lt;/p&gt;

&lt;p&gt;El riesgo aparece en varios lugares:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un reintento crea un segundo buzón y deja el primero sin dueño.&lt;/li&gt;
&lt;li&gt;Una búsqueda amplia mezcla mensajes de ejecuciones anteriores.&lt;/li&gt;
&lt;li&gt;El contenido completo del correo termina en el contexto del LLM, aunque solo se necesitaba un código.&lt;/li&gt;
&lt;li&gt;Un flujo cancelado no libera la dirección ni sus mensajes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En CI, este problema se parece a cualquier recurso temporal sin propietario. Un &lt;a href="https://dev.to/alexcarteruk/kubernetes-un-contrato-de-caducidad-para-emails-ci-58a5"&gt;contrato de caducidad para emails de CI&lt;/a&gt; ayuda a pensar en el tiempo de vida antes de hablar de prompts. La pregunta no es solo si el agente puede llamar la herramienta, sino que puede hacer exactamente con cada llamada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define la frontera de permisos
&lt;/h2&gt;

&lt;p&gt;Antes de diseñar el prompt, dibujo la frontera en palabras:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orquestador de la prueba
  ├─ crea un buzón con task_id
  ├─ dispara el evento de producto
  ├─ observa solo mensajes de esa ejecución
  ├─ extrae un dato mínimo
  └─ revoca el recurso al terminar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El agente no necesita conocer todas las operaciones internas. Puede tener tres capacidades pequeñas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;create_fixture(task_id, expires_at)&lt;/code&gt;: reserva una dirección asociada a una ejecución.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wait_for_code(fixture_id, correlation_key, deadline)&lt;/code&gt;: devuelve solo el estado y el código necesario.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;release_fixture(fixture_id)&lt;/code&gt;: marca el recurso para eliminación, incluso despues de un fallo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La diferencia importante es que el servidor aplica las restricciones. El prompt puede pedir un buzón con una duración razonable, pero nunca debe ser la única barrera. El backend comprueba que &lt;code&gt;task_id&lt;/code&gt; pertenece a la ejecución actual, que &lt;code&gt;expires_at&lt;/code&gt; no supera el máximo permitido y que el agente no está leyendo otra fixture.&lt;/p&gt;

&lt;p&gt;Un límite de permisos así reduce la superficie de error sin convertir el sistema en una arquitectura enorme. Si el agente necesita consultar el asunto, se añade un campo específico. No hace falta darle una operación genérica para listar toda la bandeja.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato mínimo para el agente
&lt;/h2&gt;

&lt;p&gt;Las respuestas de las herramientas también son una interfaz de seguridad. Un mensaje como “no encontrado” invita a improvisar; una respuesta estructurada limita mejor la siguiente acción:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"waiting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"fixture_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"fx-1842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"remaining_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"observed_messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"next_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"poll_after_3s"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yo mantendría estos campos en todas las operaciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;: &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;ready&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; o &lt;code&gt;provider_error&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fixture_id&lt;/code&gt;: identificador opaco, sin exponer la dirección completa al modelo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;remaining_ms&lt;/code&gt;: tiempo disponible para el paso actual.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attempt&lt;/code&gt;: número de consulta dentro de la misma ejecución.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next_action&lt;/code&gt;: sugerencia limitada, no una instrucción libre.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El estado &lt;code&gt;expired&lt;/code&gt; debe ser terminal. Si el agente recibe ese estado, no debería crear otra dirección automaticamente para repetir la prueba. El orquestador decide si se inicia una nueva ejecución y conserva la relación con el fallo original.&lt;/p&gt;

&lt;p&gt;Este enfoque encaja bien con &lt;a href="https://dev.to/silviutech/fastapi-contratos-de-email-para-pruebas-repetibles-27cn"&gt;contratos de email para pruebas repetibles&lt;/a&gt;: cada prueba tiene una identidad, una condición de salida y una evidencia que se puede revisar sin abrir todo el buzón.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidencia, caducidad y rollback
&lt;/h2&gt;

&lt;p&gt;Un agente confiable necesita una ruta de salida tan clara como la ruta feliz. Al terminar una ejecución, guardaría un recibo pequeño con:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;task_id&lt;/code&gt; y &lt;code&gt;fixture_id&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;instante de creación y de liberación;&lt;/li&gt;
&lt;li&gt;evento que originó el mensaje;&lt;/li&gt;
&lt;li&gt;cantidad de consultas y tiempo consumido;&lt;/li&gt;
&lt;li&gt;motivo de éxito, expiración o error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El contenido del correo no debe entrar al recibo salvo que sea necesario para depurar. Para un código de verificación basta con almacenar un hash o los últimos caracteres, dependiendo del caso. Asi el equipo puede comprobar causalidad sin convertir los logs en otra bandeja de entrada.&lt;/p&gt;

&lt;p&gt;La caducidad debería tener dos capas: una fecha aplicada por el proveedor y una limpieza de respaldo en el orquestador. Si una llamada queda a medias, el job de limpieza usa el &lt;code&gt;fixture_id&lt;/code&gt; y elimina los recursos vencidos. No conviene depender de que el LLM recuerde llamar a &lt;code&gt;release_fixture&lt;/code&gt; cuando ya recibió un error.&lt;/p&gt;

&lt;p&gt;En documentación interna pueden aparecer búsquedas imperfectas como &lt;code&gt;tempail mail&lt;/code&gt;. Las trato como señales de intención de prueba, no como nombres de herramientas que el agente deba resolver por su cuenta. También conviene registrar el motivo real del bloqueo, en vez de esconderlo detrás de un “falló el email”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A: decisiones prácticas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿El LLM debe poder crear direcciones?
&lt;/h3&gt;

&lt;p&gt;Sí, pero solo a través de una capacidad ligada a &lt;code&gt;task_id&lt;/code&gt;, con duración máxima y cuota por ejecución. La decisión de permisos vive en el servidor, no en la personalidad del agente.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Por qué no exponer la bandeja completa?
&lt;/h3&gt;

&lt;p&gt;Porque amplía el alcance de un error y hace difícil separar mensajes viejos de los nuevos. Una operación que espera un código concreto es mas fácil de auditar que una que devuelve todos los mensajes.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hago cuando el proveedor de email falla?
&lt;/h3&gt;

&lt;p&gt;Devuelve &lt;code&gt;provider_error&lt;/code&gt;, conserva el recibo y ejecuta la limpieza. No repitas automáticamente con otra fixture si no sabes si el primer evento pudo llegar; primero marca la ejecución como inconclusa.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Dónde encaja tempmailso?
&lt;/h3&gt;

&lt;p&gt;Puede servir como una &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail so&lt;/a&gt; para aislar datos de una prueba, siempre que el equipo aplique su propia política de caducidad y no lo trate como una identidad real.&lt;/p&gt;

&lt;p&gt;La arquitectura mínima es bastante concreta: una fixture con propietario, tres capacidades limitadas, respuestas estructuradas y una limpieza que no dependa del modelo. Con esos límites, los agentes LLM siguen siendo utiles para explorar flujos, pero sus herramientas de email dejan de comportarse como una caja negra.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Playwright Email Tests Need a Quarantine Queue</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 12 Nov 2026 08:24:27 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-a-quarantine-queue-3ggb</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-a-quarantine-queue-3ggb</guid>
      <description>&lt;h1&gt;
  
  
  Playwright Email Tests Need a Quarantine Queue
&lt;/h1&gt;

&lt;p&gt;Email verification tests often look like browser tests, but the hardest part is usually the boundary between the application and the inbox. The browser clicks &lt;strong&gt;Send verification email&lt;/strong&gt;, while a separate service delivers a message, indexes it, and exposes a link. If the test treats that whole path as one instant operation, a failure can look random even when the cause is repeatable.&lt;/p&gt;

&lt;p&gt;In QA automation, I prefer to treat every email test as a small investigation. The test should have an isolated inbox, a deadline, and enough evidence to answer one question: did the message never arrive, arrive late, or arrive with the wrong state? A quarantine queue is a simple way to preserve that distinction without letting one suspicious run poison the rest of the suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure is usually a race, not a random browser problem
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Playwright submits the registration form.&lt;/li&gt;
&lt;li&gt;The API accepts the request and queues an email.&lt;/li&gt;
&lt;li&gt;A worker renders and sends the message.&lt;/li&gt;
&lt;li&gt;The inbox service receives and indexes it.&lt;/li&gt;
&lt;li&gt;The browser opens the verification link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The test can only observe the edges of this process. A fixed &lt;code&gt;waitForTimeout(2000)&lt;/code&gt; hides the uncertainty, but it does not remove it. On a fast laptop it wastes time; in a busy CI environment it can be too short. That is why the same test may pass locally and fail in an enviroment with slower workers.&lt;/p&gt;

&lt;p&gt;The first useful change is to give each run a unique address or mailbox namespace. A burner email can be appropriate for a controlled test system, as long as the address is isolated, disposable, and never used for real customer data. Do not share one inbox across parallel workers: an old verification link can make a new test appear healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs in the quarantine queue
&lt;/h2&gt;

&lt;p&gt;Quarantine is not a second pass that reruns every red test forever. It is a bounded holding area for failures where the application result is not yet known. I normally record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Playwright test title and commit SHA;&lt;/li&gt;
&lt;li&gt;the worker and browser project;&lt;/li&gt;
&lt;li&gt;the run-scoped inbox identifier;&lt;/li&gt;
&lt;li&gt;the request time and the first time a matching message was seen;&lt;/li&gt;
&lt;li&gt;the message subject, provider status, and extracted link host;&lt;/li&gt;
&lt;li&gt;a screenshot, trace, and relevant server correlation ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The evidence should not contain the full email body by default. Store a hash, selected headers, and the minimum redacted excerpt needed to diagnose the problem. If a test searches for a phrase typed by a user, remember mistakes such as &lt;code&gt;tepm mail com&lt;/code&gt; or &lt;code&gt;tem email&lt;/code&gt;; keep those values as test inputs, not failure labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Playwright fixture with an evidence trail
&lt;/h2&gt;

&lt;p&gt;The fixture below shows the important shape. The inbox client is deliberately abstract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@playwright/test'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Fixtures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;waitForVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Fixtures&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${testInfo.workerIndex}-${testInfo.testId}-${Date.now()}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="n"&gt;waitForVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;runId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;waitForMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Replace with a bounded poll against the test inbox service.&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`No matching message for ${runId}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mail-context'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;startedAt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real poller should use a deadline and a backoff, not an unbounded loop. It should also filter by &lt;code&gt;runId&lt;/code&gt; before checking the subject. That ordering matters becuase two messages can have the same subject while only one belongs to this test.&lt;/p&gt;

&lt;p&gt;For a more formal boundary, the ideas in &lt;a href="https://dev.to/ryanlee91/typescript-contracts-for-react-email-tests-4n00"&gt;typed contracts for email tests&lt;/a&gt; are useful: make invalid states difficult to pass into the next step. The fixture can return a &lt;code&gt;VerifiedMessage&lt;/code&gt; only after the timestamp, recipient, and link checks succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to classify and replay a failure
&lt;/h2&gt;

&lt;p&gt;When a test fails, ask what the last confirmed state was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No send request:&lt;/strong&gt; investigate the product flow or the browser action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send accepted, no message:&lt;/strong&gt; inspect the worker, provider, and inbox boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message found, wrong run ID:&lt;/strong&gt; investigate data isolation and cleanup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct message, bad link:&lt;/strong&gt; treat it as an application or template failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct link, browser failure:&lt;/strong&gt; inspect navigation, authentication, or the target page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the second and third categories are good candidates for a bounded quarantine retry. Replay with the same evidence and a new run ID. Never replay by reusing the original mailbox, because that can turn a stale message into a false pass. If the retry succeeds, keep both attempts visible; a green retry is a signal about reliability, not proof that the first failure was harmless.&lt;/p&gt;

&lt;p&gt;When the test uses an external address for a documented scenario, use a natural label such as &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail for facebook&lt;/a&gt; only in the test-data notes, not as a substitute for a production identity. The test should verify the product's behavior and cleanup policy, not encourage a real account workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checklist
&lt;/h2&gt;

&lt;p&gt;Before merging an email test, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the inbox unique per worker and run?&lt;/li&gt;
&lt;li&gt;Does the message query require a run identifier and a fresh timestamp?&lt;/li&gt;
&lt;li&gt;Is polling bounded and observable?&lt;/li&gt;
&lt;li&gt;Are traces and screenshots attached only when useful?&lt;/li&gt;
&lt;li&gt;Can the failure be classified from the saved evidence?&lt;/li&gt;
&lt;li&gt;Does quarantine expire and avoid automatic retry loops?&lt;/li&gt;
&lt;li&gt;Is cleanup attempted even when the browser step fails?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A test that fails with a causal timeline is much cheaper to maintain than one that merely says “verification email not found.” The goal is not to make Playwright tolerate every delay. It is to make the next failure tell the team where the delay or state mismatch began.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions worth asking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I retry every missing email?
&lt;/h3&gt;

&lt;p&gt;No. Retry only when the evidence says the request was accepted and the missing message may be a transient delivery problem. A missing send request is a product failure and should remain red.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a disposable mailbox always safe for testing?
&lt;/h3&gt;

&lt;p&gt;No. Keep it restricted to non-sensitive data, understand who can access the inbox, and delete messages after the run. The isolation contract matters more than the label “disposable.”&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Playwright Email Tests Need a State Ledger</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 10 Nov 2026 23:23:18 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-a-state-ledger-7e6</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-a-state-ledger-7e6</guid>
      <description>&lt;p&gt;Email verification tests often fail in a confusing way: the browser says that the confirmation page is ready, but the message belongs to a previous run, the link is expired, or the inbox has not received anything yet. The final assertion is where the test stops, not where the failure started.&lt;/p&gt;

&lt;p&gt;This is the workflow I use when a Playwright email test needs better evidence. I give every run a small state ledger. The ledger records what the test asked for, what message it observed, and which verification state was reached. It turns a vague “email test failed” into a short causal story that QA can act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a passing browser assertion can hide an email failure
&lt;/h2&gt;

&lt;p&gt;An email verification flow crosses several systems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser submits an address.&lt;/li&gt;
&lt;li&gt;The application creates a verification request.&lt;/li&gt;
&lt;li&gt;A mail service accepts and delivers the message.&lt;/li&gt;
&lt;li&gt;The test reads the message and opens its link.&lt;/li&gt;
&lt;li&gt;The application changes the account state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the test only asserts the last step, all five stages look like one operation. A stale message can satisfy a loose subject check. A shared inbox can make the test pass with an address from another run. A retry can then hide the original problem.&lt;/p&gt;

&lt;p&gt;A burner email may help isolate test data, but isolation alone does not explain timing or message identity. The test still needs to say which request it is waiting for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the verification flow as a state ledger
&lt;/h2&gt;

&lt;p&gt;Keep the ledger small and append-only. For each run, record an event such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;EmailEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"requested"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"delivered"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"opened"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"verified"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"expired"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;messageId&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;observedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important field is &lt;code&gt;runId&lt;/code&gt;. Put it in the test address, a request header, or metadata that your mail test fixture can query. The exact transport matters less than being able to connect a message to one browser run.&lt;/p&gt;

&lt;p&gt;Do not overwrite the previous event when a poll repeats. That small choice preserve the sequence and makes a delayed delivery visible. It also keeps a test retry from erasing the evidence produced by the first attempt.&lt;/p&gt;

&lt;p&gt;For sensitive environments, pair the ledger with &lt;a href="https://dev.to/bitheirstake/verification-logs-need-a-redaction-contract-2mk5"&gt;a redaction contract for verification logs&lt;/a&gt;. Logs should identify the message without copying the full token or mailbox contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture the ledger in a Playwright test
&lt;/h2&gt;

&lt;p&gt;The browser test can write events beside its trace. Here is a deliberately simple pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EmailEvent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EmailEvent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"requested"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Send verification email"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;testAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;Verify&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"delivered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verificationUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"opened"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Email verified"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real fixture, &lt;code&gt;waitForMessage&lt;/code&gt; should reject messages whose run marker does not match. It should also report the last polling observation in its error, not only “timeout”. The test pass is more useful when the failure says “requested, no matching delivery after 30 seconds” than when it says “expected text to be visible”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose the first bad state
&lt;/h2&gt;

&lt;p&gt;When a run fails, inspect the first transition that did not happen. If &lt;code&gt;requested&lt;/code&gt; is missing, the browser action or fixture setup failed. If &lt;code&gt;delivered&lt;/code&gt; is missing, check the mail service, address routing, and delivery timeout. If &lt;code&gt;opened&lt;/code&gt; is missing, inspect URL extraction and token expiry. If &lt;code&gt;verified&lt;/code&gt; is missing, the application probably rejected the token or the page assertion is looking at the wrong state.&lt;/p&gt;

&lt;p&gt;This order stops a common debugging mistake: changing Playwright waits when the message was never delivered. More waiting can make the suite slower while keeping the root cause untouched.&lt;/p&gt;

&lt;p&gt;For CI pipelines, store the ledger with the trace and link it to &lt;a href="https://dev.to/jasonmills94/aws-cicd-email-tests-need-a-promotion-receipt-25hc"&gt;a promotion receipt for CI email tests&lt;/a&gt;. A failed run then has both the browser evidence and the environment transition that produced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small QA checklist for reliable email tests
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Give each run a unique marker and test address.&lt;/li&gt;
&lt;li&gt;Match the recipient, run marker, and message purpose.&lt;/li&gt;
&lt;li&gt;Record every state transition; do not only record the final result.&lt;/li&gt;
&lt;li&gt;Make polling errors include the last observed state.&lt;/li&gt;
&lt;li&gt;Expire or clean test data after the run, even on failure.&lt;/li&gt;
&lt;li&gt;Redact tokens, message bodies, and personal addresses from artifacts.&lt;/li&gt;
&lt;li&gt;Keep one focused test for delivery and another for token acceptance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team calls a fixture “temp org mail” in a note or dashboard, keep that label separate from the real run identifier. Informal names are handy, but they are not a reliable correlation key.&lt;/p&gt;

&lt;p&gt;The goal is not to add a large observability platform to every end-to-end test. It is to preserve enough evidence to answer one question: what was the first email state that became false? Once that answer is recorded, Playwright failures become easier to reproduce, triage, and fix.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Agentes LLM: un contrato de identidad para emails</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 10 Nov 2026 05:24:00 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-un-contrato-de-identidad-para-emails-4c90</link>
      <guid>https://dev.to/silviutech/agentes-llm-un-contrato-de-identidad-para-emails-4c90</guid>
      <description>&lt;p&gt;Un agente LLM puede completar un flujo de registro en pocos segundos y, aun así, dejar una prueba imposible de depurar. El formulario se envió, llegó el correo de verificación y el agente hizo clic en el enlace correcto. Pero cuando el paso falla, nadie sabe qué mensaje leyó, qué buzón pertenecía a la ejecución o si un reintento reutilizó datos viejos.&lt;/p&gt;

&lt;p&gt;El problema no suele ser el modelo. Es la falta de una identidad técnica para cada email de prueba. Un generador de emails falsos puede crear una dirección, pero el sistema de automatización todavía necesita decidir quién la posee, cuánto dura y qué evidencia debe producir.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: el agente pierde la identidad del email
&lt;/h2&gt;

&lt;p&gt;En un flujo sencillo, la relación parece obvia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ejecución -&amp;gt; dirección -&amp;gt; mensaje -&amp;gt; enlace -&amp;gt; resultado
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En un flujo con agentes aparecen más caminos. El modelo puede reintentar una herramienta, delegar el paso a otro agente o continuar después de un timeout. Si todos consultan una bandeja compartida, el mensaje correcto deja de ser una certeza. Un correo parecido puede activar el flujo equivocado y hacer que el resultado parezca válido.&lt;/p&gt;

&lt;p&gt;La ambigüedad aumenta cuando se usan nombres informales para buscar una solución, como &lt;code&gt;dummy e mail&lt;/code&gt; o &lt;code&gt;tempail mail&lt;/code&gt;. Esas frases pueden servir para entender consultas, pero no son una identidad de ejecución ni un criterio de seguridad.&lt;/p&gt;

&lt;p&gt;La primera decisión de arquitectura es separar el objetivo del agente de la infraestructura que lo ejecuta. El agente debe pedir “espera el mensaje de verificación de esta ejecución”, no “busca el correo más reciente en esta bandeja”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseñar una identidad por ejecución
&lt;/h2&gt;

&lt;p&gt;Cada flujo debe recibir un &lt;code&gt;run_id&lt;/code&gt; único y conservarlo hasta el cierre. La identidad puede tener esta forma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"checkout-20261110-0522-a81f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"agent-registration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"mailbox_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"mbx-7f2c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-10T05:42:00Z"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La dirección es solo una parte del contrato. &lt;code&gt;run_id&lt;/code&gt; permite correlacionar la acción del agente, la solicitud de la API, la llegada del mensaje y el resultado del test. &lt;code&gt;purpose&lt;/code&gt; evita que un mensaje de recuperación de contraseña se confunda con uno de bienvenida. &lt;code&gt;expires_at&lt;/code&gt; da al sistema una decisión clara cuando el agente abandona el flujo.&lt;/p&gt;

&lt;p&gt;En términos de diseño, el agente obtiene una referencia opaca y no necesita conocer detalles del proveedor. El adaptador de email traduce esa referencia a una dirección temporal, aplica el filtro por ejecución y devuelve eventos normalizados. Es un límite pequeño, pero hace mas fácil cambiar de proveedor o probar el adaptador en local.&lt;/p&gt;

&lt;p&gt;Cuando el flujo también mide activación, conviene &lt;a href="https://dev.to/hannahdev56/saas-mide-activacion-sin-perder-el-hilo-del-email-1nfh"&gt;medir la activación sin perder el hilo del email&lt;/a&gt;. La métrica no debería decir solamente “se recibió un correo”; debe poder responder qué ejecución lo recibió y qué acción ocurrió después.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato mínimo para agentes LLM
&lt;/h2&gt;

&lt;p&gt;Las herramientas que ve el modelo necesitan nombres y respuestas predecibles. Un contrato mínimo puede tener tres operaciones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;reserve_mailbox(purpose, ttl)&lt;/code&gt;: crea una identidad aislada y devuelve &lt;code&gt;mailbox_id&lt;/code&gt; y &lt;code&gt;run_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wait_for_message(mailbox_id, kind, timeout)&lt;/code&gt;: busca solo mensajes asociados a esa identidad.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;release_mailbox(mailbox_id, reason)&lt;/code&gt;: marca la fixture como terminada y activa la limpieza.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La respuesta de &lt;code&gt;wait_for_message&lt;/code&gt; no debe entregar el buzón completo ni datos innecesarios. Es suficiente con un evento como este:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"msg-91ab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"received_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-10T05:25:11Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://app.example.test/verify?token=redacted"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El token puede mantenerse fuera del historial visible al modelo y resolverse mediante una acción controlada. Así se reduce el riesgo de que un resumen, una traza o una llamada posterior copie un secreto. Si el agente necesita el enlace, el adaptador puede validar que pertenece al dominio esperado antes de devolverlo.&lt;/p&gt;

&lt;p&gt;No hace falta convertir &lt;code&gt;tempmailso&lt;/code&gt; en una dependencia del razonamiento del agente. Puede aparecer como término de inventario o de búsqueda, pero la integración debe depender de una interfaz propia, con límites y políticas definidos por el equipo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo evitar mezclas durante los reintentos
&lt;/h2&gt;

&lt;p&gt;Los reintentos son donde se rompe la mayoría de los diseños ingenuos. Si una llamada expira después de crear la bandeja, repetirla puede producir otra identidad. Si el primer agente vuelve a despertar, puede leer el mensaje de la segunda.&lt;/p&gt;

&lt;p&gt;La solución es que la reserva sea idempotente para una clave de ejecución. El adaptador puede aceptar &lt;code&gt;idempotency_key = run_id + purpose&lt;/code&gt; y devolver la misma referencia mientras la fixture siga activa. El estado debería distinguir, al menos, &lt;code&gt;reserved&lt;/code&gt;, &lt;code&gt;message_received&lt;/code&gt;, &lt;code&gt;verified&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt; y &lt;code&gt;expired&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;También hay que definir qué significa “mensaje más reciente”. En general, no basta con ordenar por fecha. El filtro debe comprobar &lt;code&gt;mailbox_id&lt;/code&gt;, tipo de mensaje, emisor esperado y una ventana temporal razonable. Si faltan esas señales, el agente debe recibir un resultado &lt;code&gt;ambiguous&lt;/code&gt; y detenerse, no elegir por intuición.&lt;/p&gt;

&lt;p&gt;Un reconciliador externo puede retirar las fixtures vencidas aunque el proceso del agente haya terminado mal. Si el despliegue corre sobre Kubernetes, es útil &lt;a href="https://dev.to/alexcarteruk/kubernetes-valida-alertas-tras-rotar-secretos-1ko5"&gt;validar alertas después de un cambio de secretos&lt;/a&gt; como parte del mismo enfoque: los cambios de infraestructura y las pruebas de email necesitan señales observables, no solo un log de éxito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observabilidad y límites de seguridad
&lt;/h2&gt;

&lt;p&gt;Los logs deben permitir reconstruir la secuencia sin guardar el contenido completo de los mensajes. Una línea útil contiene &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;mailbox_id&lt;/code&gt;, &lt;code&gt;tool_call_id&lt;/code&gt;, estado, latencia y motivo del cierre. Los resultados llega mejor agrupados por ejecución que por dirección, porque la dirección puede ser efímera.&lt;/p&gt;

&lt;p&gt;El modelo tampoco debería tener permiso para crear identidades sin límite, cambiar el dominio de destino o desactivar la expiración. El adaptador debe imponer un TTL máximo, permitir solo dominios de prueba y rechazar una dirección que ya pertenezca a otra ejecución. Estas reglas deben vivir fuera del prompt: un agente puede interpretar instrucciones, pero no debe ser la única barrera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Es necesario un buzón por agente?
&lt;/h3&gt;

&lt;p&gt;No siempre. La unidad segura es la ejecución o el escenario. Dos agentes pueden compartir una identidad si tienen el mismo propósito y una coordinación explícita; una bandeja global para toda la suite es mucho más dificil de investigar.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué debe hacer el agente cuando llega un mensaje ambiguo?
&lt;/h3&gt;

&lt;p&gt;Debe detener el paso, registrar la ambigüedad y solicitar una nueva consulta con criterios más estrictos. Elegir el mensaje mas reciente puede convertir un fallo de aislamiento en un falso positivo.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo usar un proveedor real en pruebas locales?
&lt;/h3&gt;

&lt;p&gt;Sí, si el adaptador oculta sus detalles, aplica expiración y evita datos personales. Para pruebas unitarias, una implementación en memoria suele ser suficiente y hace que el contrato sea más facil de verificar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de implementación
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cada ejecución recibe un &lt;code&gt;run_id&lt;/code&gt; y una identidad aislada.&lt;/li&gt;
&lt;li&gt;La reserva usa una clave idempotente para soportar reintentos.&lt;/li&gt;
&lt;li&gt;Las consultas filtran por buzón, propósito, emisor y ventana temporal.&lt;/li&gt;
&lt;li&gt;El modelo recibe eventos mínimos, no secretos ni cuerpos completos.&lt;/li&gt;
&lt;li&gt;La expiración tiene un límite impuesto por el adaptador.&lt;/li&gt;
&lt;li&gt;Un reconciliador limpia fixtures abandonadas.&lt;/li&gt;
&lt;li&gt;Los estados y las llamadas de herramientas quedan correlacionados.&lt;/li&gt;
&lt;li&gt;Los casos ambiguos terminan con una señal explícita, nunca con una elección arbitraria.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La automatización con modelos LLM se vuelve más confiable cuando el agente no tiene que adivinar qué email le pertenece. Una identidad por ejecución, un contrato pequeño y una política de expiración convierten una integración frágil en un sistema que se puede observar, reintentar y explicar.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Playwright Email Tests: Find the First Bad State</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 09 Nov 2026 20:24:08 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-find-the-first-bad-state-8lk</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-find-the-first-bad-state-8lk</guid>
      <description>&lt;p&gt;Email verification tests often fail in the inbox step, but the inbox is rarely the real cause. A signup test can pass the form, create a user, request a message, and then fail because the assertion ran before the message became visible. The failure is usualy reported as “email not found,” which hides the more useful question: &lt;strong&gt;what was the first state that became impossible?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This Playwright workflow works with a temp mail so provider, a controlled burner email service, or an internal test inbox. Give every transition a timestamp, an owner, and a clear failure message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email tests fail after the signup step
&lt;/h2&gt;

&lt;p&gt;An email test usually crosses several systems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser submits the registration form.&lt;/li&gt;
&lt;li&gt;The application commits the account and queues an email.&lt;/li&gt;
&lt;li&gt;A mail service accepts and processes the message.&lt;/li&gt;
&lt;li&gt;The inbox API exposes the message.&lt;/li&gt;
&lt;li&gt;Playwright opens the message and verifies the link or code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step has a different kind of delay. A browser assertion can wait for a DOM element, but it cannot automatically wait for a message that has not been delivered yet. A fixed &lt;code&gt;waitForTimeout&lt;/code&gt; sometimes masks this timing gap, then fails again under CI load.&lt;/p&gt;

&lt;p&gt;The inbox is not a database query; its a delayed event. Treating it like one creates misleading retries and slow debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the test as a state timeline
&lt;/h2&gt;

&lt;p&gt;Before changing timeouts, name the states your test expects. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;form_submitted
account_created
message_requested
message_received
verification_link_opened
account_verified
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record the transition that your test can observe. The browser can observe the submitted form and the verification page. Your inbox client can observe message receipt. Your application logs can confirm account creation. If the test only records the final assertion, all earlier failures collapse into one vague symptom.&lt;/p&gt;

&lt;p&gt;A small event record is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EmailTestEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;details&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailTestEvent&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;mark("form_submitted")&lt;/code&gt; immediately after submit, and mark the inbox state only after the inbox client confirms a matching message. When a test fails, print the timeline with the test name and run ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Playwright diagnostic workflow
&lt;/h2&gt;

&lt;p&gt;Use a unique address per test or per worker. A shared burner email makes parallel tests compete for the same messages, so one test can consume another test’s verification link. Unique identity also makes cleanup and investigation safer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`pw-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workerIndex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;@example.test`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test_identity_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;testEmail&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form_submitted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check your email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify your account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message_received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/verify/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verification_link_opened&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Account verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account_verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract around &lt;code&gt;waitForMessage&lt;/code&gt; should filter by recipient and subject, return a message ID, and fail with observed candidates. “Timed out after 30 seconds” is less useful than “received two messages for another recipient; no matching subject for this run.”&lt;/p&gt;

&lt;p&gt;If your inbox API supports it, pass a run-scoped token in the message metadata. A simple contract can prevent an old message from satisfying a new test. The idea is similar to keeping &lt;a href="https://dev.to/kevindev27/store-email-policy-snapshots-with-auth-events-43ba"&gt;email policy snapshots&lt;/a&gt;: preserve the evidence that explains what the system decided at that moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep inbox reads isolated and observable
&lt;/h2&gt;

&lt;p&gt;There are three boundaries worth enforcing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity boundary:&lt;/strong&gt; generate an address that no other test uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message boundary:&lt;/strong&gt; match the recipient, subject, and run marker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup boundary:&lt;/strong&gt; expire or delete the inbox after the test, while retaining a small failure record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not hide the inbox call inside a generic retry that catches every error. A retry should handle a message that is not available yet, not authentication errors, malformed responses, or a message with the wrong recipient. Those are different defects and should fail immediately.&lt;/p&gt;

&lt;p&gt;For agent-driven or command-line test systems, a documented &lt;a href="https://dev.to/mrdapperx/cli-inbox-contracts-for-ai-agents-26a9"&gt;CLI inbox contract&lt;/a&gt; is helpful for the same reason. Define what “waiting” returns, how long it waits, and which evidence is retained. Otherwise, every caller invents a slightly different polling loop.&lt;/p&gt;

&lt;p&gt;Also check the clock used in logs. Browser, application, and inbox-provider time may not line up perfectly. Use one local sequence number so the report can show event order. The report become less confusing when timestamps are supplementary evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical failure checklist
&lt;/h2&gt;

&lt;p&gt;When the test fails, inspect these items in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Was the address unique?&lt;/strong&gt; Shared inboxes create false positives and missing messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did the UI show the post-submit state?&lt;/strong&gt; If not, investigate the form or API response first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was a message requested?&lt;/strong&gt; Check the application event or request log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did the inbox receive any message?&lt;/strong&gt; Keep the candidate subjects and recipients in the failure output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did the message belong to this run?&lt;/strong&gt; Reject stale messages and links from previous attempts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was the link extracted correctly?&lt;/strong&gt; Verify the URL pattern before navigating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did the verification page fail?&lt;/strong&gt; Preserve the browser trace and the final response details.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The checklist is intentionally boring. Boring checks are repeatable, and repeatability is what turns flaky Testing into a diagnosable workflow. It also keeps a typo such as &lt;code&gt;tepm mail com&lt;/code&gt; from becoming a hidden fixture assumption in a test report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions teams usually ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I increase the timeout first?
&lt;/h3&gt;

&lt;p&gt;Only after confirming that the message is eventually delivered and belongs to the correct test. A longer timeout can reduce noise, but it cannot fix a missing event, a bad filter, or a shared inbox race.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is polling always a problem?
&lt;/h3&gt;

&lt;p&gt;No. Polling is reasonable when the provider offers no webhook or event stream. Make it bounded, observable, and selective. Record each retry reason, but keep the final failure short enough to scan in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should a retry repeat?
&lt;/h3&gt;

&lt;p&gt;Prefer retrying the inbox read, not the entire signup flow. Repeating signup may create duplicate accounts and multiple verification messages, which make the original failure harder to understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does a good failure message look like?
&lt;/h3&gt;

&lt;p&gt;It names the run, recipient, expected subject, elapsed time, last observed state, and a safe list of candidate messages. That gives the next engineer a starting point instead of another unexplained timeout.&lt;/p&gt;

&lt;p&gt;Once the test reports its first bad state, email verification stops being a mysterious end-to-end failure. Playwright remains responsible for browser behavior, the inbox client remains responsible for delivery evidence, and the timeline connects both without pretending they are one system.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: formularios ágiles sin perder el foco</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 08 Nov 2026 14:23:55 +0000</pubDate>
      <link>https://dev.to/silviutech/react-formularios-agiles-sin-perder-el-foco-b75</link>
      <guid>https://dev.to/silviutech/react-formularios-agiles-sin-perder-el-foco-b75</guid>
      <description>&lt;p&gt;Un formulario moderno no solo debe enviar datos. También tiene que explicar qué está ocurriendo mientras el usuario escribe, valida, espera una respuesta o corrige un error. Cuando esos estados no están bien diseñados, la interfaz parece lenta aunque la red responda en pocos milisegundos.&lt;/p&gt;

&lt;p&gt;En proyectos React he encontrado que una mejora pequeña, como reservar espacio para un mensaje de error o devolver el foco al campo correcto, suele tener más impacto que añadir otra librería. La idea es tratar el formulario como una secuencia de estados visibles y medibles, no como un botón con un spinner.&lt;/p&gt;

&lt;h2&gt;
  
  
  El formulario como una secuencia de estados
&lt;/h2&gt;

&lt;p&gt;Un campo de email puede pasar por varios estados distintos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vacío y listo para recibir entrada;&lt;/li&gt;
&lt;li&gt;escribiendo, con una validación que todavía no terminó;&lt;/li&gt;
&lt;li&gt;válido, pero esperando el envío;&lt;/li&gt;
&lt;li&gt;rechazado, con una explicación que se puede corregir;&lt;/li&gt;
&lt;li&gt;enviado, con una confirmación que no desaparece demasiado rápido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El problema típico es representar todos esos casos con un único &lt;code&gt;isLoading&lt;/code&gt;. Ese booleano no dice si el usuario está esperando una validación local, una respuesta del servidor o una transición de página. El resultado es una pantalla que parece bloqueada y un lector de pantalla que recibe poca información.&lt;/p&gt;

&lt;p&gt;Separar los estados hace la UI más predecible. No hace falta crear una máquina de estados completa para cada formulario; un objeto pequeño suele ser suficiente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revisa tu bandeja de entrada.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No pudimos enviar el correo. Inténtalo de nuevo.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo importante no es el nombre exacto de cada estado. Es que cada transición tenga una consecuencia visual, semántica y de interacción.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué debe comunicar cada estado
&lt;/h2&gt;

&lt;p&gt;Durante el envío, desactivar el botón puede ser correcto, pero no debería eliminar la posibilidad de entender la acción. Un texto como “Enviando…” es más util que un icono girando sin nombre. Si la operación dura más de lo esperado, la interfaz debe seguir mostrando el contexto del formulario.&lt;/p&gt;

&lt;p&gt;Para los errores, coloca el mensaje junto al campo y conéctalo mediante &lt;code&gt;aria-describedby&lt;/code&gt;. Así, el usuario puede volver al campo sin tener que descubrir dónde quedó la explicación. Después de un error de servidor, el foco puede regresar al primer campo que necesita atención; después de un éxito, un anuncio breve en una región &lt;code&gt;aria-live&lt;/code&gt; suele ser suficiente.&lt;/p&gt;

&lt;p&gt;También conviene reservar una altura mínima para el mensaje. Sin esa reserva, el contenido salta cuando aparece la validación y el foco visual se mueve. Ese pequeño salto no siempre rompe una métrica, pero si ocurre varias veces se siente como una interfaz descuidada.&lt;/p&gt;

&lt;p&gt;En formularios de registro, algunas personas buscan ejemplos con términos imperfectos como &lt;code&gt;tempail&lt;/code&gt; o &lt;code&gt;tamp mail com&lt;/code&gt;. Aunque parezcan detalles de contenido, sirven para probar que los mensajes y los resultados de validación siguen siendo entendibles cuando la entrada no coincide con el caso ideal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patrón React pequeño y medible
&lt;/h2&gt;

&lt;p&gt;Para que el componente no dependa de muchos efectos, valida en el evento que corresponde y mide dos momentos: cuándo empieza la interacción y cuándo el usuario recibe una respuesta útil. Es facil instrumentar esos puntos con una función local:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;reportMetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form_response_time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;reportMetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form_error_time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hace falta enviar el contenido del email ni datos personales para aprender algo útil. Basta con registrar el tipo de resultado, la duración y, si aplica, el paso donde ocurrió el error. Así puedes comparar una optimización de CSS, una validación en el cliente o un cambio de endpoint sin convertir el formulario en una caja negra.&lt;/p&gt;

&lt;p&gt;Una buena métrica no es solamente el tiempo total. Observa también cuántos usuarios vuelven a pulsar el botón, cuántos corrigen el mismo campo y cuánto tarda en aparecer un mensaje comprensible. Cuando esas señales mejoran, la experiencia mejora aunque el backend mantenga la misma latencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accesibilidad y rendimiento en la misma revisión
&lt;/h2&gt;

&lt;p&gt;El rendimiento percibido depende de la claridad. Un skeleton que cambia de tamaño, un botón que desaparece y un error que aparece lejos del foco hacen que una respuesta rápida se sienta peor. Por eso, la revisión debe incluir teclado, lector de pantalla y una conexión más lenta.&lt;/p&gt;

&lt;p&gt;Comprueba que el foco sea visible en todos los estados, que el contraste no dependa solo del color y que el botón conserve un nombre útil. En CSS, evita animar propiedades que obliguen al navegador a recalcular demasiado trabajo. Transiciones cortas de &lt;code&gt;opacity&lt;/code&gt; o &lt;code&gt;transform&lt;/code&gt; suelen ser más seguras que mover grandes bloques de contenido.&lt;/p&gt;

&lt;p&gt;También prueba la navegación con Tab después de un error. Si los estados se vuelve difíciles de seguir, probablemente el componente mezcla demasiadas responsabilidades. Extraer &lt;code&gt;FieldError&lt;/code&gt;, &lt;code&gt;SubmitButton&lt;/code&gt; y &lt;code&gt;FormStatus&lt;/code&gt; puede mejorar la lectura sin introducir una abstracción enorme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist antes de enviar el formulario
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;¿El usuario sabe si el formulario está listo, enviando, correcto o falló?&lt;/li&gt;
&lt;li&gt;¿El foco llega al lugar correcto despues de un error?&lt;/li&gt;
&lt;li&gt;¿El mensaje de éxito se anuncia sin interrumpir toda la página?&lt;/li&gt;
&lt;li&gt;¿El contenido mantiene su posición cuando aparece una validación?&lt;/li&gt;
&lt;li&gt;¿El flujo funciona sin ratón y con zoom del navegador?&lt;/li&gt;
&lt;li&gt;¿Mides el tiempo hasta una respuesta útil, no solo el tiempo de red?&lt;/li&gt;
&lt;li&gt;¿Puedes depurar el intento con un identificador sin guardar datos privados?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los formularios ágiles no son los que esconden toda espera. Son los que convierten la espera en una señal clara, accesible y estable. Ese enfoque tambien ayuda a probar integraciones: un &lt;a href="https://dev.to/alexcarteruk/kubernetes-un-contrato-de-fallos-para-emails-de-prueba-176"&gt;contrato de fallos para emails de prueba&lt;/a&gt; puede complementar las comprobaciones del frontend, y &lt;a href="https://dev.to/alexcarteruk/como-validar-correos-de-alertmanager-tras-rotar-secretos-en-kubernetes-4fi5"&gt;validar correos después de rotar secretos&lt;/a&gt; recuerda que la ruta de entrega también necesita estados observables.&lt;/p&gt;

&lt;p&gt;La próxima vez que un formulario parezca lento, mira primero qué está comunicando. A veces la mejora más rapida no es reducir una llamada, sino hacer que cada paso tenga un significado visible.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>React accesible: menos espera en formularios</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 08 Nov 2026 08:23:43 +0000</pubDate>
      <link>https://dev.to/silviutech/react-accesible-menos-espera-en-formularios-21la</link>
      <guid>https://dev.to/silviutech/react-accesible-menos-espera-en-formularios-21la</guid>
      <description>&lt;p&gt;Un formulario puede ser técnicamente rápido y aun así sentirse lento. El caso típico: la persona pulsa «Crear cuenta», el botón cambia apenas de color y durante dos segundos no ocurre nada visible. En ese hueco, puede pulsar otra vez, cambiar de ventana o pensar que el sitio se rompió.&lt;/p&gt;

&lt;p&gt;En interfaces React suelo tratar la espera como parte del diseño, no como un detalle posterior de rendimiento. El objetivo no es ocultar cada milisegundo, sino hacer que el estado actual sea comprensible para una persona que usa ratón, teclado o lector de pantalla. Esta misma idea ayuda a mantener limpias las pruebas de correo: un estado claro de «verificando» es más fácil de asociar con un evento que un spinner sin contexto.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema no es solo la velocidad
&lt;/h2&gt;

&lt;p&gt;Un formulario tiene varios estados que se mezclan con facilidad:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;listo para enviar;&lt;/li&gt;
&lt;li&gt;validando datos locales;&lt;/li&gt;
&lt;li&gt;enviando la petición;&lt;/li&gt;
&lt;li&gt;esperando una confirmación;&lt;/li&gt;
&lt;li&gt;completado;&lt;/li&gt;
&lt;li&gt;error recuperable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si todos se representan con el mismo botón gris, la interfaz obliga a adivinar. Además, un doble clic puede disparar dos peticiones, y un error de red puede borrar los datos que la persona ya escribió. En flujos con email, conviene separar «la petición salió» de «el mensaje fue recibido». Revisar cohortes de activación por email también resulta más útil cuando cada paso del flujo tiene una señal reconocible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseñar estados que se puedan percibir
&lt;/h2&gt;

&lt;p&gt;Primero, define una máquina de estados pequeña. No hace falta una librería para ello:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La diferencia entre &lt;code&gt;checking&lt;/code&gt; y &lt;code&gt;sending&lt;/code&gt; importa. La validación local puede terminar en unos milisegundos, mientras que la petición remota depende de la red. Mostrar mensajes distintos evita que una persona crea que el botón no funcionó.&lt;/p&gt;

&lt;p&gt;Un componente mínimo puede anunciar el cambio sin mover el foco de forma brusca:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SubmitStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Guardando tus datos…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;role="status"&lt;/code&gt; ofrece una región viva adecuada para mensajes que no son una alerta crítica. El texto debe describir la acción, no la implementación: «Comprobando tu correo» ayuda más que «loading=true». Para un lector de pantalla, esta diferencia es bastante importante.&lt;/p&gt;

&lt;p&gt;También desactiva el envío mientras la petición está activa, pero no desactives todo el formulario sin motivo. Si la contraseña contiene un error, la persona debe poder corregirla. Cuando el envío termina, conserva los valores y coloca el foco en el resumen del error o en el mensaje de éxito, según el caso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un patrón sencillo en React
&lt;/h2&gt;

&lt;p&gt;El handler puede protegerse contra dobles envíos y limpiar el estado en un orden predecible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLFormElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No se pudo guardar. Revisa tu conexión e inténtalo de nuevo.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En producción, añade una cancelación o un identificador de petición si el componente puede desmontarse mientras espera. Así no intentas actualizar un componente que ya no existe. Para procesos de email, conviene guardar también un identificador de ejecución; una dirección de correo desechable puede servir como dato controlado de prueba, pero no reemplaza la separación entre estados.&lt;/p&gt;

&lt;p&gt;Si el flujo reenvía un mensaje, la protección contra dobles envíos debe cubrir tanto el botón como la acción de teclado. El artículo sobre cómo evitar dobles envíos en React muestra por qué una bandera visual aislada no siempre basta: la regla debe vivir en la acción que inicia la petición.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teclado, lectores de pantalla y foco
&lt;/h2&gt;

&lt;p&gt;Un diseño accesible no consiste solo en añadir &lt;code&gt;aria-label&lt;/code&gt;. Comprueba el recorrido real:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Entra al formulario con &lt;code&gt;Tab&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Envía datos inválidos sin usar el ratón.&lt;/li&gt;
&lt;li&gt;Verifica que el error esté asociado al campo con &lt;code&gt;aria-describedby&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Envía datos válidos y escucha el anuncio del estado.&lt;/li&gt;
&lt;li&gt;Simula una respuesta lenta y comprueba que el foco no desaparezca.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Evita reemplazar todo el formulario al mostrar el resultado. Ese cambio puede hacer que el foco vuelva al principio de la página, y la persona se queda un poco perdida. Un resumen persistente, un borde de error además del color y un mensaje corto suelen ser suficientes.&lt;/p&gt;

&lt;p&gt;Para probar el recorrido, no necesitas crear un entorno enorme. Un caso con un tempail controlado puede comprobar el estado de espera, pero usa datos que no contengan información personal y elimina los artefactos después. La métrica útil no es solo «la petición respondió», sino cuánto tardó el usuario en recibir una señal clara.&lt;/p&gt;

&lt;h2&gt;
  
  
  Medir la espera que importa
&lt;/h2&gt;

&lt;p&gt;Mide por separado tres tiempos: interacción hasta feedback, feedback hasta respuesta y respuesta hasta confirmación visual. El primero suele explicar por qué una interfaz se percibe lenta aunque el backend sea veloz. Un mensaje de estado que aparece inmediatamente puede mejorar la confianza, pero no debe convertirse en una animación permanente.&lt;/p&gt;

&lt;p&gt;Registra estos tiempos en desarrollo o en una herramienta de observabilidad, sin incluir valores sensibles. Si el formulario pide confirmación por correo, no confundas «correo enviado» con «usuario activado». Separar ambas métricas evita conclusiones raras al revisar un embudo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist antes de publicar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;¿Cada estado tiene texto comprensible y no solo un cambio de color?&lt;/li&gt;
&lt;li&gt;¿El botón impide peticiones duplicadas durante el envío?&lt;/li&gt;
&lt;li&gt;¿Los errores están asociados a sus campos?&lt;/li&gt;
&lt;li&gt;¿&lt;code&gt;aria-live&lt;/code&gt; anuncia cambios sin interrumpir demasiado?&lt;/li&gt;
&lt;li&gt;¿El foco sigue una ruta lógica después de éxito y error?&lt;/li&gt;
&lt;li&gt;¿La respuesta lenta puede probarse sin depender de un servicio externo?&lt;/li&gt;
&lt;li&gt;¿Se distinguen entrega, confirmación y activación en las métricas?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una interfaz React bien pulida no hace que la red sea más rápida. Hace que la espera sea legible, que el teclado no pierda su lugar y que un fallo tenga una salida clara. Esa combinación de rendimiento percibido y Accesibilidad es pequeña en código, pero se nota mucho en el uso diario.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>frontend</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
