tasq/node_modules/@claude-flow/shared/dist/events/event-store.test.js

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