| """Tests for the local persistent shell backend.""" |
|
|
| import glob as glob_mod |
|
|
| import pytest |
|
|
| from tools.environments.local import LocalEnvironment |
| from tools.environments.persistent_shell import PersistentShellMixin |
|
|
|
|
| class TestLocalConfig: |
| def test_local_persistent_default_false(self, monkeypatch): |
| monkeypatch.delenv("TERMINAL_LOCAL_PERSISTENT", raising=False) |
| from tools.terminal_tool import _get_env_config |
| assert _get_env_config()["local_persistent"] is False |
|
|
| def test_local_persistent_true(self, monkeypatch): |
| monkeypatch.setenv("TERMINAL_LOCAL_PERSISTENT", "true") |
| from tools.terminal_tool import _get_env_config |
| assert _get_env_config()["local_persistent"] is True |
|
|
| def test_local_persistent_yes(self, monkeypatch): |
| monkeypatch.setenv("TERMINAL_LOCAL_PERSISTENT", "yes") |
| from tools.terminal_tool import _get_env_config |
| assert _get_env_config()["local_persistent"] is True |
|
|
|
|
| class TestMergeOutput: |
| def test_stdout_only(self): |
| assert PersistentShellMixin._merge_output("out", "") == "out" |
|
|
| def test_stderr_only(self): |
| assert PersistentShellMixin._merge_output("", "err") == "err" |
|
|
| def test_both(self): |
| assert PersistentShellMixin._merge_output("out", "err") == "out\nerr" |
|
|
| def test_empty(self): |
| assert PersistentShellMixin._merge_output("", "") == "" |
|
|
| def test_strips_trailing_newlines(self): |
| assert PersistentShellMixin._merge_output("out\n\n", "err\n") == "out\nerr" |
|
|
|
|
| class TestLocalOneShotRegression: |
| def test_echo(self): |
| env = LocalEnvironment(persistent=False) |
| r = env.execute("echo hello") |
| assert r["returncode"] == 0 |
| assert "hello" in r["output"] |
| env.cleanup() |
|
|
| def test_exit_code(self): |
| env = LocalEnvironment(persistent=False) |
| r = env.execute("exit 42") |
| assert r["returncode"] == 42 |
| env.cleanup() |
|
|
| def test_state_does_not_persist(self): |
| env = LocalEnvironment(persistent=False) |
| env.execute("export HERMES_ONESHOT_LOCAL=yes") |
| r = env.execute("echo $HERMES_ONESHOT_LOCAL") |
| assert r["output"].strip() == "" |
| env.cleanup() |
|
|
|
|
| class TestLocalPersistent: |
| @pytest.fixture |
| def env(self): |
| e = LocalEnvironment(persistent=True) |
| yield e |
| e.cleanup() |
|
|
| def test_echo(self, env): |
| r = env.execute("echo hello-persistent") |
| assert r["returncode"] == 0 |
| assert "hello-persistent" in r["output"] |
|
|
| def test_env_var_persists(self, env): |
| env.execute("export HERMES_LOCAL_PERSIST_TEST=works") |
| r = env.execute("echo $HERMES_LOCAL_PERSIST_TEST") |
| assert r["output"].strip() == "works" |
|
|
| def test_cwd_persists(self, env): |
| env.execute("cd /tmp") |
| r = env.execute("pwd") |
| assert r["output"].strip() == "/tmp" |
|
|
| def test_exit_code(self, env): |
| r = env.execute("(exit 42)") |
| assert r["returncode"] == 42 |
|
|
| def test_stderr(self, env): |
| r = env.execute("echo oops >&2") |
| assert r["returncode"] == 0 |
| assert "oops" in r["output"] |
|
|
| def test_multiline_output(self, env): |
| r = env.execute("echo a; echo b; echo c") |
| lines = r["output"].strip().splitlines() |
| assert lines == ["a", "b", "c"] |
|
|
| def test_timeout_then_recovery(self, env): |
| r = env.execute("sleep 999", timeout=2) |
| assert r["returncode"] in (124, 130) |
| r = env.execute("echo alive") |
| assert r["returncode"] == 0 |
| assert "alive" in r["output"] |
|
|
| def test_large_output(self, env): |
| r = env.execute("seq 1 1000") |
| assert r["returncode"] == 0 |
| lines = r["output"].strip().splitlines() |
| assert len(lines) == 1000 |
| assert lines[0] == "1" |
| assert lines[-1] == "1000" |
|
|
| def test_shell_variable_persists(self, env): |
| env.execute("MY_LOCAL_VAR=hello123") |
| r = env.execute("echo $MY_LOCAL_VAR") |
| assert r["output"].strip() == "hello123" |
|
|
| def test_cleanup_removes_temp_files(self, env): |
| env.execute("echo warmup") |
| prefix = env._temp_prefix |
| assert len(glob_mod.glob(f"{prefix}-*")) > 0 |
| env.cleanup() |
| remaining = glob_mod.glob(f"{prefix}-*") |
| assert remaining == [] |
|
|
| def test_state_does_not_leak_between_instances(self): |
| env1 = LocalEnvironment(persistent=True) |
| env2 = LocalEnvironment(persistent=True) |
| try: |
| env1.execute("export LEAK_TEST=from_env1") |
| r = env2.execute("echo $LEAK_TEST") |
| assert r["output"].strip() == "" |
| finally: |
| env1.cleanup() |
| env2.cleanup() |
|
|
| def test_special_characters_in_command(self, env): |
| r = env.execute("echo 'hello world'") |
| assert r["output"].strip() == "hello world" |
|
|
| def test_pipe_command(self, env): |
| r = env.execute("echo hello | tr 'h' 'H'") |
| assert r["output"].strip() == "Hello" |
|
|
| def test_multiple_commands_semicolon(self, env): |
| r = env.execute("X=42; echo $X") |
| assert r["output"].strip() == "42" |
|
|