Files
MBN_STOCK_WEBVIEW/scripts/Invoke-LegacyPackageAppearanceSmoke.ps1
Wickedness b050f2f06c feat: refine branded operator experience
Add the MBN-branded startup intro, semantic sidebar icons, and refined card-based operator visuals. Strengthen packaged UI and input smoke coverage for the updated experience.
2026-07-24 01:26:13 +09:00

1385 lines
56 KiB
PowerShell

[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.'
}
$replacementBackupPath = $null
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.'
}
$replacementBackupPath = Join-Path $directory (
'.runtime-folders.local.json.' +
[Guid]::NewGuid().ToString('N') + '.restore.backup')
[IO.File]::Replace(
$temporaryPath,
$OperatorSettingsPath,
$replacementBackupPath,
$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 ($null -ne $replacementBackupPath -and
[IO.File]::Exists($replacementBackupPath)) {
$replacementBackup = Get-Item -LiteralPath $replacementBackupPath -Force
if ($replacementBackup.PSIsContainer -or
($replacementBackup.Attributes -band ([IO.FileAttributes]::ReparsePoint -bor
[IO.FileAttributes]::Device)) -ne 0) {
Throw-SafeFailure 'The operator settings replacement backup became unsafe.'
}
[IO.File]::Delete($replacementBackupPath)
}
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
Wait-StartupIntroDismissed $application $Registration.Executable
$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 = Join-Path `
([IO.Path]::GetDirectoryName($Output)) `
([IO.Path]::GetFileNameWithoutExtension($Output) + '.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
}