Testing

Test-WinRE.ps1 is WinRE Manager’s read-only test harness. It shows you what the production script would see on a machine, without changing anything, so you can confirm the machine is in a state production can work with before you deploy. This document explains how to run it, what each menu option does, and what the results mean.

The harness exists to serve the project’s four design invariants, which architecture.md states in this order: never break Windows RE, never leave a machine without a working recovery route, minimize the reagentc /disable → reagentc /enable window, and do no work unless needed; prepare everything before touching anything. Two of those rules bear directly on the harness:

When to run the harness

Run Test-WinRE.ps1 in any of the following situations:

The harness requires no elevation and modifies nothing. It is safe to run on any machine, at any time, including on a machine you are about to hand to a user.

What the harness is

An interactive PowerShell script that:

It is the primary tool for pre-flight validation on an unfamiliar machine.

Current version: v27. The version history is:

See the .NOTES block at the top of scripts\Test-WinRE.ps1 for the complete per-version change list.

The harness has no mirror of v45 patch 1’s shrink-first pipeline. It reads the machine’s state and reports what production would do, but it does not simulate the destructive path or the plan. The shrink-first code paths are exercised on disposable VMs (see “v45 destructive-path regression” below), not through the harness.

v27 changes

Four changes, all downstream of the v48 patch 1 cycle.

The parser self-test count is unchanged from v25 (seventeen checks).

v26 changes

One diagnostic addition, downstream of the production v47 patch 3 plan-rejection enrichment.

Plan adjacency preview. Option 1 now runs a read-only mirror of the geometric and partition-identity subset of production’s Get-PartitionPlan rejection checks against the current layout. On a clean layout the section prints two green lines confirming the layout would not be rejected on the checks evaluated; on a separated-recovery or oversized-recovery layout it prints the enriched rejection reason(s) that production would log. The preview does not compute a bucket size or a planned extent, so the rejections related to planned-extent sizing are not evaluated. A clean preview is not a claim that the full plan would succeed, and the section says so explicitly. A new Get-HarnessPartitionRef helper renders a partition the same way production’s Format-PartitionRef does.

The parser self-test count is unchanged. The preview is diagnostic and does not record a PASS/FAIL/SKIP result.

v25 changes

Six functional changes and one comment set, all downstream of the production v47 patch 3 cycle. See the v47 patch 3 entry for the full context.

The parser self-test count moved from sixteen to seventeen with the Compare-WimServicingMetadata check.

v24 changes

Two changes, both downstream of the v47 patch 2 cycle.

1. The active-WIM diagnostic probe now matches production’s dual-path resolver. The prior code constructed <registered-location>\Recovery\WindowsRE\winre.wim unconditionally. That path is correct when reagentc /info returns a partition root, but doubles the path when it returns ...\Recovery\WindowsRE (producing ...\Recovery\WindowsRE\Recovery\WindowsRE\winre.wim and reporting the active WIM as missing when it is present). Production handles both forms via Ensure-RecoveryPartitionAccess, which returns a drive root or the subpath depending on which form the registered location was in. The harness now probes both forms in order (<location>\Recovery\WindowsRE\winre.wim, then <location>\winre.wim) and uses the first that resolves to a readable file.

2. Get-ThisMachineProfile trims Win32_ComputerSystemProduct.Version. Mirrors production’s Get-HardwareObject change in v47 patch 2. Some vendors pad the field with trailing whitespace (observed: ASUS sets it to "1.0 "); without trimming, the harness’s DSI mirror could compute a different HW component than production on such machines, causing a false Option-S mismatch.

v23 changes

Three changes, all downstream of the v47 patch 1 cycle.

1. $ProductionScriptVersion default bumped 46 → 47. The harness’s Get-DesiredStateId mirror carries this default to track production’s $ScriptVersion. Left at 46 it would report a false DSI MISMATCH for every state file v47 production writes, exactly the drift v21 and v22 corrected for earlier bumps. The parameter-header comment continues to name the tracking rule. The harness’s own parser self-test (Check 15) still deliberately passes 44 and 43 to prove version-sensitivity; those values are unchanged.

2. Show-StateFileParity now displays DeployedWinREMetadata. The state file’s DISM servicing metadata anchor (v47 patch 1’s DeployedWinREMetadata field, in <Version>|<SPBuild> form) renders in the parity output alongside PendingReboot, LastEnableResult, EnableFailureAttempts, and RepairAttempts. When present, it renders green. When absent (a v46-or-earlier state file), it renders yellow with a note that production v47 will force a rebuild on the next run because the drift detector has no anchor to compare against. Purely diagnostic; no production decision depends on the harness reading this field.

3. Harness version string bumped 22 → 23 in the menu title and the startup Rule.

v22 changes

Three changes, all downstream of the v46 patch 2 cycle.

1. Mirrored production v46 patch 2’s Get-WinREState version parsing. The harness’s Get-WinREState now extracts Windows RE Version from reagentc /info and returns it as Version. The parsed-state block in Option 1 reports the value alongside Status and Location, and a new “Build numbers” section renders the registered WinRE version, the active WIM build, and (when present) the backup WIM build at C:\Recovery\WindowsRE\winre.wim. This answers the operator’s question “what builds are we about to replace?” before any destructive operation.

2. New parser self-test check for the version regex. Parser: reagentc version reports [OK] when the version line matches, [FAIL] when WinRE is Enabled and the line is missing, and [SKIP] when WinRE is Disabled or reagentc suppressed the line. The self-test total moves from fifteen checks to sixteen. The check mirrors production v46 patch 2’s extraction logic: if the regex fails on a machine where WinRE is Enabled, the production log’s WinRE ... Version: line will read unknown, which is a signal that the reagentc output format has shifted under both the harness and production.

3. $ProductionScriptVersion default bumped 45 → 46. The harness’s Get-DesiredStateId mirror carries this default to track production’s $ScriptVersion. Left at 45 it would report a false DSI MISMATCH for every state file v46 production writes, exactly the drift v21 corrected for the 44 → 45 case. The parameter-header comment continues to name the tracking rule. The harness’s own parser self-test (Check 15) still deliberately passes 44 and 43 to prove version-sensitivity; those values are unchanged.

v21 changes

Two changes. This was a standalone harness correction not tied to a production release.

1. DSI-mirror drift corrected. The harness’s Get-DesiredStateId mirror carried [int]$ProductionScriptVersion = 44, while production v45 patch 1 ships $ScriptVersion = 45. Because the harness computes the same seven-field recipe production does, its default was one version behind, and every comparison against a v45-written state file returned DSI MISMATCH — production will treat the state file as stale for a state file that production would actually accept. The default is now 45, and the parameter-header comment names the version boundary explicitly: this default must be bumped whenever production’s $ScriptVersion is bumped. The harness’s own parser self-test (Check 15) still deliberately passes 44 and 43 to prove version-sensitivity; those values are unchanged.

2. Show-StateFileParity displays the state file’s RepairAttempts field. The v44 patch 6 state file carries RepairAttempts alongside PendingReboot, LastEnableResult, and EnableFailureAttempts; the harness’s parity display previously listed the other three but not this one. Purely additive; no existing display line changed.

v20 changes

Two changes.

1. The active-location classifier requires a type-coded partition on the OS disk for the DEDICATED verdict. Mirrors production’s active-location classifier change (patch 3 of the v44 patch 7 cycle). The previous harness logic computed a single $isRec flag from GptType-or-MbrType, and if that was $false it fell back to a Get-Volume -Partition label check that promoted a label-only match to $isRec = $true. The classifier now computes $isTypedRecovery and $isLabelRecovery separately, and the branches report:

The LABEL-ONLY verdict is new in v20. Before v20, a machine whose reagentc-registered WinRE location was a Basic Data partition labelled “Recovery” would have been reported as DEDICATED by the harness, while production would have taken the full-update path. This was a false positive that could have misled a field engineer. Production’s final-verification classifier (patch 4 of the v44 patch 7 cycle) has no direct harness equivalent and is not claimed; the harness classifies the reagentc-registered location, which is the active-location decision point.

2. Menu box alignment fixed. The menu box’s border is 66 columns wide interior (plus the two border characters and two leading spaces). The three content rows — title, working directory, and detected — were each padded to values that did not equal interior width minus the leading space, so the closing border character was misaligned on two of the three rows. All three rows now pad their content to exactly 65 columns, truncating with an ellipsis if content overflows. Purely cosmetic; no functional change.

The output format for every other section is unchanged from v16.

v19 changes

Two Option S corrections.

  1. Option S reports SKIP (not PASS) when the state file is absent and the VMD query is indeterminate. Before v19, Show-StateFileParity checked for the state file’s existence before consulting the VMD query result, and returned PASS with detail no state file (rebuild expected) whenever the file was missing. That was correct when VMD presence was determinable, but wrong when the VMD query had failed: production defers the entire run with EXIT_WARNING in that case rather than taking the full-update path, and the harness’s PASS disagreed with what a live run would do. The missing-state-file branch now checks $vmdQueryOk and records SKIP with detail No state file; VMD query indeterminate. The existing INDETERMINATE / SKIP handling for the state-file-present case is unchanged.
  2. Option S wording clarified. The header now reads “does the stored DesiredStateId match current inputs? This is not a full-flow simulation.” The DSI MATCH verdict reads “the stored deployment ID matches current inputs” followed by a note that production separately evaluates WinRE state/location, recovery-partition count, active WIM hash, pending-reboot/repair state, BitLocker, and other startup/flow gates before deciding what to do. The prior wording — “production will accept the state file” — implied that DSI equality alone was sufficient.

v18 changes

v18 is a combined mirror of production v44 patch 6 and a set of harness-specific fixes. The mirror changes keep the harness’s outputs in lockstep with production’s actual behavior; the harness-specific fixes close issues that the harness’s own review surfaced.

Mirror of production v44 patch 6:

  1. VMD query-failure handling. Options 1 and S now treat a PnP enumeration error as an indeterminate result rather than absence. Option 1’s VMD hardware presence section reports INDETERMINATE with the enumeration error message. Option S records SKIP in the results with reason VMD query indeterminate and prints an INDETERMINATE verdict instead of a DSI MATCH or DSI MISMATCH. Before v18, the harness could produce a “definitive” DSI that disagreed with what a live production run would compute, which is the opposite of what Option S is for.
  2. Lenovo map resolution status machine. Get-LenovoWinPEPack in the harness now sets $Script:LenovoPackResolution to the same five states as production. Test-OemMaps distinguishes malformed-entry (red, marks the test failed) from no-entry (gray, informational), and treats map-unavailable (yellow, marks the test failed) separately from both.

Harness-specific fixes:

  1. Get-DriverManifest extracted as a shared helper. Options 2, 9, and S now all use the same two-attempt / 2-second-sleep / WARN-log retry policy that production uses. Previously each option had its own fetch code, and they had drifted from each other and from production.
  2. Cleanup footgun closed. The harness refuses to delete $TestDir on exit when the directory pre-existed the run and already contained entries. The pre-existing state is captured before any directory work. This prevents Remove-Item $TestDir -Recurse -Force from wiping a user-supplied path like C:\Users\Me\Desktop.
  3. Stale extraction destinations are cleared before each extraction. Applies to Invoke-VendorExtraction, Invoke-CabExtraction, and the GitHub base-WIM test’s working directory. Before v18, a stale INF file left by an earlier run could satisfy a failed extraction’s INF-count success check. The Lenovo “non-zero exit but INFs present” branch was the sharpest case: it treats an INF-count greater than zero as success regardless of the extractor’s exit code, so a stale INF could mask a genuine extraction failure.
  4. Test-VmdDrivers records SKIP (not PASS) when the Intel CPU generation cannot be parsed. Driver applicability was not evaluated in that case, so a PASS would misrepresent what the harness actually checked. The raw CPU string is logged so the parser can be extended. The result detail reads Intel CPU generation could not be parsed.

Five new parser self-test checks (see “The parser self-test” below):

  1. DISM cmdlet availability. Verifies Mount-WindowsImage, Dismount-WindowsImage, Get-WindowsImage, Add-WindowsDriver, and Get-WindowsDriver are all present. Production hard-depends on all five; a missing cmdlet would only be discovered at injection time.
  2. Get-Disk shape. Verifies Number, FriendlyName, PartitionStyle, Size, BootFromDisk, IsSystem, and IsBoot are present.
  3. Get-Volume shape. Verifies DriveLetter, FileSystemLabel, FileSystem, Size, SizeRemaining, DriveType, HealthStatus, and UniqueId are present.
  4. CPU-generation parser regression table. Fourteen cases spanning 11th, 12th, 13th Gen, Core, Core Ultra, AMD, Celeron, Pentium, Xeon, and Atom CPU strings. Catches a Windows update or firmware rename that changes Win32_Processor.Name in a way that would silently desynchronise DesiredStateId.
  5. DesiredStateId determinism and input sensitivity. Same inputs must hash identically; flipping VMD presence must change the hash; flipping ProductionScriptVersion must change the hash; the output must be 64 hex characters. A silent change to the DSI recipe would be caught before it desynchronised every deployed state file.

Cosmetic fixes:

  1. Write-KV overflow handling. A key at or past $KeyWidth now gets a separating space before its value. Previously, Manifest VMD device IDs (23 chars, one over the 22-char key column) rendered flush against the value.
  2. Harness .NOTES block count “Four” → “Five” for the count of future-proofing checks (the v18 harness .NOTES block miscounted).
  3. Harness .NOTES block wording “in the detail” → “in the log line” for the Test-VmdDrivers SKIP path (the raw CPU string is logged via Say, not recorded in Record -Detail).

The output format is unchanged from v16 in every respect except the new colour tagging for the v18 checks, the five new parser self-test lines, and the new INDETERMINATE verdict colouring in Option S.

Running it

# Interactive
.\scripts\Test-WinRE.ps1

# Non-interactive: runs "all relevant for this machine" and exits
.\scripts\Test-WinRE.ps1 -NonInteractive

# Do not offer to delete the working directory on exit
.\scripts\Test-WinRE.ps1 -Keep

# Custom working directory
.\scripts\Test-WinRE.ps1 -TestDir D:\WinRETest

The default working directory is C:\Temp\WinRETest. All downloads and extractions go there.

Cleanup guard (v18). The harness refuses to delete $TestDir on exit when the directory pre-existed the run and already contained entries. This closes a footgun: before v18, passing -TestDir C:\Users\Me\Desktop and then choosing “no” at the cleanup prompt would have deleted the entire desktop directory. The pre-existing state is captured before any harness directory work; a directory that did not exist, or that existed but was empty, is deleted on exit as before. To override the guard, delete the directory manually or pass -Keep.

Output style (v16; extended in v17 through v26, with v20’s menu box alignment, v22’s new Build numbers section, v23’s Option S DeployedWinREMetadata display, v24’s dual-path active-WIM probe and Version trim, v25’s colour-tagged parser self-test additions and seven-component DSI-mismatch sentence, and v26’s Plan adjacency preview section as the departures)

As of v16 the harness’s output is colour-coded and, in several sections, tabular. The changes are presentation-only: the checks, the menu structure, the arguments, and the read-only contract are unchanged. v17, v18, v19, and v21 do not modify the output format. v20 changes only the menu box’s interior alignment; every other section’s output format is unchanged. v22 adds a Version line to the parsed-state block and a new “Build numbers” section. v23 adds a DeployedWinREMetadata line to Option S’s parity output. The rest of the output format is unchanged from v16.

Colour-coded values

Diagnostic values are rendered with a colour that reflects their state:

Tables

The All disks, All partitions, All volumes, and Recovery partitions sections of Option 1 render as aligned tables with fixed column headers. Each row is colour-coded by relevance: the OS partition and OS disk stand out in White, the reagentc-registered recovery partition stands out in Cyan, and other rows render Gray. Recovery-partition rows are further colour-coded green when the partition is both type-coded and on the OS disk, yellow when type-coded but on a secondary disk, and red when it is not type-coded at all.

The remaining sections — hardware, reagentc raw output, WinRE parsed state, OS partition / OS disk, OS partition supported sizes, bucket sizing preview, Build numbers, Windows Setup state, BitLocker on C:, target recovery partition state, VMD hardware presence, and the parser self-test — render as aligned key/value pairs or as free-form diagnostic lines. The parser self-test in particular is line-per-check: one [OK] / [FAIL] / [SKIP] line per check, with the state tag colour-coded.

Write-Diag and Write-KV

Diagnostic output in Option 1 and its sub-sections uses two helpers:

These replace direct Say calls in the diagnostic output. Download and extraction tests, the menu, and top-level banners continue to use Say and Rule.

Say colour-codes by -Level: INFO Gray, WARN Yellow, ERROR / FATAL Red, PASS / OK Green, FAIL Red, SKIP DarkGray, HEAD Cyan.

The menu

  1. System diagnostic (read-only info gathering)
  2. Driver manifest fetch
  3. OEM maps fetch + resolve (Dell, HP, Lenovo)
  4. GitHub base WIM - Win10 (download + extract + build check)
  5. GitHub base WIM - Win11 (download + extract + build check)
  6. HP WinPE pack (download + extract)
  7. Dell WinPE pack (prompts for OS)
  8. Lenovo WinPE pack (prompts for MT)
  9. VMD drivers (per manifest, filtered for this machine)
  S. State file parity check (production DSI vs on-disk state file)
  A. All relevant for this machine
  B. All of the above
  R. Print results summary
  Q. Quit

The menu renders the shortcut letter in colour, defaulting to Cyan. The state-file parity check (S) renders in Magenta, the aggregate runs (A, B) in Green, the summary (R) in Yellow, and quit (Q) in Red. The colour choice is a visual hint, not a semantic change to the option.

Option 1 — System diagnostic

Read-only information gathering. Dumps:

Then it runs the parser self-test (below).

BitLocker (C:) warnings (v12, updated in v14)

The BitLocker section of the diagnostic distinguishes two categories of state on C: that are relevant to the OS-fallback route. Both produce a warning block, in yellow.

Hazardous states. When the section reports ProtectionStatus not On together with a VolumeStatus of EncryptionInProgress, DecryptionInProgress, EncryptionPaused, or DecryptionPaused, the harness prints:

  WARNING: ProtectionStatus=Off, VolumeStatus=EncryptionInProgress
           Device Encryption is actively encrypting or decrypting C:.
           Production refuses the OS-fallback path in this state.
           The enable-only and dedicated-partition paths are still
           available because they target the recovery partition.

Ambiguous state. When the section reports ProtectionStatus not On together with VolumeStatus=FullyEncrypted, the harness prints a distinct block:

  WARNING: ProtectionStatus=Off, VolumeStatus=FullyEncrypted
           This state is ambiguous: legitimate suspension, OR
           Device Encryption Waiting-for-Activation.
           Production refuses the OS-fallback path in this state.

The wording of these warnings was updated in harness v14. The v12/v13 wording said production “will refuse destructive partition work” in these states, which described the removed OS-volume-gate policy and would have told a field engineer to defer a run that production would actually succeed on. Under the v43 patch 5 (further revision 5) policy, only the OS-fallback route depends on C:’s BitLocker state; the enable-only and dedicated-partition routes target the recovery partition directly and are not affected. As of v44 patch 6, the destructive partition path does not consult C:’s state either — only the OS-fallback route does.

Confirmed-safe states. VolumeStatus=FullyDecrypted (or empty) with ProtectionStatus=Off, and ProtectionStatus=On at any conversion status, are safe for the OS-fallback route. They do not trigger either warning.

The classification predicate matches production’s Test-VolumeEncrypted: the four mid-operation states are hazardous, FullyEncrypted+Off is ambiguous, and everything else is safe. The harness does not call into production’s Test-VolumeEncrypted — it queries Get-BitLockerVolume directly and applies the same predicate — but a diagnostic that disagreed with production about which states are hazardous, ambiguous, or safe would be worse than no diagnostic at all.

Target recovery partition state (v14)

The v43 patch 5 (further revision 5) policy is target-volume-based. reagentc’s BitLocker check is on the partition it is being asked to enable WinRE on, not on C:. The harness reports the BitLocker state of the partition reagentc is registered to. This is the state that determines whether the production script will need to run manage-bde -off on the target partition before calling reagentc /enable, and whether Set-RecoveryPartitionReadyForWinRE will find the partition already clean or need to prepare it.

The section header is drawn with a divider, then the details are printed as aligned key/value pairs. The diagnostic prints one of the following four classifications, depending on what manage-bde -status reports for the target partition.

Unmanaged by BitLocker. The partition is not managed by BitLocker — this is the expected state for a properly-typed recovery partition. The block reads:

  Target recovery partition state
  ────────────────────────────────
  Registered partition  Disk <n> Part <m>
  Querying              manage-bde -status <letter or volume GUID>
  Classification        unmanaged by BitLocker (reagentc /enable will accept this)

Fully decrypted. BitLocker manages the partition, but it is currently unencrypted. Also acceptable.

  Target recovery partition state
  ────────────────────────────────
  Registered partition  Disk <n> Part <m>
  Querying              manage-bde -status <letter or volume GUID>
  Classification        fully decrypted (reagentc /enable will accept this)

BitLocker-managed. The partition is encrypted or actively encrypting. Production will decrypt it in place before calling reagentc /enable; the harness prints the conversion status and a note about the expected additional runtime.

  Target recovery partition state
  ────────────────────────────────
  Registered partition  Disk <n> Part <m>
  Querying              manage-bde -status <letter or volume GUID>
  Conversion Status     <value>
  Classification        BitLocker-managed - production will decrypt in place
                  Expect up to 300s of additional runtime on the next run.

Note: the final Expect up to 300s line uses a shallower indent than the value column above it. That is a known cosmetic quirk in harness v16 and does not indicate a problem with the diagnostic. A future harness version may tidy the alignment.

Could not parse. manage-bde -status produced output the harness did not recognise. Production’s helper retries the query at runtime.

  Target recovery partition state
  ────────────────────────────────
  Registered partition  Disk <n> Part <m>
  Querying              manage-bde -status <letter or volume GUID>
  Classification        could not parse manage-bde output

When WinRE is not registered to a partition at all — the common case on a machine where WinRE is Disabled and no registered location exists — the section prints:

  Target recovery partition state
  ────────────────────────────────
  WinRE is not registered to a partition (Disabled, or unresolved).
  If the plan is OS-fallback, production checks C: directly.

The harness remains read-only: it does not assign a drive letter. If the target partition already carries a drive letter, that letter is used for the manage-bde -status query. If it does not, manage-bde is invoked against the volume’s UniqueId (the \\?\Volume{...}\ form), which manage-bde accepts as a <volume> argument.

The block is informational, not a PASS/FAIL/SKIP record. It does not appear in the summary.

Build numbers (v22)

The “Build numbers” section renders immediately after the bucket sizing preview and before the Windows Setup state. It reports three values side by side:

  Build numbers
  ─────────────
  Registered WinRE      10.0.26100.9545
  Active WIM build      26100
  Backup WIM build      26100  C:\Recovery\WindowsRE\winre.wim

The three values are the harness’s read-only mirror of production v46 patch 2’s build-drift logging. A field engineer diagnosing whether a run is about to replace a newer registered WinRE with an older WIM reads these three values before running production. If the registered version is newer than the build of the WIM the harness sees at the reagentc location, that is a signal — but not necessarily a fault — and the harness reports the values without flagging.

The block does not produce a PASS/FAIL/SKIP record and does not appear in the summary.

VMD hardware presence (v18)

The VMD section runs the same Get-PnpDevice query that production uses, with the same fail-closed semantics. Three outcomes:

The INDETERMINATE outcome is new in v18 and mirrors production v44 patch 6.

Windows Setup state (v13)

The diagnostic reads HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State → ImageState and warns when the value is present and not IMAGE_STATE_COMPLETE. Production’s Audit Mode guard defers in that state before any state-modifying action. The diagnostic’s check mirrors the guard so that a field engineer pre-flighting a freshly imaged machine sees the deferral condition before running production.

Option S — State file parity check (v15; VMD handling and wording refined in v18, v19, and v21; DeployedWinREMetadata display added in v23)

Read-only. Recomputes the DesiredStateId production would compute right now — reading the live driver manifest, resolving the OEM package for this machine’s vendor, and detecting VMD hardware presence — then reads the on-disk state file at C:\Recovery\OEM\winre_state.json and reports whether production would accept it or treat it as stale.

The output names the computed ID and the stored ID side by side, then prints one of three verdicts:

If the state file does not exist, the harness reports that production will take the full-update path (which is correct: needInject = $true when there is no state) — except when the VMD query is also indeterminate, in which case the harness records SKIP and explains that a live production run would defer rather than start the update (v19).

As of v21 the parity display also includes the state file’s RepairAttempts field alongside PendingReboot, LastEnableResult, and EnableFailureAttempts. Before v21 the display omitted this field even though the state file carries it; a field engineer diagnosing a pending-reboot loop had to read the JSON directly.

As of v23 the parity display also includes the state file’s DeployedWinREMetadata field. When present (a state file written by a v47 production run), it renders green. When absent (a v46-or-earlier state file), it renders yellow with a note that production v47 will force a rebuild on the next run because the drift detector has no anchor to compare against. Before v23 the field was silently absent from the display; a field engineer diagnosing a rebuild-on-first-v47-run had to read the JSON directly.

Option S is the field engineer’s tool for answering “is this machine about to rebuild?” without running the production script. It does not exercise the offline fallback (v44 patch 5): Option S always performs a live manifest fetch and a live VMD detection, so on an offline machine the harness reports the fetch failure rather than a DSI verdict.

Options 2 through 9 and A/B

Exercise the download and extraction paths. Each option:

Option A runs the subset relevant to the current machine (its OS, its vendor, its CPU). Option B runs everything.

The parser self-test

Seventeen checks as of v25 (sixteen as of v22, fifteen as of v18, ten before v18); v23, v24, v26, and v27 did not change the count or the contents. v25 added the Compare-WimServicingMetadata regression check. Each records PASS, FAIL, or SKIP in $Script:Results and prints one [OK] / [FAIL] / [SKIP] line. The state tags are colour-coded (green [OK], red [FAIL], dark gray [SKIP]); the line format itself is unchanged from earlier versions. v19 through v21 did not change the check count or the check contents; v22 added one check.

# Check What it verifies Since
1 reagentc status regex (Enabled\|Disabled) matches at least one line of reagentc /info output. v1
2 reagentc location regex (GLOBALROOT\|Volume GUID) matches at least one line. v1
2b reagentc version regex Windows RE Version: matches at least one line. SKIP when the line is absent (WinRE Disabled, or reagentc suppressed it). v22
3 manage-bde protection regex Protection On or Protection Off appears. v1
4 manage-bde conversion status regex A Conversion Status: value matches, or the string could not be opened by BitLocker appears. v1
5 Get-BitLockerVolume shape ProtectionStatus and VolumeStatus are non-null. SKIP if not elevated. v1
6 OS resolution Get-OSPartition and Get-OSDisk both resolve. v1
7 Get-Partition shape A sample partition exposes DiskNumber, PartitionNumber, Size, GptType, MbrType, IsBoot, IsSystem, IsActive. v1
8 WinRE location resolution The reagentc location resolves to a partition. SKIP if location is empty. v1
9 Get-RecoveryPartitions Returns at least one partition. v1
10 Get-PartitionSupportedSize Returns SizeMin and SizeMax. SKIP if no OS partition. v1
11 DISM cmdlets Mount-WindowsImage, Dismount-WindowsImage, Get-WindowsImage, Add-WindowsDriver, Get-WindowsDriver, Remove-WindowsDriver are all present. v18 (extended v25)
12 Get-Disk shape Number, FriendlyName, PartitionStyle, Size, BootFromDisk, IsSystem, IsBoot, BusType are present. v18 (extended v25)
13 Get-Volume shape DriveLetter, FileSystemLabel, FileSystem, Size, SizeRemaining, DriveType, HealthStatus, UniqueId are present. SKIP if no lettered volume to sample. v18
14 CPU generation parser Fourteen regression cases: 11th/12th/13th Gen, Core, Core Ultra, and negative cases for AMD/Celeron/Pentium/Xeon/Atom return the expected values. v18
15 DesiredStateId 64-hex, deterministic across repeat calls, VMD-sensitive, ScriptVersion-sensitive. v18
16 Compare-WimServicingMetadata Eight regression cases covering the DISM servicing-metadata comparison rules. v25

What a FAIL means

A FAIL means the machine’s Windows tooling no longer matches an assumption the production script relies on. The production script may silently misclassify state on this machine and must be adapted before deploying to it.

The most likely FAILs and their implications:

What a SKIP means

A SKIP means the check could not run in the current context. The reasons:

A SKIP does not indicate a defect.

Result states

Every test records a result via Record:

Record -Test "<name>" -Ok $true   -Detail "<description>"                        # PASS
Record -Test "<name>" -Ok $false  -Detail "<description>"                        # FAIL
Record -Test "<name>" -Ok $false  -State "SKIP" -Detail "<reason>"               # SKIP

The legacy -Ok boolean is still supported: when -State is not supplied, the state is derived from -Ok. When -State is supplied it is authoritative, and OK is $true only for PASS.

Show-Summary prints each result, then the totals. The per-result state tags are colour-coded: green for [PASS], red for [FAIL], dark gray for [SKIP]. The totals on the last line are colour-coded: the pass count green when non-zero, the fail count red when non-zero else green, the skip count yellow when non-zero else dark gray.

Passed 8, failed 1, skipped 1 (of 10)

The BitLocker, target-partition, ImageState, Build numbers, and Option S outputs are informational — Option S records a result for the parity check itself but its intermediate verdict output is a separate thing. The BitLocker, target-partition, Build numbers, and ImageState blocks do not produce PASS/FAIL/SKIP records and do not appear in Show-Summary.

Non-interactive mode

.\scripts\Test-WinRE.ps1 -NonInteractive

Runs Invoke-AllRelevant, prints the summary, exits.

-NonInteractive runs the download and extraction tests. It does not run the System Diagnostic (Option 1) or the State file parity check (Option S), so the BitLocker warnings, the target-partition state block, the ImageState check, the Build numbers block, and the DSI comparison are not printed in this mode. If you need any of those, run the harness interactively and choose Option 1 or Option S.

The exit code is 0 when no FAIL results were recorded, and 1 when at least one FAIL was recorded (v25+). SKIPs do not affect the exit code. The summary remains the authoritative view of what ran. The behavior was introduced in v25 so that CI and cron consumers can treat the harness as a regression gate rather than a reporter.

What the harness does not test

The harness is deliberately narrow. It does not and cannot test:

Those paths are covered by field testing on representative hardware. See the “Field-tested hardware” table in the README.

Differences from production

The harness shares code paths with production in the download, extraction, and CPU-generation helpers. It is documented in the file’s own docstring which functions are “based on” the production versions and which are harness-specific.

Twelve known differences:

The harness has no mirror of the v45 shrink-first pipeline. Option 1 and Option S report the machine’s current state and the DSI comparison, but they do not simulate the plan, the pre-shrink, the extension path, or the whole-layout assertion. That is by design: those paths are destructive and cannot be simulated read-only.

The v45 patch 1 changes to Ensure-AdequateRecoveryPartition are the largest rework of the destructive path since v43. The reorder moves the shrink into the reversible window; the geometry model changes from deficit-plus-slack to single-boundary; the plan gains several validity checks; the deletion loop reorders the active partition last; a post-delete extension path with a safe fallback replaces the pre-delete extend; and a whole-layout assertion verifies the created partition’s geometry. The happy path through these code paths has been exercised end-to-end on a disposable Hyper-V VM (2026-10-02 08:56).

The v46 patch 1 cycle added two physical-hardware exercises on top of the VM run.

Assert-RecoveryPartitionLayout has been exercised in the passing case. The Dell Latitude 3540 and the HP EliteBook 6 G1i 16” both took the v46 patch 1 destructive path on 2026-10-02 and both logged Verified recovery partition layout: disk 0 partition 4, offset O MiB, size S MiB before reaching DEDICATED. Both runs also confirmed the C:-to-recovery gap and the disk-end trailing reserve were within tolerance, which is what the assertion exists to verify. This is the first physical-hardware exercise of the whole-layout assertion on the v45/v46 pipeline.

The plan-rejection check has been exercised in the rejecting case. The Lenovo IdeaPad 3 15IAU7 that motivated v46 patch 1 took the v45 patch 1 pipeline on physical hardware and the post-delete geometry check in Ensure-AdequateRecoveryPartition ($plannedEnd -gt ($diskSizeNow - 1MB)) correctly rejected the plan: Get-PartitionPlan had computed tailEnd from a factory-provided partition ending exactly 1 MiB past the disk-end reserve, AlignedManagedExtentEnd landed 1 MiB past the reserve, and the post-delete check refused to proceed. The machine fell into OS-fallback with a state file that matched the failing DesiredStateId, so subsequent scheduled runs accepted the state and did not retry — the machine stayed in OS-fallback until an operator manually deleted the state file. Under v46 patch 1’s clamp the same layout produces a valid plan; the second full-update run (after the state-file reset) reached DEDICATED. This is the first physical-hardware failure of the v45 destructive path, and the first time the post-delete check fired outside a test harness.

The v47 non-destructive paths are field-verified on the ASUS PRIME H510M-D. The v47-specific code paths that remain unexercised on any storage — physical or virtual — are:

The v45 destructive-path VM test documented below remains the recommended next step before broad rollout under v47. It should be followed by a canary on a machine with an OEM driver pack so the strip-and-reinject loop runs against a non-empty set. These paths are covered by inspection, by the earlier F1/F2/F3 strip experiment, and by the mocked-geometry test plan documented below.

v48 patch 1 coverage. The intervening-anchor happy path is field-verified on the AMD Ryzen 7 5825U machine that motivated it (2026-10-06), on the exact C: | D: | Recovery layout. The v48-specific code paths that remain unexercised on any storage are: the intervening-anchor post-deletion failure behaviour (the same class of residual documented for the non-intervening path); the v48 multi-intervening rejection and surplus rejection; the transactional WIM replacement’s rollback branch (a copy failure mid-replacement); the architecture gate on ARM64 hardware; and, from v48 patch 2, the source-WIM hash cache fix on the enable-only fallthrough path. The remaining v47 gaps also stand: the strip stage against a non-zero third-party driver set, the OEM pack or VMD injection paths, the metadata-triggered rebuild branch, the pre-/disable race-detector abort branch, and the WIM_READY checkpoint save/resume round trip. The deliberate post-deletion failure test remains the gate for future changes to the post-deletion segment. See the “v48 patch 1 destructive-path coverage” section below.

v48 patch 1 destructive-path coverage

The v48 patch 1 release introduces five changes: intervening-partition handling, the architecture gate, LocalInputsId, LKG-by-hash at any discovered recovery location, and transactional WIM replacement. Four are covered below; the fifth (LKG-by-hash) is a source-selection change and is not covered here.

v46 patch 2 hardenings (inside the post-deletion gate)

The v46 patch 2 cycle added four post-review hardenings of Ensure-AdequateRecoveryPartition. Per CONTRIBUTING.md, they are treated as inside the post-deletion gate — no future change to the post-deletion segment of the destructive sequence should ship until the post-deletion failure test below runs and its result is recorded. The four hardenings:

None of the four has been exercised in the field. They were code-review hardenings in the v46 patch 2 cycle, not field-driven fixes. They are covered by parser and mocked-geometry tests, and by the same post-deletion failure test that gates the rest of the post-deletion segment. When that test runs, its recorded result must list which of the four hardenings it exercised and which it did not; that is the right moment to revisit the gate’s scope.

The post-deletion failure test (gating)

This test exercises the one residual corner the project has not closed: a failure of New-Partition or Format-Volume after the existing recovery partition has been deleted, on a machine whose C: is encrypted, with no successful retry. In that corner, Remove-OrphanPartition cleans up the newly created partition, Restore-OSPartitionSize restores C:’s geometry, the run falls through to OS-fallback, and the OS-fallback gate defers because C: is encrypted. The machine ends with neither a dedicated recovery partition nor OS-fallback.

The corner is the one place where Rule 1 (never break Windows RE) and Rule 2 (never leave a machine without a working recovery route) conflict. Rule 2 cannot be guaranteed in this case; it holds in the two ordinary cases (the destructive attempt succeeds, or it fails before deletion), but a post-deletion failure on an encrypted-C: machine reaches the corner. The v45 shrink-first reorder narrowed the trigger set — a shrink failure no longer reaches the corner because the shrink runs before deletion — but the corner itself is not closed. Closing it is the eventual post-failure check, gated on this test.

The corner is documented in the [v45 patch 1] CHANGELOG entry, in troubleshooting.md under “The OS-fallback route deferred because C: is encrypted”, and in architecture.md as the residual failure mode the eventual post-failure check will close. No future change to the post-deletion segment of Ensure-AdequateRecoveryPartition should ship until this test has been run and its result recorded. See CONTRIBUTING.md for the scope of the gate.

Setup.

Procedure.

  1. Snapshot the VM.
  2. Apply the failure injection (mock New-Partition, mock Format-Volume, or arrange the storage-level condition that will cause the failure).
  3. Run WinRE.ps1.
  4. Capture the full log.

Expected sequence.

For a New-Partition failure:

[INFO] Pre-deletion inventory:
[INFO]   Disk 0 Part 4: size 1000 MiB, label='Recovery', used=... MiB, isWinRELocation=YES
…
[INFO] Confirmed deletion of partition 4 on disk 0
…
[WARN] New-Partition attempt 1 failed: …
[WARN] New-Partition attempt 2 failed: …
[WARN] New-Partition attempt 3 failed: …
[ERROR] New-Partition failed after 3 attempts
…
[WARN] ================================================================================
[WARN] Dedicated recovery partition creation failed after all attempts.
[WARN] Falling back to C:\Recovery\WindowsRE (OS-partition recovery location).
[WARN] This is NOT equivalent to a dedicated recovery partition.
[WARN] WinRE will function but with reduced resilience. Exit code will be 2.
[WARN] ================================================================================
…
[WARN] OS-fallback deferred: C: could not be confirmed fully decrypted (Test-VolumeEncrypted=True). …

For a Format-Volume failure, the analogous sequence: the deletion block, then [ERROR] Format-Volume failed: …, then [INFO] Removing orphan partition …, then the OS-fallback deferral.

In both cases, Restore-OSPartitionSize runs after the failure and restores C: to its recorded original size. The log should show the restore.

Expected end state.

What to record.

Interpretation.

If the test confirms the corner fires as documented — the log matches the expected sequence and the end state is as described — record the result. From that point the design of the fix (a post-failure check in the post-deletion segment, as the CHANGELOG entries describe) can proceed with real field data on the corner’s exact signature and end state.

If the test reveals behavior different from the documented corner — different log sequence, different end state, or the corner does not fire at all — investigate before designing the fix. The documented behavior may need revision, and the CHANGELOG entry, this section, and troubleshooting.md would need to be corrected to match observed behavior.

If the failure injection does not reach the corner — the run succeeds despite the injection, or fails earlier in the pipeline — the injection was not effective at the intended failure point. Adjust the injection and re-run.

Paths that need coverage

Each of these tests exists to protect a specific invariant from architecture.md. The pre-shrink and deletion-failure tests are direct exercises of Rule 2 — a failing pre-shrink must leave the old recovery route intact, and a failing deletion must attempt to restore it. The whole-layout assertion and the plan-validation edge cases are Rule 1 tests — the destructive sequence must refuse to proceed on a layout it cannot prove safe. The deferral marker lifecycle is a Rule 4 test — the marker is the mechanism by which “do no work unless needed” is enforced across a run that has deferred. The MBR test confirms Rule 1 on a partition style that has not yet seen a field exercise.

How to run these tests

Each test needs a different starting state. Use a snapshot-restorable VM; the destructive path is only safe in a disposable context.

What to record when a test is run

For each test, record:

If a divergence is found, report it as a bug with the test setup and the full log. The fix ships as its own patch, gated on the test that found it. For the post-deletion failure test, the fix is the gate the CHANGELOG entry describes.

Adding a test

The harness is a single file. To add a test:

  1. Write a function that follows the existing pattern: log via Say, record the result via Record.
  2. Add a menu entry in Show-Menu using the MenuItem helper and pick a shortcut-letter colour from the palette already in use.
  3. Add a case to the switch in the main loop.
  4. If the test should run as part of “all relevant for this machine,” add it to Invoke-AllRelevant.
  5. If the test downloads anything, apply -TimeoutSec $NetworkTimeoutSeconds to every network call. The harness’s $NetworkTimeoutSeconds is declared at the top of the file and matches production’s value.

For diagnostic output added to Option 1 or its sub-sections, use Write-Diag for free-form lines and Write-KV for aligned key/value pairs. The caller chooses the colour in both cases; there is no automatic severity mapping. For values inside tables, follow the pattern of the surrounding table and pick a colour from the restricted palette (Gray, DarkGray, Cyan, Green, Yellow, Red, Magenta, White).

Do not add tests that modify the machine’s state. The harness’s contract with the operator is that it is read-only.