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 )