diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 24ad3e8409ec7..f3395734845d1 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -77,6 +77,20 @@ fi # ── Run in hermetic env ────────────────────────────────────────────────────── # env -i: start with empty environment, opt-in only what we need. # No credential var can leak — you'd have to explicitly add it here. +# +# On Windows, Python's pathlib.Path.home() does not use the POSIX HOME alias; +# it resolves USERPROFILE or HOMEDRIVE+HOMEPATH. Preserve only those non-secret +# home-resolution variables so env -i remains hermetic without creating a +# literal '~' path or breaking Windows tests. +WINDOWS_HOME_ENV=() +if [[ -n "${USERPROFILE:-}" || -n "${HOMEDRIVE:-}${HOMEPATH:-}" ]]; then + for name in USERPROFILE HOMEDRIVE HOMEPATH; do + if [[ -n "${!name:-}" ]]; then + WINDOWS_HOME_ENV+=("$name=${!name}") + fi + done +fi + echo "▶ running per-file parallel test suite via run_tests_parallel.py" echo " (TZ=UTC LANG=C.UTF-8 PYTHONHASHSEED=0; clean env)" @@ -94,6 +108,7 @@ echo "▶ launching test runner" exec env -i \ PATH="$PATH" \ HOME="$HOME" \ + "${WINDOWS_HOME_ENV[@]}" \ TZ=UTC \ LANG=C.UTF-8 \ LC_ALL=C.UTF-8 \ diff --git a/tests/plugins/memory/test_hindsight_provider.py b/tests/plugins/memory/test_hindsight_provider.py index 5cd485d4c1ad7..1083d8df9eeb3 100644 --- a/tests/plugins/memory/test_hindsight_provider.py +++ b/tests/plugins/memory/test_hindsight_provider.py @@ -10,6 +10,7 @@ import re import stat import sys +from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock @@ -36,8 +37,8 @@ @pytest.fixture(autouse=True) -def _clean_env(monkeypatch): - """Ensure no stale env vars leak between tests.""" +def _clean_env(tmp_path, monkeypatch): + """Ensure no stale env vars or Windows home state leak between tests.""" for key in ( "HINDSIGHT_API_KEY", "HINDSIGHT_API_URL", "HINDSIGHT_BANK_ID", "HINDSIGHT_BUDGET", "HINDSIGHT_MODE", "HINDSIGHT_TIMEOUT", @@ -48,6 +49,12 @@ def _clean_env(monkeypatch): ): monkeypatch.delenv(key, raising=False) + # On Windows pathlib.Path.home() resolves USERPROFILE/HOMEDRIVE+HOMEPATH, + # not the POSIX HOME alias that these tests historically monkeypatched. + # Patch the actual API and keep all legacy profile writes in tmp_path. + isolated_home = tmp_path / "user-home" + monkeypatch.setattr(Path, "home", classmethod(lambda cls: isolated_home)) + def _make_mock_client(): """Create a mock Hindsight client with async methods.""" diff --git a/tests/test_run_tests_shell_env.py b/tests/test_run_tests_shell_env.py new file mode 100644 index 0000000000000..459b2b621b7e0 --- /dev/null +++ b/tests/test_run_tests_shell_env.py @@ -0,0 +1,15 @@ +"""Windows regression tests for the canonical hermetic runner.""" + +import os +from pathlib import Path + +import pytest + + +@pytest.mark.skipif(os.name != "nt", reason="Windows home resolution is the behavior under test") +def test_runner_preserves_windows_home_resolution(): + """Path.home() must resolve when run under scripts/run_tests.sh.""" + actual = Path.home() + expected = os.environ.get("USERPROFILE") + assert expected, "canonical runner dropped USERPROFILE" + assert actual == Path(expected)