import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; import { resetDb } from '../helpers/db.js'; import { migrateUp } from '../../lib/db/migrate.js'; import * as search from '../../lib/db/repos/search.js'; import * as spacesRepo from '../../lib/db/repos/spaces.js'; import * as pagesRepo from '../../lib/db/repos/pages.js'; import * as refsRepo from '../../lib/db/repos/refs.js'; import * as resourcesRepo from '../../lib/db/repos/resources.js'; import * as sourceDocsRepo from '../../lib/db/repos/source_docs.js'; import * as conversationsRepo from '../../lib/db/repos/conversations.js'; import * as messagesRepo from '../../lib/db/repos/messages.js'; const owner = { kind: 'user', id: null }; let space, otherSpace; beforeAll(async () => { await resetDb(); await migrateUp(); }); beforeEach(async () => { await resetDb(); await migrateUp(); space = await spacesRepo.create({ slug: 's-main', name: 'Main' }, owner); otherSpace = await spacesRepo.create({ slug: 's-other', name: 'Other' }, owner); }); async function seedAll(word) { await pagesRepo.create( { space_id: space.id, slug: 'pg-search', title: `${word} page`, body_md: `body about ${word}` }, owner ); await refsRepo.create( { space_id: space.id, kind: 'url', source_url: 'https://example.com/x', title: `${word} reference`, body_text: `text mentioning ${word}` }, owner ); const res = await resourcesRepo.create( { space_id: space.id, slug: 'r-search', name: 'Res', runtime_type: 'lxc' }, owner ); await sourceDocsRepo.create( { resource_id: res.id, name: `${word} source doc`, upstream_url: 'https://example.com/sd', body_text: `doc body about ${word}` }, owner ); const conv = await conversationsRepo.create({ title: 'chat' }, owner); await messagesRepo.append(conv.id, { role: 'user', body: `let's talk about ${word}` }); } describe('search repo', () => { it('fts returns 4 hits across all kinds for the query word', async () => { await seedAll('blackflame'); const hits = await search.fts({ q: 'blackflame' }); expect(hits.length).toBe(4); const kinds = new Set(hits.map(h => h.kind)); expect(kinds).toEqual(new Set(['page','ref','source_doc','message'])); for (const h of hits) { expect(typeof h.id).toBe('string'); expect(typeof h.title_or_snippet).toBe('string'); expect(typeof h.rank).toBe('number'); } }); it('kinds filter narrows to requested branches', async () => { await seedAll('blackflame'); const hits = await search.fts({ q: 'blackflame', kinds: ['page','ref'] }); expect(hits.length).toBe(2); expect(new Set(hits.map(h => h.kind))).toEqual(new Set(['page','ref'])); }); it('space_id filter scopes pages/refs/source_docs (messages excluded)', async () => { await seedAll('blackflame'); await pagesRepo.create( { space_id: otherSpace.id, slug: 'pg-other', title: 'blackflame other', body_md: '' }, owner ); const hits = await search.fts({ q: 'blackflame', space_id: space.id }); // page+ref+source_doc from main space; no messages (no space); no other-space page expect(hits.length).toBe(3); expect(hits.every(h => !h.kind.includes('message'))).toBe(true); expect(hits.every(h => h.space_id === space.id)).toBe(true); }); it('orders by rank desc and respects limit + offset', async () => { await seedAll('blackflame'); const all = await search.fts({ q: 'blackflame', limit: 100 }); const limited = await search.fts({ q: 'blackflame', limit: 2 }); expect(limited.length).toBe(2); const ranks = all.map(h => h.rank); const sorted = [...ranks].sort((a, b) => b - a); expect(ranks).toEqual(sorted); const off = await search.fts({ q: 'blackflame', limit: 2, offset: 2 }); expect(off.length).toBe(2); expect(off[0].id).not.toBe(limited[0].id); }); it('returns empty when nothing matches', async () => { await seedAll('blackflame'); const hits = await search.fts({ q: 'whitelight' }); expect(hits).toEqual([]); }); });