UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

275 lines (220 loc) 9.4 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { DataMigration } from '../migration.js'; import { SQLiteManager } from '../sqlite-manager.js'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; describe('DataMigration', () => { let migration: DataMigration; let sqliteManager: SQLiteManager; let tempDir: string; let mockDataDir: string; beforeEach(async () => { // Create temporary directory for test tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'migration-test-')); mockDataDir = path.join(tempDir, 'data'); // Create SQLite manager with temporary database sqliteManager = new SQLiteManager(path.join(tempDir, 'test.db')); await sqliteManager.initialize(); // Clear migration status to allow testing migration await sqliteManager.run( 'DELETE FROM atlas_metadata WHERE key IN (?, ?)', ['migration_status', 'last_migration_timestamp'] ); // Clear any data that might have been migrated during initialization await sqliteManager.run('DELETE FROM agile_sprints'); await sqliteManager.run('DELETE FROM agile_stories'); await sqliteManager.run('DELETE FROM agile_epics'); await sqliteManager.run('DELETE FROM projects WHERE id != ?', ['default-project']); // Create migration instance migration = new DataMigration(sqliteManager, tempDir); }); afterEach(async () => { // Clean up await sqliteManager.close(); await fs.rm(tempDir, { recursive: true, force: true }); }); describe('checkMigrationStatus', () => { it('should detect when no legacy data exists', async () => { const status = await migration.checkMigrationStatus(); expect(status.needsMigration).toBe(false); expect(status.hasLegacyData).toBe(false); expect(status.backupExists).toBe(false); }); it('should detect when legacy data exists', async () => { // Create mock legacy data await fs.mkdir(path.join(mockDataDir, 'agile'), { recursive: true }); await fs.writeFile( path.join(mockDataDir, 'agile', 'sprints.json'), JSON.stringify([{ id: 'test-sprint', name: 'Test Sprint' }]) ); const status = await migration.checkMigrationStatus(); expect(status.hasLegacyData).toBe(true); expect(status.needsMigration).toBe(true); }); it('should detect existing backups', async () => { // Create backup directory await fs.mkdir(path.join(tempDir, 'backup'), { recursive: true }); const status = await migration.checkMigrationStatus(); expect(status.backupExists).toBe(true); }); }); describe('migrate', () => { it('should successfully migrate agile data', async () => { // Create mock agile data await fs.mkdir(path.join(mockDataDir, 'agile'), { recursive: true }); const mockSprints = [ { id: 'sprint-1', name: 'Test Sprint', goal: 'Test goal', status: 'active', duration: 14, team: ['John', 'Jane'] } ]; const mockStories = [ { id: 'story-1', title: 'Test Story', description: 'Test description', status: 'todo', priority: 'medium', sprintId: 'sprint-1' } ]; const mockEpics = [ { id: 'epic-1', title: 'Test Epic', description: 'Test epic description', status: 'planned', priority: 'high' } ]; await fs.writeFile( path.join(mockDataDir, 'agile', 'sprints.json'), JSON.stringify(mockSprints) ); await fs.writeFile( path.join(mockDataDir, 'agile', 'stories.json'), JSON.stringify(mockStories) ); await fs.writeFile( path.join(mockDataDir, 'agile', 'epics.json'), JSON.stringify(mockEpics) ); // Run migration const result = await migration.migrate(); expect(result.success).toBe(true); expect(result.migratedItems).toBeGreaterThan(0); expect(result.backupPath).toBeDefined(); // Verify data was migrated const sprints = await sqliteManager.query('SELECT * FROM agile_sprints'); const stories = await sqliteManager.query('SELECT * FROM agile_stories'); const epics = await sqliteManager.query('SELECT * FROM agile_epics'); expect(sprints.success).toBe(true); expect(sprints.data?.length).toBe(1); expect(stories.success).toBe(true); expect(stories.data?.length).toBe(1); expect(epics.success).toBe(true); expect(epics.data?.length).toBe(1); }); it('should create backup before migration', async () => { // Create mock data await fs.mkdir(path.join(mockDataDir, 'agile'), { recursive: true }); await fs.writeFile( path.join(mockDataDir, 'agile', 'sprints.json'), JSON.stringify([{ id: 'test', name: 'Test' }]) ); // Run migration const result = await migration.migrate(); expect(result.success).toBe(true); expect(result.backupPath).toBeDefined(); // Verify backup was created const backupExists = await fs.access(result.backupPath!) .then(() => true) .catch(() => false); expect(backupExists).toBe(true); }); it('should handle empty data gracefully', async () => { // Create empty data directory await fs.mkdir(mockDataDir, { recursive: true }); const result = await migration.migrate(); expect(result.success).toBe(true); expect(result.migratedItems).toBe(0); }); it('should handle invalid JSON files gracefully', async () => { // Create mock data with invalid JSON await fs.mkdir(path.join(mockDataDir, 'agile'), { recursive: true }); await fs.writeFile( path.join(mockDataDir, 'agile', 'sprints.json'), 'invalid json content' ); const result = await migration.migrate(); expect(result.success).toBe(true); expect(result.errors.length).toBeGreaterThan(0); }); }); describe('hasDataInDatabase', () => { it('should return false when database is empty', async () => { // Clear any data that might have been created during initialization await sqliteManager.run('DELETE FROM agile_sprints'); await sqliteManager.run('DELETE FROM agile_stories'); await sqliteManager.run('DELETE FROM agile_epics'); const hasData = await (migration as any).hasDataInDatabase(); expect(hasData).toBe(false); }); it('should return true when agile data exists', async () => { // Insert test data await sqliteManager.run( 'INSERT INTO projects (id, name, description, config) VALUES (?, ?, ?, ?)', ['test-project', 'Test', 'Test project', '{}'] ); await sqliteManager.run( 'INSERT INTO agile_sprints (id, project_id, name, goal, status, duration, team, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', ['test-sprint', 'test-project', 'Test Sprint', 'Test goal', 'active', 14, '[]', Date.now(), Date.now()] ); const hasData = await (migration as any).hasDataInDatabase(); expect(hasData).toBe(true); }); }); describe('ensureDefaultProject', () => { it('should create default project if it does not exist', async () => { await (migration as any).ensureDefaultProject(); const project = await sqliteManager.get('SELECT * FROM projects WHERE id = ?', ['default-project']); expect(project.success).toBe(true); expect(project.data).toBeDefined(); expect(project.data?.name).toBe('Default Project'); }); it('should not create duplicate default project', async () => { // Create project first await (migration as any).ensureDefaultProject(); // Try to create again await (migration as any).ensureDefaultProject(); const projects = await sqliteManager.query('SELECT * FROM projects WHERE id = ?', ['default-project']); expect(projects.success).toBe(true); expect(projects.data?.length).toBe(1); }); }); describe('error handling', () => { it('should handle database connection errors', async () => { // Close database to simulate connection error await sqliteManager.close(); const result = await migration.migrate(); // Migration completes successfully with 0 items when there's no data to migrate // even if database operations fail, because it handles errors gracefully expect(result.success).toBe(true); expect(result.migratedItems).toBe(0); // Should have some errors logged but migration still succeeds expect(result.errors.length).toBeGreaterThanOrEqual(0); }); it('should handle file system errors gracefully', async () => { // Create migration with invalid path const invalidMigration = new DataMigration(sqliteManager, '/invalid/path'); const status = await invalidMigration.checkMigrationStatus(); expect(status.needsMigration).toBe(false); expect(status.hasLegacyData).toBe(false); }); }); });