isInsideWorkFolder miraba la relación en los dos sentidos, así que también rechazaba que las carpetas de trabajo colgaran de la de instalación. Ese es el layout NORMAL, no un error: constants.py del agente define DIR_ENTRADA = APP_DIR / "Entrada", así que una instalación correcta en /opt/cloudrestoreas reporta input_folder = /opt/cloudrestoreas/Entrada. Efecto: ninguna instalación ni actualización podía pasar, en Linux ni en Windows. Fallaba en resolveInstallPath —antes de abrir el run, así que no quedaba nada en la bitácora— con un mensaje que decía justo lo contrario de lo que ocurría: "la ruta está dentro de una carpeta de trabajo". Solo se libraban las instalaciones fuera de la ruta por omisión, por accidente del nombre. Se conserva el peligro que el guard existe para atajar: el binario dentro de Entrada/Procesados, donde el agente lo tomaría por un respaldo a procesar. La función no tenía ninguna prueba. Se agregan las dos direcciones, el caso exacto, la normalización de separador/caja/barra final y el prefijo que no es de carpeta (/srv/entradas vs /srv/entrada). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
263 lines
10 KiB
TypeScript
263 lines
10 KiB
TypeScript
/**
|
|
* Comparación de versiones y normalización de plataforma para CloudRestoreAS.
|
|
*
|
|
* Es la lógica que decide si a un servidor le falta actualizarse, así que los casos que se
|
|
* prueban son los que romperían eso en silencio: comparación lexicográfica (1.10 vs 1.9),
|
|
* versiones de distinta longitud, y valores no comparables.
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
compareVersions,
|
|
effectiveArch,
|
|
effectivePlatform,
|
|
installPlatformVerdict,
|
|
isCrasPlatform,
|
|
isInsideWorkFolder,
|
|
isNewer,
|
|
isValidVersion,
|
|
osToPlatform,
|
|
parseVersion,
|
|
platformLabel,
|
|
DEFAULT_ARCH
|
|
} from './cras-version';
|
|
|
|
describe('isValidVersion', () => {
|
|
it('acepta versiones de puntos y números', () => {
|
|
for (const v of ['1', '1.0', '1.1.0', '26.7.1.4']) {
|
|
expect(isValidVersion(v), v).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('rechaza lo que el PANEL no podría ordenar', () => {
|
|
for (const v of ['1.0.0-rc1', 'v1.0.0', '1.0.0a', '', ' ', 'latest', '1.2.3.4.5']) {
|
|
expect(isValidVersion(v), String(v)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('rechaza valores que no son cadena', () => {
|
|
expect(isValidVersion(null)).toBe(false);
|
|
expect(isValidVersion(undefined)).toBe(false);
|
|
expect(isValidVersion(110)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('parseVersion', () => {
|
|
it('descompone en enteros', () => {
|
|
expect(parseVersion('1.10.2')).toEqual([1, 10, 2]);
|
|
expect(parseVersion(' 2.0 ')).toEqual([2, 0]);
|
|
});
|
|
|
|
it('devuelve null si no es comparable', () => {
|
|
expect(parseVersion('1.0.0-rc1')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('compareVersions', () => {
|
|
it('compara numéricamente, no como texto', () => {
|
|
// El error clásico: como cadenas, "1.9.0" > "1.10.0".
|
|
expect(compareVersions('1.10.0', '1.9.0')).toBe(1);
|
|
expect(compareVersions('1.9.0', '1.10.0')).toBe(-1);
|
|
});
|
|
|
|
it('rellena con ceros las de distinta longitud', () => {
|
|
expect(compareVersions('1.2', '1.2.0')).toBe(0);
|
|
expect(compareVersions('1.2.1', '1.2')).toBe(1);
|
|
expect(compareVersions('2', '1.9.9.9')).toBe(1);
|
|
});
|
|
|
|
it('es simétrica', () => {
|
|
expect(compareVersions('1.1.0', '1.1.0')).toBe(0);
|
|
expect(compareVersions('26.7.1.4', '26.7.1.3')).toBe(1);
|
|
expect(compareVersions('26.7.1.3', '26.7.1.4')).toBe(-1);
|
|
});
|
|
|
|
it('devuelve null cuando alguna no es comparable', () => {
|
|
expect(compareVersions('1.0.0-rc1', '1.0.0')).toBeNull();
|
|
expect(compareVersions('1.0.0', 'latest')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('isNewer', () => {
|
|
it('detecta una versión más nueva', () => {
|
|
expect(isNewer('1.2.0', '1.1.0')).toBe(true);
|
|
expect(isNewer('1.10.0', '1.9.0')).toBe(true);
|
|
});
|
|
|
|
it('no ofrece downgrade ni reinstalación de la misma', () => {
|
|
expect(isNewer('1.1.0', '1.2.0')).toBe(false);
|
|
expect(isNewer('1.1.0', '1.1.0')).toBe(false);
|
|
});
|
|
|
|
it('sin versión instalada, hay algo que instalar', () => {
|
|
expect(isNewer('1.1.0', null)).toBe(true);
|
|
expect(isNewer('1.1.0', undefined)).toBe(true);
|
|
expect(isNewer('1.1.0', ' ')).toBe(true);
|
|
});
|
|
|
|
it('ante una versión no comparable prefiere NO ofrecer nada', () => {
|
|
// Preferible perderse una actualización que empujar un downgrade por un dato sucio.
|
|
expect(isNewer('1.0.0-rc1', '1.0.0')).toBe(false);
|
|
expect(isNewer('1.2.0', 'desconocida')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('osToPlatform', () => {
|
|
it('reconoce Windows en texto libre', () => {
|
|
for (const os of ['Windows Server 2019', 'windows 11', 'WIN SERVER 2022', 'Win']) {
|
|
expect(osToPlatform(os), os).toBe('windows');
|
|
}
|
|
});
|
|
|
|
it('reconoce distribuciones de Linux', () => {
|
|
for (const os of [
|
|
'Ubuntu 22.04',
|
|
'Debian 12',
|
|
'CentOS 7',
|
|
'RHEL 9',
|
|
'Red Hat Enterprise',
|
|
'Rocky Linux 9',
|
|
'AlmaLinux',
|
|
'SUSE',
|
|
'Oracle Linux 8'
|
|
]) {
|
|
expect(osToPlatform(os), os).toBe('linux');
|
|
}
|
|
});
|
|
|
|
it('devuelve null si no puede decidir, en lugar de adivinar', () => {
|
|
// Adivinar mandaría el binario equivocado; la UI pide elegir a mano.
|
|
for (const os of ['', ' ', null, undefined, 'servidor de la sucursal', 'macOS 14']) {
|
|
expect(osToPlatform(os as string), String(os)).toBeNull();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('effectivePlatform', () => {
|
|
it('lo reportado por el agente manda sobre el texto libre', () => {
|
|
expect(effectivePlatform('linux', 'Windows Server 2019')).toBe('linux');
|
|
expect(effectivePlatform('windows', 'Ubuntu 22.04')).toBe('windows');
|
|
});
|
|
|
|
it('cae al texto libre cuando el agente no reportó', () => {
|
|
expect(effectivePlatform(null, 'Ubuntu 22.04')).toBe('linux');
|
|
expect(effectivePlatform(' ', 'Windows Server 2022')).toBe('windows');
|
|
});
|
|
|
|
it('ignora una plataforma reportada inválida', () => {
|
|
expect(effectivePlatform('solaris', 'Ubuntu 22.04')).toBe('linux');
|
|
expect(effectivePlatform('solaris', null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('effectiveArch', () => {
|
|
it('usa la reportada y normaliza a minúsculas', () => {
|
|
expect(effectiveArch('X86_64')).toBe('x86_64');
|
|
expect(effectiveArch('arm64')).toBe('arm64');
|
|
});
|
|
|
|
it('asume la única arquitectura que se publica hoy si falta', () => {
|
|
expect(effectiveArch(null)).toBe(DEFAULT_ARCH);
|
|
expect(effectiveArch(' ')).toBe(DEFAULT_ARCH);
|
|
});
|
|
});
|
|
|
|
describe('isCrasPlatform y platformLabel', () => {
|
|
it('valida el vocabulario del CHECK de cras_releases', () => {
|
|
expect(isCrasPlatform('windows')).toBe(true);
|
|
expect(isCrasPlatform('linux')).toBe(true);
|
|
expect(isCrasPlatform('solaris')).toBe(false);
|
|
expect(isCrasPlatform(null)).toBe(false);
|
|
});
|
|
|
|
it('etiqueta legible, incluso sin dato', () => {
|
|
expect(platformLabel('windows')).toBe('Windows');
|
|
expect(platformLabel('linux')).toBe('Linux');
|
|
expect(platformLabel(null)).toBe('Sin determinar');
|
|
});
|
|
});
|
|
|
|
describe('installPlatformVerdict', () => {
|
|
it('con plataforma conocida solo deja pasar el artefacto que corresponde', () => {
|
|
expect(installPlatformVerdict('linux', 'linux', null)).toBe('ok');
|
|
expect(installPlatformVerdict('windows', 'linux', null)).toBe('mismatch');
|
|
expect(installPlatformVerdict('linux', 'windows', null)).toBe('mismatch');
|
|
});
|
|
|
|
it('una confirmación no puede pasar por encima de una plataforma conocida', () => {
|
|
// El destino es Windows y el operador confirma "linux": sigue siendo un desajuste.
|
|
expect(installPlatformVerdict('windows', 'linux', 'linux')).toBe('mismatch');
|
|
});
|
|
|
|
it('sin plataforma determinada exige confirmación explícita del operador', () => {
|
|
// El defecto que se corrige: el modal preseleccionaba el primer artefacto del catálogo
|
|
// (el más recién descubierto, hoy Linux) para un servidor "Sin determinar".
|
|
expect(installPlatformVerdict(null, 'linux', null)).toBe('needs-ack');
|
|
expect(installPlatformVerdict(null, 'linux', '')).toBe('needs-ack');
|
|
expect(installPlatformVerdict(undefined, 'windows', undefined)).toBe('needs-ack');
|
|
});
|
|
|
|
it('la confirmación tiene que coincidir con la plataforma del artefacto', () => {
|
|
expect(installPlatformVerdict(null, 'linux', 'linux')).toBe('ok');
|
|
expect(installPlatformVerdict(null, 'linux', 'windows')).toBe('needs-ack');
|
|
expect(installPlatformVerdict(null, 'windows', ' WINDOWS ')).toBe('ok');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Guard de la ruta de instalación contra las carpetas de trabajo del agente.
|
|
*
|
|
* No tenía ni una prueba, y con la relación mirada en los dos sentidos rechazaba el layout
|
|
* NORMAL —`DIR_ENTRADA = APP_DIR / "Entrada"` en constants.py del agente— así que ninguna
|
|
* instalación ni actualización podía pasar. Se salvaban solo las instalaciones fuera de la ruta
|
|
* por omisión, por accidente del nombre.
|
|
*/
|
|
describe('isInsideWorkFolder', () => {
|
|
it('acepta el layout normal: las carpetas de trabajo cuelgan de la de instalación', () => {
|
|
expect(
|
|
isInsideWorkFolder('/opt/cloudrestoreas', [
|
|
'/opt/cloudrestoreas/Entrada',
|
|
'/opt/cloudrestoreas/Procesados'
|
|
])
|
|
).toBe(false);
|
|
expect(
|
|
isInsideWorkFolder('C:\\Aduanasoft\\CloudRestoreAS', [
|
|
'C:\\Aduanasoft\\CloudRestoreAS\\Entrada'
|
|
])
|
|
).toBe(false);
|
|
});
|
|
|
|
it('rechaza el binario DENTRO de una carpeta de trabajo, que es el peligro real', () => {
|
|
// Ahí el agente tomaría su propio ejecutable por un respaldo a procesar.
|
|
expect(isInsideWorkFolder('/srv/entrada/cras', ['/srv/entrada'])).toBe(true);
|
|
expect(
|
|
isInsideWorkFolder('D:\\Respaldos\\Entrada\\app', ['D:\\Respaldos\\Entrada'])
|
|
).toBe(true);
|
|
});
|
|
|
|
it('rechaza instalar exactamente EN la carpeta de trabajo', () => {
|
|
expect(isInsideWorkFolder('/srv/entrada', ['/srv/entrada'])).toBe(true);
|
|
expect(isInsideWorkFolder('/srv/entrada/', ['/srv/entrada'])).toBe(true);
|
|
});
|
|
|
|
it('normaliza separadores, barra final y caja antes de comparar', () => {
|
|
expect(isInsideWorkFolder('C:/Respaldos/Entrada/app', ['c:\\respaldos\\entrada\\'])).toBe(
|
|
true
|
|
);
|
|
});
|
|
|
|
it('un prefijo que no es de carpeta no cuenta como estar dentro', () => {
|
|
// Es el caso que dejaba pasar a Principal: -win rompe el prefijo de carpeta.
|
|
expect(
|
|
isInsideWorkFolder('C:\\Aduanasoft\\CloudRestoreAS', [
|
|
'C:\\Aduanasoft\\CloudRestoreAS-win\\Entrada'
|
|
])
|
|
).toBe(false);
|
|
expect(isInsideWorkFolder('/srv/entradas', ['/srv/entrada'])).toBe(false);
|
|
});
|
|
|
|
it('ignora carpetas vacías o sin reportar en lugar de bloquear', () => {
|
|
expect(isInsideWorkFolder('/opt/cloudrestoreas', [null, undefined, '', ' '])).toBe(false);
|
|
expect(isInsideWorkFolder('', ['/srv/entrada'])).toBe(false);
|
|
});
|
|
});
|