@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
492 lines (395 loc) • 16.2 kB
text/typescript
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
};
}