hermes-agent/tests/tools/test_local_shell_init.py
alt-glitch 4b16341975 refactor(restructure): rewrite all imports for hermes_agent package
Rewrite all import statements, patch() targets, sys.modules keys,
importlib.import_module() strings, and subprocess -m references to use
hermes_agent.* paths.

Strip sys.path.insert hacks from production code (rely on editable install).
Update COMPONENT_PREFIXES for logger filtering.
Fix 3 hardcoded getLogger() calls to use __name__.
Update transport and tool registry discovery paths.
Update plugin module path strings.
Add legacy process-name patterns for gateway PID detection.
Add main() to skills_sync for console_script entry point.
Fix _get_bundled_dir() path traversal after move.

Part of #14182, #14183
2026-04-23 08:35:34 +05:30

162 lines
5.8 KiB
Python

"""Tests for terminal.shell_init_files / terminal.auto_source_bashrc.
A bash ``-l -c`` invocation does NOT source ``~/.bashrc``, so tools that
register themselves there (nvm, asdf, pyenv) stay invisible to the
environment snapshot built by ``LocalEnvironment.init_session``. These
tests verify the config-driven prelude that fixes that.
"""
import os
from unittest.mock import patch
import pytest
from hermes_agent.backends.local import (
LocalEnvironment,
_prepend_shell_init,
_read_terminal_shell_init_config,
_resolve_shell_init_files,
)
class TestResolveShellInitFiles:
def test_auto_sources_bashrc_when_present(self, tmp_path, monkeypatch):
bashrc = tmp_path / ".bashrc"
bashrc.write_text('export MARKER=seen\n')
monkeypatch.setenv("HOME", str(tmp_path))
# Default config: auto_source_bashrc on, no explicit list.
with patch(
"hermes_agent.backends.local._read_terminal_shell_init_config",
return_value=([], True),
):
resolved = _resolve_shell_init_files()
assert resolved == [str(bashrc)]
def test_skips_bashrc_when_missing(self, tmp_path, monkeypatch):
# No bashrc written.
monkeypatch.setenv("HOME", str(tmp_path))
with patch(
"hermes_agent.backends.local._read_terminal_shell_init_config",
return_value=([], True),
):
resolved = _resolve_shell_init_files()
assert resolved == []
def test_auto_source_bashrc_off_suppresses_default(self, tmp_path, monkeypatch):
bashrc = tmp_path / ".bashrc"
bashrc.write_text('export MARKER=seen\n')
monkeypatch.setenv("HOME", str(tmp_path))
with patch(
"hermes_agent.backends.local._read_terminal_shell_init_config",
return_value=([], False),
):
resolved = _resolve_shell_init_files()
assert resolved == []
def test_explicit_list_wins_over_auto(self, tmp_path, monkeypatch):
bashrc = tmp_path / ".bashrc"
bashrc.write_text('export FROM_BASHRC=1\n')
custom = tmp_path / "custom.sh"
custom.write_text('export FROM_CUSTOM=1\n')
monkeypatch.setenv("HOME", str(tmp_path))
# auto_source_bashrc stays True but the explicit list takes precedence.
with patch(
"hermes_agent.backends.local._read_terminal_shell_init_config",
return_value=([str(custom)], True),
):
resolved = _resolve_shell_init_files()
assert resolved == [str(custom)]
assert str(bashrc) not in resolved
def test_expands_home_and_env_vars(self, tmp_path, monkeypatch):
target = tmp_path / "rc" / "custom.sh"
target.parent.mkdir()
target.write_text('export A=1\n')
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("CUSTOM_RC_DIR", str(tmp_path / "rc"))
with patch(
"hermes_agent.backends.local._read_terminal_shell_init_config",
return_value=(["~/rc/custom.sh"], False),
):
resolved_home = _resolve_shell_init_files()
with patch(
"hermes_agent.backends.local._read_terminal_shell_init_config",
return_value=(["${CUSTOM_RC_DIR}/custom.sh"], False),
):
resolved_var = _resolve_shell_init_files()
assert resolved_home == [str(target)]
assert resolved_var == [str(target)]
def test_missing_explicit_files_are_skipped_silently(self, tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path))
with patch(
"hermes_agent.backends.local._read_terminal_shell_init_config",
return_value=([str(tmp_path / "does-not-exist.sh")], False),
):
resolved = _resolve_shell_init_files()
assert resolved == []
class TestPrependShellInit:
def test_empty_list_returns_command_unchanged(self):
assert _prepend_shell_init("echo hi", []) == "echo hi"
def test_prepends_guarded_source_lines(self):
wrapped = _prepend_shell_init("echo hi", ["/tmp/a.sh", "/tmp/b.sh"])
assert "echo hi" in wrapped
# Each file is sourced through a guarded [ -r … ] && . '…' || true
# pattern so a missing/broken rc can't abort the bootstrap.
assert "/tmp/a.sh" in wrapped
assert "/tmp/b.sh" in wrapped
assert "|| true" in wrapped
assert "set +e" in wrapped
def test_escapes_single_quotes(self):
wrapped = _prepend_shell_init("echo hi", ["/tmp/o'malley.sh"])
# The path must survive as the shell receives it; embedded single
# quote is escaped as '\'' rather than breaking the outer quoting.
assert "o'\\''malley" in wrapped
@pytest.mark.skipif(
os.environ.get("CI") == "true" and not os.path.isfile("/bin/bash"),
reason="Requires bash; CI sandbox may strip it.",
)
class TestSnapshotEndToEnd:
"""Spin up a real LocalEnvironment and confirm the snapshot sources
extra init files."""
def test_snapshot_picks_up_init_file_exports(self, tmp_path, monkeypatch):
init_file = tmp_path / "custom-init.sh"
init_file.write_text(
'export HERMES_SHELL_INIT_PROBE="probe-ok"\n'
'export PATH="/opt/shell-init-probe/bin:$PATH"\n'
)
with patch(
"hermes_agent.backends.local._read_terminal_shell_init_config",
return_value=([str(init_file)], False),
):
env = LocalEnvironment(cwd=str(tmp_path), timeout=15)
try:
result = env.execute(
'echo "PROBE=$HERMES_SHELL_INIT_PROBE"; echo "PATH=$PATH"'
)
finally:
env.cleanup()
output = result.get("output", "")
assert "PROBE=probe-ok" in output
assert "/opt/shell-init-probe/bin" in output