|
4 | 4 | package com.microsoft.copilot.eclipse.ui.chat.services; |
5 | 5 |
|
6 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 7 | +import static org.junit.jupiter.api.Assertions.assertFalse; |
7 | 8 | import static org.junit.jupiter.api.Assertions.assertNotNull; |
8 | 9 | import static org.junit.jupiter.api.Assertions.assertTrue; |
9 | 10 | import static org.mockito.ArgumentMatchers.eq; |
| 11 | +import static org.mockito.Mockito.atLeastOnce; |
10 | 12 | import static org.mockito.Mockito.mock; |
| 13 | +import static org.mockito.Mockito.never; |
11 | 14 | import static org.mockito.Mockito.verify; |
12 | 15 | import static org.mockito.Mockito.when; |
13 | 16 |
|
@@ -207,4 +210,151 @@ void testUpdateApprovedMcpServerStringWithNullServers() throws Exception { |
207 | 210 | Map<String, Object> resultServers = (Map<String, Object>) result.get("servers"); |
208 | 211 | assertTrue(resultServers.isEmpty(), "Servers map should be empty when MCP servers are null"); |
209 | 212 | } |
| 213 | + |
| 214 | + @Test |
| 215 | + void testDetectChangesDropsApprovedServerWhenPluginUninstalled() throws Exception { |
| 216 | + // Regression test for https://github.com/microsoft/copilot-for-eclipse/issues/153 scenario 1: |
| 217 | + // a previously approved plugin is no longer providing any MCP server. |
| 218 | + IPreferenceStore mockPreferenceStore = mock(IPreferenceStore.class); |
| 219 | + when(mockCopilotUi.getPreferenceStore()).thenReturn(mockPreferenceStore); |
| 220 | + |
| 221 | + Map<String, Object> previouslyApprovedServers = new HashMap<>(); |
| 222 | + previouslyApprovedServers.put("server-X", Map.of("url", "http://localhost:9000")); |
| 223 | + McpRegistrationInfo persistedInfo = createMcpRegistrationInfo(true, true, "Test Plugin", |
| 224 | + previouslyApprovedServers); |
| 225 | + Map<String, McpRegistrationInfo> persisted = new HashMap<>(); |
| 226 | + persisted.put("com.example.plugin", persistedInfo); |
| 227 | + |
| 228 | + // Live extension scan returned nothing (plugin uninstalled or no longer provides servers). |
| 229 | + setExtMcpInfoMap(new HashMap<>()); |
| 230 | + |
| 231 | + invokeDetectChanges(persisted); |
| 232 | + |
| 233 | + String approvedServers = manager.getApprovedExtMcpServers(); |
| 234 | + assertNotNull(approvedServers, "Approved servers JSON should be non-null after detect"); |
| 235 | + Map<String, Object> result = gson.fromJson(approvedServers, Map.class); |
| 236 | + Map<String, Object> resultServers = (Map<String, Object>) result.get("servers"); |
| 237 | + assertTrue(resultServers.isEmpty(), |
| 238 | + "Stale approved server must be dropped when the live extension scan returns nothing"); |
| 239 | + |
| 240 | + // No new approval prompt should be raised because there is no incoming registration to approve. |
| 241 | + verify(mockMcpConfigService, never()).setNewExtMcpRegFound(true); |
| 242 | + |
| 243 | + // The verified state must be persisted so subsequent startups do not resurrect the stale entry. |
| 244 | + ArgumentCaptor<String> persistedJson = ArgumentCaptor.forClass(String.class); |
| 245 | + verify(mockPreferenceStore, atLeastOnce()).setValue(eq(Constants.MCP_EXTENSION_POINT_CONTRIB), |
| 246 | + persistedJson.capture()); |
| 247 | + assertEquals("{}", persistedJson.getValue(), |
| 248 | + "Persisted contribution map should be empty after the plugin is gone"); |
| 249 | + } |
| 250 | + |
| 251 | + @Test |
| 252 | + void testDetectChangesDropsApprovalAndFlagsRedNoticeWhenConfigChanges() throws Exception { |
| 253 | + // Regression test for https://github.com/microsoft/copilot-for-eclipse/issues/153 scenario 2: |
| 254 | + // the contributing plugin returns a different config than what was previously approved. |
| 255 | + IPreferenceStore mockPreferenceStore = mock(IPreferenceStore.class); |
| 256 | + when(mockCopilotUi.getPreferenceStore()).thenReturn(mockPreferenceStore); |
| 257 | + |
| 258 | + Map<String, Object> oldServers = new HashMap<>(); |
| 259 | + oldServers.put("server-X", Map.of("url", "http://localhost:9000")); |
| 260 | + McpRegistrationInfo persistedInfo = createMcpRegistrationInfo(true, true, "Test Plugin", oldServers); |
| 261 | + Map<String, McpRegistrationInfo> persisted = new HashMap<>(); |
| 262 | + persisted.put("com.example.plugin", persistedInfo); |
| 263 | + |
| 264 | + // Live extension scan returned the same plugin but with a different server config (port change). |
| 265 | + Map<String, Object> newServers = new HashMap<>(); |
| 266 | + newServers.put("server-X", Map.of("url", "http://localhost:9999")); |
| 267 | + McpRegistrationInfo currentInfo = createMcpRegistrationInfo(true, false, "Test Plugin", newServers); |
| 268 | + Map<String, McpRegistrationInfo> currentMap = new HashMap<>(); |
| 269 | + currentMap.put("com.example.plugin", currentInfo); |
| 270 | + setExtMcpInfoMap(currentMap); |
| 271 | + |
| 272 | + invokeDetectChanges(persisted); |
| 273 | + |
| 274 | + String approvedServers = manager.getApprovedExtMcpServers(); |
| 275 | + assertNotNull(approvedServers); |
| 276 | + Map<String, Object> result = gson.fromJson(approvedServers, Map.class); |
| 277 | + Map<String, Object> resultServers = (Map<String, Object>) result.get("servers"); |
| 278 | + assertTrue(resultServers.isEmpty(), |
| 279 | + "Changed config must not be auto-applied; LSP must not receive the (now unapproved) entry"); |
| 280 | + |
| 281 | + assertFalse(currentInfo.isApproved(), |
| 282 | + "Previously approved entry whose config changed must be marked as unapproved until the user re-approves"); |
| 283 | + verify(mockMcpConfigService).setNewExtMcpRegFound(true); |
| 284 | + verify(mockPreferenceStore, atLeastOnce()).setValue(eq(Constants.MCP_EXTENSION_POINT_CONTRIB), |
| 285 | + org.mockito.ArgumentMatchers.anyString()); |
| 286 | + } |
| 287 | + |
| 288 | + @Test |
| 289 | + void testDetectChangesPreservesApprovalWhenConfigUnchanged() throws Exception { |
| 290 | + // Companion test: when the live extension scan reports the same config as the persisted cache, |
| 291 | + // the previous approval must be carried over so the explicit LSP sync at the end of |
| 292 | + // doRegistration() can push the verified servers to the language server. |
| 293 | + IPreferenceStore mockPreferenceStore = mock(IPreferenceStore.class); |
| 294 | + when(mockCopilotUi.getPreferenceStore()).thenReturn(mockPreferenceStore); |
| 295 | + |
| 296 | + Map<String, Object> approvedServers = new HashMap<>(); |
| 297 | + approvedServers.put("server-X", Map.of("url", "http://localhost:9000")); |
| 298 | + McpRegistrationInfo persistedInfo = createMcpRegistrationInfo(true, true, "Test Plugin", approvedServers); |
| 299 | + Map<String, McpRegistrationInfo> persisted = new HashMap<>(); |
| 300 | + persisted.put("com.example.plugin", persistedInfo); |
| 301 | + |
| 302 | + // Live extension scan returned the same servers; default isApproved=false until carry-over runs. |
| 303 | + Map<String, Object> sameServers = new HashMap<>(); |
| 304 | + sameServers.put("server-X", Map.of("url", "http://localhost:9000")); |
| 305 | + McpRegistrationInfo currentInfo = createMcpRegistrationInfo(true, false, "Test Plugin", sameServers); |
| 306 | + Map<String, McpRegistrationInfo> currentMap = new HashMap<>(); |
| 307 | + currentMap.put("com.example.plugin", currentInfo); |
| 308 | + setExtMcpInfoMap(currentMap); |
| 309 | + |
| 310 | + invokeDetectChanges(persisted); |
| 311 | + |
| 312 | + assertTrue(currentInfo.isApproved(), |
| 313 | + "When the contributed config matches the persisted JSON, the previous approval must be carried over"); |
| 314 | + |
| 315 | + String approvedJson = manager.getApprovedExtMcpServers(); |
| 316 | + assertNotNull(approvedJson); |
| 317 | + Map<String, Object> result = gson.fromJson(approvedJson, Map.class); |
| 318 | + Map<String, Object> resultServers = (Map<String, Object>) result.get("servers"); |
| 319 | + assertEquals(1, resultServers.size(), |
| 320 | + "Carried-over approved server must be present in the approved servers JSON for the LSP sync"); |
| 321 | + |
| 322 | + // No red-notice should be raised because nothing has changed for the user to re-review. |
| 323 | + verify(mockMcpConfigService, never()).setNewExtMcpRegFound(true); |
| 324 | + } |
| 325 | + |
| 326 | + /** |
| 327 | + * Reflectively replace the manager's private {@code extMcpInfoMap} so tests can simulate the |
| 328 | + * outcome of {@code loadMcpRegistrationExtensionPoint()} without requiring a live OSGi |
| 329 | + * extension registry. |
| 330 | + */ |
| 331 | + private void setExtMcpInfoMap(Map<String, McpRegistrationInfo> map) throws Exception { |
| 332 | + Field field = McpExtensionPointManager.class.getDeclaredField("extMcpInfoMap"); |
| 333 | + field.setAccessible(true); |
| 334 | + field.set(manager, map); |
| 335 | + } |
| 336 | + |
| 337 | + private void invokeDetectChanges(Map<String, McpRegistrationInfo> persisted) throws Exception { |
| 338 | + // Get the scanned map that was previously set via setExtMcpInfoMap |
| 339 | + Field field = McpExtensionPointManager.class.getDeclaredField("extMcpInfoMap"); |
| 340 | + field.setAccessible(true); |
| 341 | + @SuppressWarnings("unchecked") |
| 342 | + Map<String, McpRegistrationInfo> scannedMap = (Map<String, McpRegistrationInfo>) field.get(manager); |
| 343 | + |
| 344 | + // detectChangesInMcpContribs now takes (scannedMap, persistedMap) and no longer |
| 345 | + // calls updateApprovedMcpServerString / persistExtMcpInfo (those moved to doRegistration). |
| 346 | + Method detectMethod = McpExtensionPointManager.class.getDeclaredMethod("detectChangesInMcpContribs", Map.class, |
| 347 | + Map.class); |
| 348 | + detectMethod.setAccessible(true); |
| 349 | + detectMethod.invoke(manager, scannedMap, persisted); |
| 350 | + |
| 351 | + // Mirror what doRegistration() does after detectChanges: swap the field and update/persist. |
| 352 | + field.set(manager, scannedMap); |
| 353 | + Method updateMethod = McpExtensionPointManager.class.getDeclaredMethod("updateApprovedMcpServerString", Map.class); |
| 354 | + updateMethod.setAccessible(true); |
| 355 | + updateMethod.invoke(manager, scannedMap); |
| 356 | + Method persistMethod = McpExtensionPointManager.class.getDeclaredMethod("persistExtMcpInfo", Map.class); |
| 357 | + persistMethod.setAccessible(true); |
| 358 | + persistMethod.invoke(manager, scannedMap); |
| 359 | + } |
210 | 360 | } |
0 commit comments