Skip to content

Commit 4fe5c4d

Browse files
committed
test: add rerankChain() multi-stage pipeline tests
1 parent 7740a15 commit 4fe5c4d

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { RerankerService } from '../RerankerService';
3+
import type { IRerankerProvider, RerankerInput, RerankerOutput, RerankerRequestConfig } from '../IRerankerService';
4+
5+
/** Create a mock reranker provider that multiplies scores by a factor. */
6+
function mockProvider(id: string, factor: number): IRerankerProvider {
7+
return {
8+
providerId: id,
9+
isAvailable: vi.fn().mockResolvedValue(true),
10+
rerank: vi.fn().mockImplementation(async (input: RerankerInput, config: RerankerRequestConfig): Promise<RerankerOutput> => {
11+
const topN = config.topN ?? input.documents.length;
12+
const results = input.documents
13+
.map(d => ({
14+
id: d.id,
15+
content: d.content,
16+
relevanceScore: (d.originalScore ?? 0.5) * factor,
17+
originalScore: d.originalScore,
18+
metadata: d.metadata,
19+
}))
20+
.sort((a, b) => b.relevanceScore - a.relevanceScore)
21+
.slice(0, topN);
22+
return { results };
23+
}),
24+
};
25+
}
26+
27+
describe('RerankerService.rerankChain()', () => {
28+
it('runs multiple stages sequentially, narrowing results', async () => {
29+
const service = new RerankerService({
30+
config: { providers: [{ providerId: 'stage1' }, { providerId: 'stage2' }] },
31+
});
32+
service.registerProvider(mockProvider('stage1', 1.0));
33+
service.registerProvider(mockProvider('stage2', 0.9));
34+
35+
const chunks = Array.from({ length: 10 }, (_, i) => ({
36+
id: `chunk-${i}`,
37+
content: `Content ${i}`,
38+
relevanceScore: (10 - i) / 10,
39+
heading: '',
40+
sourcePath: '',
41+
metadata: {},
42+
}));
43+
44+
const result = await service.rerankChain('test query', chunks, [
45+
{ provider: 'stage1', topK: 5 },
46+
{ provider: 'stage2', topK: 3 },
47+
]);
48+
49+
expect(result.length).toBe(3);
50+
expect(result[0].metadata?._rerankerChainStages).toBe('stage1,stage2');
51+
});
52+
53+
it('skips unavailable providers and continues', async () => {
54+
const unavailable: IRerankerProvider = {
55+
providerId: 'offline',
56+
isAvailable: vi.fn().mockResolvedValue(false),
57+
rerank: vi.fn(),
58+
};
59+
60+
const service = new RerankerService({
61+
config: { providers: [{ providerId: 'offline' }, { providerId: 'online' }] },
62+
});
63+
service.registerProvider(unavailable);
64+
service.registerProvider(mockProvider('online', 1.0));
65+
66+
const chunks = Array.from({ length: 5 }, (_, i) => ({
67+
id: `c-${i}`, content: `C ${i}`, relevanceScore: 0.5, heading: '', sourcePath: '', metadata: {},
68+
}));
69+
70+
const result = await service.rerankChain('test', chunks, [
71+
{ provider: 'offline', topK: 3 },
72+
{ provider: 'online', topK: 2 },
73+
]);
74+
75+
expect(result.length).toBe(2);
76+
expect(unavailable.rerank).not.toHaveBeenCalled();
77+
expect(result[0].metadata?._rerankerChainStages).toBe('online');
78+
});
79+
80+
it('skips unregistered providers', async () => {
81+
const service = new RerankerService({
82+
config: { providers: [{ providerId: 'real' }] },
83+
});
84+
service.registerProvider(mockProvider('real', 1.0));
85+
86+
const chunks = [{ id: 'c1', content: 'test', relevanceScore: 0.5, heading: '', sourcePath: '', metadata: {} }];
87+
88+
const result = await service.rerankChain('test', chunks, [
89+
{ provider: 'nonexistent', topK: 5 },
90+
{ provider: 'real', topK: 1 },
91+
]);
92+
93+
expect(result.length).toBe(1);
94+
expect(result[0].metadata?._rerankerChainStages).toBe('real');
95+
});
96+
97+
it('returns input unchanged when chain is empty', async () => {
98+
const service = new RerankerService({ config: { providers: [] } });
99+
const chunks = [{ id: 'c1', content: 'test', relevanceScore: 0.5, heading: '', sourcePath: '', metadata: {} }];
100+
101+
const result = await service.rerankChain('test', chunks, []);
102+
expect(result).toEqual(chunks);
103+
});
104+
105+
it('handles stage failure gracefully', async () => {
106+
const failing: IRerankerProvider = {
107+
providerId: 'failing',
108+
isAvailable: vi.fn().mockResolvedValue(true),
109+
rerank: vi.fn().mockRejectedValue(new Error('API down')),
110+
};
111+
112+
const service = new RerankerService({
113+
config: { providers: [{ providerId: 'failing' }, { providerId: 'ok' }] },
114+
});
115+
service.registerProvider(failing);
116+
service.registerProvider(mockProvider('ok', 1.0));
117+
118+
const chunks = Array.from({ length: 5 }, (_, i) => ({
119+
id: `c-${i}`, content: `C ${i}`, relevanceScore: 0.5, heading: '', sourcePath: '', metadata: {},
120+
}));
121+
122+
const result = await service.rerankChain('test', chunks, [
123+
{ provider: 'failing', topK: 3 },
124+
{ provider: 'ok', topK: 2 },
125+
]);
126+
127+
expect(result.length).toBe(2);
128+
expect(result[0].metadata?._rerankerChainStages).toBe('ok');
129+
});
130+
});

0 commit comments

Comments
 (0)