1- import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest' ;
1+ import { describe , it , expect , beforeEach , vi } from 'vitest' ;
22import { PineconeClient } from './pinecone-client.js' ;
3+ import { resolveConfig } from './config.js' ;
34import type { SearchableIndex , PineconeHit } from './types.js' ;
45import * as rerankModule from './pinecone/rerank.js' ;
56
@@ -31,26 +32,40 @@ describe('PineconeClient', () => {
3132 } ) ;
3233 } ) ;
3334
34- afterEach ( ( ) => {
35- delete process . env [ 'PINECONE_INDEX_NAME' ] ;
36- delete process . env [ 'PINECONE_RERANK_MODEL' ] ;
37- delete process . env [ 'PINECONE_TOP_K' ] ;
38- } ) ;
39-
4035 describe ( 'constructor' , ( ) => {
4136 it ( 'should initialize with provided config' , ( ) => {
4237 expect ( client ) . toBeDefined ( ) ;
4338 } ) ;
4439
45- it ( 'should use environment variables as fallbacks' , ( ) => {
46- process . env [ 'PINECONE_INDEX_NAME' ] = 'env-index' ;
47- process . env [ 'PINECONE_RERANK_MODEL' ] = 'env-model' ;
48-
49- const envClient = new PineconeClient ( {
50- apiKey : 'test-api-key' ,
51- } ) ;
52-
53- expect ( envClient ) . toBeDefined ( ) ;
40+ it ( 'honors resolveConfig overrides without PINECONE_* env on the client path' , ( ) => {
41+ const prevIndex = process . env [ 'PINECONE_INDEX_NAME' ] ;
42+ const prevModel = process . env [ 'PINECONE_RERANK_MODEL' ] ;
43+ const prevTopK = process . env [ 'PINECONE_TOP_K' ] ;
44+ delete process . env [ 'PINECONE_INDEX_NAME' ] ;
45+ delete process . env [ 'PINECONE_RERANK_MODEL' ] ;
46+ delete process . env [ 'PINECONE_TOP_K' ] ;
47+ try {
48+ const resolved = resolveConfig ( {
49+ apiKey : 'test-api-key' ,
50+ indexName : 'resolved-index' ,
51+ rerankModel : 'resolved-model' ,
52+ defaultTopK : 42 ,
53+ } ) ;
54+ const c = new PineconeClient ( {
55+ apiKey : resolved . apiKey ,
56+ indexName : resolved . indexName ,
57+ rerankModel : resolved . rerankModel ,
58+ defaultTopK : resolved . defaultTopK ,
59+ } ) ;
60+ expect ( c . getSparseIndexName ( ) ) . toBe ( 'resolved-index-sparse' ) ;
61+ } finally {
62+ if ( prevIndex !== undefined ) process . env [ 'PINECONE_INDEX_NAME' ] = prevIndex ;
63+ else delete process . env [ 'PINECONE_INDEX_NAME' ] ;
64+ if ( prevModel !== undefined ) process . env [ 'PINECONE_RERANK_MODEL' ] = prevModel ;
65+ else delete process . env [ 'PINECONE_RERANK_MODEL' ] ;
66+ if ( prevTopK !== undefined ) process . env [ 'PINECONE_TOP_K' ] = prevTopK ;
67+ else delete process . env [ 'PINECONE_TOP_K' ] ;
68+ }
5469 } ) ;
5570 } ) ;
5671
@@ -76,16 +91,16 @@ describe('PineconeClient', () => {
7691
7792 it ( 'should continue hybrid search when one index fails' , async ( ) => {
7893 const testClient = stubPineconeClient ( client ) ;
94+ const denseRef = { } as SearchableIndex ;
95+ const sparseRef = { } as SearchableIndex ;
7996
8097 testClient . ensureIndexes = async ( ) => ( {
81- denseIndex : { } as SearchableIndex ,
82- sparseIndex : { } as SearchableIndex ,
98+ denseIndex : denseRef ,
99+ sparseIndex : sparseRef ,
83100 } ) ;
84101
85- let searchCall = 0 ;
86- testClient . searchIndex = async ( ) => {
87- searchCall += 1 ;
88- if ( searchCall === 1 ) {
102+ testClient . searchIndex = async ( index ) => {
103+ if ( index === denseRef ) {
89104 throw new Error ( 'dense failure' ) ;
90105 }
91106 return [
@@ -97,16 +112,18 @@ describe('PineconeClient', () => {
97112 ] ;
98113 } ;
99114
100- const results = await client . query ( {
115+ const out = await client . query ( {
101116 query : 'hybrid search' ,
102117 namespace : 'test' ,
103118 topK : 5 ,
104119 useReranking : false ,
105120 } ) ;
106121
107- expect ( results ) . toHaveLength ( 1 ) ;
108- expect ( results [ 0 ] . content ) . toBe ( 'hybrid content' ) ;
109- expect ( results [ 0 ] . metadata . author ) . toBe ( 'tester' ) ;
122+ expect ( out . results ) . toHaveLength ( 1 ) ;
123+ expect ( out . results [ 0 ] ?. content ) . toBe ( 'hybrid content' ) ;
124+ expect ( out . results [ 0 ] ?. metadata . author ) . toBe ( 'tester' ) ;
125+ expect ( out . hybrid_leg_failed ) . toBe ( 'dense' ) ;
126+ expect ( out . degraded ) . toBe ( false ) ;
110127 } ) ;
111128
112129 it ( 'should throw when both dense and sparse searches fail' , async ( ) => {
@@ -249,15 +266,18 @@ describe('PineconeClient', () => {
249266 } ) ;
250267
251268 it ( 'uses rerankResults from pinecone/rerank when useReranking is true' , async ( ) => {
252- const spy = vi . spyOn ( rerankModule , 'rerankResults' ) . mockResolvedValue ( [
253- {
254- id : 'd1' ,
255- content : 'from dense' ,
256- score : 0.9 ,
257- metadata : { } ,
258- reranked : true ,
259- } ,
260- ] ) ;
269+ const spy = vi . spyOn ( rerankModule , 'rerankResults' ) . mockResolvedValue ( {
270+ results : [
271+ {
272+ id : 'd1' ,
273+ content : 'from dense' ,
274+ score : 0.9 ,
275+ metadata : { } ,
276+ reranked : true ,
277+ } ,
278+ ] ,
279+ degraded : false ,
280+ } ) ;
261281 try {
262282 const testClient = stubPineconeClient ( client ) ;
263283 const denseRef = { } as SearchableIndex ;
@@ -280,9 +300,54 @@ describe('PineconeClient', () => {
280300 useReranking : true ,
281301 } ) ;
282302
283- expect ( results ) . toHaveLength ( 1 ) ;
284- expect ( results [ 0 ] . reranked ) . toBe ( true ) ;
285- expect ( results [ 0 ] . content ) . toBe ( 'from dense' ) ;
303+ expect ( results . results ) . toHaveLength ( 1 ) ;
304+ expect ( results . results [ 0 ] ?. reranked ) . toBe ( true ) ;
305+ expect ( results . results [ 0 ] ?. content ) . toBe ( 'from dense' ) ;
306+ expect ( spy ) . toHaveBeenCalled ( ) ;
307+ } finally {
308+ spy . mockRestore ( ) ;
309+ }
310+ } ) ;
311+
312+ it ( 'propagates rerank degradation to hybrid query outcome' , async ( ) => {
313+ const spy = vi . spyOn ( rerankModule , 'rerankResults' ) . mockResolvedValue ( {
314+ results : [
315+ {
316+ id : 'd1' ,
317+ content : 'from dense' ,
318+ score : 0.9 ,
319+ metadata : { } ,
320+ reranked : false ,
321+ } ,
322+ ] ,
323+ degraded : true ,
324+ degradation_reason : 'rerank_failed: timeout' ,
325+ } ) ;
326+ try {
327+ const testClient = stubPineconeClient ( client ) ;
328+ const denseRef = { } as SearchableIndex ;
329+ const sparseRef = { } as SearchableIndex ;
330+ testClient . ensureIndexes = async ( ) => ( {
331+ denseIndex : denseRef ,
332+ sparseIndex : sparseRef ,
333+ } ) ;
334+ testClient . searchIndex = async ( index ) => {
335+ if ( index === denseRef ) {
336+ return [ { _id : 'd1' , _score : 0.9 , fields : { chunk_text : 'from dense' } } ] ;
337+ }
338+ return [ ] ;
339+ } ;
340+
341+ const out = await client . query ( {
342+ query : 'q' ,
343+ namespace : 'n' ,
344+ topK : 5 ,
345+ useReranking : true ,
346+ } ) ;
347+
348+ expect ( out . degraded ) . toBe ( true ) ;
349+ expect ( out . degradation_reason ) . toBe ( 'rerank_failed: timeout' ) ;
350+ expect ( out . results [ 0 ] ?. reranked ) . toBe ( false ) ;
286351 expect ( spy ) . toHaveBeenCalled ( ) ;
287352 } finally {
288353 spy . mockRestore ( ) ;
@@ -314,7 +379,7 @@ describe('PineconeClient', () => {
314379 useReranking : false ,
315380 } ) ;
316381
317- expect ( results . length ) . toBe ( 2 ) ;
382+ expect ( results . results . length ) . toBe ( 2 ) ;
318383 } ) ;
319384 } ) ;
320385
0 commit comments