diff --git a/scripts/auto-update.ps1 b/scripts/auto-update.ps1 new file mode 100644 index 0000000..6fef663 --- /dev/null +++ b/scripts/auto-update.ps1 @@ -0,0 +1,49 @@ +#requires -Version 5.1 +<# +.SYNOPSIS +Aktualisiert Scoop und alle benutzerspezifischen Pakete; elexis* bleibt gesperrt. +.DESCRIPTION +Fuehrt ausschliesslich den Update-Lauf aus. Die Aufgabe wird von update-bucket.ps1 +eingerichtet und startet dieses Skript verborgen. Der letzte Lauf steht in +auto-update.log neben diesem Skript. Globale Pakete werden nicht aktualisiert. +#> +[CmdletBinding()] +param( + [string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) +) + +$ErrorActionPreference = 'Stop' +$ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot) +$env:SCOOP = $ScoopRoot +$scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1' +if (-not (Test-Path -LiteralPath $scoopCommand -PathType Leaf)) { + throw "Scoop wurde nicht gefunden: $scoopCommand" +} + +$transcriptStarted = $false +$exitCode = 0 +try { + Start-Transcript -LiteralPath (Join-Path $PSScriptRoot 'auto-update.log') -Force | Out-Null + $transcriptStarted = $true + # Ein absoluter Scoop-Pfad und die shims im PATH funktionieren auch ohne Profil. + $env:PATH = (Join-Path $ScoopRoot 'shims') + [IO.Path]::PathSeparator + $env:PATH + $installed = @(& $scoopCommand list) + if ($LASTEXITCODE -ne 0) { throw 'Installierte Scoop-Pakete konnten nicht ermittelt werden.' } + foreach ($package in $installed) { + if ($package.Name -notlike 'elexis*' -or $package.Info -match 'Global install|Install failed') { continue } + & $scoopCommand hold $package.Name + if ($LASTEXITCODE -ne 0) { + throw "Updatesperre fuer '$($package.Name)' fehlgeschlagen. Kein Update ausgefuehrt." + } + } + & $scoopCommand update + if ($LASTEXITCODE -ne 0) { throw 'scoop update fehlgeschlagen.' } + & $scoopCommand update '*' + if ($LASTEXITCODE -ne 0) { throw 'scoop update * fehlgeschlagen.' } +} catch { + Write-Error $_ -ErrorAction Continue + $exitCode = 1 +} finally { + if ($transcriptStarted) { Stop-Transcript | Out-Null } +} +exit $exitCode diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 8d26040..8e639f1 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,4 +1,4 @@ -# 1. SCOOP CHECK & INSTALL +# 1. SCOOP CHECK & INSTALL $bucketList = scoop bucket list 2>$null | Out-String if ($bucketList) { diff --git a/scripts/setup-auto-update.ps1 b/scripts/setup-auto-update.ps1 new file mode 100644 index 0000000..3fd6961 --- /dev/null +++ b/scripts/setup-auto-update.ps1 @@ -0,0 +1,69 @@ +#requires -Version 5.1 +<# +.SYNOPSIS +Fragt nach und richtet optional die taeglichen Scoop-Auto-Updates ein. +.DESCRIPTION +Eigenstaendig ausfuehrbar, ohne Bucket-Migration. Bei y wird auto-update.ps1 +heruntergeladen und eine Aufgabe fuer taeglich 03:00 Uhr erstellt. +Die Aufgabe startet das Updateskript verborgen und holt verpasste Termine nach, +sobald die Benutzersitzung und eine Netzwerkverbindung verfuegbar sind. +Bei n wird nichts eingerichtet; bestehende Aufgaben bleiben unveraendert. +.EXAMPLE +.\scripts\setup-auto-update.ps1 +#> +[CmdletBinding()] +param( + [string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) +) + +$ErrorActionPreference = 'Stop' +$ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot) +$repository = 'https://gitea.medelexis.ch/public/scoop' + +do { + $autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant() +} while ($autoUpdateAnswer -notin @('y', 'n')) + +if ($autoUpdateAnswer -eq 'y') { + $scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1' + if (-not (Test-Path -LiteralPath $scoopCommand -PathType Leaf)) { + throw "Scoop wurde nicht gefunden: $scoopCommand" + } + $autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis' + New-Item -ItemType Directory -Path $autoUpdateDirectory -Force | Out-Null + $autoUpdateScript = Join-Path $autoUpdateDirectory 'auto-update.ps1' + $downloadPath = Join-Path $autoUpdateDirectory ('auto-update-{0}.download' -f [guid]::NewGuid()) + try { + $autoUpdateUrl = "$repository/raw/branch/master/scripts/auto-update.ps1" + Invoke-WebRequest -Uri $autoUpdateUrl -OutFile $downloadPath -UseBasicParsing + $parseTokens = $null + $parseErrors = $null + $downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors) + if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or + 'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) { + throw 'Das heruntergeladene Auto-Update-Skript ist kein gueltiges Updateskript.' + } + Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force + # Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet. + $userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name + $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value + $taskName = "Medelexis-Scoop-AutoUpdate-$userSid" + $powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + $arguments = '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -ScoopRoot "{1}"' -f $autoUpdateScript, $ScoopRoot + $action = New-ScheduledTaskAction -Execute $powershell -Argument $arguments + $trigger = New-ScheduledTaskTrigger -Daily -At '03:00' + $principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited + $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable ` + -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew ` + -ExecutionTimeLimit (New-TimeSpan -Hours 4) + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` + -Principal $principal -Settings $settings -Force ` + -Description 'Scoop taeglich um 03:00 aktualisieren; verpasste Termine nachholen; elexis* auf hold setzen.' | Out-Null + Write-Host "Scoop Auto-Update eingeschaltet: $taskName (taeglich 03:00, verpasste Termine nachholen)." + Write-Host "Ausfuehrung verborgen unter $userId, sobald der Benutzer angemeldet ist; kein Aufwecken des PCs." + } finally { + if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath } + } +} else { + Write-Host 'Auto-Update nicht eingerichtet. Eine eventuell bestehende Aufgabe bleibt unveraendert.' +} diff --git a/scripts/update-bucket.ps1 b/scripts/update-bucket.ps1 index f283b6e..fbd7216 100644 --- a/scripts/update-bucket.ps1 +++ b/scripts/update-bucket.ps1 @@ -3,11 +3,14 @@ .SYNOPSIS Stellt bestehende Scoop-Installationen von elexis/streichtech auf medelexis um. .DESCRIPTION -Ohne -Apply nur ein Probelauf. Keine Sicherungen, Neuinstallationen oder Git-Pushes. +Ohne -Apply nur ein Probelauf. Keine Sicherungen oder Git-Pushes. Bearbeitet alle Versionsverzeichnisse der benutzerspezifischen Scoop-Installation. Ein passender bestehender Bucket wird umbenannt. Danach werden elexis und streichtech entfernt. Andere Buckets wie main und extras bleiben bestehen. Alte Paketnamen mit Unterstrich werden auf passende Manifestnamen mit Bindestrich umgestellt. +ee-iso/ee_iso und ee-cli werden deinstalliert. MyElexis CLI wird neu installiert. +Mit -Apply wird abschliessend gefragt, ob taegliche Auto-Updates um 03:00 Uhr +eingerichtet werden sollen. Das separate Skript wird aus dem Repository geladen. .EXAMPLE .\scripts\update-bucket.ps1 .EXAMPLE @@ -40,6 +43,7 @@ function Test-MedelexisRepository([string] $Path) { function Get-PackageRenames([string] $BucketPath) { if (-not $BucketPath -or -not (Test-Path -LiteralPath $BucketPath)) { return } foreach ($app in Get-ChildItem -LiteralPath $apps -Directory) { + if ($app.Name -in @('ee-iso', 'ee_iso')) { continue } if (-not $app.Name.Contains('_')) { continue } $newName = $app.Name.Replace('_', '-') $newManifest = Join-Path $BucketPath "bucket\$newName.json" @@ -78,6 +82,19 @@ function Get-PackageRenames([string] $BucketPath) { } } +$installCli = (Test-Path -LiteralPath (Join-Path $apps 'ee-cli')) -or + -not (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli')) +$obsoleteApps = @(@('ee-iso', 'ee_iso', 'ee-cli') | Where-Object { + Test-Path -LiteralPath (Join-Path $apps $_) +}) +if ($installCli -and (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli'))) { + $obsoleteApps += 'myelexis-cli' +} +foreach ($obsolete in $obsoleteApps) { + Write-Host "Veraltetes Paket deinstallieren: $obsolete" +} +if ($installCli) { Write-Host 'MyElexis CLI wird aus medelexis neu installiert.' } + $renameSource = $null if (Test-Path -LiteralPath $target) { if (-not (Test-MedelexisRepository $target)) { @@ -98,6 +115,7 @@ if (Test-Path -LiteralPath $target) { $changes = @( foreach ($app in Get-ChildItem -LiteralPath $apps -Directory) { + if ($app.Name -in $obsoleteApps) { continue } foreach ($version in Get-ChildItem -LiteralPath $app.FullName -Directory) { # current und andere Verknüpfungen nicht nochmals bearbeiten. if ($version.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue } @@ -127,10 +145,17 @@ if (-not $Apply) { } Write-Host "Probelauf: $($changes.Count) install.json-Dateien würden geändert." Write-Host "Probelauf: $($renames.Count) Paketnamen würden umgestellt." + Write-Host "Probelauf: $(@($obsoleteApps).Count) alte Pakete würden deinstalliert." + Write-Host 'Mit -Apply wird optional ein Auto-Update um 03:00 Uhr mit Nachholen eingerichtet (y/n).' Write-Host 'Mit -Apply anwenden. Es werden keine Sicherungen erstellt.' return } +$activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) +if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { + throw 'ScoopRoot muss der aktiven Scoop-Installation entsprechen.' +} + if (-not (Test-Path -LiteralPath $target)) { if ($renameSource) { $bucketRoot = (Resolve-Path -LiteralPath (Join-Path $ScoopRoot 'buckets')).Path @@ -166,6 +191,27 @@ foreach ($change in $changes) { foreach ($change in $changes) { [IO.File]::WriteAllText($change.File, $change.Updated, [Text.UTF8Encoding]::new($false)) } +if ($installCli -and -not (Test-Path -LiteralPath (Join-Path $target 'bucket\myelexis-cli.json')) -and + -not (Test-Path -LiteralPath (Join-Path $target 'myelexis-cli.json'))) { + throw 'Manifest myelexis-cli fehlt im Ziel-Bucket. Keine Deinstallation.' +} +foreach ($obsolete in $obsoleteApps) { + $activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) + if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { + throw 'Für Deinstallationen muss ScoopRoot der aktiven Scoop-Installation entsprechen.' + } + & scoop uninstall $obsolete + if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath (Join-Path $apps $obsolete))) { + throw "'$obsolete' konnte nicht deinstalliert werden." + } +} +if ($installCli) { + Write-Host 'MyElexis CLI aus medelexis neu installieren ...' + & scoop install 'medelexis/myelexis-cli' + if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli\current\myelexis-cli.exe'))) { + throw 'MyElexis CLI konnte nicht vollständig installiert werden.' + } +} foreach ($rename in $renames) { $activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) { @@ -222,3 +268,47 @@ foreach ($name in @((Get-ChildItem -LiteralPath $apps -Directory).Name)) { Write-Host "Fertig: $($changes.Count) install.json-Dateien auf medelexis umgestellt." Write-Host "Paketnamen umgestellt: $($renames.Count)." Write-Host 'Von elexis/streichtech/medelexis bleibt nur medelexis bestehen. Prüfung: scoop status --local' + +do { + $autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant() +} while ($autoUpdateAnswer -notin @('y', 'n')) + +if ($autoUpdateAnswer -eq 'y') { + $autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis' + New-Item -ItemType Directory -Path $autoUpdateDirectory -Force | Out-Null + $autoUpdateScript = Join-Path $autoUpdateDirectory 'auto-update.ps1' + $downloadPath = Join-Path $autoUpdateDirectory ('auto-update-{0}.download' -f [guid]::NewGuid()) + try { + $autoUpdateUrl = "$repository/raw/branch/master/scripts/auto-update.ps1" + Invoke-WebRequest -Uri $autoUpdateUrl -OutFile $downloadPath -UseBasicParsing + $parseTokens = $null + $parseErrors = $null + $downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors) + if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or + 'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) { + throw 'Das heruntergeladene Auto-Update-Skript ist kein gueltiges Updateskript.' + } + Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force + # Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet. + $userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name + $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value + $taskName = "Medelexis-Scoop-AutoUpdate-$userSid" + $powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + $arguments = '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -ScoopRoot "{1}"' -f $autoUpdateScript, $ScoopRoot + $action = New-ScheduledTaskAction -Execute $powershell -Argument $arguments + $trigger = New-ScheduledTaskTrigger -Daily -At '03:00' + $principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited + $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable ` + -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew ` + -ExecutionTimeLimit (New-TimeSpan -Hours 4) + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` + -Principal $principal -Settings $settings -Force ` + -Description 'Scoop taeglich um 03:00 aktualisieren; verpasste Termine nachholen; elexis* auf hold setzen.' | Out-Null + Write-Host "Scoop Auto-Update eingeschaltet: $taskName (taeglich 03:00, verpasste Termine nachholen)." + Write-Host "Ausfuehrung verborgen unter $userId, sobald der Benutzer angemeldet ist; kein Aufwecken des PCs." + } finally { + if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath } + } +} else { + Write-Host 'Auto-Update nicht eingerichtet. Eine eventuell bestehende Aufgabe bleibt unveraendert.' +} diff --git a/tests/test_auto_update.ps1 b/tests/test_auto_update.ps1 new file mode 100644 index 0000000..07dd61d --- /dev/null +++ b/tests/test_auto_update.ps1 @@ -0,0 +1,154 @@ +#requires -Version 5.1 +# Isolated behavioural tests: no real downloads, Scoop updates or scheduled tasks. +$ErrorActionPreference = 'Stop' +$projectRoot = Split-Path $PSScriptRoot -Parent +$fixtureRoot = Join-Path $PSScriptRoot ('.auto-update-test-' + [guid]::NewGuid()) +$oldScoop = $env:SCOOP +$oldPath = $env:PATH +$oldFixture = $env:SCOOP_AUTO_UPDATE_TEST_ROOT +$oldFailure = $env:SCOOP_AUTO_UPDATE_TEST_FAILURE + +function Assert-True($Condition, [string] $Message) { + if (-not $Condition) { throw $Message } +} + +# These stand-ins capture scheduling settings without changing Windows. +function New-ScheduledTaskAction { param($Execute, $Argument) [pscustomobject]$PSBoundParameters } +function New-ScheduledTaskTrigger { param([switch]$Daily, $At) [pscustomobject]$PSBoundParameters } +function New-ScheduledTaskPrincipal { param($UserId, $LogonType, $RunLevel) [pscustomobject]$PSBoundParameters } +function New-ScheduledTaskSettingsSet { + param([switch]$StartWhenAvailable, [switch]$RunOnlyIfNetworkAvailable, + [switch]$AllowStartIfOnBatteries, [switch]$DontStopIfGoingOnBatteries, + $MultipleInstances, $ExecutionTimeLimit) + [pscustomobject]$PSBoundParameters +} +function Register-ScheduledTask { + param($TaskName, $Action, $Trigger, $Principal, $Settings, [switch]$Force, $Description) + $global:AutoUpdateTestTask = [pscustomobject]$PSBoundParameters +} +function Read-Host { + param($Prompt) + $global:AutoUpdateTestAnswers.Dequeue() +} +function Invoke-WebRequest { + param($Uri, $OutFile, [switch]$UseBasicParsing) + Assert-True ($Uri -in @('https://example.test/scoop/raw/branch/master/scripts/auto-update.ps1', + 'https://gitea.medelexis.ch/public/scoop/raw/branch/master/scripts/auto-update.ps1')) 'Unexpected download URL.' + if ($global:AutoUpdateTestInvalidDownload) { + 'This is not the setup script.' | Set-Content -LiteralPath $OutFile + } else { + @' +param([string] $ScoopRoot) +throw 'The downloaded runner must not execute during setup.' +'@ | Set-Content -LiteralPath $OutFile + } +} + +try { + # Spaces in the installation path exercise the scheduled command's quoting. + $scoopRoot = Join-Path $fixtureRoot 'scoop with spaces' + $shims = Join-Path $scoopRoot 'shims' + New-Item -ItemType Directory -Path $shims -Force | Out-Null + $scriptPath = Join-Path $fixtureRoot 'auto-update.ps1' + Copy-Item -LiteralPath (Join-Path $projectRoot 'scripts\auto-update.ps1') -Destination $scriptPath + $env:SCOOP_AUTO_UPDATE_TEST_ROOT = $fixtureRoot + $commandLog = Join-Path $fixtureRoot 'commands.log' + @' +param([Parameter(ValueFromRemainingArguments = $true)][string[]] $ScoopArguments) +Add-Content -LiteralPath (Join-Path $env:SCOOP_AUTO_UPDATE_TEST_ROOT 'commands.log') -Value ($ScoopArguments -join ' ') +if ($env:SCOOP_AUTO_UPDATE_TEST_FAILURE -eq ($ScoopArguments -join ' ')) { exit 1 } +if ($ScoopArguments[0] -eq 'list') { + [pscustomobject]@{ Name = 'elexis'; Info = '' } + [pscustomobject]@{ Name = 'Elexis-test'; Info = 'Held package' } + [pscustomobject]@{ Name = 'myelexis-cli'; Info = '' } + [pscustomobject]@{ Name = 'git'; Info = '' } + [pscustomobject]@{ Name = 'elexis-global'; Info = 'Global install' } + [pscustomobject]@{ Name = 'elexis-broken'; Info = 'Install failed' } +} +exit 0 +'@ | Set-Content -LiteralPath (Join-Path $shims 'scoop.ps1') -Encoding UTF8 + + & $scriptPath -ScoopRoot $scoopRoot + Assert-True ($LASTEXITCODE -eq 0) 'Expected successful update.' + Assert-True (((Get-Content $commandLog) -join '|') -eq 'list|hold elexis|hold Elexis-test|update|update *') 'Expected only local elexis* held before updates.' + Assert-True (Test-Path (Join-Path $fixtureRoot 'auto-update.log')) 'Expected update log.' + + foreach ($failure in @('list', 'hold elexis', 'update', 'update *')) { + Remove-Item -LiteralPath $commandLog + $env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $failure + # Expected errors are logged; do not let test ErrorActionPreference terminate the caller. + $ErrorActionPreference = 'Continue' + & $scriptPath -ScoopRoot $scoopRoot 2>$null + $result = $LASTEXITCODE + $ErrorActionPreference = 'Stop' + Assert-True ($result -eq 1) "Expected failure status for $failure." + $commands = @(Get-Content $commandLog) + Assert-True ($commands[-1] -eq $failure) "Must abort immediately after $failure." + } + # Exercise the actual opt-in/download block without running the bucket migration. + $tokens = $null + $errors = $null + $bucketAst = [Management.Automation.Language.Parser]::ParseFile((Join-Path $projectRoot 'scripts\update-bucket.ps1'), [ref]$tokens, [ref]$errors) + Assert-True ($errors.Count -eq 0) 'Invalid bucket script syntax.' + $optInBlock = [scriptblock]::Create((($bucketAst.EndBlock.Statements | Select-Object -Last 2 | ForEach-Object { $_.Extent.Text }) -join "`n")) + $repository = 'https://example.test/scoop' + Remove-Item -LiteralPath $commandLog + $global:AutoUpdateTestAnswers = [Collections.Generic.Queue[string]]::new() + $global:AutoUpdateTestAnswers.Enqueue('n') + & $optInBlock + $downloadDirectory = Join-Path $scoopRoot 'scripts\medelexis' + Assert-True (-not (Test-Path $downloadDirectory)) 'Declining must not download or set up anything.' + $global:AutoUpdateTestAnswers.Enqueue('invalid') + $global:AutoUpdateTestAnswers.Enqueue('Y') + & $optInBlock + $downloadedPath = Join-Path $downloadDirectory 'auto-update.ps1' + Assert-True (Test-Path $downloadedPath) 'Expected downloaded runner.' + $task = $global:AutoUpdateTestTask + Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00') 'Expected daily 03:00 schedule.' + Assert-True ($task.Settings.StartWhenAvailable -and $task.Settings.RunOnlyIfNetworkAvailable) 'Expected catch-up and network settings.' + Assert-True ($task.Settings.MultipleInstances -eq 'IgnoreNew') 'Expected overlap protection.' + Assert-True ($task.Principal.LogonType -eq 'Interactive' -and $task.Principal.RunLevel -eq 'Limited') 'Expected normal user session.' + Assert-True ($task.Action.Argument.Contains('-WindowStyle Hidden') -and $task.Action.Argument.Contains('-NonInteractive')) 'Expected background execution.' + Assert-True ($task.Action.Argument.Contains('-File "' + $downloadedPath + '"') -and $task.Action.Argument.Contains('-ScoopRoot "' + $scoopRoot + '"')) 'Expected quoted paths pointing to the downloaded runner.' + Assert-True (-not (Test-Path $commandLog)) 'Setup must not execute Scoop.' + $taskName = $task.TaskName + $global:AutoUpdateTestAnswers.Enqueue('y') + & $optInBlock + Assert-True ($global:AutoUpdateTestTask.TaskName -eq $taskName -and $global:AutoUpdateTestTask.Force) 'Repeated setup must replace the same task.' + $originalDownload = [IO.File]::ReadAllText($downloadedPath) + Remove-Variable AutoUpdateTestTask -Scope Global + $global:AutoUpdateTestInvalidDownload = $true + $global:AutoUpdateTestAnswers.Enqueue('y') + $rejected = $false + try { & $optInBlock } catch { $rejected = $true } + Assert-True $rejected 'Invalid download must be rejected.' + Assert-True ([IO.File]::ReadAllText($downloadedPath) -ceq $originalDownload) 'Invalid download must preserve the existing script.' + Assert-True ($null -eq (Get-Variable AutoUpdateTestTask -Scope Global -ErrorAction SilentlyContinue)) 'Invalid download must not register a task.' + Assert-True (@(Get-ChildItem $downloadDirectory -Filter '*.download').Count -eq 0) 'Temporary downloads must be removed.' + # The additional standalone installer must offer the same setup without migration. + $standaloneSetup = Join-Path $projectRoot 'scripts\setup-auto-update.ps1' + $global:AutoUpdateTestAnswers.Enqueue('n') + & $standaloneSetup -ScoopRoot $scoopRoot + Assert-True ($null -eq (Get-Variable AutoUpdateTestTask -Scope Global -ErrorAction SilentlyContinue)) 'Standalone n must not register a task.' + $global:AutoUpdateTestInvalidDownload = $false + $global:AutoUpdateTestAnswers.Enqueue('y') + & $standaloneSetup -ScoopRoot $scoopRoot + $task = $global:AutoUpdateTestTask + Assert-True ($task.TaskName -eq $taskName -and $task.Force) 'Standalone setup must use the same task.' + Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00' -and $task.Settings.StartWhenAvailable) 'Standalone setup must schedule daily 03:00 with catch-up.' + Assert-True ($task.Action.Argument.Contains('-WindowStyle Hidden') -and $task.Action.Argument.Contains('-File "' + $downloadedPath + '"')) 'Standalone task must invoke the downloaded runner hidden.' + Assert-True (-not (Test-Path $commandLog)) 'Standalone setup must not run Scoop or migrations.' + Write-Host 'PASS: schedule, repeat setup, hold filtering, command order, logging, failures, opt-in and download setup.' +} finally { + $env:SCOOP = $oldScoop + $env:PATH = $oldPath + $env:SCOOP_AUTO_UPDATE_TEST_ROOT = $oldFixture + $env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $oldFailure + Remove-Variable AutoUpdateTestTask, AutoUpdateTestAnswers, AutoUpdateTestInvalidDownload -Scope Global -ErrorAction SilentlyContinue + # Delete only this test's unique directory, after checking its absolute parent. + if ((Test-Path -LiteralPath $fixtureRoot) -and + [IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($fixtureRoot)) -eq [IO.Path]::GetFullPath($PSScriptRoot) -and + [IO.Path]::GetFileName($fixtureRoot).StartsWith('.auto-update-test-')) { + Remove-Item -LiteralPath $fixtureRoot -Recurse -Force + } +}