@@ -1222,3 +1222,334 @@ def test_env_list_hosts_combined_filters(self):
12221222
12231223 # dev environment should NOT appear (doesn't match env filter)
12241224 assert "0.9.0" not in output , "Version 0.9.0 from dev should NOT appear"
1225+
1226+
1227+ class TestEnvListServersCommand :
1228+ """Integration tests for env list servers command.
1229+
1230+ Reference: R10 §3.4 (10-namespace_consistency_specification_v2.md)
1231+
1232+ These tests verify that handle_env_list_servers:
1233+ 1. Reads from environment data (Hatch-managed packages only)
1234+ 2. Shows environment/server/host deployments with columns: Environment → Server → Host → Version
1235+ 3. Shows '-' for undeployed packages
1236+ 4. Supports --env and --host filters (regex patterns)
1237+ 5. Supports --host - to show only undeployed packages
1238+ 6. First column (Environment) sorted alphabetically
1239+ """
1240+
1241+ def test_env_list_servers_uniform_output (self ):
1242+ """Command should produce uniform table output with Environment → Server → Host → Version columns.
1243+
1244+ Reference: R10 §3.4 - Column order matches command structure
1245+ """
1246+ from hatch .cli .cli_env import handle_env_list_servers
1247+
1248+ mock_env_manager = MagicMock ()
1249+ mock_env_manager .list_environments .return_value = [
1250+ {"name" : "default" },
1251+ {"name" : "dev" },
1252+ ]
1253+ mock_env_manager .get_environment_data .side_effect = lambda env_name : {
1254+ "default" : {
1255+ "packages" : [
1256+ {
1257+ "name" : "weather-server" ,
1258+ "version" : "1.0.0" ,
1259+ "configured_hosts" : {"claude-desktop" : {}}
1260+ },
1261+ {
1262+ "name" : "util-lib" ,
1263+ "version" : "0.5.0" ,
1264+ "configured_hosts" : {} # Undeployed
1265+ }
1266+ ]
1267+ },
1268+ "dev" : {
1269+ "packages" : [
1270+ {
1271+ "name" : "test-server" ,
1272+ "version" : "0.1.0" ,
1273+ "configured_hosts" : {"cursor" : {}}
1274+ }
1275+ ]
1276+ },
1277+ }.get (env_name , {"packages" : []})
1278+
1279+ args = Namespace (
1280+ env_manager = mock_env_manager ,
1281+ env = None ,
1282+ host = None ,
1283+ json = False ,
1284+ )
1285+
1286+ captured_output = io .StringIO ()
1287+ with patch ('sys.stdout' , captured_output ):
1288+ result = handle_env_list_servers (args )
1289+
1290+ output = captured_output .getvalue ()
1291+
1292+ # Verify column headers present
1293+ assert "Environment" in output , "Environment column should be present"
1294+ assert "Server" in output , "Server column should be present"
1295+ assert "Host" in output , "Host column should be present"
1296+ assert "Version" in output , "Version column should be present"
1297+
1298+ # Verify data appears
1299+ assert "default" in output , "default environment should appear"
1300+ assert "dev" in output , "dev environment should appear"
1301+ assert "weather-server" in output , "weather-server should appear"
1302+ assert "util-lib" in output , "util-lib should appear"
1303+ assert "test-server" in output , "test-server should appear"
1304+
1305+ def test_env_list_servers_env_filter_exact (self ):
1306+ """--env flag with exact name should filter to matching environment only.
1307+
1308+ Reference: R10 §3.4 - --env <pattern> filter
1309+ """
1310+ from hatch .cli .cli_env import handle_env_list_servers
1311+
1312+ mock_env_manager = MagicMock ()
1313+ mock_env_manager .list_environments .return_value = [
1314+ {"name" : "default" },
1315+ {"name" : "dev" },
1316+ ]
1317+ mock_env_manager .get_environment_data .side_effect = lambda env_name : {
1318+ "default" : {
1319+ "packages" : [
1320+ {"name" : "server-a" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}}
1321+ ]
1322+ },
1323+ "dev" : {
1324+ "packages" : [
1325+ {"name" : "server-b" , "version" : "0.1.0" , "configured_hosts" : {"claude-desktop" : {}}}
1326+ ]
1327+ },
1328+ }.get (env_name , {"packages" : []})
1329+
1330+ args = Namespace (
1331+ env_manager = mock_env_manager ,
1332+ env = "default" ,
1333+ host = None ,
1334+ json = False ,
1335+ )
1336+
1337+ captured_output = io .StringIO ()
1338+ with patch ('sys.stdout' , captured_output ):
1339+ result = handle_env_list_servers (args )
1340+
1341+ output = captured_output .getvalue ()
1342+
1343+ # Matching environment should appear
1344+ assert "server-a" in output , "server-a from default should appear"
1345+
1346+ # Non-matching environment should NOT appear
1347+ assert "server-b" not in output , "server-b from dev should NOT appear"
1348+
1349+ def test_env_list_servers_env_filter_pattern (self ):
1350+ """--env flag with regex pattern should filter matching environments.
1351+
1352+ Reference: R10 §3.4 - --env accepts regex patterns
1353+ """
1354+ from hatch .cli .cli_env import handle_env_list_servers
1355+
1356+ mock_env_manager = MagicMock ()
1357+ mock_env_manager .list_environments .return_value = [
1358+ {"name" : "default" },
1359+ {"name" : "dev" },
1360+ {"name" : "dev-staging" },
1361+ ]
1362+ mock_env_manager .get_environment_data .side_effect = lambda env_name : {
1363+ "default" : {
1364+ "packages" : [
1365+ {"name" : "server-a" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}}
1366+ ]
1367+ },
1368+ "dev" : {
1369+ "packages" : [
1370+ {"name" : "server-b" , "version" : "0.1.0" , "configured_hosts" : {"claude-desktop" : {}}}
1371+ ]
1372+ },
1373+ "dev-staging" : {
1374+ "packages" : [
1375+ {"name" : "server-c" , "version" : "0.2.0" , "configured_hosts" : {"claude-desktop" : {}}}
1376+ ]
1377+ },
1378+ }.get (env_name , {"packages" : []})
1379+
1380+ args = Namespace (
1381+ env_manager = mock_env_manager ,
1382+ env = "dev.*" ,
1383+ host = None ,
1384+ json = False ,
1385+ )
1386+
1387+ captured_output = io .StringIO ()
1388+ with patch ('sys.stdout' , captured_output ):
1389+ result = handle_env_list_servers (args )
1390+
1391+ output = captured_output .getvalue ()
1392+
1393+ # Matching environments should appear
1394+ assert "server-b" in output , "server-b from dev should appear"
1395+ assert "server-c" in output , "server-c from dev-staging should appear"
1396+
1397+ # Non-matching environment should NOT appear
1398+ assert "server-a" not in output , "server-a from default should NOT appear"
1399+
1400+ def test_env_list_servers_host_filter_exact (self ):
1401+ """--host flag with exact name should filter to matching host only.
1402+
1403+ Reference: R10 §3.4 - --host <pattern> filter
1404+ """
1405+ from hatch .cli .cli_env import handle_env_list_servers
1406+
1407+ mock_env_manager = MagicMock ()
1408+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1409+ mock_env_manager .get_environment_data .return_value = {
1410+ "packages" : [
1411+ {"name" : "server-a" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}},
1412+ {"name" : "server-b" , "version" : "2.0.0" , "configured_hosts" : {"cursor" : {}}},
1413+ ]
1414+ }
1415+
1416+ args = Namespace (
1417+ env_manager = mock_env_manager ,
1418+ env = None ,
1419+ host = "claude-desktop" ,
1420+ json = False ,
1421+ )
1422+
1423+ captured_output = io .StringIO ()
1424+ with patch ('sys.stdout' , captured_output ):
1425+ result = handle_env_list_servers (args )
1426+
1427+ output = captured_output .getvalue ()
1428+
1429+ # Matching host should appear
1430+ assert "server-a" in output , "server-a on claude-desktop should appear"
1431+
1432+ # Non-matching host should NOT appear
1433+ assert "server-b" not in output , "server-b on cursor should NOT appear"
1434+
1435+ def test_env_list_servers_host_filter_pattern (self ):
1436+ """--host flag with regex pattern should filter matching hosts.
1437+
1438+ Reference: R10 §3.4 - --host accepts regex patterns
1439+ """
1440+ from hatch .cli .cli_env import handle_env_list_servers
1441+
1442+ mock_env_manager = MagicMock ()
1443+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1444+ mock_env_manager .get_environment_data .return_value = {
1445+ "packages" : [
1446+ {"name" : "server-a" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}},
1447+ {"name" : "server-b" , "version" : "2.0.0" , "configured_hosts" : {"cursor" : {}}},
1448+ {"name" : "server-c" , "version" : "3.0.0" , "configured_hosts" : {"claude-code" : {}}},
1449+ ]
1450+ }
1451+
1452+ args = Namespace (
1453+ env_manager = mock_env_manager ,
1454+ env = None ,
1455+ host = "claude.*" , # Regex pattern
1456+ json = False ,
1457+ )
1458+
1459+ captured_output = io .StringIO ()
1460+ with patch ('sys.stdout' , captured_output ):
1461+ result = handle_env_list_servers (args )
1462+
1463+ output = captured_output .getvalue ()
1464+
1465+ # Matching hosts should appear
1466+ assert "server-a" in output , "server-a on claude-desktop should appear"
1467+ assert "server-c" in output , "server-c on claude-code should appear"
1468+
1469+ # Non-matching host should NOT appear
1470+ assert "server-b" not in output , "server-b on cursor should NOT appear"
1471+
1472+ def test_env_list_servers_host_filter_undeployed (self ):
1473+ """--host - should show only undeployed packages.
1474+
1475+ Reference: R10 §3.4 - Special filter for undeployed packages
1476+ """
1477+ from hatch .cli .cli_env import handle_env_list_servers
1478+
1479+ mock_env_manager = MagicMock ()
1480+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1481+ mock_env_manager .get_environment_data .return_value = {
1482+ "packages" : [
1483+ {"name" : "deployed-server" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}},
1484+ {"name" : "util-lib" , "version" : "0.5.0" , "configured_hosts" : {}}, # Undeployed
1485+ {"name" : "debug-lib" , "version" : "0.3.0" , "configured_hosts" : {}}, # Undeployed
1486+ ]
1487+ }
1488+
1489+ args = Namespace (
1490+ env_manager = mock_env_manager ,
1491+ env = None ,
1492+ host = "-" , # Special filter for undeployed
1493+ json = False ,
1494+ )
1495+
1496+ captured_output = io .StringIO ()
1497+ with patch ('sys.stdout' , captured_output ):
1498+ result = handle_env_list_servers (args )
1499+
1500+ output = captured_output .getvalue ()
1501+
1502+ # Undeployed packages should appear
1503+ assert "util-lib" in output , "util-lib (undeployed) should appear"
1504+ assert "debug-lib" in output , "debug-lib (undeployed) should appear"
1505+
1506+ # Deployed package should NOT appear
1507+ assert "deployed-server" not in output , "deployed-server should NOT appear"
1508+
1509+ def test_env_list_servers_combined_filters (self ):
1510+ """Combined --env and --host filters should work with AND logic.
1511+
1512+ Reference: R10 §1.5 - Combined filters
1513+ """
1514+ from hatch .cli .cli_env import handle_env_list_servers
1515+
1516+ mock_env_manager = MagicMock ()
1517+ mock_env_manager .list_environments .return_value = [
1518+ {"name" : "default" },
1519+ {"name" : "dev" },
1520+ ]
1521+ mock_env_manager .get_environment_data .side_effect = lambda env_name : {
1522+ "default" : {
1523+ "packages" : [
1524+ {"name" : "server-a" , "version" : "1.0.0" , "configured_hosts" : {"claude-desktop" : {}}},
1525+ {"name" : "server-b" , "version" : "2.0.0" , "configured_hosts" : {"cursor" : {}}},
1526+ ]
1527+ },
1528+ "dev" : {
1529+ "packages" : [
1530+ {"name" : "server-c" , "version" : "0.1.0" , "configured_hosts" : {"claude-desktop" : {}}},
1531+ ]
1532+ },
1533+ }.get (env_name , {"packages" : []})
1534+
1535+ args = Namespace (
1536+ env_manager = mock_env_manager ,
1537+ env = "default" ,
1538+ host = "claude-desktop" ,
1539+ json = False ,
1540+ )
1541+
1542+ captured_output = io .StringIO ()
1543+ with patch ('sys.stdout' , captured_output ):
1544+ result = handle_env_list_servers (args )
1545+
1546+ output = captured_output .getvalue ()
1547+
1548+ # Only server-a from default on claude-desktop should appear
1549+ assert "server-a" in output , "server-a should appear"
1550+
1551+ # server-b should NOT appear (wrong host)
1552+ assert "server-b" not in output , "server-b should NOT appear"
1553+
1554+ # server-c should NOT appear (wrong env)
1555+ assert "server-c" not in output , "server-c should NOT appear"
0 commit comments