diff --git a/src/lib/server/cras-install.test.ts b/src/lib/server/cras-install.test.ts index 35916fd..49e127d 100644 --- a/src/lib/server/cras-install.test.ts +++ b/src/lib/server/cras-install.test.ts @@ -515,6 +515,29 @@ describe('checkWindowsArtifactUpdateSupport', () => { expect(checkWindowsArtifactUpdateSupport('install', SI, '1.1.4', 'Principal')).toBe(''); }); + it('si el instalador NO ESTÁ, aborta: no hay nada que ejecutar', () => { + // Es distinto de "no sé": aquí sí se sabe, y es lo peor posible. Antes se trataba como duda + // y el run continuaba, tocaba el servidor y acababa culpando al agente. + const falta = { answer: 'sin-archivo', detail: 'no existe C:\\tmp\\install.ps1' } as const; + try { + checkWindowsArtifactUpdateSupport('update', falta, '1.1.4', 'Principal'); + throw new Error('se esperaba que lanzara'); + } catch (e) { + const err = e as InstallError; + expect(err).toBeInstanceOf(InstallError); + expect(err.status).toBe(502); + expect(err.message).toContain('C:\\tmp\\install.ps1'); + expect(err.message).toMatch(/contenido-staging/); + } + }); + + it('y también aborta al instalar limpio: sin instalador no se instala nada', () => { + const falta = { answer: 'sin-archivo', detail: 'no existe X' } as const; + expect(() => + checkWindowsArtifactUpdateSupport('install', falta, '1.1.4', 'Principal') + ).toThrow(InstallError); + }); + it('NO SABER no bloquea: es la prueba que faltaba y la que dejó 1.1.4 sin poder instalarse', () => { // La sonda anterior colapsaba cualquier fallo en "no lo soporta", y sobre esa lectura se // decidía un 409. Con un artefacto correcto, eso era un candado permanente. @@ -582,11 +605,14 @@ describe('probeInstallerUpdateSupport', () => { expect((await probeInstallerUpdateSupport(sftp as never, RUTA)).answer).toBe('desconocido'); }); - it('el archivo que no existe se distingue de un instalador antiguo', async () => { - const { sftp } = fakeSftp(() => ({ code: 0, stdout: 'desconocido|no existe el archivo' })); + it('el archivo que no existe NO es "desconocido": es un hecho, y el peor', async () => { + // Que no haya instalador es definitivo — no hay nada que ejecutar. Colapsarlo en + // "desconocido" hacía que el run siguiera adelante sabiéndolo ya, gastara el intento entero + // y acabara culpando al agente de no escribir su sello. + const { sftp } = fakeSftp(() => ({ code: 0, stdout: 'sin-archivo' })); const r = await probeInstallerUpdateSupport(sftp as never, RUTA); - expect(r.answer).toBe('desconocido'); - expect(r.detail).toBe('no existe el archivo'); + expect(r.answer).toBe('sin-archivo'); + expect(r.detail).toContain(RUTA); }); it('una salida inesperada no se interpreta como respuesta', async () => { diff --git a/src/lib/server/cras-install.ts b/src/lib/server/cras-install.ts index 04a8b99..a462204 100644 --- a/src/lib/server/cras-install.ts +++ b/src/lib/server/cras-install.ts @@ -1295,7 +1295,13 @@ export async function probeWindowsTask(sftp: SftpClient): Promise l.trim()).filter(Boolean).join(', ') || + `no se pudo listar ${remoteDir} (código ${listado.code})` + ); + // Actualizar va por -UpdateInPlace, sea cual sea el arranque configurado: no vuelve a // registrar la tarea (así no pisa ajustes que el operador haya hecho sobre ella), se salta // el bootstrap —una segunda instancia purgaría el Temp\ de la que está viva— y trae respaldo @@ -1781,14 +1829,42 @@ async function installWindows( ? ' -Service' : ' -Desktop'; await appendInstallStep(runId, 'ejecutar-instalador', true, `install.ps1${flag}`); + // El script emite un CENTINELA y el panel lo exige. + // + // Antes se invocaba con `& '' …; exit $LASTEXITCODE`, y eso no distingue "corrió y + // salió 0" de "no llegó a correr": si la invocación falla —porque el archivo no está, por + // ejemplo— `$LASTEXITCODE` no se fija nunca (no corrió ningún comando nativo), así que + // `exit $null` da 0. Comprobado con PowerShell real. El panel leía 0, asentaba + // "Ejecutar instalador ✓", y el servidor seguía intacto con su agente viejo — durante + // varios runs, con esa evidencia falsa mandando el diagnóstico a otra parte. + // + // Con el centinela, la ausencia de CRAS-FIN significa "no terminó", dé lo que dé el código. const install = await execRemote( sftp, psEncoded( - `& '${remoteDir}\\CloudRestoreAS\\install.ps1'${flag} ` + - `-Prefix '${targetPath}' -PanelEnvFile '${remoteEnv}'; exit $LASTEXITCODE` + "$ErrorActionPreference = 'Stop'; " + + 'try { ' + + `& '${remoteDir}\\CloudRestoreAS\\install.ps1'${flag} ` + + `-Prefix '${targetPath}' -PanelEnvFile '${remoteEnv}'; ` + + '$c = 0; if ($null -ne $LASTEXITCODE) { $c = $LASTEXITCODE } } ' + + "catch { Write-Output ('CRAS-ERROR|' + $_.Exception.Message); exit 90 }; " + + "Write-Output ('CRAS-FIN|' + $c); exit $c" ), INSTALL_EXEC_TIMEOUT_MS ); + + const salidaInstalador = `${install.stdout}\n${install.stderr}`; + if (!salidaInstalador.includes('CRAS-FIN|')) { + const detalle = salidaInstalador.includes('CRAS-ERROR|') + ? salidaInstalador.split('CRAS-ERROR|')[1].split('\n')[0].trim() + : truncate(install.stderr || install.stdout) || 'sin salida'; + throw new InstallError( + 502, + `install.ps1 no llegó a terminar en ${target.name}: ${detalle}. El servidor quedó ` + + 'como estaba. Revisa el paso "contenido-staging" de este run para ver qué dejó ' + + 'la extracción del artefacto.' + ); + } // 75 = EX_TEMPFAIL, igual que en install.sh: hay una restauración en curso. Es un // conflicto de estado, no un fallo; con el 502 genérico el operador salía a buscar una // avería que no existe.