11import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' ;
2+ import type { Tool } from '@modelcontextprotocol/sdk/types.js' ;
23import { beforeEach , describe , expect , it , vi } from 'vitest' ;
34
4- const { registryMocks, buildStatusMock, serviceMocks, onToolCatalogInvalidatedRef } = vi . hoisted (
5- ( ) => ( {
6- registryMocks : {
7- clear : vi . fn ( ) ,
8- getRegisteredCount : vi . fn ( ( ) => 0 ) ,
9- sync : vi . fn ( ( ) => ( { added : 0 , updated : 0 , removed : 0 , total : 0 } ) ) ,
10- } ,
11- buildStatusMock : vi . fn ( ) ,
12- serviceMocks : {
13- setWorkflowEnabled : vi . fn ( ) ,
14- disconnect : vi . fn ( ) ,
15- getClientStatus : vi . fn ( ) ,
16- getLastError : vi . fn ( ) ,
17- listTools : vi . fn ( ) ,
18- invokeTool : vi . fn ( ) ,
19- } ,
20- onToolCatalogInvalidatedRef : {
21- current : undefined as ( ( ) => void ) | undefined ,
22- } ,
23- } ) ,
24- ) ;
5+ const {
6+ registryMocks,
7+ buildStatusMock,
8+ serviceMocks,
9+ onToolCatalogInvalidatedRef,
10+ getMcpBridgeAvailabilityMock,
11+ } = vi . hoisted ( ( ) => ( {
12+ registryMocks : {
13+ clear : vi . fn ( ) ,
14+ getRegisteredCount : vi . fn ( ( ) => 0 ) ,
15+ sync : vi . fn ( ( ) => ( { added : 0 , updated : 0 , removed : 0 , total : 0 } ) ) ,
16+ } ,
17+ buildStatusMock : vi . fn ( ) ,
18+ serviceMocks : {
19+ setWorkflowEnabled : vi . fn ( ) ,
20+ disconnect : vi . fn ( ) ,
21+ getClientStatus : vi . fn ( ) ,
22+ getLastError : vi . fn ( ) ,
23+ listTools : vi . fn ( ) ,
24+ invokeTool : vi . fn ( ) ,
25+ } ,
26+ onToolCatalogInvalidatedRef : {
27+ current : undefined as ( ( ) => void ) | undefined ,
28+ } ,
29+ getMcpBridgeAvailabilityMock : vi . fn ( ) ,
30+ } ) ) ;
2531
2632vi . mock ( '../registry.ts' , ( ) => ( {
2733 XcodeToolsProxyRegistry : vi . fn ( ) . mockImplementation ( ( ) => registryMocks ) ,
@@ -30,7 +36,7 @@ vi.mock('../registry.ts', () => ({
3036vi . mock ( '../core.ts' , ( ) => ( {
3137 buildXcodeToolsBridgeStatus : buildStatusMock ,
3238 classifyBridgeError : vi . fn ( ( ) => 'XCODE_MCP_UNAVAILABLE' ) ,
33- getMcpBridgeAvailability : vi . fn ( ) ,
39+ getMcpBridgeAvailability : getMcpBridgeAvailabilityMock ,
3440 serializeBridgeTool : vi . fn ( ( tool ) => tool ) ,
3541} ) ) ;
3642
@@ -83,7 +89,11 @@ describe('XcodeToolsBridgeManager', () => {
8389 serviceMocks . getLastError . mockReset ( ) ;
8490 serviceMocks . getLastError . mockReturnValue ( null ) ;
8591 serviceMocks . listTools . mockReset ( ) ;
92+ serviceMocks . listTools . mockResolvedValue ( [ ] ) ;
8693 serviceMocks . invokeTool . mockReset ( ) ;
94+
95+ getMcpBridgeAvailabilityMock . mockReset ( ) ;
96+ getMcpBridgeAvailabilityMock . mockResolvedValue ( { available : true , path : '/usr/bin/mcpbridge' } ) ;
8797 } ) ;
8898
8999 it ( 'does not resync on listChanged while a manual disconnect is in progress' , async ( ) => {
@@ -105,4 +115,27 @@ describe('XcodeToolsBridgeManager', () => {
105115 expect ( registryMocks . clear ) . toHaveBeenCalledOnce ( ) ;
106116 expect ( server . sendToolListChanged ) . toHaveBeenCalledOnce ( ) ;
107117 } ) ;
118+
119+ it ( 're-enables listChanged-driven syncs after a manual sync follows a disconnect' , async ( ) => {
120+ const server = {
121+ sendToolListChanged : vi . fn ( ) ,
122+ } as unknown as McpServer ;
123+
124+ const tools : Tool [ ] = [ { name : 'remote.tool' , inputSchema : { type : 'object' } } as Tool ] ;
125+ serviceMocks . listTools . mockResolvedValue ( tools ) ;
126+
127+ const manager = new XcodeToolsBridgeManager ( server ) ;
128+ manager . setWorkflowEnabled ( true ) ;
129+
130+ await manager . disconnectTool ( ) ;
131+ await manager . syncTools ( { reason : 'manual' } ) ;
132+
133+ const syncSpy = vi . spyOn ( manager , 'syncTools' ) ;
134+
135+ onToolCatalogInvalidatedRef . current ?.( ) ;
136+ await Promise . resolve ( ) ;
137+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
138+
139+ expect ( syncSpy ) . toHaveBeenCalledWith ( { reason : 'listChanged' } ) ;
140+ } ) ;
108141} ) ;
0 commit comments