Skip to content

fix(shell): stop PowerShell wrapper leaking a stray exit-code line to stdout#3425

Merged
max-sixty merged 3 commits into
mainfrom
fix/powershell-stray-exit-code-29183703689
Jul 12, 2026
Merged

fix(shell): stop PowerShell wrapper leaking a stray exit-code line to stdout#3425
max-sixty merged 3 commits into
mainfrom
fix/powershell-stray-exit-code-29183703689

Conversation

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Problem

The PowerShell shell-integration wrapper ends its wt function with return $exitCode. In PowerShell, return <value> writes the value to the function's output (success) stream — it does not set the process/$LASTEXITCODE. So every wt invocation appends a stray exit-code line (0 on success) to stdout, right after the real command output:

PS> wt list
<worktree table>
0            <-- stray, emitted by `return $exitCode`

This corrupts any capture of a command's output, e.g. $out = wt list --format json yields JSON with a trailing 0, and wt ... | ... pipelines see an extra element.

Exit-code propagation is already handled without the return:

  • $global:LASTEXITCODE = $exitCode (line 77) propagates $LASTEXITCODE
  • the Write-Error … -ErrorAction SilentlyContinue sets $? to $false on failure

So return $exitCode was redundant for propagation and its only observable effect was the stdout pollution.

Fix

Remove the trailing return $exitCode from the wrapper (templates/powershell.ps1) and document why it must not be reintroduced.

Verification

Reproduced against pwsh 7.6 with the rendered wrapper: with the return, wt list emits <output> then a bare 0; without it, only <output>.

Added a regression test test_powershell_wrapper_no_stray_exit_code_on_stdout (in tests/integration_tests/shell_powershell.rs, shell-integration-tests feature). It uses a .ps1 mock that emits one line and exit 0 so $LASTEXITCODE is actually set — the pre-existing wrapper test's mock only calls Write-Output, leaving $LASTEXITCODE unset, so return $null emitted nothing and the bug stayed hidden. The new test asserts stdout is exactly the mock's line, which fails before this change (a stray 0 follows) and passes after.

The src/shell/…init_powershell.snap snapshot is regenerated to drop the return line.

Why the existing test didn't catch it

test_powershell_wrapper_passes_short_flags_through writes foreach ($a in $args) { Write-Output $a } as its mock. A PowerShell .ps1 that never runs a native command or exit leaves $LASTEXITCODE at its prior value ($null in a fresh session), so the wrapper's return $exitCode became return $null, which emits nothing to the output stream. A real native wt.exe always sets $LASTEXITCODE, so the pollution only manifests in real use.

… stdout

The wrapper ended with `return $exitCode`, which in PowerShell writes the
value to the output stream — appending a bare `0` after every command's real
output and corrupting captures like `$out = wt list --format json`. Exit-code
propagation is already handled by $global:LASTEXITCODE and Write-Error, so the
return was redundant. Add a regression test whose mock exits with a real code
(the existing test's mock left $LASTEXITCODE unset, hiding the bug).
@worktrunk-bot worktrunk-bot added the nightly-cleanup Issues found by nightly code quality sweep label Jul 12, 2026

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core fix is correct — I reproduced it against pwsh 7.6 with the rendered wrapper: with return $exitCode, $out = wt list captures MOCK_OUTPUT_LINE 0 (stray trailing 0); without it, $out is just MOCK_OUTPUT_LINE. $LASTEXITCODE still propagates (3 on a failing mock), and the process exit code is unchanged. The regression test correctly forces $LASTEXITCODE to be set via exit 0 in the mock, which is what the pre-existing test missed. Good catch on why the old test didn't see it.

One accuracy nit on the added comment (not blocking, and not a regression — pre-existing behavior is identical before and after this PR):

Setting $global:LASTEXITCODE plus the Write-Error below already propagates both $LASTEXITCODE and $?.

The $LASTEXITCODE half holds, but the $? half doesn't. A non-terminating Write-Error … -ErrorAction SilentlyContinue sets $? = $false only within the function's own scope; because the wrapper is (deliberately) a simple function, that failure signal doesn't cross the function boundary, so $? is $true in the caller even when wt failed. This is why the load-bearing justification for dropping return is really just $global:LASTEXITCODE — the Write-Error doesn't actually reach the caller's $?.

Worth softening the comment so a future maintainer doesn't rely on $? reflecting a wt failure. The underlying $?-doesn't-propagate gap predates this PR and is out of scope here, but if it's worth fixing it'd be a separate change (and can't use Write-Error, since the wrapper must stay a simple function).

Verification (pwsh 7.6)

Isolated — Write-Error does set $? in its own scope:

Write-Error 'x' -ErrorAction SilentlyContinue; $?   # -> False

But through the wrapper function it doesn't reach the caller:

function foo { Write-Error 'boom' -ErrorAction SilentlyContinue }
foo; $?                                              # -> True

Rendered wrapper against a mock that does exit 3:

$env:WORKTRUNK_BIN='mock-wt-fail.ps1'; . rendered-wrapper.ps1
wt list; $q = $?
# $q       -> True   (does NOT reflect the failure)
# $LASTEXITCODE -> 3  (correct)

Capture cleanliness (the actual fix), success mock:

old (return $exitCode):  $out = wt list  ->  "MOCK_OUTPUT_LINE 0"
new (no return):         $out = wt list  ->  "MOCK_OUTPUT_LINE"

Write-Error in a simple function does not set the caller's $?; only
$global:LASTEXITCODE propagates a wt failure to scripts/CI. Soften the
comment (and the regression-test docstring) so a future maintainer does
not rely on $? reflecting a failure.
@max-sixty
max-sixty merged commit eba35d0 into main Jul 12, 2026
37 checks passed
@max-sixty
max-sixty deleted the fix/powershell-stray-exit-code-29183703689 branch July 12, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

nightly-cleanup Issues found by nightly code quality sweep

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants