fix(runtime): replace tmux pane capture with explicit bounded output (ODY-150)

This commit is contained in:
Alexandre Teixeira
2026-10-01 21:43:17 +01:00
parent 85cfe59b15
commit 4efb85ee33
6 changed files with 45 additions and 456 deletions
-157
View File
@@ -1,74 +1,4 @@
import asyncio
from types import SimpleNamespace
def test_new_tmux_session_forwards_runtime_python_environment(monkeypatch):
from src.agent_tools import subprocess_tools
calls = []
checks = 0
async def fake_has_session(_name):
nonlocal checks
checks += 1
return checks > 1
async def fake_run_exec(*args, **kwargs):
calls.append(args)
if args[:2] == ("tmux", "has-session"):
return "", "", 0
return "", "", 0
monkeypatch.setattr(subprocess_tools, "_tmux_has_session", fake_has_session)
monkeypatch.setattr(subprocess_tools, "_run_exec", fake_run_exec)
asyncio.run(subprocess_tools._ensure_tmux_session(
"ody-test",
"/workspace",
{
"PATH": "/opt/ody/bin:/usr/bin",
"VIRTUAL_ENV": "/opt/ody",
"HOME": "/workspace",
"TMPDIR": "/workspace/.tmp",
"SECRET": "must-not-forward",
},
))
new_session = next(call for call in calls if call[:2] == ("tmux", "new-session"))
assert "PATH=/opt/ody/bin:/usr/bin" in new_session
assert "VIRTUAL_ENV=/opt/ody" in new_session
assert "HOME=/workspace" in new_session
assert "TMPDIR=/workspace/.tmp" in new_session
assert not any("SECRET=" in arg for arg in new_session)
def test_reused_tmux_session_refreshes_runtime_python_environment(monkeypatch):
from src.agent_tools import subprocess_tools
sent = []
async def fake_has_session(_name):
return True
async def fake_send_line(name, line):
sent.append((name, line))
async def fake_run_exec(*_args, **_kwargs):
return "", "", 0
monkeypatch.setattr(subprocess_tools, "_tmux_has_session", fake_has_session)
monkeypatch.setattr(subprocess_tools, "_tmux_send_line", fake_send_line)
monkeypatch.setattr(subprocess_tools, "_run_exec", fake_run_exec)
asyncio.run(subprocess_tools._ensure_tmux_session(
"ody-existing",
"/workspace",
{"PATH": "/path with spaces/bin:/usr/bin", "VIRTUAL_ENV": "/path with spaces"},
))
assert sent == [(
"ody-existing",
"export PATH='/path with spaces/bin:/usr/bin' VIRTUAL_ENV='/path with spaces'",
)]
def test_workspace_alias_rewrite_does_not_duplicate_absolute_host_path():
@@ -97,93 +27,6 @@ def test_workspace_namespace_preserves_literal_paths_inside_scripts(tmp_path):
assert (Path(tmp_path) / "result.txt").read_text() == "ok"
def test_tmux_bash_runs_tool_command_with_closed_stdin(monkeypatch, tmp_path):
from src.agent_tools import subprocess_tools
sent = []
async def fake_ensure(*_args, **_kwargs):
return None
async def fake_send(_name, line):
sent.append(line)
async def fake_capture(_name):
start = next(line for line in sent if "__ODYSSEUS_CMD_START_" in line)
start = start.split("\\n")[1]
end_line = next(line for line in sent if "__ODYSSEUS_CMD_END_" in line)
end = end_line.split("\\n")[1].split("%s")[0]
return f"{start}\ngot-eof\n{end}0\n"
monkeypatch.setattr(subprocess_tools, "_ensure_tmux_session", fake_ensure)
monkeypatch.setattr(subprocess_tools, "_tmux_send_line", fake_send)
monkeypatch.setattr(subprocess_tools, "_tmux_capture", fake_capture)
monkeypatch.setattr(subprocess_tools.time, "time", lambda: 0.123456)
output, _stderr, rc, timed_out = asyncio.run(
subprocess_tools._run_tmux_bash(
"if read answer; then echo unexpected; else echo got-eof; fi",
session_id="test",
cwd=str(tmp_path),
env={},
timeout=2,
)
)
assert output == "got-eof"
assert rc == 0
assert timed_out is False
assert any("/bin/bash -lc" in line and "</dev/null" in line for line in sent)
def test_tmux_bash_timeout_destroys_session_process_tree(monkeypatch, tmp_path):
from src.agent_tools import subprocess_tools
exec_calls = []
sent = []
clock = [-2.0]
async def fake_ensure(*_args, **_kwargs):
return None
async def fake_send(name, line):
sent.append((name, line))
async def fake_capture(_name):
return "still running"
async def fake_run_exec(*args, **kwargs):
exec_calls.append(args)
return "", "", 0
async def fake_sleep(_seconds):
return None
monkeypatch.setattr(subprocess_tools, "_ensure_tmux_session", fake_ensure)
monkeypatch.setattr(subprocess_tools, "_tmux_send_line", fake_send)
monkeypatch.setattr(subprocess_tools, "_tmux_capture", fake_capture)
monkeypatch.setattr(subprocess_tools, "_run_exec", fake_run_exec)
monkeypatch.setattr(subprocess_tools.asyncio, "sleep", fake_sleep)
def fake_time():
clock[0] += 2.0
return clock[0]
monkeypatch.setattr(subprocess_tools.time, "time", fake_time)
_output, _stderr, rc, timed_out = asyncio.run(
subprocess_tools._run_tmux_bash(
"grep -r needle /large/tree",
session_id="timeout-test",
cwd=str(tmp_path),
env={},
timeout=1,
)
)
assert rc == 124 and timed_out is True
assert any(call[:3] == ("tmux", "send-keys", "-t") for call in exec_calls)
assert any(call[:3] == ("tmux", "kill-session", "-t") for call in exec_calls)
def test_direct_bash_subprocess_has_closed_stdin(monkeypatch, tmp_path):
from src.agent_tools import subprocess_tools
from src import tool_execution
+1 -1
View File
@@ -162,7 +162,7 @@ async def test_windows_bash_does_not_use_a_stray_tmux_executable(monkeypatch, tm
async def fail_tmux(*_args, **_kwargs):
pytest.fail("native Windows must not enter the POSIX tmux path")
monkeypatch.setattr(subprocess_tools, "_run_tmux_bash", fail_tmux)
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fail_tmux)
result = await subprocess_tools.BashTool().execute(
"pwd",
+1 -1
View File
@@ -17,7 +17,7 @@ async def test_a_chat_session_always_uses_the_owned_runner(monkeypatch, tmp_path
monkeypatch.setattr(subprocess_tools.shutil, "which", lambda name: "/fake/tmux" if name == "tmux" else original(name))
async def forbidden(*args, **kwargs):
pytest.fail("native Bash resurrected a persistent tmux shell")
monkeypatch.setattr(subprocess_tools, "_run_tmux_bash", forbidden)
monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", forbidden)
result = await subprocess_tools.BashTool().execute("printf ok", {"session_id": "same-chat"})
assert result["output"] == "ok"
assert result["teardown"]["dead"] is True
@@ -151,3 +151,37 @@ async def test_capture_preserves_multibyte_text_across_chunks(native_boundary):
[sys.executable, "-c", "import sys; sys.stdout.write('€' * 30000)"], argv=True)
assert result.stdout == "€" * 30000
assert result.output_truncated is False
async def test_chat_bash_captures_more_than_2000_lines(native_boundary):
result = await subprocess_tools.BashTool().execute(
"printf 'START\\n'; for i in $(seq 1 3000); do printf 'x\\n'; done; printf 'END\\n'",
{"session_id": "large-output-chat"},
)
assert result["output"].startswith("START\n")
assert result["output"].endswith("\nEND")
assert len(result["output"].splitlines()) == 3002
assert result["output_truncated"] is False
assert result["teardown"]["dead"] is True
async def test_chat_bash_reports_capture_limit_as_incomplete(native_boundary):
result = await subprocess_tools.BashTool().execute(
"for i in $(seq 1 12000); do printf 'x\\n'; done", {"session_id": "capped-chat"},
)
assert result["exit_code"] == 0
assert result["output_truncated"] is True
assert "truncated" in result["output"]
assert result["teardown"]["dead"] is True
async def test_repeated_chat_calls_refresh_environment(native_boundary):
first = await subprocess_tools.BashTool().execute('printf "%s" "$VALUE"', {
"session_id": "same-chat", "subproc_env": {"PATH": "/usr/bin:/bin", "VALUE": "first"},
})
second = await subprocess_tools.BashTool().execute('printf "%s" "$VALUE"', {
"session_id": "same-chat", "subproc_env": {"PATH": "/usr/bin:/bin", "VALUE": "second"},
})
assert first["output"] == "first"
assert second["output"] == "second"
assert first["containment"]["id"] != second["containment"]["id"]