feat(paths): remove SLOP_CONFIG env-var fallback; add --config CLI flag (FR2)

This commit is contained in:
ed
2026-06-19 07:45:10 -04:00
parent 49bc4908e6
commit 02fef00470
5 changed files with 136 additions and 22 deletions
+61 -17
View File
@@ -17,6 +17,25 @@ if project_root not in sys.path:
_RUN_ID = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
_RUN_WORKSPACE = Path(f"tests/artifacts/live_gui_workspace_{_RUN_ID}")
_ISOLATION_WORKSPACE = Path(f"tests/artifacts/_isolation_workspace_{_RUN_ID}")
_ISOLATION_WORKSPACE.mkdir(parents=True, exist_ok=True)
def _parse_config_arg(argv: list[str]) -> Path | None:
for i in range(1, len(argv)):
arg = argv[i]
if arg == "--config" and i + 1 < len(argv):
return Path(argv[i + 1]).resolve()
if arg.startswith("--config="):
return Path(arg.split("=", 1)[1]).resolve()
return None
_config_override_arg = _parse_config_arg(sys.argv)
if _config_override_arg is None:
_config_override_arg = _ISOLATION_WORKSPACE / "config_overrides.toml"
from src import paths as _paths # noqa: E402
_paths.set_config_override(_config_override_arg)
thirdparty_dir = os.path.join(os.path.dirname(__file__), "..", "thirdparty")
if thirdparty_dir not in sys.path:
sys.path.insert(0, thirdparty_dir)
@@ -178,6 +197,17 @@ if not _warmup_app_controller.wait_for_warmup(timeout=60.0):
import threading
_pytest_finished_event: threading.Event = threading.Event()
def pytest_addoption(parser) -> None:
"""
Register the --config flag so pytest does not warn about an unknown
option. Parsing happens in module body BEFORE any src/ import (see
_parse_config_arg above).
[C: tests/conftest.py:_parse_config_arg]
"""
parser.addoption("--config", action="store", default=None,
help="Manual Slop: override config.toml path for tests")
def pytest_configure(config: object) -> None:
"""
Pytest session-start hook. Installs the runtime sandbox audit hook
@@ -335,28 +365,40 @@ def _enforce_test_sandbox() -> Generator[None, None, None]:
yield
@pytest.fixture(autouse=True)
def isolate_workspace(tmp_path_factory, monkeypatch) -> Generator[None, None, None]:
def isolate_workspace(monkeypatch) -> Generator[None, None, None]:
"""
Autouse fixture to isolate tests from the active user workspace.
Protects the real config.toml and manual_slop.toml from being overwritten.
Workspace lives under tests/artifacts/_isolation_workspace_<RUN_ID>/ per
conductor/code_styleguides/workspace_paths.md (replaces the historical
tmp_path_factory.mktemp pattern). Writes placeholder TOMLs so src/ code
that reads these files at startup does not crash.
[C: tests/conftest.py:_ISOLATION_WORKSPACE, src/paths.py:set_config_override]
"""
test_workspace = tmp_path_factory.mktemp("isolated_workspace")
config_path = test_workspace / "config.toml"
import tomli_w
with open(config_path, "wb") as f:
tomli_w.dump({
'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'},
'projects': {'paths': [], 'active': ''},
'gui': {'show_windows': {}}
}, f)
test_workspace = _ISOLATION_WORKSPACE
config_path = test_workspace / "config_overrides.toml"
if not config_path.exists():
import tomli_w
with open(config_path, "wb") as f:
tomli_w.dump({
'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'},
'projects': {'paths': [], 'active': ''},
'gui': {'show_windows': {}}
}, f)
for name in (
"presets.toml", "tool_presets.toml", "personas.toml",
"workspace_profiles.toml", "credentials.toml", "mcp_env.toml",
):
(test_workspace / name).touch()
monkeypatch.setenv("SLOP_CONFIG", str(config_path))
monkeypatch.setenv("SLOP_GLOBAL_PRESETS", str(test_workspace / "presets.toml"))
monkeypatch.setenv("SLOP_GLOBAL_TOOL_PRESETS", str(test_workspace / "tool_presets.toml"))
monkeypatch.setenv("SLOP_GLOBAL_PERSONAS", str(test_workspace / "personas.toml"))
monkeypatch.setenv("SLOP_GLOBAL_WORKSPACE_PROFILES", str(test_workspace / "workspace_profiles.toml"))
monkeypatch.setenv("SLOP_CREDENTIALS", str(test_workspace / "credentials.toml"))
monkeypatch.setenv("SLOP_MCP_ENV", str(test_workspace / "mcp_env.toml"))
yield
@pytest.fixture(autouse=True)
@@ -691,8 +733,6 @@ def live_gui(request) -> Generator["_LiveGuiHandle", None, None]:
# or just run from that CWD.
env = os.environ.copy()
env["PYTHONPATH"] = str(project_root.absolute())
if config_file.exists():
env["SLOP_CONFIG"] = str(config_file.absolute())
if cred_file.exists():
env["SLOP_CREDENTIALS"] = str(cred_file.absolute())
if mcp_file.exists():
@@ -700,8 +740,12 @@ def live_gui(request) -> Generator["_LiveGuiHandle", None, None]:
env["SLOP_GLOBAL_PRESETS"] = str((temp_workspace / "presets.toml").absolute())
env["SLOP_GLOBAL_TOOL_PRESETS"] = str((temp_workspace / "tool_presets.toml").absolute())
gui_args = ["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"]
if config_file.exists():
gui_args.append(f"--config={config_file.absolute()}")
_process = subprocess.Popen(
["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"],
gui_args,
stdout=log_file,
stderr=log_file,
text=True,
+45 -1
View File
@@ -147,4 +147,48 @@ def test_sandbox_allows_pytest_cache_write() -> None:
probe.write_text("ok", encoding="utf-8")
assert probe.read_text(encoding="utf-8") == "ok"
finally:
probe.unlink(missing_ok=True)
probe.unlink(missing_ok=True)
def test_config_override_via_cli_flag(tmp_path) -> None:
"""paths.set_config_override(path) makes get_config_path() return that path."""
from src import paths
config_path = tmp_path / "my_config.toml"
config_path.write_text("[ai]\nprovider='gemini'\n", encoding="utf-8")
original = paths._CONFIG_OVERRIDE
try:
paths.set_config_override(config_path)
assert paths.get_config_path() == config_path
finally:
paths.set_config_override(original)
def test_paths_get_config_path_no_env_fallback(monkeypatch) -> None:
"""Without an override AND without SLOP_CONFIG, get_config_path returns default."""
monkeypatch.delenv("SLOP_CONFIG", raising=False)
from src import paths
original = paths._CONFIG_OVERRIDE
try:
paths.set_config_override(None)
expected = Path(__file__).resolve().parent.parent / "config.toml"
assert paths.get_config_path() == expected
finally:
paths.set_config_override(original)
def test_sloppy_py_parses_config_flag() -> None:
"""sloppy.py has a --config argparse argument that calls set_config_override."""
import ast
sloppy = Path(__file__).resolve().parent.parent / "sloppy.py"
tree = ast.parse(sloppy.read_text(encoding="utf-8"))
found_config_arg = False
found_set_override_call = False
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and node.value == "--config":
found_config_arg = True
if isinstance(node, ast.Call):
func = node.func
if isinstance(func, ast.Name) and func.id == "set_config_override":
found_set_override_call = True
assert found_config_arg, "sloppy.py must have a --config argparse argument"
assert found_set_override_call, "sloppy.py must call paths.set_config_override(args.config)"