private/WinpeStartup/Initialize-WinpeStartupEnvironment.Tests.ps1

BeforeAll {
    . "$PSScriptRoot\Initialize-WinpeStartupEnvironment.ps1"
    . "$PSScriptRoot\..\..\public\WinPE\Invoke-WinpeStartup.ps1"
    . "$PSScriptRoot\..\Get-WinpeStartupProfileCandidates.ps1"
    . "$PSScriptRoot\Initialize-WinpeStartupDrivers.ps1"
    . "$PSScriptRoot\Initialize-WinpeStartupFiles.ps1"
    . "$PSScriptRoot\Initialize-WinpeStartupMain.ps1"

    function New-CoreEnvironmentFixture {
        <#
        .SYNOPSIS
            Creates a Core environment test fixture.
 
        .DESCRIPTION
            Creates a file under the test WinpeStartup Core directory and
            returns its path.
 
        .PARAMETER RelativePath
            Path of the fixture file relative to the Core directory.
 
        .PARAMETER Lines
            Content lines to write to the fixture file.
 
        .EXAMPLE
            New-CoreEnvironmentFixture -RelativePath 'main\.env' -Lines @('SETTING=value')
 
        .NOTES
            Used by Initialize-WinpeStartupEnvironment tests.
        #>

        [CmdletBinding()]
        param (
            [string]$RelativePath,
            [string[]]$Lines
        )

        $Error.Clear()
        $path = Join-Path -Path $fixtureRoot -ChildPath "WinpeStartup\core\$RelativePath"
        [System.IO.Directory]::CreateDirectory((Split-Path -Path $path -Parent)) | Out-Null
        Set-Content -LiteralPath $path -Value $Lines -Encoding UTF8
        return $path
    }
}

Describe 'Initialize-WinpeStartupEnvironment Core environment files' {
    BeforeEach {
        $savedEnvironment = [System.Environment]::GetEnvironmentVariables('Process')
        $env:SystemDrive = 'X:'
        $fixtureRoot = Join-Path -Path $TestDrive -ChildPath ([guid]::NewGuid().ToString())
        [System.IO.Directory]::CreateDirectory($fixtureRoot) | Out-Null
        $fixtureCorePath = Join-Path -Path $fixtureRoot -ChildPath 'WinpeStartup\core'

        Mock Join-Path {
            if ($Path -eq 'X:' -and $ChildPath -eq 'WinpeStartup\core') {
                return $fixtureCorePath
            }
            return "$($Path.TrimEnd('\'))\$ChildPath"
        } -ParameterFilter { $Path -like 'X:*' }
        Mock New-Item {}
        Mock Set-ItemProperty {}
        Mock Write-Host {}
        Mock Start-Process { [pscustomobject]@{ ExitCode = 0 } }
    }

    AfterEach {
        $currentEnvironment = [System.Environment]::GetEnvironmentVariables('Process')
        foreach ($name in @($currentEnvironment.Keys)) {
            if (-not $savedEnvironment.Contains($name)) {
                [System.Environment]::SetEnvironmentVariable($name, $null, 'Process')
            }
        }
        foreach ($name in $savedEnvironment.Keys) {
            [System.Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name], 'Process')
        }
    }

    It 'loads hidden .env and application.env from a hidden immediate Core folder' {
        $path = New-CoreEnvironmentFixture -RelativePath 'main\.env' -Lines @(
            '# DATABASE SETTINGS'
            'WSCORE_TEST_DB_HOST=localhost'
            'WSCORE_TEST_DB_PORT=5432'
            'WSCORE_TEST_FEATURE=true'
            'WSCORE_TEST_QUOTED="synthetic value"'
            "WSCORE_TEST_SINGLE='synthetic string'"
        )
        $applicationPath = New-CoreEnvironmentFixture -RelativePath 'main\application.env' -Lines @(
            '# APPLICATION CONFIGURATION'
            'WSCORE_TEST_NODE_ENV=development'
            'WSCORE_TEST_PORT=3000'
            'WSCORE_TEST_DEBUG=true'
        )
        [System.IO.File]::SetAttributes($path, [System.IO.FileAttributes]::Hidden)
        [System.IO.File]::SetAttributes((Split-Path $applicationPath -Parent), [System.IO.FileAttributes]::Hidden)

        $output = @(Initialize-WinpeStartupEnvironment)

        $output.Count | Should -Be 0
        $env:WSCORE_TEST_DB_HOST | Should -Be 'localhost'
        $env:WSCORE_TEST_DB_PORT | Should -Be '5432'
        $env:WSCORE_TEST_FEATURE | Should -Be 'true'
        $env:WSCORE_TEST_QUOTED | Should -Be 'synthetic value'
        $env:WSCORE_TEST_SINGLE | Should -Be 'synthetic string'
        $env:WSCORE_TEST_NODE_ENV | Should -Be 'development'
        $env:WSCORE_TEST_PORT | Should -Be '3000'
        $env:WSCORE_TEST_DEBUG | Should -Be 'true'
        Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter {
            $Object -match '^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\] \[PASS\] Initialize env$'
        }
    }

    It 'sorts full paths across folders and lets the last file and line win' {
        New-CoreEnvironmentFixture 'z-last\.env' @('WSCORE_TEST_ORDER=last folder') | Out-Null
        New-CoreEnvironmentFixture 'a-first\z.env' @('WSCORE_TEST_ORDER=later file') | Out-Null
        New-CoreEnvironmentFixture 'a-first\.env' @('WSCORE_TEST_ORDER=first file') | Out-Null
        New-CoreEnvironmentFixture 'z-last\application.env' @(
            'WSCORE_TEST_ORDER=first line'
            'WSCORE_TEST_ORDER=last line'
        ) | Out-Null
        Mock Get-ChildItem {
            @(
                [pscustomobject]@{ FullName = (Join-Path $fixtureCorePath 'z-last') }
                [pscustomobject]@{ FullName = (Join-Path $fixtureCorePath 'a-first') }
            )
        } -ParameterFilter { $LiteralPath -eq $fixtureCorePath -and $Directory }

        Initialize-WinpeStartupEnvironment

        $env:WSCORE_TEST_ORDER | Should -Be 'last line'
    }

    It 'ignores root-level, nested, non-env and profile-folder files' {
        New-CoreEnvironmentFixture '.env' @('WSCORE_TEST_ROOT=unexpected') | Out-Null
        New-CoreEnvironmentFixture 'main\nested\deeper.env' @('WSCORE_TEST_NESTED=unexpected') | Out-Null
        New-CoreEnvironmentFixture 'main\settings.txt' @('WSCORE_TEST_TEXT=unexpected') | Out-Null
        New-CoreEnvironmentFixture 'main\.env' @('WSCORE_TEST_INCLUDED=yes') | Out-Null
        $profilesPath = Join-Path $fixtureRoot 'WinpeStartup\profiles'
        [System.IO.Directory]::CreateDirectory($profilesPath) | Out-Null
        Set-Content -LiteralPath (Join-Path $profilesPath '.env') -Value 'WSCORE_TEST_PROFILE_FILE=unexpected'

        Initialize-WinpeStartupEnvironment

        $env:WSCORE_TEST_INCLUDED | Should -Be 'yes'
        $env:WSCORE_TEST_ROOT | Should -BeNullOrEmpty
        $env:WSCORE_TEST_NESTED | Should -BeNullOrEmpty
        $env:WSCORE_TEST_TEXT | Should -BeNullOrEmpty
        $env:WSCORE_TEST_PROFILE_FILE | Should -BeNullOrEmpty
    }

    It 'parses whitespace, quotes and embedded equals without evaluating content' {
        New-CoreEnvironmentFixture 'main\literal.env' @(
            ''
            ' # ignored comment'
            ' WSCORE_TEST_SPACES = value '
            'WSCORE_TEST_EQUALS=a=b=c'
            'WSCORE_TEST_INNER=" inner space "'
            'WSCORE_TEST_HASH=value # literal'
            'WSCORE_TEST_LITERAL=$env:SystemDrive'
            'WSCORE_TEST_COMMAND=$(throw "must not execute")'
            'WSCORE_TEST_ESCAPE="line\ntext"'
        ) | Out-Null

        Initialize-WinpeStartupEnvironment

        $env:WSCORE_TEST_SPACES | Should -Be 'value'
        $env:WSCORE_TEST_EQUALS | Should -Be 'a=b=c'
        $env:WSCORE_TEST_INNER | Should -Be ' inner space '
        $env:WSCORE_TEST_HASH | Should -Be 'value # literal'
        $env:WSCORE_TEST_LITERAL | Should -Be '$env:SystemDrive'
        $env:WSCORE_TEST_COMMAND | Should -Be '$(throw "must not execute")'
        $env:WSCORE_TEST_ESCAPE | Should -Be 'line\ntext'
    }

    It 'applies empty assignments using current runtime semantics' {
        $env:WSCORE_TEST_EMPTY = 'previous'
        $env:WSCORE_TEST_EMPTY_QUOTED = 'previous'
        New-CoreEnvironmentFixture 'main\.env' @(
            'WSCORE_TEST_EMPTY='
            'WSCORE_TEST_EMPTY_QUOTED=""'
        ) | Out-Null

        Initialize-WinpeStartupEnvironment

        $env:WSCORE_TEST_EMPTY | Should -BeNullOrEmpty
        $env:WSCORE_TEST_EMPTY_QUOTED | Should -BeNullOrEmpty
    }

    It 'changes only process scope and passes imported values to child PowerShell' {
        $machineValue = [System.Environment]::GetEnvironmentVariable('WSCORE_TEST_CHILD', 'Machine')
        $userValue = [System.Environment]::GetEnvironmentVariable('WSCORE_TEST_CHILD', 'User')
        New-CoreEnvironmentFixture 'main\.env' @('WSCORE_TEST_CHILD=child-inherited-value') | Out-Null

        Initialize-WinpeStartupEnvironment
        $executable = (Get-Process -Id $PID).Path
        $childValue = & $executable -NoLogo -NoProfile -Command '$env:WSCORE_TEST_CHILD'

        $LASTEXITCODE | Should -Be 0
        $childValue | Should -Be 'child-inherited-value'
        [System.Environment]::GetEnvironmentVariable('WSCORE_TEST_CHILD', 'Machine') | Should -Be $machineValue
        [System.Environment]::GetEnvironmentVariable('WSCORE_TEST_CHILD', 'User') | Should -Be $userValue
    }

    It 'warns with source and line numbers for invalid entries and continues without leaking content' {
        $path = New-CoreEnvironmentFixture 'main\.env' @(
            'sensitive-synthetic-no-equals'
            '=sensitive-synthetic-value'
            'export WSCORE_TEST_EXPORT=sensitive-synthetic-value'
            'WSCORE_TEST_BAD="sensitive-synthetic-value'
            "WSCORE_TEST_NUL=sensitive$([char]0)synthetic"
            'WSCORE_TEST_AFTER_INVALID=valid'
        )
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 5
        for ($index = 0; $index -lt $warnings.Count; $index++) {
            $warnings[$index].ToString() | Should -Match ([regex]::Escape($path))
            $warnings[$index].ToString() | Should -Match "line $($index + 1)"
        }
        ($warnings | Out-String) | Should -Not -Match 'sensitive|synthetic'
        $env:WSCORE_TEST_EXPORT | Should -BeNullOrEmpty
        $env:WSCORE_TEST_BAD | Should -BeNullOrEmpty
        $env:WSCORE_TEST_NUL | Should -BeNullOrEmpty
        $env:WSCORE_TEST_AFTER_INVALID | Should -Be 'valid'
        Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter {
            $Object -match '^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\] \[FAIL\] Initialize env$'
        }
        Should -Invoke Write-Host -Times 0 -Exactly -ParameterFilter {
            $Object -match '\[PASS\] Initialize env$'
        }
    }

    It 'does not log imported values on the verbose stream' {
        New-CoreEnvironmentFixture 'main\.env' @('WSCORE_TEST_VERBOSE=sensitive-synthetic-value') | Out-Null

        $messages = Initialize-WinpeStartupEnvironment -Verbose 4>&1

        ($messages | Out-String) | Should -Match 'Loaded environment entry'
        ($messages | Out-String) | Should -Not -Match 'sensitive-synthetic-value'
    }

    It 'warns on read failure and continues with other files' {
        $failedPath = New-CoreEnvironmentFixture 'main\.env' @('WSCORE_TEST_UNREADABLE=unexpected')
        New-CoreEnvironmentFixture 'main\application.env' @('WSCORE_TEST_AFTER_READ_FAILURE=yes') | Out-Null
        Mock Get-Content { throw 'sensitive-synthetic-exception' } -ParameterFilter { $LiteralPath -eq $failedPath }
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 1
        $warnings[0].ToString() | Should -Match 'Failed to read'
        ($warnings | Out-String) | Should -Not -Match 'sensitive-synthetic-exception'
        $env:WSCORE_TEST_AFTER_READ_FAILURE | Should -Be 'yes'
    }

    It 'warns on folder discovery failure and continues with other folders' {
        New-CoreEnvironmentFixture 'broken\.env' @('WSCORE_TEST_UNDISCOVERED=unexpected') | Out-Null
        New-CoreEnvironmentFixture 'main\.env' @('WSCORE_TEST_AFTER_DISCOVERY_FAILURE=yes') | Out-Null
        $brokenPath = Join-Path $fixtureCorePath 'broken'
        Mock Get-ChildItem { throw 'sensitive-synthetic-exception' } -ParameterFilter { $LiteralPath -eq $brokenPath }
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 1
        $warnings[0].ToString() | Should -Match 'Failed to discover environment files'
        ($warnings | Out-String) | Should -Not -Match 'sensitive-synthetic-exception'
        $env:WSCORE_TEST_AFTER_DISCOVERY_FAILURE | Should -Be 'yes'
    }

    It 'warns on Core discovery failure but retains shell initialization' {
        Mock Test-Path { throw 'sensitive-synthetic-exception' } -ParameterFilter { $LiteralPath -eq $fixtureCorePath }
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 1
        $warnings[0].ToString() | Should -Match 'Failed to discover Core folders'
        ($warnings | Out-String) | Should -Not -Match 'sensitive-synthetic-exception'
        $env:USERPROFILE | Should -Be 'X:\windows\system32\config\systemprofile'
        Should -Invoke Set-ItemProperty -Times 2 -Exactly
    }

    It 'warns on PS 5.1 oversized-name assignment failure and continues' -Skip:($PSVersionTable.PSEdition -ne 'Desktop') {
        $oversizedName = 'N' * 32768
        New-CoreEnvironmentFixture 'main\.env' @(
            "$oversizedName=sensitive-synthetic-value"
            'WSCORE_TEST_AFTER_SET_FAILURE=yes'
        ) | Out-Null
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 1
        $warnings[0].ToString() | Should -Match 'Failed to set environment entry.*line 1'
        ($warnings | Out-String) | Should -Not -Match 'sensitive-synthetic-value'
        $env:WSCORE_TEST_AFTER_SET_FAILURE | Should -Be 'yes'
    }

    It 'allows missing Core content without warnings and preserves existing setup' {
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings

        $warnings.Count | Should -Be 0
        $env:APPDATA | Should -Be 'X:\windows\system32\config\systemprofile\AppData\Roaming'
        $env:LOCALAPPDATA | Should -Be 'X:\windows\system32\config\systemprofile\AppData\Local'
        $env:HOMEDRIVE | Should -Be 'X:'
        $env:HOMEPATH | Should -Be '\windows\system32\config\systemprofile'
        Should -Invoke New-Item -Times 6 -Exactly
        Should -Invoke Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
            $Name -eq 'USERDATA' -and $Value -eq 'X:\Windows\System32\Config\SystemProfile'
        }
        Should -Invoke Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
            $Name -eq 'ExecutionPolicy' -and $Value -eq 'Bypass'
        }
    }

    It 'allows empty Core folders without warnings' {
        [System.IO.Directory]::CreateDirectory((Join-Path $fixtureCorePath 'empty')) | Out-Null
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings

        $warnings.Count | Should -Be 0
    }

    It 'preserves shell variable precedence over Core assignments' {
        New-CoreEnvironmentFixture 'main\.env' @(
            'APPDATA=core-default'
            'USERPROFILE=core-default'
            'HOMEDRIVE=core-default'
        ) | Out-Null

        Initialize-WinpeStartupEnvironment

        $env:APPDATA | Should -Be 'X:\windows\system32\config\systemprofile\AppData\Roaming'
        $env:USERPROFILE | Should -Be 'X:\windows\system32\config\systemprofile'
        $env:HOMEDRIVE | Should -Be 'X:'
    }

    It 'does not run initialization outside WinPE' {
        $env:SystemDrive = 'C:'
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 1
        $warnings[0].ToString() | Should -Match 'Not running in WinPE'
        Should -Invoke New-Item -Times 0 -Exactly
        Should -Invoke Set-ItemProperty -Times 0 -Exactly
        Should -Invoke Start-Process -Times 0 -Exactly
    }

    It 'allows the selected startup profile to override Core process values' {
        New-CoreEnvironmentFixture 'main\.env' @(
            'WSCORE_TEST_PRECEDENCE=core-default'
            'WSCORE_TEST_CORE_ONLY=retained'
        ) | Out-Null
        $profilePath = Join-Path $fixtureRoot 'profile.json'
        Set-Content -LiteralPath $profilePath -Value '{"env":{"WSCORE_TEST_PRECEDENCE":"profile-value"}}'
        $script:OSDCloudPSDefaultParameterValuesPath = Join-Path $fixtureRoot 'absent-defaults.json'
        Mock Get-WinpeStartupProfileCandidates {
            [pscustomobject]@{ Profile = 'Test'; Path = $profilePath; Index = 0 }
        }
        Mock Initialize-WinpeStartupDrivers {}
        Mock Initialize-WinpeStartupFiles {}
        Mock Initialize-WinpeStartupMain {}
        Mock Start-Sleep {}

        Invoke-WinpeStartup -SkipOnScreenKeyboard -SkipWiFi -SkipIPConfig -SkipUpdateOSDCloud

        $env:WSCORE_TEST_PRECEDENCE | Should -Be 'profile-value'
        $env:WSCORE_TEST_CORE_ONLY | Should -Be 'retained'
    }

    It 'imports hidden registry files and root certificates with quoted paths from multiple Core folders' {
        $registryPath = New-CoreEnvironmentFixture 'main folder\winpe-reg\settings file.reg' @('synthetic')
        $certificatePath = New-CoreEnvironmentFixture 'main folder\winpe-root-cer\root file.cer' @('synthetic')
        New-CoreEnvironmentFixture 'additional\winpe-reg\extra.reg' @('synthetic') | Out-Null
        New-CoreEnvironmentFixture 'additional\winpe-root-cer\extra.cer' @('synthetic') | Out-Null
        [System.IO.File]::SetAttributes($registryPath, [System.IO.FileAttributes]::Hidden)
        [System.IO.File]::SetAttributes($certificatePath, [System.IO.FileAttributes]::Hidden)
        [System.IO.File]::SetAttributes((Split-Path $registryPath -Parent), [System.IO.FileAttributes]::Hidden)
        [System.IO.File]::SetAttributes((Split-Path $certificatePath -Parent), [System.IO.FileAttributes]::Hidden)

        $output = @(Initialize-WinpeStartupEnvironment)

        $output.Count | Should -Be 0
        Should -Invoke Start-Process -Times 4 -Exactly
        Should -Invoke Start-Process -Times 1 -Exactly -ParameterFilter {
            $FilePath -eq 'reg.exe' -and $ArgumentList.Count -eq 2 -and
            $ArgumentList[0] -eq 'import' -and $ArgumentList[1] -eq "`"$registryPath`"" -and
            $Wait -and $PassThru -and $NoNewWindow -and $ErrorAction -eq 'Stop' -and
            -not [string]::IsNullOrWhiteSpace($RedirectStandardOutput) -and
            -not [string]::IsNullOrWhiteSpace($RedirectStandardError)
        }
        Should -Invoke Start-Process -Times 1 -Exactly -ParameterFilter {
            $FilePath -eq 'certutil.exe' -and $ArgumentList.Count -eq 3 -and
            $ArgumentList[0] -eq '-addstore' -and $ArgumentList[1] -eq 'root' -and
            $ArgumentList[2] -eq "`"$certificatePath`"" -and
            $Wait -and $PassThru -and $NoNewWindow -and $ErrorAction -eq 'Stop' -and
            -not [string]::IsNullOrWhiteSpace($RedirectStandardOutput) -and
            -not [string]::IsNullOrWhiteSpace($RedirectStandardError)
        }
        Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter {
            $Object -match '^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\] \[PASS\] Initialize certificates$'
        }
        Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter {
            $Object -match '^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\] \[PASS\] Initialize registry$'
        }
    }

    It 'imports environment files before registry files and certificates and sorts native imports within each type' {
        $paths = @(
            New-CoreEnvironmentFixture 'z-last\winpe-root-cer\z.cer' @('synthetic')
            New-CoreEnvironmentFixture 'z-last\winpe-reg\z.reg' @('synthetic')
            New-CoreEnvironmentFixture 'a-first\winpe-root-cer\a.cer' @('synthetic')
            New-CoreEnvironmentFixture 'a-first\winpe-reg\a.reg' @('synthetic')
        )
        $environmentPath = New-CoreEnvironmentFixture 'a-first\.env' @('WSCORE_TEST_IMPORT_ORDER=loaded')
        $calls = [System.Collections.Generic.List[string]]::new()
        Mock Get-Content {
            $calls.Add("env $LiteralPath")
            'WSCORE_TEST_IMPORT_ORDER=loaded'
        } -ParameterFilter { $LiteralPath -eq $environmentPath }
        Mock Start-Process {
            $calls.Add("$FilePath $($ArgumentList -join ' ')")
            [pscustomobject]@{ ExitCode = 0 }
        }

        Initialize-WinpeStartupEnvironment

        $calls.Count | Should -Be 5
        $calls[0] | Should -Be "env $environmentPath"
        $calls[1] | Should -Be "reg.exe import `"$($paths[3])`""
        $calls[2] | Should -Be "reg.exe import `"$($paths[1])`""
        $calls[3] | Should -Be "certutil.exe -addstore root `"$($paths[2])`""
        $calls[4] | Should -Be "certutil.exe -addstore root `"$($paths[0])`""
    }

    It 'does not import files at incorrect levels or with incorrect extensions' {
        foreach ($relativePath in @(
            'winpe-reg\root.reg'
            'winpe-root-cer\root.cer'
            'main\file.reg'
            'main\file.cer'
            'main\nested\winpe-reg\deep.reg'
            'main\nested\winpe-root-cer\deep.cer'
            'main\winpe-reg\nested\deep.reg'
            'main\winpe-root-cer\nested\deep.cer'
            'main\winpe-reg\ignore.txt'
            'main\winpe-root-cer\ignore.txt'
        )) {
            New-CoreEnvironmentFixture $relativePath @('synthetic') | Out-Null
        }

        Initialize-WinpeStartupEnvironment

        Should -Invoke Start-Process -Times 0 -Exactly
    }

    It 'warns on nonzero tool exit codes and continues with remaining imports under ErrorAction Stop' {
        $failedRegistry = New-CoreEnvironmentFixture 'main\winpe-reg\a-failed.reg' @('synthetic')
        $failedCertificate = New-CoreEnvironmentFixture 'main\winpe-root-cer\a-failed.cer' @('synthetic')
        New-CoreEnvironmentFixture 'main\winpe-reg\z-valid.reg' @('synthetic') | Out-Null
        New-CoreEnvironmentFixture 'main\winpe-root-cer\z-valid.cer' @('synthetic') | Out-Null
        Mock Start-Process {
            if ($ArgumentList[-1] -like '*a-failed*') {
                return [pscustomobject]@{ ExitCode = 1 }
            }
            return [pscustomobject]@{ ExitCode = 0 }
        }
        $warnings = @()

        Initialize-WinpeStartupEnvironment -ErrorAction Stop -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 2
        $warnings[0].ToString() | Should -Match ([regex]::Escape($failedRegistry))
        $warnings[1].ToString() | Should -Match ([regex]::Escape($failedCertificate))
        ($warnings | Out-String) | Should -Match 'exit code 1'
        Should -Invoke Start-Process -Times 4 -Exactly
        Should -Invoke Write-Host -Times 0 -Exactly -ParameterFilter {
            $Object -match '\[PASS\] Initialize certificates$'
        }
        Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter {
            $Object -match '^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\] \[FAIL\] Initialize certificates$'
        }
        Should -Invoke Write-Host -Times 0 -Exactly -ParameterFilter {
            $Object -match '\[PASS\] Initialize registry$'
        }
        Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter {
            $Object -match '^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\] \[FAIL\] Initialize registry$'
        }
    }

    It 'warns on process launch errors and continues with subsequent registry and certificate files' {
        New-CoreEnvironmentFixture 'main\winpe-reg\a-failed.reg' @('synthetic') | Out-Null
        New-CoreEnvironmentFixture 'main\winpe-root-cer\a-failed.cer' @('synthetic') | Out-Null
        New-CoreEnvironmentFixture 'main\winpe-reg\z-valid.reg' @('synthetic') | Out-Null
        New-CoreEnvironmentFixture 'main\winpe-root-cer\z-valid.cer' @('synthetic') | Out-Null
        Mock Start-Process {
            if ($ArgumentList[-1] -like '*a-failed*') {
                throw 'Tool could not start'
            }
            [pscustomobject]@{ ExitCode = 0 }
        }
        $warnings = @()

        Initialize-WinpeStartupEnvironment -ErrorAction Stop -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 2
        ($warnings | Out-String) | Should -Match 'reg.exe'
        ($warnings | Out-String) | Should -Match 'certutil.exe'
        Should -Invoke Start-Process -Times 4 -Exactly
    }

    It 'warns on import folder discovery errors and continues with other Core folders' {
        New-CoreEnvironmentFixture 'broken\winpe-reg\file.reg' @('synthetic') | Out-Null
        New-CoreEnvironmentFixture 'broken\winpe-root-cer\file.cer' @('synthetic') | Out-Null
        New-CoreEnvironmentFixture 'main\winpe-reg\file.reg' @('synthetic') | Out-Null
        New-CoreEnvironmentFixture 'main\winpe-root-cer\file.cer' @('synthetic') | Out-Null
        $brokenRegistry = Join-Path $fixtureCorePath 'broken\winpe-reg'
        $brokenCertificates = Join-Path $fixtureCorePath 'broken\winpe-root-cer'
        Mock Get-ChildItem { throw 'Cannot enumerate folder' } -ParameterFilter {
            $LiteralPath -eq $brokenRegistry -or $LiteralPath -eq $brokenCertificates
        }
        $warnings = @()

        Initialize-WinpeStartupEnvironment -ErrorAction Stop -WarningVariable warnings -WarningAction SilentlyContinue

        $warnings.Count | Should -Be 2
        ($warnings | Out-String) | Should -Match 'Failed to discover'
        Should -Invoke Start-Process -Times 2 -Exactly
    }

    It 'allows missing or empty import folders without warnings or native tool calls' {
        [System.IO.Directory]::CreateDirectory((Join-Path $fixtureCorePath 'main\winpe-reg')) | Out-Null
        [System.IO.Directory]::CreateDirectory((Join-Path $fixtureCorePath 'main\winpe-root-cer')) | Out-Null
        [System.IO.Directory]::CreateDirectory((Join-Path $fixtureCorePath 'additional')) | Out-Null
        $warnings = @()

        Initialize-WinpeStartupEnvironment -WarningVariable warnings

        $warnings.Count | Should -Be 0
        Should -Invoke Start-Process -Times 0 -Exactly
    }

    Describe 'Invoke-WinpeStartup transcript lifecycle' {
        BeforeEach {
            $script:OSDCloudPSDefaultParameterValuesPath = Join-Path -Path $fixtureRoot -ChildPath 'absent-defaults.json'
            Mock Initialize-WinpeStartupEnvironment {}
            Mock Initialize-WinpeStartupDrivers {}
            Mock Initialize-WinpeStartupFiles {}
            Mock Initialize-WinpeStartupMain {}
            Mock Get-WinpeStartupProfileCandidates { @() }
            Mock Start-Sleep {}
            Mock Start-Transcript {}
            Mock Stop-Transcript {}
        }

        It 'stops the transcript after successful startup' {
            Invoke-WinpeStartup -SkipOnScreenKeyboard -SkipWiFi -SkipIPConfig -SkipUpdateOSDCloud

            Should -Invoke Start-Transcript -Times 1 -Exactly -ParameterFilter {
                $Path -eq 'X:\Windows\Temp\winpestartup.log' -and $Force -and $ErrorAction -eq 'Stop'
            }
            Should -Invoke Stop-Transcript -Times 1 -Exactly -ParameterFilter { $ErrorAction -eq 'Stop' }
        }

        It 'continues startup when starting the transcript fails' {
            Mock Start-Transcript { throw 'Synthetic transcript start failure' }
            $warnings = @()

            Invoke-WinpeStartup -SkipOnScreenKeyboard -SkipWiFi -SkipIPConfig -SkipUpdateOSDCloud `
                -WarningVariable warnings -WarningAction SilentlyContinue

            ($warnings | Out-String) | Should -Match 'Failed to start log'
            Should -Invoke Stop-Transcript -Times 0 -Exactly
            Should -Invoke Initialize-WinpeStartupMain -Times 1 -Exactly
        }

        It 'warns when stopping the transcript fails' {
            Mock Stop-Transcript { throw 'Synthetic transcript stop failure' }
            $warnings = @()

            Invoke-WinpeStartup -SkipOnScreenKeyboard -SkipWiFi -SkipIPConfig -SkipUpdateOSDCloud `
                -WarningVariable warnings -WarningAction SilentlyContinue

            ($warnings | Out-String) | Should -Match 'Failed to stop log'
            Should -Invoke Stop-Transcript -Times 1 -Exactly
        }

        It 'stops the transcript when a startup command terminates with an error' {
            Mock Start-Process { throw 'Synthetic process launch failure' } -ParameterFilter {
                $FilePath -eq 'powershell.exe'
            }

            {
                Invoke-WinpeStartup -SkipOnScreenKeyboard -SkipWiFi -SkipIPConfig -SkipUpdateOSDCloud `
                    -InvokeStartupCommand 'Write-Output test' -InvokeStartupCommandEA Stop
            } | Should -Throw

            Should -Invoke Stop-Transcript -Times 1 -Exactly
        }
    }
}