@@ -965,3 +965,260 @@ def get_strategy_side_effect(host_type):
965965 # claude-desktop should appear before cursor (alphabetically)
966966 assert claude_pos < cursor_pos , \
967967 "Hosts should be sorted alphabetically (claude-desktop before cursor)"
968+
969+
970+ class TestEnvListHostsCommand :
971+ """Integration tests for env list hosts command.
972+
973+ Reference: R10 §3.3 (10-namespace_consistency_specification_v2.md)
974+
975+ These tests verify that handle_env_list_hosts:
976+ 1. Reads from environment data (Hatch-managed packages only)
977+ 2. Shows environment/host/server deployments with columns: Environment → Host → Server → Version
978+ 3. Supports --env and --server filters (regex patterns)
979+ 4. First column (Environment) sorted alphabetically
980+ """
981+
982+ def test_env_list_hosts_uniform_output (self ):
983+ """Command should produce uniform table output with Environment → Host → Server → Version columns.
984+
985+ Reference: R10 §3.3 - Column order matches command structure
986+ """
987+ from hatch .cli .cli_env import handle_env_list_hosts
988+
989+ mock_env_manager = MagicMock ()
990+ mock_env_manager .list_environments .return_value = [
991+ {"name" : "default" , "is_current" : True },
992+ {"name" : "dev" , "is_current" : False },
993+ ]
994+ mock_env_manager .get_environment_data .side_effect = lambda env_name : {
995+ "default" : {
996+ "packages" : [
997+ {
998+ "name" : "weather-server" ,
999+ "version" : "1.0.0" ,
1000+ "configured_hosts" : {
1001+ "claude-desktop" : {"configured_at" : "2026-01-30" },
1002+ "cursor" : {"configured_at" : "2026-01-30" },
1003+ }
1004+ }
1005+ ]
1006+ },
1007+ "dev" : {
1008+ "packages" : [
1009+ {
1010+ "name" : "test-server" ,
1011+ "version" : "0.1.0" ,
1012+ "configured_hosts" : {
1013+ "claude-desktop" : {"configured_at" : "2026-01-30" },
1014+ }
1015+ }
1016+ ]
1017+ },
1018+ }.get (env_name , {"packages" : []})
1019+
1020+ args = Namespace (
1021+ env_manager = mock_env_manager ,
1022+ env = None ,
1023+ server = None ,
1024+ json = False ,
1025+ )
1026+
1027+ captured_output = io .StringIO ()
1028+ with patch ('sys.stdout' , captured_output ):
1029+ result = handle_env_list_hosts (args )
1030+
1031+ output = captured_output .getvalue ()
1032+
1033+ # Verify column headers present
1034+ assert "Environment" in output , "Environment column should be present"
1035+ assert "Host" in output , "Host column should be present"
1036+ assert "Server" in output , "Server column should be present"
1037+ assert "Version" in output , "Version column should be present"
1038+
1039+ # Verify data appears
1040+ assert "default" in output , "default environment should appear"
1041+ assert "dev" in output , "dev environment should appear"
1042+ assert "weather-server" in output , "weather-server should appear"
1043+ assert "test-server" in output , "test-server should appear"
1044+
1045+ def test_env_list_hosts_env_filter_exact (self ):
1046+ """--env flag with exact name should filter to matching environment only.
1047+
1048+ Reference: R10 §3.3 - --env <pattern> filter
1049+ """
1050+ from hatch .cli .cli_env import handle_env_list_hosts
1051+
1052+ mock_env_manager = MagicMock ()
1053+ mock_env_manager .list_environments .return_value = [
1054+ {"name" : "default" },
1055+ {"name" : "dev" },
1056+ ]
1057+ mock_env_manager .get_environment_data .side_effect = lambda env_name : {
1058+ "default" : {
1059+ "packages" : [
1060+ {"name" : "server-a" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}}
1061+ ]
1062+ },
1063+ "dev" : {
1064+ "packages" : [
1065+ {"name" : "server-b" , "version" : "0.1.0" , "configured_hosts" : {"claude-desktop" : {}}}
1066+ ]
1067+ },
1068+ }.get (env_name , {"packages" : []})
1069+
1070+ args = Namespace (
1071+ env_manager = mock_env_manager ,
1072+ env = "default" , # Exact match filter
1073+ server = None ,
1074+ json = False ,
1075+ )
1076+
1077+ captured_output = io .StringIO ()
1078+ with patch ('sys.stdout' , captured_output ):
1079+ result = handle_env_list_hosts (args )
1080+
1081+ output = captured_output .getvalue ()
1082+
1083+ # Matching environment should appear
1084+ assert "server-a" in output , "server-a from default should appear"
1085+
1086+ # Non-matching environment should NOT appear
1087+ assert "server-b" not in output , "server-b from dev should NOT appear"
1088+
1089+ def test_env_list_hosts_env_filter_pattern (self ):
1090+ """--env flag with regex pattern should filter matching environments.
1091+
1092+ Reference: R10 §3.3 - --env accepts regex patterns
1093+ """
1094+ from hatch .cli .cli_env import handle_env_list_hosts
1095+
1096+ mock_env_manager = MagicMock ()
1097+ mock_env_manager .list_environments .return_value = [
1098+ {"name" : "default" },
1099+ {"name" : "dev" },
1100+ {"name" : "dev-staging" },
1101+ ]
1102+ mock_env_manager .get_environment_data .side_effect = lambda env_name : {
1103+ "default" : {
1104+ "packages" : [
1105+ {"name" : "server-a" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}}
1106+ ]
1107+ },
1108+ "dev" : {
1109+ "packages" : [
1110+ {"name" : "server-b" , "version" : "0.1.0" , "configured_hosts" : {"claude-desktop" : {}}}
1111+ ]
1112+ },
1113+ "dev-staging" : {
1114+ "packages" : [
1115+ {"name" : "server-c" , "version" : "0.2.0" , "configured_hosts" : {"claude-desktop" : {}}}
1116+ ]
1117+ },
1118+ }.get (env_name , {"packages" : []})
1119+
1120+ args = Namespace (
1121+ env_manager = mock_env_manager ,
1122+ env = "dev.*" , # Regex pattern
1123+ server = None ,
1124+ json = False ,
1125+ )
1126+
1127+ captured_output = io .StringIO ()
1128+ with patch ('sys.stdout' , captured_output ):
1129+ result = handle_env_list_hosts (args )
1130+
1131+ output = captured_output .getvalue ()
1132+
1133+ # Matching environments should appear
1134+ assert "server-b" in output , "server-b from dev should appear"
1135+ assert "server-c" in output , "server-c from dev-staging should appear"
1136+
1137+ # Non-matching environment should NOT appear
1138+ assert "server-a" not in output , "server-a from default should NOT appear"
1139+
1140+ def test_env_list_hosts_server_filter (self ):
1141+ """--server flag should filter by server name regex.
1142+
1143+ Reference: R10 §3.3 - --server <pattern> filter
1144+ """
1145+ from hatch .cli .cli_env import handle_env_list_hosts
1146+
1147+ mock_env_manager = MagicMock ()
1148+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1149+ mock_env_manager .get_environment_data .return_value = {
1150+ "packages" : [
1151+ {"name" : "weather-server" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}},
1152+ {"name" : "fetch-server" , "version" : "2.0.0" , "configured_hosts" : {"claude-desktop" : {}}},
1153+ {"name" : "custom-tool" , "version" : "0.5.0" , "configured_hosts" : {"claude-desktop" : {}}},
1154+ ]
1155+ }
1156+
1157+ args = Namespace (
1158+ env_manager = mock_env_manager ,
1159+ env = None ,
1160+ server = ".*-server" , # Regex pattern
1161+ json = False ,
1162+ )
1163+
1164+ captured_output = io .StringIO ()
1165+ with patch ('sys.stdout' , captured_output ):
1166+ result = handle_env_list_hosts (args )
1167+
1168+ output = captured_output .getvalue ()
1169+
1170+ # Matching servers should appear
1171+ assert "weather-server" in output , "weather-server should match pattern"
1172+ assert "fetch-server" in output , "fetch-server should match pattern"
1173+
1174+ # Non-matching server should NOT appear
1175+ assert "custom-tool" not in output , "custom-tool should NOT match pattern"
1176+
1177+ def test_env_list_hosts_combined_filters (self ):
1178+ """Combined --env and --server filters should work with AND logic.
1179+
1180+ Reference: R10 §1.5 - Combined filters
1181+ """
1182+ from hatch .cli .cli_env import handle_env_list_hosts
1183+
1184+ mock_env_manager = MagicMock ()
1185+ mock_env_manager .list_environments .return_value = [
1186+ {"name" : "default" },
1187+ {"name" : "dev" },
1188+ ]
1189+ mock_env_manager .get_environment_data .side_effect = lambda env_name : {
1190+ "default" : {
1191+ "packages" : [
1192+ {"name" : "weather-server" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}},
1193+ {"name" : "fetch-server" , "version" : "2.0.0" , "configured_hosts" : {"claude-desktop" : {}}},
1194+ ]
1195+ },
1196+ "dev" : {
1197+ "packages" : [
1198+ {"name" : "weather-server" , "version" : "0.9.0" , "configured_hosts" : {"claude-desktop" : {}}},
1199+ ]
1200+ },
1201+ }.get (env_name , {"packages" : []})
1202+
1203+ args = Namespace (
1204+ env_manager = mock_env_manager ,
1205+ env = "default" ,
1206+ server = "weather.*" ,
1207+ json = False ,
1208+ )
1209+
1210+ captured_output = io .StringIO ()
1211+ with patch ('sys.stdout' , captured_output ):
1212+ result = handle_env_list_hosts (args )
1213+
1214+ output = captured_output .getvalue ()
1215+
1216+ # Only weather-server from default should appear
1217+ assert "weather-server" in output , "weather-server from default should appear"
1218+ assert "1.0.0" in output , "Version 1.0.0 should appear"
1219+
1220+ # fetch-server should NOT appear (doesn't match server filter)
1221+ assert "fetch-server" not in output , "fetch-server should NOT appear"
1222+
1223+ # dev environment should NOT appear (doesn't match env filter)
1224+ assert "0.9.0" not in output , "Version 0.9.0 from dev should NOT appear"
0 commit comments