test(install): cubrir la ruta personalizada C:\Aduanasoft\CloudRestoreAS-win

Esa ruta es el peor caso posible y existe en produccion: la ruta por omision
C:\Aduanasoft\CloudRestoreAS es PREFIJO DE CADENA de ella, asi que cualquier
comparacion hecha con startsWith daria por iguales dos instalaciones distintas
— y el resultado seria el fallo silencioso otra vez, actualizar una carpeta y
arrancar la otra.

El flujo ya la manejaba bien (Test-SamePath compara por igualdad exacta tras
normalizar), pero nada lo probaba: la emulacion usaba declarada/otra-carpeta,
nombres sin relacion entre si, que un startsWith mal puesto pasaria sin problema.

- Escenario `sufijo` en emular-actualizacion-windows.ps1: instalacion en
  ...\CloudRestoreAS-win y tarea apuntando a ...\CloudRestoreAS. Verificado en
  Windows: reapunta la tarea, el proceso queda corriendo desde -win y el sello en
  la version nueva.
- Nuevo scripts/probar-funciones-install.ps1: extrae las funciones del instalador
  por AST y las ejercita contra una tarea simulada, sin elevacion. Cubre los casos
  limite de la comparacion de rutas (el par de prefijo en ambos sentidos, comillas,
  barra final, mayusculas, `..`, ruta vacia) y que Sync-AgentTaskPath falle cuando
  no puede corregir.

BUILD.md documenta por que se compara por igualdad exacta y no por prefijo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 11:09:47 -06:00
parent 399b0483d5
commit 9255c6277e
4 changed files with 143 additions and 7 deletions

View File

@@ -235,16 +235,27 @@ sabiendas el binario anterior es peor que abortar. Y la confirmación de arranqu
del proceso, no solo su nombre — un agente viejo que nunca se detuvo satisface igual de bien un
`Get-Process -Name CloudRestoreAS`.
### Rutas de instalación personalizadas
`C:\Aduanasoft\CloudRestoreAS-win` es el caso a tener presente, y existe en producción: la ruta por
omisión `C:\Aduanasoft\CloudRestoreAS` es **prefijo de cadena** de ella. Por eso las rutas se
comparan por **igualdad exacta tras normalizar** (comillas, barra final, mayúsculas) y nunca con
`startsWith` — que daría por iguales dos instalaciones distintas. En el panel eso vive en un solo
sitio, `sameWindowsPath()`; en el instalador, en `Test-SamePath`.
Para reproducirlo y comprobarlo sin un servidor, desde WSL o Windows:
```powershell
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 # caso roto
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 -Escenario alineada # caso normal
scripts\emular-actualizacion-windows.ps1 -Installer .\install.ps1 -Escenario sufijo # ...-win
scripts\probar-funciones-install.ps1 # casos límite
```
Monta un agente falso (un `.exe` real que se queda vivo), una instalación en una carpeta y una
tarea apuntando a otra, corre el instalador y dice si la actualización surtió efecto. No necesita
elevación ni toca la instalación real de la máquina.
La emulación monta un agente falso (un `.exe` real que se queda vivo), una instalación en una
carpeta y una tarea apuntando a otra, corre el instalador y dice si la actualización surtió efecto.
`probar-funciones-install.ps1` extrae las funciones del instalador por AST y las ejercita contra una
tarea simulada. Ninguno de los dos necesita elevación ni toca la instalación real de la máquina.
`-UpdateInPlace` además no vuelve a registrar la tarea (así no pisa ajustes hechos sobre ella) y
se salta el bootstrap: una segunda instancia purgaría el `Temp\` de la que está viva. Es el modo

View File

@@ -4,5 +4,5 @@
# package-release.sh (nombres de artefacto y release.json) y el reporte al PANEL.
# Formato obligatorio: puntos y números, monotónico creciente — el PANEL compara
# versiones como tuplas de enteros para detectar si hay una más nueva.
__version__ = "1.1.2"
__version__ = "1.1.3"
__author__ = "Aduanasoft"

View File

@@ -31,15 +31,29 @@ param(
[string]$Etiqueta = 'instalador',
# 'desalineada' reproduce el servidor que falla; 'alineada' es el caso normal, para comprobar
# que el arreglo no lo rompe (una tarea que ya apunta bien no debe tocarse).
[ValidateSet('desalineada', 'alineada')][string]$Escenario = 'desalineada',
#
# 'sufijo' es el caso PELIGROSO y existe en producción: la instalación está en
# ...\CloudRestoreAS-win y la tarea apunta a ...\CloudRestoreAS, que es prefijo de cadena de la
# anterior. Cualquier comparación de rutas hecha con `startsWith` las daría por iguales, no
# corregiría la tarea, y la actualización volvería a no surtir efecto.
[ValidateSet('desalineada', 'alineada', 'sufijo')][string]$Escenario = 'desalineada',
[string]$Carpeta = 'C:\Users\Public\cras-e2e'
)
$ErrorActionPreference = 'Stop'
$base = $Carpeta
$pkg = Join-Path $base 'pkg'
# En el escenario 'sufijo' los nombres NO son arbitrarios: reproducen el par que se da en producción,
# donde el nombre de una carpeta es prefijo de cadena del de la otra. Con nombres sin relación entre
# sí (declarada / otra-carpeta) un `startsWith` mal puesto pasaría la prueba sin problema.
if ($Escenario -eq 'sufijo') {
$declarada = Join-Path $base 'CloudRestoreAS-win' # la instalación real
$otra = Join-Path $base 'CloudRestoreAS' # a donde apunta la tarea
} else {
$declarada = Join-Path $base 'declarada' # donde el panel cree que está (y está)
$otra = Join-Path $base 'otra-carpeta' # a donde apunta la tarea
$pkg = Join-Path $base 'pkg'
}
function Nuevo-AgenteFalso {
param([string]$Destino, [string]$Version, [switch]$EscribeSello)

View File

@@ -0,0 +1,111 @@
<#
Pruebas de las funciones de install.ps1, sin instalar nada.
Las funciones se extraen del script por AST y se ejercitan contra una tarea programada simulada.
Así se prueba el COMPORTAMIENTO —no solo que el archivo parsee— sin necesitar elevación ni
registrar una tarea 'CloudRestoreAS' de verdad en la máquina.
Complementa a emular-actualizacion-windows.ps1: ese cubre el flujo completo, este cubre los casos
límite de la comparación de rutas, que es donde se esconden los fallos silenciosos.
Uso: .\probar-funciones-install.ps1
#>
param([string]$Installer = (Join-Path (Split-Path -Parent $PSScriptRoot) 'install.ps1'))
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$tokens = $null; $errores = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path $Installer).Path, [ref]$tokens, [ref]$errores)
if ($errores -and $errores.Count -gt 0) {
Write-Host "install.ps1 no parsea: $($errores.Count) error(es)" -ForegroundColor Red
$errores | ForEach-Object { Write-Host (" L" + $_.Extent.StartLineNumber + ": " + $_.Message) }
exit 1
}
# Variables de ámbito de script que usan las funciones.
$Prefix = 'C:\Aduanasoft\CloudRestoreAS-win'
$dest = Join-Path $Prefix 'CloudRestoreAS.exe'
$TaskName = 'CloudRestoreAS'
$ProcName = 'CloudRestoreAS'
$queremos = @('Write-Step', 'Write-Ok', 'Write-Warn', 'Get-NormalizedPath', 'Test-SamePath',
'Get-AgentTaskExecute', 'Repair-AgentTaskPath', 'Sync-AgentTaskPath')
foreach ($f in $ast.FindAll({ param($n)
$n -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) {
if ($queremos -contains $f.Name) { . ([scriptblock]::Create($f.Extent.Text)) }
}
$fallos = 0
function Assert($cond, $msg) {
if ($cond) { Write-Host " OK $msg" } else { Write-Host " FALLA $msg" -ForegroundColor Red; $script:fallos++ }
}
Write-Host '=== Test-SamePath: rutas donde una es prefijo de cadena de la otra ==='
# El caso que existe en produccion. Un `-like "$a*"` o un StartsWith aqui daria por iguales dos
# instalaciones distintas, no se corregiria la tarea, y la actualizacion no surtiria efecto.
$porOmision = 'C:\Aduanasoft\CloudRestoreAS'
$personalizada = 'C:\Aduanasoft\CloudRestoreAS-win'
Assert (-not (Test-SamePath $porOmision $personalizada)) 'no confunde la ruta por omision con la -win'
Assert (-not (Test-SamePath $personalizada $porOmision)) 'ni al reves'
Assert (-not (Test-SamePath "$porOmision\CloudRestoreAS.exe" "$personalizada\CloudRestoreAS.exe")) `
'tampoco con el ejecutable completo'
Assert (Test-SamePath $personalizada $personalizada) 'y la personalizada sigue siendo igual a si misma'
Write-Host '=== Test-SamePath: normalizacion ==='
Assert (Test-SamePath "`"$personalizada`"" $personalizada) 'tolera comillas (la tarea las guarda asi)'
Assert (Test-SamePath $personalizada.ToUpper() $personalizada) 'NTFS no distingue mayusculas'
Assert (Test-SamePath "$personalizada\" $personalizada) 'ignora la barra final'
Assert (Test-SamePath "$Prefix\..\CloudRestoreAS-win\x.exe" "$Prefix\x.exe") 'normaliza .. en la ruta'
Assert (-not (Test-SamePath '' $personalizada)) 'una ruta vacia no es igual a nada'
Write-Host '=== Get-AgentTaskExecute ==='
function Get-AgentTask { [pscustomobject]@{ Actions = @(
[pscustomobject]@{ Execute = "`"$porOmision\CloudRestoreAS.exe`""; Arguments = '--start-engine --headless' }) } }
Assert ((Get-AgentTaskExecute) -eq "$porOmision\CloudRestoreAS.exe") 'devuelve la ruta sin comillas'
# Una accion ComHandler no tiene .Execute: con StrictMode, tocarla a ciegas seria un error.
function Get-AgentTask { [pscustomobject]@{ Actions = @(
[pscustomobject]@{ ClassId = '{guid}' },
[pscustomobject]@{ Execute = "$porOmision\CloudRestoreAS.exe"; Arguments = '' }) } }
Assert ((Get-AgentTaskExecute) -eq "$porOmision\CloudRestoreAS.exe") 'se salta acciones sin Execute'
function Get-AgentTask { $null }
Assert ((Get-AgentTaskExecute) -eq '') 'sin tarea devuelve cadena vacia'
Write-Host '=== Sync-AgentTaskPath: el fallo silencioso ==='
$script:reapuntadoA = $null
$script:argsPreservados = $null
function Set-ScheduledTask { param($TaskName, $Action)
$ejec = @($Action | Where-Object { $_.PSObject.Properties['Execute'] -and $_.Execute })
$script:reapuntadoA = $ejec[0].Execute
$script:argsPreservados = $ejec[0].Arguments
return $true }
function New-ScheduledTaskAction { param($Execute, $Argument, $WorkingDirectory)
[pscustomobject]@{ Execute = $Execute; Arguments = $Argument } }
function Get-AgentTask { [pscustomobject]@{ Actions = @(
[pscustomobject]@{ Execute = "$porOmision\CloudRestoreAS.exe"; Arguments = '--start-engine --headless' }) } }
Sync-AgentTaskPath
Assert ($script:reapuntadoA -eq $dest) "reapunta de la carpeta por omision a $dest"
Assert ($script:argsPreservados -eq '--start-engine --headless') 'conserva los argumentos originales'
$script:reapuntadoA = $null
function Get-AgentTask { [pscustomobject]@{ Actions = @(
[pscustomobject]@{ Execute = $dest; Arguments = '--start-engine --headless' }) } }
Sync-AgentTaskPath
Assert ($null -eq $script:reapuntadoA) 'una tarea ya alineada no se toca'
# Si no se puede corregir tiene que FALLAR, no arrancar el binario viejo en silencio.
function Set-ScheduledTask { param($TaskName, $Action) throw 'Acceso denegado' }
function Get-AgentTask { [pscustomobject]@{ Actions = @(
[pscustomobject]@{ Execute = "$porOmision\CloudRestoreAS.exe"; Arguments = '' }) } }
$lanzo = $false; $msg = ''
try { Sync-AgentTaskPath } catch { $lanzo = $true; $msg = $_.Exception.Message }
Assert $lanzo 'sin permiso para corregir -> lanza en vez de seguir'
Assert ($lanzo -and $msg -match 'sin efecto') 'el mensaje explica que la actualizacion no surtiria efecto'
Write-Host ''
if ($fallos -gt 0) { Write-Host "FALLOS: $fallos" -ForegroundColor Red; exit 1 }
Write-Host 'TODO OK' -ForegroundColor Green