Skip to content

Commit 7c4c985

Browse files
committed
Add comprehensive unit tests for YDocSessionManager yroom-kernel connection logic
Tests cover: - Cached room_id usage and reconstruction from database - Non-notebook session handling - Idempotency (avoiding duplicate connections) - Edge cases (missing yrooms, sessions without kernels) - get_session() integration with _ensure_yroom_connected() These tests verify the critical functionality for maintaining yroom-kernel connections for persistent kernels across server restarts.
1 parent 3707159 commit 7c4c985

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
"""Tests for YDocSessionManager yroom-kernel connection logic.
2+
3+
These tests verify that the session manager properly maintains connections
4+
between yrooms (collaborative document state) and kernel clients, especially
5+
for persistent kernels that survive server restarts.
6+
"""
7+
import pytest
8+
from unittest.mock import AsyncMock, Mock, MagicMock, patch
9+
from jupyter_server_documents.session_manager import YDocSessionManager
10+
11+
12+
@pytest.fixture
13+
def session_manager():
14+
"""Create a mock session manager for testing."""
15+
manager = YDocSessionManager()
16+
17+
# Mock required dependencies
18+
manager.serverapp = Mock()
19+
manager.file_id_manager = Mock()
20+
manager.yroom_manager = Mock()
21+
manager.log = Mock()
22+
23+
# Initialize the _room_ids dict
24+
manager._room_ids = {}
25+
26+
return manager
27+
28+
29+
@pytest.fixture
30+
def mock_kernel_client():
31+
"""Create a mock kernel client with _yrooms attribute."""
32+
client = Mock()
33+
client._yrooms = set()
34+
return client
35+
36+
37+
@pytest.fixture
38+
def mock_yroom():
39+
"""Create a mock YRoom."""
40+
yroom = Mock()
41+
yroom.room_id = "json:notebook:test-file-id"
42+
return yroom
43+
44+
45+
class TestEnsureYRoomConnected:
46+
"""Tests for _ensure_yroom_connected method."""
47+
48+
@pytest.mark.asyncio
49+
async def test_uses_cached_room_id(self, session_manager, mock_yroom, mock_kernel_client):
50+
"""Test that cached room_id is used when available."""
51+
session_id = "session-123"
52+
kernel_id = "kernel-456"
53+
room_id = "json:notebook:cached-file-id"
54+
55+
# Set up cached room_id
56+
session_manager._room_ids[session_id] = room_id
57+
58+
# Mock yroom manager
59+
session_manager.yroom_manager.get_room.return_value = mock_yroom
60+
mock_yroom.room_id = room_id
61+
62+
# Mock kernel manager
63+
mock_kernel_manager = Mock()
64+
mock_kernel_manager.kernel_client = mock_kernel_client
65+
session_manager.serverapp.kernel_manager.get_kernel.return_value = mock_kernel_manager
66+
67+
await session_manager._ensure_yroom_connected(session_id, kernel_id)
68+
69+
# Verify cached room_id was used
70+
session_manager.yroom_manager.get_room.assert_called_once_with(room_id)
71+
72+
# Verify yroom was added to kernel client
73+
assert mock_yroom in mock_kernel_client._yrooms
74+
75+
@pytest.mark.asyncio
76+
async def test_reconstructs_room_id_from_session_path(self, session_manager, mock_yroom, mock_kernel_client):
77+
"""Test that room_id is reconstructed from session path when not cached."""
78+
session_id = "session-123"
79+
kernel_id = "kernel-456"
80+
path = "/path/to/notebook.ipynb"
81+
file_id = "reconstructed-file-id"
82+
room_id = f"json:notebook:{file_id}"
83+
84+
# Mock get_session to return session with path
85+
mock_session = {
86+
"id": session_id,
87+
"type": "notebook",
88+
"path": path
89+
}
90+
91+
with patch.object(YDocSessionManager, 'get_session', new_callable=AsyncMock) as mock_get_session:
92+
# Use super() call to avoid recursion
93+
mock_get_session.return_value = mock_session
94+
95+
# Mock file_id_manager
96+
session_manager.file_id_manager.index.return_value = file_id
97+
98+
# Mock yroom manager
99+
session_manager.yroom_manager.get_room.return_value = mock_yroom
100+
mock_yroom.room_id = room_id
101+
102+
# Mock kernel manager
103+
mock_kernel_manager = Mock()
104+
mock_kernel_manager.kernel_client = mock_kernel_client
105+
session_manager.serverapp.kernel_manager.get_kernel.return_value = mock_kernel_manager
106+
107+
await session_manager._ensure_yroom_connected(session_id, kernel_id)
108+
109+
# Verify room_id was reconstructed
110+
session_manager.file_id_manager.index.assert_called_once_with(path)
111+
112+
# Verify room_id was cached
113+
assert session_manager._room_ids[session_id] == room_id
114+
115+
# Verify yroom was added to kernel client
116+
assert mock_yroom in mock_kernel_client._yrooms
117+
118+
@pytest.mark.asyncio
119+
async def test_skips_non_notebook_sessions(self, session_manager):
120+
"""Test that non-notebook sessions are skipped."""
121+
session_id = "session-123"
122+
kernel_id = "kernel-456"
123+
124+
# Mock get_session to return console session
125+
mock_session = {
126+
"id": session_id,
127+
"type": "console",
128+
"path": "/path/to/console"
129+
}
130+
131+
with patch.object(YDocSessionManager, 'get_session', new_callable=AsyncMock) as mock_get_session:
132+
mock_get_session.return_value = mock_session
133+
134+
await session_manager._ensure_yroom_connected(session_id, kernel_id)
135+
136+
# Verify no room_id was created
137+
assert session_id not in session_manager._room_ids
138+
139+
@pytest.mark.asyncio
140+
async def test_skips_when_yroom_already_connected(self, session_manager, mock_yroom, mock_kernel_client):
141+
"""Test that already-connected yrooms are not re-added."""
142+
session_id = "session-123"
143+
kernel_id = "kernel-456"
144+
room_id = "json:notebook:test-file-id"
145+
146+
# Set up cached room_id
147+
session_manager._room_ids[session_id] = room_id
148+
149+
# Mock yroom manager
150+
session_manager.yroom_manager.get_room.return_value = mock_yroom
151+
152+
# Yroom already in kernel client's _yrooms
153+
mock_kernel_client._yrooms.add(mock_yroom)
154+
155+
# Mock kernel manager
156+
mock_kernel_manager = Mock()
157+
mock_kernel_manager.kernel_client = mock_kernel_client
158+
session_manager.serverapp.kernel_manager.get_kernel.return_value = mock_kernel_manager
159+
160+
# Track initial state
161+
initial_yrooms_count = len(mock_kernel_client._yrooms)
162+
163+
await session_manager._ensure_yroom_connected(session_id, kernel_id)
164+
165+
# Verify yroom was not added again (count unchanged)
166+
assert len(mock_kernel_client._yrooms) == initial_yrooms_count
167+
168+
@pytest.mark.asyncio
169+
async def test_handles_missing_yroom_gracefully(self, session_manager):
170+
"""Test that missing yroom is handled gracefully without errors."""
171+
session_id = "session-123"
172+
kernel_id = "kernel-456"
173+
room_id = "json:notebook:missing-file-id"
174+
175+
# Set up cached room_id
176+
session_manager._room_ids[session_id] = room_id
177+
178+
# Mock yroom manager to return None (yroom doesn't exist)
179+
session_manager.yroom_manager.get_room.return_value = None
180+
181+
# Should not raise an error
182+
await session_manager._ensure_yroom_connected(session_id, kernel_id)
183+
184+
@pytest.mark.asyncio
185+
async def test_handles_kernel_client_without_yrooms_attribute(self, session_manager, mock_yroom):
186+
"""Test graceful handling when kernel client doesn't have _yrooms attribute."""
187+
session_id = "session-123"
188+
kernel_id = "kernel-456"
189+
room_id = "json:notebook:test-file-id"
190+
191+
# Set up cached room_id
192+
session_manager._room_ids[session_id] = room_id
193+
194+
# Mock yroom manager
195+
session_manager.yroom_manager.get_room.return_value = mock_yroom
196+
197+
# Mock kernel client WITHOUT _yrooms attribute
198+
mock_kernel_client = Mock(spec=[]) # Empty spec, no _yrooms
199+
200+
# Mock kernel manager
201+
mock_kernel_manager = Mock()
202+
mock_kernel_manager.kernel_client = mock_kernel_client
203+
session_manager.serverapp.kernel_manager.get_kernel.return_value = mock_kernel_manager
204+
205+
# Should not raise an error
206+
await session_manager._ensure_yroom_connected(session_id, kernel_id)
207+
208+
209+
class TestGetSession:
210+
"""Tests for get_session method override."""
211+
212+
@pytest.mark.asyncio
213+
async def test_calls_ensure_yroom_connected(self, session_manager):
214+
"""Test that get_session calls _ensure_yroom_connected for notebook sessions."""
215+
session_id = "session-123"
216+
kernel_id = "kernel-456"
217+
218+
mock_session = {
219+
"id": session_id,
220+
"kernel": {"id": kernel_id},
221+
"type": "notebook"
222+
}
223+
224+
with patch.object(YDocSessionManager, 'get_session', new_callable=AsyncMock) as mock_super_get_session:
225+
mock_super_get_session.return_value = mock_session
226+
227+
with patch.object(session_manager, '_ensure_yroom_connected', new_callable=AsyncMock) as mock_ensure:
228+
result = await session_manager.get_session(session_id=session_id)
229+
230+
# Verify _ensure_yroom_connected was called
231+
mock_ensure.assert_called_once_with(session_id, kernel_id)
232+
233+
# Verify session was returned
234+
assert result == mock_session
235+
236+
@pytest.mark.asyncio
237+
async def test_handles_none_session(self, session_manager):
238+
"""Test that get_session handles None session gracefully."""
239+
with patch.object(YDocSessionManager, 'get_session', new_callable=AsyncMock) as mock_super_get_session:
240+
mock_super_get_session.return_value = None
241+
242+
with patch.object(session_manager, '_ensure_yroom_connected', new_callable=AsyncMock) as mock_ensure:
243+
result = await session_manager.get_session(session_id="missing-session")
244+
245+
# Verify _ensure_yroom_connected was NOT called
246+
mock_ensure.assert_not_called()
247+
248+
# Verify None was returned
249+
assert result is None
250+
251+
@pytest.mark.asyncio
252+
async def test_handles_session_without_kernel(self, session_manager):
253+
"""Test that get_session handles sessions without kernel gracefully."""
254+
mock_session = {
255+
"id": "session-123",
256+
"kernel": None,
257+
"type": "notebook"
258+
}
259+
260+
with patch.object(YDocSessionManager, 'get_session', new_callable=AsyncMock) as mock_super_get_session:
261+
mock_super_get_session.return_value = mock_session
262+
263+
with patch.object(session_manager, '_ensure_yroom_connected', new_callable=AsyncMock) as mock_ensure:
264+
result = await session_manager.get_session(session_id="session-123")
265+
266+
# Verify _ensure_yroom_connected was NOT called
267+
mock_ensure.assert_not_called()
268+
269+
# Verify session was returned
270+
assert result == mock_session

0 commit comments

Comments
 (0)