1+ import { createDeferred } from '@sim/testing/helpers'
12import { getMockLogger } from '@sim/testing/mocks/logger.mock'
23import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
3- import { measureSearchStage , withSearchDiagnostics } from '@/lib/knowledge/search/diagnostics'
4+ import {
5+ measureSearchStage ,
6+ recordSearchProviderResult ,
7+ withSearchDiagnostics ,
8+ withSearchProviderDiagnostics ,
9+ withSearchRequestDiagnostics ,
10+ } from '@/lib/knowledge/search/diagnostics'
411
512const logs = getMockLogger ( 'KnowledgeSearchDiagnostics' )
613
@@ -9,21 +16,13 @@ beforeEach(() => {
916} )
1017afterEach ( ( ) => vi . useRealTimers ( ) )
1118
12- function _deferred ( ) {
13- let resolve ! : ( ) => void
14- const promise = new Promise < void > ( ( done ) => {
15- resolve = done
16- } )
17- return { promise, resolve }
18- }
19-
2019describe ( 'search pipeline diagnostics' , ( ) => {
2120 it ( 'preserves failures, records repeated stage errors, and does not log error content' , async ( ) => {
2221 const error = new Error ( 'private provider response' )
2322 await expect (
2423 withSearchDiagnostics ( { surface : 'copilot' } , async ( ) => {
25- await measureSearchStage ( 'vector.authorization ' , ( ) => 'allowed' )
26- return measureSearchStage ( 'vector.authorization ' , ( ) => {
24+ await measureSearchStage ( 'live.resolve ' , ( ) => 'allowed' )
25+ return measureSearchStage ( 'live.resolve ' , ( ) => {
2726 throw error
2827 } )
2928 } )
@@ -33,11 +32,143 @@ describe('search pipeline diagnostics', () => {
3332 expect . objectContaining ( {
3433 outcome : 'error' ,
3534 stages : {
36- 'vector.authorization ' : { count : 2 , totalMs : 0 , maxMs : 0 , errors : 1 } ,
35+ 'live.resolve ' : { count : 2 , totalMs : 0 , maxMs : 0 , errors : 1 } ,
3736 } ,
3837 } )
3938 )
4039 expect ( JSON . stringify ( logs . info . mock . calls ) ) . not . toContain ( error . message )
4140 expect ( vi . getTimerCount ( ) ) . toBe ( 0 )
4241 } )
42+
43+ it ( 'isolates concurrent provider failures and retains errors caught by a search adapter' , async ( ) => {
44+ const error = Object . assign ( new Error ( 'private query, provider response, and credential' ) , {
45+ code : 'private credential' ,
46+ httpStatus : 403 ,
47+ diagnosticCode : 'provider_access_denied' ,
48+ } )
49+ const result = await withSearchDiagnostics ( { surface : 'dashboard' } , ( ) =>
50+ Promise . all ( [
51+ withSearchProviderDiagnostics ( { provider : 'gmail' , accountIndex : 0 } , async ( ) => {
52+ await measureSearchStage ( 'live.search' , ( ) =>
53+ withSearchRequestDiagnostics ( { transport : 'native_http' , method : 'GET' } , async ( ) => {
54+ await Promise . resolve ( )
55+ throw error
56+ } ) . catch ( ( ) => undefined )
57+ )
58+ recordSearchProviderResult ( { status : 'partial' , resultCount : 1 } )
59+ return [ 'available result' ]
60+ } ) ,
61+ withSearchProviderDiagnostics ( { provider : 'github' , accountIndex : 1 } , async ( ) => {
62+ await measureSearchStage ( 'live.verify' , ( ) =>
63+ withSearchRequestDiagnostics (
64+ { transport : 'native_http' , method : 'GET' } ,
65+ async ( record ) => {
66+ record ( { httpStatus : 200 } )
67+ }
68+ )
69+ )
70+ recordSearchProviderResult ( { status : 'ok' , resultCount : 2 } )
71+ return [ 'other result' ]
72+ } ) ,
73+ ] )
74+ )
75+ expect ( result ) . toEqual ( [ [ 'available result' ] , [ 'other result' ] ] )
76+ const summaries = logs . info . mock . calls
77+ . filter ( ( [ message ] ) => message === 'Live search provider completed' )
78+ . map ( ( [ , data ] ) => data )
79+ expect ( summaries ) . toEqual (
80+ expect . arrayContaining ( [
81+ expect . objectContaining ( {
82+ provider : 'gmail' ,
83+ requests : [
84+ expect . objectContaining ( {
85+ stage : 'live.search' ,
86+ outcome : 'error' ,
87+ failure : {
88+ reason : 'provider_access_denied' ,
89+ httpStatus : 403 ,
90+ } ,
91+ } ) ,
92+ ] ,
93+ results : [ { status : 'partial' , resultCount : 1 } ] ,
94+ } ) ,
95+ expect . objectContaining ( {
96+ provider : 'github' ,
97+ requests : [
98+ expect . objectContaining ( { stage : 'live.verify' , outcome : 'success' , httpStatus : 200 } ) ,
99+ ] ,
100+ results : [ { status : 'ok' , resultCount : 2 } ] ,
101+ } ) ,
102+ ] )
103+ )
104+ const serialized = JSON . stringify ( logs . info . mock . calls )
105+ expect ( serialized ) . not . toContain ( 'private' )
106+ expect ( serialized ) . not . toContain ( 'available result' )
107+ expect ( serialized ) . not . toContain ( 'other result' )
108+ expect ( new Set ( summaries . map ( ( summary ) => summary . searchId ) ) . size ) . toBe ( 1 )
109+ } )
110+
111+ it ( 'preserves cancellation and bounds request diagnostics without losing failure counts' , async ( ) => {
112+ const aborted = new DOMException ( 'private cancellation reason' , 'AbortError' )
113+ await expect (
114+ withSearchDiagnostics ( { } , ( ) =>
115+ withSearchProviderDiagnostics ( { provider : 'gmail' , accountIndex : 0 } , async ( ) => {
116+ for ( let index = 0 ; index < 140 ; index ++ ) {
117+ await withSearchRequestDiagnostics (
118+ { transport : 'native_http' , method : 'GET' } ,
119+ async ( ) => {
120+ throw Object . assign ( new Error ( 'private failure' ) , { code : 'ECONNRESET' } )
121+ }
122+ ) . catch ( ( ) => undefined )
123+ }
124+ return withSearchRequestDiagnostics (
125+ { transport : 'native_http' , method : 'GET' } ,
126+ async ( ) => {
127+ throw aborted
128+ }
129+ )
130+ } )
131+ )
132+ ) . rejects . toBe ( aborted )
133+ expect ( logs . info ) . toHaveBeenCalledWith (
134+ 'Live search provider completed' ,
135+ expect . objectContaining ( {
136+ requestCount : 141 ,
137+ failedRequestCount : 141 ,
138+ omittedRequestCount : 13 ,
139+ outcome : 'error' ,
140+ failure : { reason : 'aborted' } ,
141+ } )
142+ )
143+ expect ( JSON . stringify ( logs . info . mock . calls ) ) . not . toContain ( 'private' )
144+ expect ( vi . getTimerCount ( ) ) . toBe ( 0 )
145+ } )
146+
147+ it ( 'records preview failures that settle after the search has returned' , async ( ) => {
148+ const pending = createDeferred < void > ( )
149+ let request : Promise < unknown > | undefined
150+ await withSearchDiagnostics ( { } , ( ) =>
151+ withSearchProviderDiagnostics ( { provider : 'gmail' , accountIndex : 0 } , async ( ) => {
152+ request = withSearchRequestDiagnostics (
153+ { transport : 'native_http' , method : 'GET' } ,
154+ ( ) => pending . promise
155+ ) . catch ( ( ) => undefined )
156+ } )
157+ )
158+ expect ( logs . info ) . toHaveBeenCalledWith (
159+ 'Live search provider completed' ,
160+ expect . objectContaining ( { pendingRequestCount : 1 } )
161+ )
162+ pending . reject ( new DOMException ( 'private preview content' , 'AbortError' ) )
163+ await request
164+ expect ( logs . info ) . toHaveBeenLastCalledWith (
165+ 'Live search request completed after provider' ,
166+ expect . objectContaining ( {
167+ requestIndex : 0 ,
168+ outcome : 'error' ,
169+ failure : { reason : 'aborted' } ,
170+ } )
171+ )
172+ expect ( JSON . stringify ( logs . info . mock . calls ) ) . not . toContain ( 'private preview content' )
173+ } )
43174} )
0 commit comments