| 1 | $ErrorActionPreference = 'Stop' |
| 2 | . (Join-Path $PSScriptRoot 'test-windows-upgrade-startup.ps1') |
| 3 | . (Join-Path $PSScriptRoot 'windows-upgrade-ui-evidence.ps1') |
| 4 | |
| 5 | function Assert-True($value, [string]$message) { if (-not $value) { throw $message } } |
| 6 | function Assert-Throws([scriptblock]$action, [string]$message) { |
| 7 | $caught = $false |
| 8 | try { & $action } catch { $caught = $true } |
| 9 | Assert-True $caught $message |
| 10 | } |
| 11 | |
| 12 | $testRoot = Join-Path ([IO.Path]::GetTempPath()) ('reasonix-upgrade-contract-' + [guid]::NewGuid().ToString('N')) |
| 13 | $savedHome = $HOME |
| 14 | try { |
| 15 | $release = Join-Path $testRoot 'install/releases/test/app/resources' |
| 16 | New-Item -ItemType Directory -Path $release -Force | Out-Null |
| 17 | $application = Join-Path $testRoot 'install/Reasonix.exe' |
| 18 | $builder = Join-Path $testRoot 'fixture.exe' |
| 19 | Set-Content -LiteralPath $application -Value 'mock launcher' |
| 20 | Set-Content -LiteralPath $builder -Value 'mock builder' |
| 21 | $version = 'v1.38.9-5' |
| 22 | $commit = '0123456789abcdef0123456789abcdef01234567' |
| 23 | @{activeVersion=$version; activeDir='releases/test'} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $testRoot 'install/current.json') |
| 24 | $buildPath = Join-Path $release 'build.json' |
| 25 | @{version=$version; commit=$commit} | ConvertTo-Json | Set-Content -LiteralPath $buildPath |
| 26 | $script:calls = [Collections.Generic.List[string]]::new() |
| 27 | $script:failPhase = '' |
| 28 | # Only replace native process boundaries; run the real evidence orchestration. |
| 29 | function Invoke-UpgradeFixture([string]$builder, [string[]]$arguments) { |
| 30 | $mode = $arguments[1] |
| 31 | $fixtureHome = $arguments[3] |
| 32 | $report = $arguments[5] |
| 33 | Assert-True ($fixtureHome -ne $HOME -and $fixtureHome.Contains('home # %20 中文')) 'Fixture must use a disposable home.' |
| 34 | if ($mode -eq 'create') { |
| 35 | $script:calls.Add('create') |
| 36 | @{visibleText='assistant-only marker'} | ConvertTo-Json | Set-Content -LiteralPath $report |
| 37 | } else { |
| 38 | $phase = $arguments[7] |
| 39 | $script:calls.Add("verify-$phase") |
| 40 | if ($script:failPhase -eq $phase) { throw 'Injected verification failure' } |
| 41 | } |
| 42 | } |
| 43 | function Invoke-UpgradeStartup([string]$installRoot, [string]$version, [string]$fixtureHome, [string]$text, [string]$evidence, [bool]$prepareHistorical) { |
| 44 | Assert-True ($text -eq 'assistant-only marker') 'Startup must require the assistant marker.' |
| 45 | Assert-True ($prepareHistorical -eq ((Split-Path $evidence -Leaf) -eq 'first')) 'Only the first launch explicitly prepares old content; canonical restart restores automatically.' |
| 46 | $script:calls.Add('startup-' + (Split-Path $evidence -Leaf)) |
| 47 | } |
| 48 | $evidence = Join-Path $testRoot 'success' |
| 49 | Invoke-WindowsUpgradeAcceptance $application $builder $version $evidence |
| 50 | $result = Get-Content -LiteralPath (Join-Path $evidence 'result.json') -Raw | ConvertFrom-Json |
| 51 | Assert-True ($HOME -eq $savedHome) 'Acceptance changed the PowerShell home.' |
| 52 | Assert-True ($result.sourceSha -eq $commit) 'Evidence must read build.json.commit.' |
| 53 | Assert-True ($result.applicationSha256 -eq (Get-FileHash -Algorithm SHA256 -LiteralPath $application).Hash) 'Evidence must hash the tested application.' |
| 54 | Assert-True (($script:calls -join ',') -eq 'create,startup-first,verify-first,startup-restart,verify-restart') 'Upgrade and restart must both be checked in order.' |
| 55 | |
| 56 | foreach ($invalidCommit in @('', 'unknown', '0123456')) { |
| 57 | @{version=$version; commit=$invalidCommit; sourceSha=$commit} | ConvertTo-Json | Set-Content -LiteralPath $buildPath |
| 58 | $badEvidence = Join-Path $testRoot ('invalid-' + [guid]::NewGuid().ToString('N')) |
| 59 | Assert-Throws { Invoke-WindowsUpgradeAcceptance $application $builder $version $badEvidence } 'Missing/invalid commit must fail closed.' |
| 60 | Assert-True (-not (Test-Path -LiteralPath $badEvidence)) 'Invalid package must not get acceptance evidence.' |
| 61 | } |
| 62 | @{version=$version; commit=$commit} | ConvertTo-Json | Set-Content -LiteralPath $buildPath |
| 63 | foreach ($phase in @('first', 'restart')) { |
| 64 | $script:failPhase = $phase |
| 65 | $badEvidence = Join-Path $testRoot "failure-$phase" |
| 66 | Assert-Throws { Invoke-WindowsUpgradeAcceptance $application $builder $version $badEvidence } 'Data verification failure must propagate.' |
| 67 | Assert-True (-not (Test-Path -LiteralPath (Join-Path $badEvidence 'result.json'))) 'Failed verification cannot publish a pass.' |
| 68 | } |
| 69 | |
| 70 | # Model the native tree enumeration boundary. Selection and visibility checks |
| 71 | # below use the same function as packaged Windows acceptance. |
| 72 | function Get-UpgradeUIDescendants($element) { |
| 73 | foreach ($child in $element.Children) { $child; Get-UpgradeUIDescendants $child } |
| 74 | } |
| 75 | function New-UIElement([string]$name, [string]$id='', [bool]$offscreen=$false, [object[]]$children=@()) { |
| 76 | return [pscustomobject]@{ |
| 77 | Current=[pscustomobject]@{Name=$name; AutomationId=$id; IsOffscreen=$offscreen; BoundingRectangle=[pscustomobject]@{Width=200; Height=20}} |
| 78 | Children=$children |
| 79 | } |
| 80 | } |
| 81 | $marker = 'assistant-only marker' |
| 82 | $script:clicked = 0 |
| 83 | function Invoke-UpgradeUIButton($element) { $script:clicked++ } |
| 84 | $pending = New-UIElement 'Import and open' 'reasonix-prepare-restored-session' |
| 85 | $pendingRoot = New-UIElement '' '' $false @($pending) |
| 86 | $pending.Current.IsOffscreen = $true |
| 87 | Assert-True (-not (Invoke-PendingHistoricalSession $pendingRoot)) 'Hidden preparation actions must not be clicked.' |
| 88 | $pending.Current.IsOffscreen = $false |
| 89 | Assert-True (Invoke-PendingHistoricalSession $pendingRoot) 'Explicit preparation must invoke the actual pending-session action.' |
| 90 | Assert-True ($script:clicked -eq 1) 'Invoke only the visible action.' |
| 91 | $body = New-UIElement $marker |
| 92 | $transcript = New-UIElement '' 'reasonix-chat-transcript-upgrade-tab' $false @($body) |
| 93 | $sidebar = New-UIElement $marker 'sidebar-topic' |
| 94 | $root = New-UIElement '' '' $false @($sidebar) |
| 95 | Assert-True (-not (Test-VisibleUpgradeHistory $root $marker)) 'A matching sidebar title must not pass.' |
| 96 | $root.Children = @($sidebar, $transcript) |
| 97 | Assert-True (Test-VisibleUpgradeHistory $root $marker) 'Visible transcript assistant body should pass.' |
| 98 | $root.Children = @($sidebar, $transcript, $pending) |
| 99 | Assert-True (-not (Test-VisibleUpgradeHistory $root $marker)) 'Prepared content must not retain the import action.' |
| 100 | $root.Children = @($sidebar, $transcript) |
| 101 | $failure = New-UIElement 'Failed to load conversation history. Previous content was kept when available — retry to try again.' |
| 102 | $transcript.Children = @($body, $failure) |
| 103 | Assert-True (-not (Test-VisibleUpgradeHistory $root $marker)) 'Visible history with an obsolete failure notice must not pass.' |
| 104 | $transcript.Children = @($body) |
| 105 | $recovery = New-UIElement 'Loading history' 'reasonix-session-recovery-test' |
| 106 | $root.Children = @($sidebar, $transcript, $recovery) |
| 107 | Assert-True (-not (Test-VisibleUpgradeHistory $root $marker)) 'History behind an unresolved recovery banner must not pass.' |
| 108 | $recovery.Current.IsOffscreen = $true |
| 109 | Assert-True (Test-VisibleUpgradeHistory $root $marker) 'An offscreen recovery surface does not block the visible session.' |
| 110 | $root.Children = @($sidebar, $transcript) |
| 111 | $body.Current.IsOffscreen = $true |
| 112 | Assert-True (-not (Test-VisibleUpgradeHistory $root $marker)) 'Offscreen history must not pass.' |
| 113 | $body.Current.IsOffscreen = $false |
| 114 | $transcript.Current.IsOffscreen = $true |
| 115 | Assert-True (-not (Test-VisibleUpgradeHistory $root $marker)) 'Hidden transcript must not pass.' |
| 116 | $transcript.Current.IsOffscreen = $false |
| 117 | $body.Current.BoundingRectangle.Width = 0 |
| 118 | Assert-True (-not (Test-VisibleUpgradeHistory $root $marker)) 'Zero-size history must not pass.' |
| 119 | $body.Current.BoundingRectangle.Width = 200 |
| 120 | $body.Current.Name = 'unrelated body' |
| 121 | Assert-True (-not (Test-VisibleUpgradeHistory $root $marker)) 'Matching text outside the transcript must not pass.' |
| 122 | Write-Host 'Windows upgrade orchestration and UI evidence contracts passed (mocked native boundaries).' |
| 123 | } finally { |
| 124 | Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction SilentlyContinue |
| 125 | } |
| 126 |