[CmdletBinding()] param( [ValidateRange(1024, 65535)] [int]$Port = 9353, [string]$Output, [string]$Screenshot, [string]$ExchangeDirectory, [string]$NodePath, [ValidateRange(10, 120)] [int]$LaunchTimeoutSeconds = 30, [ValidateRange(60, 900)] [int]$HarnessTimeoutSeconds = 180, [switch]$StaticAudit ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $requestedPort = $Port $requestedOutput = $Output $requestedScreenshot = $Screenshot $requestedExchangeDirectory = $ExchangeDirectory $requestedNodePath = $NodePath $requestedLaunchTimeout = $LaunchTimeoutSeconds $requestedHarnessTimeout = $HarnessTimeoutSeconds $sharedScript = Join-Path $PSScriptRoot 'Invoke-LegacyPackageInputSmoke.ps1' if (-not [IO.File]::Exists($sharedScript)) { throw [InvalidOperationException]::new('The shared package input safety script is missing.') } # Reuse the exact Debug AppX package, DryRun, loopback CDP, package identity, # process ownership, Tornado isolation and neutral normal-close guards. This # definitions-only import launches nothing. . $sharedScript ` -BuildFlavor DebugAppX ` -Profile ReadOnly ` -Port $requestedPort ` -Output $requestedOutput ` -Screenshot $requestedScreenshot ` -NodePath $requestedNodePath ` -LaunchTimeoutSeconds $requestedLaunchTimeout ` -HarnessTimeoutSeconds $requestedHarnessTimeout ` -DefinitionsOnly $Port = $requestedPort $Output = $requestedOutput $Screenshot = $requestedScreenshot $ExchangeDirectory = $requestedExchangeDirectory $NodePath = $requestedNodePath $LaunchTimeoutSeconds = $requestedLaunchTimeout $HarnessTimeoutSeconds = $requestedHarnessTimeout $HarnessPath = Join-Path $PSScriptRoot 'Test-LegacyPackageAppearanceSmoke.mjs' Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; using System.Threading; public static class LegacyAppearanceInputNative { [StructLayout(LayoutKind.Sequential)] private struct MouseInput { public int Dx; public int Dy; public uint MouseData; public uint Flags; public uint Time; public UIntPtr ExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct KeyboardInput { public ushort VirtualKey; public ushort ScanCode; public uint Flags; public uint Time; public UIntPtr ExtraInfo; } [StructLayout(LayoutKind.Sequential)] private struct HardwareInput { public uint Message; public ushort ParameterLow; public ushort ParameterHigh; } [StructLayout(LayoutKind.Explicit)] private struct InputUnion { [FieldOffset(0)] public MouseInput Mouse; [FieldOffset(0)] public KeyboardInput Keyboard; [FieldOffset(0)] public HardwareInput Hardware; } [StructLayout(LayoutKind.Sequential)] private struct Input { public uint Type; public InputUnion Union; } [DllImport("user32.dll")] private static extern uint SendInput( uint inputCount, Input[] inputs, int inputSize); [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int virtualKey); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern IntPtr GetAncestor(IntPtr window, uint flags); [DllImport("user32.dll")] private static extern bool IsWindow(IntPtr window); private const uint InputKeyboard = 1; private const uint KeyUp = 0x0002; private const uint GetAncestorRoot = 2; private const ushort VirtualKeyHome = 0x24; private const ushort VirtualKeyDown = 0x28; private const ushort VirtualKeyEnter = 0x0D; public static int InputStructureBytes() { return Marshal.SizeOf(typeof(Input)); } public static int ExpectedInputStructureBytes() { return IntPtr.Size == 8 ? 40 : 28; } public static void SendClosedSelectKeys(IntPtr expectedRoot, int optionIndex) { if (expectedRoot == IntPtr.Zero || !IsWindow(expectedRoot) || optionIndex < 0 || optionIndex > 2 || GetAncestor(GetForegroundWindow(), GetAncestorRoot) != expectedRoot) { throw new InvalidOperationException( "The closed appearance select keyboard contract is invalid."); } AssertReleased(VirtualKeyHome); AssertReleased(VirtualKeyDown); AssertReleased(VirtualKeyEnter); ushort held = 0; try { SendKey(VirtualKeyHome, false); held = VirtualKeyHome; Thread.Sleep(45); SendKey(VirtualKeyHome, true); held = 0; Thread.Sleep(60); for (int index = 0; index < optionIndex; index++) { AssertForeground(expectedRoot); SendKey(VirtualKeyDown, false); held = VirtualKeyDown; Thread.Sleep(45); SendKey(VirtualKeyDown, true); held = 0; Thread.Sleep(60); } AssertForeground(expectedRoot); SendKey(VirtualKeyEnter, false); held = VirtualKeyEnter; Thread.Sleep(45); SendKey(VirtualKeyEnter, true); held = 0; Thread.Sleep(300); AssertForeground(expectedRoot); } finally { if (held != 0) SendKey(held, true); AssertReleased(VirtualKeyHome); AssertReleased(VirtualKeyDown); AssertReleased(VirtualKeyEnter); } } private static void AssertForeground(IntPtr expectedRoot) { if (GetAncestor(GetForegroundWindow(), GetAncestorRoot) != expectedRoot) { throw new InvalidOperationException( "The exact package window lost foreground during appearance input."); } } private static void AssertReleased(ushort virtualKey) { if ((GetAsyncKeyState(virtualKey) & 0x8000) != 0) { throw new InvalidOperationException( "An appearance input key remained held."); } } private static void SendKey(ushort virtualKey, bool keyUp) { int inputSize = InputStructureBytes(); if (inputSize != ExpectedInputStructureBytes()) { throw new InvalidOperationException( "The Win32 INPUT structure size is not ABI-compatible."); } Input input = new Input { Type = InputKeyboard, Union = new InputUnion { Keyboard = new KeyboardInput { VirtualKey = virtualKey, Flags = keyUp ? KeyUp : 0 } } }; if (SendInput(1, new Input[] { input }, inputSize) != 1) { throw new InvalidOperationException( "Windows did not accept the one-shot appearance key input."); } } } '@ $script:Phase = 'preflight' $script:ApplicationProcess = $null $script:ApplicationWasLaunched = $false $script:CurrentApplicationClosed = $true $script:AppearanceRequestCalls = 0 $script:AppearanceAcknowledgementCalls = 0 $script:BaselineSettingsState = $null $script:BaselineSettingsBytes = $null $script:BaselineRecoveryPath = $null $script:BaselineRecoveryCreated = $false $script:BaselineRecoverySha256 = $null $script:BaselineRestored = $false $script:BaselineRestoreAttempted = $false function Test-ClosedAppearanceValue( [string]$Kind, [object]$Value) { if ($Value -isnot [string]) { return $false } switch -CaseSensitive ($Kind) { 'colorTheme' { return @('system', 'light', 'dark') -ccontains [string]$Value } 'viewMode' { return @('automatic', 'compact', 'cards') -ccontains [string]$Value } 'startWorkspace' { return @('stockCut', 'lastWorkspace') -ccontains [string]$Value } default { return $false } } } function New-Appearance( [string]$ColorTheme, [string]$ViewMode, [string]$StartWorkspace) { if (-not (Test-ClosedAppearanceValue 'colorTheme' $ColorTheme) -or -not (Test-ClosedAppearanceValue 'viewMode' $ViewMode) -or -not (Test-ClosedAppearanceValue 'startWorkspace' $StartWorkspace)) { Throw-SafeFailure 'An appearance projection is outside the closed schema.' } return [pscustomobject][ordered]@{ colorTheme = $ColorTheme viewMode = $ViewMode startWorkspace = $StartWorkspace } } function Test-AppearanceEqual([object]$Left, [object]$Right) { return $null -ne $Left -and $null -ne $Right -and [string]$Left.colorTheme -ceq [string]$Right.colorTheme -and [string]$Left.viewMode -ceq [string]$Right.viewMode -and [string]$Left.startWorkspace -ceq [string]$Right.startWorkspace } function Get-ContextHash( [object]$SceneDirectory, [object]$ResourceDirectory, [object]$BackgroundDirectory, [object]$NavigationExpanded) { $context = [ordered]@{ sceneDirectory = $SceneDirectory resourceDirectory = $ResourceDirectory backgroundDirectory = $BackgroundDirectory navigationExpanded = $NavigationExpanded } $bytes = $Utf8.GetBytes(($context | ConvertTo-Json -Compress)) return Get-ByteArraySha256 $bytes } function Assert-SettingsScalarTypes([object]$Value) { foreach ($name in @('sceneDirectory', 'resourceDirectory', 'backgroundDirectory')) { $candidate = $Value.$name if ($null -ne $candidate -and $candidate -isnot [string]) { Throw-SafeFailure 'The operator settings folder projection is not a string or null.' } } if ($Value.navigationExpanded -isnot [bool]) { Throw-SafeFailure 'The operator navigation setting is not Boolean.' } } function Get-OperatorSettingsFileState { if (-not [IO.File]::Exists($OperatorSettingsPath)) { return [pscustomobject][ordered]@{ exists = $false length = 0 sha256 = $null schemaVersion = 0 contextSha256 = Get-ContextHash $null $null $null $true appearance = New-Appearance 'system' 'automatic' 'stockCut' } } $item = Get-Item -LiteralPath $OperatorSettingsPath -Force if ($item.PSIsContainer -or ($item.Attributes -band ([IO.FileAttributes]::ReparsePoint -bor [IO.FileAttributes]::Device)) -ne 0 -or $item.Length -le 0 -or $item.Length -gt 16KB) { Throw-SafeFailure 'The operator settings file is not a bounded regular file.' } $bytes = [IO.File]::ReadAllBytes($OperatorSettingsPath) if ($bytes.Length -ne $item.Length) { Throw-SafeFailure 'The operator settings file changed while it was read.' } try { $raw = $StrictUtf8.GetString($bytes) $value = $raw | ConvertFrom-Json -ErrorAction Stop } catch { Throw-SafeFailure 'The operator settings file is not strict UTF-8 JSON.' } if ($null -eq $value -or (ConvertTo-FiniteNumber $value.schemaVersion 'Operator settings schema version') ` -notin @(1, 2)) { Throw-SafeFailure 'The operator settings schema version is unsupported.' } $schemaVersion = [int]$value.schemaVersion $expectedNames = if ($schemaVersion -eq 1) { @('schemaVersion', 'sceneDirectory', 'resourceDirectory', 'backgroundDirectory', 'navigationExpanded') } else { @('schemaVersion', 'sceneDirectory', 'resourceDirectory', 'backgroundDirectory', 'navigationExpanded', 'colorTheme', 'viewMode', 'startWorkspace') } Assert-ExactJsonPropertyNames $value $expectedNames 'Operator settings JSON' foreach ($name in $expectedNames) { $escapedName = [regex]::Escape($name) if ([regex]::Matches($raw, '"' + $escapedName + '"\s*:').Count -ne 1) { Throw-SafeFailure 'The operator settings JSON contains a duplicate property.' } } Assert-SettingsScalarTypes $value $appearance = if ($schemaVersion -eq 1) { New-Appearance 'system' 'automatic' 'stockCut' } else { New-Appearance ` ([string]$value.colorTheme) ` ([string]$value.viewMode) ` ([string]$value.startWorkspace) } return [pscustomobject][ordered]@{ exists = $true length = $bytes.Length sha256 = Get-ByteArraySha256 $bytes schemaVersion = $schemaVersion contextSha256 = Get-ContextHash ` $value.sceneDirectory ` $value.resourceDirectory ` $value.backgroundDirectory ` $value.navigationExpanded appearance = $appearance } } function Wait-OperatorSettingsAppearance( [object]$ExpectedAppearance, [string]$ExpectedContextSha256, [datetime]$Deadline) { while ([datetime]::UtcNow -lt $Deadline) { try { $state = Get-OperatorSettingsFileState if ($state.exists -eq $true -and $state.schemaVersion -eq 2 -and [string]$state.contextSha256 -ceq $ExpectedContextSha256 -and (Test-AppearanceEqual $state.appearance $ExpectedAppearance)) { return $state } } catch { # The native atomic save can briefly move between generations. The # bounded wait only observes; it never reissues input. } Start-Sleep -Milliseconds 50 } Throw-SafeFailure 'The expected appearance settings file was not observed after one input.' } function Initialize-ExactBaselineRecovery([string]$RecoveryPath) { $script:BaselineSettingsState = Get-OperatorSettingsFileState $script:BaselineSettingsBytes = $null $script:BaselineRecoveryPath = $RecoveryPath if ($script:BaselineSettingsState.exists -eq $true) { $bytes = [IO.File]::ReadAllBytes($OperatorSettingsPath) if ((Get-ByteArraySha256 $bytes) -cne [string]$script:BaselineSettingsState.sha256) { Throw-SafeFailure 'The operator settings baseline changed before backup.' } $stream = [IO.FileStream]::new( $RecoveryPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $stream.Write($bytes, 0, $bytes.Length) $stream.Flush($true) } finally { $stream.Dispose() } $script:BaselineSettingsBytes = $bytes $script:BaselineRecoveryCreated = $true $script:BaselineRecoverySha256 = Get-ByteArraySha256 $bytes } else { $marker = $Utf8.GetBytes( '{"schemaVersion":1,"operatorSettingsOriginallyAbsent":true}' + "`n") $stream = [IO.FileStream]::new( $RecoveryPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $stream.Write($marker, 0, $marker.Length) $stream.Flush($true) } finally { $stream.Dispose() } $script:BaselineRecoveryCreated = $true $script:BaselineRecoverySha256 = Get-ByteArraySha256 $marker } } function Restore-ExactBaselineSettings { if ($script:BaselineRestoreAttempted) { Throw-SafeFailure 'The exact operator settings restoration is one-shot and was already attempted.' } $script:BaselineRestoreAttempted = $true if (@(Get-LegacyApplicationProcesses).Count -ne 0) { Throw-SafeFailure 'The exact operator settings baseline cannot be restored while the app runs.' } $directory = [IO.Path]::GetDirectoryName($OperatorSettingsPath) if ([string]::IsNullOrWhiteSpace($directory)) { Throw-SafeFailure 'The operator settings restore directory is invalid.' } if ($script:BaselineSettingsState.exists -eq $true) { if (-not $script:BaselineRecoveryCreated -or -not [IO.File]::Exists($script:BaselineRecoveryPath)) { Throw-SafeFailure 'The exact operator settings recovery copy is missing.' } $recoveryItem = Get-Item -LiteralPath $script:BaselineRecoveryPath -Force $recoveryBytes = [IO.File]::ReadAllBytes($script:BaselineRecoveryPath) if ($recoveryItem.PSIsContainer -or ($recoveryItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or $recoveryBytes.Length -ne $recoveryItem.Length -or (Get-ByteArraySha256 $recoveryBytes) -cne [string]$script:BaselineSettingsState.sha256) { Throw-SafeFailure 'The exact operator settings recovery copy changed.' } if (-not [IO.Directory]::Exists($directory)) { [void][IO.Directory]::CreateDirectory($directory) } $temporaryPath = Join-Path $directory ( '.runtime-folders.local.json.' + [Guid]::NewGuid().ToString('N') + '.restore.tmp') try { $stream = [IO.FileStream]::new( $temporaryPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $stream.Write($recoveryBytes, 0, $recoveryBytes.Length) $stream.Flush($true) } finally { $stream.Dispose() } if ([IO.File]::Exists($OperatorSettingsPath)) { $targetItem = Get-Item -LiteralPath $OperatorSettingsPath -Force if ($targetItem.PSIsContainer -or ($targetItem.Attributes -band ([IO.FileAttributes]::ReparsePoint -bor [IO.FileAttributes]::Device)) -ne 0) { Throw-SafeFailure 'The operator settings restore target became unsafe.' } [IO.File]::Replace($temporaryPath, $OperatorSettingsPath, $null, $true) } else { [IO.File]::Move($temporaryPath, $OperatorSettingsPath) } $temporaryPath = $null } finally { if ($null -ne $temporaryPath -and [IO.File]::Exists($temporaryPath)) { [IO.File]::Delete($temporaryPath) } } } elseif ([IO.File]::Exists($OperatorSettingsPath)) { if (-not $script:BaselineRecoveryCreated -or -not [IO.File]::Exists($script:BaselineRecoveryPath) -or (Get-FileHash -Algorithm SHA256 -LiteralPath ` $script:BaselineRecoveryPath).Hash -cne [string]$script:BaselineRecoverySha256) { Throw-SafeFailure 'The original-absence recovery marker changed.' } $targetItem = Get-Item -LiteralPath $OperatorSettingsPath -Force if ($targetItem.PSIsContainer -or ($targetItem.Attributes -band ([IO.FileAttributes]::ReparsePoint -bor [IO.FileAttributes]::Device)) -ne 0) { Throw-SafeFailure 'The newly created operator settings restore target became unsafe.' } [IO.File]::Delete($OperatorSettingsPath) } $restored = Get-OperatorSettingsFileState if ($restored.exists -ne $script:BaselineSettingsState.exists -or ($restored.exists -eq $true -and [string]$restored.sha256 -cne [string]$script:BaselineSettingsState.sha256)) { Throw-SafeFailure 'The exact original operator settings bytes were not restored.' } if (-not $script:BaselineSettingsState.exists -and $script:BaselineRecoveryCreated -and -not [IO.File]::Exists($OperatorSettingsPath) -and (Get-FileHash -Algorithm SHA256 -LiteralPath ` $script:BaselineRecoveryPath).Hash -cne [string]$script:BaselineRecoverySha256) { Throw-SafeFailure 'The original-absence recovery marker changed before cleanup.' } if ($script:BaselineRecoveryCreated -and [IO.File]::Exists($script:BaselineRecoveryPath)) { [IO.File]::Delete($script:BaselineRecoveryPath) $script:BaselineRecoveryCreated = $false } $script:BaselineRestored = $true return $restored } function Get-AlternateAppearance([object]$Baseline) { return New-Appearance ` $(if ([string]$Baseline.colorTheme -cne 'dark') { 'dark' } else { 'light' }) ` $(if ([string]$Baseline.viewMode -cne 'cards') { 'cards' } else { 'compact' }) ` $(if ([string]$Baseline.startWorkspace -cne 'lastWorkspace') { 'lastWorkspace' } else { 'stockCut' }) } function Get-SequenceAppearance( [int]$Sequence, [object]$Initial, [object]$Final, [switch]$After) { $color = [string]$Initial.colorTheme $view = [string]$Initial.viewMode $start = [string]$Initial.startWorkspace if ($Sequence -ge 2 -and $After) { $color = [string]$Final.colorTheme } if ($Sequence -ge 3) { $color = [string]$Final.colorTheme } if ($Sequence -ge 3 -and $After) { $view = [string]$Final.viewMode } if ($Sequence -ge 4) { $color = [string]$Final.colorTheme $view = [string]$Final.viewMode } if ($Sequence -ge 4 -and $After) { $start = [string]$Final.startWorkspace } return New-Appearance $color $view $start } function Write-AppearanceAcknowledgement( [string]$Path, [object]$Request, [object]$SettingsState) { $acknowledgement = [ordered]@{ schemaVersion = 1 phase = [string]$Request.phase sequence = [int]$Request.sequence token = [string]$Request.token result = 'PASS' operation = [string]$Request.operation targetId = [string]$Request.targetId appearanceKey = if ($null -eq $Request.appearanceKey) { $null } else { [string]$Request.appearanceKey } targetValue = if ($null -eq $Request.targetValue) { $null } else { [string]$Request.targetValue } optionIndex = if ($null -eq $Request.optionIndex) { $null } else { [int]$Request.optionIndex } packageIdentityValidated = $true dryRunValidated = $true tornadoConnectionCount = 0 inputRetryCount = 0 mouseDownCalls = 1 mouseUpCalls = 1 homeKeyCalls = [int]$Request.homeKeyCalls arrowDownKeyCalls = [int]$Request.arrowDownKeyCalls enterKeyCalls = [int]$Request.enterKeyCalls settings = [ordered]@{ exists = [bool]$SettingsState.exists length = [int64]$SettingsState.length sha256 = [string]$SettingsState.sha256 schemaVersion = [int]$SettingsState.schemaVersion contextSha256 = [string]$SettingsState.contextSha256 appearance = $SettingsState.appearance } } $bytes = $Utf8.GetBytes(($acknowledgement | ConvertTo-Json -Depth 8 -Compress) + "`n") $temporaryPath = Join-Path ([IO.Path]::GetDirectoryName($Path)) ( ([IO.Path]::GetFileName($Path)) + '.' + [Guid]::NewGuid().ToString('N') + '.tmp') try { $stream = [IO.FileStream]::new( $temporaryPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $stream.Write($bytes, 0, $bytes.Length) $stream.Flush($true) } finally { $stream.Dispose() } [IO.File]::Move($temporaryPath, $Path) $temporaryPath = $null } finally { if ($null -ne $temporaryPath -and [IO.File]::Exists($temporaryPath)) { [IO.File]::Delete($temporaryPath) } } return [pscustomobject]@{ Bytes = $bytes Sha256 = Get-ByteArraySha256 $bytes } } function Assert-RequestAppearance([object]$Value, [object]$Expected, [string]$Label) { Assert-ExactJsonPropertyNames $Value @( 'colorTheme', 'viewMode', 'startWorkspace') $Label if (-not (Test-AppearanceEqual $Value $Expected)) { Throw-SafeFailure "$Label does not match the closed phase progression." } } function Invoke-ExactAppearanceRequest( [string]$RequestPath, [string]$AcknowledgementPath, [int]$ExpectedSequence, [string]$ExpectedPhase, [object]$PhaseInitial, [object]$PhaseFinal, [Diagnostics.Process]$Application, [string]$ExpectedApplicationPath) { $script:AppearanceRequestCalls++ if ($script:AppearanceRequestCalls -ne $ExpectedSequence -or [IO.File]::Exists($AcknowledgementPath)) { Throw-SafeFailure 'The appearance input one-shot budget changed.' } $requestEvidence = Wait-ReadSmallJsonEvidenceFile ` $RequestPath ` 'Appearance input request' ` ([datetime]::UtcNow.AddSeconds(3)) $request = $requestEvidence.Value Assert-ExactJsonPropertyNames $request @( 'schemaVersion', 'phase', 'sequence', 'token', 'targetUrl', 'operation', 'targetId', 'appearanceKey', 'targetValue', 'optionIndex', 'expectedBefore', 'expectedAfter', 'point', 'viewport', 'devicePixelRatio', 'mouseDownCalls', 'mouseUpCalls', 'homeKeyCalls', 'arrowDownKeyCalls', 'enterKeyCalls', 'inputRetryCount') 'Appearance input request' Assert-ExactJsonPropertyNames $request.viewport @('width', 'height') ` 'Appearance input viewport' $token = [string]$request.token if ((ConvertTo-FiniteNumber $request.schemaVersion 'Appearance input schema') -ne 1 -or (ConvertTo-FiniteNumber $request.sequence 'Appearance input sequence') -ne $ExpectedSequence -or $request.phase -isnot [string] -or [string]$request.phase -cne $ExpectedPhase -or $request.targetUrl -isnot [string] -or [string]$request.targetUrl -cne 'https://legacy-parity.mbn.local/index.html' -or $request.token -isnot [string] -or $token -cnotmatch '\A[0-9A-F]{32}\z' -or (ConvertTo-FiniteNumber $request.inputRetryCount 'Appearance input retry count') -ne 0 -or (ConvertTo-FiniteNumber $request.mouseDownCalls 'Appearance mouse-down count') -ne 1 -or (ConvertTo-FiniteNumber $request.mouseUpCalls 'Appearance mouse-up count') -ne 1) { Throw-SafeFailure 'The appearance input request violates the common closed contract.' } $expectedBefore = Get-SequenceAppearance ` $ExpectedSequence $PhaseInitial $PhaseFinal $expectedAfter = Get-SequenceAppearance ` $ExpectedSequence $PhaseInitial $PhaseFinal -After Assert-RequestAppearance $request.expectedBefore $expectedBefore ` 'Appearance expected-before projection' Assert-RequestAppearance $request.expectedAfter $expectedAfter ` 'Appearance expected-after projection' $plan = switch ($ExpectedSequence) { 1 { [pscustomobject]@{ Operation = 'application-click'; TargetId = 'workspace-settings-tab' AppearanceKey = $null; TargetValue = $null; OptionIndex = $null }; break } 2 { [pscustomobject]@{ Operation = 'select-appearance-option'; TargetId = 'operator-color-theme' AppearanceKey = 'colorTheme'; TargetValue = [string]$PhaseFinal.colorTheme OptionIndex = @('system', 'light', 'dark').IndexOf( [string]$PhaseFinal.colorTheme) }; break } 3 { [pscustomobject]@{ Operation = 'select-appearance-option'; TargetId = 'operator-view-mode' AppearanceKey = 'viewMode'; TargetValue = [string]$PhaseFinal.viewMode OptionIndex = @('automatic', 'compact', 'cards').IndexOf( [string]$PhaseFinal.viewMode) }; break } 4 { [pscustomobject]@{ Operation = 'select-appearance-option'; TargetId = 'operator-start-workspace' AppearanceKey = 'startWorkspace'; TargetValue = [string]$PhaseFinal.startWorkspace OptionIndex = @('stockCut', 'lastWorkspace').IndexOf( [string]$PhaseFinal.startWorkspace) }; break } default { Throw-SafeFailure 'The appearance input sequence is outside its four-step plan.' } } $actualAppearanceKey = if ($null -eq $request.appearanceKey) { $null } else { [string]$request.appearanceKey } $actualTargetValue = if ($null -eq $request.targetValue) { $null } else { [string]$request.targetValue } $actualOptionIndex = if ($null -eq $request.optionIndex) { $null } else { [int](ConvertTo-FiniteNumber $request.optionIndex 'Appearance option index') } $homeCount = ConvertTo-FiniteNumber $request.homeKeyCalls 'Appearance Home count' $downCount = ConvertTo-FiniteNumber $request.arrowDownKeyCalls ` 'Appearance ArrowDown count' $enterCount = ConvertTo-FiniteNumber $request.enterKeyCalls 'Appearance Enter count' if ($request.operation -isnot [string] -or [string]$request.operation -cne $Plan.Operation -or $request.targetId -isnot [string] -or [string]$request.targetId -cne $Plan.TargetId -or $actualAppearanceKey -cne $Plan.AppearanceKey -or $actualTargetValue -cne $Plan.TargetValue -or $actualOptionIndex -ne $Plan.OptionIndex -or $homeCount -ne $(if ($ExpectedSequence -eq 1) { 0 } else { 1 }) -or $downCount -ne $(if ($ExpectedSequence -eq 1) { 0 } else { $Plan.OptionIndex }) -or $enterCount -ne $(if ($ExpectedSequence -eq 1) { 0 } else { 1 })) { Throw-SafeFailure 'The appearance input request is outside the exact control plan.' } $viewportWidth = ConvertTo-FiniteNumber $request.viewport.width ` 'Appearance viewport width' $viewportHeight = ConvertTo-FiniteNumber $request.viewport.height ` 'Appearance viewport height' $devicePixelRatio = ConvertTo-FiniteNumber $request.devicePixelRatio ` 'Appearance device pixel ratio' if ($viewportWidth -lt 1 -or $viewportWidth -gt 16384 -or $viewportHeight -lt 1 -or $viewportHeight -gt 16384 -or $devicePixelRatio -lt 0.25 -or $devicePixelRatio -gt 5) { Throw-SafeFailure 'The appearance viewport contract is invalid.' } $point = ConvertTo-ExactInputPoint ` $request.point $viewportWidth $viewportHeight 'Appearance input point' $beforeFile = Get-OperatorSettingsFileState if ([string]$beforeFile.contextSha256 -cne [string]$script:BaselineSettingsState.contextSha256 -or -not (Test-AppearanceEqual $beforeFile.appearance $expectedBefore)) { Throw-SafeFailure 'The operator settings file changed before physical appearance input.' } Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath Assert-NoTornadoConnection $Application.Id [LegacyPackageInputNative]::SendApplicationClick( $Application.MainWindowHandle, $Application.Id, 30001, $point.X, $point.Y, $viewportWidth, $viewportHeight, $devicePixelRatio, $false) if ($ExpectedSequence -gt 1) { [LegacyPackageInputNative]::AcquireOwnedForeground( $Application.MainWindowHandle, $Application.MainWindowHandle, $Application.Id, 30001) [LegacyAppearanceInputNative]::SendClosedSelectKeys( $Application.MainWindowHandle, [int]$Plan.OptionIndex) } Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath Assert-NoTornadoConnection $Application.Id $afterFile = if ($ExpectedSequence -eq 1) { $observed = Get-OperatorSettingsFileState if ($observed.exists -ne $beforeFile.exists -or [string]$observed.sha256 -cne [string]$beforeFile.sha256) { Throw-SafeFailure 'Opening Settings unexpectedly changed the settings file.' } $observed } else { Wait-OperatorSettingsAppearance ` $expectedAfter ` ([string]$script:BaselineSettingsState.contextSha256) ` ([datetime]::UtcNow.AddSeconds(15)) } $script:AppearanceAcknowledgementCalls++ if ($script:AppearanceAcknowledgementCalls -ne $ExpectedSequence) { Throw-SafeFailure 'The appearance acknowledgement one-shot budget changed.' } $acknowledgement = Write-AppearanceAcknowledgement ` $AcknowledgementPath $request $afterFile return [pscustomobject][ordered]@{ sequence = $ExpectedSequence operation = [string]$Plan.Operation targetId = [string]$Plan.TargetId requestPath = $RequestPath requestSha256 = $requestEvidence.Sha256 acknowledgementPath = $AcknowledgementPath acknowledgementSha256 = $acknowledgement.Sha256 resultingSettingsSha256 = [string]$afterFile.sha256 } } function Invoke-AppearanceHarnessUnderWatch( [string]$PhaseName, [object]$ExpectedInitial, [object]$ExpectedFinal, [string]$PhaseOutput, [string]$PhaseScreenshot, [string]$PhaseExchangeDirectory, [Diagnostics.Process]$Application, [string]$ExpectedApplicationPath) { $arguments = @( $HarnessPath, '--phase', $PhaseName, '--port', [string]$Port, '--output', $PhaseOutput, '--screenshot', $PhaseScreenshot, '--exchange-directory', $PhaseExchangeDirectory, '--expected-color-theme', [string]$ExpectedInitial.colorTheme, '--expected-view-mode', [string]$ExpectedInitial.viewMode, '--expected-start-workspace', [string]$ExpectedInitial.startWorkspace, '--final-color-theme', [string]$ExpectedFinal.colorTheme, '--final-view-mode', [string]$ExpectedFinal.viewMode, '--final-start-workspace', [string]$ExpectedFinal.startWorkspace, '--timeout-ms', [string]($HarnessTimeoutSeconds * 1000) ) | ForEach-Object { ConvertTo-QuotedProcessArgument ([string]$_) } $startInfo = New-Object Diagnostics.ProcessStartInfo $startInfo.FileName = $NodePath $startInfo.WorkingDirectory = $RepositoryRoot $startInfo.Arguments = $arguments -join ' ' $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardInput = $true try { $node = [Diagnostics.Process]::Start($startInfo) } catch { Throw-SafeFailure 'The appearance input helper could not be started.' } if ($null -eq $node) { Throw-SafeFailure 'The appearance input helper did not start.' } $script:AppearanceRequestCalls = 0 $script:AppearanceAcknowledgementCalls = 0 $nextSequence = 1 $exchangeEvidence = New-Object 'Collections.Generic.List[object]' $deadline = [datetime]::UtcNow.AddSeconds($HarnessTimeoutSeconds + 30) $monitoringFailure = $null try { while (-not $node.HasExited) { if ([datetime]::UtcNow -ge $deadline) { Throw-SafeFailure 'The appearance helper exceeded its global deadline.' } Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath Assert-NoTornadoConnection $Application.Id if ($nextSequence -le 4) { $requestPath = Join-Path $PhaseExchangeDirectory ( $nextSequence.ToString('00') + '.request.json') $acknowledgementPath = Join-Path $PhaseExchangeDirectory ( $nextSequence.ToString('00') + '.ack.json') if ([IO.File]::Exists($requestPath)) { $exchange = Invoke-ExactAppearanceRequest ` $requestPath ` $acknowledgementPath ` $nextSequence ` $PhaseName ` $ExpectedInitial ` $ExpectedFinal ` $Application ` $ExpectedApplicationPath [void]$exchangeEvidence.Add($exchange) $nextSequence++ } } Start-Sleep -Milliseconds 75 $node.Refresh() } if ($node.ExitCode -ne 0 -or $nextSequence -ne 5 -or $exchangeEvidence.Count -ne 4 -or $script:AppearanceRequestCalls -ne 4 -or $script:AppearanceAcknowledgementCalls -ne 4) { Throw-SafeFailure 'The appearance helper did not complete four physical inputs.' } Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath Assert-NoTornadoConnection $Application.Id } catch { $monitoringFailure = $_.Exception.Message } if ($null -ne $monitoringFailure) { $stopped = $node.HasExited if (-not $stopped) { try { $node.StandardInput.WriteLine('CANCEL') $node.StandardInput.Flush() $stopped = $node.WaitForExit(5000) } catch { $stopped = $false } } if (-not $stopped) { try { # Only the bounded Node helper is stopped. The package is never # force-terminated and no appearance input is retried. $node.Kill() $stopped = $node.WaitForExit(10000) } catch { $stopped = $false } } Throw-SafeFailure ( "$monitoringFailure The appearance helper was stopped if possible; " + 'the packaged app was left open and no input was retried.') } return [pscustomobject]@{ ExitCode = $node.ExitCode Exchanges = @($exchangeEvidence.ToArray()) } } function Read-AppearancePhaseEvidence( [string]$Path, [string]$ScreenshotPath, [string]$ExpectedPhase, [object]$ExpectedInitial, [object]$ExpectedFinal) { try { $item = Get-Item -LiteralPath $Path -Force if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or $item.Length -le 0 -or $item.Length -gt 512KB) { Throw-SafeFailure 'Appearance phase evidence is not a bounded regular file.' } $bytes = [IO.File]::ReadAllBytes($Path) if ($bytes.Length -ne $item.Length) { Throw-SafeFailure 'Appearance phase evidence changed while it was read.' } $value = $StrictUtf8.GetString($bytes) | ConvertFrom-Json -ErrorAction Stop $sealed = [pscustomobject]@{ Value = $value Sha256 = Get-ByteArraySha256 $bytes } } catch [InvalidOperationException] { throw } catch { Throw-SafeFailure 'Appearance phase evidence is not strict bounded JSON.' } $value = $sealed.Value if ([int]$value.schemaVersion -ne 1 -or [string]$value.profile -cne 'operator-appearance-persistence' -or [string]$value.phase -cne $ExpectedPhase -or [string]$value.result -cne 'PASS' -or $null -ne $value.error -or [string]$value.target.expectedUrl -cne 'https://legacy-parity.mbn.local/index.html' -or [int]$value.target.port -ne $Port -or [string]$value.target.url -cne 'https://legacy-parity.mbn.local/index.html' -or [string]$value.safety.requiredMode -cne 'dryRun' -or [string]$value.safety.requiredPhase -cne 'idle' -or [int]$value.safety.inputRetryCount -ne 0 -or $value.safety.appCloseRequested -ne $false -or $value.safety.playoutIntentIssued -ne $false -or @($value.safety.blockedOutboundMessages).Count -ne 0 -or @($value.safety.stateViolations).Count -ne 0 -or -not (Test-AppearanceEqual $value.expectedInitial $ExpectedInitial) -or -not (Test-AppearanceEqual $value.expectedFinal $ExpectedFinal) -or @($value.exchanges).Count -ne 4 -or @($value.selections).Count -ne 3 -or ($ExpectedPhase -ceq 'verify-restore' -and $value.persistenceVerifiedBeforeInput -ne $true) -or ($ExpectedPhase -ceq 'verify-restore' -and $value.startWorkspaceBehaviorVerifiedBeforeInput -ne $true) -or ($ExpectedPhase -ceq 'mutate' -and ($value.persistenceVerifiedBeforeInput -ne $false -or $value.startWorkspaceBehaviorVerifiedBeforeInput -ne $false))) { Throw-SafeFailure 'The appearance phase evidence violates its sealed contract.' } $selectionKeys = @('colorTheme', 'viewMode', 'startWorkspace') for ($selectionIndex = 0; $selectionIndex -lt $selectionKeys.Count; $selectionIndex++) { $selection = $value.selections[$selectionIndex] $selectionKey = $selectionKeys[$selectionIndex] $expectedTarget = [string]$ExpectedFinal.$selectionKey $expectedDownCount = switch -CaseSensitive ($selectionKey) { 'colorTheme' { @('system', 'light', 'dark').IndexOf($expectedTarget); break } 'viewMode' { @('automatic', 'compact', 'cards').IndexOf($expectedTarget); break } 'startWorkspace' { @('stockCut', 'lastWorkspace').IndexOf($expectedTarget); break } } if ([string]$selection.appearanceKey -cne $selectionKey -or [string]$selection.targetValue -cne $expectedTarget -or $selection.trustedChange -ne $true -or [int]$selection.physicalKeyCounts.home -ne 1 -or [int]$selection.physicalKeyCounts.arrowDown -ne $expectedDownCount -or [int]$selection.physicalKeyCounts.enter -ne 1 -or [int]$selection.physicalKeyCounts.total -ne (2 + $expectedDownCount) -or [int]$selection.outboundIntentCount -ne 1 -or $selection.restartRequired -ne $false) { Throw-SafeFailure 'An appearance selection lacks trusted physical input evidence.' } } $phaseExchanges = @($value.exchanges) for ($exchangeIndex = 0; $exchangeIndex -lt $phaseExchanges.Count; $exchangeIndex++) { $exchange = $phaseExchanges[$exchangeIndex] if ($exchange.physicalInput.pointerDown.isTrusted -ne $true -or $exchange.physicalInput.pointerUp.isTrusted -ne $true -or [string]$exchange.physicalInput.pointerDown.pointerType -cne 'mouse' -or [string]$exchange.physicalInput.pointerUp.pointerType -cne 'mouse') { Throw-SafeFailure 'An appearance exchange lacks trusted pointer evidence.' } if ($exchangeIndex -gt 0) { $selection = $value.selections[$exchangeIndex - 1] if ($exchange.physicalInput.change.isTrusted -ne $true -or [string]$exchange.physicalInput.change.type -cne 'change' -or [string]$exchange.physicalInput.change.value -cne [string]$selection.targetValue -or [int]$exchange.physicalKeyCounts.home -ne [int]$selection.physicalKeyCounts.home -or [int]$exchange.physicalKeyCounts.arrowDown -ne [int]$selection.physicalKeyCounts.arrowDown -or [int]$exchange.physicalKeyCounts.enter -ne [int]$selection.physicalKeyCounts.enter) { Throw-SafeFailure 'An appearance exchange lacks its trusted change/key seal.' } } } $screenshot = Get-Item -LiteralPath $ScreenshotPath -Force if ($screenshot.PSIsContainer -or ($screenshot.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or $screenshot.Length -le 0 -or [int64]$value.screenshot.bytes -ne $screenshot.Length -or [string]$value.screenshot.sha256 -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $ScreenshotPath).Hash) { Throw-SafeFailure 'The appearance phase screenshot is not sealed exactly.' } return [pscustomobject]@{ Value = $value Sha256 = $sealed.Sha256 ScreenshotSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $ScreenshotPath).Hash } } function Start-ExactAppearanceApplication( [pscustomobject]$Registration, [int]$LaunchOrdinal) { if ($LaunchOrdinal -notin @(1, 2)) { Throw-SafeFailure 'The appearance launch ordinal is outside its two-launch budget.' } Assert-NoExistingApplication Assert-PortIsFree $Port $application = Start-PackagedDryRunApplication $Registration $Port [void](Wait-ExactLoopbackListener ` $Port ` $application ` ([datetime]::UtcNow.AddSeconds($LaunchTimeoutSeconds))) Assert-NoTornadoConnection $application.Id $script:ApplicationProcess = $application $script:ApplicationWasLaunched = $true $script:CurrentApplicationClosed = $false return $application } function Close-ExactAppearanceApplication( [Diagnostics.Process]$Application, [string]$ExpectedPath) { if ($script:CloseMainWindowCalls -ne 0 -or $script:PrimaryInvokeCalls -ne 0) { Throw-SafeFailure 'The per-launch normal-close budget was not reset.' } Invoke-ExactNormalClose $Application $ExpectedPath Wait-PortReleased $Port ([datetime]::UtcNow.AddSeconds(15)) if (@(Get-LegacyApplicationProcesses).Count -ne 0 -or $script:CloseMainWindowCalls -ne 1 -or $script:PrimaryInvokeCalls -ne 1) { Throw-SafeFailure 'The exact appearance package did not complete one normal close.' } $script:CurrentApplicationClosed = $true $script:ApplicationWasLaunched = $false $script:CloseMainWindowCalls = 0 $script:PrimaryInvokeCalls = 0 } function Write-FinalAppearanceEvidence([string]$Path, [object]$Value) { $bytes = $Utf8.GetBytes(($Value | ConvertTo-Json -Depth 10) + "`n") $stream = [IO.FileStream]::new( $Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $stream.Write($bytes, 0, $bytes.Length) $stream.Flush($true) } finally { $stream.Dispose() } } if ($StaticAudit) { [pscustomobject][ordered]@{ result = 'PASS' profile = 'operator-appearance-persistence' packageFlavor = 'DebugAppX' launchCount = 2 phaseCount = 2 physicalSettingsClickCount = 2 physicalSelectCount = 6 inputRetryCount = 0 rawBaselineRecovery = $true packageForceTerminationAllowed = $false sendInputClickAvailable = $null -ne [LegacyPackageInputNative].GetMethod('SendApplicationClick') sendInputSelectKeysAvailable = $null -ne [LegacyAppearanceInputNative].GetMethod('SendClosedSelectKeys') inputStructureBytes = [LegacyAppearanceInputNative]::InputStructureBytes() expectedInputStructureBytes = [LegacyAppearanceInputNative]::ExpectedInputStructureBytes() inputStructureSizeValidated = [LegacyAppearanceInputNative]::InputStructureBytes() -eq [LegacyAppearanceInputNative]::ExpectedInputStructureBytes() harnessPath = $HarnessPath harnessSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $HarnessPath).Hash } | ConvertTo-Json -Compress return } $timestamp = [datetime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ') $Output = Resolve-EvidencePath $Output "legacy-package-appearance-$timestamp.json" $Screenshot = Resolve-EvidencePath $Screenshot "legacy-package-appearance-$timestamp.png" if ([string]::IsNullOrWhiteSpace($ExchangeDirectory)) { $ExchangeDirectory = [IO.Path]::ChangeExtension($Output, $null) + '.exchanges' } elseif ([IO.Path]::IsPathRooted($ExchangeDirectory)) { $ExchangeDirectory = [IO.Path]::GetFullPath($ExchangeDirectory) } else { $ExchangeDirectory = [IO.Path]::GetFullPath((Join-Path $RepositoryRoot $ExchangeDirectory)) } $NodePath = Resolve-NodeExecutable $NodePath $phaseOneDirectory = Join-Path $ExchangeDirectory 'phase-01-input' $phaseTwoDirectory = Join-Path $ExchangeDirectory 'phase-02-input' $phaseOneOutput = Join-Path $ExchangeDirectory 'phase-01.json' $phaseTwoOutput = Join-Path $ExchangeDirectory 'phase-02.json' $phaseOneScreenshot = Join-Path $ExchangeDirectory 'phase-01.png' $recoveryPath = Join-Path $ExchangeDirectory '.operator-settings-baseline.recovery' try { if (-not [IO.File]::Exists($HarnessPath)) { Throw-SafeFailure 'The tracked appearance harness is missing.' } Assert-OutputPathAvailable $Output 'Output' Assert-OutputPathAvailable $Screenshot 'Screenshot' Assert-OutputPathAvailable $ExchangeDirectory 'Exchange directory' [void][IO.Directory]::CreateDirectory($ExchangeDirectory) [void][IO.Directory]::CreateDirectory($phaseOneDirectory) [void][IO.Directory]::CreateDirectory($phaseTwoDirectory) if (@(Get-ChildItem -LiteralPath $phaseOneDirectory -Force).Count -ne 0 -or @(Get-ChildItem -LiteralPath $phaseTwoDirectory -Force).Count -ne 0) { Throw-SafeFailure 'A new appearance exchange directory is not empty.' } Assert-LocalConfigurationIsDryRun Assert-NoUnsafeEnvironment $registration = Get-ExactDevelopmentPackage Assert-NoExistingApplication Assert-PortIsFree $Port Initialize-ExactBaselineRecovery $recoveryPath $baselineAppearance = $script:BaselineSettingsState.appearance $alternateAppearance = Get-AlternateAppearance $baselineAppearance $script:Phase = 'first package launch and physical mutation' $firstApplication = Start-ExactAppearanceApplication $registration 1 $firstProcessId = $firstApplication.Id $firstHarness = Invoke-AppearanceHarnessUnderWatch ` 'mutate' ` $baselineAppearance ` $alternateAppearance ` $phaseOneOutput ` $phaseOneScreenshot ` $phaseOneDirectory ` $firstApplication ` $registration.Executable $firstEvidence = Read-AppearancePhaseEvidence ` $phaseOneOutput ` $phaseOneScreenshot ` 'mutate' ` $baselineAppearance ` $alternateAppearance $mutatedSettings = Get-OperatorSettingsFileState if ($mutatedSettings.schemaVersion -ne 2 -or [string]$mutatedSettings.contextSha256 -cne [string]$script:BaselineSettingsState.contextSha256 -or -not (Test-AppearanceEqual $mutatedSettings.appearance $alternateAppearance)) { Throw-SafeFailure 'The alternate appearance was not persisted before relaunch.' } $script:Phase = 'first normal close' Close-ExactAppearanceApplication $firstApplication $registration.Executable $script:Phase = 'second package launch and physical restoration' $secondApplication = Start-ExactAppearanceApplication $registration 2 $secondProcessId = $secondApplication.Id $secondHarness = Invoke-AppearanceHarnessUnderWatch ` 'verify-restore' ` $alternateAppearance ` $baselineAppearance ` $phaseTwoOutput ` $Screenshot ` $phaseTwoDirectory ` $secondApplication ` $registration.Executable $secondEvidence = Read-AppearancePhaseEvidence ` $phaseTwoOutput ` $Screenshot ` 'verify-restore' ` $alternateAppearance ` $baselineAppearance $logicalRestored = Get-OperatorSettingsFileState if ($logicalRestored.schemaVersion -ne 2 -or [string]$logicalRestored.contextSha256 -cne [string]$script:BaselineSettingsState.contextSha256 -or -not (Test-AppearanceEqual $logicalRestored.appearance $baselineAppearance)) { Throw-SafeFailure 'The baseline appearance was not restored by physical input.' } $script:Phase = 'second normal close' Close-ExactAppearanceApplication $secondApplication $registration.Executable $script:Phase = 'exact raw settings restoration' $exactRestored = Restore-ExactBaselineSettings $script:Phase = 'seal final evidence' $finalEvidence = [ordered]@{ schemaVersion = 1 profile = 'operator-appearance-persistence' result = 'PASS' packageFullName = $PackageFullName packageMode = $PackageModeLabel playoutMode = 'DryRun' cdpAddress = '127.0.0.1' cdpPort = $Port processIds = @($firstProcessId, $secondProcessId) launchCount = 2 normalCloseCount = 2 tornadoPortConnections = 0 inputRetryCount = 0 baseline = [ordered]@{ existed = [bool]$script:BaselineSettingsState.exists sha256 = [string]$script:BaselineSettingsState.sha256 schemaVersion = [int]$script:BaselineSettingsState.schemaVersion contextSha256 = [string]$script:BaselineSettingsState.contextSha256 appearance = $baselineAppearance } alternate = $alternateAppearance mutation = [ordered]@{ physicalSelectCount = 3 exchangeCount = $firstHarness.Exchanges.Count phaseEvidence = $phaseOneOutput phaseEvidenceSha256 = $firstEvidence.Sha256 screenshot = $phaseOneScreenshot screenshotSha256 = $firstEvidence.ScreenshotSha256 } relaunch = [ordered]@{ persistedBeforeInput = [bool]$secondEvidence.Value.persistenceVerifiedBeforeInput physicalRestoreSelectCount = 3 exchangeCount = $secondHarness.Exchanges.Count phaseEvidence = $phaseTwoOutput phaseEvidenceSha256 = $secondEvidence.Sha256 } restoration = [ordered]@{ logicalAppearanceRestored = $true exactOriginalBytesRestored = $true finalExists = [bool]$exactRestored.exists finalSha256 = [string]$exactRestored.sha256 recoveryCopyRemoved = -not [IO.File]::Exists($recoveryPath) } screenshot = $Screenshot screenshotSha256 = $secondEvidence.ScreenshotSha256 exchangeDirectory = $ExchangeDirectory } Write-FinalAppearanceEvidence $Output $finalEvidence $script:Phase = 'complete' [pscustomobject]@{ result = 'PASS' packageFullName = $PackageFullName packageMode = $PackageModeLabel playoutMode = 'DryRun' launchCount = 2 physicalSelectCount = 6 persistenceVerified = $true exactOriginalSettingsRestored = $true output = $Output outputSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $Output).Hash screenshot = $Screenshot screenshotSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $Screenshot).Hash exchangeDirectory = $ExchangeDirectory } } catch { $message = $_.Exception.Message $running = $false try { $running = @(Get-LegacyApplicationProcesses).Count -gt 0 } catch { $running = $false } if (-not $running -and $null -ne $script:BaselineSettingsState -and -not $script:BaselineRestored -and -not $script:BaselineRestoreAttempted) { try { [void](Restore-ExactBaselineSettings) } catch { Throw-SafeFailure ( "Appearance smoke stopped during '$($script:Phase)': $message " + 'The app is closed, but exact settings restoration also failed. ' + "Preserve the recovery evidence at '$ExchangeDirectory'.") } } if ($running) { Throw-SafeFailure ( "Appearance smoke stopped during '$($script:Phase)': $message " + 'The application was intentionally left open; no input retry, close retry, ' + 'forced termination, or settings-file overwrite was attempted. The exact ' + "recovery copy remains under '$ExchangeDirectory'.") } if ($script:BaselineRestoreAttempted -and -not $script:BaselineRestored) { Throw-SafeFailure ( "Appearance smoke stopped during '$($script:Phase)': $message " + 'The one-shot settings restoration had an unclear or failed result and was not ' + "retried. Preserve the recovery evidence at '$ExchangeDirectory'.") } throw }