Files
wehub-resource-sync 643e9f9fcb
.NET Tests / test-codebase (push) Waiting to run
Translation Validation / validate-translations (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:01:52 +08:00

1156 lines
49 KiB
PowerShell

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$runningOnWindows = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
[System.Runtime.InteropServices.OSPlatform]::Windows
)
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$srcRoot = Join-Path $repoRoot 'src'
$configuration = if ($env:CONFIGURATION) { $env:CONFIGURATION } else { 'Release' }
$defaultManifest = if ($runningOnWindows) {
Join-Path $PSScriptRoot 'cli-e2e.manifest.windows.json'
}
else {
Join-Path $PSScriptRoot 'cli-e2e.manifest.linux.json'
}
$manifestPath = if ($env:UNIGETUI_CLI_E2E_MANIFEST) {
$env:UNIGETUI_CLI_E2E_MANIFEST
}
else {
$defaultManifest
}
if (-not (Test-Path $manifestPath)) {
throw "CLI E2E manifest not found at $manifestPath"
}
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json -AsHashtable
if ($null -eq $manifest) {
throw "Could not parse CLI E2E manifest at $manifestPath"
}
$artifactRoot = if ($env:UNIGETUI_CLI_E2E_ARTIFACTS) {
$env:UNIGETUI_CLI_E2E_ARTIFACTS
}
else {
Join-Path ([System.IO.Path]::GetTempPath()) ("unigetui-cli-e2e-" + [Guid]::NewGuid().ToString('N'))
}
if (Test-Path $artifactRoot) {
Remove-Item -Recurse -Force $artifactRoot
}
New-Item -ItemType Directory -Path $artifactRoot -Force | Out-Null
$artifactRoot = (Resolve-Path $artifactRoot).Path
$daemonRoot = $artifactRoot
$downloadRoot = Join-Path $daemonRoot 'downloads'
$coveragePath = Join-Path $daemonRoot 'coverage.json'
$inventoryPath = Join-Path $daemonRoot 'environment.json'
$daemonStdOutLog = Join-Path $daemonRoot 'headless-daemon.stdout.log'
$daemonStdErrLog = Join-Path $daemonRoot 'headless-daemon.stderr.log'
$preserveArtifacts = $true
$localDataRoot = if ($runningOnWindows) {
Join-Path $daemonRoot 'AppData\Local'
}
else {
Join-Path $daemonRoot '.local\share'
}
$roamingDataRoot = if ($runningOnWindows) {
Join-Path $daemonRoot 'AppData\Roaming'
}
else {
$null
}
$dotnetHomeRoot = Join-Path $daemonRoot '.dotnet-home'
$npmGlobalRoot = Join-Path $daemonRoot 'npm-global'
New-Item -ItemType Directory -Path $downloadRoot -Force | Out-Null
New-Item -ItemType Directory -Path $localDataRoot -Force | Out-Null
New-Item -ItemType Directory -Path $dotnetHomeRoot -Force | Out-Null
New-Item -ItemType Directory -Path $npmGlobalRoot -Force | Out-Null
if (-not [string]::IsNullOrWhiteSpace($roamingDataRoot)) {
New-Item -ItemType Directory -Path $roamingDataRoot -Force | Out-Null
}
$env:DOTNET_CLI_HOME = $dotnetHomeRoot
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
$env:UNIGETUI_GITHUB_TOKEN_NAMESPACE = "cli-e2e-$([Guid]::NewGuid().ToString('N'))"
$env:UNIGETUI_AVALONIA_DEVTOOLS = 'disabled'
$env:npm_config_prefix = $npmGlobalRoot
if (-not $runningOnWindows) {
$env:HOME = $daemonRoot
$env:USERPROFILE = $daemonRoot
$env:XDG_DATA_HOME = $localDataRoot
}
$coverage = [ordered]@{
manifest = $manifest.name
tested = @()
excluded = @($manifest.excludedCommands)
}
function Add-Tested {
param(
[Parameter(Mandatory = $true)]
[string] $Command,
[string] $Mode = 'success'
)
$script:coverage.tested += [ordered]@{
command = $Command
mode = $Mode
}
}
function Write-Stage {
param(
[Parameter(Mandatory = $true)]
[string] $Name
)
Write-Host "== $Name =="
}
function Find-BuiltArtifact {
param(
[Parameter(Mandatory = $true)]
[string] $ProjectDirectory,
[Parameter(Mandatory = $true)]
[string] $FileName
)
$outputRoot = Join-Path $ProjectDirectory 'bin'
if (-not (Test-Path $outputRoot)) {
return $null
}
return Get-ChildItem -Path $outputRoot -Recurse -File -Filter $FileName |
Sort-Object @{
Expression = { if ($_.FullName -like "*\bin\*\$configuration\*") { 0 } else { 1 } }
}, @{
Expression = { $_.FullName }
} |
Select-Object -First 1 -ExpandProperty FullName
}
function Get-ManifestManagerByRole {
param(
[Parameter(Mandatory = $true)]
[string] $Role
)
$matches = @($manifest.packageManagers | Where-Object { @($_.roles) -contains $Role })
if ($matches.Count -eq 0) {
return $null
}
return $matches[0]
}
function Get-PackageArguments {
param(
[Parameter(Mandatory = $true)]
[hashtable] $Fixture,
[switch] $IncludeVersion,
[switch] $IncludeSource
)
$arguments = @(
'--manager', [string]$Fixture.manager,
'--id', [string]$Fixture.packageId
)
if ($IncludeVersion -and $Fixture.ContainsKey('installVersion')) {
$arguments += @('--version', [string]$Fixture.installVersion)
}
if ($IncludeSource -and $Fixture.ContainsKey('sourceName')) {
$arguments += @('--source', [string]$Fixture.sourceName)
}
if ($Fixture.ContainsKey('scope')) {
$arguments += @('--scope', [string]$Fixture.scope)
}
return $arguments
}
function Find-PackageMatch {
param(
[Parameter(Mandatory = $true)]
[object[]] $Packages,
[Parameter(Mandatory = $true)]
[hashtable] $Fixture,
[string] $Version
)
return @($Packages | Where-Object {
$_.id -eq $Fixture.packageId -and (
-not $PSBoundParameters.ContainsKey('Version') -or $_.version -eq $Version
)
})[0]
}
function Resolve-QueueOutputPath {
param(
[Parameter(Mandatory = $true)]
[hashtable] $QueueFixture
)
$safeManager = ([string]$QueueFixture.manager) -replace '[^A-Za-z0-9._-]', '_'
$safePackage = ([string]$QueueFixture.packageId) -replace '[^A-Za-z0-9._-]', '_'
$targetDirectory = Join-Path $downloadRoot "$safeManager-$safePackage"
New-Item -ItemType Directory -Path $targetDirectory -Force | Out-Null
return $targetDirectory
}
function Get-DaemonCommand {
$daemonProject = Join-Path $srcRoot ([string]$manifest.daemon.project)
if (-not (Test-Path $daemonProject)) {
throw "Daemon project not found at $daemonProject"
}
switch ([string]$manifest.daemon.kind) {
'windows-exe' {
$daemonExe = if ($env:UNIGETUI_DAEMON_EXE) {
$env:UNIGETUI_DAEMON_EXE
}
else {
Find-BuiltArtifact -ProjectDirectory (Split-Path $daemonProject -Parent) -FileName "$($manifest.daemon.assemblyName).exe"
}
if ([string]::IsNullOrWhiteSpace($daemonExe) -or -not (Test-Path $daemonExe)) {
throw "Windows headless executable was not found. Expected $($manifest.daemon.assemblyName).exe under $(Split-Path $daemonProject -Parent)\bin\$configuration"
}
return @{
FilePath = (Resolve-Path $daemonExe).Path
WorkingDirectory = Split-Path (Resolve-Path $daemonExe).Path -Parent
}
}
'avalonia-dll' {
$daemonDll = if ($env:UNIGETUI_DAEMON_DLL) {
$env:UNIGETUI_DAEMON_DLL
}
else {
Find-BuiltArtifact -ProjectDirectory (Split-Path $daemonProject -Parent) -FileName "$($manifest.daemon.assemblyName).dll"
}
if ([string]::IsNullOrWhiteSpace($daemonDll) -or -not (Test-Path $daemonDll)) {
throw "Avalonia headless daemon DLL was not found. Expected $($manifest.daemon.assemblyName).dll under $(Split-Path $daemonProject -Parent)\bin\$configuration"
}
$resolvedDll = (Resolve-Path $daemonDll).Path
return @{
FilePath = 'dotnet'
WorkingDirectory = Split-Path $resolvedDll -Parent
PrefixArguments = @($resolvedDll)
}
}
'avalonia-native' {
$fileName = if ($runningOnWindows) { "$($manifest.daemon.assemblyName).exe" } else { $manifest.daemon.assemblyName }
$daemonNative = if ($env:UNIGETUI_DAEMON_EXE) {
$env:UNIGETUI_DAEMON_EXE
}
else {
Find-BuiltArtifact -ProjectDirectory (Split-Path $daemonProject -Parent) -FileName $fileName
}
if ([string]::IsNullOrWhiteSpace($daemonNative) -or -not (Test-Path $daemonNative)) {
throw "NativeAOT headless executable was not found. Expected $fileName under $(Split-Path $daemonProject -Parent)\bin\$configuration"
}
return @{
FilePath = (Resolve-Path $daemonNative).Path
WorkingDirectory = Split-Path (Resolve-Path $daemonNative).Path -Parent
}
}
default {
throw "Unsupported daemon kind $($manifest.daemon.kind)"
}
}
}
$pipeName = "UniGetUI.CI.$([Guid]::NewGuid().ToString('N'))"
$transportArgs = @('--transport', 'named-pipe', '--pipe-name', $pipeName)
$daemonExtraArgs = @('--headless', '--ipc-api-transport', 'named-pipe', '--ipc-api-pipe-name', $pipeName)
$daemonStartupTimeoutSeconds = if ($runningOnWindows) { 300 } else { 120 }
$daemonCommand = Get-DaemonCommand
$cliCommand = $daemonCommand
$process = $null
$gracefulShutdown = $false
function Get-DaemonLog {
$stdout = if (Test-Path $daemonStdOutLog) { Get-Content $daemonStdOutLog -Raw } else { '' }
$stderr = if (Test-Path $daemonStdErrLog) { Get-Content $daemonStdErrLog -Raw } else { '' }
return ($stdout, $stderr -join [Environment]::NewLine).Trim()
}
function Stop-Daemon {
if ($null -ne $script:process -and -not $script:process.HasExited) {
Stop-Process -Id $script:process.Id
}
}
function Invoke-CliRaw {
param(
[Parameter(Mandatory = $true)]
[string[]] $Arguments
)
$commandArguments = @()
if ($cliCommand.ContainsKey('PrefixArguments')) {
$commandArguments += $cliCommand.PrefixArguments
}
$commandArguments += $transportArgs + $Arguments
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
$startInfo.FileName = $cliCommand.FilePath
$startInfo.WorkingDirectory = $cliCommand.WorkingDirectory
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
foreach ($argument in $commandArguments) {
[void]$startInfo.ArgumentList.Add([string]$argument)
}
$commandProcess = [System.Diagnostics.Process]::Start($startInfo)
if ($null -eq $commandProcess) {
throw "Failed to start CLI command: $($cliCommand.FilePath) $($commandArguments -join ' ')"
}
$stdout = $commandProcess.StandardOutput.ReadToEnd()
$stderr = $commandProcess.StandardError.ReadToEnd()
$commandProcess.WaitForExit()
$text = (@($stdout, $stderr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
$_.TrimEnd()
}) -join [Environment]::NewLine
return @{
ExitCode = $commandProcess.ExitCode
Text = $text
}
}
function Invoke-CliJson {
param(
[Parameter(Mandatory = $true)]
[string[]] $Arguments
)
$result = Invoke-CliRaw -Arguments $Arguments
if ($result.ExitCode -ne 0) {
throw "CLI command failed ($($result.ExitCode)): $($Arguments -join ' ')`n$($result.Text)"
}
if ([string]::IsNullOrWhiteSpace($result.Text)) {
throw "CLI command returned empty output: $($Arguments -join ' ')"
}
return $result.Text | ConvertFrom-Json
}
function Invoke-CliFailure {
param(
[Parameter(Mandatory = $true)]
[string[]] $Arguments
)
$result = Invoke-CliRaw -Arguments $Arguments
if ($result.ExitCode -eq 0) {
throw "CLI command unexpectedly succeeded: $($Arguments -join ' ')`n$($result.Text)"
}
return $result
}
function Wait-ForCliCondition {
param(
[Parameter(Mandatory = $true)]
[string[]] $Arguments,
[Parameter(Mandatory = $true)]
[scriptblock] $Condition,
[Parameter(Mandatory = $true)]
[string] $FailureMessage,
[int] $TimeoutSeconds = 120,
[int] $DelaySeconds = 2
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$lastResponse = $null
$lastError = $null
do {
try {
$lastResponse = Invoke-CliJson -Arguments $Arguments
$lastError = $null
if (& $Condition $lastResponse) {
return $lastResponse
}
}
catch {
$lastError = $_.Exception.Message
}
Start-Sleep -Seconds $DelaySeconds
} while ((Get-Date) -lt $deadline)
if ($null -ne $lastResponse) {
throw "$FailureMessage`nLast payload: $($lastResponse | ConvertTo-Json -Depth 10)"
}
throw "$FailureMessage`nLast error: $lastError"
}
function Wait-ForInstalledPackage {
param(
[Parameter(Mandatory = $true)]
[hashtable] $Fixture,
[string] $ExpectedVersion,
[int] $TimeoutSeconds = 180
)
return Wait-ForCliCondition `
-Arguments @('package', 'installed', '--manager', [string]$Fixture.manager) `
-Condition {
param($response)
@($response.packages | Where-Object {
$_.id -eq $Fixture.packageId -and (
[string]::IsNullOrWhiteSpace($ExpectedVersion) -or $_.version -eq $ExpectedVersion
)
}).Count -gt 0
} `
-FailureMessage "package installed did not report $($Fixture.packageId) for manager $($Fixture.manager)" `
-TimeoutSeconds $TimeoutSeconds `
-DelaySeconds 3
}
function Wait-ForPackageRemoval {
param(
[Parameter(Mandatory = $true)]
[hashtable] $Fixture,
[int] $TimeoutSeconds = 180
)
return Wait-ForCliCondition `
-Arguments @('package', 'installed', '--manager', [string]$Fixture.manager) `
-Condition {
param($response)
@($response.packages | Where-Object { $_.id -eq $Fixture.packageId }).Count -eq 0
} `
-FailureMessage "$($Fixture.packageId) still appears in package installed for manager $($Fixture.manager)" `
-TimeoutSeconds $TimeoutSeconds `
-DelaySeconds 3
}
function Wait-ForPackageUpdateVisibility {
param(
[Parameter(Mandatory = $true)]
[hashtable] $Fixture,
[int] $TimeoutSeconds = 120
)
return Wait-ForCliCondition `
-Arguments @('package', 'updates', '--manager', [string]$Fixture.manager) `
-Condition {
param($response)
@($response.updates | Where-Object { $_.id -eq $Fixture.packageId }).Count -gt 0
} `
-FailureMessage "package updates did not report $($Fixture.packageId) for manager $($Fixture.manager)" `
-TimeoutSeconds $TimeoutSeconds `
-DelaySeconds 3
}
function Get-LatestFixtureVersion {
param(
[Parameter(Mandatory = $true)]
[hashtable] $Fixture,
[Parameter(Mandatory = $true)]
[object] $SearchResponse,
[Parameter(Mandatory = $true)]
[object] $VersionResponse
)
$searchMatch = @($SearchResponse.packages | Where-Object { $_.id -eq $Fixture.packageId })[0]
if ($null -eq $searchMatch) {
throw "package search did not return $($Fixture.packageId) for manager $($Fixture.manager)"
}
$candidateVersions = @()
if (-not [string]::IsNullOrWhiteSpace($searchMatch.version)) {
$candidateVersions += [string]$searchMatch.version
}
$candidateVersions += @($VersionResponse.versions)
$latestVersion = @($candidateVersions | Where-Object {
-not [string]::IsNullOrWhiteSpace($_) -and $_ -ne $Fixture.installVersion
})[0]
if ([string]::IsNullOrWhiteSpace($latestVersion)) {
throw "Could not resolve a newer version for $($Fixture.packageId) on manager $($Fixture.manager)"
}
return [string]$latestVersion
}
function Assert-JsonCommandSucceeded {
param(
[Parameter(Mandatory = $true)]
[object] $Response,
[Parameter(Mandatory = $true)]
[string] $Label
)
if ($Response.status -ne 'success') {
throw "$Label failed: $($Response | ConvertTo-Json -Depth 10)"
}
}
function Write-EnvironmentInventory {
$inventory = [ordered]@{
manifest = $manifest.name
os = if ($runningOnWindows) { 'windows' } else { 'linux' }
dotnet = (& dotnet --version)
python = ''
pip = ''
npm = ''
}
try {
$inventory.python = (& python --version 2>&1 | Out-String).Trim()
}
catch {
$inventory.python = $_.Exception.Message
}
try {
$inventory.pip = (& python -m pip --version 2>&1 | Out-String).Trim()
}
catch {
$inventory.pip = $_.Exception.Message
}
try {
$inventory.npm = (& npm --version 2>&1 | Out-String).Trim()
}
catch {
$inventory.npm = $_.Exception.Message
}
Set-Content -Path $inventoryPath -Value ($inventory | ConvertTo-Json -Depth 8) -Encoding UTF8
}
Write-EnvironmentInventory
try {
$daemonArguments = @()
if ($daemonCommand.ContainsKey('PrefixArguments')) {
$daemonArguments += $daemonCommand.PrefixArguments
}
$daemonArguments += $daemonExtraArgs
$process = Start-Process `
-FilePath $daemonCommand.FilePath `
-ArgumentList $daemonArguments `
-WorkingDirectory $daemonCommand.WorkingDirectory `
-RedirectStandardOutput $daemonStdOutLog `
-RedirectStandardError $daemonStdErrLog `
-PassThru
$status = Wait-ForCliCondition `
-Arguments @('status') `
-Condition { param($response) $response.running -and $response.transport -eq 'named-pipe' } `
-FailureMessage 'Headless daemon never became ready over named-pipe IPC.' `
-TimeoutSeconds $daemonStartupTimeoutSeconds `
-DelaySeconds 2
Write-Stage 'Status and headless transport'
if ($status.namedPipeName -ne $pipeName) {
throw "status did not report the expected named pipe name. Expected $pipeName, got $($status.namedPipeName)"
}
if (-not $runningOnWindows) {
$expectedSocketPath = "/tmp/$pipeName"
if ($status.namedPipePath -ne $expectedSocketPath) {
throw "status did not report the expected Unix socket path. Expected $expectedSocketPath, got $($status.namedPipePath)"
}
}
Add-Tested 'status'
$version = Invoke-CliJson -Arguments @('version')
if ($version.build -le 0) {
throw "version did not return a positive build number"
}
Add-Tested 'version'
$appState = Invoke-CliJson -Arguments @('app', 'status')
if (-not $appState.app.headless -or $appState.app.windowAvailable -or $appState.app.canNavigate -or -not $appState.app.canQuit) {
throw "app status did not report the expected headless state: $($appState | ConvertTo-Json -Depth 8)"
}
Add-Tested 'app status'
Invoke-CliFailure -Arguments @('app', 'show') | Out-Null
Add-Tested 'app show' 'expected-failure'
Invoke-CliFailure -Arguments @('app', 'navigate', '--page', 'settings') | Out-Null
Add-Tested 'app navigate' 'expected-failure'
$bundleFixture = Get-ManifestManagerByRole -Role 'bundle'
Invoke-CliFailure -Arguments @('package', 'show', '--id', [string]$bundleFixture.packageId, '--source', [string]$bundleFixture.sourceName) | Out-Null
Add-Tested 'package show' 'expected-failure'
if ($runningOnWindows -and $manifest.transport.verifyNoTcpListener) {
$connections = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -eq $process.Id }
if (@($connections).Count -ne 0) {
throw "Headless named-pipe session opened a TCP listener unexpectedly: $(@($connections | Select-Object -ExpandProperty LocalPort) -join ', ')"
}
}
Write-Stage 'Manager inspection'
$managers = Invoke-CliJson -Arguments @('manager', 'list')
foreach ($fixture in @($manifest.packageManagers)) {
if (@($managers.managers | Where-Object { $_.name -eq $fixture.manager }).Count -eq 0) {
throw "manager list did not report $($fixture.manager)"
}
}
Add-Tested 'manager list'
foreach ($fixture in @($manifest.packageManagers)) {
$maintenance = Invoke-CliJson -Arguments @('manager', 'maintenance', '--manager', [string]$fixture.manager)
if ($maintenance.maintenance.manager -ne $fixture.manager) {
throw "manager maintenance did not return the expected manager payload for $($fixture.manager)"
}
if (@($maintenance.maintenance.supportedActions | Where-Object { $_ -eq 'reload' }).Count -eq 0) {
throw "manager maintenance did not expose reload for $($fixture.manager)"
}
$reloadManager = Invoke-CliJson -Arguments @('manager', 'reload', '--manager', [string]$fixture.manager)
if ($reloadManager.operationStatus -ne 'completed') {
throw "manager reload did not complete successfully for $($fixture.manager): $($reloadManager | ConvertTo-Json -Depth 8)"
}
}
Add-Tested 'manager maintenance'
Add-Tested 'manager reload'
$toggleManagerFixture = Get-ManifestManagerByRole -Role 'toggle-manager'
$disableManager = Invoke-CliJson -Arguments @('manager', 'disable', '--manager', [string]$toggleManagerFixture.manager)
if ($disableManager.manager.enabled) {
throw "manager disable did not disable $($toggleManagerFixture.manager)"
}
$enableManager = Invoke-CliJson -Arguments @('manager', 'enable', '--manager', [string]$toggleManagerFixture.manager)
if (-not $enableManager.manager.enabled) {
throw "manager enable did not re-enable $($toggleManagerFixture.manager)"
}
Add-Tested 'manager enable'
Add-Tested 'manager disable'
$disableNotifications = Invoke-CliJson -Arguments @('manager', 'notifications', 'disable', '--manager', [string]$bundleFixture.manager)
if (-not $disableNotifications.manager.notificationsSuppressed) {
throw "manager notifications disable did not suppress notifications for $($bundleFixture.manager)"
}
$enableNotifications = Invoke-CliJson -Arguments @('manager', 'notifications', 'enable', '--manager', [string]$bundleFixture.manager)
if ($enableNotifications.manager.notificationsSuppressed) {
throw "manager notifications enable did not re-enable notifications for $($bundleFixture.manager)"
}
Add-Tested 'manager notifications disable'
Add-Tested 'manager notifications enable'
foreach ($fixture in @($manifest.packageManagers)) {
$sources = Invoke-CliJson -Arguments @('source', 'list', '--manager', [string]$fixture.manager)
if (@($sources.sources | Where-Object { $_.name -eq $fixture.sourceName }).Count -eq 0) {
throw "source list did not report $($fixture.sourceName) for $($fixture.manager)"
}
}
Add-Tested 'source list'
$sourceFixture = Get-ManifestManagerByRole -Role 'source'
if ($null -ne $sourceFixture) {
Write-Stage 'Source add/remove'
$sourceDirectory = Join-Path $daemonRoot 'LocalFeed'
New-Item -ItemType Directory -Path $sourceDirectory -Force | Out-Null
$sourceUri = ([System.Uri](Resolve-Path $sourceDirectory).Path).AbsoluteUri
$customSourceName = 'ci-local-feed'
$addedSource = Invoke-CliJson -Arguments @(
'source', 'add',
'--manager', [string]$sourceFixture.manager,
'--name', $customSourceName,
'--url', $sourceUri
)
Assert-JsonCommandSucceeded -Response $addedSource -Label 'source add'
$sourcesAfterAdd = Invoke-CliJson -Arguments @('source', 'list', '--manager', [string]$sourceFixture.manager)
if (@($sourcesAfterAdd.sources | Where-Object { $_.name -eq $customSourceName -and $_.url -eq $sourceUri }).Count -eq 0) {
throw "source add did not expose the configured custom source"
}
$removedSource = Invoke-CliJson -Arguments @(
'source', 'remove',
'--manager', [string]$sourceFixture.manager,
'--name', $customSourceName,
'--url', $sourceUri
)
Assert-JsonCommandSucceeded -Response $removedSource -Label 'source remove'
$sourcesAfterRemove = Invoke-CliJson -Arguments @('source', 'list', '--manager', [string]$sourceFixture.manager)
if (@($sourcesAfterRemove.sources | Where-Object { $_.name -eq $customSourceName }).Count -ne 0) {
throw "source remove did not remove the configured custom source"
}
Add-Tested 'source add'
Add-Tested 'source remove'
}
Write-Stage 'Settings and secure settings'
$settings = Invoke-CliJson -Arguments @('settings', 'list')
if (@($settings.settings | Where-Object { $_.name -eq 'FreshValue' }).Count -eq 0) {
throw "settings list did not report FreshValue"
}
Add-Tested 'settings list'
$setFreshValue = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'FreshValue', '--value', 'cli-smoke')
if ($setFreshValue.setting.stringValue -ne 'cli-smoke') {
throw "settings set did not persist FreshValue"
}
$getFreshValue = Invoke-CliJson -Arguments @('settings', 'get', '--key', 'FreshValue')
if ($getFreshValue.setting.stringValue -ne 'cli-smoke') {
throw "settings get did not return FreshValue"
}
Add-Tested 'settings set'
Add-Tested 'settings get'
$setFreshBool = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'FreshBoolSetting', '--enabled', 'true')
if (-not $setFreshBool.setting.boolValue) {
throw "settings set did not enable FreshBoolSetting"
}
$secureList = Invoke-CliJson -Arguments @('settings', 'secure', 'list')
if (@($secureList.settings | Where-Object { $_.key -eq $manifest.secureSettings.toggleKey }).Count -eq 0) {
throw "settings secure list did not report $($manifest.secureSettings.toggleKey)"
}
$secureGet = Invoke-CliJson -Arguments @('settings', 'secure', 'get', '--key', [string]$manifest.secureSettings.toggleKey)
if ($secureGet.setting.key -ne $manifest.secureSettings.toggleKey) {
throw "settings secure get did not return the requested key"
}
Add-Tested 'settings secure list'
Add-Tested 'settings secure get'
if ($manifest.secureSettings.allowSet) {
$secureSetOn = Invoke-CliJson -Arguments @(
'settings', 'secure', 'set',
'--key', [string]$manifest.secureSettings.toggleKey,
'--enabled', 'true'
)
if (-not $secureSetOn.setting.enabled) {
throw "settings secure set did not enable $($manifest.secureSettings.toggleKey)"
}
Add-Tested 'settings secure set'
$maintenanceWithCustomPaths = Invoke-CliJson -Arguments @('manager', 'maintenance', '--manager', [string]$manifest.secureSettings.managerForExecutableOverride)
if (-not $maintenanceWithCustomPaths.maintenance.customExecutablePathsAllowed) {
throw "manager maintenance did not reflect enabled custom executable paths"
}
$setExecutable = Invoke-CliJson -Arguments @(
'manager', 'set-executable',
'--manager', [string]$manifest.secureSettings.managerForExecutableOverride,
'--path', [string]$maintenanceWithCustomPaths.maintenance.effectiveExecutablePath
)
if ($setExecutable.maintenance.configuredExecutablePath -ne $maintenanceWithCustomPaths.maintenance.effectiveExecutablePath) {
throw "manager set-executable did not persist the configured executable path"
}
$clearExecutable = Invoke-CliJson -Arguments @(
'manager', 'clear-executable',
'--manager', [string]$manifest.secureSettings.managerForExecutableOverride
)
if (-not [string]::IsNullOrWhiteSpace($clearExecutable.maintenance.configuredExecutablePath)) {
throw "manager clear-executable did not clear the custom executable path"
}
Add-Tested 'manager set-executable'
Add-Tested 'manager clear-executable'
$secureSetOff = Invoke-CliJson -Arguments @(
'settings', 'secure', 'set',
'--key', [string]$manifest.secureSettings.toggleKey,
'--enabled', 'false'
)
if ($secureSetOff.setting.enabled) {
throw "settings secure set did not disable $($manifest.secureSettings.toggleKey)"
}
}
Write-Stage 'Shortcut and backup'
$syntheticShortcut = Join-Path $daemonRoot 'SyntheticShortcut.lnk'
New-Item -ItemType File -Path $syntheticShortcut | Out-Null
$keepShortcut = Invoke-CliJson -Arguments @('shortcut', 'set', '--path', $syntheticShortcut, '--status', 'keep')
if ($keepShortcut.shortcut.status -ne 'keep') {
throw "shortcut set did not persist keep"
}
$shortcuts = Invoke-CliJson -Arguments @('shortcut', 'list')
if (@($shortcuts.shortcuts | Where-Object { $_.path -eq $syntheticShortcut -and $_.status -eq 'keep' -and $_.existsOnDisk }).Count -eq 0) {
throw "shortcut list did not report the kept shortcut"
}
$deleteShortcut = Invoke-CliJson -Arguments @('shortcut', 'set', '--path', $syntheticShortcut, '--status', 'delete')
if ($deleteShortcut.shortcut.status -ne 'delete' -or (Test-Path $syntheticShortcut)) {
throw "shortcut set --status delete did not delete the shortcut"
}
$resetShortcut = Invoke-CliJson -Arguments @('shortcut', 'reset', '--path', $syntheticShortcut)
if ($resetShortcut.shortcut.status -ne 'unknown') {
throw "shortcut reset did not clear the shortcut verdict"
}
$resetAllShortcuts = Invoke-CliJson -Arguments @('shortcut', 'reset-all')
Assert-JsonCommandSucceeded -Response $resetAllShortcuts -Label 'shortcut reset-all'
Add-Tested 'shortcut list'
Add-Tested 'shortcut set'
Add-Tested 'shortcut reset'
Add-Tested 'shortcut reset-all'
$appLog = Invoke-CliJson -Arguments @('log', 'app', '--level', '5')
if (@($appLog.entries).Count -eq 0) {
throw "log app returned no entries"
}
Add-Tested 'log app'
$backupStatus = Invoke-CliJson -Arguments @('backup', 'status')
if ([string]::IsNullOrWhiteSpace($backupStatus.backup.backupDirectory)) {
throw "backup status did not report the backup directory"
}
Add-Tested 'backup status'
$backupDirectory = Join-Path $daemonRoot 'backups'
$setBackupDirectory = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'ChangeBackupOutputDirectory', '--value', $backupDirectory)
$setBackupFileName = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'ChangeBackupFileName', '--value', 'cli-e2e-backup')
$disableBackupTimestamping = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'EnableBackupTimestamping', '--enabled', 'false')
if ($setBackupDirectory.setting.stringValue -ne $backupDirectory -or $setBackupFileName.setting.stringValue -ne 'cli-e2e-backup' -or $disableBackupTimestamping.setting.boolValue) {
throw "backup settings did not persist correctly"
}
$localBackup = Invoke-CliJson -Arguments @('backup', 'local', 'create')
Assert-JsonCommandSucceeded -Response $localBackup -Label 'backup local create'
if (-not (Test-Path $localBackup.path)) {
throw "backup local create did not write the reported backup path"
}
Add-Tested 'backup local create'
Write-Stage 'Package discovery'
$fixtureState = @{}
foreach ($fixture in @($manifest.packageManagers)) {
$search = Invoke-CliJson -Arguments @('package', 'search', '--manager', [string]$fixture.manager, '--query', [string]$fixture.query, '--max-results', '20')
$details = Invoke-CliJson -Arguments (@('package', 'details') + (Get-PackageArguments -Fixture $fixture -IncludeSource))
$versions = Invoke-CliJson -Arguments (@('package', 'versions') + (Get-PackageArguments -Fixture $fixture -IncludeSource))
if (@($versions.versions | Where-Object { $_ -eq $fixture.installVersion }).Count -eq 0) {
throw "package versions did not include $($fixture.installVersion) for $($fixture.packageId) on $($fixture.manager)"
}
if ($details.package.id -ne $fixture.packageId) {
throw "package details did not return $($fixture.packageId) for $($fixture.manager)"
}
$latestVersion = Get-LatestFixtureVersion -Fixture $fixture -SearchResponse $search -VersionResponse $versions
$downloadDirectory = Join-Path $downloadRoot (([string]$fixture.manager -replace '[^A-Za-z0-9._-]', '_') + '-sync')
New-Item -ItemType Directory -Path $downloadDirectory -Force | Out-Null
$download = Invoke-CliJson -Arguments (@('package', 'download') + (Get-PackageArguments -Fixture $fixture -IncludeSource) + @('--output', $downloadDirectory))
Assert-JsonCommandSucceeded -Response $download -Label "package download $($fixture.packageId)"
if ([string]::IsNullOrWhiteSpace($download.outputPath) -or -not (Test-Path $download.outputPath)) {
throw "package download did not create an artifact for $($fixture.packageId) on $($fixture.manager)"
}
$fixtureState[[string]$fixture.manager] = [ordered]@{
fixture = $fixture
latestVersion = $latestVersion
}
}
Add-Tested 'package search'
Add-Tested 'package details'
Add-Tested 'package versions'
Add-Tested 'package download'
Write-Stage 'Bundle roundtrip and bundle install'
$resetBundle = Invoke-CliJson -Arguments @('bundle', 'reset')
Assert-JsonCommandSucceeded -Response $resetBundle -Label 'bundle reset'
$bundleAfterReset = Invoke-CliJson -Arguments @('bundle', 'get')
if ($bundleAfterReset.bundle.packageCount -ne 0) {
throw "bundle get did not return an empty bundle after reset"
}
$addBundlePackage = Invoke-CliJson -Arguments (@('bundle', 'add') + (Get-PackageArguments -Fixture $bundleFixture -IncludeVersion -IncludeSource) + @('--selection', 'search'))
if ($addBundlePackage.package.id -ne $bundleFixture.packageId) {
throw "bundle add did not add $($bundleFixture.packageId)"
}
$bundle = Invoke-CliJson -Arguments @('bundle', 'get')
if (@($bundle.bundle.packages | Where-Object { $_.id -eq $bundleFixture.packageId -and $_.selectedVersion -eq $bundleFixture.installVersion }).Count -eq 0) {
throw "bundle get did not report the selected bundle package"
}
$exportedBundle = Invoke-CliJson -Arguments @('bundle', 'export')
if ([string]::IsNullOrWhiteSpace($exportedBundle.content)) {
throw "bundle export returned no content"
}
$bundleRoundtripPath = Join-Path $daemonRoot 'BundleRoundtrip.json'
Set-Content -Path $bundleRoundtripPath -Value $exportedBundle.content -Encoding UTF8
$removeBundlePackage = Invoke-CliJson -Arguments (@('bundle', 'remove') + (Get-PackageArguments -Fixture $bundleFixture -IncludeSource))
if ($removeBundlePackage.removedCount -lt 1) {
throw "bundle remove did not remove $($bundleFixture.packageId)"
}
$importBundle = Invoke-CliJson -Arguments @('bundle', 'import', '--path', $bundleRoundtripPath)
Assert-JsonCommandSucceeded -Response $importBundle -Label 'bundle import'
$bundleInstall = Invoke-CliJson -Arguments @('bundle', 'install')
if ($bundleInstall.status -ne 'success' -or @($bundleInstall.results | Where-Object { $_.package.id -eq $bundleFixture.packageId }).Count -eq 0) {
throw "bundle install did not report a successful package result: $($bundleInstall | ConvertTo-Json -Depth 10)"
}
Wait-ForInstalledPackage -Fixture $bundleFixture | Out-Null
Add-Tested 'bundle get'
Add-Tested 'bundle reset'
Add-Tested 'bundle add'
Add-Tested 'bundle remove'
Add-Tested 'bundle export'
Add-Tested 'bundle import'
Add-Tested 'bundle install'
Write-Stage 'Operation queue control'
$queuedOperationIds = @()
foreach ($queueFixture in @($manifest.queueOperations)) {
$queueDownload = Invoke-CliJson -Arguments @(
'package', 'download',
'--manager', [string]$queueFixture.manager,
'--id', [string]$queueFixture.packageId,
'--source', [string]$queueFixture.sourceName,
'--output', (Resolve-QueueOutputPath -QueueFixture $queueFixture),
'--wait', 'false'
)
if ($queueDownload.status -ne 'success' -or $queueDownload.completed -or [string]::IsNullOrWhiteSpace($queueDownload.operationId)) {
throw "package download --wait false did not return an in-progress operation payload for $($queueFixture.packageId): $($queueDownload | ConvertTo-Json -Depth 10)"
}
$queuedOperationIds += [string]$queueDownload.operationId
}
$queuedOperations = Wait-ForCliCondition `
-Arguments @('operation', 'list') `
-Condition {
param($response)
$targeted = @($response.operations | Where-Object { $queuedOperationIds -contains $_.id })
$targeted.Count -eq $queuedOperationIds.Count
} `
-FailureMessage 'operation list never reported the queued download operations.' `
-TimeoutSeconds 180 `
-DelaySeconds 2
$queuedOperation = @($queuedOperations.operations | Where-Object {
$queuedOperationIds -contains $_.id
})[0]
$operationDetails = Invoke-CliJson -Arguments @('operation', 'get', '--id', $queuedOperation.id)
if ($operationDetails.operation.id -ne $queuedOperation.id) {
throw "operation get did not return the requested queued operation id"
}
$operationOutput = Invoke-CliJson -Arguments @('operation', 'output', '--id', $queuedOperation.id, '--tail', '10')
if ($operationOutput.output.operationId -ne $queuedOperation.id) {
throw "operation output did not return the requested queued operation id"
}
foreach ($operationId in $queuedOperationIds) {
$waitedOperation = Invoke-CliJson -Arguments @('operation', 'wait', '--id', $operationId, '--timeout', '300', '--delay', '1')
if ($waitedOperation.operation.status -ne 'succeeded') {
throw "operation wait did not report success for operation ${operationId}: $($waitedOperation | ConvertTo-Json -Depth 10)"
}
}
foreach ($operationId in $queuedOperationIds) {
$completedOutput = Invoke-CliJson -Arguments @('operation', 'output', '--id', $operationId)
if ($completedOutput.output.lineCount -lt 0) {
throw "operation output reported an invalid line count for operation $operationId"
}
$forget = Invoke-CliJson -Arguments @('operation', 'forget', '--id', $operationId)
Assert-JsonCommandSucceeded -Response $forget -Label "operation forget $operationId"
}
$operationsAfterForget = Invoke-CliJson -Arguments @('operation', 'list')
if (@($operationsAfterForget.operations | Where-Object { $queuedOperationIds -contains $_.id }).Count -ne 0) {
throw "operation forget did not remove all queued download operations"
}
Add-Tested 'operation list'
Add-Tested 'operation get'
Add-Tested 'operation output'
Add-Tested 'operation wait'
Add-Tested 'operation forget'
Write-Stage 'Package lifecycle and updates'
$specificUpdateFixture = @($manifest.packageManagers | Where-Object { @($_.roles) -contains 'specific-update' })[0]
if ($null -eq $specificUpdateFixture) {
throw 'The CLI E2E manifest must define a package manager fixture with the specific-update role.'
}
$directInstallFixtures = @($manifest.packageManagers | Where-Object {
$null -ne $specificUpdateFixture -and [string]$_.manager -eq [string]$specificUpdateFixture.manager
})
$otherInstallFixtures = @($manifest.packageManagers | Where-Object {
[string]$_.manager -ne [string]$bundleFixture.manager -and
[string]$_.manager -ne [string]$specificUpdateFixture.manager
})
$directInstallFixtures += $otherInstallFixtures
$bundleLatestVersion = [string]$fixtureState[$bundleFixture.manager].latestVersion
foreach ($installFixture in $directInstallFixtures) {
$installResult = Invoke-CliJson -Arguments (@('package', 'install') + (Get-PackageArguments -Fixture $installFixture -IncludeVersion -IncludeSource))
Assert-JsonCommandSucceeded -Response $installResult -Label "package install $($installFixture.manager)"
Wait-ForInstalledPackage -Fixture $installFixture -ExpectedVersion $installFixture.installVersion | Out-Null
}
Add-Tested 'package install'
Add-Tested 'package installed'
$updateDiscoveryFixtures = @($manifest.packageManagers | Where-Object { @($_.roles) -contains 'update-discovery' })
foreach ($updateDiscoveryFixture in $updateDiscoveryFixtures) {
$reloadManager = Invoke-CliJson -Arguments @('manager', 'reload', '--manager', [string]$updateDiscoveryFixture.manager)
if ($reloadManager.operationStatus -ne 'completed') {
throw "manager reload before package updates did not complete successfully for $($updateDiscoveryFixture.manager): $($reloadManager | ConvertTo-Json -Depth 8)"
}
}
foreach ($updateDiscoveryFixture in $updateDiscoveryFixtures) {
Wait-ForPackageUpdateVisibility -Fixture $updateDiscoveryFixture | Out-Null
}
$allUpdates = Invoke-CliJson -Arguments @('package', 'updates')
if ($null -eq $allUpdates.updates) {
throw "package updates did not return an updates payload"
}
Add-Tested 'package updates'
$ignoredAdd = Invoke-CliJson -Arguments @('package', 'ignored', 'add', '--manager', [string]$bundleFixture.manager, '--id', [string]$bundleFixture.packageId)
Assert-JsonCommandSucceeded -Response $ignoredAdd -Label 'package ignored add'
$ignoredList = Invoke-CliJson -Arguments @('package', 'ignored', 'list')
if (@($ignoredList.ignoredUpdates | Where-Object { $_.packageId -eq $bundleFixture.packageId }).Count -eq 0) {
throw "package ignored list did not report the ignored dotnet-tool fixture"
}
$ignoredRemove = Invoke-CliJson -Arguments @('package', 'ignored', 'remove', '--manager', [string]$bundleFixture.manager, '--id', [string]$bundleFixture.packageId)
Assert-JsonCommandSucceeded -Response $ignoredRemove -Label 'package ignored remove'
Add-Tested 'package ignored list'
Add-Tested 'package ignored add'
Add-Tested 'package ignored remove'
$specificUpdate = Invoke-CliJson -Arguments (@(
'package', 'update'
) + (Get-PackageArguments -Fixture $specificUpdateFixture -IncludeSource) + @(
'--version', [string]$fixtureState[$specificUpdateFixture.manager].latestVersion
))
Assert-JsonCommandSucceeded -Response $specificUpdate -Label "package update $($specificUpdateFixture.manager)"
Wait-ForInstalledPackage -Fixture $specificUpdateFixture -ExpectedVersion ([string]$fixtureState[$specificUpdateFixture.manager].latestVersion) | Out-Null
Add-Tested 'package update'
$updateManager = Invoke-CliFailure -Arguments @('package', 'update-manager', '--manager', [string]$bundleFixture.manager)
if ($updateManager.Text -notmatch 'cannot update manager packages') {
throw "package update-manager did not report the expected headless limitation: $($updateManager.Text)"
}
Add-Tested 'package update-manager' 'expected-failure'
$updateAll = Invoke-CliFailure -Arguments @('package', 'update-all')
if ($updateAll.Text -notmatch 'cannot update all packages') {
throw "package update-all did not report the expected headless limitation: $($updateAll.Text)"
}
Add-Tested 'package update-all' 'expected-failure'
$reinstall = Invoke-CliJson -Arguments (@('package', 'reinstall') + (Get-PackageArguments -Fixture $bundleFixture -IncludeSource))
Assert-JsonCommandSucceeded -Response $reinstall -Label 'package reinstall'
Wait-ForInstalledPackage -Fixture $bundleFixture -ExpectedVersion $bundleLatestVersion | Out-Null
Add-Tested 'package reinstall'
$repair = Invoke-CliJson -Arguments (@('package', 'repair') + (Get-PackageArguments -Fixture $bundleFixture -IncludeSource))
Assert-JsonCommandSucceeded -Response $repair -Label 'package repair'
Wait-ForInstalledPackage -Fixture $bundleFixture -ExpectedVersion $bundleLatestVersion | Out-Null
Add-Tested 'package repair'
$installedAll = Invoke-CliJson -Arguments @('package', 'installed')
foreach ($fixture in @($manifest.packageManagers)) {
if (@($installedAll.packages | Where-Object { $_.id -eq $fixture.packageId }).Count -eq 0) {
throw "package installed did not report $($fixture.packageId) after lifecycle operations"
}
}
Write-Stage 'Logs'
$operationHistory = Invoke-CliJson -Arguments @('log', 'operations')
if ($null -eq $operationHistory.history) {
throw "log operations did not return a history payload"
}
$managerLog = Wait-ForCliCondition `
-Arguments @('log', 'manager', '--manager', [string]$bundleFixture.manager, '--verbose') `
-Condition {
param($response)
@(
$response.managers |
Where-Object {
$_.name -eq $bundleFixture.manager -and
@($_.tasks | Where-Object { @($_.lines | Where-Object { $_ -match $bundleFixture.packageId }).Count -gt 0 }).Count -gt 0
}
).Count -gt 0
} `
-FailureMessage "log manager did not capture package activity for $($bundleFixture.packageId)" `
-TimeoutSeconds 180 `
-DelaySeconds 3
Add-Tested 'log operations'
Add-Tested 'log manager'
Write-Stage 'Package uninstall'
$uninstallValidationFixtures = @($manifest.packageManagers)
if ($null -ne $manifest.PSObject.Properties['uninstallValidationManagers']) {
$uninstallValidationManagerNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($managerName in @($manifest.uninstallValidationManagers)) {
if (-not [string]::IsNullOrWhiteSpace([string]$managerName)) {
[void]$uninstallValidationManagerNames.Add([string]$managerName)
}
}
$uninstallValidationFixtures = @($manifest.packageManagers | Where-Object {
$uninstallValidationManagerNames.Contains([string]$_.manager)
})
}
foreach ($fixture in $uninstallValidationFixtures) {
$uninstall = Invoke-CliJson -Arguments (@('package', 'uninstall') + (Get-PackageArguments -Fixture $fixture -IncludeSource))
Assert-JsonCommandSucceeded -Response $uninstall -Label "package uninstall $($fixture.packageId)"
if (-not [string]::IsNullOrWhiteSpace($uninstall.operationId)) {
$waitedUninstall = Invoke-CliJson -Arguments @('operation', 'wait', '--id', [string]$uninstall.operationId, '--timeout', '300', '--delay', '1')
if ($waitedUninstall.operation.status -ne 'succeeded') {
throw "package uninstall did not complete successfully for $($fixture.packageId): $($waitedUninstall | ConvertTo-Json -Depth 10)"
}
}
$reloadManager = Invoke-CliJson -Arguments @('manager', 'reload', '--manager', [string]$fixture.manager)
if ($reloadManager.operationStatus -ne 'completed') {
throw "manager reload after uninstall did not complete successfully for $($fixture.manager): $($reloadManager | ConvertTo-Json -Depth 8)"
}
Wait-ForPackageRemoval -Fixture $fixture | Out-Null
}
if ($uninstallValidationFixtures.Count -gt 0) {
Add-Tested 'package uninstall'
}
Write-Stage 'Settings reset and shutdown'
$clearFreshValue = Invoke-CliJson -Arguments @('settings', 'clear', '--key', 'FreshValue')
if ($clearFreshValue.setting.isSet) {
throw "settings clear did not clear FreshValue"
}
$disableFreshBool = Invoke-CliJson -Arguments @('settings', 'set', '--key', 'FreshBoolSetting', '--enabled', 'false')
if ($disableFreshBool.setting.boolValue) {
throw "settings set did not disable FreshBoolSetting"
}
Add-Tested 'settings clear'
$resetSettings = Invoke-CliJson -Arguments @('settings', 'reset')
Assert-JsonCommandSucceeded -Response $resetSettings -Label 'settings reset'
Add-Tested 'settings reset'
$postResetStatus = Invoke-CliJson -Arguments @('status')
if (-not $postResetStatus.running) {
throw "settings reset broke the active IPC session"
}
$quitApp = Invoke-CliJson -Arguments @('app', 'quit')
Assert-JsonCommandSucceeded -Response $quitApp -Label 'app quit'
Add-Tested 'app quit'
$quitDeadline = (Get-Date).AddSeconds(30)
while (-not $process.HasExited -and (Get-Date) -lt $quitDeadline) {
Start-Sleep -Seconds 1
}
if (-not $process.HasExited) {
throw "app quit did not stop the headless daemon"
}
$gracefulShutdown = $true
}
finally {
$coverage.status = if ($gracefulShutdown) { 'success' } else { 'failed' }
Set-Content -Path $coveragePath -Value ($coverage | ConvertTo-Json -Depth 12) -Encoding UTF8
if (-not $gracefulShutdown) {
Stop-Daemon
}
$daemonLog = Get-DaemonLog
if (-not [string]::IsNullOrWhiteSpace($daemonLog)) {
Write-Host '--- Headless daemon log ---'
Write-Host $daemonLog
}
if (-not $preserveArtifacts) {
Remove-Item -Recurse -Force $daemonRoot -ErrorAction SilentlyContinue
}
}