UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

492 lines (395 loc) 16.2 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ProcessStore } from '../process-store.js'; import { ProcessDefinition, ProcessExecution } from '../types.js'; import { ConfigManager } from '../../../config/config-manager.js'; import { InMemoryFileSystemAdapter } from '../file-system-adapter.js'; import path from 'path'; describe('ProcessStore', () => { let store: ProcessStore; let mockConfigManager: vi.Mocked<ConfigManager>; let mockFs: InMemoryFileSystemAdapter; it('should create ProcessStore instance', () => { const config = {} as any; const testStore = new ProcessStore(config); expect(testStore).toBeInstanceOf(ProcessStore); }); beforeEach(async () => { vi.clearAllMocks(); mockConfigManager = { getStorageManager: vi.fn().mockReturnValue({ getModuleDataPath: vi.fn().mockResolvedValue('/mock') }) } as any; mockFs = new InMemoryFileSystemAdapter(); store = new ProcessStore(mockConfigManager, mockFs); }); describe('initialization', () => { it('should create data directory on init', async () => { await store.init(); expect(mockFs.hasDirectory('/mock/processes')).toBe(true); expect(mockFs.hasDirectory('/mock/executions')).toBe(true); expect(mockFs.hasDirectory('/mock/templates')).toBe(true); }); }); describe('saveProcess', () => { it('should save process to file', async () => { await store.init(); const process: ProcessDefinition = createTestProcess(); await store.saveProcess(process); expect(mockFs.hasFile(`/mock/processes/${process.id}.json`)).toBe(true); const savedData = await mockFs.readFile(`/mock/processes/${process.id}.json`, 'utf-8'); expect(JSON.parse(savedData)).toEqual(process); }); it('should update cache after save', async () => { await store.init(); const process = createTestProcess(); await store.saveProcess(process); // Should retrieve from cache without reading file const retrieved = await store.getProcess(process.id); expect(retrieved).toEqual(process); // The file should have been written only once expect(mockFs.getFileCount()).toBe(1); }); }); describe('getProcess', () => { it('should retrieve process from file', async () => { await store.init(); const process = createTestProcess(); // Save the process first await mockFs.writeFile(`/mock/processes/${process.id}.json`, JSON.stringify(process)); const retrieved = await store.getProcess(process.id); expect(retrieved).toEqual(process); }); it('should return null for non-existent process', async () => { await store.init(); // Don't create the file, so it doesn't exist const result = await store.getProcess('non-existent'); expect(result).toBeNull(); }); it('should use cache for repeated reads', async () => { await store.init(); const process = createTestProcess(); // Save the process first await mockFs.writeFile(`/mock/processes/${process.id}.json`, JSON.stringify(process)); // Clear the file to ensure cache is used const fileCountBefore = mockFs.getFileCount(); // First read await store.getProcess(process.id); // Second read const cached = await store.getProcess(process.id); // Should get the same object from cache expect(cached).toEqual(process); // File count should remain the same expect(mockFs.getFileCount()).toBe(fileCountBefore); }); }); describe('getAllProcesses', () => { it('should return all processes', async () => { await store.init(); const process1 = createTestProcess({ id: 'proc-1' }); const process2 = createTestProcess({ id: 'proc-2' }); // Save processes await mockFs.writeFile('/mock/processes/proc-1.json', JSON.stringify(process1)); await mockFs.writeFile('/mock/processes/proc-2.json', JSON.stringify(process2)); const processes = await store.getAllProcesses(); expect(processes).toHaveLength(2); expect(processes).toContainEqual(process1); expect(processes).toContainEqual(process2); }); it('should filter by persona', async () => { await store.init(); const founderProcess = createTestProcess({ id: 'proc-1', persona: 'founder' }); const engineerProcess = createTestProcess({ id: 'proc-2', persona: 'software-engineer' }); // Save processes await mockFs.writeFile('/mock/processes/proc-1.json', JSON.stringify(founderProcess)); await mockFs.writeFile('/mock/processes/proc-2.json', JSON.stringify(engineerProcess)); const processes = await store.getAllProcesses({ persona: 'founder' }); expect(processes).toHaveLength(1); expect(processes[0].persona).toBe('founder'); }); it('should filter by enabled triggers', async () => { await store.init(); const processWithEnabled = createTestProcess({ id: 'proc-1', triggers: [{ id: 't1', type: 'manual', name: 'Manual', enabled: true }] }); const processWithDisabled = createTestProcess({ id: 'proc-2', triggers: [{ id: 't2', type: 'manual', name: 'Manual', enabled: false }] }); // Save processes await mockFs.writeFile('/mock/processes/proc-1.json', JSON.stringify(processWithEnabled)); await mockFs.writeFile('/mock/processes/proc-2.json', JSON.stringify(processWithDisabled)); const processes = await store.getAllProcesses({ hasEnabledTriggers: true }); expect(processes).toHaveLength(1); expect(processes[0].id).toBe('proc-1'); }); it('should handle empty directory', async () => { await store.init(); // Don't add any files const processes = await store.getAllProcesses(); expect(processes).toEqual([]); }); }); describe('deleteProcess', () => { it('should delete process file', async () => { await store.init(); const processId = 'proc-to-delete'; // Save a process first await mockFs.writeFile(`/mock/processes/${processId}.json`, JSON.stringify(createTestProcess({ id: processId }))); await store.deleteProcess(processId); expect(mockFs.hasFile(`/mock/processes/${processId}.json`)).toBe(false); }); it('should remove from cache after delete', async () => { await store.init(); const process = createTestProcess(); // First save and cache it await store.saveProcess(process); // Verify it's cached const cached = await store.getProcess(process.id); expect(cached).toEqual(process); // Then delete await store.deleteProcess(process.id); // Try to get again - should return null const result = await store.getProcess(process.id); expect(result).toBeNull(); }); }); describe('saveExecution', () => { it('should save execution to file', async () => { await store.init(); const execution: ProcessExecution = createTestExecution(); await store.saveExecution(execution); expect(mockFs.hasFile(`/mock/executions/${execution.processId}/${execution.id}.json`)).toBe(true); const savedData = await mockFs.readFile(`/mock/executions/${execution.processId}/${execution.id}.json`, 'utf-8'); expect(JSON.parse(savedData)).toEqual(execution); }); it('should create process execution directory', async () => { await store.init(); const execution = createTestExecution(); await store.saveExecution(execution); expect(mockFs.hasDirectory(`/mock/executions/${execution.processId}`)).toBe(true); }); }); describe('getExecution', () => { it('should retrieve execution from file', async () => { await store.init(); const execution = createTestExecution(); // Save the execution first await mockFs.mkdir(`/mock/executions/${execution.processId}`, { recursive: true }); await mockFs.writeFile( `/mock/executions/${execution.processId}/${execution.id}.json`, JSON.stringify(execution) ); const retrieved = await store.getExecution(execution.processId, execution.id); expect(retrieved).toEqual(execution); }); it('should return null for non-existent execution', async () => { await store.init(); const result = await store.getExecution('proc-1', 'exec-none'); expect(result).toBeNull(); }); }); describe('getProcessExecutions', () => { it('should return all executions for a process', async () => { await store.init(); const processId = 'proc-1'; const exec1 = createTestExecution({ id: 'exec-1', processId }); const exec2 = createTestExecution({ id: 'exec-2', processId }); // Save executions await mockFs.mkdir(`/mock/executions/${processId}`, { recursive: true }); await mockFs.writeFile(`/mock/executions/${processId}/exec-1.json`, JSON.stringify(exec1)); await mockFs.writeFile(`/mock/executions/${processId}/exec-2.json`, JSON.stringify(exec2)); const executions = await store.getProcessExecutions(processId); expect(executions).toHaveLength(2); expect(executions).toContainEqual(exec1); expect(executions).toContainEqual(exec2); }); it('should filter by status', async () => { await store.init(); const processId = 'proc-1'; const completedExec = createTestExecution({ id: 'exec-1', processId, status: 'completed' }); const failedExec = createTestExecution({ id: 'exec-2', processId, status: 'failed' }); // Save executions await mockFs.mkdir(`/mock/executions/${processId}`, { recursive: true }); await mockFs.writeFile(`/mock/executions/${processId}/exec-1.json`, JSON.stringify(completedExec)); await mockFs.writeFile(`/mock/executions/${processId}/exec-2.json`, JSON.stringify(failedExec)); const executions = await store.getProcessExecutions(processId, { status: 'completed' }); expect(executions).toHaveLength(1); expect(executions[0].status).toBe('completed'); }); it('should limit results', async () => { await store.init(); const processId = 'proc-1'; const executions = Array.from({ length: 5 }, (_, i) => createTestExecution({ id: `exec-${i}`, processId }) ); // Save executions await mockFs.mkdir(`/mock/executions/${processId}`, { recursive: true }); for (const exec of executions) { await mockFs.writeFile(`/mock/executions/${processId}/${exec.id}.json`, JSON.stringify(exec)); } const results = await store.getProcessExecutions(processId, { limit: 3 }); expect(results).toHaveLength(3); }); it('should sort by date descending', async () => { await store.init(); const processId = 'proc-1'; const oldExec = createTestExecution({ id: 'exec-old', processId, startedAt: '2024-01-01T00:00:00Z' }); const newExec = createTestExecution({ id: 'exec-new', processId, startedAt: '2024-01-02T00:00:00Z' }); // Save executions await mockFs.mkdir(`/mock/executions/${processId}`, { recursive: true }); await mockFs.writeFile(`/mock/executions/${processId}/exec-old.json`, JSON.stringify(oldExec)); await mockFs.writeFile(`/mock/executions/${processId}/exec-new.json`, JSON.stringify(newExec)); const executions = await store.getProcessExecutions(processId); expect(executions[0].id).toBe('exec-new'); expect(executions[1].id).toBe('exec-old'); }); }); describe('searchProcesses', () => { it('should search by name', async () => { await store.init(); const processes = [ createTestProcess({ id: 'p1', name: 'Daily Report Generator' }), createTestProcess({ id: 'p2', name: 'Weekly Summary' }), createTestProcess({ id: 'p3', name: 'Report Builder' }) ]; // Save processes for (const p of processes) { await mockFs.writeFile(`/mock/processes/${p.id}.json`, JSON.stringify(p)); } const results = await store.searchProcesses('report'); expect(results).toHaveLength(2); expect(results.map(p => p.id).sort()).toEqual(['p1', 'p3']); }); it('should search by description', async () => { await store.init(); const processes = [ createTestProcess({ id: 'p1', name: 'Process 1', description: 'Generates financial reports' }), createTestProcess({ id: 'p2', name: 'Process 2', description: 'Sends email notifications' }) ]; // Save processes for (const p of processes) { await mockFs.writeFile(`/mock/processes/${p.id}.json`, JSON.stringify(p)); } const results = await store.searchProcesses('financial'); expect(results).toHaveLength(1); expect(results[0].id).toBe('p1'); }); it('should be case insensitive', async () => { await store.init(); const process = createTestProcess({ id: 'p1', name: 'UPPERCASE Process' }); await mockFs.writeFile(`/mock/processes/${process.id}.json`, JSON.stringify(process)); const results = await store.searchProcesses('uppercase'); expect(results).toHaveLength(1); }); }); describe('getStats', () => { it('should return process statistics', async () => { await store.init(); const processes = [ createTestProcess({ id: 'p1', persona: 'founder', triggers: [ { id: 't1', type: 'schedule', name: 'Daily', enabled: true, config: { cron: '0 9 * * *' } } ] }), createTestProcess({ id: 'p2', persona: 'founder', triggers: [ { id: 't2', type: 'manual', name: 'Manual', enabled: false } ] }), createTestProcess({ id: 'p3', persona: 'software-engineer', triggers: [ { id: 't3', type: 'event', name: 'Event', enabled: true, config: { eventType: 'test' } } ] }) ]; // Save processes for (const p of processes) { await mockFs.writeFile(`/mock/processes/${p.id}.json`, JSON.stringify(p)); } const stats = await store.getStats(); expect(stats).toEqual({ totalProcesses: 3, byPersona: { founder: 2, 'software-engineer': 1 }, byTriggerType: { schedule: 1, manual: 1, event: 1 }, enabledTriggers: 2, totalTriggers: 3 }); }); }); }); // Helper functions function createTestProcess(overrides: Partial<ProcessDefinition> = {}): ProcessDefinition { return { id: 'process-test-123', name: 'Test Process', version: '1.0.0', triggers: [], activities: [], variables: {}, metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), executionCount: 0 }, ...overrides }; } function createTestExecution(overrides: Partial<ProcessExecution> = {}): ProcessExecution { return { id: 'exec-test-123', processId: 'process-123', processVersion: '1.0.0', status: 'completed', triggeredBy: 'manual', startedAt: new Date().toISOString(), completedAt: new Date().toISOString(), variables: {}, activityResults: [], logs: [], ...overrides }; }