11"""Tests for the ping command."""
22
33import pytest
4- from unittest .mock import Mock , patch
4+ from unittest .mock import Mock , AsyncMock , patch , PropertyMock
55from mcp_cli .commands .servers .ping import PingCommand
66
77
@@ -33,7 +33,6 @@ def test_command_properties(self, command):
3333 async def test_execute_all_servers (self , command ):
3434 """Test pinging all servers."""
3535 from mcp_cli .tools .models import ServerInfo
36- from unittest .mock import AsyncMock
3736
3837 mock_tm = Mock ()
3938 mock_server = ServerInfo (
@@ -45,6 +44,9 @@ async def test_execute_all_servers(self, command):
4544 namespace = "test" ,
4645 )
4746 mock_tm .get_server_info = AsyncMock (return_value = [mock_server ])
47+ mock_tm .check_server_health = AsyncMock (return_value = {
48+ "test-server" : {"status" : "healthy" , "ping_success" : True },
49+ })
4850
4951 with patch ("chuk_term.ui.output" ):
5052 result = await command .execute (tool_manager = mock_tm )
@@ -54,7 +56,6 @@ async def test_execute_all_servers(self, command):
5456 async def test_execute_specific_server (self , command ):
5557 """Test pinging a specific server."""
5658 from mcp_cli .tools .models import ServerInfo
57- from unittest .mock import AsyncMock
5859
5960 mock_tm = Mock ()
6061 mock_server = ServerInfo (
@@ -66,6 +67,9 @@ async def test_execute_specific_server(self, command):
6667 namespace = "test" ,
6768 )
6869 mock_tm .get_server_info = AsyncMock (return_value = [mock_server ])
70+ mock_tm .check_server_health = AsyncMock (return_value = {
71+ "test-server" : {"status" : "healthy" , "ping_success" : True },
72+ })
6973
7074 with patch ("chuk_term.ui.output" ):
7175 result = await command .execute (tool_manager = mock_tm , server_index = 0 )
@@ -81,9 +85,8 @@ async def test_execute_no_tool_manager(self, command):
8185
8286 @pytest .mark .asyncio
8387 async def test_execute_failed_ping (self , command ):
84- """Test when server is disconnected ."""
88+ """Test when server ping fails (transport-level) ."""
8589 from mcp_cli .tools .models import ServerInfo
86- from unittest .mock import AsyncMock
8790
8891 mock_tm = Mock ()
8992 mock_server = ServerInfo (
@@ -95,6 +98,9 @@ async def test_execute_failed_ping(self, command):
9598 namespace = "test" ,
9699 )
97100 mock_tm .get_server_info = AsyncMock (return_value = [mock_server ])
101+ mock_tm .check_server_health = AsyncMock (return_value = {
102+ "test-server" : {"status" : "unhealthy" , "ping_success" : False },
103+ })
98104
99105 with patch ("chuk_term.ui.output" ):
100106 result = await command .execute (tool_manager = mock_tm )
@@ -103,8 +109,6 @@ async def test_execute_failed_ping(self, command):
103109 @pytest .mark .asyncio
104110 async def test_execute_error_handling (self , command ):
105111 """Test error handling during ping."""
106- from unittest .mock import AsyncMock
107-
108112 mock_tm = Mock ()
109113 mock_tm .get_server_info = AsyncMock (side_effect = Exception ("Network error" ))
110114
@@ -130,7 +134,6 @@ async def test_execute_with_context_exception(self, command):
130134 async def test_execute_with_args_list (self , command ):
131135 """Test executing with args as list."""
132136 from mcp_cli .tools .models import ServerInfo
133- from unittest .mock import AsyncMock
134137
135138 mock_tm = Mock ()
136139 mock_server1 = ServerInfo (
@@ -150,6 +153,10 @@ async def test_execute_with_args_list(self, command):
150153 namespace = "test" ,
151154 )
152155 mock_tm .get_server_info = AsyncMock (return_value = [mock_server1 , mock_server2 ])
156+ mock_tm .check_server_health = AsyncMock (return_value = {
157+ "server1" : {"status" : "healthy" , "ping_success" : True },
158+ "server2" : {"status" : "healthy" , "ping_success" : True },
159+ })
153160
154161 with patch ("chuk_term.ui.output" ):
155162 # Pass args as a list
@@ -162,7 +169,6 @@ async def test_execute_with_args_list(self, command):
162169 async def test_execute_with_args_string (self , command ):
163170 """Test executing with args as string."""
164171 from mcp_cli .tools .models import ServerInfo
165- from unittest .mock import AsyncMock
166172
167173 mock_tm = Mock ()
168174 mock_server = ServerInfo (
@@ -174,6 +180,9 @@ async def test_execute_with_args_string(self, command):
174180 namespace = "test" ,
175181 )
176182 mock_tm .get_server_info = AsyncMock (return_value = [mock_server ])
183+ mock_tm .check_server_health = AsyncMock (return_value = {
184+ "server1" : {"status" : "healthy" , "ping_success" : True },
185+ })
177186
178187 with patch ("chuk_term.ui.output" ):
179188 # Pass args as a string
@@ -183,8 +192,6 @@ async def test_execute_with_args_string(self, command):
183192 @pytest .mark .asyncio
184193 async def test_execute_no_servers (self , command ):
185194 """Test when no servers are available."""
186- from unittest .mock import AsyncMock
187-
188195 mock_tm = Mock ()
189196 mock_tm .get_server_info = AsyncMock (return_value = [])
190197
@@ -197,7 +204,6 @@ async def test_execute_no_servers(self, command):
197204 async def test_execute_with_context_success (self , command ):
198205 """Test getting tool manager from context successfully."""
199206 from mcp_cli .tools .models import ServerInfo
200- from unittest .mock import AsyncMock
201207
202208 mock_server = ServerInfo (
203209 id = 1 ,
@@ -210,6 +216,9 @@ async def test_execute_with_context_success(self, command):
210216
211217 mock_tm = Mock ()
212218 mock_tm .get_server_info = AsyncMock (return_value = [mock_server ])
219+ mock_tm .check_server_health = AsyncMock (return_value = {
220+ "test-server" : {"status" : "healthy" , "ping_success" : True },
221+ })
213222
214223 mock_ctx = Mock ()
215224 mock_ctx .tool_manager = mock_tm
@@ -224,7 +233,6 @@ async def test_execute_with_context_success(self, command):
224233 async def test_execute_filter_by_index (self , command ):
225234 """Test filtering servers by index."""
226235 from mcp_cli .tools .models import ServerInfo
227- from unittest .mock import AsyncMock
228236
229237 mock_tm = Mock ()
230238 mock_server1 = ServerInfo (
@@ -244,6 +252,10 @@ async def test_execute_filter_by_index(self, command):
244252 namespace = "test" ,
245253 )
246254 mock_tm .get_server_info = AsyncMock (return_value = [mock_server1 , mock_server2 ])
255+ mock_tm .check_server_health = AsyncMock (return_value = {
256+ "server1" : {"status" : "healthy" , "ping_success" : True },
257+ "server2" : {"status" : "healthy" , "ping_success" : True },
258+ })
247259
248260 with patch ("chuk_term.ui.output" ):
249261 # Filter by index "0" - should only match first server
@@ -263,21 +275,126 @@ async def test_execute_context_returns_none(self, command):
263275
264276 @pytest .mark .asyncio
265277 async def test_execute_server_ping_exception (self , command ):
266- """Test when accessing server.connected raises an exception."""
267- from unittest . mock import AsyncMock , PropertyMock
278+ """Test when accessing health check raises an exception for a server ."""
279+ from mcp_cli . tools . models import ServerInfo
268280
269281 mock_tm = Mock ()
270-
271- # Create a mock server that raises exception when connected is accessed
272- mock_server = Mock ()
273- mock_server .name = "test-server"
274- type(mock_server ).connected = PropertyMock (
275- side_effect = Exception ("Connection check failed" )
282+ mock_server = ServerInfo (
283+ id = 1 ,
284+ name = "test-server" ,
285+ status = "running" ,
286+ connected = True ,
287+ tool_count = 5 ,
288+ namespace = "test" ,
276289 )
290+ mock_tm .get_server_info = AsyncMock (return_value = [mock_server ])
291+ # Return empty health dict so .get() works but returns no ping_success
292+ mock_tm .check_server_health = AsyncMock (return_value = {})
293+
294+ with patch ("chuk_term.ui.output" ):
295+ result = await command .execute (tool_manager = mock_tm )
296+ # Should fail because the server didn't respond to ping
297+ assert result .success is False
277298
299+ @pytest .mark .asyncio
300+ async def test_execute_sse_server_ping (self , command ):
301+ """Test that ping works for SSE servers (issue #203).
302+
303+ SSE transports don't expose raw streams, so ping must use
304+ the transport-level health check instead.
305+ """
306+ from mcp_cli .tools .models import ServerInfo , TransportType
307+
308+ mock_tm = Mock ()
309+ mock_server = ServerInfo (
310+ id = 0 ,
311+ name = "sse-echo" ,
312+ status = "running" ,
313+ connected = True ,
314+ tool_count = 3 ,
315+ namespace = "sse-echo" ,
316+ transport = TransportType .SSE ,
317+ url = "http://localhost:8081/mcp" ,
318+ )
278319 mock_tm .get_server_info = AsyncMock (return_value = [mock_server ])
320+ mock_tm .check_server_health = AsyncMock (return_value = {
321+ "sse-echo" : {"status" : "healthy" , "ping_success" : True },
322+ })
279323
280324 with patch ("chuk_term.ui.output" ):
281325 result = await command .execute (tool_manager = mock_tm )
282- # Should fail because the server ping raised an exception
326+ assert result .success is True
327+ # Verify check_server_health was called (not just server.connected)
328+ mock_tm .check_server_health .assert_awaited_once ()
329+
330+ @pytest .mark .asyncio
331+ async def test_execute_mixed_transport_servers (self , command ):
332+ """Test ping with both stdio and SSE servers."""
333+ from mcp_cli .tools .models import ServerInfo , TransportType
334+
335+ mock_tm = Mock ()
336+ stdio_server = ServerInfo (
337+ id = 0 ,
338+ name = "sqlite" ,
339+ status = "running" ,
340+ connected = True ,
341+ tool_count = 5 ,
342+ namespace = "sqlite" ,
343+ transport = TransportType .STDIO ,
344+ )
345+ sse_server = ServerInfo (
346+ id = 1 ,
347+ name = "sse-echo" ,
348+ status = "running" ,
349+ connected = True ,
350+ tool_count = 3 ,
351+ namespace = "sse-echo" ,
352+ transport = TransportType .SSE ,
353+ url = "http://localhost:8081/mcp" ,
354+ )
355+ mock_tm .get_server_info = AsyncMock (
356+ return_value = [stdio_server , sse_server ]
357+ )
358+ mock_tm .check_server_health = AsyncMock (return_value = {
359+ "sqlite" : {"status" : "healthy" , "ping_success" : True },
360+ "sse-echo" : {"status" : "healthy" , "ping_success" : True },
361+ })
362+
363+ with patch ("chuk_term.ui.output" ):
364+ result = await command .execute (tool_manager = mock_tm )
365+ assert result .success is True
366+
367+ @pytest .mark .asyncio
368+ async def test_online_count_reported (self , command ):
369+ """Test that online/total count is reported."""
370+ from mcp_cli .tools .models import ServerInfo
371+
372+ mock_tm = Mock ()
373+ servers = [
374+ ServerInfo (
375+ id = i ,
376+ name = f"server{ i } " ,
377+ status = "running" ,
378+ connected = True ,
379+ tool_count = 1 ,
380+ namespace = "test" ,
381+ )
382+ for i in range (3 )
383+ ]
384+ mock_tm .get_server_info = AsyncMock (return_value = servers )
385+ mock_tm .check_server_health = AsyncMock (return_value = {
386+ "server0" : {"status" : "healthy" , "ping_success" : True },
387+ "server1" : {"status" : "unhealthy" , "ping_success" : False },
388+ "server2" : {"status" : "healthy" , "ping_success" : True },
389+ })
390+
391+ info_calls = []
392+ with patch ("chuk_term.ui.output" ) as mock_output :
393+ mock_output .info = lambda msg : info_calls .append (msg )
394+ mock_output .success = lambda msg : None
395+ mock_output .error = lambda msg : None
396+ result = await command .execute (tool_manager = mock_tm )
397+ # One server failed, so overall success is False
283398 assert result .success is False
399+ # Check that the summary line was emitted
400+ assert any ("2/3 servers online" in call for call in info_calls )
0 commit comments