5a581c78a2
Build / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
Introduce provider, model, and invocation contracts with encrypted configuration persistence. Add web runtime fencing, route fallback, recovery middleware, workspace scoping, and comprehensive tests.
999 lines
42 KiB
Python
999 lines
42 KiB
Python
"""Tests for EvoScientist configuration management."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from EvoScientist.config import (
|
|
EvoScientistConfig,
|
|
MemoryControls,
|
|
MemoryObservationTarget,
|
|
MemoryObservationWriter,
|
|
MemorySkillSynthesisCadence,
|
|
MemorySkillSynthesisMode,
|
|
apply_config_to_env,
|
|
get_config_dir,
|
|
get_config_path,
|
|
get_config_value,
|
|
get_effective_config,
|
|
list_config,
|
|
load_config,
|
|
reset_config,
|
|
save_config,
|
|
set_config_value,
|
|
)
|
|
|
|
|
|
def test_langgraph_dev_port_defaults_to_ai4sci_runtime_port():
|
|
assert EvoScientistConfig().langgraph_dev_port == 3076
|
|
|
|
# =============================================================================
|
|
# Fixtures
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _restore_dangerous_env():
|
|
"""Snapshot/restore EVOSCIENTIST_DANGEROUS_MODE around every test.
|
|
|
|
apply_config_to_env writes this var via direct ``os.environ`` assignment, and
|
|
monkeypatch's ``delenv`` of an originally-absent key records no undo — so
|
|
without this, a test that turns dangerous mode on would leak the env var into
|
|
later tests (an order-dependent landmine, e.g. under pytest-randomly).
|
|
"""
|
|
_sentinel = object()
|
|
_prev = os.environ.get("EVOSCIENTIST_DANGEROUS_MODE", _sentinel)
|
|
yield
|
|
if _prev is _sentinel:
|
|
os.environ.pop("EVOSCIENTIST_DANGEROUS_MODE", None)
|
|
else:
|
|
os.environ["EVOSCIENTIST_DANGEROUS_MODE"] = _prev
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_config_dir(tmp_path, monkeypatch):
|
|
"""Use a temporary directory for config during tests."""
|
|
config_dir = tmp_path / "evoscientist"
|
|
monkeypatch.delenv("EVOSCIENTIST_CONFIG_DIR", raising=False)
|
|
monkeypatch.delenv("EVOSCIENTIST_HOME", raising=False)
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
# Also clear any API keys from environment
|
|
for key in [
|
|
"ANTHROPIC_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"TAVILY_API_KEY",
|
|
"S2_API_KEY",
|
|
"EVOSCIENTIST_DEFAULT_MODE",
|
|
"EVOSCIENTIST_WORKSPACE_DIR",
|
|
"EVOSCIENTIST_UI_BACKEND",
|
|
"EVOSCIENTIST_MEMORY_PROFILE_ENABLED",
|
|
"EVOSCIENTIST_MEMORY_OBSERVATIONS_ENABLED",
|
|
"EVOSCIENTIST_MEMORY_OBSERVATION_WRITER",
|
|
"EVOSCIENTIST_MEMORY_WORKERS_ENABLED",
|
|
"EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_ENABLED",
|
|
"EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_MODE",
|
|
"EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_CADENCE",
|
|
"EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_TIME",
|
|
"EVOSCIENTIST_AUXILIARY_MODEL",
|
|
"EVOSCIENTIST_AUXILIARY_PROVIDER",
|
|
"EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE",
|
|
"EVOSCIENTIST_OPENROUTER_HTTP_REFERER",
|
|
"EVOSCIENTIST_OPENROUTER_APP_TITLE",
|
|
"EVOSCIENTIST_OPENROUTER_APP_CATEGORIES",
|
|
"EVOSCIENTIST_DANGEROUS_MODE",
|
|
]:
|
|
monkeypatch.delenv(key, raising=False)
|
|
return config_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def clean_env(monkeypatch):
|
|
"""Remove environment variables that affect config (but keep temp config dir)."""
|
|
for key in [
|
|
"ANTHROPIC_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"TAVILY_API_KEY",
|
|
"S2_API_KEY",
|
|
"EVOSCIENTIST_DEFAULT_MODE",
|
|
"EVOSCIENTIST_WORKSPACE_DIR",
|
|
"EVOSCIENTIST_UI_BACKEND",
|
|
"EVOSCIENTIST_MEMORY_PROFILE_ENABLED",
|
|
"EVOSCIENTIST_MEMORY_OBSERVATIONS_ENABLED",
|
|
"EVOSCIENTIST_MEMORY_OBSERVATION_WRITER",
|
|
"EVOSCIENTIST_MEMORY_WORKERS_ENABLED",
|
|
"EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_ENABLED",
|
|
"EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_MODE",
|
|
"EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_CADENCE",
|
|
"EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_TIME",
|
|
"EVOSCIENTIST_AUXILIARY_MODEL",
|
|
"EVOSCIENTIST_AUXILIARY_PROVIDER",
|
|
"EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE",
|
|
"EVOSCIENTIST_OPENROUTER_HTTP_REFERER",
|
|
"EVOSCIENTIST_OPENROUTER_APP_TITLE",
|
|
"EVOSCIENTIST_OPENROUTER_APP_CATEGORIES",
|
|
"EVOSCIENTIST_DANGEROUS_MODE",
|
|
]:
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
|
|
# =============================================================================
|
|
# Test EvoScientistConfig dataclass
|
|
# =============================================================================
|
|
|
|
|
|
class TestEvoScientistConfig:
|
|
def test_default_values(self):
|
|
"""Test that default values are set correctly."""
|
|
config = EvoScientistConfig()
|
|
|
|
assert config.anthropic_api_key == ""
|
|
assert config.openai_api_key == ""
|
|
assert config.tavily_api_key == ""
|
|
assert config.semantic_scholar_api_key == ""
|
|
assert config.provider == "anthropic"
|
|
assert config.model == "claude-sonnet-4-6"
|
|
assert config.default_mode == "daemon"
|
|
assert config.default_workdir == ""
|
|
assert config.show_thinking is True
|
|
assert config.ui_backend == "tui"
|
|
assert config.log_level == "warning"
|
|
assert config.openrouter_anthropic_prompt_cache is True
|
|
assert config.openrouter_http_referer == (
|
|
"https://github.com/EvoScientist/EvoScientist"
|
|
)
|
|
assert config.openrouter_app_title == "EvoScientist"
|
|
assert config.openrouter_app_categories == "creative-writing,personal-agent"
|
|
assert config.memory_profile_enabled is True
|
|
assert config.memory_observations_enabled is True
|
|
assert config.memory_observation_writer == MemoryObservationWriter.ALL
|
|
assert config.memory_workers_enabled is True
|
|
assert config.memory_skill_synthesis_enabled is True
|
|
assert config.memory_skill_synthesis_mode == MemorySkillSynthesisMode.REVIEW
|
|
assert (
|
|
config.memory_skill_synthesis_cadence == MemorySkillSynthesisCadence.WEEKLY
|
|
)
|
|
assert config.memory_skill_synthesis_time == "03:00"
|
|
assert config.ollama_base_url == ""
|
|
assert config.channel_debug_tracing is False
|
|
assert config.imessage_enabled is False
|
|
assert config.imessage_allowed_senders == ""
|
|
assert config.repetitive_tool_call_threshold == 2
|
|
assert config.max_consecutive_tool_errors == 3
|
|
|
|
def test_auth_mode_default(self):
|
|
"""Test that anthropic_auth_mode defaults to api_key."""
|
|
config = EvoScientistConfig()
|
|
assert config.anthropic_auth_mode == "api_key"
|
|
|
|
def test_auth_mode_set(self):
|
|
"""Test that anthropic_auth_mode can be set."""
|
|
config = EvoScientistConfig(anthropic_auth_mode="oauth")
|
|
assert config.anthropic_auth_mode == "oauth"
|
|
|
|
def test_openai_auth_mode_default(self):
|
|
"""Test that openai_auth_mode defaults to api_key."""
|
|
config = EvoScientistConfig()
|
|
assert config.openai_auth_mode == "api_key"
|
|
|
|
def test_openai_auth_mode_set(self):
|
|
"""Test that openai_auth_mode can be set."""
|
|
config = EvoScientistConfig(openai_auth_mode="oauth")
|
|
assert config.openai_auth_mode == "oauth"
|
|
|
|
def test_custom_values(self):
|
|
"""Test that custom values can be set."""
|
|
config = EvoScientistConfig(
|
|
anthropic_api_key="sk-ant-test",
|
|
provider="openai",
|
|
model="gpt-4o",
|
|
default_mode="run",
|
|
)
|
|
|
|
assert config.anthropic_api_key == "sk-ant-test"
|
|
assert config.provider == "openai"
|
|
assert config.model == "gpt-4o"
|
|
assert config.default_mode == "run"
|
|
|
|
def test_dangerous_mode_default(self):
|
|
"""dangerous_mode defaults off and does not force auto_approve."""
|
|
config = EvoScientistConfig()
|
|
assert config.dangerous_mode is False
|
|
assert config.auto_approve is False
|
|
|
|
def test_dangerous_mode_implies_auto_approve(self):
|
|
"""Enabling dangerous_mode forces auto_approve via __post_init__."""
|
|
config = EvoScientistConfig(dangerous_mode=True)
|
|
assert config.dangerous_mode is True
|
|
assert config.auto_approve is True
|
|
|
|
@pytest.mark.parametrize(
|
|
"kwargs",
|
|
[
|
|
{"repetitive_tool_call_threshold": -1},
|
|
{"max_consecutive_tool_errors": -1},
|
|
{"max_consecutive_tool_errors": True},
|
|
],
|
|
)
|
|
def test_tool_guard_thresholds_must_be_non_negative_integers(self, kwargs):
|
|
with pytest.raises(ValueError, match="non-negative integer"):
|
|
EvoScientistConfig(**kwargs)
|
|
|
|
|
|
# =============================================================================
|
|
# Test config path functions
|
|
# =============================================================================
|
|
|
|
|
|
class TestConfigPaths:
|
|
def test_get_config_dir_with_explicit_override(self, monkeypatch, tmp_path):
|
|
"""An explicit config directory has the highest priority."""
|
|
config_dir = tmp_path / "gateway-config"
|
|
monkeypatch.setenv("EVOSCIENTIST_CONFIG_DIR", str(config_dir))
|
|
monkeypatch.setenv("EVOSCIENTIST_HOME", str(tmp_path / "runtime-home"))
|
|
|
|
assert get_config_dir() == config_dir.resolve()
|
|
|
|
def test_get_config_dir_with_evoscientist_home(self, monkeypatch, tmp_path):
|
|
"""Runtime home keeps configuration and data under one root."""
|
|
home = tmp_path / "runtime-home"
|
|
monkeypatch.delenv("EVOSCIENTIST_CONFIG_DIR", raising=False)
|
|
monkeypatch.setenv("EVOSCIENTIST_HOME", str(home))
|
|
|
|
assert get_config_dir() == home.resolve() / "config"
|
|
|
|
def test_get_config_dir_with_xdg(self, monkeypatch, tmp_path):
|
|
"""Test config dir uses XDG_CONFIG_HOME when set."""
|
|
monkeypatch.delenv("EVOSCIENTIST_CONFIG_DIR", raising=False)
|
|
monkeypatch.delenv("EVOSCIENTIST_HOME", raising=False)
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
config_dir = get_config_dir()
|
|
assert config_dir == tmp_path / "evoscientist"
|
|
|
|
def test_get_config_dir_default(self, monkeypatch):
|
|
"""Test config dir defaults to ~/.config/evoscientist."""
|
|
monkeypatch.delenv("EVOSCIENTIST_CONFIG_DIR", raising=False)
|
|
monkeypatch.delenv("EVOSCIENTIST_HOME", raising=False)
|
|
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
|
|
config_dir = get_config_dir()
|
|
assert config_dir == Path.home() / ".config" / "evoscientist"
|
|
|
|
def test_get_config_path(self, temp_config_dir):
|
|
"""Test config file path."""
|
|
config_path = get_config_path()
|
|
assert config_path == temp_config_dir / "config.yaml"
|
|
|
|
|
|
# =============================================================================
|
|
# Test load/save/reset
|
|
# =============================================================================
|
|
|
|
|
|
class TestLoadSaveReset:
|
|
def test_load_returns_defaults_when_no_file(self, temp_config_dir, clean_env):
|
|
"""Test that load returns defaults when config file doesn't exist."""
|
|
config = load_config()
|
|
assert config.provider == "anthropic"
|
|
assert config.model == "claude-sonnet-4-6"
|
|
|
|
def test_save_creates_file(self, temp_config_dir, clean_env):
|
|
"""Test that save creates the config file."""
|
|
config = EvoScientistConfig(provider="openai", model="gpt-4o")
|
|
save_config(config)
|
|
|
|
config_path = get_config_path()
|
|
assert config_path.exists()
|
|
|
|
with open(config_path) as f:
|
|
data = yaml.safe_load(f)
|
|
assert data["provider"] == "openai"
|
|
assert data["model"] == "gpt-4o"
|
|
|
|
def test_save_restricts_config_permissions(self, temp_config_dir, clean_env):
|
|
"""Config file permissions should not depend on the process umask."""
|
|
original_umask = os.umask(0)
|
|
try:
|
|
save_config(EvoScientistConfig(anthropic_api_key="test-key"))
|
|
finally:
|
|
os.umask(original_umask)
|
|
|
|
config_path = get_config_path()
|
|
if os.name == "nt":
|
|
assert config_path.exists()
|
|
# Windows reports pseudo-permission bits, so we don't test them here.
|
|
return
|
|
|
|
assert config_path.parent.stat().st_mode & 0o777 == 0o700
|
|
assert config_path.stat().st_mode & 0o777 == 0o600
|
|
|
|
def test_load_reads_saved_config(self, temp_config_dir, clean_env):
|
|
"""Test that load reads previously saved config."""
|
|
original = EvoScientistConfig(
|
|
anthropic_api_key="test-key",
|
|
provider="openai",
|
|
)
|
|
save_config(original)
|
|
|
|
loaded = load_config()
|
|
assert loaded.anthropic_api_key == "test-key"
|
|
assert loaded.provider == "openai"
|
|
|
|
def test_load_reads_manually_edited_utf8_config(self, temp_config_dir, clean_env):
|
|
"""Config loading handles localized content from manually edited YAML."""
|
|
config_path = get_config_path()
|
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
config_path.write_text(
|
|
"provider: openai\nmodel: gpt-4o\ndefault_workdir: /tmp/café\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
loaded = load_config()
|
|
|
|
assert loaded.provider == "openai"
|
|
assert loaded.model == "gpt-4o"
|
|
assert loaded.default_workdir == "/tmp/café"
|
|
|
|
def test_save_writes_utf8_config(self, temp_config_dir, clean_env):
|
|
"""Config saving writes localized content with UTF-8 encoding."""
|
|
save_config(EvoScientistConfig(default_workdir="/tmp/résumé"))
|
|
|
|
config_path = get_config_path()
|
|
raw = config_path.read_text(encoding="utf-8")
|
|
assert "default_workdir: /tmp/résumé" in raw
|
|
assert load_config().default_workdir == "/tmp/résumé"
|
|
|
|
def test_reset_deletes_config_file(self, temp_config_dir, clean_env):
|
|
"""Test that reset deletes the config file."""
|
|
config = EvoScientistConfig(provider="openai")
|
|
save_config(config)
|
|
|
|
config_path = get_config_path()
|
|
assert config_path.exists()
|
|
|
|
reset_config()
|
|
assert not config_path.exists()
|
|
|
|
def test_reset_no_file_is_safe(self, temp_config_dir, clean_env):
|
|
"""Test that reset is safe when no config file exists."""
|
|
reset_config() # Should not raise
|
|
|
|
def test_load_ignores_invalid_fields(self, temp_config_dir, clean_env):
|
|
"""Test that load ignores unknown fields in config file."""
|
|
config_path = get_config_path()
|
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(config_path, "w") as f:
|
|
yaml.safe_dump(
|
|
{
|
|
"provider": "openai",
|
|
"unknown_field": "should_be_ignored",
|
|
"another_bad": 123,
|
|
},
|
|
f,
|
|
)
|
|
|
|
config = load_config()
|
|
assert config.provider == "openai"
|
|
assert not hasattr(config, "unknown_field")
|
|
|
|
|
|
# =============================================================================
|
|
# Test get/set single values
|
|
# =============================================================================
|
|
|
|
|
|
class TestGetSetValues:
|
|
def test_get_config_value(self, temp_config_dir, clean_env):
|
|
"""Test getting a single config value."""
|
|
config = EvoScientistConfig(model="gpt-4o-mini")
|
|
save_config(config)
|
|
|
|
assert get_config_value("model") == "gpt-4o-mini"
|
|
|
|
def test_get_config_value_invalid_key(self, temp_config_dir, clean_env):
|
|
"""Test getting an invalid key returns None."""
|
|
assert get_config_value("nonexistent_key") is None
|
|
|
|
def test_set_config_value(self, temp_config_dir, clean_env):
|
|
"""Test setting a single config value."""
|
|
save_config(EvoScientistConfig())
|
|
|
|
result = set_config_value("model", "gpt-4o")
|
|
assert result is True
|
|
|
|
config = load_config()
|
|
assert config.model == "gpt-4o"
|
|
|
|
def test_set_config_value_invalid_key(self, temp_config_dir, clean_env):
|
|
"""Test setting an invalid key returns False."""
|
|
result = set_config_value("nonexistent_key", "value")
|
|
assert result is False
|
|
|
|
def test_set_config_value_type_coercion_bool(self, temp_config_dir, clean_env):
|
|
"""Test that boolean values are coerced correctly."""
|
|
save_config(EvoScientistConfig())
|
|
|
|
set_config_value("show_thinking", "false")
|
|
assert get_config_value("show_thinking") is False
|
|
|
|
set_config_value("show_thinking", "1")
|
|
assert get_config_value("show_thinking") is True
|
|
|
|
set_config_value("show_thinking", "yes")
|
|
assert get_config_value("show_thinking") is True
|
|
|
|
def test_set_imessage_enabled_coercion(self, temp_config_dir, clean_env):
|
|
"""Test that imessage_enabled is coerced from string to bool."""
|
|
save_config(EvoScientistConfig())
|
|
|
|
set_config_value("imessage_enabled", "true")
|
|
assert get_config_value("imessage_enabled") is True
|
|
|
|
set_config_value("imessage_enabled", "false")
|
|
assert get_config_value("imessage_enabled") is False
|
|
|
|
def test_set_imessage_allowed_senders(self, temp_config_dir, clean_env):
|
|
"""Test that imessage_allowed_senders stores comma-separated string."""
|
|
save_config(EvoScientistConfig())
|
|
|
|
set_config_value("imessage_allowed_senders", "+1234567890,+0987654321")
|
|
assert get_config_value("imessage_allowed_senders") == "+1234567890,+0987654321"
|
|
|
|
def test_set_channel_debug_tracing_coercion(self, temp_config_dir, clean_env):
|
|
"""Test that channel_debug_tracing is coerced from string to bool."""
|
|
save_config(EvoScientistConfig())
|
|
|
|
set_config_value("channel_debug_tracing", "true")
|
|
assert get_config_value("channel_debug_tracing") is True
|
|
|
|
set_config_value("channel_debug_tracing", "false")
|
|
assert get_config_value("channel_debug_tracing") is False
|
|
|
|
def test_set_memory_observation_writer_validates_value(
|
|
self, temp_config_dir, clean_env
|
|
):
|
|
"""Observation writer mode accepts only the supported product controls."""
|
|
save_config(
|
|
EvoScientistConfig(memory_observation_writer=MemoryObservationWriter.ALL)
|
|
)
|
|
|
|
assert set_config_value("memory_observation_writer", "worker") is True
|
|
assert get_config_value("memory_observation_writer") == "worker"
|
|
assert set_config_value("memory_observation_writer", "AGENT") is True
|
|
assert get_config_value("memory_observation_writer") == "agent"
|
|
assert set_config_value("memory_observation_writer", "subagent") is False
|
|
assert get_config_value("memory_observation_writer") == "agent"
|
|
|
|
def test_memory_controls_observation_writer_targets(self):
|
|
"""MemoryControls centralizes observation writer target semantics."""
|
|
worker_controls = MemoryControls.from_config(
|
|
EvoScientistConfig(
|
|
memory_profile_enabled=False,
|
|
memory_observations_enabled=True,
|
|
memory_observation_writer=MemoryObservationWriter.WORKER,
|
|
memory_workers_enabled=True,
|
|
)
|
|
)
|
|
all_controls = MemoryControls.from_config(
|
|
EvoScientistConfig(
|
|
memory_profile_enabled=False,
|
|
memory_observations_enabled=True,
|
|
memory_observation_writer=MemoryObservationWriter.ALL,
|
|
memory_workers_enabled=True,
|
|
)
|
|
)
|
|
|
|
assert worker_controls.observation_tool_enabled(
|
|
MemoryObservationTarget.TURN_WORKER
|
|
)
|
|
assert worker_controls.worker_needed(MemoryObservationTarget.TURN_WORKER)
|
|
assert worker_controls.observation_tool_enabled(
|
|
MemoryObservationTarget.SUBAGENT_WORKER
|
|
)
|
|
assert not worker_controls.observation_tool_enabled(
|
|
MemoryObservationTarget.AGENT
|
|
)
|
|
assert all_controls.worker_needed(MemoryObservationTarget.TURN_WORKER)
|
|
assert all_controls.observation_tool_enabled(MemoryObservationTarget.AGENT)
|
|
assert all_controls.observation_tool_enabled(
|
|
MemoryObservationTarget.TURN_WORKER
|
|
)
|
|
assert all_controls.observation_tool_enabled(
|
|
MemoryObservationTarget.SUBAGENT_WORKER
|
|
)
|
|
|
|
def test_set_memory_skill_synthesis_values_validate(
|
|
self, temp_config_dir, clean_env
|
|
):
|
|
save_config(EvoScientistConfig())
|
|
|
|
assert set_config_value("memory_skill_synthesis_mode", "auto") is True
|
|
assert get_config_value("memory_skill_synthesis_mode") == "auto"
|
|
assert set_config_value("memory_skill_synthesis_mode", "always") is False
|
|
assert get_config_value("memory_skill_synthesis_mode") == "auto"
|
|
|
|
assert set_config_value("memory_skill_synthesis_cadence", "nightly") is True
|
|
assert get_config_value("memory_skill_synthesis_cadence") == "nightly"
|
|
assert set_config_value("memory_skill_synthesis_cadence", "hourly") is False
|
|
assert get_config_value("memory_skill_synthesis_cadence") == "nightly"
|
|
|
|
assert set_config_value("memory_skill_synthesis_time", "3:05") is True
|
|
assert get_config_value("memory_skill_synthesis_time") == "03:05"
|
|
assert set_config_value("memory_skill_synthesis_time", "24:00") is False
|
|
assert get_config_value("memory_skill_synthesis_time") == "03:05"
|
|
|
|
loaded = load_config()
|
|
assert loaded.memory_skill_synthesis_mode == MemorySkillSynthesisMode.AUTO
|
|
assert (
|
|
loaded.memory_skill_synthesis_cadence == MemorySkillSynthesisCadence.NIGHTLY
|
|
)
|
|
|
|
def test_list_config(self, temp_config_dir, clean_env):
|
|
"""Test listing all config values."""
|
|
config = EvoScientistConfig(provider="openai", model="gpt-4o")
|
|
save_config(config)
|
|
|
|
all_config = list_config()
|
|
assert isinstance(all_config, dict)
|
|
assert all_config["provider"] == "openai"
|
|
assert all_config["model"] == "gpt-4o"
|
|
|
|
|
|
# =============================================================================
|
|
# Test priority chain (CLI > env > file > default)
|
|
# =============================================================================
|
|
|
|
|
|
class TestPriorityChain:
|
|
def test_defaults_when_nothing_set(self, temp_config_dir, clean_env, monkeypatch):
|
|
"""Test defaults are used when nothing is configured."""
|
|
# Ensure no env vars affect the test
|
|
for key in [
|
|
"ANTHROPIC_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"TAVILY_API_KEY",
|
|
"EVOSCIENTIST_DEFAULT_MODE",
|
|
"EVOSCIENTIST_WORKSPACE_DIR",
|
|
"EVOSCIENTIST_UI_BACKEND",
|
|
]:
|
|
monkeypatch.delenv(key, raising=False)
|
|
config = get_effective_config()
|
|
assert config.provider == "anthropic"
|
|
assert config.default_mode == "daemon"
|
|
|
|
def test_file_overrides_defaults(self, temp_config_dir, clean_env):
|
|
"""Test file config overrides defaults."""
|
|
save_config(EvoScientistConfig(provider="openai"))
|
|
|
|
config = get_effective_config()
|
|
assert config.provider == "openai"
|
|
|
|
def test_env_overrides_file(self, temp_config_dir, monkeypatch):
|
|
"""Test environment variables override file config."""
|
|
save_config(EvoScientistConfig(default_mode="run"))
|
|
monkeypatch.setenv("EVOSCIENTIST_DEFAULT_MODE", "daemon")
|
|
|
|
config = get_effective_config()
|
|
assert config.default_mode == "daemon"
|
|
|
|
def test_cli_overrides_env(self, temp_config_dir, monkeypatch):
|
|
"""Test CLI arguments override environment variables."""
|
|
monkeypatch.setenv("EVOSCIENTIST_DEFAULT_MODE", "daemon")
|
|
|
|
config = get_effective_config(cli_overrides={"default_mode": "run"})
|
|
assert config.default_mode == "run"
|
|
|
|
def test_cli_overrides_file(self, temp_config_dir, clean_env):
|
|
"""Test CLI arguments override file config."""
|
|
save_config(EvoScientistConfig(model="gpt-4o"))
|
|
|
|
config = get_effective_config(cli_overrides={"model": "claude-opus-4-8"})
|
|
assert config.model == "claude-opus-4-8"
|
|
|
|
def test_env_ui_backend_override(self, temp_config_dir, monkeypatch):
|
|
"""UI backend can be selected via environment variable."""
|
|
save_config(EvoScientistConfig(ui_backend="cli"))
|
|
monkeypatch.setenv("EVOSCIENTIST_UI_BACKEND", "tui")
|
|
config = get_effective_config()
|
|
assert config.ui_backend == "tui"
|
|
|
|
def test_env_log_level_override(self, temp_config_dir, monkeypatch):
|
|
"""Log level can be selected via environment variable."""
|
|
save_config(EvoScientistConfig(log_level="warning"))
|
|
monkeypatch.setenv("EVOSCIENTIST_LOG_LEVEL", "DEBUG")
|
|
config = get_effective_config()
|
|
assert config.log_level == "DEBUG"
|
|
|
|
def test_env_reasoning_effort_is_not_a_config_override(self, temp_config_dir, monkeypatch):
|
|
"""Reasoning is selected by the invocation plan, never deployment env."""
|
|
save_config(EvoScientistConfig())
|
|
monkeypatch.setenv("EVOSCIENTIST_REASONING_EFFORT", "high")
|
|
config = get_effective_config()
|
|
assert not hasattr(config, "reasoning_effort")
|
|
|
|
def test_env_channel_debug_tracing_override(self, temp_config_dir, monkeypatch):
|
|
"""Channel tracing can be enabled via environment variable."""
|
|
save_config(EvoScientistConfig(channel_debug_tracing=False))
|
|
monkeypatch.setenv("EVOSCIENTIST_CHANNEL_DEBUG_TRACING", "true")
|
|
config = get_effective_config()
|
|
assert config.channel_debug_tracing is True
|
|
|
|
def test_env_memory_controls_override(self, temp_config_dir, monkeypatch):
|
|
"""Memory controls can be selected via environment variables."""
|
|
save_config(
|
|
EvoScientistConfig(
|
|
memory_profile_enabled=True,
|
|
memory_observations_enabled=True,
|
|
memory_observation_writer=MemoryObservationWriter.ALL,
|
|
memory_workers_enabled=True,
|
|
)
|
|
)
|
|
monkeypatch.setenv("EVOSCIENTIST_MEMORY_PROFILE_ENABLED", "false")
|
|
monkeypatch.setenv("EVOSCIENTIST_MEMORY_OBSERVATIONS_ENABLED", "false")
|
|
monkeypatch.setenv("EVOSCIENTIST_MEMORY_OBSERVATION_WRITER", "worker")
|
|
monkeypatch.setenv("EVOSCIENTIST_MEMORY_WORKERS_ENABLED", "false")
|
|
|
|
config = get_effective_config()
|
|
assert config.memory_profile_enabled is False
|
|
assert config.memory_observations_enabled is False
|
|
assert config.memory_observation_writer == MemoryObservationWriter.WORKER
|
|
assert config.memory_workers_enabled is False
|
|
|
|
def test_sandbox_execute_timeout_default(self, temp_config_dir, clean_env):
|
|
"""Sandbox execute timeout defaults to 300 seconds."""
|
|
assert EvoScientistConfig().sandbox_execute_timeout == 300
|
|
assert get_effective_config().sandbox_execute_timeout == 300
|
|
|
|
def test_env_sandbox_execute_timeout_override(self, temp_config_dir, monkeypatch):
|
|
"""Sandbox execute timeout can be set via env var and coerces to int."""
|
|
monkeypatch.setenv("EVOSCIENTIST_SANDBOX_EXECUTE_TIMEOUT", "600")
|
|
config = get_effective_config()
|
|
assert config.sandbox_execute_timeout == 600
|
|
assert isinstance(config.sandbox_execute_timeout, int)
|
|
|
|
def test_sandbox_execute_timeout_invalid_falls_back(self):
|
|
"""Non-positive / non-int values fall back to the default (would
|
|
otherwise crash CustomSandboxBackend construction at startup)."""
|
|
assert (
|
|
EvoScientistConfig(sandbox_execute_timeout=0).sandbox_execute_timeout == 300
|
|
)
|
|
assert (
|
|
EvoScientistConfig(sandbox_execute_timeout=-5).sandbox_execute_timeout
|
|
== 300
|
|
)
|
|
assert (
|
|
EvoScientistConfig(sandbox_execute_timeout="abc").sandbox_execute_timeout
|
|
== 300
|
|
)
|
|
assert (
|
|
EvoScientistConfig(sandbox_execute_timeout=True).sandbox_execute_timeout
|
|
== 300
|
|
)
|
|
|
|
def test_set_sandbox_execute_timeout_rejects_invalid(
|
|
self, temp_config_dir, clean_env
|
|
):
|
|
"""set_config_value must reject (not silently persist) a non-positive timeout."""
|
|
save_config(EvoScientistConfig(sandbox_execute_timeout=120))
|
|
assert set_config_value("sandbox_execute_timeout", 0) is False
|
|
assert set_config_value("sandbox_execute_timeout", -5) is False
|
|
# bool is an int subclass; reject it before coercion turns True into 1.
|
|
assert set_config_value("sandbox_execute_timeout", True) is False
|
|
# The earlier valid value is untouched on disk.
|
|
assert get_config_value("sandbox_execute_timeout") == 120
|
|
# A valid value still goes through.
|
|
assert set_config_value("sandbox_execute_timeout", 600) is True
|
|
assert get_config_value("sandbox_execute_timeout") == 600
|
|
|
|
def test_env_api_key_override(self, temp_config_dir, monkeypatch):
|
|
"""Test API keys from env override file."""
|
|
save_config(EvoScientistConfig(anthropic_api_key="file-key"))
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "env-key")
|
|
|
|
config = get_effective_config()
|
|
assert config.anthropic_api_key == "env-key"
|
|
|
|
def test_env_auth_mode_override(self, temp_config_dir, monkeypatch):
|
|
"""Test auth mode from env overrides file."""
|
|
save_config(EvoScientistConfig(anthropic_auth_mode="api_key"))
|
|
monkeypatch.setenv("EVOSCIENTIST_ANTHROPIC_AUTH_MODE", "oauth")
|
|
|
|
config = get_effective_config()
|
|
assert config.anthropic_auth_mode == "oauth"
|
|
|
|
def test_env_openai_auth_mode_override(self, temp_config_dir, monkeypatch):
|
|
"""Test openai_auth_mode from env overrides file."""
|
|
save_config(EvoScientistConfig(openai_auth_mode="api_key"))
|
|
monkeypatch.setenv("EVOSCIENTIST_OPENAI_AUTH_MODE", "oauth")
|
|
|
|
config = get_effective_config()
|
|
assert config.openai_auth_mode == "oauth"
|
|
|
|
def test_env_openrouter_anthropic_prompt_cache_opt_out(
|
|
self, temp_config_dir, monkeypatch
|
|
):
|
|
"""Test OpenRouter Anthropic prompt cache opt-out env overrides file."""
|
|
save_config(EvoScientistConfig(openrouter_anthropic_prompt_cache=True))
|
|
monkeypatch.setenv("EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE", "false")
|
|
|
|
config = get_effective_config()
|
|
assert config.openrouter_anthropic_prompt_cache is False
|
|
|
|
def test_env_openrouter_app_attribution_override(
|
|
self, temp_config_dir, monkeypatch
|
|
):
|
|
"""OpenRouter app-attribution env vars should override file config."""
|
|
save_config(EvoScientistConfig())
|
|
monkeypatch.setenv("EVOSCIENTIST_OPENROUTER_HTTP_REFERER", "https://acme.test")
|
|
monkeypatch.setenv("EVOSCIENTIST_OPENROUTER_APP_TITLE", "Acme")
|
|
monkeypatch.setenv(
|
|
"EVOSCIENTIST_OPENROUTER_APP_CATEGORIES", "cli-agent,programming-app"
|
|
)
|
|
|
|
config = get_effective_config()
|
|
assert config.openrouter_http_referer == "https://acme.test"
|
|
assert config.openrouter_app_title == "Acme"
|
|
assert config.openrouter_app_categories == "cli-agent,programming-app"
|
|
|
|
def test_set_openrouter_anthropic_prompt_cache(self, temp_config_dir, clean_env):
|
|
"""Test OpenRouter Anthropic prompt cache can be set through config."""
|
|
save_config(EvoScientistConfig())
|
|
|
|
assert set_config_value("openrouter_anthropic_prompt_cache", "false") is True
|
|
assert get_config_value("openrouter_anthropic_prompt_cache") is False
|
|
|
|
|
|
# =============================================================================
|
|
# Test apply_config_to_env
|
|
# =============================================================================
|
|
|
|
|
|
class TestApplyConfigToEnv:
|
|
def test_applies_api_keys_when_not_set(self, clean_env):
|
|
"""Test that API keys are applied to env when not already set."""
|
|
config = EvoScientistConfig(
|
|
anthropic_api_key="config-ant-key",
|
|
openai_api_key="config-oai-key",
|
|
tavily_api_key="config-tav-key",
|
|
semantic_scholar_api_key="config-s2-key",
|
|
)
|
|
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("ANTHROPIC_API_KEY") == "config-ant-key"
|
|
assert os.environ.get("OPENAI_API_KEY") == "config-oai-key"
|
|
assert os.environ.get("TAVILY_API_KEY") == "config-tav-key"
|
|
assert os.environ.get("S2_API_KEY") == "config-s2-key"
|
|
|
|
def test_does_not_override_existing_env(self, monkeypatch):
|
|
"""Test that existing env vars are not overridden."""
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "existing-key")
|
|
|
|
config = EvoScientistConfig(anthropic_api_key="config-key")
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("ANTHROPIC_API_KEY") == "existing-key"
|
|
|
|
def test_empty_config_keys_not_applied(self, clean_env):
|
|
"""Test that empty config keys don't create env vars."""
|
|
config = EvoScientistConfig() # All empty
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("ANTHROPIC_API_KEY") is None
|
|
assert os.environ.get("OPENAI_API_KEY") is None
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE") is None
|
|
|
|
def test_openrouter_anthropic_prompt_cache_opt_out_applied(
|
|
self, clean_env, monkeypatch
|
|
):
|
|
"""Test OpenRouter Anthropic prompt cache opt-out config is applied to env."""
|
|
monkeypatch.delenv(
|
|
"EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE", raising=False
|
|
)
|
|
config = EvoScientistConfig(openrouter_anthropic_prompt_cache=False)
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE") == (
|
|
"false"
|
|
)
|
|
|
|
def test_openrouter_app_attribution_applied_to_env(self, clean_env, monkeypatch):
|
|
"""Config app-attribution values are exported to env for models.py."""
|
|
for env in (
|
|
"EVOSCIENTIST_OPENROUTER_HTTP_REFERER",
|
|
"EVOSCIENTIST_OPENROUTER_APP_TITLE",
|
|
"EVOSCIENTIST_OPENROUTER_APP_CATEGORIES",
|
|
):
|
|
monkeypatch.delenv(env, raising=False)
|
|
config = EvoScientistConfig(
|
|
openrouter_http_referer="https://acme.test",
|
|
openrouter_app_title="Acme",
|
|
openrouter_app_categories="cli-agent,programming-app",
|
|
)
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_HTTP_REFERER") == (
|
|
"https://acme.test"
|
|
)
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_APP_TITLE") == "Acme"
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_APP_CATEGORIES") == (
|
|
"cli-agent,programming-app"
|
|
)
|
|
|
|
def test_openrouter_app_attribution_env_not_overwritten(
|
|
self, clean_env, monkeypatch
|
|
):
|
|
"""apply_config_to_env must not clobber an already-set attribution env var."""
|
|
monkeypatch.setenv("EVOSCIENTIST_OPENROUTER_APP_TITLE", "existing-title")
|
|
config = EvoScientistConfig(openrouter_app_title="config-title")
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_APP_TITLE") == "existing-title"
|
|
|
|
def test_openrouter_app_attribution_empty_config_not_applied(
|
|
self, clean_env, monkeypatch
|
|
):
|
|
"""Empty-string attribution config must not create env vars."""
|
|
for env in (
|
|
"EVOSCIENTIST_OPENROUTER_HTTP_REFERER",
|
|
"EVOSCIENTIST_OPENROUTER_APP_TITLE",
|
|
"EVOSCIENTIST_OPENROUTER_APP_CATEGORIES",
|
|
):
|
|
monkeypatch.delenv(env, raising=False)
|
|
config = EvoScientistConfig(
|
|
openrouter_http_referer="",
|
|
openrouter_app_title="",
|
|
openrouter_app_categories="",
|
|
)
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_HTTP_REFERER") is None
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_APP_TITLE") is None
|
|
assert os.environ.get("EVOSCIENTIST_OPENROUTER_APP_CATEGORIES") is None
|
|
|
|
def test_dangerous_mode_round_trips_to_env(self, clean_env, monkeypatch):
|
|
"""dangerous_mode set via CLI override must survive a fresh re-read.
|
|
|
|
Regression: a --dangerous CLI flag is never persisted to file/env, so a
|
|
fresh get_effective_config() (warning banner, run_in_background, the
|
|
langgraph dev subprocess) would read it back as False while the backend
|
|
is already unconfined. apply_config_to_env round-trips it to env.
|
|
"""
|
|
from EvoScientist.config import get_effective_config
|
|
|
|
monkeypatch.delenv("EVOSCIENTIST_DANGEROUS_MODE", raising=False)
|
|
cfg = get_effective_config({"dangerous_mode": True})
|
|
assert cfg.dangerous_mode is True
|
|
|
|
apply_config_to_env(cfg)
|
|
assert os.environ.get("EVOSCIENTIST_DANGEROUS_MODE") == "true"
|
|
# The fresh, no-override read now agrees with the backend.
|
|
assert get_effective_config().dangerous_mode is True
|
|
|
|
def test_dangerous_mode_not_applied_when_off(self, clean_env, monkeypatch):
|
|
"""dangerous_mode=False must not write the env var."""
|
|
monkeypatch.delenv("EVOSCIENTIST_DANGEROUS_MODE", raising=False)
|
|
apply_config_to_env(EvoScientistConfig())
|
|
assert os.environ.get("EVOSCIENTIST_DANGEROUS_MODE") is None
|
|
|
|
def test_dangerous_mode_off_clears_stale_env(self, clean_env, monkeypatch):
|
|
"""Re-applying a non-dangerous config clears a previously-set env var.
|
|
|
|
Regression: the round-trip used to be set-only, so once dangerous mode
|
|
was applied in a process it could never be lowered — leaking unconfined
|
|
access into later non-dangerous reads.
|
|
"""
|
|
monkeypatch.delenv("EVOSCIENTIST_DANGEROUS_MODE", raising=False)
|
|
apply_config_to_env(EvoScientistConfig(dangerous_mode=True))
|
|
assert os.environ.get("EVOSCIENTIST_DANGEROUS_MODE") == "true"
|
|
apply_config_to_env(EvoScientistConfig()) # dangerous off
|
|
assert os.environ.get("EVOSCIENTIST_DANGEROUS_MODE") is None
|
|
|
|
def test_ollama_base_url_applied(self, clean_env, monkeypatch):
|
|
"""Test that ollama_base_url is applied to OLLAMA_BASE_URL env var."""
|
|
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
|
|
config = EvoScientistConfig(ollama_base_url="http://localhost:11434")
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("OLLAMA_BASE_URL") == "http://localhost:11434"
|
|
|
|
def test_ollama_base_url_not_overridden(self, monkeypatch):
|
|
"""Test that existing OLLAMA_BASE_URL env var is not overridden."""
|
|
monkeypatch.setenv("OLLAMA_BASE_URL", "http://existing:11434")
|
|
config = EvoScientistConfig(ollama_base_url="http://new:11434")
|
|
apply_config_to_env(config)
|
|
|
|
assert os.environ.get("OLLAMA_BASE_URL") == "http://existing:11434"
|
|
|
|
|
|
class TestAuxiliaryModelConfig:
|
|
"""auxiliary_model / auxiliary_provider config fields (plain str, optional)."""
|
|
|
|
def test_defaults_empty(self):
|
|
cfg = EvoScientistConfig()
|
|
assert cfg.auxiliary_model == ""
|
|
assert cfg.auxiliary_provider == ""
|
|
|
|
def test_save_and_load_round_trip(self, temp_config_dir, clean_env):
|
|
save_config(
|
|
EvoScientistConfig(
|
|
auxiliary_model="claude-haiku-4-5",
|
|
auxiliary_provider="anthropic",
|
|
)
|
|
)
|
|
loaded = load_config()
|
|
assert loaded.auxiliary_model == "claude-haiku-4-5"
|
|
assert loaded.auxiliary_provider == "anthropic"
|
|
|
|
def test_get_set_value(self, temp_config_dir, clean_env):
|
|
save_config(EvoScientistConfig())
|
|
assert set_config_value("auxiliary_model", "qwen3.6-flash") is True
|
|
assert set_config_value("auxiliary_provider", "dashscope") is True
|
|
assert get_config_value("auxiliary_model") == "qwen3.6-flash"
|
|
assert get_config_value("auxiliary_provider") == "dashscope"
|
|
|
|
def test_env_overrides_file(self, temp_config_dir, monkeypatch):
|
|
save_config(EvoScientistConfig(auxiliary_model="claude-haiku-4-5"))
|
|
monkeypatch.setenv("EVOSCIENTIST_AUXILIARY_MODEL", "gpt-5.5")
|
|
monkeypatch.setenv("EVOSCIENTIST_AUXILIARY_PROVIDER", "openai")
|
|
|
|
config = get_effective_config()
|
|
assert config.auxiliary_model == "gpt-5.5"
|
|
assert config.auxiliary_provider == "openai"
|
|
|
|
|
|
def test_scheduler_config_defaults_and_env(monkeypatch):
|
|
from EvoScientist.config.settings import EvoScientistConfig, get_effective_config
|
|
|
|
c = EvoScientistConfig()
|
|
assert c.enable_scheduler is True
|
|
assert c.scheduler_default_timezone == ""
|
|
assert c.memory_skill_synthesis_enabled is True
|
|
assert c.memory_skill_synthesis_mode == MemorySkillSynthesisMode.REVIEW
|
|
assert c.memory_skill_synthesis_cadence == MemorySkillSynthesisCadence.WEEKLY
|
|
assert c.memory_skill_synthesis_time == "03:00"
|
|
|
|
monkeypatch.setenv("EVOSCIENTIST_ENABLE_SCHEDULER", "false")
|
|
eff = get_effective_config({})
|
|
assert eff.enable_scheduler is False
|
|
|
|
monkeypatch.setenv("EVOSCIENTIST_SCHEDULER_DEFAULT_TIMEZONE", "America/New_York")
|
|
monkeypatch.setenv("EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_ENABLED", "false")
|
|
monkeypatch.setenv("EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_MODE", "auto")
|
|
monkeypatch.setenv("EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_CADENCE", "monthly")
|
|
monkeypatch.setenv("EVOSCIENTIST_MEMORY_SKILL_SYNTHESIS_TIME", "4:30")
|
|
eff2 = get_effective_config({})
|
|
assert eff2.scheduler_default_timezone == "America/New_York"
|
|
assert eff2.memory_skill_synthesis_enabled is False
|
|
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
|