@@ -785,3 +785,128 @@ def test_bulk_upgrade_with_failures(
785785
786786 assert len (result .upgrades ) == 1
787787 assert len (result .failures ) == 1
788+
789+
790+ class TestAptPackageNameResolver :
791+ """Tests for #35: apt package name resolution from catalog."""
792+
793+ def test_resolve_apt_package_name_fd (self ):
794+ """resolve_apt_package_name('fd') should return 'fd-find'."""
795+ from cli_audit .catalog import resolve_apt_package_name
796+ assert resolve_apt_package_name ("fd" ) == "fd-find"
797+
798+ def test_resolve_apt_package_name_bat (self ):
799+ """resolve_apt_package_name('bat') should return 'bat'."""
800+ from cli_audit .catalog import resolve_apt_package_name
801+ assert resolve_apt_package_name ("bat" ) == "bat"
802+
803+ def test_resolve_apt_package_name_delta (self ):
804+ """resolve_apt_package_name('delta') should return 'git-delta'."""
805+ from cli_audit .catalog import resolve_apt_package_name
806+ assert resolve_apt_package_name ("delta" ) == "git-delta"
807+
808+ def test_resolve_apt_package_name_unknown_returns_tool_name (self ):
809+ """Tools without apt mapping should fall back to tool name."""
810+ from cli_audit .catalog import resolve_apt_package_name
811+ assert resolve_apt_package_name ("nonexistent_tool_xyz" ) == "nonexistent_tool_xyz"
812+
813+ def test_resolve_apt_package_name_ripgrep (self ):
814+ """ripgrep has apt config with package 'ripgrep'."""
815+ from cli_audit .catalog import resolve_apt_package_name
816+ assert resolve_apt_package_name ("ripgrep" ) == "ripgrep"
817+
818+ def test_resolve_apt_package_name_with_legacy_packages_field (self ):
819+ """Tools using legacy 'packages' field should resolve correctly."""
820+ import json
821+ from cli_audit .catalog import resolve_apt_package_name
822+
823+ fake_json = json .dumps ({
824+ "name" : "legacy_tool" ,
825+ "packages" : {"apt" : "legacy-apt-pkg" },
826+ })
827+ with patch ("builtins.open" , mock_open (read_data = fake_json )):
828+ with patch ("pathlib.Path.exists" , return_value = True ):
829+ result = resolve_apt_package_name ("legacy_tool" )
830+ assert result == "legacy-apt-pkg"
831+
832+ def test_resolve_apt_package_name_tool_without_apt_method (self ):
833+ """Tools with available_methods but no apt method should fall back."""
834+ from cli_audit .catalog import resolve_apt_package_name
835+ # tokei has cargo method but no apt method
836+ result = resolve_apt_package_name ("tokei" )
837+ # Should fall back - check it doesn't crash at minimum
838+ assert isinstance (result , str )
839+
840+ def test_resolve_apt_package_name_with_malformed_json (self ):
841+ """Malformed catalog JSON should fall back to tool name."""
842+ from cli_audit .catalog import resolve_apt_package_name
843+
844+ with patch ("builtins.open" , mock_open (read_data = "not valid json{{{" )):
845+ with patch ("pathlib.Path.exists" , return_value = True ):
846+ result = resolve_apt_package_name ("broken_tool" )
847+ assert result == "broken_tool"
848+
849+
850+ class TestCheckUpgradeAvailableAptResolved :
851+ """Tests for #35: apt-cache policy uses resolved package name."""
852+
853+ @patch ("cli_audit.upgrade.subprocess.run" )
854+ def test_get_available_version_apt_uses_resolved_name (self , mock_run ):
855+ """get_available_version for apt must use resolve_apt_package_name."""
856+ clear_version_cache ()
857+
858+ mock_run .return_value = MagicMock (
859+ returncode = 0 ,
860+ stdout = "fd-find:\n Installed: 8.7.0-1\n Candidate: 9.0.0-1\n " ,
861+ )
862+
863+ version = get_available_version ("fd" , "apt" )
864+
865+ # Verify subprocess was called with the resolved package name
866+ mock_run .assert_called_once ()
867+ call_args = mock_run .call_args [0 ][0 ]
868+ assert call_args == ["apt-cache" , "policy" , "fd-find" ], (
869+ f"apt-cache policy should use resolved name 'fd-find', got: { call_args } "
870+ )
871+ assert version == "9.0.0"
872+
873+ clear_version_cache ()
874+
875+ @patch ("cli_audit.upgrade.subprocess.run" )
876+ def test_get_available_version_apt_delta_uses_git_delta (self , mock_run ):
877+ """get_available_version for delta/apt must use 'git-delta'."""
878+ clear_version_cache ()
879+
880+ mock_run .return_value = MagicMock (
881+ returncode = 0 ,
882+ stdout = "git-delta:\n Installed: (none)\n Candidate: 0.16.5-1\n " ,
883+ )
884+
885+ version = get_available_version ("delta" , "apt" )
886+
887+ call_args = mock_run .call_args [0 ][0 ]
888+ assert call_args == ["apt-cache" , "policy" , "git-delta" ], (
889+ f"apt-cache policy should use resolved name 'git-delta', got: { call_args } "
890+ )
891+ assert version == "0.16.5"
892+
893+ clear_version_cache ()
894+
895+ @patch ("cli_audit.upgrade.subprocess.run" )
896+ def test_get_available_version_apt_ripgrep_unchanged (self , mock_run ):
897+ """get_available_version for ripgrep/apt should still use 'ripgrep'."""
898+ clear_version_cache ()
899+
900+ mock_run .return_value = MagicMock (
901+ returncode = 0 ,
902+ stdout = "ripgrep:\n Installed: 14.1.0-1\n Candidate: 14.1.1-1\n " ,
903+ )
904+
905+ version = get_available_version ("ripgrep" , "apt" )
906+
907+ call_args = mock_run .call_args [0 ][0 ]
908+ assert call_args == ["apt-cache" , "policy" , "ripgrep" ], (
909+ f"apt-cache policy should use 'ripgrep', got: { call_args } "
910+ )
911+
912+ clear_version_cache ()
0 commit comments