La configuración que quedó grabada en cada bóveda, para siempre
Greenhouse tiene una metáfora de plantas que lo atraviesa todo. Las ideas son “Semillas,” pasan a “Germinando,” un proyecto avanza por “Brotando,” “Creciendo,” “Floreciendo,” hasta llegar a “Fructificando.” Cada una de esas palabras vivía en un struct de Rust y se serializaba directamente en el config.yaml de cada bóveda en el momento en que se creaba la bóveda.
Eso suena inofensivo hasta pensar en qué implica para cualquier cosa que no sea la plantilla de plantas actual en el idioma actual. Quiero ofrecer en algún momento traducciones al español y al portugués y, por separado, he estado dándole vueltas a la idea de una plantilla que reemplace las palabras de plantas por los nombres de etapa exactos que ya usa un profesor de proceso creativo en particular con sus estudiantes. Ninguno de los dos es un cambio de interfaz. Los dos son una migración de datos por bóveda, porque cada bóveda que se había creado hasta entonces ya tenía escrito “Brotando” en el disco en inglés, de forma permanente, y nada leía una tabla de referencia en su lugar.
La solución fue dejar de tratar esas palabras como datos. Los nombres de etapa y los términos decorativos de plantas ahora se derivan en el momento de la lectura a partir del id de una etapa, a través de una sola función, cada vez. Están marcados #[serde(skip)] en el struct de Rust, así que no se escriben en config.yaml y tampoco se vuelven a leer desde ahí, aunque una bóveda antigua ya los tenga guardados de antes de esta solución. Decidí que, de ahora en adelante, esos valores viejos y obsoletos simplemente se ignoran. Sin script de migración, sin reescribir archivos existentes. Un futuro cambio de plantilla o de idioma ahora es una edición de una sola función, no un problema por cada bóveda.
Mientras estaba en eso, encontré una segunda versión, más pequeña, del mismo tipo de error. Las tarjetas del panel adivinaban el nombre visible de una etapa poniendo en mayúscula su id crudo (humanizeStage), sin pasar en absoluto por la configuración real, mientras que la vista de detalle del proyecto usaba el nombre realmente configurado. Los dos coincidían hoy solo porque los ids de la plantilla por defecto, al ponerse en mayúscula, dan casualmente las mismas palabras que sus nombres. Una plantilla personalizada habría mostrado “S1” en una pantalla y algo legible en la otra. Misma causa raíz, nombres de etapa tratados como cadenas improvisadas en lugar de una única fuente real de verdad, apareciendo simplemente en dos capas distintas de la aplicación en vez de una.
La parte que más me alegra haber hecho antes de dar esto por terminado: mi primer intento de cobertura de pruebas pasó completo, y nada de eso habría detectado un cable cortado. Cada mock que había escrito le daba a la aplicación una lista de etapas vacía o nombres de etapa que resultaban idénticos a lo que ya producía el viejo mecanismo de respaldo defectuoso. Una segunda revisión del diff señaló que el error que estaba corrigiendo es, por definición, visible solo cuando el nombre real de la configuración difiere del adivinado, y ninguna de mis pruebas ejercitaba ese caso. Agregué pruebas que usan deliberadamente un nombre de etapa que diverge del id, y luego les hice pruebas de mutación quitando temporalmente la conexión real y confirmando que efectivamente fallaban. Así fue. Pruebas verdes que no pueden fallar no son pruebas, son decoración.
Lecturas relacionadas
Los errores nunca fueron el problema. Las cadenas de texto sí.
Cada error que Greenhouse le mostraba a los usuarios nació como una cadena de texto en inglés formateada a mano en Rust. El arreglo: enviar la forma del problema por el cable, no una oración sobre él.
Dos notas que nadie vio jamás
Greenhouse guardaba fielmente una nota de captura y una nota de traspaso, y no mostraba ninguna de las dos: una no tenía campo en el tipo de wire, la otra tenía un comando funcional que nadie llamaba.
El escaneo que se ofreció a importarse a sí mismo
Un escaneo de importación que encontró la propia estructura interna de la app, y luego volvió a ofrecer una carpeta que ya había importado - dos versiones de la misma conversación faltante entre capas.