Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,57 @@ def _isolate_hermes_home(_hermetic_environment):
return None


_REPO_ROOT_CLI_CONFIG = Path(__file__).resolve().parent.parent / "cli-config.yaml"


@pytest.fixture(autouse=True)
def _guard_repo_root_cli_config():
"""Fail any test that creates or mutates ``<repo>/cli-config.yaml``.

``cli.save_config_value()`` falls back to writing the PROJECT config —
``cli-config.yaml`` next to ``cli.py``, i.e. the repo root — whenever
the user config (``cli._hermes_home / 'config.yaml'``) does not exist.
Under ``_hermetic_environment`` the per-test HERMES_HOME tempdir is
always empty, so any test that reaches the real ``save_config_value``
silently drops a ``cli-config.yaml`` into the developer's checkout.
The file is gitignored (invisible in ``git status``) and is then read
back as the project-config fallback by every later run, breaking e.g.
``tests/hermes_cli/test_ignore_user_config_flags.py`` and
``tests/test_hermes_state.py``.

This tripwire pins the blame on the offending test instead of letting
the pollution surface as unrelated failures three runs later. It also
restores the pre-test state so one offender can't cascade.
"""
before = (
_REPO_ROOT_CLI_CONFIG.read_bytes()
if _REPO_ROOT_CLI_CONFIG.exists()
else None
)
yield
after = (
_REPO_ROOT_CLI_CONFIG.read_bytes()
if _REPO_ROOT_CLI_CONFIG.exists()
else None
)
if after == before:
return
# Restore first so the pollution doesn't outlive the failing test.
if before is None:
_REPO_ROOT_CLI_CONFIG.unlink()
else:
_REPO_ROOT_CLI_CONFIG.write_bytes(before)
pytest.fail(
"this test wrote to the repo-root cli-config.yaml "
f"({_REPO_ROOT_CLI_CONFIG}). It reached the real "
"cli.save_config_value() with an empty HERMES_HOME, so the write "
"fell through to the project-config fallback in the developer's "
"checkout. Point cli._hermes_home at a tmp_path containing a "
"config.yaml (or monkeypatch cli.save_config_value) instead. "
f"Written content was:\n{(after or b'').decode('utf-8', 'replace')}"
)


# ── Module-level state reset — replaced by per-file process isolation ──────
#
# Each test FILE runs in a freshly-spawned ``python -m pytest <file>``
Expand Down
16 changes: 16 additions & 0 deletions tests/test_tui_gateway_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3338,6 +3338,10 @@ def switch_model(self, **_kwargs):
)
monkeypatch.setattr(server, "_restart_slash_worker", lambda sid, session: None)
monkeypatch.setattr(server, "_emit", lambda *args, **kwargs: None)
# Plain /model persists globally by default; without this patch the real
# save_config_value() writes a cli-config.yaml into the repo root (the
# per-test HERMES_HOME is empty, so it falls through to project config).
monkeypatch.setattr("cli.save_config_value", lambda _key, _value: True)

resp = server.handle_request(
{
Expand Down Expand Up @@ -3436,6 +3440,9 @@ def test_config_set_model_explicit_provider_skips_broken_default_init(monkeypatc
monkeypatch.setattr(server, "_wait_agent", lambda *_args: seen.__setitem__("wait", seen["wait"] + 1))
monkeypatch.setattr(server, "_emit", lambda *args, **kwargs: None)
monkeypatch.setattr(server, "_restart_slash_worker", lambda *args, **kwargs: None)
# Plain /model persists globally by default; keep the real
# save_config_value() from writing a repo-root cli-config.yaml.
monkeypatch.setattr("cli.save_config_value", lambda _key, _value: True)

def fake_runtime_provider(*, requested=None, target_model=None, **_kwargs):
seen["requested"].append((requested, target_model))
Expand Down Expand Up @@ -3552,6 +3559,9 @@ def switch_model(self, **_kwargs):
)
monkeypatch.setattr(server, "_restart_slash_worker", lambda sid, session: None)
monkeypatch.setattr(server, "_emit", lambda *args, **kwargs: None)
# Plain /model persists globally by default; keep the real
# save_config_value() from writing a repo-root cli-config.yaml.
monkeypatch.setattr("cli.save_config_value", lambda _key, _value: True)

try:
server.handle_request(
Expand Down Expand Up @@ -3613,6 +3623,9 @@ def switch_model(self, **_kwargs):
)
monkeypatch.setattr(server, "_restart_slash_worker", lambda sid, session: None)
monkeypatch.setattr(server, "_emit", lambda *args, **kwargs: None)
# Plain /model persists globally by default; keep the real
# save_config_value() from writing a repo-root cli-config.yaml.
monkeypatch.setattr("cli.save_config_value", lambda _key, _value: True)

try:
server.handle_request(
Expand Down Expand Up @@ -3695,6 +3708,9 @@ def append_message(self, session_id, role, content=None, **_kwargs):
monkeypatch.delenv("HERMES_INFERENCE_MODEL", raising=False)
monkeypatch.setattr(server, "_restart_slash_worker", lambda sid, session: None)
monkeypatch.setattr(server, "_emit", lambda *args, **kwargs: None)
# Plain /model persists globally by default; keep the real
# save_config_value() from writing a repo-root cli-config.yaml.
monkeypatch.setattr("cli.save_config_value", lambda _key, _value: True)

def fake_switch_model(**kwargs):
return types.SimpleNamespace(
Expand Down
Loading