Files
hermes-agent/tests/tools/test_local_env_cwd_recovery.py
T
Teknium 39975613b1 test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00

174 lines
6.3 KiB
Python

"""Tests for LocalEnvironment recovery when ``self.cwd`` is deleted.
When a tool call inside the persistent terminal session ``rm -rf``'s its own
working directory, the next ``subprocess.Popen(..., cwd=self.cwd)`` would
otherwise raise ``FileNotFoundError`` before bash starts, wedging every
subsequent terminal/file-tool call until the gateway restarts.
Regression coverage for https://github.com/NousResearch/hermes-agent/issues/17558.
"""
import os
import shutil
import tempfile
import threading
from unittest.mock import MagicMock, patch
from tools.environments.local import (
LocalEnvironment,
_resolve_safe_cwd,
)
class TestResolveSafeCwd:
"""Pure-function unit tests for the recovery helper."""
def test_returns_cwd_when_directory_exists(self, tmp_path):
path = str(tmp_path)
assert _resolve_safe_cwd(path) == path
def test_returns_root_when_only_root_exists(self, monkeypatch):
"""If every ancestor except the filesystem root is gone, the root
itself is still a valid recovery target — don't skip it just because
``os.path.dirname('/') == '/'`` is the loop's exit condition."""
sep = os.path.sep
monkeypatch.setattr(os.path, "isdir", lambda p: p == sep)
assert _resolve_safe_cwd("/no/such/deep/dir") == sep
def _fake_interrupt():
return threading.Event()
def _make_fake_popen(captured: dict, fds: list):
"""Build a fake ``Popen`` whose ``stdout`` exposes a real OS file
descriptor so ``BaseEnvironment._wait_for_process`` can call
``select.select([fd], ...)`` and ``os.read(fd, ...)`` against it without
tripping ``TypeError: fileno() returned a non-integer`` from a MagicMock
``fileno()`` (or worse, accidentally reading from the test runner's own
stdout).
The pipe's write end is closed immediately so the drain loop sees EOF on
the first iteration. Every fd handed out is appended to ``fds`` so the
caller can clean up after the test.
"""
def fake_popen(cmd, **kwargs):
captured["cwd"] = kwargs.get("cwd")
captured["env"] = kwargs.get("env", {})
read_fd, write_fd = os.pipe()
os.close(write_fd)
stdout = os.fdopen(read_fd, "rb", buffering=0)
fds.append(stdout)
proc = MagicMock()
proc.poll.return_value = 0
proc.returncode = 0
proc.stdout = stdout
proc.stdin = MagicMock()
return proc
return fake_popen
def _close_fds(fds):
for f in fds:
try:
f.close()
except Exception:
pass
class TestRunBashCwdRecovery:
"""End-to-end recovery: deleted ``self.cwd`` must not crash Popen."""
def test_recovers_when_cwd_deleted_after_init(self, tmp_path, caplog):
"""Reproduces the wedge from #17558: cwd was valid when the
snapshot was taken, but a subsequent command deleted it before the
next ``Popen``."""
wedged = tmp_path / "wedge-repro"
wedged.mkdir()
with patch.object(LocalEnvironment, "init_session", autospec=True, return_value=None):
env = LocalEnvironment(cwd=str(wedged), timeout=10)
# The previous tool call deleted the working directory.
shutil.rmtree(wedged)
assert env.cwd == str(wedged) and not os.path.isdir(env.cwd)
captured = {}
fds: list = []
try:
with patch("tools.environments.local._find_bash", return_value="/bin/bash"), \
patch("subprocess.Popen", side_effect=_make_fake_popen(captured, fds)), \
patch("tools.terminal_tool._interrupt_event", _fake_interrupt()), \
caplog.at_level("WARNING", logger="tools.environments.local"):
env.execute("echo hello")
finally:
_close_fds(fds)
# Popen must have been handed a real, existing directory.
assert captured["cwd"] == str(tmp_path)
assert os.path.isdir(captured["cwd"])
# ``self.cwd`` is updated so the next call doesn't re-warn.
assert env.cwd == str(tmp_path)
# The warning surfaces the wedge so it isn't silently masked.
assert any("missing on disk" in rec.message for rec in caplog.records)
def test_no_warning_when_cwd_still_exists(self, tmp_path, caplog):
with patch.object(LocalEnvironment, "init_session", autospec=True, return_value=None):
env = LocalEnvironment(cwd=str(tmp_path), timeout=10)
captured = {}
fds: list = []
try:
with patch("tools.environments.local._find_bash", return_value="/bin/bash"), \
patch("subprocess.Popen", side_effect=_make_fake_popen(captured, fds)), \
patch("tools.terminal_tool._interrupt_event", _fake_interrupt()), \
caplog.at_level("WARNING", logger="tools.environments.local"):
env.execute("echo hello")
finally:
_close_fds(fds)
assert captured["cwd"] == str(tmp_path)
assert env.cwd == str(tmp_path)
assert not any("missing on disk" in rec.message for rec in caplog.records)
class TestUpdateCwdRejectsMissingPaths:
"""``_update_cwd`` must not propagate a deleted path back into ``self.cwd``."""
def test_skips_assignment_when_marker_path_missing(self, tmp_path):
original = tmp_path / "starting"
original.mkdir()
with patch.object(LocalEnvironment, "init_session", autospec=True, return_value=None):
env = LocalEnvironment(cwd=str(original), timeout=10)
# Simulate the stale-marker case: the prior command emitted a cwd
# marker for a directory that has since been deleted.
deleted = tmp_path / "wedge-repro"
marker = env._cwd_marker
env._update_cwd(
{"output": f"x\n{marker}{deleted}{marker}\n", "returncode": 0}
)
assert env.cwd == str(original)
def test_accepts_assignment_when_marker_path_exists(self, tmp_path):
original = tmp_path / "starting"
original.mkdir()
new_dir = tmp_path / "next"
new_dir.mkdir()
with patch.object(LocalEnvironment, "init_session", autospec=True, return_value=None):
env = LocalEnvironment(cwd=str(original), timeout=10)
marker = env._cwd_marker
env._update_cwd(
{"output": f"x\n{marker}{new_dir}{marker}\n", "returncode": 0}
)
assert env.cwd == str(new_dir)