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();
|
const text = chunk.contentText.toLowerCase();
|
||||||
let score = 0;
|
let score = 0;
|
||||||
for (const term of terms) {
|
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 };
|
return { chunk, score };
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,12 +6,16 @@ const repoMock = {
|
|||||||
createBase: vi.fn(),
|
createBase: vi.fn(),
|
||||||
getBase: vi.fn(),
|
getBase: vi.fn(),
|
||||||
updateBase: vi.fn(),
|
updateBase: vi.fn(),
|
||||||
|
deleteBase: vi.fn(),
|
||||||
listSources: vi.fn(),
|
listSources: vi.fn(),
|
||||||
createSource: vi.fn(),
|
createSource: vi.fn(),
|
||||||
getSource: vi.fn(),
|
getSource: vi.fn(),
|
||||||
updateSource: vi.fn(),
|
updateSource: vi.fn(),
|
||||||
|
deleteSource: vi.fn(),
|
||||||
upsertChunks: vi.fn(),
|
upsertChunks: vi.fn(),
|
||||||
listChunks: vi.fn(),
|
listChunks: vi.fn(),
|
||||||
|
searchChunks: vi.fn(),
|
||||||
|
getBaseStats: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock('./repository.js', () => repoMock);
|
vi.mock('./repository.js', () => repoMock);
|
||||||
@ -131,4 +135,88 @@ describe('knowledgeRoutes', () => {
|
|||||||
sourceId: 'ksrc_1',
|
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