mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 23:11:37 +08:00
163 lines
5.7 KiB
Python
163 lines
5.7 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 tools.environments.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(
|
||
|
|
"tools.environments.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(
|
||
|
|
"tools.environments.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(
|
||
|
|
"tools.environments.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(
|
||
|
|
"tools.environments.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(
|
||
|
|
"tools.environments.local._read_terminal_shell_init_config",
|
||
|
|
return_value=(["~/rc/custom.sh"], False),
|
||
|
|
):
|
||
|
|
resolved_home = _resolve_shell_init_files()
|
||
|
|
|
||
|
|
with patch(
|
||
|
|
"tools.environments.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(
|
||
|
|
"tools.environments.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(
|
||
|
|
"tools.environments.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
|