@@ -26,6 +26,7 @@ async def test_create_session_returns_session_when_ping_succeeds():
2626
2727 mock_session = AsyncMock ()
2828 mock_session .send_ping = AsyncMock (return_value = None )
29+ mock_session .list_tools = AsyncMock (return_value = [])
2930
3031 with patch .object (
3132 KAgentMCPSessionManager .__bases__ [0 ],
@@ -37,6 +38,7 @@ async def test_create_session_returns_session_when_ping_succeeds():
3738
3839 assert result is mock_session
3940 mock_session .send_ping .assert_awaited_once ()
41+ mock_session .list_tools .assert_awaited_once ()
4042 parent_create .assert_awaited_once ()
4143
4244
@@ -49,6 +51,7 @@ async def test_create_session_invalidates_and_retries_when_ping_fails():
4951
5052 fresh_session = AsyncMock ()
5153 fresh_session .send_ping = AsyncMock (return_value = None )
54+ fresh_session .list_tools = AsyncMock (return_value = [])
5255
5356 call_count = 0
5457
@@ -100,6 +103,7 @@ async def _slow_ping():
100103
101104 fresh_session = AsyncMock ()
102105 fresh_session .send_ping = AsyncMock (return_value = None )
106+ fresh_session .list_tools = AsyncMock (return_value = [])
103107
104108 call_count = 0
105109
@@ -133,6 +137,7 @@ async def test_create_session_accepts_method_not_found_as_alive():
133137 mock_session .send_ping = AsyncMock (
134138 side_effect = McpError (error = ErrorData (code = - 32601 , message = "Method not found: ping" ))
135139 )
140+ mock_session .list_tools = AsyncMock (return_value = [])
136141
137142 with patch .object (
138143 KAgentMCPSessionManager .__bases__ [0 ],
@@ -143,9 +148,105 @@ async def test_create_session_accepts_method_not_found_as_alive():
143148 result = await mgr .create_session ()
144149
145150 assert result is mock_session
151+ mock_session .list_tools .assert_awaited_once ()
146152 parent_create .assert_awaited_once ()
147153
148154
155+ @pytest .mark .asyncio
156+ async def test_create_session_invalidates_when_list_tools_returns_session_terminated ():
157+ """After ping passes, list_tools is used to revalidate; 404/session terminated → prune and recreate."""
158+ mgr = _make_manager ()
159+
160+ stale_session = AsyncMock ()
161+ stale_session .send_ping = AsyncMock (return_value = None )
162+ stale_session .list_tools = AsyncMock (side_effect = Exception ("Session terminated" ))
163+
164+ fresh_session = AsyncMock ()
165+ fresh_session .send_ping = AsyncMock (return_value = None )
166+ fresh_session .list_tools = AsyncMock (return_value = [])
167+
168+ call_count = 0
169+
170+ async def _parent_create (headers = None ):
171+ nonlocal call_count
172+ call_count += 1
173+ if call_count == 1 :
174+ return stale_session
175+ return fresh_session
176+
177+ with (
178+ patch .object (
179+ KAgentMCPSessionManager .__bases__ [0 ],
180+ "create_session" ,
181+ side_effect = _parent_create ,
182+ ),
183+ patch .object (mgr , "close" , new_callable = AsyncMock ) as mock_close ,
184+ ):
185+ result = await mgr .create_session ()
186+
187+ assert result is fresh_session
188+ mock_close .assert_awaited_once ()
189+ assert call_count == 2
190+ stale_session .list_tools .assert_awaited_once ()
191+
192+
193+ @pytest .mark .asyncio
194+ async def test_create_session_invalidates_when_list_tools_returns_404 ():
195+ """list_tools returning 404 (session invalid) triggers prune and recreate."""
196+ mgr = _make_manager ()
197+
198+ stale_session = AsyncMock ()
199+ stale_session .send_ping = AsyncMock (return_value = None )
200+ stale_session .list_tools = AsyncMock (side_effect = McpError (error = ErrorData (code = - 32000 , message = "404 Not Found" )))
201+
202+ fresh_session = AsyncMock ()
203+ fresh_session .send_ping = AsyncMock (return_value = None )
204+ fresh_session .list_tools = AsyncMock (return_value = [])
205+
206+ call_count = 0
207+
208+ async def _parent_create (headers = None ):
209+ nonlocal call_count
210+ call_count += 1
211+ if call_count == 1 :
212+ return stale_session
213+ return fresh_session
214+
215+ with (
216+ patch .object (
217+ KAgentMCPSessionManager .__bases__ [0 ],
218+ "create_session" ,
219+ side_effect = _parent_create ,
220+ ),
221+ patch .object (mgr , "close" , new_callable = AsyncMock ) as mock_close ,
222+ ):
223+ result = await mgr .create_session ()
224+
225+ assert result is fresh_session
226+ mock_close .assert_awaited_once ()
227+ assert call_count == 2
228+ stale_session .list_tools .assert_awaited_once ()
229+
230+
231+ @pytest .mark .asyncio
232+ async def test_create_session_propagates_non_session_error_from_list_tools ():
233+ """Transient errors (e.g. timeout) from list_tools are propagated, not treated as session invalid."""
234+ mgr = _make_manager ()
235+
236+ mock_session = AsyncMock ()
237+ mock_session .send_ping = AsyncMock (return_value = None )
238+ mock_session .list_tools = AsyncMock (side_effect = asyncio .TimeoutError ())
239+
240+ with patch .object (
241+ KAgentMCPSessionManager .__bases__ [0 ],
242+ "create_session" ,
243+ new_callable = AsyncMock ,
244+ return_value = mock_session ,
245+ ):
246+ with pytest .raises (asyncio .TimeoutError ):
247+ await mgr .create_session ()
248+
249+
149250@pytest .mark .asyncio
150251async def test_create_session_recovers_even_when_close_raises ():
151252 """Recovery must succeed even if close() raises during stale session teardown."""
@@ -155,6 +256,8 @@ async def test_create_session_recovers_even_when_close_raises():
155256 stale_session .send_ping = AsyncMock (side_effect = Exception ("Session terminated" ))
156257
157258 fresh_session = AsyncMock ()
259+ fresh_session .send_ping = AsyncMock (return_value = None )
260+ fresh_session .list_tools = AsyncMock (return_value = [])
158261
159262 call_count = 0
160263
0 commit comments