import { beforeEach, describe, expect, it, vi } from 'vitest'; import Database from 'better-sqlite3'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import crypto from 'node:crypto'; import { RepositoryService } from '$lib/server/services/repository.service'; import { VersionService } from '$lib/server/services/version.service'; let db: Database.Database; let queue: null = null; vi.mock('$lib/server/db/client', () => ({ getClient: () => db })); vi.mock('$lib/server/db/client.js', () => ({ getClient: () => db })); vi.mock('$lib/server/pipeline/startup', () => ({ getQueue: () => queue })); vi.mock('$lib/server/pipeline/startup.js', () => ({ getQueue: () => queue })); vi.mock('$lib/server/embeddings/registry', () => ({ createProviderFromProfile: () => null })); vi.mock('$lib/server/embeddings/registry.js', () => ({ createProviderFromProfile: () => null })); import { POST as postLibraries } from './libs/+server.js'; import { GET as getLibraries } from './libs/+server.js'; import { GET as getLibrary } from './libs/[id]/+server.js'; import { GET as getJobs } from './jobs/+server.js'; import { GET as getJob } from './jobs/[id]/+server.js'; import { GET as getVersions, POST as postVersions } from './libs/[id]/versions/+server.js'; import { GET as getContext } from './context/+server.js'; import { DEFAULT_TOKEN_BUDGET } from '$lib/server/api/token-budget.js'; const NOW_S = Math.floor(Date.now() / 1000); function createTestDb(): Database.Database { const client = new Database(':memory:'); client.pragma('foreign_keys = ON'); const migrationsFolder = join(import.meta.dirname, '../../../lib/server/db/migrations'); const ftsFile = join(import.meta.dirname, '../../../lib/server/db/fts.sql'); // Apply all migration files in order const migration0 = readFileSync(join(migrationsFolder, '0000_large_master_chief.sql'), 'utf-8'); const migration1 = readFileSync(join(migrationsFolder, '0001_quick_nighthawk.sql'), 'utf-8'); const migration2 = readFileSync(join(migrationsFolder, '0002_silky_stellaris.sql'), 'utf-8'); const migration3 = readFileSync(join(migrationsFolder, '0003_multiversion_config.sql'), 'utf-8'); const migration4 = readFileSync(join(migrationsFolder, '0004_complete_sentry.sql'), 'utf-8'); // Apply first migration const statements0 = migration0 .split('--> statement-breakpoint') .map((statement) => statement.trim()) .filter(Boolean); for (const statement of statements0) { client.exec(statement); } // Apply second migration const statements1 = migration1 .split('--> statement-breakpoint') .map((statement) => statement.trim()) .filter(Boolean); for (const statement of statements1) { client.exec(statement); } const statements2 = migration2 .split('--> statement-breakpoint') .map((statement) => statement.trim()) .filter(Boolean); for (const statement of statements2) { client.exec(statement); } const statements3 = migration3 .split('--> statement-breakpoint') .map((statement) => statement.trim()) .filter(Boolean); for (const statement of statements3) { client.exec(statement); } const statements4 = migration4 .split('--> statement-breakpoint') .map((statement) => statement.trim()) .filter(Boolean); for (const statement of statements4) { client.exec(statement); } client.exec(readFileSync(ftsFile, 'utf-8')); return client; } function seedRepo( client: Database.Database, overrides: { id?: string; title?: string; source?: 'github' | 'local'; sourceUrl?: string; state?: 'pending' | 'indexing' | 'indexed' | 'error'; } = {} ): string { const id = overrides.id ?? '/facebook/react'; client .prepare( `INSERT INTO repositories (id, title, source, source_url, state, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)` ) .run( id, overrides.title ?? 'React', overrides.source ?? 'github', overrides.sourceUrl ?? 'https://github.com/facebook/react', overrides.state ?? 'indexed', NOW_S, NOW_S ); return id; } function seedVersion(client: Database.Database, repositoryId: string, tag: string): string { const versionId = `${repositoryId}/${tag}`; client .prepare( `INSERT INTO repository_versions (id, repository_id, tag, state, total_snippets, indexed_at, created_at) VALUES (?, ?, ?, 'indexed', 0, ?, ?)` ) .run(versionId, repositoryId, tag, NOW_S, NOW_S); return versionId; } function seedDocument( client: Database.Database, repositoryId: string, versionId: string | null = null ): string { const documentId = crypto.randomUUID(); client .prepare( `INSERT INTO documents (id, repository_id, version_id, file_path, checksum, indexed_at) VALUES (?, ?, ?, ?, ?, ?)` ) .run(documentId, repositoryId, versionId, 'README.md', 'checksum', NOW_S); return documentId; } function seedSnippet( client: Database.Database, options: { documentId: string; repositoryId: string; versionId?: string | null; type?: 'code' | 'info'; title?: string | null; content: string; language?: string | null; breadcrumb?: string | null; tokenCount?: number; } ): string { const snippetId = crypto.randomUUID(); client .prepare( `INSERT INTO snippets (id, document_id, repository_id, version_id, type, title, content, language, breadcrumb, token_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) .run( snippetId, options.documentId, options.repositoryId, options.versionId ?? null, options.type ?? 'info', options.title ?? null, options.content, options.language ?? null, options.breadcrumb ?? null, options.tokenCount ?? 0, NOW_S ); return snippetId; } function seedEmbedding(client: Database.Database, snippetId: string, values: number[]): void { client .prepare( `INSERT INTO snippet_embeddings (snippet_id, profile_id, model, dimensions, embedding, created_at) VALUES (?, 'local-default', 'Xenova/all-MiniLM-L6-v2', ?, ?, ?)` ) .run(snippetId, values.length, Buffer.from(Float32Array.from(values).buffer), NOW_S); } describe('API contract integration', () => { beforeEach(() => { db = createTestDb(); queue = null; }); it('POST /api/v1/libs returns repository and job DTOs in camelCase', async () => { const response = await postLibraries({ request: new Request('http://test/api/v1/libs', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: 'github', sourceUrl: 'https://github.com/facebook/react' }) }), url: new URL('http://test/api/v1/libs') } as never); expect(response.status).toBe(201); const body = await response.json(); expect(body.library.sourceUrl).toBe('https://github.com/facebook/react'); expect(body.library.totalSnippets).toBe(0); expect(body.library.lastIndexedAt).toBeNull(); expect(body.library).not.toHaveProperty('source_url'); expect(body.library).not.toHaveProperty('total_snippets'); expect(body.job.repositoryId).toBe('/facebook/react'); expect(body.job.processedFiles).toBe(0); expect(body.job).not.toHaveProperty('repository_id'); expect(body.job).not.toHaveProperty('processed_files'); }); it('GET /api/v1/libs/:id returns repository DTO plus version tags', async () => { const repoService = new RepositoryService(db); const versionService = new VersionService(db); repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' }); versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0'); const response = await getLibrary({ params: { id: encodeURIComponent('/facebook/react') } } as never); expect(response.status).toBe(200); const body = await response.json(); expect(body.sourceUrl).toBe('https://github.com/facebook/react'); expect(body.totalSnippets).toBe(0); expect(body.versions).toEqual(['v18.3.0']); expect(body).not.toHaveProperty('source_url'); expect(body).not.toHaveProperty('total_snippets'); }); it('GET /api/v1/libs includes embedding counts and indexed versions per repository', async () => { const repositoryId = seedRepo(db); const versionId = seedVersion(db, repositoryId, 'v18.3.0'); const baseDocId = seedDocument(db, repositoryId); const versionDocId = seedDocument(db, repositoryId, versionId); const baseSnippetId = seedSnippet(db, { documentId: baseDocId, repositoryId, content: 'Base branch snippet' }); const versionSnippetId = seedSnippet(db, { documentId: versionDocId, repositoryId, versionId, content: 'Versioned snippet' }); seedEmbedding(db, baseSnippetId, [1, 0]); seedEmbedding(db, versionSnippetId, [0, 1]); const response = await getLibraries({ url: new URL('http://test/api/v1/libs') } as never); expect(response.status).toBe(200); const body = await response.json(); expect(body.libraries).toHaveLength(1); expect(body.libraries[0].embeddingCount).toBe(2); expect(body.libraries[0].indexedVersions).toEqual(['main', 'v18.3.0']); }); it('GET /api/v1/jobs and /api/v1/jobs/:id return job DTOs in camelCase', async () => { const repoService = new RepositoryService(db); repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' }); const createdJob = repoService.createIndexingJob('/facebook/react'); const listResponse = await getJobs({ url: new URL('http://test/api/v1/jobs?repositoryId=%2Ffacebook%2Freact') } as never); const listBody = await listResponse.json(); expect(listBody.jobs).toHaveLength(1); expect(listBody.jobs[0].repositoryId).toBe('/facebook/react'); expect(listBody.jobs[0].totalFiles).toBe(0); expect(listBody.jobs[0]).not.toHaveProperty('repository_id'); const itemResponse = await getJob({ params: { id: createdJob.id } } as never); const itemBody = await itemResponse.json(); expect(itemBody.job.repositoryId).toBe('/facebook/react'); expect(itemBody.job.processedFiles).toBe(0); expect(itemBody.job).not.toHaveProperty('processed_files'); }); it('GET and POST /api/v1/libs/:id/versions return version and job DTOs in camelCase', async () => { const repoService = new RepositoryService(db); repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' }); const postResponse = await postVersions({ params: { id: encodeURIComponent('/facebook/react') }, request: new Request('http://test/api/v1/libs/%2Ffacebook%2Freact/versions', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ tag: 'v18.3.0', title: 'React v18.3.0', autoIndex: true }) }) } as never); const postBody = await postResponse.json(); expect(postResponse.status).toBe(201); expect(postBody.version.repositoryId).toBe('/facebook/react'); expect(postBody.version.totalSnippets).toBe(0); expect(postBody.version).not.toHaveProperty('repository_id'); expect(postBody.job.repositoryId).toBe('/facebook/react'); expect(postBody.job).not.toHaveProperty('repository_id'); const getResponse = await getVersions({ params: { id: encodeURIComponent('/facebook/react') } } as never); const getBody = await getResponse.json(); expect(getBody.versions).toHaveLength(1); expect(getBody.versions[0].repositoryId).toBe('/facebook/react'); expect(getBody.versions[0].totalSnippets).toBe(0); expect(getBody.versions[0]).not.toHaveProperty('repository_id'); expect(getBody.versions[0]).not.toHaveProperty('total_snippets'); }); it('POST /api/v1/libs/:id/versions creates distinct jobs for different versions of the same repo', async () => { const repoService = new RepositoryService(db); repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' }); const postV1 = await postVersions({ params: { id: encodeURIComponent('/facebook/react') }, request: new Request('http://test/api/v1/libs/%2Ffacebook%2Freact/versions', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ tag: 'v18.3.0', autoIndex: true }) }) } as never); const bodyV1 = await postV1.json(); const postV2 = await postVersions({ params: { id: encodeURIComponent('/facebook/react') }, request: new Request('http://test/api/v1/libs/%2Ffacebook%2Freact/versions', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ tag: 'v17.0.2', autoIndex: true }) }) } as never); const bodyV2 = await postV2.json(); expect(postV1.status).toBe(201); expect(postV2.status).toBe(201); expect(bodyV1.job.id).not.toBe(bodyV2.job.id); expect(bodyV1.job.versionId).toBe('/facebook/react/v18.3.0'); expect(bodyV2.job.versionId).toBe('/facebook/react/v17.0.2'); }); it('GET /api/v1/context returns informative txt output for empty results', async () => { const repositoryId = seedRepo(db); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(repositoryId)}&query=${encodeURIComponent('no matches here')}&type=txt` ) } as never); expect(response.status).toBe(200); expect(response.headers.get('content-type')).toContain('text/plain'); const body = await response.text(); expect(body).toContain('## Context Results'); expect(body).toContain('No matching snippets found'); expect(body).toContain('Repository: React (/facebook/react)'); expect(body).toContain('Result count: 0'); }); it('GET /api/v1/context does not token-filter default JSON responses for the UI', async () => { const repositoryId = seedRepo(db); const documentId = seedDocument(db, repositoryId); seedSnippet(db, { documentId, repositoryId, type: 'info', title: 'Large result', content: 'Large result body', tokenCount: DEFAULT_TOKEN_BUDGET + 1 }); seedSnippet(db, { documentId, repositoryId, type: 'info', title: 'Small result', content: 'Small result body', tokenCount: 5 }); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(repositoryId)}&query=${encodeURIComponent('result')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); expect(body.snippets).toHaveLength(2); expect(body.resultCount).toBe(2); }); it('GET /api/v1/context returns additive repository and version metadata for versioned results', async () => { const repositoryId = seedRepo(db); const versionId = seedVersion(db, repositoryId, 'v18.3.0'); const documentId = seedDocument(db, repositoryId, versionId); // Insert version-specific rules (versioned queries no longer inherit the NULL row). db.prepare( `INSERT INTO repository_configs (repository_id, version_id, rules, updated_at) VALUES (?, ?, ?, ?)` ).run(repositoryId, versionId, JSON.stringify(['Prefer hooks over classes']), NOW_S); seedSnippet(db, { documentId, repositoryId, versionId, type: 'code', title: 'useThing', content: 'export function useThing() { return true; }', language: 'ts', breadcrumb: 'Hooks > useThing', tokenCount: 42 }); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(`${repositoryId}/v18.3.0`)}&query=${encodeURIComponent('useThing')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); expect(body.snippets).toHaveLength(1); expect(body.rules).toEqual(['Prefer hooks over classes']); expect(body.totalTokens).toBe(42); expect(body.localSource).toBe(false); expect(body.resultCount).toBe(1); expect(body.repository).toEqual({ id: '/facebook/react', title: 'React', source: 'github', sourceUrl: 'https://github.com/facebook/react', branch: 'main', isLocal: false }); expect(body.version).toEqual({ requested: 'v18.3.0', resolved: 'v18.3.0', id: '/facebook/react/v18.3.0' }); expect(body.snippets[0].origin).toEqual({ repositoryId: '/facebook/react', repositoryTitle: 'React', source: 'github', sourceUrl: 'https://github.com/facebook/react', version: 'v18.3.0', versionId: '/facebook/react/v18.3.0', isLocal: false }); }); it('GET /api/v1/context returns only version-specific rules for versioned queries (no NULL row contamination)', async () => { const repositoryId = seedRepo(db); const versionId = seedVersion(db, repositoryId, 'v2.0.0'); const documentId = seedDocument(db, repositoryId, versionId); // Insert repo-wide rules (version_id IS NULL) — these must NOT appear in versioned queries. db.prepare( `INSERT INTO repository_configs (repository_id, version_id, rules, updated_at) VALUES (?, NULL, ?, ?)` ).run(repositoryId, JSON.stringify(['Repo-wide rule']), NOW_S); // Insert version-specific rules. db.prepare( `INSERT INTO repository_configs (repository_id, version_id, rules, updated_at) VALUES (?, ?, ?, ?)` ).run(repositoryId, versionId, JSON.stringify(['Version-specific rule']), NOW_S); seedSnippet(db, { documentId, repositoryId, versionId, content: 'some versioned content' }); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(`${repositoryId}/v2.0.0`)}&query=${encodeURIComponent('versioned content')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); // Only the version-specific rule should appear — NULL row must not contaminate. expect(body.rules).toEqual(['Version-specific rule']); }); it('GET /api/v1/context returns only repo-wide rules when no version is requested', async () => { const repositoryId = seedRepo(db); const documentId = seedDocument(db, repositoryId); // Insert repo-wide rules (version_id IS NULL). db.prepare( `INSERT INTO repository_configs (repository_id, version_id, rules, updated_at) VALUES (?, NULL, ?, ?)` ).run(repositoryId, JSON.stringify(['Repo-wide rule only']), NOW_S); seedSnippet(db, { documentId, repositoryId, content: 'some content' }); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(repositoryId)}&query=${encodeURIComponent('some content')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); expect(body.rules).toEqual(['Repo-wide rule only']); }); it('GET /api/v1/context versioned query returns only the version-specific rules row', async () => { const repositoryId = seedRepo(db); const versionId = seedVersion(db, repositoryId, 'v3.0.0'); const documentId = seedDocument(db, repositoryId, versionId); const sharedRule = 'Use TypeScript strict mode'; // Insert repo-wide NULL row — must NOT bleed into versioned query results. db.prepare( `INSERT INTO repository_configs (repository_id, version_id, rules, updated_at) VALUES (?, NULL, ?, ?)` ).run(repositoryId, JSON.stringify([sharedRule]), NOW_S); db.prepare( `INSERT INTO repository_configs (repository_id, version_id, rules, updated_at) VALUES (?, ?, ?, ?)` ).run(repositoryId, versionId, JSON.stringify([sharedRule, 'Version-only rule']), NOW_S); seedSnippet(db, { documentId, repositoryId, versionId, content: 'dedup test content' }); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(`${repositoryId}/v3.0.0`)}&query=${encodeURIComponent('dedup test')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); // Returns only the version-specific row as stored — no NULL row merge. expect(body.rules).toEqual([sharedRule, 'Version-only rule']); }); it('GET /api/v1/context versioned query returns empty rules when only NULL row exists (no NULL contamination)', async () => { const repositoryId = seedRepo(db); const versionId = seedVersion(db, repositoryId, 'v1.0.0'); const documentId = seedDocument(db, repositoryId, versionId); // Only a repo-wide NULL row exists — no version-specific config. db.prepare( `INSERT INTO repository_configs (repository_id, version_id, rules, updated_at) VALUES (?, NULL, ?, ?)` ).run(repositoryId, JSON.stringify(['HEAD rules that must not contaminate v1']), NOW_S); seedSnippet(db, { documentId, repositoryId, versionId, content: 'v1 content' }); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(`${repositoryId}/v1.0.0`)}&query=${encodeURIComponent('v1 content')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); // No version-specific config row → empty rules. NULL row must not bleed in. expect(body.rules).toEqual([]); }); it('GET /api/v1/context returns 404 with VERSION_NOT_FOUND when version does not exist', async () => { const repositoryId = seedRepo(db); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(`${repositoryId}/v99.0.0`)}&query=${encodeURIComponent('foo')}` ) } as never); expect(response.status).toBe(404); const body = await response.json(); expect(body.code).toBe('VERSION_NOT_FOUND'); }); it('GET /api/v1/context resolves a version by full commit SHA', async () => { const repositoryId = seedRepo(db); const fullSha = 'a'.repeat(40); // Insert version with a commit_hash db.prepare( `INSERT INTO repository_versions (id, repository_id, tag, commit_hash, state, total_snippets, indexed_at, created_at) VALUES (?, ?, ?, ?, 'indexed', 0, ?, ?)` ).run(`${repositoryId}/v2.0.0`, repositoryId, 'v2.0.0', fullSha, NOW_S, NOW_S); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(`${repositoryId}/${fullSha}`)}&query=${encodeURIComponent('anything')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); expect(body.version?.resolved).toBe('v2.0.0'); }); it('GET /api/v1/context resolves a version by short SHA prefix (8 chars)', async () => { const repositoryId = seedRepo(db); const fullSha = 'b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0'; const shortSha = fullSha.slice(0, 8); db.prepare( `INSERT INTO repository_versions (id, repository_id, tag, commit_hash, state, total_snippets, indexed_at, created_at) VALUES (?, ?, ?, ?, 'indexed', 0, ?, ?)` ).run(`${repositoryId}/v3.0.0`, repositoryId, 'v3.0.0', fullSha, NOW_S, NOW_S); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(`${repositoryId}/${shortSha}`)}&query=${encodeURIComponent('anything')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); expect(body.version?.resolved).toBe('v3.0.0'); }); it('GET /api/v1/context includes searchModeUsed in JSON response', async () => { const repositoryId = seedRepo(db); const documentId = seedDocument(db, repositoryId); seedSnippet(db, { documentId, repositoryId, content: 'search mode used test snippet' }); const response = await getContext({ url: new URL( `http://test/api/v1/context?libraryId=${encodeURIComponent(repositoryId)}&query=${encodeURIComponent('search mode used')}` ) } as never); expect(response.status).toBe(200); const body = await response.json(); expect(body.searchModeUsed).toBeDefined(); expect(['keyword', 'semantic', 'hybrid', 'keyword_fallback']).toContain(body.searchModeUsed); }); });