diff --git a/map.json b/map.json index 4b20fd28..67e05f55 100644 --- a/map.json +++ b/map.json @@ -138,5 +138,10 @@ "id": "linux-detect-nvme-readiness", "path": "src/linux/linux-detect-nvme-readiness.sh", "description": "Read-only check of whether a Linux OS disk can boot from an NVMe disk controller. Reports unstable /dev/sd* references in /etc/fstab, whether the initramfs contains an NVMe driver, and the dracut hostonly setting. Makes no changes. NOTE: use option --run-on-repair." + }, + { + "id": "win-enable-nvme-boot-driver", + "path": "src/windows/win-enable-nvme-boot-driver.ps1", + "description": "Enables the Windows inbox stornvme boot-start driver on the offline OS disk of a VM that no longer boots after switching to an NVMe disk controller. Mode=Report (default) is read-only; Mode=Repair backs up, writes and verifies; Mode=Rollback restores the backup emitted by Repair. NOTE: use option --run-on-repair." } ] diff --git a/src/windows/win-enable-nvme-boot-driver.ps1 b/src/windows/win-enable-nvme-boot-driver.ps1 index 46a4cfb8..b177287a 100644 --- a/src/windows/win-enable-nvme-boot-driver.ps1 +++ b/src/windows/win-enable-nvme-boot-driver.ps1 @@ -22,6 +22,10 @@ # ######################################################################################################### +# Mode and BackupFile are used inside the Invoke-WithHive script block below; PSScriptAnalyzer +# cannot see through that script block, so PSReviewUnusedParameter reports them as unused. +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', + Justification = 'Mode and BackupFile are used inside the Invoke-WithHive script block below; PSScriptAnalyzer cannot see through that script block.')] Param( [Parameter(Mandatory = $false)][ValidateSet('Report', 'Repair', 'Rollback')][string]$Mode = 'Report', [Parameter(Mandatory = $false)][ValidatePattern('^[A-Za-z]:?$')][string]$OsDriveLetter = '', diff --git a/tests/test-map-catalog.ps1 b/tests/test-map-catalog.ps1 new file mode 100644 index 00000000..08c0cf1e --- /dev/null +++ b/tests/test-map-catalog.ps1 @@ -0,0 +1,56 @@ +# Catalogue tests for map.json — the public run-id contract. +# Run from the repository root: +# pwsh -NoProfile -File ./tests/test-map-catalog.ps1 + +$ErrorActionPreference = 'Stop' +$repositoryRoot = Split-Path $PSScriptRoot -Parent +$mapPath = Join-Path $repositoryRoot 'map.json' + +function Assert-True { + Param([bool]$Condition, [string]$Message) + if (-not $Condition) { throw "ASSERTION FAILED: $Message" } +} + +# A run id that resolves to nothing, or to the wrong file, fails only at the moment an operator is +# running a repair against a broken VM. That is the worst possible time to discover it. +$catalog = @(Get-Content -LiteralPath $mapPath -Raw | ConvertFrom-Json) +Assert-True ($catalog.Count -gt 0) 'map.json must contain at least one entry.' + +foreach ($entry in $catalog) { + Assert-True (-not [string]::IsNullOrWhiteSpace($entry.id)) 'Every entry needs an id.' + Assert-True (-not [string]::IsNullOrWhiteSpace($entry.path)) "Entry '$($entry.id)' needs a path." + Assert-True (-not [string]::IsNullOrWhiteSpace($entry.description)) "Entry '$($entry.id)' needs a description." + Assert-True ($entry.path -notmatch '\\') "Entry '$($entry.id)' must use forward slashes; the path is used in a URL." + Assert-True (Test-Path -LiteralPath (Join-Path $repositoryRoot $entry.path) -PathType Leaf) ` + "Entry '$($entry.id)' points at '$($entry.path)', which does not exist." +} + +$duplicates = @($catalog | Group-Object id | Where-Object Count -gt 1) +Assert-True ($duplicates.Count -eq 0) "Duplicate run ids: $(($duplicates | ForEach-Object Name) -join ', ')." + +# The extension resolves a run id to a path and then downloads the whole bundle. An entry whose OS +# does not match its directory sends a PowerShell script to a Linux guest, or the reverse. +foreach ($entry in $catalog) { + if ($entry.path -like 'src/windows/*') { + Assert-True ($entry.path -like '*.ps1') "Windows entry '$($entry.id)' must point at a .ps1 file." + } + elseif ($entry.path -like 'src/linux/*') { + Assert-True ($entry.path -like '*.sh') "Linux entry '$($entry.id)' must point at a .sh file." + } + else { + throw "Entry '$($entry.id)' is outside src/windows and src/linux: '$($entry.path)'." + } +} + +$nvmeRecovery = @($catalog | Where-Object id -eq 'win-enable-nvme-boot-driver') +Assert-True ($nvmeRecovery.Count -eq 1) 'win-enable-nvme-boot-driver must be registered exactly once.' +Assert-True ($nvmeRecovery[0].path -eq 'src/windows/win-enable-nvme-boot-driver.ps1') ` + 'win-enable-nvme-boot-driver must point at the Windows recovery script.' +Assert-True ($nvmeRecovery[0].description -match '--run-on-repair') ` + 'The recovery run id operates on an attached offline disk, so its description must say --run-on-repair.' + +foreach ($detector in 'win-detect-nvme-readiness', 'linux-detect-nvme-readiness') { + Assert-True (@($catalog | Where-Object id -eq $detector).Count -eq 1) "$detector must stay registered." +} + +Write-Host "PASS: map.json catalogue — $($catalog.Count) entries, unique ids, existing paths, OS/extension agreement, NVMe run ids registered."