From eaa64946db048cb38f900bde53deb86ab7eaca40 Mon Sep 17 00:00:00 2001 From: Hal <22532613+HalRiveria@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:18:47 -0400 Subject: [PATCH 1/3] Start programs in the current location, and resolve relative paths there Programs ran in the PowerShell process's working directory, which cd and Set-Location never change. So after "cd D:\work" at the prompt and then ".\script.ps1", every program started in whatever folder the console opened in. Relative paths in a program's arguments then pointed at the wrong folder: it could fail to find its input, or write its output somewhere unexpected and still report success. A FilePath like .\tool.exe couldn't be started at all, and a relative WorkingDirectory was resolved against the wrong folder too. Start-PkProcess, which every entry point uses, now defaults to the caller's current file-system location, using the real folder behind a New-PSDrive drive and the last file-system location when the current one is HKLM: or similar. A relative WorkingDirectory, and a FilePath with a folder in it, are resolved against that location. Bare names are still found the way Windows finds programs. If the current folder has been deleted, the old default is used. An explicit absolute WorkingDirectory behaves as before. Co-Authored-By: Claude Opus 5.5 --- ParallelKit.Harness.ps1 | 69 +++++++++++++++++++++++++++++++++++++++++ ParallelKit.ps1 | 35 +++++++++++++++++---- README.md | 2 ++ 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/ParallelKit.Harness.ps1 b/ParallelKit.Harness.ps1 index c1859d6..bfa532e 100644 --- a/ParallelKit.Harness.ps1 +++ b/ParallelKit.Harness.ps1 @@ -1813,6 +1813,75 @@ foreach ($s in $scenarios) { } Write-Host "" +# ============================================================================== +Write-Host "=== W: relative paths and the current location ===" -ForegroundColor Cyan +# Programs start in the caller's current location, which cd/Set-Location +# change but the process's working directory doesn't follow. +Reset-Harness +$here = Join-Path $env:TEMP ("pk-harness-wd-" + $PID) +$null = New-Item -ItemType Directory -Force -Path (Join-Path $here 'sub') +[System.IO.File]::WriteAllText((Join-Path $here 'pk-wd.cmd'), "@echo pk-wd ran in %CD%`r`n") +$gone = Join-Path $env:TEMP ("pk-harness-wd-gone-" + $PID) +$null = New-Item -ItemType Directory -Force -Path $gone +function Get-HarnessCd { + # Where a program started by each entry point runs, via "cmd /c cd". + $wd = @{} + $wd.Batch = (Invoke-PkProcessBatch -Commands @(@{ FilePath = $cmdExe; Arguments = '/c cd' })).Results[0].Stdout.Trim() + $wd.WithTimeout = (Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd').Stdout.Trim() + $started = Start-PkProcess -FilePath $cmdExe -Arguments '/c cd' + $null = Wait-PkProcess -Process $started['Proc'] -TimeoutMs 10000 + $wd.StartProcess = (Complete-PkProcess -Job $started -Dispose $true).Stdout.Trim() + $out = @{ Text = $null } + $feed = @{ Given = $false } + $null = Invoke-PkProcessDispatch -MaxConcurrent 1 ` + -GroupProvider { if ($feed.Given) { return $null }; $feed.Given = $true; return 'g' }.GetNewClosure() ` + -JobBuilder { param($g) return @(@{ FilePath = $cmdExe; Arguments = '/c cd' }) }.GetNewClosure() ` + -JobFinalize { param($job, $pending) $out.Text = (Complete-PkProcess -Job $job).Stdout.Trim() }.GetNewClosure() + $wd.Dispatch = $out.Text + return $wd +} +Push-Location -LiteralPath $here +try { + Test-Assert ([Environment]::CurrentDirectory -ne $here) ("precondition: the process's working directory isn't the test folder (" + [Environment]::CurrentDirectory + ")") + $wd = Get-HarnessCd + $wrong = @($wd.Keys | Where-Object { $wd[$_] -ine $here }) + Test-Assert ($wrong.Count -eq 0) ("every entry point starts programs in the current location by default (wrong: " + (@($wrong | ForEach-Object { $_ + '=' + $wd[$_] }) -join ', ') + ")") + + $relText = $null + try { $relText = (Invoke-PkProcessWithTimeout -FilePath '.\pk-wd.cmd').Stdout.Trim() } catch { $relText = [string]$_ } + $relBatch = (Invoke-PkProcessBatch -Commands @(@{ FilePath = '.\pk-wd.cmd' })).Results[0] + Test-Assert ($relText -ieq ("pk-wd ran in " + $here) -and $null -eq $relBatch.StartError -and $relBatch.Stdout.Trim() -ieq ("pk-wd ran in " + $here)) ("a FilePath with a folder in it (.\pk-wd.cmd) is found relative to the current location (" + $relText + ")") + + $subText = $null + try { $subText = (Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd' -WorkingDirectory 'sub').Stdout.Trim() } catch { $subText = [string]$_ } + Test-Assert ($subText -ieq (Join-Path $here 'sub')) ("a relative WorkingDirectory is relative to the current location (" + $subText + ")") + + $null = New-PSDrive -Name PkHarnessWd -PSProvider FileSystem -Root $here -Scope Script + try { + Push-Location -LiteralPath 'PkHarnessWd:\' + try { $onDrive = (Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd').Stdout.Trim() } finally { Pop-Location } + } finally { Remove-PSDrive -Name PkHarnessWd -Scope Script } + Test-Assert ($onDrive -ieq $here) ("on a drive made with New-PSDrive, the program runs in the real folder behind it (" + $onDrive + ")") + + Push-Location -LiteralPath 'HKLM:\' + try { $fromReg = (Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd').Stdout.Trim() } finally { Pop-Location } + Test-Assert ($fromReg -ieq $here) ("from a registry location, the program runs in the last file-system location (" + $fromReg + ")") + + Push-Location -LiteralPath $gone + try { + [System.IO.Directory]::Delete($gone, $true) # Remove-Item refuses to delete the current location + $fromGone = Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd' + Test-Assert ($fromGone.ExitCode -eq 0 -and $fromGone.Stdout.Trim() -ieq [Environment]::CurrentDirectory.TrimEnd('\')) ("if the current folder has been deleted, the program runs in the process's working directory instead (" + $fromGone.Stdout.Trim() + ")") + } finally { Pop-Location } + + $explicit = Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd' -WorkingDirectory $env:SystemRoot + Test-Assert ($explicit.Stdout.Trim() -ieq $env:SystemRoot) "an explicit WorkingDirectory still wins over the current location" +} finally { + Pop-Location + Remove-Item -LiteralPath $here, $gone -Recurse -Force -ErrorAction SilentlyContinue +} +Write-Host "" + Set-PkMessageHandler -Handler $null Reset-PkAbortState diff --git a/ParallelKit.ps1 b/ParallelKit.ps1 index c7365fd..0ae280f 100644 --- a/ParallelKit.ps1 +++ b/ParallelKit.ps1 @@ -765,12 +765,14 @@ function Start-PkProcess { # Pass what this returns to Wait-PkProcess and Complete-PkProcess. Throws if the # program can't be started. # .PARAMETER FilePath - # The program to run. + # The program to run. A path with a folder in it (.\tool.exe, bin\tool.exe) is + # relative to your current location. A bare name (tool.exe) is found the way + # Windows finds programs, mostly on PATH. # .PARAMETER Arguments # Its command line as one string (see ConvertTo-PkArgumentString). # .PARAMETER WorkingDirectory - # Where to run it. Default: the PowerShell process's working directory, which - # isn't always your current location. + # Where to run it. Default: your current location. A relative path is relative + # to your current location too. # .PARAMETER OutputEncoding # How to decode its output: a name like 'utf-8', a code page number, or a # [System.Text.Encoding]. Default: the console's code page. @@ -801,12 +803,33 @@ function Start-PkProcess { $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.CreateNoWindow = $true - if ($WorkingDirectory) { $psi.WorkingDirectory = $WorkingDirectory } + + # Relative paths mean your current location, as everywhere else in + # PowerShell, not the process's working directory (which cd and + # Set-Location never change). From a location that isn't on disk (HKLM:, + # say) that's the last file-system one. If that folder is gone, fall back + # to the process's working directory, as before. + $here = $ExecutionContext.SessionState.Path.CurrentFileSystemLocation.ProviderPath + if (-not [System.IO.Directory]::Exists($here)) { $here = $null } # In a try/catch on purpose: a bare .NET call that throws does not # reliably stop a PowerShell script, and a $null process left behind - # would be polled forever. + # would be polled forever. The path handling is in here too: it throws + # on a path with characters Windows doesn't allow. try { + if ($WorkingDirectory) { + if ($null -ne $here -and -not [System.IO.Path]::IsPathRooted($WorkingDirectory)) { + $WorkingDirectory = [System.IO.Path]::Combine($here, $WorkingDirectory) + } + $psi.WorkingDirectory = $WorkingDirectory + } elseif ($null -ne $here) { + $psi.WorkingDirectory = $here + } + # Only a path with a folder in it: a bare name is left to Windows' + # own search, which looks on PATH. + if ($null -ne $here -and -not [System.IO.Path]::IsPathRooted($FilePath) -and ($FilePath.Contains('\') -or $FilePath.Contains('/'))) { + $psi.FileName = [System.IO.Path]::Combine($here, $FilePath) + } $proc = [System.Diagnostics.Process]::Start($psi) } catch { throw ("Start-PkProcess: could not start '" + $FilePath + "': " + $_) @@ -965,7 +988,7 @@ function Invoke-PkProcessWithTimeout { # .PARAMETER StreamTimeoutMs # How long to wait for the output after the program exits (default 15000). # .PARAMETER WorkingDirectory - # Where to run it, as for Start-PkProcess. + # Where to run it, as for Start-PkProcess. Default: your current location. # .PARAMETER OutputEncoding # How to decode its output, as for Start-PkProcess. # .PARAMETER IgnoreAbort diff --git a/README.md b/README.md index 5a90e1c..1a94597 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ Long parameter lists in these examples use splatting (`@batchParams`) instead of Each command is a hashtable with `FilePath`, `Arguments` and optionally `WorkingDirectory`, plus any keys of your own. `Arguments` is passed to the program as one string, exactly as written. `ConvertTo-PkArgumentString @(...)` builds that string from a list, quoting each argument so the program gets exactly that list back, spaces, quotes and trailing backslashes included. It follows the rules most Windows programs use; `cmd.exe /c` parses its own way. Programs run hidden, with their output captured. +Programs run in your current location (`Get-Location`) unless a command says otherwise, and a relative `WorkingDirectory`, or a `FilePath` with a folder in it like `.\tools\cjxl.exe`, is relative to that location too. A bare name like `cjxl.exe` is found the way Windows finds programs, mostly on `PATH`. A `.bat` or `.cmd` program can't run in a network-share (`\\server\share`) folder: `cmd.exe` falls back to `C:\Windows` there, so give such commands a local `WorkingDirectory`. + `Invoke-PkProcessBatch` returns an object with `Results`, `Aborted` and `ForceKilledCount`. Each result is an object with `Command`, `ExitCode`, `Stdout`, `Stderr`, `StreamTimedOut`, `StreamError`, `StartError`, `Aborted` and `TimedOut`, every field always present, so `$batch.Results | Format-Table` reads well: - `Command` is your original command hashtable, so extra keys like `Source` above come back with it. From d05b9a520ea92bf4749cccb3e7ae16b53d355f4b Mon Sep 17 00:00:00 2001 From: Hal <22532613+HalRiveria@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:24:27 -0400 Subject: [PATCH 2/3] Read program paths as PowerShell does, and name the path tried on failure A WorkingDirectory or FilePath on a drive PowerShell knows, like ~ or one made with New-PSDrive, was passed to Windows untranslated and failed. Those paths are now resolved the way PowerShell resolves them, and anything that isn't a file-system path to PowerShell is still taken relative to the last file-system location. When a program with a relative path can't be started, the error names the full path that was tried. Co-Authored-By: Claude Opus 5.5 --- ParallelKit.Harness.ps1 | 11 ++++++++++- ParallelKit.ps1 | 43 +++++++++++++++++++++++++++++------------ README.md | 2 +- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/ParallelKit.Harness.ps1 b/ParallelKit.Harness.ps1 index bfa532e..6c265b6 100644 --- a/ParallelKit.Harness.ps1 +++ b/ParallelKit.Harness.ps1 @@ -1849,6 +1849,9 @@ try { $relText = $null try { $relText = (Invoke-PkProcessWithTimeout -FilePath '.\pk-wd.cmd').Stdout.Trim() } catch { $relText = [string]$_ } + $missingText = '' + try { $null = Invoke-PkProcessWithTimeout -FilePath '.\no-such-tool.exe' } catch { $missingText = [string]$_ } + Test-Assert ($missingText -like ("*'.\no-such-tool.exe' (" + (Join-Path $here 'no-such-tool.exe') + ")*")) ("a program that can't be started is named with the full path that was tried (" + $missingText + ")") $relBatch = (Invoke-PkProcessBatch -Commands @(@{ FilePath = '.\pk-wd.cmd' })).Results[0] Test-Assert ($relText -ieq ("pk-wd ran in " + $here) -and $null -eq $relBatch.StartError -and $relBatch.Stdout.Trim() -ieq ("pk-wd ran in " + $here)) ("a FilePath with a folder in it (.\pk-wd.cmd) is found relative to the current location (" + $relText + ")") @@ -1860,8 +1863,14 @@ try { try { Push-Location -LiteralPath 'PkHarnessWd:\' try { $onDrive = (Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd').Stdout.Trim() } finally { Pop-Location } + $driveText = $null + try { $driveText = (Invoke-PkProcessWithTimeout -FilePath 'PkHarnessWd:\pk-wd.cmd' -WorkingDirectory 'PkHarnessWd:\sub').Stdout.Trim() } catch { $driveText = [string]$_ } } finally { Remove-PSDrive -Name PkHarnessWd -Scope Script } Test-Assert ($onDrive -ieq $here) ("on a drive made with New-PSDrive, the program runs in the real folder behind it (" + $onDrive + ")") + Test-Assert ($driveText -ieq ("pk-wd ran in " + (Join-Path $here 'sub'))) ("a FilePath and WorkingDirectory on a New-PSDrive drive are translated to the real folders (" + $driveText + ")") + $homeText = $null + try { $homeText = (Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd' -WorkingDirectory '~').Stdout.Trim() } catch { $homeText = [string]$_ } + Test-Assert ($homeText -ieq (Resolve-Path -LiteralPath '~').ProviderPath) ("a WorkingDirectory of ~ is your home folder (" + $homeText + ")") Push-Location -LiteralPath 'HKLM:\' try { $fromReg = (Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd').Stdout.Trim() } finally { Pop-Location } @@ -1871,7 +1880,7 @@ try { try { [System.IO.Directory]::Delete($gone, $true) # Remove-Item refuses to delete the current location $fromGone = Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd' - Test-Assert ($fromGone.ExitCode -eq 0 -and $fromGone.Stdout.Trim() -ieq [Environment]::CurrentDirectory.TrimEnd('\')) ("if the current folder has been deleted, the program runs in the process's working directory instead (" + $fromGone.Stdout.Trim() + ")") + Test-Assert ($fromGone.ExitCode -eq 0 -and $fromGone.Stdout.Trim().TrimEnd('\') -ieq [Environment]::CurrentDirectory.TrimEnd('\')) ("if the current folder has been deleted, the program runs in the process's working directory instead (" + $fromGone.Stdout.Trim() + ")") } finally { Pop-Location } $explicit = Invoke-PkProcessWithTimeout -FilePath $cmdExe -Arguments '/c cd' -WorkingDirectory $env:SystemRoot diff --git a/ParallelKit.ps1 b/ParallelKit.ps1 index 0ae280f..b3ac6c6 100644 --- a/ParallelKit.ps1 +++ b/ParallelKit.ps1 @@ -756,6 +756,23 @@ function Resolve-PkEncoding { } } +function Resolve-PkFileSystemPath { + # A path the way PowerShell reads it, turned into one Windows can use: + # relative to the current location, or on a drive like Data:\ or ~. + # Anything that isn't a file-system path to PowerShell (relative while + # the current location is HKLM:, say) is taken relative to $Here, the + # last file-system location. + param([string]$Path, [string]$Here) + if ([System.IO.Path]::IsPathRooted($Path)) { return $Path } + try { + $provider = $null + $drive = $null + $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path, [ref]$provider, [ref]$drive) + if ($provider.Name -eq 'FileSystem') { return $resolved } + } catch { } # not a path PowerShell can read (an unknown drive, say): the folder below is the best guess + return [System.IO.Path]::Combine($Here, $Path) +} + function Start-PkProcess { # .SYNOPSIS # Starts one program hidden, with its output captured in the background. @@ -765,14 +782,16 @@ function Start-PkProcess { # Pass what this returns to Wait-PkProcess and Complete-PkProcess. Throws if the # program can't be started. # .PARAMETER FilePath - # The program to run. A path with a folder in it (.\tool.exe, bin\tool.exe) is - # relative to your current location. A bare name (tool.exe) is found the way - # Windows finds programs, mostly on PATH. + # The program to run. A path with a folder in it is read as PowerShell reads + # paths: .\tool.exe and bin\tool.exe are relative to your current location, and + # drives like Data:\ or ~ work. A bare name (tool.exe) is found the way Windows + # finds programs, mostly on PATH. # .PARAMETER Arguments # Its command line as one string (see ConvertTo-PkArgumentString). # .PARAMETER WorkingDirectory - # Where to run it. Default: your current location. A relative path is relative - # to your current location too. + # Where to run it. Default: your current location. Read as PowerShell reads + # paths, so a relative path is relative to your current location, and drives + # like Data:\ or ~ work. # .PARAMETER OutputEncoding # How to decode its output: a name like 'utf-8', a code page number, or a # [System.Text.Encoding]. Default: the console's code page. @@ -818,21 +837,21 @@ function Start-PkProcess { # on a path with characters Windows doesn't allow. try { if ($WorkingDirectory) { - if ($null -ne $here -and -not [System.IO.Path]::IsPathRooted($WorkingDirectory)) { - $WorkingDirectory = [System.IO.Path]::Combine($here, $WorkingDirectory) - } + if ($null -ne $here) { $WorkingDirectory = Resolve-PkFileSystemPath -Path $WorkingDirectory -Here $here } $psi.WorkingDirectory = $WorkingDirectory } elseif ($null -ne $here) { $psi.WorkingDirectory = $here } # Only a path with a folder in it: a bare name is left to Windows' # own search, which looks on PATH. - if ($null -ne $here -and -not [System.IO.Path]::IsPathRooted($FilePath) -and ($FilePath.Contains('\') -or $FilePath.Contains('/'))) { - $psi.FileName = [System.IO.Path]::Combine($here, $FilePath) + if ($null -ne $here -and ($FilePath.Contains('\') -or $FilePath.Contains('/'))) { + $psi.FileName = Resolve-PkFileSystemPath -Path $FilePath -Here $here } $proc = [System.Diagnostics.Process]::Start($psi) } catch { - throw ("Start-PkProcess: could not start '" + $FilePath + "': " + $_) + $tried = "" + if ($psi.FileName -ne $FilePath) { $tried = " (" + $psi.FileName + ")" } + throw ("Start-PkProcess: could not start '" + $FilePath + "'" + $tried + ": " + $_) } $null = [PkChildProcessJob]::Assign($proc) return @{ @@ -1708,7 +1727,7 @@ function Get-PkWorkerSafeFunctionNames { return @('Write-PkMessage', 'Test-PkAbortSignal', 'Test-PkCancelRequested', 'Initialize-PkThreadPool', 'Stop-PkProcessTree', 'Stop-PkJobProcesses', 'Resolve-PkEncoding', 'Start-PkProcess', 'Wait-PkProcess', 'Complete-PkProcess', 'Invoke-PkProcessWithTimeout', 'Invoke-PkProcessDispatch', 'Invoke-PkProcessDispatchCore', 'Invoke-PkProcessBatch', - 'Register-PkCancellationSource', 'Unregister-PkCancellationSource', 'New-PkBatchResult', 'ConvertTo-PkArgumentString', 'Get-PkForceStopReason') + 'Register-PkCancellationSource', 'Unregister-PkCancellationSource', 'New-PkBatchResult', 'ConvertTo-PkArgumentString', 'Get-PkForceStopReason', 'Resolve-PkFileSystemPath') } function Receive-PkCompletedHandles { diff --git a/README.md b/README.md index 1a94597..0726f97 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Long parameter lists in these examples use splatting (`@batchParams`) instead of Each command is a hashtable with `FilePath`, `Arguments` and optionally `WorkingDirectory`, plus any keys of your own. `Arguments` is passed to the program as one string, exactly as written. `ConvertTo-PkArgumentString @(...)` builds that string from a list, quoting each argument so the program gets exactly that list back, spaces, quotes and trailing backslashes included. It follows the rules most Windows programs use; `cmd.exe /c` parses its own way. Programs run hidden, with their output captured. -Programs run in your current location (`Get-Location`) unless a command says otherwise, and a relative `WorkingDirectory`, or a `FilePath` with a folder in it like `.\tools\cjxl.exe`, is relative to that location too. A bare name like `cjxl.exe` is found the way Windows finds programs, mostly on `PATH`. A `.bat` or `.cmd` program can't run in a network-share (`\\server\share`) folder: `cmd.exe` falls back to `C:\Windows` there, so give such commands a local `WorkingDirectory`. +Programs run in your current location (`Get-Location`) unless a command says otherwise, and a relative `WorkingDirectory`, or a `FilePath` with a folder in it like `.\tools\cjxl.exe`, is relative to that location too. Drives PowerShell knows, like `~` or one made with `New-PSDrive`, work in both. A bare name like `cjxl.exe` is found the way Windows finds programs, mostly on `PATH`. A `.bat` or `.cmd` program can't run in a network-share (`\\server\share`) folder: `cmd.exe` falls back to `C:\Windows` there, so give such commands a local `WorkingDirectory`. `Invoke-PkProcessBatch` returns an object with `Results`, `Aborted` and `ForceKilledCount`. Each result is an object with `Command`, `ExitCode`, `Stdout`, `Stderr`, `StreamTimedOut`, `StreamError`, `StartError`, `Aborted` and `TimedOut`, every field always present, so `$batch.Results | Format-Table` reads well: From 9e688d5c8460ccc3f1c9f26035c156490fa3870c Mon Sep 17 00:00:00 2001 From: Hal <22532613+HalRiveria@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:01:50 -0400 Subject: [PATCH 3/3] Compare against the test folder as PowerShell reports it On GitHub's runners %TEMP% is an 8.3 short path (C:\Users\RUNNER~1\...), while PowerShell's current location, and so the folder programs start in, uses the long name. The new working-directory checks compared against the short form and failed although the programs ran in the right folder. Co-Authored-By: Claude Opus 5.5 --- ParallelKit.Harness.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ParallelKit.Harness.ps1 b/ParallelKit.Harness.ps1 index 6c265b6..e4cc5a4 100644 --- a/ParallelKit.Harness.ps1 +++ b/ParallelKit.Harness.ps1 @@ -1841,6 +1841,9 @@ function Get-HarnessCd { return $wd } Push-Location -LiteralPath $here +# %TEMP% can be an 8.3 short path (C:\Users\RUNNER~1\... on GitHub's +# runners); compare against the folder as PowerShell reports it. +$here = (Get-Location).ProviderPath try { Test-Assert ([Environment]::CurrentDirectory -ne $here) ("precondition: the process's working directory isn't the test folder (" + [Environment]::CurrentDirectory + ")") $wd = Get-HarnessCd