@@ -476,3 +476,262 @@ def test_run_registration_with_grpc_source(self) -> None:
476476 mock_process .assert_called_once ()
477477 call_args = mock_process .call_args [1 ]
478478 assert call_args .get ("target" ) is mock_translate
479+
480+ def test_run_register_without_source_warns (self ) -> None :
481+ """Test that register=True without stdio or grpc prints a warning."""
482+ with (
483+ patch ("mcpgateway.translate.main" ),
484+ patch ("multiprocessing.Process" ) as mock_process ,
485+ patch ("cforge.commands.server.run.get_console" ) as mock_console ,
486+ patch ("cforge.commands.server.run.make_authenticated_request" ) as mock_request ,
487+ ):
488+ mock_console_instance = MagicMock ()
489+ mock_console .return_value = mock_console_instance
490+
491+ # No stdio or grpc, but register=True
492+ invoke_typer_command (run , register = True )
493+
494+ # Verify warning was printed about needing stdio or grpc
495+ assert any ("Warning" in str (call ) and "register" in str (call ).lower () for call in mock_console_instance .print .call_args_list )
496+
497+ # Verify registration was NOT attempted (since it was disabled)
498+ mock_request .assert_not_called ()
499+
500+ # Verify translate_main was still called via Process
501+ mock_process .assert_called_once ()
502+
503+ def test_run_health_check_connection_error_retry (self ) -> None :
504+ """Test that health check retries on connection errors."""
505+ import requests as real_requests
506+
507+ with (
508+ patch ("mcpgateway.translate.main" ),
509+ patch ("multiprocessing.Process" ) as mock_process ,
510+ patch ("cforge.commands.server.run.requests" ) as mock_requests ,
511+ patch ("cforge.commands.server.run.make_authenticated_request" ) as mock_request ,
512+ patch ("cforge.commands.server.run.time" ) as mock_time ,
513+ ):
514+ # First call raises ConnectionError, second succeeds
515+ mock_get_res = MagicMock ()
516+ mock_get_res .status_code = 200
517+ mock_requests .get = MagicMock (
518+ side_effect = [
519+ real_requests .exceptions .ConnectionError ("Connection refused" ),
520+ mock_get_res ,
521+ ]
522+ )
523+ mock_requests .exceptions = real_requests .exceptions
524+
525+ # Mock time to control the loop
526+ mock_time .time = MagicMock (side_effect = [0 , 0.5 , 1 ])
527+ mock_time .sleep = MagicMock ()
528+
529+ mock_request .return_value = {"id" : "test-server-id" }
530+
531+ invoke_typer_command (run , stdio = "uvx mcp-server-git" , port = 9000 , register = True )
532+
533+ # Verify health check was retried
534+ assert mock_requests .get .call_count == 2
535+
536+ # Verify sleep was called after connection error
537+ mock_time .sleep .assert_called_once_with (0.5 )
538+
539+ # Verify registration succeeded after retry
540+ mock_request .assert_called_once ()
541+ mock_process .assert_called_once ()
542+
543+ def test_run_health_check_timeout (self ) -> None :
544+ """Test that health check timeout exits with error."""
545+ import requests as real_requests
546+
547+ import typer
548+
549+ with (
550+ patch ("mcpgateway.translate.main" ),
551+ patch ("multiprocessing.Process" ),
552+ patch ("cforge.commands.server.run.requests" ) as mock_requests ,
553+ patch ("cforge.commands.server.run.make_authenticated_request" ) as mock_request ,
554+ patch ("cforge.commands.server.run.time" ) as mock_time ,
555+ patch ("cforge.commands.server.run.get_console" ) as mock_console ,
556+ patch ("cforge.commands.server.run.typer.exit" , side_effect = typer .Exit (1 )) as mock_exit ,
557+ ):
558+ # Always raise ConnectionError
559+ mock_requests .get = MagicMock (side_effect = real_requests .exceptions .ConnectionError ("Connection refused" ))
560+ mock_requests .exceptions = real_requests .exceptions
561+
562+ # Mock time to simulate timeout
563+ mock_time .time = MagicMock (side_effect = [0 , 5 , 11 ]) # Start, after first try, after timeout
564+ mock_time .sleep = MagicMock ()
565+
566+ mock_console_instance = MagicMock ()
567+ mock_console .return_value = mock_console_instance
568+
569+ invoke_typer_command (run , stdio = "uvx mcp-server-git" , port = 9000 , register = True , register_timeout = 10.0 )
570+
571+ # Verify timeout error message was printed
572+ assert any ("Failed to connect" in str (call ) for call in mock_console_instance .print .call_args_list )
573+
574+ # Verify typer.exit was called with error code
575+ mock_exit .assert_called_once_with (1 )
576+
577+ # Registration should not have been attempted
578+ mock_request .assert_not_called ()
579+
580+ def test_run_temporary_cleanup_success (self ) -> None :
581+ """Test that temporary server cleanup function works correctly."""
582+ with (
583+ patch ("mcpgateway.translate.main" ),
584+ patch ("multiprocessing.Process" ),
585+ patch ("cforge.commands.server.run.requests" ) as mock_requests ,
586+ patch ("cforge.commands.server.run.make_authenticated_request" ) as mock_request ,
587+ patch ("cforge.commands.server.run.atexit" ) as mock_atexit ,
588+ patch ("cforge.commands.server.run.get_console" ) as mock_console ,
589+ ):
590+ # Mock returning a 200 on health
591+ mock_get_res = MagicMock ()
592+ mock_get_res .status_code = 200
593+ mock_requests .get = MagicMock (return_value = mock_get_res )
594+
595+ mock_request .return_value = {"id" : "temp-server-id" , "name" : "temp-server" }
596+
597+ mock_console_instance = MagicMock ()
598+ mock_console .return_value = mock_console_instance
599+
600+ invoke_typer_command (run , stdio = "uvx mcp-server-git" , port = 9000 , temporary = True )
601+
602+ # Verify cleanup handler was registered
603+ mock_atexit .register .assert_called_once ()
604+
605+ # Get the cleanup function and call it
606+ cleanup_fn = mock_atexit .register .call_args [0 ][0 ]
607+
608+ # Reset mock_request to track cleanup call
609+ mock_request .reset_mock ()
610+
611+ # Call cleanup function
612+ cleanup_fn ()
613+
614+ # Verify unregistration was attempted
615+ mock_request .assert_called_once ()
616+ call_args = mock_request .call_args
617+ assert call_args [0 ][0 ] == "DELETE"
618+ assert "/gateways/temp-server-id" in call_args [0 ][1 ]
619+
620+ def test_run_temporary_cleanup_failure (self ) -> None :
621+ """Test that temporary server cleanup handles errors gracefully."""
622+ with (
623+ patch ("mcpgateway.translate.main" ),
624+ patch ("multiprocessing.Process" ),
625+ patch ("cforge.commands.server.run.requests" ) as mock_requests ,
626+ patch ("cforge.commands.server.run.make_authenticated_request" ) as mock_request ,
627+ patch ("cforge.commands.server.run.atexit" ) as mock_atexit ,
628+ patch ("cforge.commands.server.run.get_console" ) as mock_console ,
629+ ):
630+ # Mock returning a 200 on health
631+ mock_get_res = MagicMock ()
632+ mock_get_res .status_code = 200
633+ mock_requests .get = MagicMock (return_value = mock_get_res )
634+
635+ mock_request .return_value = {"id" : "temp-server-id" , "name" : "temp-server" }
636+
637+ mock_console_instance = MagicMock ()
638+ mock_console .return_value = mock_console_instance
639+
640+ invoke_typer_command (run , stdio = "uvx mcp-server-git" , port = 9000 , temporary = True )
641+
642+ # Get the cleanup function
643+ cleanup_fn = mock_atexit .register .call_args [0 ][0 ]
644+
645+ # Make the DELETE request fail
646+ mock_request .reset_mock ()
647+ mock_request .side_effect = Exception ("Network error" )
648+
649+ # Call cleanup function - should not raise
650+ cleanup_fn ()
651+
652+ # Verify warning was printed
653+ assert any ("Warning" in str (call ) and "unregister" in str (call ).lower () for call in mock_console_instance .print .call_args_list )
654+
655+ def test_run_health_check_retries_on_non_200 (self ) -> None :
656+ """Test that health check retries when server returns non-200 status."""
657+ with (
658+ patch ("mcpgateway.translate.main" ),
659+ patch ("multiprocessing.Process" ) as mock_process ,
660+ patch ("cforge.commands.server.run.requests" ) as mock_requests ,
661+ patch ("cforge.commands.server.run.make_authenticated_request" ) as mock_request ,
662+ patch ("cforge.commands.server.run.time" ) as mock_time ,
663+ ):
664+ # First call returns 503, second returns 200
665+ mock_get_res_503 = MagicMock ()
666+ mock_get_res_503 .status_code = 503
667+ mock_get_res_200 = MagicMock ()
668+ mock_get_res_200 .status_code = 200
669+ mock_requests .get = MagicMock (side_effect = [mock_get_res_503 , mock_get_res_200 ])
670+
671+ # Mock time to control the loop
672+ mock_time .time = MagicMock (side_effect = [0 , 0.5 , 1 ])
673+ mock_time .sleep = MagicMock ()
674+
675+ mock_request .return_value = {"id" : "test-server-id" }
676+
677+ invoke_typer_command (run , stdio = "uvx mcp-server-git" , port = 9000 , register = True )
678+
679+ # Verify health check was retried (called twice)
680+ assert mock_requests .get .call_count == 2
681+
682+ # Verify registration succeeded
683+ mock_request .assert_called_once ()
684+ mock_process .assert_called_once ()
685+
686+ def test_run_registration_name_fallback_for_filtered_command (self ) -> None :
687+ """Test that server name falls back to stdio-server when all parts are filtered."""
688+ with (
689+ patch ("mcpgateway.translate.main" ),
690+ patch ("multiprocessing.Process" ) as mock_process ,
691+ patch ("cforge.commands.server.run.requests" ) as mock_requests ,
692+ patch ("cforge.commands.server.run.make_authenticated_request" ) as mock_request ,
693+ ):
694+ # Mock returning a 200 on health
695+ mock_get_res = MagicMock ()
696+ mock_get_res .status_code = 200
697+ mock_requests .get = MagicMock (return_value = mock_get_res )
698+
699+ mock_request .return_value = {"id" : "test-server-id" }
700+
701+ # Use a command where all parts get filtered (uvx, python, node, etc.)
702+ invoke_typer_command (run , stdio = "uvx python node" , port = 9000 , register = True )
703+
704+ # Verify name falls back to stdio-server-{port}
705+ call_args = mock_request .call_args
706+ json_data = call_args [1 ]["json_data" ]
707+ assert json_data ["name" ] == "stdio-server-9000"
708+
709+ mock_process .assert_called_once ()
710+
711+ def test_run_temporary_without_source_uses_fallback_name (self ) -> None :
712+ """Test that temporary registration without stdio/grpc uses fallback server name."""
713+ with (
714+ patch ("mcpgateway.translate.main" ),
715+ patch ("multiprocessing.Process" ) as mock_process ,
716+ patch ("cforge.commands.server.run.requests" ) as mock_requests ,
717+ patch ("cforge.commands.server.run.make_authenticated_request" ) as mock_request ,
718+ patch ("cforge.commands.server.run.atexit" ) as mock_atexit ,
719+ ):
720+ # Mock returning a 200 on health
721+ mock_get_res = MagicMock ()
722+ mock_get_res .status_code = 200
723+ mock_requests .get = MagicMock (return_value = mock_get_res )
724+
725+ mock_request .return_value = {"id" : "temp-server-id" }
726+
727+ # temporary=True bypasses the stdio/grpc check, allowing registration without source
728+ invoke_typer_command (run , port = 9000 , temporary = True )
729+
730+ # Verify fallback name server-{port} was used
731+ call_args = mock_request .call_args
732+ json_data = call_args [1 ]["json_data" ]
733+ assert json_data ["name" ] == "server-9000"
734+
735+ # Verify cleanup was registered
736+ mock_atexit .register .assert_called_once ()
737+ mock_process .assert_called_once ()
0 commit comments