Skip to content

Commit 4b86328

Browse files
committed
Server(fix[new_session]): raise descriptive error when proc.stdout is empty
why: Server.new_session reads session_stdout via proc.stdout[0] without checking that proc.stdout has any elements. If tmux returns empty stdout (without writing to stderr — corrupt pipe, kernel-level signal mid-write, mocked transport), this raises an unhelpful IndexError instead of a branded LibTmuxException. The TMUX env restoration was already wrapped in try/finally on master, but the empty-stdout guard was missing. what: - add 'if not proc.stdout: raise LibTmuxException' check before indexing proc.stdout[0] - add tests for empty stdout, TMUX env restoration on cmd-side errors, and TMUX env restoration on setup-phase errors (to verify the existing try/finally covers arg-building, not just the cmd() call) - tests use monkeypatch per CLAUDE.md (cannot trigger empty stdout from a healthy tmux server otherwise)
1 parent 3a6fb89 commit 4b86328

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

src/libtmux/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1994,6 +1994,10 @@ def new_session(
19941994
if proc.stderr:
19951995
raise exc.LibTmuxException(proc.stderr)
19961996

1997+
if not proc.stdout:
1998+
msg = "new-session produced no output"
1999+
raise exc.LibTmuxException(msg)
2000+
19972001
session_stdout = proc.stdout[0]
19982002

19992003
finally:

tests/test_server.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,94 @@ def test_new_session(server: Server) -> None:
108108
assert server.has_session("test_new_session")
109109

110110

111+
def test_new_session_empty_stdout(
112+
server: Server,
113+
monkeypatch: pytest.MonkeyPatch,
114+
) -> None:
115+
"""Server.new_session raises LibTmuxException when tmux returns no output.
116+
117+
monkeypatch is used to simulate empty stdout, which cannot be triggered
118+
through normal tmux operations on a healthy server.
119+
"""
120+
original_cmd = server.cmd
121+
122+
def mock_cmd(cmd: str, *args: t.Any, **kwargs: t.Any) -> t.Any:
123+
result = original_cmd(cmd, *args, **kwargs)
124+
if cmd == "new-session":
125+
result.stdout = []
126+
return result
127+
128+
monkeypatch.setattr(server, "cmd", mock_cmd)
129+
130+
with pytest.raises(exc.LibTmuxException, match="new-session produced no output"):
131+
server.new_session(session_name="test_empty_stdout")
132+
133+
monkeypatch.undo()
134+
if server.has_session("test_empty_stdout"):
135+
server.kill_session("test_empty_stdout")
136+
137+
138+
def test_new_session_restores_tmux_env_on_error(
139+
server: Server,
140+
monkeypatch: pytest.MonkeyPatch,
141+
) -> None:
142+
"""Server.new_session restores TMUX env var when an exception is raised.
143+
144+
monkeypatch is used to simulate empty stdout (forcing the exception path),
145+
which cannot be triggered through normal tmux operations.
146+
"""
147+
original_cmd = server.cmd
148+
sentinel = "/tmp/libtmux-test-fake-socket,12345,0"
149+
150+
monkeypatch.setenv("TMUX", sentinel)
151+
152+
def mock_cmd(cmd: str, *args: t.Any, **kwargs: t.Any) -> t.Any:
153+
result = original_cmd(cmd, *args, **kwargs)
154+
if cmd == "new-session":
155+
result.stdout = []
156+
return result
157+
158+
monkeypatch.setattr(server, "cmd", mock_cmd)
159+
160+
with pytest.raises(exc.LibTmuxException, match="new-session produced no output"):
161+
server.new_session(session_name="test_env_restore")
162+
163+
assert os.environ.get("TMUX") == sentinel
164+
165+
monkeypatch.undo()
166+
if server.has_session("test_env_restore"):
167+
server.kill_session("test_env_restore")
168+
169+
170+
def test_new_session_restores_tmux_env_on_setup_error(
171+
server: Server,
172+
monkeypatch: pytest.MonkeyPatch,
173+
tmp_path: pathlib.Path,
174+
) -> None:
175+
"""Server.new_session restores TMUX env when setup code before cmd() raises.
176+
177+
monkeypatch makes pathlib.Path.expanduser raise to verify the try/finally
178+
covers the arg-building phase, not just the cmd() call.
179+
"""
180+
sentinel = "/tmp/libtmux-test-fake-socket,99999,0"
181+
182+
monkeypatch.setenv("TMUX", sentinel)
183+
184+
def broken_expanduser(self: pathlib.Path) -> t.Any:
185+
msg = "injected setup error"
186+
raise RuntimeError(msg)
187+
188+
monkeypatch.setattr(pathlib.Path, "expanduser", broken_expanduser)
189+
190+
with pytest.raises(RuntimeError, match="injected setup error"):
191+
server.new_session(
192+
session_name="test_setup_error",
193+
start_directory=tmp_path,
194+
)
195+
196+
assert os.environ.get("TMUX") == sentinel
197+
198+
111199
def test_new_session_returns_populated_session(server: Server) -> None:
112200
"""Server.new_session returns Session populated from -P output."""
113201
session = server.new_session(session_name="test_populated")

0 commit comments

Comments
 (0)