@@ -26,6 +26,7 @@ import {
2626 oauthConsent ,
2727 organization ,
2828 organizationSearchIntegration ,
29+ organizationSearchMcpInvocation ,
2930 rateLimitBucket ,
3031 user ,
3132 workspace ,
@@ -35,9 +36,19 @@ import { generateId } from '@sim/utils/id'
3536import { isPlainRecord } from '@sim/utils/object'
3637import { and , eq , inArray } from 'drizzle-orm'
3738import { NextRequest } from 'next/server'
38- import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest'
39+ import { afterAll , afterEach , beforeAll , describe , expect , it , vi } from 'vitest'
3940
40- const fixtures = vi . hoisted ( ( ) => ( { storageRoot : '' } ) )
41+ const fixtures = vi . hoisted ( ( ) => ( {
42+ storageRoot : '' ,
43+ afterResponse : [ ] as Array < ( ) => Promise < void > > ,
44+ } ) )
45+ vi . mock ( '@/lib/core/utils/after-response' , ( ) => ( {
46+ afterResponse : ( task : ( ) => Promise < void > ) => fixtures . afterResponse . push ( task ) ,
47+ } ) )
48+
49+ async function flushAfterResponse ( ) {
50+ for ( const task of fixtures . afterResponse . splice ( 0 ) ) await task ( )
51+ }
4152vi . mock ( '@/lib/uploads/core/setup.server' , ( ) => ( {
4253 get UPLOAD_DIR_SERVER ( ) {
4354 return fixtures . storageRoot
@@ -401,6 +412,8 @@ describe('organization Search MCP with real ingestion and current access', () =>
401412 bobOAuth = await connect ( OAUTH_ACCESS_TOKEN_PREFIX + oauthTokens . bob , true )
402413 } )
403414
415+ afterEach ( flushAfterResponse )
416+
404417 afterAll ( async ( ) => {
405418 await Promise . all ( clients . map ( ( client ) => client . close ( ) ) )
406419 await db . delete ( oauthClient ) . where ( eq ( oauthClient . clientId , oauthClientId ) )
@@ -508,6 +521,74 @@ describe('organization Search MCP with real ingestion and current access', () =>
508521 expect ( await applicationSearch ( bobPrincipal ) ) . toEqual ( [ ] )
509522 } )
510523
524+ it ( 'persists content-free per-client tool outcomes separately from search counters' , async ( ) => {
525+ await db
526+ . delete ( organizationSearchMcpInvocation )
527+ . where ( eq ( organizationSearchMcpInvocation . organizationId , organizationId ) )
528+ const clientName = 'MCP fixture client' . repeat ( 20 )
529+ await db
530+ . update ( oauthClient )
531+ . set ( { name : clientName } )
532+ . where ( eq ( oauthClient . clientId , oauthClientId ) )
533+ try {
534+ await aliceOAuth . listTools ( )
535+ expect ( fixtures . afterResponse ) . toHaveLength ( 0 )
536+ await search ( aliceOAuth )
537+ await value ( aliceOAuth , 'read_document' , { documentId } )
538+ expect ( ( await call ( bob , 'read_document' , { documentId } ) ) . isError ) . toBe ( true )
539+ expect ( fixtures . afterResponse ) . toHaveLength ( 3 )
540+ await db
541+ . update ( oauthClient )
542+ . set ( { name : 'Renamed client' } )
543+ . where ( eq ( oauthClient . clientId , oauthClientId ) )
544+ await flushAfterResponse ( )
545+ const rows = await db
546+ . select ( )
547+ . from ( organizationSearchMcpInvocation )
548+ . where ( eq ( organizationSearchMcpInvocation . organizationId , organizationId ) )
549+ . orderBy ( organizationSearchMcpInvocation . createdAt )
550+ . limit ( 10 )
551+ expect ( rows ) . toHaveLength ( 3 )
552+ expect ( rows ) . toMatchObject ( [
553+ {
554+ organizationId,
555+ userId : aliceId ,
556+ authKind : 'oauth_access_token' ,
557+ oauthClientId,
558+ clientName : clientName . slice ( 0 , 256 ) ,
559+ toolName : 'search' ,
560+ outcome : 'success' ,
561+ } ,
562+ {
563+ organizationId,
564+ userId : aliceId ,
565+ authKind : 'oauth_access_token' ,
566+ oauthClientId,
567+ clientName : clientName . slice ( 0 , 256 ) ,
568+ toolName : 'read_document' ,
569+ outcome : 'success' ,
570+ } ,
571+ {
572+ organizationId,
573+ userId : bobId ,
574+ authKind : 'personal_api_key' ,
575+ oauthClientId : null ,
576+ clientName : null ,
577+ toolName : 'read_document' ,
578+ outcome : 'error' ,
579+ } ,
580+ ] )
581+ expect ( rows . every ( ( row ) => row . durationMs >= 0 ) ) . toBe ( true )
582+ expect ( JSON . stringify ( rows ) ) . not . toContain ( documentId )
583+ expect ( JSON . stringify ( rows ) ) . not . toContain ( oauthTokens . alice )
584+ } finally {
585+ await db
586+ . update ( oauthClient )
587+ . set ( { name : 'Search MCP OAuth fixture' } )
588+ . where ( eq ( oauthClient . clientId , oauthClientId ) )
589+ }
590+ } )
591+
511592 it ( 'enforces current document and organization access on Search OAuth clients' , async ( ) => {
512593 expect ( ( await aliceOAuth . listTools ( ) ) . tools ) . toHaveLength ( 3 )
513594 expect ( await search ( aliceOAuth ) ) . toEqual ( await search ( alice ) )
0 commit comments