@@ -34,6 +34,23 @@ vi.mock('@/lib/documents/utils', () => ({
3434 retryWithExponentialBackoff : vi . fn ( ) . mockImplementation ( ( fn ) => fn ( ) ) ,
3535} ) )
3636
37+ vi . mock ( '@/lib/tokenization/estimators' , ( ) => ( {
38+ estimateTokenCount : vi . fn ( ) . mockReturnValue ( { count : 521 } ) ,
39+ } ) )
40+
41+ vi . mock ( '@/providers/utils' , ( ) => ( {
42+ calculateCost : vi . fn ( ) . mockReturnValue ( {
43+ input : 0.00001042 ,
44+ output : 0 ,
45+ total : 0.00001042 ,
46+ pricing : {
47+ input : 0.02 ,
48+ output : 0 ,
49+ updatedAt : '2025-07-10' ,
50+ } ,
51+ } ) ,
52+ } ) )
53+
3754mockConsoleLogger ( )
3855
3956describe ( 'Knowledge Search API Route' , ( ) => {
@@ -206,7 +223,7 @@ describe('Knowledge Search API Route', () => {
206223 expect ( mockGetUserId ) . toHaveBeenCalledWith ( expect . any ( String ) , 'workflow-123' )
207224 } )
208225
209- it ( 'should return unauthorized for unauthenticated request' , async ( ) => {
226+ it . concurrent ( 'should return unauthorized for unauthenticated request' , async ( ) => {
210227 mockGetUserId . mockResolvedValue ( null )
211228
212229 const req = createMockRequest ( 'POST' , validSearchData )
@@ -218,7 +235,7 @@ describe('Knowledge Search API Route', () => {
218235 expect ( data . error ) . toBe ( 'Unauthorized' )
219236 } )
220237
221- it ( 'should return not found for workflow that does not exist' , async ( ) => {
238+ it . concurrent ( 'should return not found for workflow that does not exist' , async ( ) => {
222239 const workflowData = {
223240 ...validSearchData ,
224241 workflowId : 'nonexistent-workflow' ,
@@ -268,7 +285,7 @@ describe('Knowledge Search API Route', () => {
268285 expect ( data . error ) . toBe ( 'Knowledge bases not found: kb-missing' )
269286 } )
270287
271- it ( 'should validate search parameters' , async ( ) => {
288+ it . concurrent ( 'should validate search parameters' , async ( ) => {
272289 const invalidData = {
273290 knowledgeBaseIds : '' , // Empty string
274291 query : '' , // Empty query
@@ -314,7 +331,7 @@ describe('Knowledge Search API Route', () => {
314331 expect ( data . data . topK ) . toBe ( 10 ) // Default value
315332 } )
316333
317- it ( 'should handle OpenAI API errors' , async ( ) => {
334+ it . concurrent ( 'should handle OpenAI API errors' , async ( ) => {
318335 mockGetUserId . mockResolvedValue ( 'user-123' )
319336 mockDbChain . limit . mockResolvedValueOnce ( mockKnowledgeBases )
320337
@@ -334,7 +351,7 @@ describe('Knowledge Search API Route', () => {
334351 expect ( data . error ) . toBe ( 'Failed to perform vector search' )
335352 } )
336353
337- it ( 'should handle missing OpenAI API key' , async ( ) => {
354+ it . concurrent ( 'should handle missing OpenAI API key' , async ( ) => {
338355 vi . doMock ( '@/lib/env' , ( ) => ( {
339356 env : {
340357 OPENAI_API_KEY : undefined ,
@@ -353,7 +370,7 @@ describe('Knowledge Search API Route', () => {
353370 expect ( data . error ) . toBe ( 'Failed to perform vector search' )
354371 } )
355372
356- it ( 'should handle database errors during search' , async ( ) => {
373+ it . concurrent ( 'should handle database errors during search' , async ( ) => {
357374 mockGetUserId . mockResolvedValue ( 'user-123' )
358375 mockDbChain . limit . mockResolvedValueOnce ( mockKnowledgeBases )
359376 mockDbChain . limit . mockRejectedValueOnce ( new Error ( 'Database error' ) )
@@ -375,7 +392,7 @@ describe('Knowledge Search API Route', () => {
375392 expect ( data . error ) . toBe ( 'Failed to perform vector search' )
376393 } )
377394
378- it ( 'should handle invalid OpenAI response format' , async ( ) => {
395+ it . concurrent ( 'should handle invalid OpenAI response format' , async ( ) => {
379396 mockGetUserId . mockResolvedValue ( 'user-123' )
380397 mockDbChain . limit . mockResolvedValueOnce ( mockKnowledgeBases )
381398
@@ -395,5 +412,124 @@ describe('Knowledge Search API Route', () => {
395412 expect ( response . status ) . toBe ( 500 )
396413 expect ( data . error ) . toBe ( 'Failed to perform vector search' )
397414 } )
415+
416+ describe ( 'Cost tracking' , ( ) => {
417+ it . concurrent ( 'should include cost information in successful search response' , async ( ) => {
418+ mockGetUserId . mockResolvedValue ( 'user-123' )
419+ mockDbChain . where . mockResolvedValueOnce ( mockKnowledgeBases )
420+ mockDbChain . limit . mockResolvedValueOnce ( mockSearchResults )
421+
422+ mockFetch . mockResolvedValue ( {
423+ ok : true ,
424+ json : ( ) =>
425+ Promise . resolve ( {
426+ data : [ { embedding : mockEmbedding } ] ,
427+ } ) ,
428+ } )
429+
430+ const req = createMockRequest ( 'POST' , validSearchData )
431+ const { POST } = await import ( './route' )
432+ const response = await POST ( req )
433+ const data = await response . json ( )
434+
435+ expect ( response . status ) . toBe ( 200 )
436+ expect ( data . success ) . toBe ( true )
437+
438+ // Verify cost information is included
439+ expect ( data . data . cost ) . toBeDefined ( )
440+ expect ( data . data . cost . input ) . toBe ( 0.00001042 )
441+ expect ( data . data . cost . output ) . toBe ( 0 )
442+ expect ( data . data . cost . total ) . toBe ( 0.00001042 )
443+ expect ( data . data . cost . tokens ) . toEqual ( {
444+ prompt : 521 ,
445+ completion : 0 ,
446+ total : 521 ,
447+ } )
448+ expect ( data . data . cost . model ) . toBe ( 'text-embedding-3-small' )
449+ expect ( data . data . cost . pricing ) . toEqual ( {
450+ input : 0.02 ,
451+ output : 0 ,
452+ updatedAt : '2025-07-10' ,
453+ } )
454+ } )
455+
456+ it ( 'should call cost calculation functions with correct parameters' , async ( ) => {
457+ const { estimateTokenCount } = await import ( '@/lib/tokenization/estimators' )
458+ const { calculateCost } = await import ( '@/providers/utils' )
459+
460+ mockGetUserId . mockResolvedValue ( 'user-123' )
461+ mockDbChain . where . mockResolvedValueOnce ( mockKnowledgeBases )
462+ mockDbChain . limit . mockResolvedValueOnce ( mockSearchResults )
463+
464+ mockFetch . mockResolvedValue ( {
465+ ok : true ,
466+ json : ( ) =>
467+ Promise . resolve ( {
468+ data : [ { embedding : mockEmbedding } ] ,
469+ } ) ,
470+ } )
471+
472+ const req = createMockRequest ( 'POST' , validSearchData )
473+ const { POST } = await import ( './route' )
474+ await POST ( req )
475+
476+ // Verify token estimation was called with correct parameters
477+ expect ( estimateTokenCount ) . toHaveBeenCalledWith ( 'test search query' , 'openai' )
478+
479+ // Verify cost calculation was called with correct parameters
480+ expect ( calculateCost ) . toHaveBeenCalledWith ( 'text-embedding-3-small' , 521 , 0 , false )
481+ } )
482+
483+ it ( 'should handle cost calculation with different query lengths' , async ( ) => {
484+ const { estimateTokenCount } = await import ( '@/lib/tokenization/estimators' )
485+ const { calculateCost } = await import ( '@/providers/utils' )
486+
487+ // Mock different token count for longer query
488+ vi . mocked ( estimateTokenCount ) . mockReturnValue ( {
489+ count : 1042 ,
490+ confidence : 'high' ,
491+ provider : 'openai' ,
492+ method : 'precise' ,
493+ } )
494+ vi . mocked ( calculateCost ) . mockReturnValue ( {
495+ input : 0.00002084 ,
496+ output : 0 ,
497+ total : 0.00002084 ,
498+ pricing : {
499+ input : 0.02 ,
500+ output : 0 ,
501+ updatedAt : '2025-07-10' ,
502+ } ,
503+ } )
504+
505+ const longQueryData = {
506+ ...validSearchData ,
507+ query :
508+ 'This is a much longer search query with many more tokens to test cost calculation accuracy' ,
509+ }
510+
511+ mockGetUserId . mockResolvedValue ( 'user-123' )
512+ mockDbChain . where . mockResolvedValueOnce ( mockKnowledgeBases )
513+ mockDbChain . limit . mockResolvedValueOnce ( mockSearchResults )
514+
515+ mockFetch . mockResolvedValue ( {
516+ ok : true ,
517+ json : ( ) =>
518+ Promise . resolve ( {
519+ data : [ { embedding : mockEmbedding } ] ,
520+ } ) ,
521+ } )
522+
523+ const req = createMockRequest ( 'POST' , longQueryData )
524+ const { POST } = await import ( './route' )
525+ const response = await POST ( req )
526+ const data = await response . json ( )
527+
528+ expect ( response . status ) . toBe ( 200 )
529+ expect ( data . data . cost . input ) . toBe ( 0.00002084 )
530+ expect ( data . data . cost . tokens . prompt ) . toBe ( 1042 )
531+ expect ( calculateCost ) . toHaveBeenCalledWith ( 'text-embedding-3-small' , 1042 , 0 , false )
532+ } )
533+ } )
398534 } )
399535} )
0 commit comments