@@ -737,6 +737,49 @@ describe('McpService.discoverTools per-server caching', () => {
737737 expect ( mockCacheAdapter . delete ) . toHaveBeenCalledWith ( failureKey )
738738 } )
739739
740+ it ( 'best-effort deletes both cache keys and publishes the barrier when ordered invalidation fails' , async ( ) => {
741+ const serverKey = `workspace:${ WORKSPACE_ID } :server:mcp-a`
742+ const failureKey = `${ serverKey } :failure`
743+ mockGetWorkspaceServersRows . mockResolvedValue ( [ dbRow ( 'mcp-a' , 'A' ) ] )
744+ cacheStore . set ( serverKey , {
745+ tools : [ tool ( 'stale-tool' , 'mcp-a' ) ] ,
746+ expiry : Date . now ( ) + 60_000 ,
747+ } )
748+ cacheStore . set ( failureKey , { tools : [ ] , expiry : Date . now ( ) + 60_000 } )
749+ mockCacheAdapter . applyMutationIfCurrent . mockRejectedValueOnce (
750+ new Error ( 'atomic invalidation unavailable' )
751+ )
752+
753+ await mcpService . clearCache ( WORKSPACE_ID )
754+
755+ const mutationId = mockCacheAdapter . applyMutationIfCurrent . mock . calls [ 0 ] [ 1 ]
756+ expect ( cacheStore . has ( serverKey ) ) . toBe ( false )
757+ expect ( cacheStore . has ( failureKey ) ) . toBe ( false )
758+ expect ( mockCacheAdapter . delete ) . toHaveBeenCalledWith ( serverKey )
759+ expect ( mockCacheAdapter . delete ) . toHaveBeenCalledWith ( failureKey )
760+ expect ( mockUpdateSet ) . toHaveBeenCalledWith ( {
761+ toolCount : 0 ,
762+ lastToolsRefresh : new Date ( mutationId ) ,
763+ } )
764+ } )
765+
766+ it ( 'preserves a newer cache winner when invalidation is superseded' , async ( ) => {
767+ const serverKey = `workspace:${ WORKSPACE_ID } :server:mcp-a`
768+ const failureKey = `${ serverKey } :failure`
769+ const winner = tool ( 'winner-tool' , 'mcp-a' )
770+ mockGetWorkspaceServersRows . mockResolvedValue ( [ dbRow ( 'mcp-a' , 'A' ) ] )
771+ cacheStore . set ( serverKey , { tools : [ winner ] , expiry : Date . now ( ) + 60_000 } )
772+ cacheStore . set ( failureKey , { tools : [ ] , expiry : Date . now ( ) + 60_000 } )
773+ mockCacheAdapter . applyMutationIfCurrent . mockResolvedValueOnce ( false )
774+
775+ await mcpService . clearCache ( WORKSPACE_ID )
776+
777+ expect ( cacheStore . get ( serverKey ) ?. tools ) . toEqual ( [ winner ] )
778+ expect ( cacheStore . has ( failureKey ) ) . toBe ( true )
779+ expect ( mockCacheAdapter . delete ) . not . toHaveBeenCalled ( )
780+ expect ( mockUpdateSet ) . not . toHaveBeenCalled ( )
781+ } )
782+
740783 it ( 'does not negative-cache OAuth-required errors' , async ( ) => {
741784 mockGetWorkspaceServersRows . mockResolvedValue ( [ dbRow ( 'mcp-a' , 'A' ) ] )
742785 mockListTools . mockRejectedValueOnce ( new McpOauthAuthorizationRequiredError ( 'mcp-a' , 'A' ) )
0 commit comments