From 2fc30ee6a52e503b672e90d8742a2a9de21c2cf8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:02:06 +0000 Subject: [PATCH 1/2] chore(deps): bump pygments from 2.19.2 to 2.20.0 Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0. - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.19.2...2.20.0) --- updated-dependencies: - dependency-name: pygments dependency-version: 2.20.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- uv.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 401a91e..f0f9809 100644 --- a/uv.lock +++ b/uv.lock @@ -414,7 +414,7 @@ wheels = [ [[package]] name = "promptfoo" -version = "0.1.1" +version = "0.1.3" source = { editable = "." } dependencies = [ { name = "posthog", version = "6.9.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -446,11 +446,11 @@ provides-extras = ["dev"] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] From 5a2fa15cd5dbab97aca6deae39f3386d293cbfb9 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Thu, 2 Apr 2026 10:48:03 -0700 Subject: [PATCH 2/2] fix: normalize subprocess exit codes on Windows --- src/promptfoo/cli.py | 15 +++++++++++++- tests/test_cli.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/promptfoo/cli.py b/src/promptfoo/cli.py index 2d35896..c93dd80 100644 --- a/src/promptfoo/cli.py +++ b/src/promptfoo/cli.py @@ -15,6 +15,7 @@ _WRAPPER_ENV = "PROMPTFOO_PY_WRAPPER" _WINDOWS_SHELL_EXTENSIONS = (".bat", ".cmd") +_WINDOWS_STATUS_SIGN_BIT = 1 << 31 _VERSION_ENV = "PROMPTFOO_VERSION" @@ -173,6 +174,18 @@ def _run_command(cmd: list[str], env: Optional[dict[str, str]] = None) -> subpro return subprocess.run(cmd, env=env) +def _normalize_exit_code(returncode: int) -> int: + """Normalize subprocess return codes into portable process exit codes.""" + if os.name == "nt": + if returncode < 0 or returncode & _WINDOWS_STATUS_SIGN_BIT: + return 1 + return returncode + + if returncode < 0: + return 128 + min(abs(returncode), 127) + return returncode + + def main() -> NoReturn: """ Main entry point for the promptfoo CLI wrapper. @@ -207,7 +220,7 @@ def main() -> NoReturn: print("Or ensure Node.js is properly installed.", file=sys.stderr) sys.exit(1) - sys.exit(result.returncode) + sys.exit(_normalize_exit_code(result.returncode)) except KeyboardInterrupt: sys.exit(130) diff --git a/tests/test_cli.py b/tests/test_cli.py index 0e4a1c0..b41ba7e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -22,6 +22,7 @@ _WRAPPER_ENV, _find_external_promptfoo, _find_windows_promptfoo, + _normalize_exit_code, _normalize_path, _requires_shell, _resolve_argv0, @@ -358,6 +359,34 @@ def test_run_command_passes_environment(self, monkeypatch: pytest.MonkeyPatch) - assert call_args.kwargs.get("env") == env +class TestExitCodeNormalization: + """Test subprocess exit code normalization.""" + + @pytest.mark.parametrize("returncode", [0, 1, 100, 255]) + def test_normalize_exit_code_preserves_standard_codes( + self, returncode: int, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Standard shell exit codes pass through unchanged.""" + monkeypatch.setattr(os, "name", "nt") + + assert _normalize_exit_code(returncode) == returncode + + @pytest.mark.parametrize("returncode", [4294967295, 3221226505, -1]) + def test_normalize_exit_code_maps_windows_error_statuses( + self, returncode: int, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Windows unsigned and NTSTATUS failure values map to exit code 1.""" + monkeypatch.setattr(os, "name", "nt") + + assert _normalize_exit_code(returncode) == 1 + + def test_normalize_exit_code_maps_unix_signal_status(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Unix signal-style negative return codes map to 128 + signal number.""" + monkeypatch.setattr(os, "name", "posix") + + assert _normalize_exit_code(-15) == 143 + + # ============================================================================= # Integration Tests for main() # ============================================================================= @@ -533,6 +562,26 @@ def test_main_returns_subprocess_exit_code(self, monkeypatch: pytest.MonkeyPatch assert exc_info.value.code == 42 + @pytest.mark.parametrize("raw_returncode", [4294967295, 3221226505]) + def test_main_normalizes_windows_error_statuses(self, raw_returncode: int, monkeypatch: pytest.MonkeyPatch) -> None: + """Converts Windows-specific subprocess statuses into a stable exit code.""" + monkeypatch.setattr(os, "name", "nt") + monkeypatch.setattr(sys, "argv", ["promptfoo", "eval", "-c", "missing.yaml"]) + monkeypatch.setattr( + "shutil.which", + lambda cmd, path=None: { + "node": "C:\\Program Files\\nodejs\\node.exe", + "npx": "C:\\Program Files\\nodejs\\npx.cmd", + }.get(cmd), + ) + monkeypatch.setattr("promptfoo.cli.record_wrapper_used", lambda mode: None) + monkeypatch.setattr(subprocess, "run", MagicMock(return_value=subprocess.CompletedProcess([], raw_returncode))) + + with pytest.raises(SystemExit) as exc_info: + main() + + assert exc_info.value.code == 1 + # ============================================================================= # Platform-Specific Tests