@@ -792,6 +792,106 @@ def test_credit_invalid_value_errors(self, mock_which, tmp_path, monkeypatch):
792792 assert "true or false" in result .output .lower ()
793793
794794
795+ class TestMainCallback :
796+ def test_no_subcommand_prints_banner_and_help (self ):
797+ result = runner .invoke (app , [])
798+ assert result .exit_code == 0
799+ # Banner contains the ASCII art — check a recognizable fragment
800+ assert "RALPHIFY" in result .output .upper () or "ralph" in result .output .lower ()
801+ # Help text should be present
802+ assert "run" in result .output .lower ()
803+
804+ def test_no_subcommand_shows_tagline (self ):
805+ result = runner .invoke (app , [])
806+ assert result .exit_code == 0
807+ assert "Ralph is always running" in result .output
808+
809+
810+ class TestAdd :
811+ @patch ("ralphify._source.fetch_ralphs" )
812+ @patch ("ralphify._source.parse_github_source" )
813+ def test_add_single_ralph (self , mock_parse , mock_fetch , tmp_path , monkeypatch ):
814+ monkeypatch .chdir (tmp_path )
815+ from ralphify ._source import ParsedSource , FetchResult
816+ parsed = ParsedSource (
817+ repo_url = "https://github.com/owner/repo.git" ,
818+ subpath = "my-ralph" ,
819+ handle = "owner/repo/my-ralph" ,
820+ name = "my-ralph" ,
821+ )
822+ mock_parse .return_value = parsed
823+ dest = tmp_path / ".ralphify" / "ralphs" / "my-ralph"
824+ mock_fetch .return_value = FetchResult (installed = [("my-ralph" , dest )])
825+
826+ result = runner .invoke (app , ["add" , "owner/repo/my-ralph" ])
827+ assert result .exit_code == 0
828+ assert "Added" in result .output
829+ assert "my-ralph" in result .output
830+ assert "ralph run my-ralph" in result .output
831+
832+ @patch ("ralphify._source.fetch_ralphs" )
833+ @patch ("ralphify._source.parse_github_source" )
834+ def test_add_multiple_ralphs (self , mock_parse , mock_fetch , tmp_path , monkeypatch ):
835+ monkeypatch .chdir (tmp_path )
836+ from ralphify ._source import ParsedSource , FetchResult
837+ parsed = ParsedSource (
838+ repo_url = "https://github.com/owner/repo.git" ,
839+ subpath = None ,
840+ handle = "owner/repo" ,
841+ name = "repo" ,
842+ )
843+ mock_parse .return_value = parsed
844+ mock_fetch .return_value = FetchResult (installed = [
845+ ("ralph-a" , tmp_path / "a" ),
846+ ("ralph-b" , tmp_path / "b" ),
847+ ])
848+
849+ result = runner .invoke (app , ["add" , "owner/repo" ])
850+ assert result .exit_code == 0
851+ assert "Added 2 ralphs" in result .output
852+ assert "ralph-a" in result .output
853+ assert "ralph-b" in result .output
854+ assert "ralph run <name>" in result .output
855+
856+ @patch ("ralphify._source.parse_github_source" , side_effect = ValueError ("Cannot parse source 'bad'" ))
857+ def test_add_invalid_source_errors (self , mock_parse , tmp_path , monkeypatch ):
858+ monkeypatch .chdir (tmp_path )
859+ result = runner .invoke (app , ["add" , "bad" ])
860+ assert result .exit_code == 1
861+ assert "Cannot parse source" in result .output
862+
863+ @patch ("ralphify._source.fetch_ralphs" , side_effect = RuntimeError ("git clone failed" ))
864+ @patch ("ralphify._source.parse_github_source" )
865+ def test_add_fetch_failure_errors (self , mock_parse , mock_fetch , tmp_path , monkeypatch ):
866+ monkeypatch .chdir (tmp_path )
867+ from ralphify ._source import ParsedSource
868+ mock_parse .return_value = ParsedSource (
869+ repo_url = "https://github.com/owner/repo.git" ,
870+ subpath = None ,
871+ handle = "owner/repo" ,
872+ name = "repo" ,
873+ )
874+ result = runner .invoke (app , ["add" , "owner/repo" ])
875+ assert result .exit_code == 1
876+ assert "git clone failed" in result .output
877+
878+ @patch ("ralphify._source.fetch_ralphs" )
879+ @patch ("ralphify._source.parse_github_source" )
880+ def test_add_creates_ralphs_dir (self , mock_parse , mock_fetch , tmp_path , monkeypatch ):
881+ monkeypatch .chdir (tmp_path )
882+ from ralphify ._source import ParsedSource , FetchResult
883+ mock_parse .return_value = ParsedSource (
884+ repo_url = "https://github.com/owner/repo.git" ,
885+ subpath = "x" ,
886+ handle = "owner/repo/x" ,
887+ name = "x" ,
888+ )
889+ mock_fetch .return_value = FetchResult (installed = [("x" , tmp_path / "x" )])
890+ result = runner .invoke (app , ["add" , "owner/repo/x" ])
891+ assert result .exit_code == 0
892+ assert (tmp_path / ".ralphify" / "ralphs" ).is_dir ()
893+
894+
795895class TestTwoStageCtrlC :
796896 """Test the two-stage Ctrl+C signal handler installed by the run command."""
797897
0 commit comments