|
22 | 22 | _WRAPPER_ENV, |
23 | 23 | _find_external_promptfoo, |
24 | 24 | _find_windows_promptfoo, |
| 25 | + _normalize_exit_code, |
25 | 26 | _normalize_path, |
26 | 27 | _requires_shell, |
27 | 28 | _resolve_argv0, |
@@ -358,6 +359,34 @@ def test_run_command_passes_environment(self, monkeypatch: pytest.MonkeyPatch) - |
358 | 359 | assert call_args.kwargs.get("env") == env |
359 | 360 |
|
360 | 361 |
|
| 362 | +class TestExitCodeNormalization: |
| 363 | + """Test subprocess exit code normalization.""" |
| 364 | + |
| 365 | + @pytest.mark.parametrize("returncode", [0, 1, 100, 255]) |
| 366 | + def test_normalize_exit_code_preserves_standard_codes( |
| 367 | + self, returncode: int, monkeypatch: pytest.MonkeyPatch |
| 368 | + ) -> None: |
| 369 | + """Standard shell exit codes pass through unchanged.""" |
| 370 | + monkeypatch.setattr(os, "name", "nt") |
| 371 | + |
| 372 | + assert _normalize_exit_code(returncode) == returncode |
| 373 | + |
| 374 | + @pytest.mark.parametrize("returncode", [4294967295, 3221226505, -1]) |
| 375 | + def test_normalize_exit_code_maps_windows_error_statuses( |
| 376 | + self, returncode: int, monkeypatch: pytest.MonkeyPatch |
| 377 | + ) -> None: |
| 378 | + """Windows unsigned and NTSTATUS failure values map to exit code 1.""" |
| 379 | + monkeypatch.setattr(os, "name", "nt") |
| 380 | + |
| 381 | + assert _normalize_exit_code(returncode) == 1 |
| 382 | + |
| 383 | + def test_normalize_exit_code_maps_unix_signal_status(self, monkeypatch: pytest.MonkeyPatch) -> None: |
| 384 | + """Unix signal-style negative return codes map to 128 + signal number.""" |
| 385 | + monkeypatch.setattr(os, "name", "posix") |
| 386 | + |
| 387 | + assert _normalize_exit_code(-15) == 143 |
| 388 | + |
| 389 | + |
361 | 390 | # ============================================================================= |
362 | 391 | # Integration Tests for main() |
363 | 392 | # ============================================================================= |
@@ -533,6 +562,30 @@ def test_main_returns_subprocess_exit_code(self, monkeypatch: pytest.MonkeyPatch |
533 | 562 |
|
534 | 563 | assert exc_info.value.code == 42 |
535 | 564 |
|
| 565 | + @pytest.mark.parametrize("raw_returncode", [4294967295, 3221226505]) |
| 566 | + def test_main_normalizes_windows_error_statuses(self, raw_returncode: int, monkeypatch: pytest.MonkeyPatch) -> None: |
| 567 | + """Converts Windows-specific subprocess statuses into a stable exit code.""" |
| 568 | + monkeypatch.setattr(os, "name", "nt") |
| 569 | + monkeypatch.setattr(sys, "argv", ["promptfoo", "eval", "-c", "missing.yaml"]) |
| 570 | + monkeypatch.setattr( |
| 571 | + "shutil.which", |
| 572 | + lambda cmd, path=None: { |
| 573 | + "node": "C:\\Program Files\\nodejs\\node.exe", |
| 574 | + "npx": "C:\\Program Files\\nodejs\\npx.cmd", |
| 575 | + }.get(cmd), |
| 576 | + ) |
| 577 | + monkeypatch.setattr("promptfoo.cli.record_wrapper_used", lambda mode: None) |
| 578 | + monkeypatch.setattr( |
| 579 | + subprocess, |
| 580 | + "run", |
| 581 | + MagicMock(return_value=subprocess.CompletedProcess([], raw_returncode)), |
| 582 | + ) |
| 583 | + |
| 584 | + with pytest.raises(SystemExit) as exc_info: |
| 585 | + main() |
| 586 | + |
| 587 | + assert exc_info.value.code == 1 |
| 588 | + |
536 | 589 |
|
537 | 590 | # ============================================================================= |
538 | 591 | # Platform-Specific Tests |
|
0 commit comments