/ tests / tools / test_init_session_cwd_respect.py
test_init_session_cwd_respect.py
  1  """Tests that init_session() respects the configured cwd.
  2  
  3  The bug: when terminal.cwd is set in config.yaml, the configured path was
  4  displayed in the TUI banner but actual terminal commands ran in os.getcwd()
  5  (the directory where ``hermes chat`` was started).
  6  
  7  Root cause: init_session() captures the login shell environment by running
  8  ``pwd -P`` inside a ``bash -l -c`` bootstrap.  Profile scripts (.bashrc,
  9  .bash_profile, etc.) can change the working directory before ``pwd -P``
 10  runs, so _update_cwd() overwrites self.cwd with the wrong directory.
 11  
 12  Fix: the bootstrap now includes an explicit ``cd`` back to self.cwd before
 13  running ``pwd -P``, so the configured cwd is always what gets recorded.
 14  """
 15  
 16  from tempfile import TemporaryFile
 17  from unittest.mock import MagicMock
 18  
 19  from tools.environments.base import BaseEnvironment
 20  
 21  
 22  class _TestableEnv(BaseEnvironment):
 23      """Concrete subclass for testing base class methods."""
 24  
 25      def __init__(self, cwd="/tmp", timeout=10):
 26          super().__init__(cwd=cwd, timeout=timeout)
 27  
 28      def _run_bash(self, cmd_string, *, login=False, timeout=120, stdin_data=None):
 29          raise NotImplementedError("Use mock")
 30  
 31      def cleanup(self):
 32          pass
 33  
 34  
 35  class TestInitSessionCwdRespect:
 36      """init_session() must preserve the configured cwd."""
 37  
 38      def test_bootstrap_contains_cd_to_configured_cwd(self):
 39          """The bootstrap script must cd to self.cwd before running pwd."""
 40          env = _TestableEnv(cwd="/my/project")
 41  
 42          # Capture the bootstrap script that init_session would pass to _run_bash
 43          captured = {}
 44  
 45          def mock_run_bash(cmd_string, *, login=False, timeout=120, stdin_data=None):
 46              captured["cmd"] = cmd_string
 47              mock = MagicMock()
 48              mock.poll.return_value = 0
 49              mock.returncode = 0
 50              stdout = TemporaryFile(mode="w+b")
 51              stdout.seek(0)
 52              mock.stdout = stdout
 53              return mock
 54  
 55          env._run_bash = mock_run_bash
 56          env.init_session()
 57  
 58          assert "cmd" in captured, "init_session did not call _run_bash"
 59          bootstrap = captured["cmd"]
 60  
 61          # The cd must appear before pwd -P so the configured cwd is recorded
 62          cd_pos = bootstrap.find("builtin cd")
 63          pwd_pos = bootstrap.find("pwd -P")
 64          assert cd_pos != -1, "bootstrap must contain 'builtin cd'"
 65          assert pwd_pos != -1, "bootstrap must contain 'pwd -P'"
 66          assert cd_pos < pwd_pos, (
 67              "builtin cd must appear before pwd -P in the bootstrap so "
 68              "the configured cwd is what gets recorded"
 69          )
 70  
 71          # The cd target must be the configured path (shlex.quote only adds
 72          # quotes when the path contains shell-special characters)
 73          assert "/my/project" in bootstrap, (
 74              "bootstrap cd must target the configured cwd (/my/project)"
 75          )
 76  
 77      def test_configured_cwd_survives_init_session(self):
 78          """self.cwd must be the configured path after init_session completes."""
 79          configured_cwd = "/my/project"
 80          env = _TestableEnv(cwd=configured_cwd)
 81  
 82          marker = env._cwd_marker
 83  
 84          def mock_run_bash(cmd_string, *, login=False, timeout=120, stdin_data=None):
 85              mock = MagicMock()
 86              mock.poll.return_value = 0
 87              mock.returncode = 0
 88              # Simulate output where pwd reports the configured cwd
 89              output = f"snapshot output\n{marker}{configured_cwd}{marker}\n"
 90              stdout = TemporaryFile(mode="w+b")
 91              stdout.write(output.encode("utf-8"))
 92              stdout.seek(0)
 93              mock.stdout = stdout
 94              return mock
 95  
 96          env._run_bash = mock_run_bash
 97          env.init_session()
 98  
 99          assert env.cwd == configured_cwd, (
100              f"Expected cwd={configured_cwd!r} after init_session, got {env.cwd!r}"
101          )
102  
103      def test_default_cwd_still_works(self):
104          """When no custom cwd is configured, default /tmp behavior is preserved."""
105          env = _TestableEnv()  # default cwd="/tmp"
106  
107          marker = env._cwd_marker
108  
109          def mock_run_bash(cmd_string, *, login=False, timeout=120, stdin_data=None):
110              mock = MagicMock()
111              mock.poll.return_value = 0
112              mock.returncode = 0
113              output = f"snapshot output\n{marker}/tmp{marker}\n"
114              stdout = TemporaryFile(mode="w+b")
115              stdout.write(output.encode("utf-8"))
116              stdout.seek(0)
117              mock.stdout = stdout
118              return mock
119  
120          env._run_bash = mock_run_bash
121          env.init_session()
122  
123          assert env.cwd == "/tmp"
124  
125      def test_bootstrap_cd_uses_shlex_quote(self):
126          """Paths with spaces must be properly quoted in the bootstrap cd."""
127          env = _TestableEnv(cwd="/my project/with spaces")
128  
129          captured = {}
130  
131          def mock_run_bash(cmd_string, *, login=False, timeout=120, stdin_data=None):
132              captured["cmd"] = cmd_string
133              mock = MagicMock()
134              mock.poll.return_value = 0
135              mock.returncode = 0
136              stdout = TemporaryFile(mode="w+b")
137              stdout.seek(0)
138              mock.stdout = stdout
139              return mock
140  
141          env._run_bash = mock_run_bash
142          env.init_session()
143  
144          bootstrap = captured["cmd"]
145          # shlex.quote wraps paths with spaces in single quotes
146          assert "'/my project/with spaces'" in bootstrap, (
147              "bootstrap cd must properly quote paths with spaces"
148          )