Files
winutil/pester/win11creator.Tests.ps1
T
KristianandChris Titus 92d5a08d49 Fix WaaSMedicSvc restoration using direct registry write (#5096)
* Fix WaaSMedicSvc restoration using direct registry write

Set-Service fails on WaaSMedicSvc with Access Denied since it's a
protected service (LaunchProtected=2). FirstLogon.ps1 silently
swallowed this failure via -ErrorAction SilentlyContinue, permanently
leaving WaaSMedicSvc disabled after setup.

Removes WaaSMedicSvc from the Set-Service restoration loop and
restores it via a direct registry write instead, matching the same
technique already used to disable it in WinUtil-PostInstall.ps1.

Added a test verifying the old Set-Service-based restoration is gone
and the new registry-write fix is present.

Addresses the WaaSMedicSvc portion of #5095; the broader AppX/Store
failure investigation is out of scope for this change.

* Assert exact registry-write contract for WaaSMedicSvc test

Per CodeRabbit review: the previous assertion only checked that
Set-ItemProperty and WaaSMedicSvc appeared near each other, which
would pass even with a wrong -Value or -Type. Now asserts the
complete command including -Value 3 and -Type DWord.

* Surface service restoration failures in FirstLogon.log

BITS, wuauserv, UsoSvc, and WaaSMedicSvc restoration all used
-ErrorAction SilentlyContinue, which suppresses errors before they
reach the output stream FirstLogon.ps1 redirects to its log file
(*>&1 ... >> FirstLogon.log). This meant any restoration failure —
not just the WaaSMedicSvc one already fixed — was invisible even in
the log.

Changed -ErrorAction SilentlyContinue to Continue on both the
Set-Service loop and the WaaSMedicSvc registry write, so failures
still don't halt the script but now actually land in
FirstLogon.log for troubleshooting.

Addresses the logging portion of the reporter's suggestion in #5095.

* Assert -ErrorAction Continue in WaaSMedicSvc test

Per CodeRabbit review: the existing test only checked the
Set-ItemProperty command and value, not the -ErrorAction Continue
change made for logging. Extended the same test to also assert
both the Set-ItemProperty and Set-Service lines use Continue instead
of SilentlyContinue.

* Extend WaaSMedicSvc test to cover full ErrorAction Continue

Per CodeRabbit review: the registry-write assertion stopped at
-Type DWord, so a regression back to -ErrorAction SilentlyContinue
would still pass. Extended the pattern to include -ErrorAction
Continue at the end of the command.

* Exercise FirstLogon service restoration behavior

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-09-29 12:46:08 -05:00

1203 lines
72 KiB
PowerShell

#===========================================================================
# Tests - Win11 Creator
Describe "Win11 Creator setup media" {
BeforeAll {
$script:repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$script:isoWorkflowPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISO.ps1"
$script:isoUsbWorkflowPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISOUSB.ps1"
$script:isoScriptPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISOScript.ps1"
$script:autoUnattendPath = Join-Path $script:repoRoot "tools\autounattend.xml"
function Get-WinUtilFunctionText {
param (
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$FunctionName
)
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path), [ref]$tokens, [ref]$errors)
if ($errors.Count -gt 0) {
throw "Unable to parse $Path`: $($errors[0].Message)"
}
$functionAst = $ast.Find({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -eq $FunctionName
}, $true)
if (-not $functionAst) {
throw "Unable to find function $FunctionName in $Path."
}
return $functionAst.Extent.Text
}
# Root package folders the fixture below yields, once nested_pkg\x64 is folded into
# its parent. Written out rather than derived, so a broken dedup fails the assertions
# instead of quietly moving the expectation with it.
$script:expectedRootPackages = 8
$script:resilienceDriverFixtures = @(
@{ Path = 'system_pkg'; Name = 'chipset.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'group_a\duplicate'; Name = 'audio.inf'; Class = 'Media' },
@{ Path = 'group_b\duplicate'; Name = 'extension.inf'; Class = 'Media' },
@{ Path = 'nested_pkg'; Name = 'main.inf'; Class = 'Net' },
@{ Path = 'nested_pkg\x64'; Name = 'extra.inf'; Class = 'Net' },
@{ Path = 'net_pkg_v2'; Name = 'network2.inf'; Class = 'Net' }
)
# Stands in for `dism /Online /Export-Driver`, which the ISO script now invokes
# through Invoke-WinUtilISODism rather than Start-Process. The two `duplicate` leaf
# names cover per-package failure logging, which must stay unambiguous; `nested_pkg`
# covers a package holding INFs at its root and in a subfolder; and `net_pkg_v2` is a
# sibling whose name starts with `net_pkg`, which the dedup must not swallow.
function Export-WinUtilTestDriverPackage {
param (
[Parameter(Mandatory)][string[]]$Arguments,
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Fixtures
)
$destination = @($Arguments | Where-Object { $_ -like '/Destination:*' })
if ($destination.Count -ne 1) {
throw "Expected exactly one /Destination argument in the mocked DISM export: $($Arguments -join ' ')"
}
$exportRoot = $destination[0].Substring('/Destination:'.Length)
$script:driverExportRoot = $exportRoot
# .NET file APIs rather than the cmdlets: Set-Content's -Encoding comes from the
# FileSystem provider, and a wildcard character in $exportRoot stops the provider
# resolving, so the parameter disappears.
foreach ($fixture in $fixtures) {
$fixturePath = Join-Path $exportRoot $fixture.Path
[void][System.IO.Directory]::CreateDirectory($fixturePath)
[System.IO.File]::WriteAllText(
(Join-Path $fixturePath $fixture.Name),
(@(
'[Version]'
"Class=$($fixture.Class)"
if ($fixture.Provider) { "Provider=$($fixture.Provider)" }
if ($fixture.DriverVer) {
$versionKeyword = if ($fixture.VersionKeyword) { $fixture.VersionKeyword } else { 'DriverVer' }
"$versionKeyword=$($fixture.DriverVer)"
}
) -join "`r`n"),
[System.Text.Encoding]::ASCII)
}
}
function New-WinUtilDriverExportHarness {
param (
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Fixtures,
[string]$FailedDriverPath,
[switch]$FailAllAddDrivers,
[switch]$FailDiscard
)
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
$script:driverExportRoot = $null
$script:driverExportFixtures = $Fixtures
$script:failedDriverPath = $FailedDriverPath
$script:failAllAddDrivers = $FailAllAddDrivers
$script:failDiscard = $FailDiscard
$script:exportedInfsAtAddDriver = $null
Set-Item -Path function:global:dism.exe -Value {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
$global:LASTEXITCODE = 0
if ($Arguments -contains '/Get-WimInfo') {
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
'[==========================100.0%==========================]'
} elseif ($Arguments -contains '/Export-Driver') {
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures $script:driverExportFixtures
} elseif ($Arguments -contains '/Add-Driver') {
# Snapshot what's still on disk right as DISM would /Recurse over it: this is the
# only point excluded folders are provably gone, since the SUT wipes the whole
# export root in its own cleanup once Invoke-WinUtilISOScript returns.
$script:exportRootAtAddDriver = @(Get-ChildItem -LiteralPath $script:driverExportRoot -Directory -Recurse -ErrorAction SilentlyContinue | ForEach-Object FullName)
$script:exportedInfsAtAddDriver = @(Get-ChildItem -LiteralPath $script:driverExportRoot -Filter '*.inf' -File -Recurse -ErrorAction SilentlyContinue | ForEach-Object FullName)
if ($script:failAllAddDrivers -or ($script:failedDriverPath -and @($Arguments | Where-Object { $_ -like "*$($script:failedDriverPath)" }).Count -gt 0)) {
$global:LASTEXITCODE = 13
'Error: 13'
}
} elseif ($Arguments -contains '/Discard' -and $script:failDiscard) {
$global:LASTEXITCODE = 50
'Discard failed'
}
}
}
$script:modifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOModify"
$script:mountAndVerifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOMountAndVerify"
$script:checkExistingWorkFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOCheckExistingWork"
$script:cleanAndResetFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOCleanAndReset"
$script:exportFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOExport"
$script:writeUsbFunction = Get-WinUtilFunctionText -Path $script:isoUsbWorkflowPath -FunctionName "Invoke-WinUtilISOWriteUSB"
$script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName"
$script:wimMetadataAssertionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Assert-WinUtilISOWimMetadata"
}
It "autounattend template does not force a product key" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$productKeyCount = $xml.SelectNodes("//u:ProductKey", $nsMgr).Count
if ($productKeyCount -ne 0) {
throw "Expected no ProductKey nodes, found $productKeyCount."
}
}
It "sets every hardware bypass before Windows Setup checks requirements" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$paths = @($xml.SelectNodes('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:RunSynchronous/u:RunSynchronousCommand/u:Path', $nsMgr) | ForEach-Object InnerText) -join "`n"
foreach ($bypass in 'BypassTPMCheck', 'BypassSecureBootCheck', 'BypassRAMCheck', 'BypassCPUCheck', 'BypassStorageCheck') {
$paths | Should -Match ([regex]::Escape($bypass))
}
}
It "sets OOBE-sensitive registry values before OOBE starts" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$paths = @($xml.SelectNodes('/u:unattend/u:settings[@pass="specialize"]/u:component[@name="Microsoft-Windows-Deployment"]/u:RunSynchronous/u:RunSynchronousCommand/u:Path', $nsMgr) | ForEach-Object InnerText) -join "`n"
foreach ($valueName in 'BypassNRO', 'PreventDeviceEncryption', 'ShippedWithReserves') {
$paths | Should -Match ([regex]::Escape($valueName))
}
}
It "ISO script accepts selected edition and driver-only WIM servicing metadata" {
$isoScriptPath = Join-Path $PSScriptRoot "..\functions\private\Invoke-WinUtilISOScript.ps1"
$content = Get-Content -Path $isoScriptPath -Raw
foreach ($pattern in @(
'\[string\]\$InstallEditionId',
'\[string\]\$InstallImagePath',
'\[int\]\$InstallImageIndex',
'sources\\ei\.cfg',
'PID\.txt'
)) {
if ($content -notmatch $pattern) {
throw "Expected Invoke-WinUtilISOScript.ps1 to match pattern: $pattern"
}
}
}
It "starts each new ISO modification in a fresh working directory" {
foreach ($expectedText in @(
'$workDir = Join-Path $env:TEMP "WinUtil_Win11ISO_$(Get-Date -Format ''yyyyMMdd_HHmmss'')"',
'$workDir = "$($workDir)_$(([guid]::NewGuid()).ToString(''N'').Substring(0, 8))"'
)) {
$script:modifyFunction | Should -Match ([regex]::Escape($expectedText))
}
$script:modifyFunction | Should -Not -Match ([regex]::Escape("Reusing existing temp directory"))
}
It "turns ISO-script warning messages into job warnings" {
$script:modifyFunction | Should -Match 'if \(\$m -like "Warning:\*"\)[\s\S]*Write-WinUtilISOLog -Level "WARN" -Message \$m -SkipSessionLog[\s\S]*Write-Warning \$m'
}
It "keeps WIM servicing limited to DISM Add-Driver without image export or cleanup" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
foreach ($expectedText in @(
"'/Mount-Image'",
"'/Add-Driver'",
"'/Commit'",
"`$mountDir = Join-Path (Split-Path -Path `$ContentRoot -Parent) 'wim_mount'",
'install.wim metadata validation passed',
"Join-Path `$ContentRoot 'sources\boot.wim'"
)) {
$isoScriptContent | Should -Match ([regex]::Escape($expectedText))
}
foreach ($forbiddenText in @(
'Mount-WindowsImage',
'Dismount-WindowsImage',
'Export-WindowsImage',
'Set-WindowsImage',
'/ResetBase',
'/Cleanup-Image',
'$WinpeDriver$'
)) {
$isoScriptContent | Should -Not -Match ([regex]::Escape($forbiddenText))
}
}
It "injects only SCSIAdapter or HDC storage drivers into boot.wim" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
$isoScriptContent | Should -Match 'SCSIAdapter\|HDC'
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot 'sources\boot.wim'"))
$isoScriptContent | Should -Not -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
$isoScriptContent | Should -Not -Match ([regex]::Escape('sources\$OEM$\$$\Drivers'))
$isoScriptContent | Should -Not -Match ([regex]::Escape('WinUtil-InstallDrivers.ps1'))
$isoScriptContent | Should -Not -Match ([regex]::Escape('SetupComplete.cmd'))
}
It "rejects invalid WIM metadata before and after driver injection" {
. ([scriptblock]::Create($script:wimMetadataAssertionFunction))
$valid = @{ Languages = 'en-US'; Installation = 'Client'; Edition = 'Professional'; ProductSuite = 'Terminal Server'; ProductType = 'WinNT' }
$invalidBefore = $valid.Clone()
$invalidBefore.Edition = '<undefined>'
$invalidAfter = $valid.Clone()
$invalidAfter.ProductType = '<undefined>'
{ Assert-WinUtilISOWimMetadata -Before $invalidBefore } | Should -Throw '*already invalid*'
{ Assert-WinUtilISOWimMetadata -Before $valid -After $invalidAfter } | Should -Throw '*validation failed*'
}
It "runs every background ISO workflow through the shared job layer" {
$jobNames = @{
mountAndVerify = "ISO mount"
modify = "ISO modify"
cleanAndReset = "ISO cleanup"
export = "ISO export"
writeUsb = "USB write"
}
foreach ($entry in @(
@{ Text = $script:mountAndVerifyFunction; Name = $jobNames.mountAndVerify },
@{ Text = $script:modifyFunction; Name = $jobNames.modify },
@{ Text = $script:cleanAndResetFunction; Name = $jobNames.cleanAndReset },
@{ Text = $script:exportFunction; Name = $jobNames.export },
@{ Text = $script:writeUsbFunction; Name = $jobNames.writeUsb }
)) {
$entry.Text | Should -Match ([regex]::Escape("Start-WinUtilJob -Name `"$($entry.Name)`""))
# The job layer owns the busy state, the progress bar, and the taskbar item
$entry.Text | Should -Not -Match ([regex]::Escape('Win11ISOProcessRunning'))
$entry.Text | Should -Not -Match ([regex]::Escape('RunspaceFactory]::CreateRunspace()'))
$entry.Text | Should -Not -Match ([regex]::Escape('SessionStateProxy.SetVariable'))
$entry.Text | Should -Not -Match ([regex]::Escape('[System.Windows.MessageBox]::Show'))
}
}
It "runs ISO mount and verification outside the UI thread" {
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Start-WinUtilJob -Name "ISO mount"'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('IsoPath = $isoPath'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Invoke-WPFUIThread -ScriptBlock {'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Write-WinUtilISOLog'))
$script:mountAndVerifyFunction | Should -Not -Match ([regex]::Escape('Write-Win11ISOLog'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $true'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOMountButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOMountButton"].IsEnabled = $true'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOModifyButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOModifyButton"].IsEnabled = [bool]$Verified'))
}
It "dismounts an ISO and keeps modification disabled when verification fails" {
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$verified = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$verified = $true'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Dismount-DiskImage -ImagePath $isoPath -ErrorAction Stop'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["Win11ISOImagePath"] = $null'))
$script:mountAndVerifyFunction.IndexOf('Dismount-DiskImage -ImagePath $isoPath') | Should -BeGreaterThan $script:mountAndVerifyFunction.IndexOf('finally')
}
It "dismounts a previously verified ISO before mounting a replacement" {
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$previous = $sync["Win11ISOImagePath"]'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Dismount-DiskImage -ImagePath $previous -ErrorAction Stop'))
$script:mountAndVerifyFunction.IndexOf('Dismount-DiskImage -ImagePath $previous') |
Should -BeLessThan $script:mountAndVerifyFunction.IndexOf('$sync["Win11ISOImagePath"] = $null')
}
It "keeps the stored path and restores the interface when the previous ISO will not dismount" {
$dismountIndex = $script:mountAndVerifyFunction.IndexOf('Dismount-DiskImage -ImagePath $previous')
$resetIndex = $script:mountAndVerifyFunction.IndexOf('$sync["Win11ISOImagePath"] = $null')
$throwIndex = $script:mountAndVerifyFunction.IndexOf('throw $stillMounted')
$throwIndex | Should -BeGreaterThan $dismountIndex
$throwIndex | Should -BeLessThan $resetIndex
$script:mountAndVerifyFunction.IndexOf('try {') | Should -BeLessThan $dismountIndex
}
It "keeps ISO cleanup in finally so stopping modification cannot bypass it" {
$finallyIndex = $script:modifyFunction.IndexOf('finally')
$finallyIndex | Should -BeGreaterThan -1
$script:modifyFunction.IndexOf('Dismount-DiskImage', $finallyIndex) | Should -BeGreaterThan $finallyIndex
$script:modifyFunction.IndexOf('Remove-Item -Path $workDir -Recurse -Force', $finallyIndex) | Should -BeGreaterThan $finallyIndex
$script:modifyFunction.IndexOf('$sync["Win11ISOImagePath"] = $null', $finallyIndex) | Should -BeGreaterThan $finallyIndex
}
It "retries existing-work discovery after the active job finishes" {
$script:checkExistingWorkFunction | Should -Match ([regex]::Escape('Invoke-WinUtilWhenIdle -DelayMilliseconds 500'))
$script:checkExistingWorkFunction | Should -Match ([regex]::Escape('Invoke-WinUtilISOCheckExistingWork'))
$script:checkExistingWorkFunction | Should -Match ([regex]::Escape('$sync["Win11ISOExistingWorkRetryPending"] = $false'))
}
It "reports ISO progress and logging through the shared helpers" {
$content = Get-Content -Path $script:isoWorkflowPath -Raw
$usbContent = Get-Content -Path $script:isoUsbWorkflowPath -Raw
foreach ($source in @($content, $usbContent)) {
$source | Should -Match ([regex]::Escape('Step-WinUtilJob -Status'))
$source | Should -Not -Match '(?m)^\s*function (Log|SetProgress)\('
$source | Should -Not -Match ([regex]::Escape('$sync["WPFTweaksProgressLabel"]'))
}
# Every status-log line also lands in the session log
$content | Should -Match ([regex]::Escape('Write-WinUtilLog -Level $Level -Component "Win11Creator" -Message $Message'))
}
It "blocks oversized install.esd before USB erase confirmation" {
$script:writeUsbFunction | Should -Match ([regex]::Escape('$installEsd = Join-Path $contentsDir "sources\install.esd"'))
$script:writeUsbFunction | Should -Match ([regex]::Escape('$esdSizeBytes -ge 4GB'))
$script:writeUsbFunction | Should -Match 'install\.esd file'
$guardIndex = $script:writeUsbFunction.IndexOf('$installEsd = Join-Path $contentsDir "sources\install.esd"')
$confirmationIndex = $script:writeUsbFunction.IndexOf('Confirm USB Erase')
$guardIndex | Should -BeGreaterThan -1
$confirmationIndex | Should -BeGreaterThan $guardIndex
}
It "clears install.wim read-only attribute before FAT32 splitting" {
$splitGuardIndex = $script:writeUsbFunction.IndexOf('$wimSizeMB -gt 3800')
$readOnlyResetIndex = $script:writeUsbFunction.IndexOf(
'Set-ItemProperty -LiteralPath $installWim -Name IsReadOnly -Value $false'
)
$splitCommandIndex = $script:writeUsbFunction.IndexOf('Split-WindowsImage')
$splitGuardIndex | Should -BeGreaterThan -1
$readOnlyResetIndex | Should -BeGreaterThan $splitGuardIndex
$splitCommandIndex | Should -BeGreaterThan $readOnlyResetIndex
}
It "maps Windows edition names to setup edition IDs" {
. ([scriptblock]::Create($script:editionIdFunction))
$cases = @{
"Windows 11 Home Single Language" = "CoreSingleLanguage"
"Windows 11 Home N" = "CoreN"
"Windows 11 Home" = "Core"
"Windows 11 Pro for Workstations N" = "ProfessionalWorkstationN"
"Windows 11 Pro for Workstations" = "ProfessionalWorkstation"
"Windows 11 Pro Education N" = "ProfessionalEducationN"
"Windows 11 Pro Education" = "ProfessionalEducation"
"Windows 11 Pro N" = "ProfessionalN"
"Windows 11 Pro" = "Professional"
"Windows 11 Education N" = "EducationN"
"Windows 11 Education" = "Education"
"Windows 11 Enterprise LTSC N" = "EnterpriseSN"
"Windows 11 Enterprise LTSC" = "EnterpriseS"
"Windows 11 Enterprise N" = "EnterpriseN"
"Windows 11 Enterprise" = "Enterprise"
}
foreach ($case in $cases.GetEnumerator()) {
Get-WinUtilEditionIdFromName -EditionName $case.Key | Should -Be $case.Value
}
Get-WinUtilEditionIdFromName -EditionName "Windows 11 Unknown Edition" | Should -Be ""
}
It "writes ei.cfg and removes stale PID.txt for the selected edition" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$sourcesDir = Join-Path $contentRoot "sources"
$logs = [System.Collections.Generic.List[string]]::new()
$logger = { param($message) $logs.Add([string]$message) }
try {
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $sourcesDir "PID.txt") -Value "stale-key" -Encoding UTF8
Set-Content -Path (Join-Path $sourcesDir "ei.cfg") -Value "stale-cfg" -Encoding UTF8
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InstallEditionId "Professional" -Log $logger
Test-Path (Join-Path $sourcesDir "PID.txt") | Should -BeFalse
Test-Path (Join-Path $sourcesDir "ei.cfg") | Should -BeTrue
(Get-Content -Path (Join-Path $sourcesDir "ei.cfg")) -join "|" |
Should -Be "[EditionID]|Professional|[Channel]|Retail|[VL]|0"
($logs -join "|") | Should -Match "Removed sources\\PID\.txt"
($logs -join "|") | Should -Match "Written sources\\ei\.cfg"
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "adds eligible drivers to one install.wim index and does not create `$WinpeDriver$" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDrivers_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'system_pkg'; Name = 'chipset.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'SCSIAdapter' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'group_a\duplicate'; Name = 'audio.inf'; Class = 'Media' },
@{ Path = 'hdx_asusext_apot_g5-tse.inf_amd64_aabbccddeeff0011'; Name = 'hdx_asusext_apot_g5-tse.inf'; Class = 'Extension' },
@{ Path = 'ntprint.inf_x86_7426e1b60aa62272'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2023,10.0.26100.8875' },
@{ Path = 'ntprint.inf_x86_58e7118cdecb935e'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '6/1/2024,10.0.26100.9168' },
@{ Path = 'nested_pkg'; Name = 'main.inf'; Class = 'Net' },
@{ Path = 'nested_pkg\x64'; Name = 'extra.inf'; Class = 'Net' },
@{ Path = 'net_pkg_v2'; Name = 'network2.inf'; Class = 'Net' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$driversInjected = [ref]$false
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
($logs -join '|') | Should -Match 'Warning: boot.wim was not found'
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 9
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Get-WimInfo' }).Count | Should -Be 2
($script:dismCalls -join "`n") | Should -Not -Match '/Cleanup-Image|/Export-Image'
($logs -join '|') | Should -Match "Added 9 of 9 driver packages"
# nested_pkg holds INFs at its root and under x64. The root add already recurses
# into x64, so the subfolder must not get an /Add-Driver call of its own.
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' -and $_ -match ([regex]::Escape('nested_pkg')) }).Count | Should -Be 1
($script:dismCalls -join "`n") | Should -Not -Match ([regex]::Escape('nested_pkg\x64'))
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
($logs -join '|') | Should -Match 'Exported 10 of 11 driver packages \(1 excluded\)'
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e' \(DriverVer 6/1/2024,10\.0\.26100\.9168\)"
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
($logs -join '|') | Should -Match 'DISM mount completed.'
($logs -join '|') | Should -Not -Match '100.0%'
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'hdx_asusext_apot_g5-tse.inf_amd64_aabbccddeeff0011')
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_7426e1b60aa62272')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_58e7118cdecb935e')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'system_pkg')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'group_a\duplicate')
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "injects SCSIAdapter storage drivers into boot.wim index 2 and not `$WinpeDriver`$" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoBootWim_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$bootWim = Join-Path $contentRoot 'sources\boot.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'hdc_pkg'; Name = 'ide.inf'; Class = 'HDC' },
@{ Path = 'name_only_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'iastorhsacomponent.inf_amd64_1b2a068a8496b6a2'; Name = 'iaStorHsaComponent.inf'; Class = 'SoftwareComponent' },
@{ Path = 'iastorhsa_ext.inf_amd64_ba71359697f80d4e'; Name = 'iaStorHsa_Ext.inf'; Class = 'Extension' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
Set-Content -Path $bootWim -Value 'mock-boot'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'boot\.wim' -and $_ -match '/Index:2' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'install\.wim' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 2
$bootMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'boot\.wim' } | Select-Object -First 1
$installMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' -and $_ -match 'install\.wim' } | Select-Object -First 1
$bootMountIndex = [Array]::IndexOf($script:dismCalls.ToArray(), $bootMountCall)
$installMountIndex = [Array]::IndexOf($script:dismCalls.ToArray(), $installMountCall)
$bootAdds = @($script:dismCalls[$bootMountIndex..($installMountIndex - 1)] | Where-Object { $_ -match '/Add-Driver' })
$bootAdds.Count | Should -Be 2
($bootAdds -join "`n") | Should -Match ([regex]::Escape('scsi_pkg'))
($bootAdds -join "`n") | Should -Match ([regex]::Escape('hdc_pkg'))
($bootAdds -join "`n") | Should -Not -Match ([regex]::Escape('net_pkg'))
($bootAdds -join "`n") | Should -Not -Match 'iastorhsa'
($bootAdds -join "`n") | Should -Not -Match ([regex]::Escape('name_only_pkg'))
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 8
($logs -join '|') | Should -Match 'Added 2 of 2 driver packages to boot.wim'
($logs -join '|') | Should -Match 'Added 6 of 6 driver packages to install.wim'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "injects only the newest duplicate storage package into boot.wim" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoBootWimDedup_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$bootWim = Join-Path $contentRoot 'sources\boot.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'iastorvd.inf_amd64_11111111aaaaaaaa'; Name = 'iaStorVD.inf'; Class = 'SCSIAdapter'; DriverVer = '1/1/2023,20.2.1.1016' },
@{ Path = 'iastorvd.inf_amd64_22222222bbbbbbbb'; Name = 'iaStorVD.inf'; Class = 'SCSIAdapter'; DriverVer = '6/1/2024,20.2.8.1028' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
Set-Content -Path $bootWim -Value 'mock-boot'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
$bootAdds = @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' })
@($bootAdds | Where-Object { $_ -match '22222222bbbbbbbb' }).Count | Should -Be 2
@($bootAdds | Where-Object { $_ -match '11111111aaaaaaaa' }).Count | Should -Be 0
($logs -join '|') | Should -Match 'Added 1 of 1 driver packages to boot.wim'
($logs -join '|') | Should -Match 'Added 1 of 1 driver packages to install.wim'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "injects Class=Extension driver packages like any other export" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoExtensionInject_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'ext_pkg_lower'; Name = 'lowercase_extension.inf'; Class = 'extension' },
@{ Path = 'ext_pkg_quoted'; Name = 'quoted_extension.inf'; Class = '"Extension"' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$driversInjected = [ref]$false
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 3
($logs -join '|') | Should -Match 'Exported 3 of 3 driver packages \(0 excluded\)'
$driversInjected.Value | Should -BeTrue
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ext_pkg_lower')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ext_pkg_quoted')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'net_pkg')
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "keeps a stale duplicate folder that contains a retained nested package" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoStaleNestedRetained_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'main.inf_amd64_11111111aaaaaaaa'; Name = 'main.inf'; Class = 'Net'; DriverVer = '1/1/2023,1.0.0.0' },
@{ Path = 'main.inf_amd64_22222222bbbbbbbb'; Name = 'main.inf'; Class = 'Net'; DriverVer = '6/1/2024,2.0.0.0' },
@{ Path = 'main.inf_amd64_11111111aaaaaaaa\retained_child'; Name = 'network.inf'; Class = 'Net' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$driversInjected = [ref]$false
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*main\.inf_amd64_11111111aaaaaaaa' \(DriverVer 1/1/2023,1\.0\.0\.0\) superseded by '.*main\.inf_amd64_22222222bbbbbbbb' \(DriverVer 6/1/2024,2\.0\.0\.0\)"
($logs -join '|') | Should -Match "Keeping excluded driver package directory '.*main\.inf_amd64_11111111aaaaaaaa' because it contains a retained nested package"
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 2
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'main.inf_amd64_11111111aaaaaaaa')
$script:exportedInfsAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'main.inf_amd64_11111111aaaaaaaa\main.inf')
$script:exportedInfsAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'main.inf_amd64_11111111aaaaaaaa\retained_child\network.inf')
$driversInjected.Value | Should -BeTrue
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "removes a stale duplicate INF before recursively adding its retained ancestor" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoStaleNestedExcluded_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'root_pkg'; Name = 'root.inf'; Class = 'Net' },
@{ Path = 'root_pkg\dup.inf_amd64_11111111aaaaaaaa'; Name = 'dup.inf'; Class = 'Media'; DriverVer = '1/1/2020,1.0.0.0' },
@{ Path = 'root_pkg\dup.inf_amd64_22222222bbbbbbbb'; Name = 'dup.inf'; Class = 'Media'; DriverVer = '6/1/2024,2.0.0.0' },
@{ Path = 'root_pkg\dup.inf_amd64_11111111aaaaaaaa\retained_grandchild'; Name = 'storage.inf'; Class = 'SCSIAdapter' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional'
$addDriverCalls = @($script:dismCalls | Where-Object { $_ -match '/Add-Driver' })
$addDriverCalls.Count | Should -Be 1
$addDriverCalls[0] | Should -Match ([regex]::Escape('/Driver:' + (Join-Path $script:driverExportRoot 'root_pkg')))
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'root_pkg\dup.inf_amd64_11111111aaaaaaaa')
$script:exportedInfsAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'root_pkg\root.inf')
$script:exportedInfsAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'root_pkg\dup.inf_amd64_11111111aaaaaaaa\dup.inf')
$script:exportedInfsAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'root_pkg\dup.inf_amd64_11111111aaaaaaaa\retained_grandchild\storage.inf')
$script:exportedInfsAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'root_pkg\dup.inf_amd64_22222222bbbbbbbb\dup.inf')
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "keeps packages from different providers even when the INF name and architecture match" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoProviderCollision_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
@{ Path = 'device.inf_amd64_11111111aaaaaaaa'; Name = 'device.inf'; Class = 'Net'; Provider = 'Contoso'; DriverVer = '1/1/2023,1.0.0.0' },
@{ Path = 'device.inf_amd64_22222222bbbbbbbb'; Name = 'device.inf'; Class = 'Net'; Provider = 'Fabrikam'; DriverVer = '1/1/2024,2.0.0.0' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 2
($logs -join '|') | Should -Match 'Exported 2 of 2 driver packages \(0 excluded\)'
($logs -join '|') | Should -Not -Match 'Excluding stale duplicate driver package'
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'device.inf_amd64_11111111aaaaaaaa')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'device.inf_amd64_22222222bbbbbbbb')
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "drops stale duplicate driver versions and keeps only the highest DriverVer" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoStaleDedup_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures @(
# Three-way duplicate mirroring the real ntprint.inf report: only the newest DriverVer should survive.
@{ Path = 'ntprint.inf_x86_7426e1b60aa62272'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2023,10.0.26100.8875' },
@{ Path = 'ntprint.inf_x86_6688e7b66f8d9fb5'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '1/1/2024,10.0.26100.8972' },
@{ Path = 'ntprint.inf_x86_58e7118cdecb935e'; Name = 'ntprint.inf'; Class = 'Printer'; DriverVer = '6/1/2024,10.0.26100.9168' },
# A duplicate pair where one package is missing DriverVer entirely: the parseable one must win.
@{ Path = 'sample.inf_amd64_11111111aaaaaaaa'; Name = 'sample.inf'; Class = 'Net' },
@{ Path = 'sample.inf_amd64_22222222bbbbbbbb'; Name = 'sample.inf'; Class = 'Net'; DriverVer = '3/1/2024,1.2.3.4' },
# A duplicate pair keyed entirely on case-insensitive DriverVer parsing: the uppercase
# DRIVERVER on the newer package must still be read and win the comparison.
@{ Path = 'caps.inf_amd64_33333333cccccccc'; Name = 'caps.inf'; Class = 'Net'; DriverVer = '1/1/2020,1.0.0.0'; VersionKeyword = 'driverver' },
@{ Path = 'caps.inf_amd64_44444444dddddddd'; Name = 'caps.inf'; Class = 'Net'; DriverVer = '1/1/2021,2.0.0.0'; VersionKeyword = 'DRIVERVER' }
)
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 3
($logs -join '|') | Should -Match 'Exported 3 of 7 driver packages \(4 excluded\)'
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_7426e1b60aa62272' \(DriverVer 1/1/2023,10\.0\.26100\.8875\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*ntprint\.inf_x86_6688e7b66f8d9fb5' \(DriverVer 1/1/2024,10\.0\.26100\.8972\) superseded by '.*ntprint\.inf_x86_58e7118cdecb935e'"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*sample\.inf_amd64_11111111aaaaaaaa' \(DriverVer unknown\) superseded by '.*sample\.inf_amd64_22222222bbbbbbbb' \(DriverVer 3/1/2024,1\.2\.3\.4\)"
($logs -join '|') | Should -Match "Excluding stale duplicate driver package '.*caps\.inf_amd64_33333333cccccccc' \(DriverVer 1/1/2020,1\.0\.0\.0\) superseded by '.*caps\.inf_amd64_44444444dddddddd' \(DriverVer 1/1/2021,2\.0\.0\.0\)"
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_7426e1b60aa62272')
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_6688e7b66f8d9fb5')
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'sample.inf_amd64_11111111aaaaaaaa')
$script:exportRootAtAddDriver | Should -Not -Contain (Join-Path $script:driverExportRoot 'caps.inf_amd64_33333333cccccccc')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'ntprint.inf_x86_58e7118cdecb935e')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'sample.inf_amd64_22222222bbbbbbbb')
$script:exportRootAtAddDriver | Should -Contain (Join-Path $script:driverExportRoot 'caps.inf_amd64_44444444dddddddd')
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "discards partial changes and commits the remaining drivers when one package fails to add" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDriverPartial_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures $script:resilienceDriverFixtures -FailedDriverPath 'group_a\duplicate'
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$driversInjected = [ref]$false
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 2
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
$lastMountCall = $script:dismCalls | Where-Object { $_ -match '/Mount-Image' } | Select-Object -Last 1
$lastMountIndex = [Array]::LastIndexOf($script:dismCalls.ToArray(), $lastMountCall)
$finalMountCalls = @($script:dismCalls[($lastMountIndex + 1)..($script:dismCalls.Count - 1)])
@($finalMountCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be ($script:expectedRootPackages - 1)
($finalMountCalls -join "`n") | Should -Not -Match ([regex]::Escape('group_a\duplicate'))
($logs -join '|') | Should -Match "Added $($script:expectedRootPackages - 1) of $script:expectedRootPackages driver packages"
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
# group_a and group_b share a leaf name, so the warning must name the failing
# package by its subpath; a leaf-only name would read 'duplicate' and be ambiguous.
($logs -join '|') | Should -Match ([regex]::Escape("failed to add driver package 'group_a\duplicate'"))
($logs -join '|') | Should -Match ([regex]::Escape('add-driver:group_b\duplicate completed'))
$driversInjected.Value | Should -BeTrue
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "discards the mount and keeps going when every driver package fails to add" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDriverAllFail_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures $script:resilienceDriverFixtures -FailAllAddDrivers
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$driversInjected = [ref]$true
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -DriversInjected $driversInjected -Log {
param($message)
$logs.Add([string]$message)
}
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be $script:expectedRootPackages
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 0
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be $script:expectedRootPackages
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be $script:expectedRootPackages
@($script:dismCalls | Where-Object { $_ -match '/Get-WimInfo' }).Count | Should -Be 1
($logs -join '|') | Should -Match "none of the $script:expectedRootPackages exported driver packages could be added"
($logs -join '|') | Should -Not -Match "Added 0 of $script:expectedRootPackages"
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
$driversInjected.Value | Should -BeFalse
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "finds exported drivers when the temp path contains wildcard characters" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDriverGlob_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
New-WinUtilDriverExportHarness -Fixtures $script:resilienceDriverFixtures
# A Windows account named like "John [Work]" puts wildcard characters in %TEMP%.
# Get-ChildItem -Path would glob them, find nothing, and abort the whole ISO run.
$originalTemp = $env:TEMP
$bracketTemp = Join-Path ([IO.Path]::GetTempPath()) "WinUtil [Glob] $([guid]::NewGuid())"
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
New-Item -Path $bracketTemp -ItemType Directory -Force | Out-Null
$env:TEMP = $bracketTemp
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
($logs -join '|') | Should -Match "Added $script:expectedRootPackages of $script:expectedRootPackages driver packages"
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 1
($logs -join '|') | Should -Match ([regex]::Escape('add-driver:nested_pkg completed'))
} finally {
$env:TEMP = $originalTemp
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $bracketTemp -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "stops when a potentially partial mount cannot be discarded after a package fails" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDriverDiscardFailure_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
New-WinUtilDriverExportHarness -Fixtures $script:resilienceDriverFixtures -FailAllAddDrivers -FailDiscard
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
{ Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' } |
Should -Throw '*Failed to discard the potentially partial install.wim mount after driver package*'
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 2
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 0
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "discards a partially mounted install.wim after mount failure" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailure_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
if ($Arguments -contains '/Get-WimInfo') {
$global:LASTEXITCODE = 0
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
$global:LASTEXITCODE = 50
'Mount failed'
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} elseif ($Arguments -contains '/Export-Driver') {
$global:LASTEXITCODE = 0
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures @(
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }
)
} else {
$global:LASTEXITCODE = 0
}
}
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
{ Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' } |
Should -Throw '*DISM mount failed*'
@($script:dismCalls | Where-Object { $_ -match '/Get-MountedImageInfo' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "reports a friendly explanation when DISM fails with a known exit code" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailureKnownCode_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
if ($Arguments -contains '/Get-WimInfo') {
$global:LASTEXITCODE = 0
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
$global:LASTEXITCODE = 112
'Mount failed'
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} elseif ($Arguments -contains '/Export-Driver') {
$global:LASTEXITCODE = 0
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures @(
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }
)
} else {
$global:LASTEXITCODE = 0
}
}
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
{ Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' } |
Should -Throw '*112*Disk is full*'
@($script:dismCalls | Where-Object { $_ -match '/Get-MountedImageInfo' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "falls back to the plain exit code for an unmapped DISM failure" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailureUnknownCode_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
if ($Arguments -contains '/Get-WimInfo') {
$global:LASTEXITCODE = 0
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
$global:LASTEXITCODE = 999
'Mount failed'
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} elseif ($Arguments -contains '/Export-Driver') {
$global:LASTEXITCODE = 0
Export-WinUtilTestDriverPackage -Arguments $Arguments -Fixtures @(
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' }
)
} else {
$global:LASTEXITCODE = 0
}
}
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
$thrown = $null
try {
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional'
} catch {
$thrown = $_.Exception.Message
}
$thrown | Should -Be 'DISM mount failed with exit code 999.'
@($script:dismCalls | Where-Object { $_ -match '/Get-MountedImageInfo' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "does not add driver setup artifacts when injection is disabled" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoNoDrivers_$([guid]::NewGuid())"
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InjectCurrentSystemDrivers $false -InstallEditionId 'Core'
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "enables configuration-set fallback when staging OEM setup scripts" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoFallback_$([guid]::NewGuid())"
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InstallEditionId 'Core'
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
$answerFile.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:UseConfigurationSet', $nsMgr).InnerText |
Should -Be 'true'
Test-Path (Join-Path $contentRoot 'sources\$OEM$\$$\Setup\Scripts\FirstLogon.ps1') | Should -BeTrue
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "attempts winget oscdimg install and exits before export when fallback fails" {
$content = Get-Content -Path $script:isoWorkflowPath -Raw
foreach ($expectedText in @(
'oscdimg.exe not found. Attempting to install via winget...',
'Install-WinUtilWinget',
'Get-Command winget',
'$env:ProgramFiles\WinGet\Packages',
'$env:ProgramFiles\WinGet\Links\oscdimg.exe',
'install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements',
'oscdimg.exe still not found after install attempt.',
'oscdimg Not Found'
)) {
$content | Should -Match ([regex]::Escape($expectedText))
}
([regex]::Matches($content, 'function Get-WinUtilOscdimgPath', 'IgnoreCase')).Count | Should -Be 1
$content | Should -Match ([regex]::Escape('Write-WinUtilISOLog -Level "WARN" -Message "oscdimg.exe still not found after install attempt."'))
# The export job stops at the dialog instead of running oscdimg without a binary
$exportOscdimgIndex = $script:exportFunction.IndexOf('$oscdimg = Get-WinUtilOscdimgPath')
$exportDialogIndex = $script:exportFunction.IndexOf('oscdimg Not Found', $exportOscdimgIndex)
$exportRunIndex = $script:exportFunction.IndexOf('Running oscdimg...', $exportOscdimgIndex)
$exportOscdimgIndex | Should -BeGreaterThan -1
$exportDialogIndex | Should -BeGreaterThan $exportOscdimgIndex
$exportRunIndex | Should -BeGreaterThan $exportDialogIndex
$script:exportFunction | Should -Match ([regex]::Escape('return'))
}
Context "FirstLogon update service restoration" {
BeforeAll {
[xml]$unattend = Get-Content -LiteralPath $script:autoUnattendPath -Raw
$firstLogon = $unattend.SelectSingleNode("//*[local-name()='File' and @path='C:\Windows\Setup\Scripts\FirstLogon.ps1']").InnerText
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput($firstLogon, [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw 'FirstLogon script failed to parse.' }
$blocks = @($ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.ScriptBlockExpressionAst] -and
$node.ScriptBlock.Find({
param($command)
$command -is [System.Management.Automation.Language.CommandAst] -and
$command.GetCommandName() -eq 'Set-Service'
}, $false)
}, $true))
if ($blocks.Count -ne 1) { throw 'Expected exactly one service restoration block.' }
# Never execute the surrounding FirstLogon cleanup, downloads, or installer.
$commands = $blocks[0].ScriptBlock.FindAll({
param($node)
$node -is [System.Management.Automation.Language.CommandAst]
}, $true)
foreach ($command in $commands) {
if ($command.GetCommandName() -notin @('reg.exe', 'Set-Service', 'Set-ItemProperty')) {
throw "Unexpected command in restoration block: $($command.Extent.Text)"
}
}
$script:restoreServices = $blocks[0].ScriptBlock.GetScriptBlock()
function reg.exe { param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments) }
}
BeforeEach {
Mock reg.exe { }
Mock Set-Service { }
Mock Set-ItemProperty { }
}
It "restores ordinary services and writes the protected Medic startup value directly" {
& $script:restoreServices
Should -Invoke Set-Service -Times 3 -Exactly
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'BITS' -and $StartupType -eq 'Manual' -and $ErrorAction -eq 'Continue' }
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'wuauserv' -and $StartupType -eq 'Manual' -and $ErrorAction -eq 'Continue' }
Should -Invoke Set-Service -Times 1 -Exactly -ParameterFilter { $Name -eq 'UsoSvc' -and $StartupType -eq 'Automatic' -and $ErrorAction -eq 'Continue' }
Should -Invoke Set-Service -Times 0 -Exactly -ParameterFilter { $Name -eq 'WaaSMedicSvc' }
Should -Invoke Set-ItemProperty -Times 1 -Exactly
Should -Invoke Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -and
$Name -eq 'Start' -and $Value -eq 3 -and $Type -eq 'DWord' -and $ErrorAction -eq 'Continue'
}
}
It "keeps failures observable while attempting the remaining restoration work" {
$script:restorationCalls = [System.Collections.Generic.List[string]]::new()
Mock Set-Service {
param($Name, $ErrorAction)
$script:restorationCalls.Add($Name)
if ($script:restorationCalls.Count -eq 1) {
Write-Error 'simulated service restoration failure' -ErrorAction $ErrorAction
}
}
Mock Set-ItemProperty {
param($ErrorAction)
$script:restorationCalls.Add('Medic registry')
Write-Error 'simulated Medic registry failure' -ErrorAction $ErrorAction
}
$output = @(& $script:restoreServices 2>&1)
$failures = @($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] })
$failures.Count | Should -Be 2
$failures[0].Exception.Message | Should -Be 'simulated service restoration failure'
$failures[1].Exception.Message | Should -Be 'simulated Medic registry failure'
$script:restorationCalls.Count | Should -Be 4
@($script:restorationCalls | Select-Object -First 3 | Sort-Object) | Should -Be @('BITS', 'UsoSvc', 'wuauserv')
$script:restorationCalls[3] | Should -Be 'Medic registry'
}
}
}