diff --git a/tests/conftest.py b/tests/conftest.py index a6458d7..1e46d61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,11 @@ """Shared fixtures for EvoScientist tests.""" +from pathlib import Path + import pytest +_NONEXISTENT_DOTENV = str(Path(__file__).with_name(".pytest-dotenv-does-not-exist")) + @pytest.fixture(autouse=True) def _reset_tool_selection_state(): @@ -164,3 +168,24 @@ def restore_model_passthrough_patch(): yield finally: _reset() + + +@pytest.fixture(autouse=True) +def _isolate_dotenv(monkeypatch): + """Keep the developer's real .env out of the test environment. + + ``get_effective_config`` runs ``load_dotenv(find_dotenv(usecwd=True), + override=True)``, so any test that loads config injects the repo's + real .env into ``os.environ`` for the rest of the pytest process. + An empty-valued line like ``MINIMAX_BASE_URL=`` then makes + ``os.environ.get(key, default)`` return "" instead of the default, + breaking unrelated tests later in the run (see issue #322). + + Pointing ``find_dotenv`` at a fixed path that does not exist makes + ``load_dotenv`` a no-op without creating a temporary directory for + every test. + """ + monkeypatch.setattr( + "EvoScientist.config.settings.find_dotenv", + lambda *args, **kwargs: _NONEXISTENT_DOTENV, + ) diff --git a/tests/test_config.py b/tests/test_config.py index df7a7a7..522d964 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -53,11 +53,6 @@ def temp_config_dir(tmp_path, monkeypatch): """Use a temporary directory for config during tests.""" config_dir = tmp_path / "evoscientist" monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path)) - # Prevent load_dotenv from loading the project's real .env file - monkeypatch.setattr( - "EvoScientist.config.settings.find_dotenv", - lambda *a, **k: str(tmp_path / ".env"), - ) # Also clear any API keys from environment for key in [ "ANTHROPIC_API_KEY", @@ -930,3 +925,30 @@ def test_scheduler_config_defaults_and_env(monkeypatch): assert eff2.memory_skill_synthesis_mode == MemorySkillSynthesisMode.AUTO assert eff2.memory_skill_synthesis_cadence == MemorySkillSynthesisCadence.MONTHLY assert eff2.memory_skill_synthesis_time == "04:30" + + +# ============================================================================= +# Dotenv isolation (issue #322) +# ============================================================================= + + +class TestDotenvIsolation: + def test_env_file_not_leaked_into_process_env(self, tmp_path, monkeypatch): + """A .env in cwd must not leak into os.environ during tests. + + Without the suite-wide ``_isolate_dotenv`` fixture, + ``get_effective_config`` loads the developer's real .env with + ``override=True``; an empty-valued line like ``MINIMAX_BASE_URL=`` + then poisons ``os.environ.get(key, default)`` lookups for every + test that runs afterwards in the same process. + """ + monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path)) + repro_dir = tmp_path / "repro" + repro_dir.mkdir() + (repro_dir / ".env").write_text("MINIMAX_BASE_URL=\n") + monkeypatch.chdir(repro_dir) + monkeypatch.delenv("MINIMAX_BASE_URL", raising=False) + + get_effective_config() + + assert "MINIMAX_BASE_URL" not in os.environ