Compare commits

...
Author SHA1 Message Date
Simon Daniel Streich ee6d5b9b1e init 2026-10-21 12:06:08 +02:00
5 changed files with 364 additions and 2 deletions

No files matched your search

+49
View File
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
# 1. SCOOP CHECK & INSTALL
# 1. SCOOP CHECK & INSTALL
$bucketList = scoop bucket list 2>$null | Out-String
if ($bucketList) {
+69
View File
@@ -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.'
}
+91 -1
View File
@@ -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.'
}
+154
View File
@@ -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
}
}