/** * Unit tests for VersionService. * * Uses an in-memory SQLite database seeded from the canonical migration file. * Returned rows have snake_case column names matching the raw better-sqlite3 * output — the same pattern used by repository.service.test.ts. */ import { describe, it, expect, beforeEach } from 'vitest'; import Database from 'better-sqlite3'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { VersionService } from './version.service'; import { RepositoryService } from './repository.service'; import { AlreadyExistsError, NotFoundError } from '$lib/server/utils/validation'; // --------------------------------------------------------------------------- // Test DB factory // --------------------------------------------------------------------------- function createTestDb(): Database.Database { const client = new Database(':memory:'); client.pragma('foreign_keys = ON'); const migrationsFolder = join(import.meta.dirname, '../db/migrations'); // 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'); // Apply first migration const statements0 = migration0 .split('--> statement-breakpoint') .map((s) => s.trim()) .filter(Boolean); for (const stmt of statements0) { client.exec(stmt); } // Apply second migration const statements1 = migration1 .split('--> statement-breakpoint') .map((s) => s.trim()) .filter(Boolean); for (const stmt of statements1) { client.exec(stmt); } return client; } // Raw row shape returned by better-sqlite3 SELECT * FROM repository_versions. interface RawVersion { id: string; repository_id: string; tag: string; title: string | null; state: string; total_snippets: number; indexed_at: number | null; created_at: number; } function setup() { const client = createTestDb(); const repoService = new RepositoryService(client); const versionService = new VersionService(client); // Add a parent repository used across most tests. repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' }); return { client, repoService, versionService }; } // --------------------------------------------------------------------------- // list() // --------------------------------------------------------------------------- describe('VersionService.list()', () => { it('returns an empty array when no versions exist', () => { const { versionService } = setup(); expect(versionService.list('/facebook/react')).toEqual([]); }); it('returns all versions for a repository', () => { const { versionService } = setup(); versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0'); versionService.add('/facebook/react', 'v17.0.2', 'React v17.0.2'); const versions = versionService.list('/facebook/react'); expect(versions).toHaveLength(2); }); it('returns newest versions first', () => { const { client, versionService } = setup(); const now = Math.floor(Date.now() / 1000); // Insert with explicit timestamps to guarantee ordering. client .prepare( `INSERT INTO repository_versions (id, repository_id, tag, state, total_snippets, created_at) VALUES (?, '/facebook/react', ?, 'pending', 0, ?)` ) .run('/facebook/react/v17.0.2', 'v17.0.2', now - 10); client .prepare( `INSERT INTO repository_versions (id, repository_id, tag, state, total_snippets, created_at) VALUES (?, '/facebook/react', ?, 'pending', 0, ?)` ) .run('/facebook/react/v18.3.0', 'v18.3.0', now); const versions = versionService.list('/facebook/react') as unknown as RawVersion[]; // Newest created_at first. expect(versions[0].tag).toBe('v18.3.0'); }); it('returns empty array for an unknown repository (no FK check on list)', () => { const { versionService } = setup(); expect(versionService.list('/unknown/repo')).toEqual([]); }); }); // --------------------------------------------------------------------------- // add() // --------------------------------------------------------------------------- describe('VersionService.add()', () => { it('creates a version with the correct ID format', () => { const { versionService } = setup(); const version = versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0'); expect(version.id).toBe('/facebook/react/v18.3.0'); expect(version.repositoryId).toBe('/facebook/react'); expect(version.tag).toBe('v18.3.0'); expect(version.title).toBe('React v18.3.0'); expect(version.state).toBe('pending'); expect(version.totalSnippets).toBe(0); expect(version.indexedAt).toBeNull(); }); it('creates a version without a title', () => { const { versionService } = setup(); const version = versionService.add('/facebook/react', 'v18.3.0'); expect(version.title).toBeNull(); }); it('throws NotFoundError when the parent repository does not exist', () => { const { versionService } = setup(); expect(() => versionService.add('/unknown/repo', 'v1.0.0')).toThrow(NotFoundError); }); it('throws AlreadyExistsError when adding a duplicate tag', () => { const { versionService } = setup(); versionService.add('/facebook/react', 'v18.3.0'); expect(() => versionService.add('/facebook/react', 'v18.3.0')).toThrow(AlreadyExistsError); }); it('allows the same tag for different repositories', () => { const { repoService, versionService } = setup(); // Use a repo name without dots so resolveGitHubId produces a predictable ID. repoService.add({ source: 'github', sourceUrl: 'https://github.com/vercel/nextjs' }); versionService.add('/facebook/react', 'v18.3.0'); const v = versionService.add('/vercel/nextjs', 'v18.3.0'); expect(v.id).toBe('/vercel/nextjs/v18.3.0'); }); }); // --------------------------------------------------------------------------- // remove() // --------------------------------------------------------------------------- describe('VersionService.remove()', () => { it('removes an existing version', () => { const { versionService } = setup(); versionService.add('/facebook/react', 'v18.3.0'); versionService.remove('/facebook/react', 'v18.3.0'); expect(versionService.getByTag('/facebook/react', 'v18.3.0')).toBeNull(); }); it('throws NotFoundError when the version does not exist', () => { const { versionService } = setup(); expect(() => versionService.remove('/facebook/react', 'v99.0.0')).toThrow(NotFoundError); }); it('cascades to documents and snippets on delete', () => { const { client, versionService } = setup(); versionService.add('/facebook/react', 'v18.3.0'); const now = Math.floor(Date.now() / 1000); const docId = crypto.randomUUID(); client .prepare( `INSERT INTO documents (id, repository_id, version_id, file_path, checksum, indexed_at) VALUES (?, '/facebook/react', '/facebook/react/v18.3.0', 'README.md', 'abc', ?)` ) .run(docId, now); versionService.remove('/facebook/react', 'v18.3.0'); const doc = client.prepare(`SELECT id FROM documents WHERE id = ?`).get(docId); expect(doc).toBeUndefined(); }); }); // --------------------------------------------------------------------------- // getByTag() // --------------------------------------------------------------------------- describe('VersionService.getByTag()', () => { it('returns null when the version does not exist', () => { const { versionService } = setup(); expect(versionService.getByTag('/facebook/react', 'v99.0.0')).toBeNull(); }); it('returns the version record when it exists', () => { const { versionService } = setup(); versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0'); const version = versionService.getByTag('/facebook/react', 'v18.3.0'); expect(version).not.toBeNull(); if (!version) throw new Error('Expected version to exist'); expect(version.tag).toBe('v18.3.0'); expect(version?.repositoryId).toBe('/facebook/react'); }); }); // --------------------------------------------------------------------------- // registerFromConfig() // --------------------------------------------------------------------------- describe('VersionService.registerFromConfig()', () => { it('registers multiple versions from config', () => { const { versionService } = setup(); const result = versionService.registerFromConfig('/facebook/react', [ { tag: 'v18.3.0', title: 'React v18.3.0' }, { tag: 'v17.0.2', title: 'React v17.0.2' } ]); expect(result).toHaveLength(2); expect(versionService.list('/facebook/react')).toHaveLength(2); }); it('skips already-registered tags idempotently', () => { const { versionService } = setup(); versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0'); const result = versionService.registerFromConfig('/facebook/react', [ { tag: 'v18.3.0', title: 'React v18.3.0 (duplicate)' }, { tag: 'v17.0.2', title: 'React v17.0.2' } ]); // Both entries are returned but only one new row is created. expect(result).toHaveLength(2); expect(versionService.list('/facebook/react')).toHaveLength(2); }); it('returns an empty array when given an empty previousVersions list', () => { const { versionService } = setup(); const result = versionService.registerFromConfig('/facebook/react', []); expect(result).toEqual([]); }); it('throws NotFoundError when the parent repository does not exist', () => { const { versionService } = setup(); expect(() => versionService.registerFromConfig('/unknown/repo', [{ tag: 'v1.0.0', title: 'v1' }]) ).toThrow(NotFoundError); }); it('sets all registered versions to state pending', () => { const { versionService } = setup(); versionService.registerFromConfig('/facebook/react', [ { tag: 'v18.3.0', title: 'React v18.3.0' } ]); const version = versionService.getByTag('/facebook/react', 'v18.3.0'); if (!version) throw new Error('Expected version to exist'); expect(version.state).toBe('pending'); }); });