fix(knowledge): align searchChunks scoring with routes, add 5 new tests
- repository.ts: searchChunks now includes tag matching (+2 per tag hit) consistent with scoreChunk() in routes.ts - routes.test.ts: add 5 new tests — stats endpoint, delete draft base, reject non-draft delete, delete source, search chunks - Total: 9 knowledge tests (was 4)
This commit is contained in:
parent
036d17d8f0
commit
28b6668fb1
@ -124,7 +124,8 @@ export async function searchChunks(
|
||||
const text = chunk.contentText.toLowerCase();
|
||||
let score = 0;
|
||||
for (const term of terms) {
|
||||
if (text.includes(term)) score++;
|
||||
if (text.includes(term)) score += 1;
|
||||
if (chunk.tags.some(tag => tag.toLowerCase() === term)) score += 2;
|
||||
}
|
||||
return { chunk, score };
|
||||
})
|
||||
|
||||
@ -6,12 +6,16 @@ const repoMock = {
|
||||
createBase: vi.fn(),
|
||||
getBase: vi.fn(),
|
||||
updateBase: vi.fn(),
|
||||
deleteBase: vi.fn(),
|
||||
listSources: vi.fn(),
|
||||
createSource: vi.fn(),
|
||||
getSource: vi.fn(),
|
||||
updateSource: vi.fn(),
|
||||
deleteSource: vi.fn(),
|
||||
upsertChunks: vi.fn(),
|
||||
listChunks: vi.fn(),
|
||||
searchChunks: vi.fn(),
|
||||
getBaseStats: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('./repository.js', () => repoMock);
|
||||
@ -131,4 +135,88 @@ describe('knowledgeRoutes', () => {
|
||||
sourceId: 'ksrc_1',
|
||||
});
|
||||
});
|
||||
|
||||
it('GET /knowledge/bases/:id/stats returns source and chunk counts', async () => {
|
||||
repoMock.getBase.mockResolvedValue({ id: 'kb_1', productId: 'lysnrai' });
|
||||
repoMock.getBaseStats.mockResolvedValue({
|
||||
knowledgeBaseId: 'kb_1',
|
||||
sourceCount: 3,
|
||||
chunkCount: 42,
|
||||
totalTokens: 8500,
|
||||
indexedSources: 2,
|
||||
pendingSources: 1,
|
||||
failedSources: 0,
|
||||
});
|
||||
|
||||
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||
const res = await app.inject({ method: 'GET', url: '/api/knowledge/bases/kb_1/stats' });
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
expect(body.sourceCount).toBe(3);
|
||||
expect(body.chunkCount).toBe(42);
|
||||
expect(body.totalTokens).toBe(8500);
|
||||
});
|
||||
|
||||
it('DELETE /knowledge/bases/:id deletes draft base', async () => {
|
||||
repoMock.getBase.mockResolvedValue({ id: 'kb_1', productId: 'lysnrai', status: 'draft' });
|
||||
repoMock.deleteBase.mockResolvedValue(undefined);
|
||||
|
||||
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||
const res = await app.inject({ method: 'DELETE', url: '/api/knowledge/bases/kb_1' });
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json()).toEqual({ deleted: true });
|
||||
});
|
||||
|
||||
it('DELETE /knowledge/bases/:id rejects non-draft base', async () => {
|
||||
repoMock.getBase.mockResolvedValue({ id: 'kb_1', productId: 'lysnrai', status: 'active' });
|
||||
|
||||
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||
const res = await app.inject({ method: 'DELETE', url: '/api/knowledge/bases/kb_1' });
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(repoMock.deleteBase).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('DELETE /knowledge/bases/:id/sources/:sourceId deletes a source', async () => {
|
||||
repoMock.getBase.mockResolvedValue({ id: 'kb_1', productId: 'lysnrai' });
|
||||
repoMock.getSource.mockResolvedValue({ id: 'ksrc_1', knowledgeBaseId: 'kb_1' });
|
||||
repoMock.deleteSource.mockResolvedValue(undefined);
|
||||
|
||||
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/knowledge/bases/kb_1/sources/ksrc_1',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(repoMock.deleteSource).toHaveBeenCalledWith('ksrc_1', 'kb_1');
|
||||
});
|
||||
|
||||
it('POST /knowledge/bases/:id/search returns matching chunks', async () => {
|
||||
repoMock.searchChunks.mockResolvedValue([
|
||||
{
|
||||
id: 'chunk_1',
|
||||
sourceId: 'ksrc_1',
|
||||
ordinal: 0,
|
||||
contentText: 'Restart the worker before retrying.',
|
||||
tokenCount: 12,
|
||||
citations: [],
|
||||
tags: ['worker'],
|
||||
},
|
||||
]);
|
||||
|
||||
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/knowledge/bases/kb_1/search',
|
||||
payload: { query: 'worker', limit: 5 },
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
expect(body.count).toBe(1);
|
||||
expect(body.chunks[0].id).toBe('chunk_1');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user