@@ -1070,3 +1070,307 @@ func TestConverter_IncomingAuthRequired(t *testing.T) {
10701070 })
10711071 }
10721072}
1073+
1074+ // Test helpers for MCPToolConfig tests
1075+ func newMCPToolConfig (name , namespace string , filter []string , overrides map [string ]mcpv1alpha1.ToolOverride ) * mcpv1alpha1.MCPToolConfig {
1076+ return & mcpv1alpha1.MCPToolConfig {
1077+ ObjectMeta : metav1.ObjectMeta {Name : name , Namespace : namespace },
1078+ Spec : mcpv1alpha1.MCPToolConfigSpec {ToolsFilter : filter , ToolsOverride : overrides },
1079+ }
1080+ }
1081+
1082+ func toolOverride (name , desc string ) mcpv1alpha1.ToolOverride {
1083+ return mcpv1alpha1.ToolOverride {Name : name , Description : desc }
1084+ }
1085+
1086+ func vmcpToolOverride (name , desc string ) * vmcpconfig.ToolOverride {
1087+ return & vmcpconfig.ToolOverride {Name : name , Description : desc }
1088+ }
1089+
1090+ func TestResolveMCPToolConfig (t * testing.T ) {
1091+ t .Parallel ()
1092+
1093+ ns := "test-ns"
1094+ tests := []struct {
1095+ name string
1096+ configName string
1097+ existing * mcpv1alpha1.MCPToolConfig
1098+ expectError bool
1099+ }{
1100+ {
1101+ name : "successfully resolve existing MCPToolConfig" ,
1102+ configName : "test-config" ,
1103+ existing : newMCPToolConfig ("test-config" , ns , []string {"tool1" , "tool2" }, nil ),
1104+ },
1105+ {
1106+ name : "error when MCPToolConfig not found" ,
1107+ configName : "nonexistent" ,
1108+ expectError : true ,
1109+ },
1110+ {
1111+ name : "successfully resolve with overrides" ,
1112+ configName : "config-with-overrides" ,
1113+ existing : newMCPToolConfig ("config-with-overrides" , ns , []string {"fetch" },
1114+ map [string ]mcpv1alpha1.ToolOverride {"fetch" : toolOverride ("renamed_fetch" , "Renamed tool" )}),
1115+ },
1116+ }
1117+
1118+ for _ , tt := range tests {
1119+ t .Run (tt .name , func (t * testing.T ) {
1120+ t .Parallel ()
1121+
1122+ var k8sClient client.Client
1123+ if tt .existing != nil {
1124+ k8sClient = newTestK8sClient (t , tt .existing )
1125+ } else {
1126+ k8sClient = newTestK8sClient (t )
1127+ }
1128+
1129+ converter := newTestConverter (t , newNoOpMockResolver (t ))
1130+ converter .k8sClient = k8sClient
1131+
1132+ result , err := converter .resolveMCPToolConfig (context .Background (), ns , tt .configName )
1133+
1134+ if tt .expectError {
1135+ assert .Error (t , err )
1136+ assert .Nil (t , result )
1137+ } else {
1138+ assert .NoError (t , err )
1139+ assert .NotNil (t , result )
1140+ assert .Equal (t , tt .existing .Spec , result .Spec )
1141+ }
1142+ })
1143+ }
1144+ }
1145+
1146+ func TestMergeToolConfigFilter (t * testing.T ) {
1147+ t .Parallel ()
1148+
1149+ tests := []struct {
1150+ name string
1151+ existing []string
1152+ config * mcpv1alpha1.MCPToolConfig
1153+ expected []string
1154+ }{
1155+ {
1156+ name : "merge when workload has none" ,
1157+ existing : nil ,
1158+ config : newMCPToolConfig ("" , "" , []string {"tool1" , "tool2" }, nil ),
1159+ expected : []string {"tool1" , "tool2" },
1160+ },
1161+ {
1162+ name : "inline takes precedence" ,
1163+ existing : []string {"inline_tool" },
1164+ config : newMCPToolConfig ("" , "" , []string {"config_tool" }, nil ),
1165+ expected : []string {"inline_tool" },
1166+ },
1167+ {
1168+ name : "no change when config has no filter" ,
1169+ existing : []string {"existing_tool" },
1170+ config : newMCPToolConfig ("" , "" , nil , nil ),
1171+ expected : []string {"existing_tool" },
1172+ },
1173+ {
1174+ name : "empty filter from config" ,
1175+ existing : nil ,
1176+ config : newMCPToolConfig ("" , "" , []string {}, nil ),
1177+ expected : nil ,
1178+ },
1179+ }
1180+
1181+ for _ , tt := range tests {
1182+ t .Run (tt .name , func (t * testing.T ) {
1183+ t .Parallel ()
1184+
1185+ wtc := & vmcpconfig.WorkloadToolConfig {Filter : tt .existing }
1186+ (& Converter {}).mergeToolConfigFilter (wtc , tt .config )
1187+
1188+ assert .Equal (t , tt .expected , wtc .Filter )
1189+ })
1190+ }
1191+ }
1192+
1193+ func TestMergeToolConfigOverrides (t * testing.T ) {
1194+ t .Parallel ()
1195+
1196+ tests := []struct {
1197+ name string
1198+ existing map [string ]* vmcpconfig.ToolOverride
1199+ config * mcpv1alpha1.MCPToolConfig
1200+ expected map [string ]* vmcpconfig.ToolOverride
1201+ }{
1202+ {
1203+ name : "merge when workload has none" ,
1204+ existing : nil ,
1205+ config : newMCPToolConfig ("" , "" , nil , map [string ]mcpv1alpha1.ToolOverride {"tool1" : toolOverride ("renamed_tool1" , "Renamed description" )}),
1206+ expected : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("renamed_tool1" , "Renamed description" )},
1207+ },
1208+ {
1209+ name : "inline takes precedence" ,
1210+ existing : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("inline_name" , "Inline description" )},
1211+ config : newMCPToolConfig ("" , "" , nil , map [string ]mcpv1alpha1.ToolOverride {"tool1" : toolOverride ("config_name" , "Config description" )}),
1212+ expected : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("inline_name" , "Inline description" )},
1213+ },
1214+ {
1215+ name : "merge non-conflicting" ,
1216+ existing : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("inline_tool1" , "Inline description" )},
1217+ config : newMCPToolConfig ("" , "" , nil , map [string ]mcpv1alpha1.ToolOverride {"tool2" : toolOverride ("config_tool2" , "Config description" )}),
1218+ expected : map [string ]* vmcpconfig.ToolOverride {
1219+ "tool1" : vmcpToolOverride ("inline_tool1" , "Inline description" ),
1220+ "tool2" : vmcpToolOverride ("config_tool2" , "Config description" ),
1221+ },
1222+ },
1223+ {
1224+ name : "no change when config has no overrides" ,
1225+ existing : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("existing_name" , "" )},
1226+ config : newMCPToolConfig ("" , "" , nil , nil ),
1227+ expected : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("existing_name" , "" )},
1228+ },
1229+ }
1230+
1231+ for _ , tt := range tests {
1232+ t .Run (tt .name , func (t * testing.T ) {
1233+ t .Parallel ()
1234+
1235+ wtc := & vmcpconfig.WorkloadToolConfig {Overrides : tt .existing }
1236+ (& Converter {}).mergeToolConfigOverrides (wtc , tt .config )
1237+
1238+ assert .Equal (t , tt .expected , wtc .Overrides )
1239+ })
1240+ }
1241+ }
1242+
1243+ func TestApplyInlineOverrides (t * testing.T ) {
1244+ t .Parallel ()
1245+
1246+ tests := []struct {
1247+ name string
1248+ inline map [string ]mcpv1alpha1.ToolOverride
1249+ existing map [string ]* vmcpconfig.ToolOverride
1250+ expected map [string ]* vmcpconfig.ToolOverride
1251+ }{
1252+ {
1253+ name : "apply to empty workload" ,
1254+ inline : map [string ]mcpv1alpha1.ToolOverride {"tool1" : toolOverride ("renamed_tool1" , "Inline description" )},
1255+ existing : nil ,
1256+ expected : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("renamed_tool1" , "Inline description" )},
1257+ },
1258+ {
1259+ name : "replace existing" ,
1260+ inline : map [string ]mcpv1alpha1.ToolOverride {"tool1" : toolOverride ("new_name" , "New description" )},
1261+ existing : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("old_name" , "Old description" )},
1262+ expected : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("new_name" , "New description" )},
1263+ },
1264+ {
1265+ name : "no change when no inline" ,
1266+ inline : nil ,
1267+ existing : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("existing_name" , "" )},
1268+ expected : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("existing_name" , "" )},
1269+ },
1270+ {
1271+ name : "multiple overrides" ,
1272+ inline : map [string ]mcpv1alpha1.ToolOverride {
1273+ "tool1" : toolOverride ("renamed_tool1" , "Description 1" ),
1274+ "tool2" : toolOverride ("renamed_tool2" , "Description 2" ),
1275+ },
1276+ existing : nil ,
1277+ expected : map [string ]* vmcpconfig.ToolOverride {
1278+ "tool1" : vmcpToolOverride ("renamed_tool1" , "Description 1" ),
1279+ "tool2" : vmcpToolOverride ("renamed_tool2" , "Description 2" ),
1280+ },
1281+ },
1282+ }
1283+
1284+ for _ , tt := range tests {
1285+ t .Run (tt .name , func (t * testing.T ) {
1286+ t .Parallel ()
1287+
1288+ wtc := & vmcpconfig.WorkloadToolConfig {Overrides : tt .existing }
1289+ toolConfig := mcpv1alpha1.WorkloadToolConfig {Overrides : tt .inline }
1290+ (& Converter {}).applyInlineOverrides (toolConfig , wtc )
1291+
1292+ assert .Equal (t , tt .expected , wtc .Overrides )
1293+ })
1294+ }
1295+ }
1296+
1297+ func TestConvertToolConfigs (t * testing.T ) {
1298+ t .Parallel ()
1299+
1300+ tests := []struct {
1301+ name string
1302+ tools []mcpv1alpha1.WorkloadToolConfig
1303+ existingConfig * mcpv1alpha1.MCPToolConfig
1304+ expectedWorkload string
1305+ expectedFilter []string
1306+ expectedOverride map [string ]* vmcpconfig.ToolOverride
1307+ }{
1308+ {
1309+ name : "inline config only" ,
1310+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1311+ Workload : "backend1" ,
1312+ Filter : []string {"tool1" , "tool2" },
1313+ Overrides : map [string ]mcpv1alpha1.ToolOverride {"tool1" : toolOverride ("renamed_tool1" , "Renamed" )},
1314+ }},
1315+ expectedWorkload : "backend1" ,
1316+ expectedFilter : []string {"tool1" , "tool2" },
1317+ expectedOverride : map [string ]* vmcpconfig.ToolOverride {"tool1" : vmcpToolOverride ("renamed_tool1" , "Renamed" )},
1318+ },
1319+ {
1320+ name : "with MCPToolConfig reference" ,
1321+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1322+ Workload : "backend1" ,
1323+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "test-config" },
1324+ }},
1325+ existingConfig : newMCPToolConfig ("test-config" , "default" , []string {"fetch" },
1326+ map [string ]mcpv1alpha1.ToolOverride {"fetch" : toolOverride ("renamed_fetch" , "Renamed fetch" )}),
1327+ expectedWorkload : "backend1" ,
1328+ expectedFilter : []string {"fetch" },
1329+ expectedOverride : map [string ]* vmcpconfig.ToolOverride {"fetch" : vmcpToolOverride ("renamed_fetch" , "Renamed fetch" )},
1330+ },
1331+ {
1332+ name : "inline takes precedence" ,
1333+ tools : []mcpv1alpha1.WorkloadToolConfig {{
1334+ Workload : "backend1" ,
1335+ Filter : []string {"inline_tool" },
1336+ ToolConfigRef : & mcpv1alpha1.ToolConfigRef {Name : "test-config" },
1337+ Overrides : map [string ]mcpv1alpha1.ToolOverride {"fetch" : toolOverride ("inline_fetch" , "Inline override" )},
1338+ }},
1339+ existingConfig : newMCPToolConfig ("test-config" , "default" , []string {"config_tool" },
1340+ map [string ]mcpv1alpha1.ToolOverride {"fetch" : toolOverride ("config_fetch" , "Config override" )}),
1341+ expectedWorkload : "backend1" ,
1342+ expectedFilter : []string {"inline_tool" },
1343+ expectedOverride : map [string ]* vmcpconfig.ToolOverride {"fetch" : vmcpToolOverride ("inline_fetch" , "Inline override" )},
1344+ },
1345+ }
1346+
1347+ for _ , tt := range tests {
1348+ t .Run (tt .name , func (t * testing.T ) {
1349+ t .Parallel ()
1350+
1351+ ctx := log .IntoContext (context .Background (), logr .Discard ())
1352+ var k8sClient client.Client
1353+ if tt .existingConfig != nil {
1354+ k8sClient = newTestK8sClient (t , tt .existingConfig )
1355+ } else {
1356+ k8sClient = newTestK8sClient (t )
1357+ }
1358+
1359+ converter := newTestConverter (t , newNoOpMockResolver (t ))
1360+ converter .k8sClient = k8sClient
1361+
1362+ vmcp := & mcpv1alpha1.VirtualMCPServer {
1363+ ObjectMeta : metav1.ObjectMeta {Name : "test-vmcp" , Namespace : "default" },
1364+ Spec : mcpv1alpha1.VirtualMCPServerSpec {Aggregation : & mcpv1alpha1.AggregationConfig {Tools : tt .tools }},
1365+ }
1366+
1367+ agg := & vmcpconfig.AggregationConfig {}
1368+ converter .convertToolConfigs (ctx , vmcp , agg )
1369+
1370+ require .Len (t , agg .Tools , 1 )
1371+ assert .Equal (t , tt .expectedWorkload , agg .Tools [0 ].Workload )
1372+ assert .Equal (t , tt .expectedFilter , agg .Tools [0 ].Filter )
1373+ assert .Equal (t , tt .expectedOverride , agg .Tools [0 ].Overrides )
1374+ })
1375+ }
1376+ }
0 commit comments