El error que mis pruebas unitarias nunca podrían haber encontrado
Esta semana publiqué una vista de tablero kanban para Greenhouse, y la parte interesante no es el tablero, es un error que toda mi suite de pruebas dejó pasar, y cómo lo encontré de todos modos.
La funcionalidad
El dashboard de Greenhouse tiene una zona “Worklist”: todos los proyectos activos en una sola lista, el más descuidado primero. Lo pedido era una alternativa estilo kanban, columnas por etapa del pipeline (Select, Explore, Shape…) para poder ver la distribución de un vistazo. Acoté el alcance antes de escribir cualquier código: solo lectura (avanzar un proyecto sigue pasando por el diálogo de confirmación existente, sin arrastrar y soltar), una vista nueva junto a Worklist en vez de un reemplazo, solo proyectos activos. Nada de eso fue adivinar, el propio issue marcaba estas cosas como preguntas abiertas, así que las resolví de entrada en vez de asumir.
El lado de los datos resultó no necesitar ningún cambio de backend. Una corrección anterior ya había colapsado la vieja división entre enfriamiento y disponible en un solo worklist completo, así que cada proyecto activo con su etapa de pipeline ya estaba ahí, en los datos que el dashboard ya traía. Agrupar por etapa es un filtro de una línea.
El atajo que parecía gratis
Acá es donde se puso interesante. El componente del dashboard ya guarda todo el estado diario en memoria: ideas capturadas, revisión del vault, el worklist, todo. Mi nuevo componente de tablero necesitaba exactamente una porción de eso: el worklist. Así que hice lo obvio y lo pasé hacia abajo como prop en vez de volver a pedirlo. Se sintió como una optimización limpia, ¿para qué hacer un segundo viaje de red por datos que el padre ya tiene ahí mismo?
Compiló. Pasó el chequeo de tipos. Todas mis pruebas de componentes con IPC simulado pasaron, porque esas pruebas le entregan el worklist directamente al componente, no hay forma de que una prueba así exprese “la copia del padre ya está desactualizada.”
Lo que lo detectó
No me basta con confiar en la suite de pruebas simuladas para una UI nueva en este código, también manejo la app compilada real a través de una sesión de WebDriver de verdad, clics reales, llamadas reales al backend, sin mocks en ningún punto del recorrido. Para este caso sembré un proyecto activo nuevo directamente por IPC (evitando la UI por completo, igual que lo haría un script), y después hice clic en “Board view.”
Columna vacía. Cero proyectos mostrados, cuando debería haber habido uno.
El dashboard ya había traído su estado diario una vez, en el momento de su propio montaje, antes de que la llamada IPC de mi script siquiera ocurriera. Mi componente de tablero renderizaba fielmente una prop que había quedado obsoleta en el instante en que existieron datos nuevos en cualquier otro lugar. En el uso real del día a día esta secuencia exacta no puede pasar, porque cada botón real de la UI que cambia el worklist ya refresca la copia del dashboard después. Pero ahí está el problema: la corrección del tablero dependía en silencio de que cada funcionalidad futura recordara mantener intacta esa cadena de refresco. Nada obliga a eso. Es el tipo de suposición que se sostiene hoy y se rompe en seis meses cuando alguien agrega un nuevo camino de mutación y no sabe que esta prop en particular cuenta con eso.
La corrección real
Volví a mirar cómo las otras dos vistas de ventana completa de esta app ya manejan exactamente esta forma de problema, una vista de detalle de proyecto y un explorador del vault, ambas ocupando el mismo espacio en el layout. Las dos traen sus propios datos al montarse, cada vez, sin importar lo que el padre ya tenga cargado. Me había desviado en silencio de un patrón establecido porque reutilizar la prop parecía eficiencia gratis, y lo pagué con un error real.
La corrección fue eliminar la prop y darle al tablero su propia búsqueda de datos, igual que ya hacen las otras dos vistas. Hay un efecto secundario agradable: como Svelte desmonta y reconstruye este componente cada vez que entra y sale de vista, volver al tablero después de editar un proyecto en otro lugar muestra los datos actuales de forma automática, sin necesidad de cablear ningún refresco manual en ningún lado.
La lección
Una prueba simulada solo puede ser tan honesta como el mock. Si a la prueba de un componente se le entregan exactamente los datos que se espera que reciba, la prueba va a confirmar que el componente muestra exactamente esos datos, no dice nada sobre si esos datos siguen siendo ciertos para cuando un usuario real los ve. La brecha solo aparece cuando algo maneja la app real con un backend real que puede cambiar por debajo de la UI a mitad de sesión. Vale la pena recordarlo la próxima vez que reutilizar una prop ya cargada del padre parezca una ganancia gratis frente a volver a pedirla.
Lecturas relacionadas
Un dashboard que dejó de decir la hora
Un bloque $derived que leyó Date.now() exactamente una vez, una medianoche que significaba cosas distintas para el frontend y para el motor en Rust, y la oración gramaticalmente rota que lo demostró.
El botón de cancelar que no cancelaba
Escape parecía funcionar en cada prueba manual. Escribir la prueba de regresión obligó a ver el orden real de eventos, y el blur perdido que guardaba lo que debía descartarse.
Seis ítems pequeños de UI, y los dos casi-desastres escondidos adentro
Un bloque de CSS que grep decía que estaba muerto pero del que dependía una prueba, y una sola línea de localStorage que rompió treinta y nueve pruebas sin relación por culpa de una actualización de Node.