Files
aego/tools/ci/windows-user-path-test.ps1

260 lines
12 KiB
PowerShell

$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '../release/windows/user-path.ps1')
function Assert-Equal($Actual, $Expected, [string]$Message) {
if ($Actual -cne $Expected) {
throw "${Message}: expected [$Expected], got [$Actual]"
}
}
$dir = "C:\Users\O'Brien\Aego"
foreach ($before in @('', 'C:\Tools', 'C:\Tools;', 'C:\Tools;;', ';;', '%USERPROFILE%\tools;C:\Bin', ';C:\Bin;;C:\Other;')) {
$installed = Add-AegoPath $before $dir
if (-not $installed.Added) { throw "Expected a new entry for [$before]" }
$removed = Remove-AegoPath $installed.Value $dir
if (-not $removed.Removed) { throw "Failed to remove owned entry for [$before]" }
Assert-Equal $removed.Value $before 'Install/uninstall must preserve the original string'
}
$random = [Random]::new(125)
$segments = @('', 'C:\Tools', '%USERPROFILE%\bin', 'C:\Has Space', 'C:\Other')
for ($trial = 0; $trial -lt 500; $trial++) {
$parts = [System.Collections.Generic.List[string]]::new()
$count = $random.Next(1, 12)
for ($i = 0; $i -lt $count; $i++) { $parts.Add($segments[$random.Next($segments.Count)]) }
$before = [string]::Join(';', $parts)
$added = Add-AegoPath $before $dir
$removed = Remove-AegoPath $added.Value $dir
if (-not $added.Added -or -not $removed.Removed -or $removed.Value -cne $before) {
throw "Randomized PATH round trip failed on trial $trial"
}
}
$before = 'C:\Tools;C:\USERS\O''BRIEN\AEGO;;C:\Other;'
$unchanged = Add-AegoPath $before $dir
if ($unchanged.Added) { throw 'Existing entries must not become installer-owned.' }
Assert-Equal $unchanged.Value $before 'Existing entry changed'
$installed = Add-AegoPath 'C:\Tools;' $dir
$edited = $installed.Value + ';C:\New;;'
$removed = Remove-AegoPath $edited $dir
Assert-Equal $removed.Value 'C:\Tools;;C:\New;;' 'Uninstall discarded a later user edit'
$duplicate = Remove-AegoPath ($installed.Value + ';' + $dir) $dir
Assert-Equal $duplicate.Value ('C:\Tools;;' + $dir) 'Uninstall removed a later duplicate'
$absent = Remove-AegoPath 'C:\Tools;;' $dir
if ($absent.Removed) { throw 'Absent entry was reported as removed.' }
Assert-Equal $absent.Value 'C:\Tools;;' 'Absent entry changed PATH'
$refused = $false
try { Add-AegoPath 'C:\Tools' 'C:\bad;segment' | Out-Null } catch { $refused = $true }
if (-not $refused) { throw 'A directory containing a semicolon must be rejected.' }
# Exercise the actual registry algorithm with isolated in-memory keys. The native
# installer smoke additionally checks real HKCU on a GitHub-hosted Windows VM.
class FakeKey {
[hashtable]$Values = @{}
[hashtable]$Kinds = @{}
[string]$FailSetName = ''
[object]$FailSetValue = $null
[string[]] GetValueNames() { return [string[]]@($this.Values.Keys) }
[object] GetValue([string]$name, [object]$fallback) {
if ($this.Values.ContainsKey($name)) { return $this.Values[$name] }
return $fallback
}
[object] GetValue([string]$name, [object]$fallback, [object]$options) { return $this.GetValue($name, $fallback) }
[Microsoft.Win32.RegistryValueKind] GetValueKind([string]$name) { return $this.Kinds[$name] }
[void] SetValue([string]$name, [object]$value, [Microsoft.Win32.RegistryValueKind]$kind) {
if ($name -eq $this.FailSetName -and $value -eq $this.FailSetValue) { throw 'Simulated registry write failure.' }
$this.Values[$name] = $value
$this.Kinds[$name] = $kind
}
[void] DeleteValue([string]$name, [bool]$throwOnMissingValue) {
$this.Values.Remove($name)
$this.Kinds.Remove($name)
}
}
foreach ($scenario in @('absent', 'empty', 'trailing')) {
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$original = ''
if ($scenario -eq 'empty') { $envKey.SetValue('Path', '', [Microsoft.Win32.RegistryValueKind]::String) }
if ($scenario -eq 'trailing') {
$original = '%USERPROFILE%\tools;;C:\Other;'
$envKey.SetValue('Path', $original, [Microsoft.Win32.RegistryValueKind]::ExpandString)
}
Invoke-AegoPathUpdate Install $dir $envKey $state
Invoke-AegoPathUpdate Install $dir $envKey $state # update must keep ownership
Invoke-AegoPathUpdate Uninstall $dir $envKey $state
$after = Read-AegoUserPath $envKey
Assert-Equal $after.Value $original "$scenario registry round trip"
if ($after.Present -ne ($scenario -ne 'absent')) { throw "$scenario changed Path value presence" }
if ($scenario -eq 'empty' -and $after.Kind -ne [Microsoft.Win32.RegistryValueKind]::String) { throw 'Empty PATH type changed.' }
}
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$original = 'C:\Tools;' + $dir + ';;'
$envKey.SetValue('Path', $original, [Microsoft.Win32.RegistryValueKind]::ExpandString)
Invoke-AegoPathUpdate Install $dir $envKey $state
Invoke-AegoPathUpdate Uninstall $dir $envKey $state
Assert-Equal (Read-AegoUserPath $envKey).Value $original 'Pre-existing user entry was removed'
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$envKey.SetValue('Path', 'C:\Tools;', [Microsoft.Win32.RegistryValueKind]::String)
Invoke-AegoPathUpdate Install $dir $envKey $state
$edited = (Read-AegoUserPath $envKey).Value + ';C:\UserAdded;'
$envKey.SetValue('Path', $edited, [Microsoft.Win32.RegistryValueKind]::String)
Invoke-AegoPathUpdate Uninstall $dir $envKey $state
Assert-Equal (Read-AegoUserPath $envKey).Value 'C:\Tools;;C:\UserAdded;' 'Later user PATH changes were lost'
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
Invoke-AegoPathUpdate Install $dir $envKey $state
$newDir = 'C:\Another\Aego'
Invoke-AegoPathUpdate Install $newDir $envKey $state
Assert-Equal (Read-AegoUserPath $envKey).Value $newDir 'Moving the install left its old PATH entry'
Invoke-AegoPathUpdate Uninstall $newDir $envKey $state
if ((Read-AegoUserPath $envKey).Present) { throw 'Moved install did not restore absent PATH.' }
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
Invoke-AegoPathUpdate Install $dir $envKey $state
$envKey.SetValue('Path', ($dir + ';'), [Microsoft.Win32.RegistryValueKind]::String)
Invoke-AegoPathUpdate Uninstall $dir $envKey $state
$after = Read-AegoUserPath $envKey
if (-not $after.Present -or $after.Value -cne '' -or $after.Kind -ne [Microsoft.Win32.RegistryValueKind]::String) {
throw 'A later empty PATH entry or registry type was lost.'
}
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$envKey.SetValue('Path', '', [Microsoft.Win32.RegistryValueKind]::String)
Invoke-AegoPathUpdate Install $dir $envKey $state
$envKey.DeleteValue('Path', $false) # user removes the owned entry and the value
Invoke-AegoPathUpdate Install $dir $envKey $state
Invoke-AegoPathUpdate Uninstall $dir $envKey $state
if ((Read-AegoUserPath $envKey).Present) { throw 'Update ignored the user removing PATH.' }
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$envKey.SetValue('Path', '', [Microsoft.Win32.RegistryValueKind]::String)
Invoke-AegoPathUpdate Install $dir $envKey $state
$envKey.SetValue('Path', $dir, [Microsoft.Win32.RegistryValueKind]::ExpandString)
Invoke-AegoPathUpdate Uninstall $dir $envKey $state
$after = Read-AegoUserPath $envKey
if (-not $after.Present -or $after.Kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString) {
throw 'A later registry type change was lost.'
}
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$envKey.SetValue('Path', 'unsupported', [Microsoft.Win32.RegistryValueKind]::Binary)
$refused = $false
try { Invoke-AegoPathUpdate Install $dir $envKey $state } catch { $refused = $true }
if (-not $refused -or $envKey.GetValue('Path', '') -cne 'unsupported' -or $state.Values.Count -ne 0) {
throw 'An unsupported PATH type was not refused safely.'
}
Invoke-AegoPathUpdate Uninstall $dir $envKey $state
if ($envKey.GetValue('Path', '') -cne 'unsupported') { throw 'Unowned uninstall changed unsupported PATH data.' }
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$envKey.SetValue('Path', 'C:\Tools;', [Microsoft.Win32.RegistryValueKind]::String)
$state.FailSetName = 'PathOwned'
$state.FailSetValue = 1
$refused = $false
try { Invoke-AegoPathUpdate Install $dir $envKey $state } catch { $refused = $true }
$after = Read-AegoUserPath $envKey
if (-not $refused -or $after.Value -cne 'C:\Tools;' -or $after.Kind -ne [Microsoft.Win32.RegistryValueKind]::String -or $state.GetValue('PathOwned', 0) -ne 0) {
throw 'A failed ownership write did not roll back the PATH edit.'
}
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$envKey.SetValue('Path', 'C:\Tools;', [Microsoft.Win32.RegistryValueKind]::String)
$envKey.FailSetName = 'Path'
$envKey.FailSetValue = 'C:\Tools;;' + $dir
$refused = $false
try { Invoke-AegoPathUpdate Install $dir $envKey $state } catch { $refused = $true }
if (-not $refused -or (Read-AegoUserPath $envKey).Value -cne 'C:\Tools;' -or $state.GetValue('PathOwned', 0) -ne 0) {
throw 'A failed PATH write changed the existing value or claimed ownership.'
}
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
$envKey.SetValue('Path', 'C:\Tools;;', [Microsoft.Win32.RegistryValueKind]::String)
$state.SetValue('PathOwned', 1, [Microsoft.Win32.RegistryValueKind]::DWord)
$state.SetValue('PathDir', '', [Microsoft.Win32.RegistryValueKind]::String)
$refused = $false
try { Invoke-AegoPathUpdate Uninstall $dir $envKey $state } catch { $refused = $true }
if (-not $refused -or (Read-AegoUserPath $envKey).Value -cne 'C:\Tools;;') {
throw 'Corrupt ownership metadata was allowed to alter PATH.'
}
$envKey = [FakeKey]::new()
$state = [FakeKey]::new()
Invoke-AegoPathUpdate Install $dir $envKey $state
$refused = $false
try { Invoke-AegoPathUpdate Uninstall 'C:\Another\Aego' $envKey $state } catch { $refused = $true }
if (-not $refused -or (Read-AegoUserPath $envKey).Value -cne $dir -or $state.GetValue('PathOwned', 0) -ne 1) {
throw 'Uninstalling a different directory changed an owned entry.'
}
Write-Host 'Windows user PATH string and registry-state tests passed.'
# On Windows, exercise the same algorithm with real registry keys under a fresh
# random HKCU subkey. These keys are not the actual user's environment or Aego
# installation; only the exact test key created here is removed afterwards.
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) {
$testPath = 'Software\AegoCiPathTest-' + [guid]::NewGuid().ToString('N')
$existing = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($testPath)
if ($null -ne $existing) {
$existing.Dispose()
throw 'Refusing to reuse an existing registry test key.'
}
$testRoot = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($testPath)
try {
foreach ($scenario in @('absent', 'empty', 'trailing', 'preexisting', 'edited')) {
$scenarioKey = $testRoot.CreateSubKey($scenario)
$envKey = $scenarioKey.CreateSubKey('Environment')
$state = $scenarioKey.CreateSubKey('State')
try {
$original = ''
$originalKind = [Microsoft.Win32.RegistryValueKind]::ExpandString
if ($scenario -eq 'empty') {
$originalKind = [Microsoft.Win32.RegistryValueKind]::String
$envKey.SetValue('Path', '', $originalKind)
} elseif ($scenario -eq 'trailing' -or $scenario -eq 'edited') {
$original = '%USERPROFILE%\tools;;C:\Other;'
$envKey.SetValue('Path', $original, $originalKind)
} elseif ($scenario -eq 'preexisting') {
$original = 'C:\Tools;' + $dir + ';;'
$envKey.SetValue('Path', $original, $originalKind)
}
Invoke-AegoPathUpdate Install $dir $envKey $state
Invoke-AegoPathUpdate Install $dir $envKey $state
if ($scenario -eq 'edited') {
$envKey.SetValue('Path', ((Read-AegoUserPath $envKey).Value + ';C:\Added;'), $originalKind)
$original += ';C:\Added;'
}
Invoke-AegoPathUpdate Uninstall $dir $envKey $state
$after = Read-AegoUserPath $envKey
if ($after.Present -ne ($scenario -ne 'absent') -or $after.Value -cne $original -or ($after.Present -and $after.Kind -ne $originalKind)) {
throw "Real registry PATH round trip failed: $scenario"
}
} finally {
$state.Dispose()
$envKey.Dispose()
$scenarioKey.Dispose()
}
}
} finally {
$testRoot.Dispose()
[Microsoft.Win32.Registry]::CurrentUser.DeleteSubKeyTree($testPath)
}
Write-Host 'Isolated real HKCU PATH tests passed.'
}