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:
saravanakumardb1 2026-03-20 01:04:32 -07:00
parent 036d17d8f0
commit 28b6668fb1
2 changed files with 90 additions and 1 deletions

View File

@ -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 };
})

View File

@ -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');
});
});