f748173809
Allow the stable launcher path to be replaced while an existing Jcode process is still using the previous executable. Preserve rollback behavior, clean stale launcher backups during later installs and uninstall, and cover live replacement plus injected final-move failure cases.
466 lines
26 KiB
PowerShell
466 lines
26 KiB
PowerShell
param()
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
$repoRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
$installScript = Join-Path $repoRoot 'scripts\install.ps1'
|
|
$uninstallScript = Join-Path $repoRoot 'scripts\uninstall.ps1'
|
|
$testRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("jcode-windows-setup-eval-{0}" -f ([guid]::NewGuid().ToString('N')))
|
|
|
|
$envNames = @(
|
|
'LOCALAPPDATA',
|
|
'APPDATA',
|
|
'USERPROFILE',
|
|
'JCODE_HOME',
|
|
'TEMP',
|
|
'TMP',
|
|
'PATH',
|
|
'JCODE_INSTALL_PS1_IMPORT_ONLY',
|
|
'JCODE_UNINSTALL_PS1_IMPORT_ONLY',
|
|
'JCODE_SKIP_SERVER_RELOAD',
|
|
'JCODE_INSTALL_SKIP_BINARY_VALIDATION',
|
|
'JCODE_DISABLE_ENV_BROADCAST',
|
|
'JCODE_WINDOWS_SETUP_SKIP_EXTERNALS'
|
|
)
|
|
$originalEnv = @{}
|
|
foreach ($name in $envNames) {
|
|
$originalEnv[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
|
|
}
|
|
|
|
function Restore-TestEnvironment {
|
|
foreach ($name in $script:envNames) {
|
|
$value = $script:originalEnv[$name]
|
|
if ($null -eq $value) {
|
|
[Environment]::SetEnvironmentVariable($name, $null, 'Process')
|
|
} else {
|
|
[Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
|
}
|
|
}
|
|
}
|
|
|
|
function Assert-True($Condition, [string]$Message) {
|
|
if (-not $Condition) { throw $Message }
|
|
}
|
|
|
|
function Assert-False($Condition, [string]$Message) {
|
|
if ($Condition) { throw $Message }
|
|
}
|
|
|
|
function Assert-Equal($Expected, $Actual, [string]$Message) {
|
|
if ($Expected -ne $Actual) {
|
|
throw "$Message`nExpected: $Expected`nActual: $Actual"
|
|
}
|
|
}
|
|
|
|
function Assert-Contains([string]$Haystack, [string]$Needle, [string]$Message) {
|
|
if (-not $Haystack.Contains($Needle)) {
|
|
throw "$Message`nMissing: $Needle"
|
|
}
|
|
}
|
|
|
|
function Assert-NotContains([string]$Haystack, [string]$Needle, [string]$Message) {
|
|
if ($Haystack.Contains($Needle)) {
|
|
throw "$Message`nUnexpected: $Needle"
|
|
}
|
|
}
|
|
|
|
function Assert-PathExists([string]$Path, [string]$Message) {
|
|
Assert-True (Test-Path -LiteralPath $Path) $Message
|
|
}
|
|
|
|
function Assert-PathMissing([string]$Path, [string]$Message) {
|
|
Assert-False (Test-Path -LiteralPath $Path) $Message
|
|
}
|
|
|
|
function Assert-PathCount([string]$PathValue, [string]$Entry, [int]$ExpectedCount, [string]$Message) {
|
|
$entryKey = ConvertTo-JcodePathKey $Entry
|
|
$count = 0
|
|
foreach ($candidate in (Split-JcodePathList $PathValue)) {
|
|
if ((ConvertTo-JcodePathKey $candidate) -eq $entryKey) { $count += 1 }
|
|
}
|
|
Assert-Equal $ExpectedCount $count $Message
|
|
}
|
|
|
|
function Invoke-Case([string]$Name, [scriptblock]$Body) {
|
|
Write-Host "CASE $Name"
|
|
& $Body
|
|
$script:passedCases += 1
|
|
}
|
|
|
|
function New-IsolatedWindowsProfile([string]$Name) {
|
|
$root = Join-Path $script:testRoot $Name
|
|
$local = Join-Path $root 'Local App Data'
|
|
$profile = Join-Path $root 'User Profile'
|
|
$appData = Join-Path $profile 'AppData\Roaming'
|
|
$jcodeHome = Join-Path $root 'Jcode Home'
|
|
$temp = Join-Path $root 'Temp'
|
|
foreach ($dir in @($local, $profile, $appData, $jcodeHome, $temp)) {
|
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
}
|
|
|
|
$env:LOCALAPPDATA = $local
|
|
$env:USERPROFILE = $profile
|
|
$env:APPDATA = $appData
|
|
$env:JCODE_HOME = $jcodeHome
|
|
$env:TEMP = $temp
|
|
$env:TMP = $temp
|
|
|
|
return [pscustomobject]@{
|
|
Root = $root
|
|
LocalAppData = $local
|
|
UserProfile = $profile
|
|
AppData = $appData
|
|
JcodeHome = $jcodeHome
|
|
Temp = $temp
|
|
InstallDir = Join-Path $local 'jcode\bin'
|
|
LauncherPath = Join-Path $local 'jcode\bin\jcode.exe'
|
|
BuildsDir = Join-Path $local 'jcode\builds'
|
|
SetupHintsPath = Join-Path $jcodeHome 'setup_hints.json'
|
|
HotkeyDir = Join-Path $jcodeHome 'hotkey'
|
|
StartupShortcutPath = Join-Path $appData 'Microsoft\Windows\Start Menu\Programs\Startup\jcode-hotkey.lnk'
|
|
}
|
|
}
|
|
|
|
function Set-InstallScriptProfileGlobals($Profile) {
|
|
$script:JcodeHome = $Profile.JcodeHome
|
|
$script:HotkeyDir = $Profile.HotkeyDir
|
|
$script:SetupHintsPath = $Profile.SetupHintsPath
|
|
}
|
|
|
|
function Get-CimInstance { @() }
|
|
|
|
$passedCases = 0
|
|
$coveredScenarios = [ordered]@{
|
|
clean_install = $false
|
|
upgrade_idempotency = $false
|
|
path_persistence_deduplication = $false
|
|
wm_settingchange = $false
|
|
copilot_key_mapping = $false
|
|
opt_out = $false
|
|
uninstall_cleanup = $false
|
|
spaces_non_ascii_paths = $false
|
|
missing_windows_terminal = $false
|
|
rollback_failure = $false
|
|
}
|
|
New-Item -ItemType Directory -Path $testRoot -Force | Out-Null
|
|
|
|
try {
|
|
$env:JCODE_INSTALL_PS1_IMPORT_ONLY = '1'
|
|
$env:JCODE_SKIP_SERVER_RELOAD = '1'
|
|
$env:JCODE_INSTALL_SKIP_BINARY_VALIDATION = '1'
|
|
. $installScript -SkipAlacrittySetup -SkipHotkeySetup
|
|
|
|
Invoke-Case 'release_lookup_avoids_unauthenticated_github_api' {
|
|
Assert-Equal 'v1.2.3' (Resolve-JcodeReleaseTagFromUri 'https://github.com/1jehuang/jcode/releases/tag/v1.2.3') 'release redirect parser should extract the stable tag'
|
|
Assert-Equal 'v1.2.3-rc.1' (Resolve-JcodeReleaseTagFromUri 'https://github.com/1jehuang/jcode/releases/tag/v1.2.3-rc.1?source=latest') 'release redirect parser should stop before query parameters'
|
|
Assert-Equal $true (Test-JcodeReleaseTag 'v1.2.3') 'stable release tags should validate'
|
|
Assert-Equal $false (Test-JcodeReleaseTag 'latest') 'unversioned release labels should not validate'
|
|
$scriptText = Get-Content -LiteralPath $installScript -Raw
|
|
Assert-NotContains $scriptText 'api.github.com/repos/$Repo/releases/latest' 'installer should not use the rate-limited unauthenticated GitHub API'
|
|
Assert-Contains $scriptText 'jcode.sh/releases' 'installer should include independent static release metadata'
|
|
|
|
$script:releaseLookupRequests = @()
|
|
function Invoke-WebRequest {
|
|
param(
|
|
[switch]$UseBasicParsing,
|
|
[string]$Method,
|
|
[Parameter(Mandatory = $true)][string]$Uri,
|
|
[string]$OutFile
|
|
)
|
|
$script:releaseLookupRequests += $Uri
|
|
if ($Uri -eq 'https://jcode.sh/releases/latest/version') {
|
|
return [pscustomobject]@{ Content = "v1.2.3`n" }
|
|
}
|
|
if ($Uri -eq 'https://jcode.sh/releases/v1.2.3/download-bases') {
|
|
return [pscustomobject]@{ Content = "https://mirror.example/releases/v1.2.3`n" }
|
|
}
|
|
if ($Uri -eq 'https://github.com/1jehuang/jcode/releases/latest') {
|
|
throw 'simulated GitHub block'
|
|
}
|
|
throw "unexpected URI: $Uri"
|
|
}
|
|
try {
|
|
Assert-Equal 'v1.2.3' (Get-LatestJcodeReleaseTag) 'static metadata should cover a blocked GitHub release lookup'
|
|
$bases = @(Get-JcodeReleaseDownloadBases 'v1.2.3')
|
|
Assert-Equal 'https://mirror.example/releases/v1.2.3' $bases[0] 'configured mirror should be preferred'
|
|
Assert-Equal 'https://github.com/1jehuang/jcode/releases/download/v1.2.3' $bases[1] 'GitHub should remain the final fallback'
|
|
} finally {
|
|
Remove-Item Function:\Invoke-WebRequest -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
Invoke-Case 'path_persistence_and_deduplication' {
|
|
$profile = New-IsolatedWindowsProfile 'path-dedupe'
|
|
$installVariant = ($profile.InstallDir.ToUpperInvariant() + '\')
|
|
$currentPath = "C:\Tools;$installVariant;$($profile.InstallDir);C:\Tools\;C:\Other"
|
|
$pathUpdate = Resolve-JcodePathUpdate -InstallDir $profile.InstallDir -CurrentPath $currentPath
|
|
Assert-Equal "$($profile.InstallDir);C:\Tools;C:\Tools\;C:\Other" $pathUpdate.Path 'install PATH update should prepend the canonical launcher dir without rewriting unrelated entries'
|
|
Assert-PathCount $pathUpdate.Path $profile.InstallDir 1 'updated PATH should contain exactly one jcode launcher dir'
|
|
Assert-Equal 2 $pathUpdate.RemovedManagedEntries 'PATH update should remove both stale jcode launcher entries before re-adding one'
|
|
Assert-Equal 0 $pathUpdate.RemovedDuplicateEntries 'PATH update should preserve unrelated duplicate entries'
|
|
|
|
$script:setCalls = 0
|
|
$script:broadcastCalls = 0
|
|
$script:appliedPath = $null
|
|
$setPathAction = { param($value) $script:setCalls += 1; $script:appliedPath = $value }
|
|
$broadcastAction = { $script:broadcastCalls += 1 }
|
|
$first = Set-JcodeUserPath -InstallDir $profile.InstallDir -CurrentPath 'C:\Tools' -SetUserPathAction $setPathAction -BroadcastAction $broadcastAction
|
|
Assert-Equal 1 $script:setCalls 'user PATH setter should be called once when PATH changes'
|
|
Assert-Equal 1 $script:broadcastCalls 'environment broadcast should be called once when PATH changes'
|
|
Assert-Equal $true $first.Broadcasted 'changed PATH update should report a broadcast'
|
|
$second = Set-JcodeUserPath -InstallDir $profile.InstallDir -CurrentPath $script:appliedPath -SetUserPathAction $setPathAction -BroadcastAction $broadcastAction
|
|
Assert-Equal 1 $script:setCalls 'user PATH setter should not be called when PATH is already correct'
|
|
Assert-Equal 1 $script:broadcastCalls 'environment broadcast should not be called when PATH is unchanged'
|
|
Assert-Equal $false $second.Broadcasted 'unchanged PATH update should not report a broadcast'
|
|
$script:coveredScenarios.path_persistence_deduplication = $true
|
|
}
|
|
|
|
Invoke-Case 'wm_settingchange_broadcast_contract' {
|
|
$text = Get-Content -LiteralPath $installScript -Raw
|
|
Assert-Contains $text '0x001A' 'installer should broadcast WM_SETTINGCHANGE'
|
|
Assert-Contains $text '"Environment"' 'installer should broadcast the Environment lParam'
|
|
Assert-Contains $text 'SendMessageTimeout([IntPtr]0xffff' 'installer should broadcast to HWND_BROADCAST'
|
|
$env:JCODE_DISABLE_ENV_BROADCAST = '1'
|
|
Assert-Equal $false (Send-JcodeEnvironmentChangedBroadcast) 'broadcast helper should honor the deterministic no-mutate opt-out'
|
|
$env:JCODE_DISABLE_ENV_BROADCAST = $null
|
|
$script:coveredScenarios.wm_settingchange = $true
|
|
}
|
|
|
|
$script:mockUserPath = $null
|
|
$script:pathWrites = 0
|
|
$script:pathBroadcasts = 0
|
|
function Set-JcodeUserPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$InstallDir,
|
|
[AllowNull()][string]$CurrentPath,
|
|
[scriptblock]$SetUserPathAction,
|
|
[scriptblock]$BroadcastAction,
|
|
[bool]$Broadcast = $true
|
|
)
|
|
$update = Resolve-JcodePathUpdate -InstallDir $InstallDir -CurrentPath $script:mockUserPath
|
|
if ($update.Changed) {
|
|
$script:mockUserPath = $update.Path
|
|
$script:pathWrites += 1
|
|
if ($Broadcast) { $script:pathBroadcasts += 1 }
|
|
}
|
|
$update | Add-Member -NotePropertyName Broadcasted -NotePropertyValue ([bool]($update.Changed -and $Broadcast))
|
|
return $update
|
|
}
|
|
function Test-AlacrittyInstalled { return $false }
|
|
function Find-AlacrittyPath { return $null }
|
|
|
|
Invoke-Case 'clean_install_isolated_profile_and_opt_out' {
|
|
$profile = New-IsolatedWindowsProfile 'clean-install'
|
|
Set-InstallScriptProfileGlobals $profile
|
|
$source = Join-Path $profile.Root 'jcode-v1.exe'
|
|
Set-Content -Path $source -Value 'version-one' -NoNewline
|
|
$script:mockUserPath = 'C:\Tools'
|
|
$script:pathWrites = 0
|
|
$script:pathBroadcasts = 0
|
|
$script:InstallDir = $profile.InstallDir
|
|
$script:Version = 'v0.0.1-eval'
|
|
$script:ArtifactExePath = $source
|
|
$script:ArtifactTgzPath = $null
|
|
$script:SkipAlacrittySetup = $true
|
|
$script:SkipHotkeySetup = $true
|
|
|
|
Invoke-JcodeInstall
|
|
|
|
Assert-PathExists $profile.LauncherPath 'clean install should create the launcher in the isolated LOCALAPPDATA tree'
|
|
Assert-Equal 'version-one' (Get-Content -LiteralPath $profile.LauncherPath -Raw) 'launcher should contain the local artifact contents'
|
|
Assert-PathExists (Join-Path $profile.BuildsDir 'stable\jcode.exe') 'clean install should populate the stable build channel'
|
|
Assert-PathExists (Join-Path $profile.BuildsDir 'versions\0.0.1-eval\jcode.exe') 'clean install should populate the immutable versioned build'
|
|
Assert-PathCount $script:mockUserPath $profile.InstallDir 1 'clean install should persist exactly one launcher PATH entry in the mocked user PATH'
|
|
Assert-Equal 1 $script:pathWrites 'clean install should write mocked user PATH once'
|
|
Assert-Equal 1 $script:pathBroadcasts 'clean install should broadcast exactly once for a PATH change'
|
|
Assert-PathExists $profile.SetupHintsPath 'clean install should write setup hints into isolated JCODE_HOME'
|
|
$state = Get-Content -LiteralPath $profile.SetupHintsPath -Raw | ConvertFrom-Json
|
|
Assert-Equal $false $state.hotkey_configured 'SkipHotkeySetup should record hotkey opt-out without configuring the listener'
|
|
Assert-Equal $false $state.alacritty_configured 'SkipAlacrittySetup should avoid deterministic terminal installation side effects'
|
|
Assert-PathMissing $profile.HotkeyDir 'hotkey opt-out should not create hotkey files'
|
|
$script:coveredScenarios.clean_install = $true
|
|
$script:coveredScenarios.opt_out = $true
|
|
}
|
|
|
|
Invoke-Case 'upgrade_and_idempotency_do_not_duplicate_path' {
|
|
$profile = New-IsolatedWindowsProfile 'upgrade-install'
|
|
Set-InstallScriptProfileGlobals $profile
|
|
$sourceV1 = Join-Path $profile.Root 'jcode-v1.exe'
|
|
$sourceV2 = Join-Path $profile.Root 'jcode-v2.exe'
|
|
Set-Content -Path $sourceV1 -Value 'version-one' -NoNewline
|
|
Set-Content -Path $sourceV2 -Value 'version-two' -NoNewline
|
|
$script:mockUserPath = 'C:\Tools'
|
|
$script:pathWrites = 0
|
|
$script:pathBroadcasts = 0
|
|
$script:InstallDir = $profile.InstallDir
|
|
$script:SkipAlacrittySetup = $true
|
|
$script:SkipHotkeySetup = $true
|
|
|
|
$script:Version = 'v1.0.0-eval'
|
|
$script:ArtifactExePath = $sourceV1
|
|
Invoke-JcodeInstall
|
|
$writesAfterFirstInstall = $script:pathWrites
|
|
$broadcastsAfterFirstInstall = $script:pathBroadcasts
|
|
|
|
$script:Version = 'v1.0.1-eval'
|
|
$script:ArtifactExePath = $sourceV2
|
|
Invoke-JcodeInstall
|
|
Assert-Equal 'version-two' (Get-Content -LiteralPath $profile.LauncherPath -Raw) 'upgrade should replace launcher contents with the new build'
|
|
Assert-PathCount $script:mockUserPath $profile.InstallDir 1 'upgrade should preserve exactly one launcher PATH entry'
|
|
Assert-Equal $writesAfterFirstInstall $script:pathWrites 'upgrade should not rewrite PATH when it is already correct'
|
|
Assert-Equal $broadcastsAfterFirstInstall $script:pathBroadcasts 'upgrade should not rebroadcast when PATH is unchanged'
|
|
|
|
Invoke-JcodeInstall
|
|
Assert-PathCount $script:mockUserPath $profile.InstallDir 1 'reinstalling same version should remain idempotent for PATH'
|
|
Assert-Equal $writesAfterFirstInstall $script:pathWrites 'idempotent reinstall should not write PATH again'
|
|
$script:coveredScenarios.upgrade_idempotency = $true
|
|
}
|
|
|
|
Invoke-Case 'copilot_key_mapping_and_spaces_non_ascii_paths' {
|
|
$profile = New-IsolatedWindowsProfile 'hotkey-spaces-nonascii'
|
|
Set-InstallScriptProfileGlobals $profile
|
|
$env:JCODE_WINDOWS_SETUP_SKIP_EXTERNALS = '1'
|
|
$jcodeExe = Join-Path $profile.Root '路径 With Spaces\jcode.exe'
|
|
New-Item -ItemType Directory -Path (Split-Path -Parent $jcodeExe) -Force | Out-Null
|
|
Set-Content -Path $jcodeExe -Value 'fake exe' -NoNewline
|
|
New-Item -ItemType Directory -Path $profile.HotkeyDir -Force | Out-Null
|
|
Set-Content -Path (Join-Path $profile.HotkeyDir 'jcode-hotkey.ps1') -Value 'legacy listener' -Force
|
|
|
|
$ok = Install-JcodeHotkey -JcodeExePath $jcodeExe
|
|
Assert-Equal $true $ok 'hotkey install should succeed using the deterministic external-command skip hook'
|
|
$vbsPath = Join-Path $profile.HotkeyDir 'jcode-hotkey-launcher.vbs'
|
|
Assert-PathMissing $vbsPath 'hotkey install should remove the legacy hidden VBScript trampoline'
|
|
$shortcutScriptPath = Join-Path $profile.HotkeyDir 'jcode-hotkey-shortcut.ps1'
|
|
Assert-PathExists $shortcutScriptPath 'hotkey install should render the deterministic Startup shortcut script under the isolated JCODE_HOME'
|
|
$shortcutScript = Get-Content -LiteralPath $shortcutScriptPath -Raw
|
|
Assert-Contains $shortcutScript 'powershell.exe' 'Startup shortcut should target PowerShell directly'
|
|
Assert-Contains $shortcutScript 'ExecutionPolicy RemoteSigned' 'Startup shortcut should use RemoteSigned execution policy'
|
|
Assert-NotContains $shortcutScript 'ExecutionPolicy Bypass' 'Startup shortcut should not bypass execution policy'
|
|
Assert-Contains $shortcutScript 'setup-hotkey --listen-windows-hotkey' 'Startup shortcut should start the native Windows hotkey listener'
|
|
Assert-Contains $shortcutScript $jcodeExe 'Startup shortcut should preserve spaces and non-ASCII characters in the jcode path'
|
|
Assert-PathMissing (Join-Path $profile.HotkeyDir 'jcode-hotkey.ps1') 'hotkey upgrade should remove the legacy PowerShell listener'
|
|
$scriptText = Get-Content -LiteralPath $installScript -Raw
|
|
Assert-Contains $scriptText 'Configured Alt+; and the Copilot key' 'installer should document both Windows launch-key mappings'
|
|
$script:coveredScenarios.copilot_key_mapping = $true
|
|
$script:coveredScenarios.spaces_non_ascii_paths = $true
|
|
}
|
|
|
|
Invoke-Case 'missing_windows_terminal_is_not_required' {
|
|
$profile = New-IsolatedWindowsProfile 'missing-windows-terminal'
|
|
Set-InstallScriptProfileGlobals $profile
|
|
$env:JCODE_WINDOWS_SETUP_SKIP_EXTERNALS = '1'
|
|
$env:WT_SESSION = $null
|
|
$env:WindowsTerminal = $null
|
|
$jcodeExe = Join-Path $profile.Root 'No Windows Terminal\jcode.exe'
|
|
New-Item -ItemType Directory -Path (Split-Path -Parent $jcodeExe) -Force | Out-Null
|
|
Set-Content -Path $jcodeExe -Value 'fake exe' -NoNewline
|
|
|
|
Assert-Equal $true (Install-JcodeHotkey -JcodeExePath $jcodeExe) 'hotkey setup should not require Windows Terminal to be installed or active'
|
|
$scriptText = Get-Content -LiteralPath $installScript -Raw
|
|
Assert-NotContains $scriptText 'wt.exe' 'installer should not shell out to Windows Terminal for hotkey setup'
|
|
$script:coveredScenarios.missing_windows_terminal = $true
|
|
}
|
|
|
|
Invoke-Case 'launcher_rollback_failure_preserves_existing_install' {
|
|
$profile = New-IsolatedWindowsProfile 'rollback-failure'
|
|
New-Item -ItemType Directory -Path $profile.InstallDir -Force | Out-Null
|
|
Set-Content -Path $profile.LauncherPath -Value 'known-good' -NoNewline
|
|
$missingSource = Join-Path $profile.Root 'missing-source.exe'
|
|
$threw = $false
|
|
try {
|
|
Install-JcodeLauncher -SourcePath $missingSource -LauncherPath $profile.LauncherPath | Out-Null
|
|
} catch {
|
|
$threw = $true
|
|
}
|
|
Assert-Equal $true $threw 'launcher install should surface copy failures'
|
|
Assert-Equal 'known-good' (Get-Content -LiteralPath $profile.LauncherPath -Raw) 'failed launcher install should preserve the existing launcher'
|
|
$tempLaunchers = @(Get-ChildItem -LiteralPath $profile.InstallDir -Filter '.jcode-launcher-*.tmp.exe' -Force -ErrorAction SilentlyContinue)
|
|
Assert-Equal 0 $tempLaunchers.Count 'failed launcher install should not leave temporary launcher files behind'
|
|
$script:coveredScenarios.rollback_failure = $true
|
|
}
|
|
|
|
$env:JCODE_UNINSTALL_PS1_IMPORT_ONLY = '1'
|
|
. $uninstallScript
|
|
|
|
function Get-CimInstance { @() }
|
|
$script:uninstallUserPath = $null
|
|
$script:uninstallSetCalls = 0
|
|
$script:uninstallBroadcasts = 0
|
|
function Remove-JcodeUserPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$InstallDir,
|
|
[AllowNull()][string]$CurrentPath,
|
|
[scriptblock]$SetUserPathAction,
|
|
[scriptblock]$BroadcastAction,
|
|
[bool]$Broadcast = $true
|
|
)
|
|
$update = Resolve-JcodePathRemoval -InstallDir $InstallDir -CurrentPath $script:uninstallUserPath
|
|
if ($update.Changed) {
|
|
$script:uninstallUserPath = $update.Path
|
|
$script:uninstallSetCalls += 1
|
|
if ($Broadcast) { $script:uninstallBroadcasts += 1 }
|
|
}
|
|
$update | Add-Member -NotePropertyName Broadcasted -NotePropertyValue ([bool]($update.Changed -and $Broadcast))
|
|
return $update
|
|
}
|
|
|
|
Invoke-Case 'uninstall_guards_purge_paths_and_process_scope' {
|
|
$profile = New-IsolatedWindowsProfile 'uninstall-safety'
|
|
Assert-Equal $true (Test-JcodeSafePurgePath $profile.JcodeHome) 'dedicated jcode data directories should be purgeable'
|
|
Assert-Equal $false (Test-JcodeSafePurgePath $profile.UserProfile) 'the user profile must never be accepted as a purge target'
|
|
Assert-Equal $false (Test-JcodeSafePurgePath $profile.Root) 'a parent workspace must never be accepted as a purge target'
|
|
Assert-Equal $true (Test-JcodeManagedExecutablePath -ExecutablePath $profile.LauncherPath -LauncherPath $profile.LauncherPath -BuildsDir $profile.BuildsDir) 'the installed launcher should be recognized as managed'
|
|
Assert-Equal $true (Test-JcodeManagedExecutablePath -ExecutablePath (Join-Path $profile.InstallDir '.jcode-launcher-old-a1b2c3.exe') -LauncherPath $profile.LauncherPath -BuildsDir $profile.BuildsDir) 'a renamed live-upgrade launcher should be recognized as managed'
|
|
Assert-Equal $false (Test-JcodeManagedExecutablePath -ExecutablePath (Join-Path $profile.InstallDir 'other-tool.exe') -LauncherPath $profile.LauncherPath -BuildsDir $profile.BuildsDir) 'unrelated executables beside the launcher must not be terminated'
|
|
Assert-Equal $true (Test-JcodeManagedExecutablePath -ExecutablePath (Join-Path $profile.BuildsDir 'stable\jcode.exe') -LauncherPath $profile.LauncherPath -BuildsDir $profile.BuildsDir) 'installed version binaries should be recognized as managed'
|
|
Assert-Equal $false (Test-JcodeManagedExecutablePath -ExecutablePath (Join-Path $profile.Root 'development\jcode.exe') -LauncherPath $profile.LauncherPath -BuildsDir $profile.BuildsDir) 'unrelated development binaries must not be terminated'
|
|
}
|
|
|
|
Invoke-Case 'uninstall_cleanup_removes_binaries_path_and_keeps_user_data' {
|
|
$profile = New-IsolatedWindowsProfile 'uninstall-cleanup'
|
|
New-Item -ItemType Directory -Path $profile.InstallDir -Force | Out-Null
|
|
New-Item -ItemType Directory -Path (Join-Path $profile.BuildsDir 'stable') -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $profile.JcodeHome -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $profile.HotkeyDir -Force | Out-Null
|
|
New-Item -ItemType Directory -Path (Split-Path -Parent $profile.StartupShortcutPath) -Force | Out-Null
|
|
Set-Content -Path $profile.LauncherPath -Value 'installed launcher' -NoNewline
|
|
$oldLauncherPath = Join-Path $profile.InstallDir '.jcode-launcher-old-a1b2c3.exe'
|
|
Set-Content -Path $oldLauncherPath -Value 'previous running launcher' -NoNewline
|
|
Set-Content -Path (Join-Path $profile.BuildsDir 'stable\jcode.exe') -Value 'stable build' -NoNewline
|
|
Set-Content -Path (Join-Path $profile.JcodeHome 'config.toml') -Value 'kept = true' -NoNewline
|
|
Set-Content -Path (Join-Path $profile.HotkeyDir 'jcode-hotkey.ps1') -Value 'legacy listener' -NoNewline
|
|
Set-Content -Path $profile.StartupShortcutPath -Value 'startup shortcut' -NoNewline
|
|
@{ hotkey_configured = $true; hotkey_dismissed = $false } | ConvertTo-Json | Set-Content -Path $profile.SetupHintsPath -Encoding UTF8
|
|
$installVariant = ($profile.InstallDir.ToUpperInvariant() + '\')
|
|
$script:uninstallUserPath = "$($profile.InstallDir);C:\Keep;$installVariant"
|
|
$script:uninstallSetCalls = 0
|
|
$script:uninstallBroadcasts = 0
|
|
|
|
$exitCode = Invoke-JcodeUninstall -InstallDir $profile.InstallDir -Yes
|
|
Assert-Equal 0 $exitCode 'uninstall should complete successfully in the isolated profile'
|
|
Assert-PathMissing $profile.LauncherPath 'uninstall should remove the launcher'
|
|
Assert-PathMissing $oldLauncherPath 'uninstall should remove renamed live-upgrade launchers'
|
|
Assert-PathMissing $profile.InstallDir 'uninstall should remove the empty launcher directory'
|
|
Assert-PathMissing $profile.BuildsDir 'uninstall should remove installed build binaries'
|
|
Assert-PathMissing $profile.StartupShortcutPath 'uninstall should remove the launch-hotkey Startup shortcut'
|
|
Assert-PathMissing (Join-Path $profile.HotkeyDir 'jcode-hotkey.ps1') 'uninstall should remove legacy launch-hotkey artifacts'
|
|
Assert-PathExists (Join-Path $profile.JcodeHome 'config.toml') 'uninstall without -Purge should keep user data'
|
|
$setupHints = Get-Content -LiteralPath $profile.SetupHintsPath -Raw | ConvertFrom-Json
|
|
Assert-Equal $false $setupHints.hotkey_configured 'uninstall should clear the persisted hotkey-configured state'
|
|
Assert-Equal $true $setupHints.hotkey_dismissed 'uninstall should keep the removed hotkey prompt dismissed'
|
|
Assert-Equal 'C:\Keep' $script:uninstallUserPath 'uninstall should remove all jcode-managed PATH variants and keep unrelated entries'
|
|
Assert-Equal 1 $script:uninstallSetCalls 'uninstall should write mocked user PATH once when cleanup changes it'
|
|
Assert-Equal 1 $script:uninstallBroadcasts 'uninstall should broadcast once after PATH cleanup'
|
|
$script:coveredScenarios.uninstall_cleanup = $true
|
|
}
|
|
|
|
$missingScenarios = @($coveredScenarios.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key })
|
|
Assert-Equal 0 $missingScenarios.Count ("missing scenario coverage: {0}" -f ($missingScenarios -join ', '))
|
|
Write-Host "Scenario checklist: $($coveredScenarios.Count)/$($coveredScenarios.Count) requested Windows setup scenarios covered." -ForegroundColor Green
|
|
Write-Host "All $passedCases Windows setup evaluation cases passed." -ForegroundColor Green
|
|
} finally {
|
|
Restore-TestEnvironment
|
|
Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|