Files
webterm/tests/test_session_manager.py
T
GitHub Copilot d5343117d3 Filter DA1 responses from replay buffer on WebSocket connect
The replay buffer can contain DA1/DA2 terminal attribute responses
(e.g., \x1b[?1;10;0c) that were captured before filtering was added
to the session classes. These responses appear as visible text like
'1;10;0c' when sent to the client on reconnect.

This adds an additional filter pass when sending the replay buffer,
ensuring no DA1 responses reach the client regardless of when they
were captured.
2026-01-29 19:13:40 +00:00

266 lines
9.5 KiB
Python

"""Tests for session_manager module."""
import platform
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from webterm.config import App
from webterm.docker_watcher import AUTO_COMMAND_SENTINEL
from webterm.session_manager import SessionManager
from webterm.types import RouteKey, SessionID
class TestSessionManager:
"""Tests for SessionManager class."""
@pytest.fixture
def mock_poller(self):
"""Create a mock poller."""
return MagicMock()
@pytest.fixture
def mock_path(self, tmp_path):
"""Create a mock path."""
return tmp_path
@pytest.fixture
def sample_apps(self):
"""Create sample apps."""
return [
App(name="Test Terminal", slug="terminal", path="./", command="bash", terminal=True),
App(name="Test App", slug="app", path="./", command="python app.py", terminal=False),
]
def test_init(self, mock_poller, mock_path, sample_apps):
"""Test SessionManager initialization."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
assert manager.poller == mock_poller
assert manager.path == mock_path
assert len(manager.apps) == 2
assert "terminal" in manager.apps_by_slug
assert "app" in manager.apps_by_slug
assert len(manager.sessions) == 0
assert len(manager.routes) == 0
def test_get_default_app(self, mock_poller, mock_path, sample_apps):
"""Test getting the default app."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
assert manager.get_default_app() == sample_apps[0]
def test_get_default_app_empty(self, mock_poller, mock_path):
"""Test getting the default app when no apps are configured."""
manager = SessionManager(mock_poller, mock_path, [])
assert manager.get_default_app() is None
def test_add_app(self, mock_poller, mock_path):
"""Test adding an app."""
manager = SessionManager(mock_poller, mock_path, [])
manager.add_app("New App", "python new.py", "newapp", terminal=False)
assert len(manager.apps) == 1
assert "newapp" in manager.apps_by_slug
assert manager.apps_by_slug["newapp"].name == "New App"
def test_add_app_auto_slug(self, mock_poller, mock_path):
"""Test adding an app with auto-generated slug."""
manager = SessionManager(mock_poller, mock_path, [])
manager.add_app("Auto App", "python auto.py", "", terminal=False)
assert len(manager.apps) == 1
# Slug should be auto-generated
assert len(manager.apps[0].slug) > 0
def test_get_session_not_found(self, mock_poller, mock_path, sample_apps):
"""Test getting a non-existent session."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
result = manager.get_session(SessionID("nonexistent"))
assert result is None
def test_get_session_by_route_key_not_found(self, mock_poller, mock_path, sample_apps):
"""Test getting session by non-existent route key."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
result = manager.get_session_by_route_key(RouteKey("nonexistent"))
assert result is None
def test_on_session_end(self, mock_poller, mock_path, sample_apps):
"""Test session end cleanup."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
# Manually add a session
session_id = SessionID("test-session")
route_key = RouteKey("test-route")
mock_session = MagicMock()
manager.sessions[session_id] = mock_session
manager.routes[route_key] = session_id
# End session
manager.on_session_end(session_id)
assert session_id not in manager.sessions
assert route_key not in manager.routes
def test_on_session_end_nonexistent(self, mock_poller, mock_path, sample_apps):
"""Test session end for non-existent session."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
# Should not raise
manager.on_session_end(SessionID("nonexistent"))
@pytest.mark.asyncio
async def test_close_all_empty(self, mock_poller, mock_path, sample_apps):
"""Test closing all sessions when empty."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
# Should not raise
await manager.close_all()
@pytest.mark.asyncio
async def test_close_all_with_sessions(self, mock_poller, mock_path, sample_apps):
"""Test closing all sessions."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
# Add mock sessions
mock_session = MagicMock()
mock_session.close = AsyncMock()
mock_session.wait = AsyncMock()
manager.sessions[SessionID("s1")] = mock_session
await manager.close_all(timeout=1.0)
mock_session.close.assert_called_once()
@pytest.mark.asyncio
async def test_close_session(self, mock_poller, mock_path, sample_apps):
"""Test closing a specific session."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
mock_session = MagicMock()
mock_session.close = AsyncMock()
session_id = SessionID("test-session")
manager.sessions[session_id] = mock_session
await manager.close_session(session_id)
mock_session.close.assert_called_once()
@pytest.mark.asyncio
async def test_close_session_nonexistent(self, mock_poller, mock_path, sample_apps):
"""Test closing a non-existent session."""
manager = SessionManager(mock_poller, mock_path, sample_apps)
# Should not raise
await manager.close_session(SessionID("nonexistent"))
@pytest.mark.asyncio
async def test_new_session_no_app(self, mock_poller, mock_path):
"""Test creating session with no matching app."""
manager = SessionManager(mock_poller, mock_path, [])
result = await manager.new_session(
"nonexistent",
SessionID("test"),
RouteKey("route"),
)
assert result is None
@pytest.mark.asyncio
@pytest.mark.skipif(platform.system() == "Windows", reason="Terminal not supported on Windows")
async def test_new_terminal_session(self, mock_poller, mock_path):
"""Test creating a new terminal session."""
from webterm.terminal_session import TerminalSession
app = App(name="Terminal", slug="term", path="./", command="echo test", terminal=True)
manager = SessionManager(mock_poller, mock_path, [app])
with patch.object(TerminalSession, "open", new_callable=AsyncMock):
result = await manager.new_session(
"term",
SessionID("test-session"),
RouteKey("test-route"),
)
assert result is not None
assert isinstance(result, TerminalSession)
assert SessionID("test-session") in manager.sessions
assert RouteKey("test-route") in manager.routes
@pytest.mark.asyncio
@pytest.mark.skipif(platform.system() == "Windows", reason="Terminal not supported on Windows")
async def test_new_docker_exec_session(self, mock_poller, mock_path):
from webterm.docker_exec_session import DockerExecSession
app = App(
name="my-container",
slug="my-container",
path="./",
command=AUTO_COMMAND_SENTINEL,
terminal=True,
)
manager = SessionManager(mock_poller, mock_path, [app])
with patch.object(DockerExecSession, "open", new_callable=AsyncMock):
result = await manager.new_session(
"my-container",
SessionID("test-session"),
RouteKey("test-route"),
)
assert result is not None
assert isinstance(result, DockerExecSession)
class TestSessionManagerRoutes:
"""Tests for SessionManager route handling."""
@pytest.fixture
def manager(self, tmp_path):
"""Create a session manager with mock poller."""
mock_poller = MagicMock()
return SessionManager(mock_poller, tmp_path, [])
def test_route_mapping(self, manager):
"""Test route to session mapping."""
session_id = SessionID("session1")
route_key = RouteKey("route1")
manager.routes[route_key] = session_id
assert manager.routes.get(route_key) == session_id
assert manager.routes.get_key(session_id) == route_key
def test_get_session_by_route(self, manager):
"""Test getting session by route key."""
session_id = SessionID("session1")
route_key = RouteKey("route1")
mock_session = MagicMock()
manager.sessions[session_id] = mock_session
manager.routes[route_key] = session_id
result = manager.get_session_by_route_key(route_key)
assert result == mock_session
def test_get_first_running_session_none(self, manager):
"""Test getting first running session when empty."""
assert manager.get_first_running_session() is None
def test_get_first_running_session_found(self, manager):
"""Test getting first running session."""
session_id = SessionID("s1")
route_key = RouteKey("r1")
mock_session = MagicMock()
mock_session.is_running.return_value = True
manager.sessions[session_id] = mock_session
manager.routes[route_key] = session_id
result = manager.get_first_running_session()
assert result == (route_key, mock_session)