diff --git a/docs/MVP_VALIDATION_MATRIX.md b/docs/MVP_VALIDATION_MATRIX.md index 3984e84..85e004e 100644 --- a/docs/MVP_VALIDATION_MATRIX.md +++ b/docs/MVP_VALIDATION_MATRIX.md @@ -162,6 +162,10 @@ it never means a universal veto. Request visibility alone is not a sleep test. ### Hardware procedure (manual, not CI) +The [physical power validation kit](PHYSICAL_POWER_VALIDATION.md) provides passive +preparation, read-only evidence collection and the eight-case S3 worksheet. +Preparation does not launch Wardoff or perform a power transition. + Use a prepared test PC, or a disposable VM only if it exposes the required power states. Do not run disruptive tests on a workstation with unsaved work. Do not enable Hibernate, change button/lid actions, set request overrides or edit a diff --git a/docs/PHYSICAL_POWER_VALIDATION.md b/docs/PHYSICAL_POWER_VALIDATION.md new file mode 100644 index 0000000..ba5d1ac --- /dev/null +++ b/docs/PHYSICAL_POWER_VALIDATION.md @@ -0,0 +1,164 @@ +# Physical power validation kit + +This kit prepares manual evidence for the idle-power contract in the +[MVP validation matrix](MVP_VALIDATION_MATRIX.md). It does not perform the tests. +VM acceptance does not establish physical S3, monitor, Hibernate or Modern +Standby acceptance. + +## Passive preparation + +`tests/physical_power/Prepare-PhysicalPowerValidation.ps1` verifies a supplied +binary's SHA-256, copies it into a fresh local directory and records read-only +power settings. It creates a provenance manifest and a pending results template. +It does not build or launch Wardoff, change settings, schedule work or initiate +a power transition. Existing files are not overwritten. + +Example, from the repository root, with real paths and provenance supplied by +the operator: + +```powershell +.\tests\physical_power\Prepare-PhysicalPowerValidation.ps1 ` + -WardoffExecutable 'C:\TestArtifacts\wardoff.exe' ` + -ExpectedSha256 '' ` + -OutputDirectory 'C:\TestArtifacts\physical-power-run' ` + -ApplicationSourceCommit '' ` + -AcceptedIntegrationCommit '' +``` + +The two commits may differ only when the artifact's application source has been +verified against the accepted application source. A matching supplied hash alone +does not prove that relationship. Keep the build and acceptance evidence with +the kit. Preparation is complete when the kit is present and verified; every +physical result remains pending. + +For focused verification of the preparation tools, run +`tests/physical_power/Test-PassivePreparation.ps1`. It uses a text artifact and +mocked diagnostics in a temporary directory; it does not run Wardoff, invoke +real power diagnostics or query the event log. + +`Collect-PhysicalPowerCheckpoint.ps1` is also read-only. It verifies the kit +binary without executing it, captures power settings, requests and overrides, +and records recent power-event identifiers and timestamps where accessible. +Failures to read a diagnostic are preserved, not converted into passes. Capture +files can contain other applications' names or paths; keep the entire kit local +and review/redact evidence before sharing it. + +```powershell +# Use a fresh label for each capture. This does not start a test. +.\Collect-PhysicalPowerCheckpoint.ps1 -KitDirectory . -Label 'before-tests' +``` + +## Conditions for a future test window + +Run the remaining instructions only during an explicitly arranged maintenance +window. Sleep stops work on this PC, including unrelated Codex agents. Save work, +coordinate with the owners of that work and have a person present to observe the +monitor and wake the PC. Preparation does not authorize stopping their processes. +The kit neither discovers nor manages other agents. + +Use an already suitable test setup. Record Windows build, hardware, power source, +the physical monitor in use, the existing sleep/display timeouts and the means +of waking the PC. Do not enable Hibernate, change the power plan, add request +overrides or change firmware merely to make a result pass. If a required timeout +is disabled, a state is unavailable, another application prevents sleep, or +remote/input activity prevents a clean idle interval, record the constraint and +defer that case. Resolve setup questions separately before another test window. + +An S3 PC is not a Modern Standby PC. Test S0 Low Power Idle on native supporting +hardware. Test Hibernate only where it is already enabled and available. Do not +count either missing capability as a pass. On a laptop, do not approach critical +battery levels to conduct a test. + +Use a non-elevated Wardoff runtime for idle-request acquisition where possible. +An elevated console may be needed for `powercfg /requests`; that diagnostic +requirement is separate from the runtime. Record the actual privilege level. +Do not toggle Start with Windows or test unrelated protection layers in this run. + +## Execution and evidence + +These are manual steps for the future window. No preparation script performs +them. From the kit directory, start `wardoff.exe --allow` when no Wardoff runtime +is already active. Record the new test runtime's PID, exact executable path and +process start time for cleanup. If a pre-existing runtime is present, defer rather +than taking control of it. Keep the visible tray for the normal Quit check. + +During the window, use `wardoff.exe --status` for state/layer evidence and +`wardoff.exe --log --tail 30` for recent logs. Save their outputs locally with UTC +timestamps. Mode changes use `wardoff.exe --block` and `wardoff.exe --allow`. +Those commands can start a runtime if none exists, so use them only after +establishing ownership of the test runtime. Record new PIDs if it is restarted. + +Run the checkpoint collector before each idle interval and after observation or +wake. Use fresh labels such as `block-before`, `block-after`, `wake-after`, +`allow-before`, `allow-after`, `quit-after` and `kill-after`. Inspect exit codes in +the captures. An unavailable request listing leaves request evidence pending. + +For idle observations, note the last input time, expected deadline and actual +elapsed time. Wait beyond the relevant configured timeout, allowing for normal +Windows timer scheduling. Do not touch the keyboard/mouse, use a remote desktop, +run interactive diagnostics or poll Wardoff while the idle interval is in +progress. A person should observe the physical screen without generating input. +Record how sleep and resume were identified; an event identifier or a visible +request alone does not prove the physical transition. + +| Case | Manual action | Required evidence and expected behavior | +| --- | --- | --- | +| S3-1 | Enter Block; wait beyond the existing automatic sleep timeout | PC remains awake. Record elapsed idle time and physical observation, plus system/display requests before and after. | +| S3-2 | In Block, wait beyond the existing automatic display timeout | Physical monitor remains active. Record observation separately from request visibility. S3-1 and S3-2 may share one sufficiently long idle interval. | +| S3-3 | In Block, choose Sleep from the Windows Start power menu | PC actually enters S3. Explicit Windows sleep is expected to proceed. Record the transition and how the PC was woken. | +| S3-4 | Resume the same Block runtime | State stays Block, `layers.sleep=true`, and both requests are present again. Then repeat an idle interval beyond the sleep/display timeouts to verify actual protection after wake. | +| S3-5 | Switch the same runtime to Allow; wait without input | Wardoff stays alive with `layers.sleep=false`; its requests disappear. Existing automatic display-off and sleep work. Observe both where the existing timeout ordering allows it; otherwise record which observation needs another suitable setup. | +| S3-6 | Capture requests in Block and Allow | Wardoff appears under SYSTEM and DISPLAY in Block with reason `Wardoff Block mode: prevent idle sleep and automatic display timeout.`; disappears in Allow. No extra residual Wardoff request remains. | +| S3-7 | Return the owned runtime to Block, then Quit directly from its tray | Owned process exits and its requests disappear, without first switching to Allow. | +| S3-8 | Start a fresh owned test runtime, enter Block, then terminate that exact PID | Immediately before termination, recheck PID, exact executable path and process start time against the owned runtime record. Abort on a mismatch, an exited process or an unreadable identity. Only that verified process is terminated; its requests disappear after abrupt exit. Never kill all processes by name. Record the identity and before/after request evidence. | + +Keep `manifest.json` unchanged: its `physicalTestsExecuted=false` records the +state at preparation time. In `results.json`, set `physicalTestsExecuted=true` +when the first physical test is actually attempted, even if that attempt fails. +Keep it false if all cases are deferred. Change each case from `pending` to +`pass` or `fail` only after observation. Use `not-tested` with a reason when +capabilities, setup or scheduling prevent an attempt; no transition is needed +to document that deferral. Add evidence filenames, UTC times, durations and +explanatory notes. Do not set a pass from capability detection, +synthetic resume messages or request visibility alone. Repeat failed or +ambiguous cases only after identifying the confounding condition. + +## Other supported environments and resume paths + +On a prepared Hibernate-capable machine, repeat explicit Windows Hibernate from +Block and verify actual S4/resume, the surviving Block state and renewed request +pair. Repeat an idle interval after resume. Wardoff does not promise a general +Hibernate veto. Record these as the two Hibernate rows in `results.json`. + +On native Modern Standby hardware, repeat applicable idle, explicit-sleep, +resume and release checks on AC and DC. Record the complete observation window, +including beyond the configured sleep timeout plus five minutes on DC, and an +AC/DC change where applicable. Windows limits system-required requests on DC; +expiry at that boundary is not evidence of a universal sleep veto defect. Never +refresh requests in a loop to defeat that limit. Record the two Modern Standby +rows with the applicable contract and observed behavior, not a blanket promise +that the PC must remain awake indefinitely. + +For broader matrix coverage, separately test tray Sleep/Hibernate from Block +and Allow. A tray action from Block temporarily enters Allow and restores Block +after wake with `block_restored_after_wake`; an action from Allow stays Allow. +Record existing Sleep-button/lid behavior and unattended/maintenance wake only +where the setup supports them. These additional observations do not replace the +eight requested S3 cases. + +## Completion and recovery + +Capture a final checkpoint after the owned test process exits. Compare the active +scheme and settings with the baseline, and compare overrides with the +`before-tests` checkpoint. The kit and Wardoff's idle-power layer should not +alter them. If unrelated activity changed the environment, record the difference +and investigate ownership before any restoration; do not overwrite another +owner's changes. The kit has no automatic restoration action because it makes +no power-setting changes. + +Confirm no owned test process or Wardoff request remains, leave the machine +awake, and notify the operator that normal work can resume. Retain failures, +skips and untested states in the final record. Request an independent review of +the evidence before claiming physical acceptance. Shutdown/reboot, forced +critical-battery tests and unrelated autostart/update changes are outside this +kit's scope. diff --git a/tests/physical_power/Collect-PhysicalPowerCheckpoint.ps1 b/tests/physical_power/Collect-PhysicalPowerCheckpoint.ps1 new file mode 100644 index 0000000..201e481 --- /dev/null +++ b/tests/physical_power/Collect-PhysicalPowerCheckpoint.ps1 @@ -0,0 +1,60 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$KitDirectory, + [Parameter(Mandatory=$true)][ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{0,47}$')][string]$Label +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$provider = $null; $drive = $null +$kit = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($KitDirectory,[ref]$provider,[ref]$drive) +if ($provider.Name -ne 'FileSystem') { throw 'Only FileSystem paths are allowed' } +$manifestPath = Join-Path $kit 'manifest.json' +$binary = Join-Path $kit 'wardoff.exe' +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw 'Kit manifest is missing' } +if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { throw 'Kit Wardoff binary is missing' } +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema -ne 'wardoff-physical-power-validation-kit-v1') { throw 'Kit manifest schema mismatch' } +$actualSha256 = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToUpperInvariant() +if ($actualSha256 -ne ([string]$manifest.wardoffSha256).ToUpperInvariant()) { throw 'Kit Wardoff hash mismatch' } +$checkpoint = Join-Path (Join-Path $kit 'checkpoints') $Label +if (Test-Path -LiteralPath $checkpoint) { throw 'Checkpoint already exists' } +New-Item -ItemType Directory -Path $checkpoint -Force:$false | Out-Null +function Save-PowerCfg { + param([string]$Name, [string[]]$CommandArgs) + $stdout = Join-Path $checkpoint ($Name + '.stdout.txt') + $stderr = Join-Path $checkpoint ($Name + '.stderr.txt') + $exitCode = 0 + $previous = $ErrorActionPreference + try { $ErrorActionPreference = 'Continue'; & (Join-Path $env:SystemRoot 'System32\powercfg.exe') @CommandArgs 1> $stdout 2> $stderr; $exitCode = $LASTEXITCODE } + catch { $_ | Out-File -LiteralPath $stderr -Encoding UTF8; $exitCode = 1 } + finally { $ErrorActionPreference = $previous } + return [ordered]@{ name=$Name; arguments=$CommandArgs; stdout=$stdout; stderr=$stderr; exitCode=$exitCode } +} +$captures = @( + (Save-PowerCfg 'powercfg-a' @('/a')), + (Save-PowerCfg 'powercfg-getactivescheme' @('/getactivescheme')), + (Save-PowerCfg 'powercfg-query' @('/query','SCHEME_CURRENT')), + (Save-PowerCfg 'powercfg-requests' @('/requests')), + (Save-PowerCfg 'powercfg-requestsoverride' @('/requestsoverride')) +) +$eventStatus = @(); $events = @() +$start = [DateTime]::Parse($manifest.preparedUtc).ToUniversalTime() +foreach ($provider in @('Microsoft-Windows-Kernel-Power','Microsoft-Windows-Power-Troubleshooter')) { + try { + $providerEvents = @(Get-WinEvent -FilterHashtable @{ LogName='System'; ProviderName=$provider; StartTime=$start } -MaxEvents 100 -ErrorAction Stop | + Select-Object -First 100 Id,RecordId,@{Name='TimeCreatedUtc';Expression={$_.TimeCreated.ToUniversalTime().ToString('o')}}) + $events += $providerEvents + $eventStatus += [ordered]@{ provider=$provider; status='captured'; count=$providerEvents.Count; error=$null } + } catch { + $id = $_.FullyQualifiedErrorId + if ($id -match 'NoMatchingEventsFound') { + $eventStatus += [ordered]@{ provider=$provider; status='no-events'; count=0; error=$null } + } else { + $eventStatus += [ordered]@{ provider=$provider; status='error'; count=0; error=$_.Exception.GetType().Name } + } + } +} +ConvertTo-Json -InputObject @($events) -Depth 4 | Set-Content -LiteralPath (Join-Path $checkpoint 'power-events.json') -Encoding UTF8 +$record = [ordered]@{ schema='wardoff-physical-power-checkpoint-v1'; utc=[DateTime]::UtcNow.ToString('o'); label=$Label; wardoffSha256=$actualSha256; captures=$captures; eventLog=$eventStatus; physicalTransitionObserved=$false } +$record | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $checkpoint 'checkpoint.json') -Encoding UTF8 +Write-Output ('Created passive checkpoint: ' + $checkpoint) diff --git a/tests/physical_power/Prepare-PhysicalPowerValidation.ps1 b/tests/physical_power/Prepare-PhysicalPowerValidation.ps1 new file mode 100644 index 0000000..2790d08 --- /dev/null +++ b/tests/physical_power/Prepare-PhysicalPowerValidation.ps1 @@ -0,0 +1,78 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$WardoffExecutable, + [Parameter(Mandatory=$true)][ValidatePattern('^[0-9A-Fa-f]{64}$')][string]$ExpectedSha256, + [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$OutputDirectory, + [Parameter(Mandatory=$true)][ValidatePattern('^[0-9A-Fa-f]{40}$')][string]$ApplicationSourceCommit, + [Parameter(Mandatory=$true)][ValidatePattern('^[0-9A-Fa-f]{40}$')][string]$AcceptedIntegrationCommit +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$resolve = { + param([string]$Path) + $provider = $null; $drive = $null + $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path,[ref]$provider,[ref]$drive) + if ($provider.Name -ne 'FileSystem') { throw 'Only FileSystem paths are allowed' } + return $resolved +} +$source = & $resolve $WardoffExecutable +$kit = & $resolve $OutputDirectory +if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { throw 'Wardoff executable is missing' } +if (Test-Path -LiteralPath $kit) { throw 'Output directory already exists' } +$sourceSha256 = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash.ToUpperInvariant() +if ($sourceSha256 -ne $ExpectedSha256.ToUpperInvariant()) { throw 'Wardoff executable hash mismatch' } +$collector = Join-Path $PSScriptRoot 'Collect-PhysicalPowerCheckpoint.ps1' +$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +$guide = Join-Path $repo 'docs\PHYSICAL_POWER_VALIDATION.md' +$matrix = Join-Path $repo 'docs\MVP_VALIDATION_MATRIX.md' +if (-not (Test-Path -LiteralPath $collector -PathType Leaf)) { throw 'Collector script is missing' } +if (-not (Test-Path -LiteralPath $guide -PathType Leaf)) { throw 'Physical power guide is missing' } +if (-not (Test-Path -LiteralPath $matrix -PathType Leaf)) { throw 'Validation matrix is missing' } +$preparedUtc = [DateTime]::UtcNow.ToString('o') +New-Item -ItemType Directory -Path $kit -Force:$false | Out-Null +New-Item -ItemType Directory -Path (Join-Path $kit 'baseline') -Force:$false | Out-Null +New-Item -ItemType Directory -Path (Join-Path $kit 'checkpoints') -Force:$false | Out-Null +Copy-Item -LiteralPath $source -Destination (Join-Path $kit 'wardoff.exe') +Copy-Item -LiteralPath $collector -Destination (Join-Path $kit 'Collect-PhysicalPowerCheckpoint.ps1') +Copy-Item -LiteralPath $guide -Destination (Join-Path $kit 'PHYSICAL_POWER_VALIDATION.md') +Copy-Item -LiteralPath $matrix -Destination (Join-Path $kit 'MVP_VALIDATION_MATRIX.md') +$actualSha256 = (Get-FileHash -LiteralPath (Join-Path $kit 'wardoff.exe') -Algorithm SHA256).Hash.ToUpperInvariant() +if ($actualSha256 -ne $ExpectedSha256.ToUpperInvariant()) { throw 'Copied Wardoff executable hash mismatch' } +function Save-BaselinePowerCfg { + param([string]$Name, [string[]]$CommandArgs) + $stdout = Join-Path (Join-Path $kit 'baseline') ($Name + '.stdout.txt') + $stderr = Join-Path (Join-Path $kit 'baseline') ($Name + '.stderr.txt') + $exitCode = 0 + $previous = $ErrorActionPreference + try { $ErrorActionPreference = 'Continue'; & (Join-Path $env:SystemRoot 'System32\powercfg.exe') @CommandArgs 1> $stdout 2> $stderr; $exitCode = $LASTEXITCODE } + catch { $_ | Out-File -LiteralPath $stderr -Encoding UTF8; $exitCode = 1 } + finally { $ErrorActionPreference = $previous } + return [ordered]@{ name=$Name; arguments=$CommandArgs; stdout=$stdout; stderr=$stderr; exitCode=$exitCode } +} +$baseline = @( + (Save-BaselinePowerCfg 'powercfg-a' @('/a')), + (Save-BaselinePowerCfg 'powercfg-getactivescheme' @('/getactivescheme')), + (Save-BaselinePowerCfg 'powercfg-query' @('/query','SCHEME_CURRENT')) +) +$baseline | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $kit 'baseline\baseline.json') -Encoding UTF8 +$cases = @() +foreach ($number in 1..8) { $cases += [ordered]@{ id=('S3-' + $number); status='pending'; observed=$null; notes=$null } } +$cases += [ordered]@{ id='Hibernate-explicit'; status='pending'; observed=$null; notes=$null } +$cases += [ordered]@{ id='Hibernate-resume'; status='pending'; observed=$null; notes=$null } +$cases += [ordered]@{ id='Modern-Standby-AC'; status='pending'; observed=$null; notes=$null } +$cases += [ordered]@{ id='Modern-Standby-DC'; status='pending'; observed=$null; notes=$null } +$manifest = [ordered]@{ + schema = 'wardoff-physical-power-validation-kit-v1' + preparedUtc = $preparedUtc + wardoffExecutable = (Join-Path $kit 'wardoff.exe') + wardoffSha256 = $actualSha256 + applicationSourceCommit = $ApplicationSourceCommit + acceptedIntegrationCommit = $AcceptedIntegrationCommit + baseline = $baseline + physicalTestsExecuted = $false + cases = $cases +} +$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $kit 'manifest.json') -Encoding UTF8 +$results = [ordered]@{ schema='wardoff-physical-power-validation-results-v1'; physicalTestsExecuted=$false; cases=$cases } +$results | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $kit 'results.json') -Encoding UTF8 +Write-Output ('Prepared passive physical power kit: ' + $kit) diff --git a/tests/physical_power/Test-PassivePreparation.ps1 b/tests/physical_power/Test-PassivePreparation.ps1 new file mode 100644 index 0000000..abd090e --- /dev/null +++ b/tests/physical_power/Test-PassivePreparation.ps1 @@ -0,0 +1,115 @@ +# Focused safety checks. Uses a text fixture and mocked diagnostics; never runs +# Wardoff, powercfg or an actual event-log query. +[CmdletBinding()] +param() +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$checks = 0 +function Assert-True([bool]$Condition, [string]$Message) { + if (-not $Condition) { throw $Message } + $script:checks++ +} +function Assert-Throws([scriptblock]$Action, [string]$Pattern) { + $caught = $null + try { & $Action | Out-Null } catch { $caught = $_ } + Assert-True ($null -ne $caught -and $caught.Exception.Message -match $Pattern) 'Expected refusal was not observed' +} +$testRoot = Join-Path ([IO.Path]::GetTempPath()) ('wardoff-passive-' + [Guid]::NewGuid().ToString('N')) +$originalLocation = Get-Location +$hadExitCode = Test-Path Variable:global:LASTEXITCODE +$originalExitCode = if ($hadExitCode) { $global:LASTEXITCODE } else { $null } +try { + $cloneScripts = Join-Path $testRoot 'tests\physical_power' + New-Item -ItemType Directory -Path $cloneScripts | Out-Null + New-Item -ItemType Directory -Path (Join-Path $testRoot 'docs') | Out-Null + $repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) + foreach ($name in @('PHYSICAL_POWER_VALIDATION.md','MVP_VALIDATION_MATRIX.md')) { + Copy-Item -LiteralPath (Join-Path $repo ('docs\' + $name)) -Destination (Join-Path $testRoot ('docs\' + $name)) + } + $stub = Join-Path $testRoot 'fake-powercfg.ps1' + @' +param([Parameter(ValueFromRemainingArguments=$true)][string[]]$CommandArgs) +$global:LASTEXITCODE = 0 +if ($CommandArgs[0] -eq '/requests') { + $global:LASTEXITCODE = 5 + Write-Error 'Fictitious diagnostic permission failure' -ErrorAction Continue +} else { Write-Output 'Fictitious read-only power setting' } +'@ | Set-Content -LiteralPath $stub -Encoding UTF8 + $allowed = @('Set-StrictMode','Test-Path','Get-FileHash','Join-Path','Split-Path','New-Item','Out-Null','Copy-Item','Out-File','ConvertTo-Json','Set-Content','Write-Output','Get-Content','ConvertFrom-Json','Get-WinEvent','Select-Object','Save-PowerCfg','Save-BaselinePowerCfg') + $nativeExpression = '(Join-Path $env:SystemRoot ''System32\powercfg.exe'')' + foreach ($name in @('Prepare-PhysicalPowerValidation.ps1','Collect-PhysicalPowerCheckpoint.ps1')) { + $path = Join-Path $PSScriptRoot $name + $tokens = $null; $errors = $null + $ast = [Management.Automation.Language.Parser]::ParseFile($path,[ref]$tokens,[ref]$errors) + Assert-True ($errors.Count -eq 0) 'PowerShell parse failure' + foreach ($command in $ast.FindAll({param($node) $node -is [Management.Automation.Language.CommandAst]},$true)) { + $commandName = $command.GetCommandName() + if ($null -eq $commandName) { + Assert-True ($command.Extent.Text.StartsWith('& ' + $nativeExpression) -or $command.Extent.Text.StartsWith('& $resolve ')) 'Unexpected dynamic command' + } else { Assert-True ($allowed -contains $commandName) ('Unexpected command: ' + $commandName) } + } + $sourceText = Get-Content -LiteralPath $path -Raw + Assert-True ($sourceText -notmatch '(?i)SetSuspendState|DllImport|Add-Type|Start-Process|Stop-Process|SetThreadExecutionState') 'Power-affecting operation detected' + Assert-True (($sourceText.Split(@($nativeExpression),[StringSplitOptions]::None).Count - 1) -eq 1) 'Diagnostic mock replacement must be exact' + $sourceText.Replace($nativeExpression, ("'" + $stub.Replace("'","''") + "'")) | Set-Content -LiteralPath (Join-Path $cloneScripts $name) -Encoding UTF8 + } + # This function shadows the event reader only in this test's scope. + function Get-WinEvent { + [CmdletBinding()] + param([hashtable]$FilterHashtable,[int]$MaxEvents) + if ($MaxEvents -ne 100) { throw 'Event query must be bounded' } + if ($FilterHashtable.ProviderName -eq 'Microsoft-Windows-Power-Troubleshooter') { + $record = New-Object Management.Automation.ErrorRecord ([InvalidOperationException]::new('Fixture has no matching events')), 'NoMatchingEventsFound', ([Management.Automation.ErrorCategory]::ObjectNotFound), $null + $PSCmdlet.ThrowTerminatingError($record) + } + [pscustomobject]@{Id=42;RecordId=123;TimeCreated=[DateTime]::UtcNow;Message='Fictitious omitted field';MachineName='Fictitious omitted field'} + } + Set-Location -LiteralPath $testRoot + Set-Content -LiteralPath 'fixture.exe' -Value 'Text fixture; never execute' -Encoding UTF8 + $hash = (Get-FileHash -LiteralPath 'fixture.exe' -Algorithm SHA256).Hash + $prepare = Join-Path $cloneScripts 'Prepare-PhysicalPowerValidation.ps1' + $collect = Join-Path $cloneScripts 'Collect-PhysicalPowerCheckpoint.ps1' + $parameters = @{WardoffExecutable='fixture.exe';ExpectedSha256=$hash;OutputDirectory='kit';ApplicationSourceCommit=('a' * 40);AcceptedIntegrationCommit=('b' * 40)} + $invalid = $parameters.Clone(); $invalid.ExpectedSha256 = '0' * 64 + Assert-Throws { & $prepare @invalid } 'hash mismatch' + Assert-True (-not (Test-Path -LiteralPath 'kit')) 'Invalid artifact created output' + $invalid = $parameters.Clone(); $invalid.OutputDirectory = 'Variable:WardoffFixtureKit' + Assert-Throws { & $prepare @invalid } 'Only FileSystem' + $invalid = $parameters.Clone(); $invalid.WardoffExecutable = 'Variable:WardoffFixtureBinary' + Assert-Throws { & $prepare @invalid } 'Only FileSystem' + Assert-Throws { & $collect -KitDirectory 'Variable:WardoffFixtureKit' -Label 'provider' } 'Only FileSystem' + Assert-True (-not (Test-Path -LiteralPath 'kit')) 'Non-filesystem path created output' + & $prepare @parameters | Out-Null + $kit = Join-Path $testRoot 'kit' + Assert-True (Test-Path -LiteralPath (Join-Path $kit 'PHYSICAL_POWER_VALIDATION.md') -PathType Leaf) 'Copied validation matrix guide is missing' + $manifest = Get-Content -LiteralPath (Join-Path $kit 'manifest.json') -Raw | ConvertFrom-Json + $results = Get-Content -LiteralPath (Join-Path $kit 'results.json') -Raw | ConvertFrom-Json + Assert-True ($manifest.wardoffSha256 -eq $hash) 'Artifact binding differs' + Assert-True (-not $manifest.physicalTestsExecuted -and -not $results.physicalTestsExecuted) 'Preparation claimed execution' + Assert-True ($results.cases.Count -eq 12 -and @($results.cases | Where-Object status -ne 'pending').Count -eq 0) 'Pending cases are incomplete' + Assert-Throws { & $prepare @parameters } 'already exists' + Set-Location -LiteralPath $kit + & $collect -KitDirectory . -Label 'fixture' | Out-Null + $record = Get-Content -LiteralPath 'checkpoints\fixture\checkpoint.json' -Raw | ConvertFrom-Json + Assert-True (-not $record.physicalTransitionObserved) 'Checkpoint claimed a transition' + Assert-True (($record.captures | Where-Object name -eq 'powercfg-requests').exitCode -eq 5) 'Native diagnostic failure was lost' + Assert-True ($record.eventLog[0].count -eq 1 -and $record.eventLog[1].status -eq 'no-events') ('Provider isolation failed: ' + ($record.eventLog | ConvertTo-Json -Compress)) + $eventsText = Get-Content -LiteralPath 'checkpoints\fixture\power-events.json' -Raw + Assert-True ($eventsText.TrimStart().StartsWith('[') -and $eventsText -notmatch 'Message|MachineName') 'Event export is not a sanitized array' + $vectors = @($record.captures | ForEach-Object { $_.arguments -join ' ' }) + Assert-True (($vectors -join '|') -eq '/a|/getactivescheme|/query SCHEME_CURRENT|/requests|/requestsoverride') 'Diagnostics include unexpected arguments' + Assert-Throws { & $collect -KitDirectory . -Label 'fixture' } 'already exists' + Assert-Throws { & $collect -KitDirectory . -Label '../escape' } 'pattern' + Add-Content -LiteralPath 'wardoff.exe' -Value 'fixture corruption' + Assert-Throws { & $collect -KitDirectory . -Label 'corrupt' } 'hash mismatch' + Assert-True (-not (Test-Path -LiteralPath 'checkpoints\corrupt')) 'Corrupted artifact created checkpoint' + Write-Output ('PASS: ' + $checks + ' passive checks; all diagnostics mocked; no physical tests executed') +} finally { + Set-Location -LiteralPath $originalLocation.Path + if ($hadExitCode) { $global:LASTEXITCODE = $originalExitCode } else { Remove-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue } + # Delete only the fresh directory owned by this test, inside the OS temp dir. + $resolvedTemp = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\' + $resolvedTest = [IO.Path]::GetFullPath($testRoot) + if (-not $resolvedTest.StartsWith($resolvedTemp,[StringComparison]::OrdinalIgnoreCase) -or (Split-Path -Leaf $resolvedTest) -notmatch '^wardoff-passive-[0-9a-f]{32}$') { throw 'Unsafe test cleanup path' } + if (Test-Path -LiteralPath $resolvedTest) { Remove-Item -LiteralPath $resolvedTest -Recurse -Force } +}