- Fix 15 ESLint errors across pipeline workers, SSE endpoints, and UI - Replace explicit any with proper entity types in worker entries - Remove unused imports and variables (basename, SSEEvent, getBroadcasterFn, seedRules) - Use empty catch clauses instead of unused error variables - Use SvelteSet for reactive Set state in repository page - Fix operator precedence in nullish coalescing expression - Replace $state+$effect with $derived for concurrency input - Use resolve() directly in href for navigation lint rule - Update ARCHITECTURE.md and FINDINGS.md for worker-thread architecture
699 lines
23 KiB
TypeScript
699 lines
23 KiB
TypeScript
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);
|
|
});
|
|
});
|