293 lines
14 KiB
JavaScript
293 lines
14 KiB
JavaScript
/**
|
|
* Event Store Tests
|
|
*
|
|
* Comprehensive tests for the event sourcing implementation.
|
|
* Tests event storage, retrieval, filtering, snapshots, and projections.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { EventStore } from './event-store.js';
|
|
import { AgentStateProjection, TaskHistoryProjection, MemoryIndexProjection } from './projections.js';
|
|
import { createAgentSpawnedEvent, createAgentStartedEvent, createAgentTaskAssignedEvent, createAgentTaskCompletedEvent, createTaskCreatedEvent, createTaskStartedEvent, createTaskCompletedEvent, createMemoryStoredEvent, createMemoryRetrievedEvent, createMemoryDeletedEvent, } from './domain-events.js';
|
|
describe('EventStore', () => {
|
|
let eventStore;
|
|
beforeEach(async () => {
|
|
eventStore = new EventStore({ databasePath: ':memory:', verbose: false });
|
|
await eventStore.initialize();
|
|
});
|
|
afterEach(async () => {
|
|
await eventStore.shutdown();
|
|
});
|
|
describe('Event Appending', () => {
|
|
it('should append events to the store', async () => {
|
|
const event = createAgentSpawnedEvent('agent-1', 'coder', 'core', ['coding', 'testing']);
|
|
await eventStore.append(event);
|
|
const events = await eventStore.getEvents('agent-1');
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0].type).toBe('agent:spawned');
|
|
expect(events[0].version).toBe(1);
|
|
});
|
|
it('should increment version for each event', async () => {
|
|
const event1 = createAgentSpawnedEvent('agent-1', 'coder', 'core', []);
|
|
const event2 = createAgentStartedEvent('agent-1');
|
|
await eventStore.append(event1);
|
|
await eventStore.append(event2);
|
|
const events = await eventStore.getEvents('agent-1');
|
|
expect(events[0].version).toBe(1);
|
|
expect(events[1].version).toBe(2);
|
|
});
|
|
it('should maintain version per aggregate', async () => {
|
|
const agent1Event = createAgentSpawnedEvent('agent-1', 'coder', 'core', []);
|
|
const agent2Event = createAgentSpawnedEvent('agent-2', 'tester', 'quality', []);
|
|
await eventStore.append(agent1Event);
|
|
await eventStore.append(agent2Event);
|
|
const agent1Events = await eventStore.getEvents('agent-1');
|
|
const agent2Events = await eventStore.getEvents('agent-2');
|
|
expect(agent1Events[0].version).toBe(1);
|
|
expect(agent2Events[0].version).toBe(1);
|
|
});
|
|
});
|
|
describe('Event Retrieval', () => {
|
|
beforeEach(async () => {
|
|
// Setup: Create multiple events
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
|
|
await eventStore.append(createAgentStartedEvent('agent-1'));
|
|
await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Build feature', 'Description', 'high', []));
|
|
await eventStore.append(createTaskStartedEvent('task-1', 'agent-1'));
|
|
});
|
|
it('should retrieve events by aggregate ID', async () => {
|
|
const events = await eventStore.getEvents('agent-1');
|
|
expect(events).toHaveLength(2);
|
|
expect(events[0].type).toBe('agent:spawned');
|
|
expect(events[1].type).toBe('agent:started');
|
|
});
|
|
it('should retrieve events from specific version', async () => {
|
|
const events = await eventStore.getEvents('agent-1', 2);
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0].type).toBe('agent:started');
|
|
});
|
|
it('should retrieve events by type', async () => {
|
|
const events = await eventStore.getEventsByType('task:created');
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0].aggregateId).toBe('task-1');
|
|
});
|
|
});
|
|
describe('Event Filtering', () => {
|
|
beforeEach(async () => {
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
|
|
await eventStore.append(createAgentSpawnedEvent('agent-2', 'tester', 'quality', []));
|
|
await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task 1', 'Desc', 'high', []));
|
|
await eventStore.append(createMemoryStoredEvent('mem-1', 'default', 'key1', 'semantic', 1024));
|
|
});
|
|
it('should filter by aggregate types', async () => {
|
|
const events = await eventStore.query({ aggregateTypes: ['agent'] });
|
|
expect(events).toHaveLength(2);
|
|
expect(events.every((e) => e.aggregateType === 'agent')).toBe(true);
|
|
});
|
|
it('should filter by event types', async () => {
|
|
const events = await eventStore.query({ eventTypes: ['agent:spawned'] });
|
|
expect(events).toHaveLength(2);
|
|
expect(events.every((e) => e.type === 'agent:spawned')).toBe(true);
|
|
});
|
|
it('should filter by aggregate IDs', async () => {
|
|
const events = await eventStore.query({ aggregateIds: ['agent-1', 'task-1'] });
|
|
expect(events).toHaveLength(2);
|
|
});
|
|
it('should support pagination', async () => {
|
|
const page1 = await eventStore.query({ limit: 2, offset: 0 });
|
|
const page2 = await eventStore.query({ limit: 2, offset: 2 });
|
|
expect(page1).toHaveLength(2);
|
|
expect(page2).toHaveLength(2);
|
|
expect(page1[0].id).not.toBe(page2[0].id);
|
|
});
|
|
});
|
|
describe('Event Replay', () => {
|
|
it('should replay all events', async () => {
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
|
|
await eventStore.append(createAgentStartedEvent('agent-1'));
|
|
await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task', 'Desc', 'high', []));
|
|
const events = [];
|
|
for await (const event of eventStore.replay()) {
|
|
events.push(event);
|
|
}
|
|
expect(events).toHaveLength(3);
|
|
});
|
|
it('should replay from specific version', async () => {
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
|
|
await eventStore.append(createAgentStartedEvent('agent-1'));
|
|
await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task', 'Desc', 'high', []));
|
|
const events = [];
|
|
for await (const event of eventStore.replay(2)) {
|
|
events.push(event);
|
|
}
|
|
expect(events.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|
|
describe('Snapshots', () => {
|
|
it('should save snapshots', async () => {
|
|
const snapshot = {
|
|
aggregateId: 'agent-1',
|
|
aggregateType: 'agent',
|
|
version: 5,
|
|
state: { status: 'active', tasks: ['task-1'] },
|
|
timestamp: Date.now(),
|
|
};
|
|
await eventStore.saveSnapshot(snapshot);
|
|
const retrieved = await eventStore.getSnapshot('agent-1');
|
|
expect(retrieved).not.toBeNull();
|
|
expect(retrieved?.version).toBe(5);
|
|
expect(retrieved?.state).toEqual({ status: 'active', tasks: ['task-1'] });
|
|
});
|
|
it('should return latest snapshot', async () => {
|
|
const snapshot1 = {
|
|
aggregateId: 'agent-1',
|
|
aggregateType: 'agent',
|
|
version: 5,
|
|
state: { status: 'active' },
|
|
timestamp: Date.now(),
|
|
};
|
|
const snapshot2 = {
|
|
aggregateId: 'agent-1',
|
|
aggregateType: 'agent',
|
|
version: 10,
|
|
state: { status: 'completed' },
|
|
timestamp: Date.now(),
|
|
};
|
|
await eventStore.saveSnapshot(snapshot1);
|
|
await eventStore.saveSnapshot(snapshot2);
|
|
const retrieved = await eventStore.getSnapshot('agent-1');
|
|
expect(retrieved?.version).toBe(10);
|
|
});
|
|
});
|
|
describe('Statistics', () => {
|
|
it('should provide event store statistics', async () => {
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
|
|
await eventStore.append(createAgentSpawnedEvent('agent-2', 'tester', 'quality', []));
|
|
await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task', 'Desc', 'high', []));
|
|
const stats = await eventStore.getStats();
|
|
expect(stats.totalEvents).toBe(3);
|
|
expect(stats.eventsByType['agent:spawned']).toBe(2);
|
|
expect(stats.eventsByType['task:created']).toBe(1);
|
|
expect(stats.eventsByAggregate['agent-1']).toBe(1);
|
|
expect(stats.eventsByAggregate['agent-2']).toBe(1);
|
|
});
|
|
});
|
|
});
|
|
describe('AgentStateProjection', () => {
|
|
let eventStore;
|
|
let projection;
|
|
beforeEach(async () => {
|
|
eventStore = new EventStore({ databasePath: ':memory:' });
|
|
await eventStore.initialize();
|
|
projection = new AgentStateProjection(eventStore);
|
|
});
|
|
afterEach(async () => {
|
|
await eventStore.shutdown();
|
|
});
|
|
it('should build agent state from events', async () => {
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', ['coding']));
|
|
await eventStore.append(createAgentStartedEvent('agent-1'));
|
|
await projection.initialize();
|
|
const agent = projection.getAgent('agent-1');
|
|
expect(agent).not.toBeNull();
|
|
expect(agent?.role).toBe('coder');
|
|
expect(agent?.status).toBe('active');
|
|
});
|
|
it('should track task assignments', async () => {
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
|
|
await eventStore.append(createAgentTaskAssignedEvent('agent-1', 'task-1', Date.now()));
|
|
await projection.initialize();
|
|
const agent = projection.getAgent('agent-1');
|
|
expect(agent?.currentTask).toBe('task-1');
|
|
});
|
|
it('should track completed tasks', async () => {
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
|
|
await eventStore.append(createAgentTaskCompletedEvent('agent-1', 'task-1', { success: true }, Date.now(), 5000));
|
|
await projection.initialize();
|
|
const agent = projection.getAgent('agent-1');
|
|
expect(agent?.completedTasks).toContain('task-1');
|
|
expect(agent?.taskCount).toBe(1);
|
|
});
|
|
it('should filter agents by status', async () => {
|
|
await eventStore.append(createAgentSpawnedEvent('agent-1', 'coder', 'core', []));
|
|
await eventStore.append(createAgentSpawnedEvent('agent-2', 'tester', 'quality', []));
|
|
await eventStore.append(createAgentStartedEvent('agent-1'));
|
|
await projection.initialize();
|
|
const activeAgents = projection.getAgentsByStatus('active');
|
|
const idleAgents = projection.getAgentsByStatus('idle');
|
|
expect(activeAgents).toHaveLength(1);
|
|
expect(idleAgents).toHaveLength(1);
|
|
});
|
|
});
|
|
describe('TaskHistoryProjection', () => {
|
|
let eventStore;
|
|
let projection;
|
|
beforeEach(async () => {
|
|
eventStore = new EventStore({ databasePath: ':memory:' });
|
|
await eventStore.initialize();
|
|
projection = new TaskHistoryProjection(eventStore);
|
|
});
|
|
afterEach(async () => {
|
|
await eventStore.shutdown();
|
|
});
|
|
it('should build task history from events', async () => {
|
|
await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Build Feature', 'Description', 'high', []));
|
|
await eventStore.append(createTaskStartedEvent('task-1', 'agent-1'));
|
|
await eventStore.append(createTaskCompletedEvent('task-1', { success: true }, 5000));
|
|
await projection.initialize();
|
|
const task = projection.getTask('task-1');
|
|
expect(task).not.toBeNull();
|
|
expect(task?.title).toBe('Build Feature');
|
|
expect(task?.status).toBe('completed');
|
|
expect(task?.assignedAgent).toBe('agent-1');
|
|
});
|
|
it('should calculate average task duration', async () => {
|
|
await eventStore.append(createTaskCreatedEvent('task-1', 'implementation', 'Task 1', 'Desc', 'high', []));
|
|
await eventStore.append(createTaskCompletedEvent('task-1', { success: true }, 5000));
|
|
await eventStore.append(createTaskCreatedEvent('task-2', 'implementation', 'Task 2', 'Desc', 'high', []));
|
|
await eventStore.append(createTaskCompletedEvent('task-2', { success: true }, 3000));
|
|
await projection.initialize();
|
|
const avgDuration = projection.getAverageTaskDuration();
|
|
expect(avgDuration).toBe(4000);
|
|
});
|
|
});
|
|
describe('MemoryIndexProjection', () => {
|
|
let eventStore;
|
|
let projection;
|
|
beforeEach(async () => {
|
|
eventStore = new EventStore({ databasePath: ':memory:' });
|
|
await eventStore.initialize();
|
|
projection = new MemoryIndexProjection(eventStore);
|
|
});
|
|
afterEach(async () => {
|
|
await eventStore.shutdown();
|
|
});
|
|
it('should build memory index from events', async () => {
|
|
await eventStore.append(createMemoryStoredEvent('mem-1', 'default', 'key1', 'semantic', 1024));
|
|
await eventStore.append(createMemoryRetrievedEvent('mem-1', 'default', 'key1', 1));
|
|
await projection.initialize();
|
|
const memory = projection.getMemory('mem-1');
|
|
expect(memory).not.toBeNull();
|
|
expect(memory?.namespace).toBe('default');
|
|
expect(memory?.accessCount).toBe(1);
|
|
});
|
|
it('should track memory deletions', async () => {
|
|
await eventStore.append(createMemoryStoredEvent('mem-1', 'default', 'key1', 'semantic', 1024));
|
|
await eventStore.append(createMemoryDeletedEvent('mem-1', 'default', 'key1'));
|
|
await projection.initialize();
|
|
const memory = projection.getMemory('mem-1');
|
|
expect(memory?.isDeleted).toBe(true);
|
|
const activeMemories = projection.getActiveMemories();
|
|
expect(activeMemories).toHaveLength(0);
|
|
});
|
|
it('should calculate total size by namespace', async () => {
|
|
await eventStore.append(createMemoryStoredEvent('mem-1', 'ns1', 'key1', 'semantic', 1024));
|
|
await eventStore.append(createMemoryStoredEvent('mem-2', 'ns1', 'key2', 'semantic', 2048));
|
|
await eventStore.append(createMemoryStoredEvent('mem-3', 'ns2', 'key3', 'semantic', 512));
|
|
await projection.initialize();
|
|
const ns1Size = projection.getTotalSizeByNamespace('ns1');
|
|
const ns2Size = projection.getTotalSizeByNamespace('ns2');
|
|
expect(ns1Size).toBe(3072);
|
|
expect(ns2Size).toBe(512);
|
|
});
|
|
});
|
|
//# sourceMappingURL=event-store.test.js.map
|