Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ def _build_script(self, command: str, sentinel: str) -> str:
# earlier command is still there when a cmdlet-only command
# runs. Snapshot it instead of clearing it: the variable stays
# readable by the user's own commands, which persistent mode is
# meant to preserve, and a value that differs afterwards can
# meant to preserve, and a different object afterwards can
# only have been written by this command.
" $__af_last = $LASTEXITCODE;"
" try {"
Expand All @@ -393,7 +393,12 @@ def _build_script(self, command: str, sentinel: str) -> str:
# $? has to be read on the very next statement: anything else
# in between overwrites it.
" $__af_ok = $?;"
" if ($LASTEXITCODE -ne $__af_last) { $__af_rc = $LASTEXITCODE }"
# Compare by reference, not by value: every native exit stores
# a newly boxed int, so a command that fails with the same code
# as the previous one still counts as a change. A value
# comparison would miss it, and $? cannot catch it either,
# because it reflects Invoke-Expression, which succeeded.
" if (-not [object]::ReferenceEquals($LASTEXITCODE, $__af_last)) { $__af_rc = $LASTEXITCODE }"
" elseif (-not $__af_ok) { $__af_rc = 1 }"
" } catch {"
" [Console]::Error.WriteLine($_.ToString());"
Expand Down
30 changes: 30 additions & 0 deletions python/packages/tools/tests/test_local_shell_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,36 @@ async def test_persistent_powershell_does_not_inherit_previous_exit_code() -> No
)


@pytest.mark.skipif(_POWERSHELL is None, reason="PowerShell is not installed")
async def test_persistent_powershell_reports_repeated_native_exit_code(tmp_path: os.PathLike[str]) -> None:
"""A native command failing with the same code as the previous one must still report it.

``$LASTEXITCODE`` keeps the same value in that case, and ``$?`` reflects
``Invoke-Expression`` rather than the native command, so the second
failure used to be reported as ``exit_code=0``.
Runs wherever PowerShell is installed, not just on Windows.
"""
assert _POWERSHELL is not None
failing = f"& '{sys.executable}' -c 'import sys; sys.exit(3)'"
async with LocalShellTool(
mode="persistent",
shell=[_POWERSHELL, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "-"],
approval_mode="never_require",
acknowledge_unsafe=True,
workdir=str(tmp_path),
) as tool:
for attempt in range(1, 4):
result = await tool.run(failing)
assert result.exit_code == 3, f"attempt {attempt} reported rc {result.exit_code}"

succeeding = await tool.run("Write-Output ok")
assert succeeding.exit_code == 0

readback = await tool.run("Write-Output $LASTEXITCODE")
assert readback.exit_code == 0
assert readback.stdout.strip() == "3"


@pytest.mark.skipif(sys.platform != "win32", reason="PowerShell-specific encoding")
async def test_persistent_powershell_utf8_roundtrip() -> None:
"""Non-ASCII output should round-trip without mojibake."""
Expand Down
Loading