2.6.0: auditoría, 100 % local y 26 funciones verificadas

Sale a la red solo lo que se pide: la tipografía viene con el programa
(static/manrope.woff2) y el comprobador de versiones tiene interruptor en
Ajustes. Nueva prueba funcional (prueba_funcional.py) que levanta una copia
sobre un cliente de ensayo y ejercita las 26 funciones por su API, con una
conversión real: 26 de 26.

Fallos que sacó y quedan arreglados:
- Un renombrado solo de mayúsculas («10- octubre» → «10- Octubre») se
  descartaba en Plan.agregar y en mover; deshacer decía «sitio ocupado».
- Las carpetas que crea el plan no entraban en el inventario hasta el
  siguiente escaneo (_recontar_carpetas las da de alta y recuenta).
- Una fusión de carpetas cortaba en el primer archivo que tropezaba.
- «Viejo» era distinto en convertidor, stock y respaldo: ahora es_viejo.
- /api/actualizacion informaba versión vacía hasta la primera consulta.
- Filas de sueltos por carpeta de origen; mes del stock el más repetido;
  fuera tres rutas sin uso.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-09-24 20:57:41 -05:00
padre 9d4d1e9cc2
commit 1151320bf2
Se han modificado 7 ficheros con 645 adiciones y 32 borrados

Ver fichero

@@ -221,6 +221,51 @@ class Servidor(unittest.TestCase):
c, res = self.post("/api/deshacer", {"lote": res["lote"]})
self.assertEqual(c, 200)
self.assertEqual(antes, self._arbol())
# 4a. un mes escrito en minúsculas se normaliza de verdad (renombrado solo de mayúsculas), la
# carpeta nueva entra en el inventario sin volver a escanear, y deshacer lo devuelve
os.makedirs(os.path.join(self.raiz, "2026", "10- octubre"), exist_ok=True)
with open(os.path.join(self.raiz, "2026", "10- octubre", "Final.mp4"), "wb") as fh:
fh.write(os.urandom(4096))
self.post("/api/escanear", {"raiz": self.raiz})
self.esperar_escaneo()
pl = self.get("/api/plan" + q)
ren = [f for f in pl["filas"] if f["accion"] == "renombrar" and f["destino"].endswith("10- Octubre")]
self.assertTrue(ren, "el plan no propone normalizar «10- octubre»")
c, res = self.post("/api/aplicar", {"raiz": self.raiz, "ids": [f["id"] for f in ren]})
self.assertEqual(c, 200)
self.assertEqual(res["errores"], [])
self.assertIn("10- Octubre", os.listdir(os.path.join(self.raiz, "2026")))
ex = self.get("/api/estructura/existen?raiz=" + urllib.parse.quote(self.raiz)
+ "&ruta=" + urllib.parse.quote(json.dumps(["{anio}", "{mes}"])))
self.assertTrue(any(x.endswith("10- Octubre") for x in ex["carpetas"]), "la carpeta renombrada no está en el inventario")
c, _ = self.post("/api/deshacer", {"lote": res["lote"]})
self.assertIn("10- octubre", os.listdir(os.path.join(self.raiz, "2026")))
shutil.rmtree(os.path.join(self.raiz, "2026"), ignore_errors=True)
# 4a-bis. dos copias idénticas se ven como repetidas y una se aparta; dos con el mismo nombre y
# peso pero distinto contenido NO se tocan
os.makedirs(os.path.join(self.raiz, "Dup A"), exist_ok=True)
os.makedirs(os.path.join(self.raiz, "Dup B"), exist_ok=True)
cuerpo = os.urandom(4096) * 1500 # 6 MB: por encima del umbral
for d in ("Dup A", "Dup B"):
with open(os.path.join(self.raiz, d, "igual.mp4"), "wb") as fh:
fh.write(cuerpo)
with open(os.path.join(self.raiz, "Dup A", "parecido.mp4"), "wb") as fh:
fh.write(os.urandom(4096) * 1500)
with open(os.path.join(self.raiz, "Dup B", "parecido.mp4"), "wb") as fh:
fh.write(os.urandom(4096) * 1500)
self.post("/api/escanear", {"raiz": self.raiz})
self.esperar_escaneo()
dup = self.get("/api/duplicados" + q)
self.assertEqual(len(dup["grupos"]), 2)
c, ap = self.post("/api/duplicados/apartar", {"raiz": self.raiz})
self.assertEqual(ap["movimientos"], 1) # solo la idéntica
self.assertEqual(ap["distintos"], 1) # la parecida se deja, y se avisa
shutil.rmtree(os.path.join(self.raiz, "Dup A"), ignore_errors=True)
shutil.rmtree(os.path.join(self.raiz, "Dup B"), ignore_errors=True)
self.post("/api/escanear", {"raiz": self.raiz})
self.esperar_escaneo()
# 4b. el mapa dice dónde va a acabar cada cosa, con las plantillas de verdad
est = self.get("/api/estructura" + q)
proy = next(c for c in est["cubetas"] if c["clave"] == "PROYECTOS")