La carpeta que se quedó donde estaba
Greenhouse, mi gestor de proyectos creativos, tiene una regla incorporada en casi todas sus partes: la carpeta de un proyecto vive donde sea que digan su estado y su etapa. ¿Proyecto activo en la etapa Explore? Su carpeta está bajo 20-explore/. ¿Se archiva? La carpeta se mueve a 90-vault/. Cada pantalla que abre una carpeta o lista sus archivos simplemente calcula la ruta a partir de la fila de la base de datos. Nunca tiene que preguntarle al sistema de archivos “espera, ¿dónde puse esto en realidad?”.
Encontré el único lugar donde esa suposición era falsa durante una segunda revisión completa de código de la app (las hago periódicamente, tratando al código como si mereciera un par de ojos externo aunque no lo tenga). El error estaba en “adoptar e importar”, la función que permite apuntar Greenhouse a una carpeta que ya se tenía en el disco y llevarla al pipeline en vez de empezar de cero.
El atajo que tenía sentido en su momento
Cuando construí adoptar/importar, tomé una decisión deliberada: importar en el lugar. Escribir la fila de la base de datos, sembrar un archivo project.md, y dejar la carpeta exactamente donde ya la tenía el explorador de archivos del sistema operativo. Mover archivos de un lado a otro parecía más riesgo del que necesitaba la función para una primera versión, y el comentario del código lo decía explícitamente:
/// v1 is adopt-in-place - the folder is recorded and mirrored where it sits;
/// it is not physically moved into the stage directory.
Eso suena como un recorte de alcance razonable. No lo era. Cada otro cambio de estado en la app (promover una idea, avanzar una etapa, archivar un proyecto) mueve físicamente la carpeta como parte de la transición. Importar era la única entrada a “proyecto activo” que no lo hacía. Así que en el momento en que se importaba una carpeta, su ubicación real en el disco y la ubicación que calculaba cada resolutor de rutas a partir de la base de datos divergían, para siempre.
Lo que eso realmente rompía
Al rastrearlo hasta el final: el previsualizador de archivos integrado en la app fallaba en cada elemento importado, porque buscaba en la carpeta de etapa calculada y los archivos seguían en la raíz del vault. “Show in Finder” abría una carpeta que todavía no existía. La primera vez que se tocaba o avanzaba un proyecto importado, Greenhouse creaba obedientemente la carpeta de etapa que esperaba encontrar, escribía un project.md nuevo dentro, y dejaba la carpeta con los archivos reales varada en la raíz para siempre, una carpeta fantasma con un solo archivo al lado de la real, que tenía todo lo que importaba. Archivar el elemento cambiaba su estado a Vaulted en la base de datos mientras los archivos no se movían ni un centímetro.
Nada de esto aparecía en las pruebas existentes, porque las pruebas de importación se detenían en “la fila de la base de datos se ve bien y project.md se escribió.” Nada encadenaba la importación con las operaciones que vienen después, previsualizar un archivo, avanzar una etapa, archivar la cosa. El chequeo end-to-end que verificaba que la importación funcionara nunca iba más allá de ver aparecer el proyecto importado como una tarjeta.
La corrección ya estaba en el código
La buena noticia de un código con un invariante consistente es que corregir una violación de este significa copiar un patrón que ya existe en otros tres lugares. promote_idea, la función que convierte una idea capturada en un proyecto activo, hace exactamente lo que necesitaba hacer la importación: calcular el destino bajo el directorio de etapa, crear el padre si hace falta, renombrar la carpeta si todavía no está ahí, y después escribir el archivo espejo en la nueva ubicación.
let dest = stage_dir(root, stage).join(&candidate.name);
if candidate.path != dest {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::rename(&candidate.path, &dest)?;
}
Mover antes de escribir en la base de datos, no después. Si el renombrado falla, la carpeta sigue intacta donde sea que haya empezado; si hubiera insertado la fila de la base de datos primero y el renombrado hubiera fallado después, habría reproducido exactamente el error que estaba corrigiendo: una fila de base de datos apuntando a una ubicación a la que el archivo nunca llegó.
Un efecto secundario que me gustó: la corrección también resolvió en silencio un problema menor y relacionado. Volver a escanear en busca de carpetas importables solía necesitar una verificación en la base de datos para evitar ofrecer para siempre una carpeta ya importada, ya que adoptar en el lugar significaba que la carpeta se quedaba ahí en el disco pareciendo importable. Una vez que importar realmente mueve la carpeta, el escaneo bruto del sistema de archivos deja de verla directamente. La verificación de base de datos pasa de ser estructural a ser una línea de defensa en profundidad, gratis.
Escribir las pruebas que lo hubieran detectado
El resultado útil de una revisión como esta no es solo la corrección, es la lista de pruebas faltantes, porque cada síntoma se traduce directamente en una prueba. Terminé con cinco: importar y después verificar que la ruta de “show in Finder” realmente exista, importar y después confirmar que el previsualizador liste el archivo sembrado, importar y después avanzar y verificar que la carpeta siguió a la etapa (nada varado en la ubicación anterior), importar y después tocar y confirmar que no aparezca ninguna carpeta fantasma, e importar y después archivar y verificar que los archivos realmente hayan llegado al directorio del vault. Cada una de esas es una prueba que habría fallado con el código anterior y ahora pasa.
También llevé la corrección a través de un build real en ejecución, no solo la suite de pruebas de Rust. Greenhouse tiene una pequeña configuración de WebDriver justo para esto: lanza la app real de Tauri contra un vault descartable y hace clics a través de ella como lo haría una persona. Extendí el script de importación existente para dejar caer un archivo WAV real en la carpeta antes de importarla, después hacer clic para previsualizarlo y avanzarlo una etapa, verificando que el elemento de audio realmente resolviera y reprodujera a través de la URL del protocolo de assets, y que el archivo siguiera al lado de project.md después del movimiento de etapa. Ver un clic real abrir un reproductor de audio real que buscaba un archivo que se había movido físicamente entre dos directorios fue más convincente que cualquier aserción en una prueba unitaria.
Lo que me llevo de esto
La lección no es “probar más.” Es notar cuándo un sistema depende tanto de un invariante que nada lo verifica directamente, porque cada ruta de código existente resulta preservarlo por construcción. Esa es exactamente la condición bajo la cual una ruta de código nueva puede violarlo en silencio. Si cuatro funciones en un archivo reubican una carpeta como efecto secundario y se está por agregar una quinta que no lo hace, lo que se agrega es un caso especial que necesita su propia justificación, por más que parezca una función más chica. En este caso no la había, así que la corrección fue dejar de ser especial.
Lecturas relacionadas
Enseñarle a un agente a probar mi app de Tauri a clics
macOS no tiene un WebDriver oficial de Tauri, un plugin de la comunidad llena el hueco, y la unificación de features de Cargo casi termina compilando todo el servidor de pruebas dentro de los builds de release.
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.