@@ -1004,6 +1004,58 @@ describe('McpService.discoverTools per-server caching', () => {
10041004 expect ( mockListTools ) . toHaveBeenCalledTimes ( 2 )
10051005 } )
10061006
1007+ it ( 'reacquires mutation ownership after cache invalidation during retry backoff' , async ( ) => {
1008+ vi . useFakeTimers ( { toFake : [ 'setTimeout' , 'clearTimeout' ] } )
1009+ try {
1010+ const serverKey = `workspace:${ WORKSPACE_ID } :server:mcp-a`
1011+ mockGetWorkspaceServersRows . mockResolvedValue ( [ dbRow ( 'mcp-a' , 'A' ) ] )
1012+
1013+ let rejectFirstAttempt : ( ( error : Error ) => void ) | undefined
1014+ let markFirstAttemptStarted : ( ( ) => void ) | undefined
1015+ const firstAttemptStarted = new Promise < void > ( ( resolve ) => {
1016+ markFirstAttemptStarted = resolve
1017+ } )
1018+ mockListTools
1019+ . mockImplementationOnce (
1020+ ( ) =>
1021+ new Promise ( ( _resolve , reject ) => {
1022+ rejectFirstAttempt = reject
1023+ markFirstAttemptStarted ?.( )
1024+ } )
1025+ )
1026+ . mockResolvedValueOnce ( [ tool ( 'retry-winner' , 'mcp-a' ) ] )
1027+
1028+ const discovery = mcpService . discoverServerToolsWithMetadata (
1029+ USER_ID ,
1030+ 'mcp-a' ,
1031+ WORKSPACE_ID ,
1032+ true
1033+ )
1034+ await firstAttemptStarted
1035+
1036+ rejectFirstAttempt ?.( new Error ( 'Request timed out' ) )
1037+ await vi . advanceTimersByTimeAsync ( 0 )
1038+ expect ( mockListTools ) . toHaveBeenCalledTimes ( 1 )
1039+
1040+ await mcpService . clearCache ( WORKSPACE_ID )
1041+ expect ( mockCacheAdapter . beginMutation ) . toHaveBeenCalledTimes ( 2 )
1042+
1043+ await vi . runAllTimersAsync ( )
1044+ await expect ( discovery ) . resolves . toEqual ( {
1045+ tools : [ tool ( 'retry-winner' , 'mcp-a' ) ] ,
1046+ state : 'published' ,
1047+ } )
1048+
1049+ expect ( mockCacheAdapter . beginMutation ) . toHaveBeenCalledTimes ( 3 )
1050+ expect ( cacheStore . get ( serverKey ) ?. tools ) . toEqual ( [ tool ( 'retry-winner' , 'mcp-a' ) ] )
1051+ expect ( mockUpdateSet ) . toHaveBeenCalledWith (
1052+ expect . objectContaining ( { connectionStatus : 'connected' , toolCount : 1 } )
1053+ )
1054+ } finally {
1055+ vi . useRealTimers ( )
1056+ }
1057+ } )
1058+
10071059 it ( 'persists and negative-caches per-server UnauthorizedError for headers auth' , async ( ) => {
10081060 const reflectedCredential = 'Bearer static-secret-for-server-discovery'
10091061 mockGetWorkspaceServersRows . mockResolvedValue ( [
0 commit comments