"""Tests for terminal_session module.""" import asyncio import os import platform import pty import shlex from unittest.mock import AsyncMock, MagicMock, patch import pytest # Skip tests on Windows pytestmark = pytest.mark.skipif( platform.system() == "Windows", reason="Terminal sessions not supported on Windows", ) class TestTerminalSession: """Tests for TerminalSession class.""" def test_import(self): """Test that module can be imported.""" from textual_webterm.terminal_session import TerminalSession assert TerminalSession is not None def test_replay_buffer_size(self): """Test replay buffer size constant.""" from textual_webterm.terminal_session import REPLAY_BUFFER_SIZE assert REPLAY_BUFFER_SIZE == 64 * 1024 # 64KB def test_init(self): """Test TerminalSession initialization.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") assert session.session_id == "test-session" assert session.command == "bash" assert session.master_fd is None assert session.pid is None assert session._task is None def test_init_default_shell(self): """Test that default shell is used when command is empty.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() with patch.dict(os.environ, {"SHELL": "/bin/zsh"}): session = TerminalSession(mock_poller, "test-session", "") assert session.command == "/bin/zsh" @pytest.mark.asyncio async def test_replay_buffer_add(self): """Test adding data to replay buffer.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") await session._add_to_replay_buffer(b"test data") assert session._replay_buffer_size == 9 assert await session.get_replay_buffer() == b"test data" @pytest.mark.asyncio async def test_replay_buffer_multiple_adds(self): """Test adding multiple chunks to replay buffer.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") await session._add_to_replay_buffer(b"chunk1") await session._add_to_replay_buffer(b"chunk2") assert await session.get_replay_buffer() == b"chunk1chunk2" @pytest.mark.asyncio async def test_replay_buffer_overflow(self): """Test that replay buffer trims old data when exceeding limit.""" from textual_webterm.terminal_session import ( REPLAY_BUFFER_SIZE, TerminalSession, ) mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") # Add more data than buffer size chunk_size = 1024 for _i in range(100): # 100KB total await session._add_to_replay_buffer(b"x" * chunk_size) # Buffer should be trimmed assert session._replay_buffer_size <= REPLAY_BUFFER_SIZE + chunk_size def test_update_connector(self): """Test updating connector.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") mock_connector = MagicMock() session.update_connector(mock_connector) assert session._connector == mock_connector def test_is_running_not_started(self): """Test is_running when session not started.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") assert session.is_running() is False @pytest.mark.asyncio async def test_send_bytes_no_fd(self): """Test send_bytes returns False when no master_fd.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") result = await session.send_bytes(b"test") assert result is False @pytest.mark.asyncio async def test_send_meta(self): """Test send_meta returns True.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") result = await session.send_meta({}) assert result is True @pytest.mark.asyncio async def test_close_no_pid(self): """Test close when no pid.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") # Should not raise await session.close() @pytest.mark.asyncio async def test_wait_no_task(self): """Test wait when no task.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") # Should not raise await session.wait() def test_rich_repr(self): """Test rich repr output.""" from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() session = TerminalSession(mock_poller, "test-session", "bash") repr_items = list(session.__rich_repr__()) assert ("session_id", "test-session") in repr_items assert ("command", "bash") in repr_items @pytest.mark.asyncio async def test_open_uses_shlex_split_and_execvp_with_args(self): from textual_webterm.terminal_session import TerminalSession mock_poller = MagicMock() command = 'echo "hello world"' session = TerminalSession(mock_poller, "test-session", command) with ( patch("textual_webterm.terminal_session.pty.fork", return_value=(pty.CHILD, 123)) as mock_fork, patch("textual_webterm.terminal_session.version", return_value="0.0.0"), patch("textual_webterm.terminal_session.shlex.split", wraps=shlex.split) as mock_split, patch("textual_webterm.terminal_session.os.execvp", side_effect=OSError()) as mock_execvp, patch("textual_webterm.terminal_session.os._exit", side_effect=SystemExit(1)) as mock_exit, pytest.raises(SystemExit), ): await session.open() mock_fork.assert_called_once() mock_split.assert_called_once_with(command) mock_execvp.assert_called_once_with("echo", ["echo", "hello world"]) mock_exit.assert_called_once_with(1) @pytest.mark.asyncio async def test_open_parent_branch_sets_fd_and_pid(self): from textual_webterm.terminal_session import TerminalSession poller = MagicMock() session = TerminalSession(poller, "sid", "bash") with ( patch("textual_webterm.terminal_session.pty.fork", return_value=(1234, 99)), patch.object(session, "_set_terminal_size") as set_size, ): await session.open(width=80, height=24) assert session.pid == 1234 assert session.master_fd == 99 set_size.assert_called_once_with(80, 24) @pytest.mark.asyncio async def test_open_bad_command_exits(self): from textual_webterm.terminal_session import TerminalSession poller = MagicMock() session = TerminalSession(poller, "sid", "bad") with ( patch("textual_webterm.terminal_session.pty.fork", return_value=(pty.CHILD, 123)), patch("textual_webterm.terminal_session.shlex.split", side_effect=ValueError("bad")), patch("textual_webterm.terminal_session.os._exit", side_effect=SystemExit(1)) as mock_exit, pytest.raises(SystemExit), ): await session.open() mock_exit.assert_called_once_with(1) @pytest.mark.asyncio async def test_run_reads_from_poller_and_closes(self): from textual_webterm.terminal_session import TerminalSession queue: asyncio.Queue[bytes | None] = asyncio.Queue() await queue.put(b"hello") await queue.put(None) poller = MagicMock() poller.add_file = MagicMock(return_value=queue) poller.remove_file = MagicMock() connector = MagicMock() connector.on_data = AsyncMock() connector.on_close = AsyncMock() session = TerminalSession(poller, "sid", "bash") session.master_fd = 10 session._connector = connector with patch("textual_webterm.terminal_session.os.close") as mock_close: await session.run() connector.on_data.assert_awaited_once_with(b"hello") connector.on_close.assert_awaited_once() poller.remove_file.assert_called_once_with(10) mock_close.assert_called_once_with(10) @pytest.mark.asyncio async def test_start_updates_connector_when_already_running(self): from textual_webterm.terminal_session import TerminalSession poller = MagicMock() session = TerminalSession(poller, "sid", "bash") session.master_fd = 10 existing = asyncio.create_task(asyncio.sleep(0)) session._task = existing connector = MagicMock() task = await session.start(connector) assert task is existing assert session._connector is connector await existing @pytest.mark.asyncio async def test_send_bytes_writes_via_poller(self): from textual_webterm.terminal_session import TerminalSession poller = MagicMock() poller.write = AsyncMock() session = TerminalSession(poller, "sid", "bash") session.master_fd = 10 assert await session.send_bytes(b"x") is True poller.write.assert_awaited_once_with(10, b"x")