mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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.
129 lines
5 KiB
Python
129 lines
5 KiB
Python
"""Regression tests for cwd-staleness in ShellFileOperations.
|
|
|
|
The bug: ShellFileOperations captured the terminal env's cwd at __init__
|
|
time and used that stale value for every subsequent _exec() call. When
|
|
a user ran ``cd`` via the terminal tool, ``env.cwd`` updated but
|
|
``ops.cwd`` did not. Relative paths passed to patch/read/write/search
|
|
then targeted the wrong directory — typically the session's start dir
|
|
instead of the current working directory.
|
|
|
|
Observed symptom: patch_replace() returned ``success=True`` with a
|
|
plausible diff, but the user's ``git diff`` showed no change (because
|
|
the patch landed in a different directory's copy of the same file).
|
|
|
|
Fix: _exec() now prefers the LIVE ``env.cwd`` over the init-time
|
|
``self.cwd``. Explicit ``cwd`` arg to _exec still wins over both.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
from tools.file_operations import ShellFileOperations
|
|
|
|
|
|
class _FakeEnv:
|
|
"""Minimal terminal env that tracks cwd across execute() calls.
|
|
|
|
Matches the real ``BaseEnvironment`` contract: ``cwd`` attribute plus
|
|
an ``execute(command, cwd=...)`` method whose return dict carries
|
|
``output`` and ``returncode``. Commands are executed in a real
|
|
subdirectory so file system effects match production.
|
|
"""
|
|
|
|
def __init__(self, start_cwd: str):
|
|
self.cwd = start_cwd
|
|
self.calls: list[dict] = []
|
|
|
|
def execute(self, command: str, cwd: str = None, **kwargs) -> dict:
|
|
import subprocess
|
|
self.calls.append({"command": command, "cwd": cwd})
|
|
# Simulate cd by updating self.cwd (the real env does the same
|
|
# via _extract_cwd_from_output after a successful command)
|
|
if command.strip().startswith("cd "):
|
|
new = command.strip()[3:].strip()
|
|
self.cwd = new
|
|
return {"output": "", "returncode": 0}
|
|
# Actually run the command — handle stdin via subprocess
|
|
stdin_data = kwargs.get("stdin_data")
|
|
proc = subprocess.run(
|
|
["bash", "-c", command],
|
|
cwd=cwd or self.cwd,
|
|
input=stdin_data,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return {
|
|
"output": proc.stdout + proc.stderr,
|
|
"returncode": proc.returncode,
|
|
}
|
|
|
|
|
|
class TestShellFileOpsCwdTracking:
|
|
"""_exec() must use live env.cwd, not the init-time cached cwd."""
|
|
|
|
def test_exec_follows_env_cwd_after_cd(self, tmp_path):
|
|
dir_a = tmp_path / "a"
|
|
dir_b = tmp_path / "b"
|
|
dir_a.mkdir()
|
|
dir_b.mkdir()
|
|
(dir_a / "target.txt").write_text("content-a\n")
|
|
(dir_b / "target.txt").write_text("content-b\n")
|
|
|
|
env = _FakeEnv(start_cwd=str(dir_a))
|
|
ops = ShellFileOperations(env, cwd=str(dir_a))
|
|
assert ops.cwd == str(dir_a) # init-time
|
|
|
|
# Simulate the user running `cd b` in terminal
|
|
env.execute(f"cd {dir_b}")
|
|
assert env.cwd == str(dir_b)
|
|
assert ops.cwd == str(dir_a), "ops.cwd is still init-time (fallback only)"
|
|
|
|
# Reading a relative path must now hit dir_b, not dir_a
|
|
result = ops._exec("cat target.txt")
|
|
assert result.exit_code == 0
|
|
assert "content-b" in result.stdout, (
|
|
f"Expected dir_b content, got {result.stdout!r}. "
|
|
"Stale ops.cwd leaked through — _exec must prefer env.cwd."
|
|
)
|
|
|
|
|
|
def test_env_without_cwd_attribute_falls_back_to_self_cwd(self, tmp_path):
|
|
"""Backends without a cwd attribute still work via init-time cwd."""
|
|
dir_a = tmp_path / "fixed"
|
|
dir_a.mkdir()
|
|
(dir_a / "target.txt").write_text("fixed-content\n")
|
|
|
|
class _NoCwdEnv:
|
|
def execute(self, command, cwd=None, **kwargs):
|
|
import subprocess
|
|
proc = subprocess.run(["bash", "-c", command], cwd=cwd,
|
|
capture_output=True, text=True)
|
|
return {"output": proc.stdout, "returncode": proc.returncode}
|
|
|
|
env = _NoCwdEnv()
|
|
ops = ShellFileOperations(env, cwd=str(dir_a))
|
|
result = ops._exec("cat target.txt")
|
|
assert result.exit_code == 0
|
|
assert "fixed-content" in result.stdout
|
|
|
|
def test_patch_returns_success_only_when_file_actually_written(self, tmp_path):
|
|
"""Safety rail: patch_replace success must reflect the real file state.
|
|
|
|
This test doesn't trigger the bug directly (it would require manual
|
|
corruption of the write), but it pins the invariant: when
|
|
patch_replace returns success=True, the file on disk matches the
|
|
intended content. If a future write_file change ever regresses,
|
|
this test catches it.
|
|
"""
|
|
target = tmp_path / "file.txt"
|
|
target.write_text("old content\n")
|
|
|
|
env = _FakeEnv(start_cwd=str(tmp_path))
|
|
ops = ShellFileOperations(env, cwd=str(tmp_path))
|
|
|
|
result = ops.patch_replace(str(target), "old content\n", "new content\n")
|
|
assert result.success is True
|
|
assert result.error is None
|
|
assert target.read_text() == "new content\n", (
|
|
"patch_replace claimed success but file wasn't written correctly"
|
|
)
|