From 28b6668fb12916964bd08d35a74b26d1cf4e20c8 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 20 Mar 2026 01:04:32 -0700 Subject: [PATCH] fix(knowledge): align searchChunks scoring with routes, add 5 new tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../src/modules/knowledge/repository.ts | 3 +- .../src/modules/knowledge/routes.test.ts | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/services/platform-service/src/modules/knowledge/repository.ts b/services/platform-service/src/modules/knowledge/repository.ts index 0fc28122..3514bbd1 100644 --- a/services/platform-service/src/modules/knowledge/repository.ts +++ b/services/platform-service/src/modules/knowledge/repository.ts @@ -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 }; }) diff --git a/services/platform-service/src/modules/knowledge/routes.test.ts b/services/platform-service/src/modules/knowledge/routes.test.ts index a923da21..18323e9b 100644 --- a/services/platform-service/src/modules/knowledge/routes.test.ts +++ b/services/platform-service/src/modules/knowledge/routes.test.ts @@ -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'); + }); });