Files
trueref-legacy/src/routes/api/v1/api-contract.integration.test.ts
Giancarmine Salucci 6297edf109 chore(TRUEREF-0022): fix lint errors and update architecture docs
- 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
2026-03-30 17:28:38 +02:00

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);
});
});