FABRIC/src/store.test.ts
jedarden cf7f727210 feat(bd-j1t): first-class worker state machine — NeedleState + gap-based stuck detection
Replace coarse NeedleWorkerStatus ('idle'|'executing'|'draining'|'starting')
with the real NEEDLE state machine: BOOTING→SELECTING→CLAIMING→WORKING→CLOSING→STOPPED.

- Add NeedleState type, VALID_TRANSITIONS map, needleStateToStatus() helper in types.ts
- Track needleState + lastStateTransition per worker by consuming worker.state_transition events
- Surface all six states in TUI worker cards (WorkerGrid, WorkerDetail) with per-state icons/colors
- Surface all six states in web WorkerGrid.tsx with NEEDLE_STATE_LABELS and NEEDLE_STATE_COLORS
- Add getNeedleStateColor/getNeedleStateIcon to colors.ts
- Rewire stuck detection to fire on state-transition gaps (state_gap pattern in stuckDetection.ts)
- Add sequence-based event ordering via compareEventsBySequence and queryOrdered()
- Legacy event-type fallback preserved for workers not emitting state_transition events

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 14:26:16 -04:00

1707 lines
45 KiB
TypeScript

/**
* Tests for FABRIC In-Memory Event Store
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { InMemoryEventStore, getStore, resetStore } from './store.js';
import { LogEvent } from './types.js';
describe('InMemoryEventStore', () => {
let store: InMemoryEventStore;
beforeEach(() => {
store = new InMemoryEventStore();
});
const createEvent = (overrides: Partial<LogEvent> = {}): LogEvent => ({
ts: Date.now(),
worker: 'w-test',
level: 'info',
msg: 'Test message',
...overrides,
});
describe('add', () => {
it('should add an event to the store', () => {
const event = createEvent();
store.add(event);
expect(store.size).toBe(1);
});
it('should add multiple events', () => {
store.add(createEvent({ worker: 'w1' }));
store.add(createEvent({ worker: 'w2' }));
store.add(createEvent({ worker: 'w3' }));
expect(store.size).toBe(3);
});
it('should update worker info when adding event', () => {
const event = createEvent({ worker: 'w-new' });
store.add(event);
const worker = store.getWorker('w-new');
expect(worker).toBeDefined();
expect(worker?.id).toBe('w-new');
});
});
describe('query', () => {
beforeEach(() => {
// Add some test events
store.add(createEvent({ worker: 'w1', level: 'info', bead: 'bd-1', ts: 1000 }));
store.add(createEvent({ worker: 'w1', level: 'debug', bead: 'bd-1', ts: 2000 }));
store.add(createEvent({ worker: 'w2', level: 'error', bead: 'bd-2', ts: 3000 }));
store.add(createEvent({ worker: 'w2', level: 'info', bead: 'bd-2', ts: 4000 }));
store.add(createEvent({ worker: 'w3', level: 'warn', bead: 'bd-3', ts: 5000 }));
});
it('should return all events without filter', () => {
const events = store.query();
expect(events).toHaveLength(5);
});
it('should filter by worker', () => {
const events = store.query({ worker: 'w1' });
expect(events).toHaveLength(2);
expect(events.every((e) => e.worker === 'w1')).toBe(true);
});
it('should filter by level', () => {
const events = store.query({ level: 'error' });
expect(events).toHaveLength(1);
expect(events[0].worker).toBe('w2');
});
it('should filter by bead', () => {
const events = store.query({ bead: 'bd-2' });
expect(events).toHaveLength(2);
expect(events.every((e) => e.bead === 'bd-2')).toBe(true);
});
it('should filter by since timestamp', () => {
const events = store.query({ since: 3000 });
expect(events).toHaveLength(3);
});
it('should filter by until timestamp', () => {
const events = store.query({ until: 3000 });
expect(events).toHaveLength(3);
});
it('should combine multiple filters', () => {
const events = store.query({ worker: 'w2', level: 'error' });
expect(events).toHaveLength(1);
expect(events[0].ts).toBe(3000);
});
it('should return empty array when no matches', () => {
const events = store.query({ worker: 'nonexistent' });
expect(events).toEqual([]);
});
it('should return a copy of events array', () => {
const events1 = store.query();
const events2 = store.query();
expect(events1).not.toBe(events2); // Different array references
expect(events1).toEqual(events2); // Same content
});
});
describe('getWorker', () => {
it('should return undefined for unknown worker', () => {
expect(store.getWorker('unknown')).toBeUndefined();
});
it('should return worker info for known worker', () => {
store.add(createEvent({ worker: 'w-known' }));
const worker = store.getWorker('w-known');
expect(worker).toBeDefined();
expect(worker?.id).toBe('w-known');
expect(worker?.status).toBe('active');
});
});
describe('getWorkers', () => {
it('should return empty array when no events', () => {
expect(store.getWorkers()).toEqual([]);
});
it('should return all workers', () => {
store.add(createEvent({ worker: 'w1' }));
store.add(createEvent({ worker: 'w2' }));
store.add(createEvent({ worker: 'w3' }));
const workers = store.getWorkers();
expect(workers).toHaveLength(3);
expect(workers.map((w) => w.id).sort()).toEqual(['w1', 'w2', 'w3']);
});
});
describe('worker status tracking', () => {
it('should set status to active for new worker', () => {
store.add(createEvent({ worker: 'w-new' }));
const worker = store.getWorker('w-new');
expect(worker?.status).toBe('active');
});
it('should set status to error on error event', () => {
store.add(createEvent({ worker: 'w-test', level: 'error' }));
const worker = store.getWorker('w-test');
expect(worker?.status).toBe('error');
});
it('should set status to idle on bead.completed event', () => {
store.add(createEvent({ worker: 'w-test', msg: 'bead.completed' }));
const worker = store.getWorker('w-test');
expect(worker?.status).toBe('idle');
});
it('should set status to idle on worker.idle event', () => {
store.add(createEvent({ worker: 'w-test', msg: 'worker.idle' }));
const worker = store.getWorker('w-test');
expect(worker?.status).toBe('idle');
});
it('should set status to active on worker.started event', () => {
// First make it idle
store.add(createEvent({ worker: 'w-test', msg: 'bead.completed' }));
// Then active
store.add(createEvent({ worker: 'w-test', msg: 'worker.started' }));
const worker = store.getWorker('w-test');
expect(worker?.status).toBe('active');
});
it('should increment beadsCompleted when bead.completed event has bead', () => {
store.add(createEvent({ worker: 'w-test', msg: 'bead.completed', bead: 'bd-1' }));
store.add(createEvent({ worker: 'w-test', msg: 'bead.completed', bead: 'bd-2' }));
const worker = store.getWorker('w-test');
expect(worker?.beadsCompleted).toBe(2);
});
it('should track firstSeen timestamp', () => {
const earlyTs = 1000;
const lateTs = 5000;
store.add(createEvent({ worker: 'w-test', ts: lateTs }));
store.add(createEvent({ worker: 'w-test', ts: earlyTs }));
const worker = store.getWorker('w-test');
expect(worker?.firstSeen).toBe(lateTs); // First event sets firstSeen
});
it('should track lastActivity timestamp', () => {
const ts1 = 1000;
const ts2 = 5000;
store.add(createEvent({ worker: 'w-test', ts: ts1 }));
store.add(createEvent({ worker: 'w-test', ts: ts2 }));
const worker = store.getWorker('w-test');
expect(worker?.lastActivity).toBe(ts2);
});
it('should track lastEvent', () => {
const event1 = createEvent({ worker: 'w-test', msg: 'First' });
const event2 = createEvent({ worker: 'w-test', msg: 'Second' });
store.add(event1);
store.add(event2);
const worker = store.getWorker('w-test');
expect(worker?.lastEvent?.msg).toBe('Second');
});
});
describe('clear', () => {
it('should clear all events', () => {
store.add(createEvent());
store.add(createEvent());
store.clear();
expect(store.size).toBe(0);
});
it('should clear all workers', () => {
store.add(createEvent({ worker: 'w1' }));
store.add(createEvent({ worker: 'w2' }));
store.clear();
expect(store.getWorkers()).toEqual([]);
});
});
describe('maxEvents limit', () => {
it('should trim old events when over limit', () => {
const smallStore = new InMemoryEventStore(3);
smallStore.add(createEvent({ ts: 1 }));
smallStore.add(createEvent({ ts: 2 }));
smallStore.add(createEvent({ ts: 3 }));
smallStore.add(createEvent({ ts: 4 }));
expect(smallStore.size).toBe(3);
});
it('should keep most recent events', () => {
const smallStore = new InMemoryEventStore(2);
smallStore.add(createEvent({ ts: 1, msg: 'old' }));
smallStore.add(createEvent({ ts: 2, msg: 'mid' }));
smallStore.add(createEvent({ ts: 3, msg: 'new' }));
const events = smallStore.query();
expect(events).toHaveLength(2);
expect(events[0].msg).toBe('mid');
expect(events[1].msg).toBe('new');
});
it('should use default maxEvents of 10000', () => {
const defaultStore = new InMemoryEventStore();
// Add 10001 events
for (let i = 0; i < 10001; i++) {
defaultStore.add(createEvent({ ts: i }));
}
expect(defaultStore.size).toBe(10000);
});
});
describe('size property', () => {
it('should return 0 for empty store', () => {
expect(store.size).toBe(0);
});
it('should return correct count after adds', () => {
store.add(createEvent());
store.add(createEvent());
expect(store.size).toBe(2);
});
});
describe('collision detection', () => {
it('should detect collision when multiple workers modify same file', () => {
const ts = Date.now();
const path = '/src/test.ts';
// Worker 1 modifies file
store.add(createEvent({
worker: 'w1',
path,
tool: 'Edit',
ts
}));
// Worker 2 modifies same file within collision window
store.add(createEvent({
worker: 'w2',
path,
tool: 'Edit',
ts: ts + 1000 // Within 5 second window
}));
const collisions = store.getCollisions();
expect(collisions).toHaveLength(1);
expect(collisions[0].path).toBe(path);
expect(collisions[0].workers).toContain('w1');
expect(collisions[0].workers).toContain('w2');
expect(collisions[0].isActive).toBe(true);
});
it('should not detect collision for events outside time window', () => {
const ts = Date.now();
const path = '/src/test.ts';
// Worker 1 modifies file
store.add(createEvent({
worker: 'w1',
path,
tool: 'Edit',
ts
}));
// Worker 2 modifies same file after collision window
store.add(createEvent({
worker: 'w2',
path,
tool: 'Edit',
ts: ts + 10000 // Outside 5 second window
}));
const collisions = store.getCollisions();
expect(collisions).toHaveLength(0);
});
it('should not detect collision for different files', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
path: '/src/a.ts',
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w2',
path: '/src/b.ts',
tool: 'Edit',
ts: ts + 1000
}));
const collisions = store.getCollisions();
expect(collisions).toHaveLength(0);
});
it('should not detect collision for same worker modifying same file', () => {
const ts = Date.now();
const path = '/src/test.ts';
store.add(createEvent({
worker: 'w1',
path,
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w1',
path,
tool: 'Write',
ts: ts + 1000
}));
const collisions = store.getCollisions();
expect(collisions).toHaveLength(0);
});
it('should only detect collisions for file modification tools', () => {
const ts = Date.now();
const path = '/src/test.ts';
// Read tool - not a modification
store.add(createEvent({
worker: 'w1',
path,
tool: 'Read',
ts
}));
store.add(createEvent({
worker: 'w2',
path,
tool: 'Read',
ts: ts + 1000
}));
const collisions = store.getCollisions();
expect(collisions).toHaveLength(0);
});
it('should detect collisions for Edit, Write, and NotebookEdit tools', () => {
const ts = Date.now();
const path = '/src/test.ts';
store.add(createEvent({
worker: 'w1',
path,
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w2',
path,
tool: 'Write',
ts: ts + 1000
}));
store.add(createEvent({
worker: 'w3',
path,
tool: 'NotebookEdit',
ts: ts + 2000
}));
const collisions = store.getCollisions();
expect(collisions).toHaveLength(1);
expect(collisions[0].workers).toHaveLength(3);
});
it('should set hasCollision flag on worker info', () => {
const ts = Date.now();
const path = '/src/test.ts';
store.add(createEvent({
worker: 'w1',
path,
tool: 'Edit',
ts
}));
expect(store.getWorker('w1')?.hasCollision).toBe(false);
store.add(createEvent({
worker: 'w2',
path,
tool: 'Edit',
ts: ts + 1000
}));
expect(store.getWorker('w1')?.hasCollision).toBe(true);
expect(store.getWorker('w2')?.hasCollision).toBe(true);
});
it('should track active files for workers', () => {
const ts = Date.now();
const path = '/src/test.ts';
store.add(createEvent({
worker: 'w1',
path,
tool: 'Edit',
ts
}));
const worker = store.getWorker('w1');
expect(worker?.activeFiles).toContain(path);
});
it('should get collisions for specific worker', () => {
const ts = Date.now();
const path = '/src/test.ts';
store.add(createEvent({
worker: 'w1',
path,
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w2',
path,
tool: 'Edit',
ts: ts + 1000
}));
const worker1Collisions = store.getWorkerCollisions('w1');
expect(worker1Collisions).toHaveLength(1);
const worker3Collisions = store.getWorkerCollisions('w3');
expect(worker3Collisions).toHaveLength(0);
});
});
describe('Cross-Reference Integration', () => {
it('should track cross-references when events are added', () => {
const ts = Date.now();
// Add events with related entities
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
path: '/src/file.ts',
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
path: '/src/file.ts',
tool: 'Write',
ts: ts + 1000
}));
// Query cross-references
const stats = store.getCrossReferenceStats();
expect(stats.totalLinks).toBeGreaterThan(0);
expect(stats.totalEntities).toBeGreaterThan(0);
});
it('should create links between events and workers', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w-test-123',
msg: 'Starting task',
ts
}));
// Get links for the worker
const links = store.getCrossReferenceLinksForEntity('worker', 'w-test-123');
expect(links.length).toBeGreaterThan(0);
// Should have links to events
const eventLinks = links.filter(l => l.targetType === 'event' || l.sourceType === 'event');
expect(eventLinks.length).toBeGreaterThan(0);
});
it('should create links between events and files', () => {
const ts = Date.now();
const filePath = '/src/test.ts';
store.add(createEvent({
worker: 'w1',
path: filePath,
tool: 'Edit',
ts
}));
// Get links for the file
const links = store.getCrossReferenceLinksForEntity('file', filePath);
expect(links.length).toBeGreaterThan(0);
});
it('should create links between events and beads', () => {
const ts = Date.now();
const beadId = 'bd-test-123';
store.add(createEvent({
worker: 'w1',
bead: beadId,
msg: 'Working on bead',
ts
}));
// Get links for the bead
const links = store.getCrossReferenceLinksForEntity('bead', beadId);
expect(links.length).toBeGreaterThan(0);
});
it('should find linked entities', () => {
const ts = Date.now();
const workerId = 'w-linked';
const filePath = '/src/linked.ts';
store.add(createEvent({
worker: workerId,
path: filePath,
tool: 'Edit',
ts
}));
// Get linked entities for the worker
const linkedEntities = store.getLinkedEntities('worker', workerId);
expect(linkedEntities.length).toBeGreaterThan(0);
// Should include event and/or file entity
// Note: file entity linking happens during batch processing
const hasEventOrFileEntity = linkedEntities.some(
e => (e.type === 'event') || (e.type === 'file' && e.id === filePath)
);
expect(hasEventOrFileEntity).toBe(true);
});
it('should get cross-reference entity details', () => {
const ts = Date.now();
const workerId = 'w-entity-test';
store.add(createEvent({
worker: workerId,
msg: 'Test event',
ts
}));
// Get entity
const entity = store.getCrossReferenceEntity('worker', workerId);
expect(entity).toBeDefined();
expect(entity?.type).toBe('worker');
expect(entity?.id).toBe(workerId);
expect(entity?.linkCount).toBeGreaterThan(0);
});
it('should query cross-references with filters', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
path: '/src/file.ts',
tool: 'Edit',
ts
}));
// Query links by relationship type
const sameBeadLinks = store.queryCrossReferences({ relationship: 'same_bead' });
expect(Array.isArray(sameBeadLinks)).toBe(true);
// Query links by source type
const eventLinks = store.queryCrossReferences({ sourceType: 'event' });
expect(Array.isArray(eventLinks)).toBe(true);
expect(eventLinks.every(l => l.sourceType === 'event')).toBe(true);
});
it('should find navigation paths between entities', () => {
const ts = Date.now();
const workerId = 'w-path';
const beadId = 'bd-path';
store.add(createEvent({
worker: workerId,
bead: beadId,
msg: 'Working',
ts
}));
// Find path from worker to bead
const path = store.findCrossReferencePath('worker', workerId, 'bead', beadId);
// Path may or may not exist depending on link creation timing
if (path) {
expect(path.start.id).toBe(workerId);
expect(path.end.id).toBe(beadId);
expect(path.length).toBeGreaterThan(0);
}
});
it('should clear cross-references when store is cleared', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
ts
}));
let stats = store.getCrossReferenceStats();
expect(stats.totalLinks).toBeGreaterThan(0);
store.clear();
stats = store.getCrossReferenceStats();
expect(stats.totalLinks).toBe(0);
expect(stats.totalEntities).toBe(0);
});
it('should get all cross-reference entities', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
path: '/src/test.ts',
tool: 'Edit',
ts
}));
const allEntities = store.getAllCrossReferenceEntities();
expect(Array.isArray(allEntities)).toBe(true);
expect(allEntities.length).toBeGreaterThan(0);
// Should have different entity types
const types = new Set(allEntities.map(e => e.type));
expect(types.size).toBeGreaterThan(1);
});
it('should get all cross-reference links', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
path: '/src/test.ts',
tool: 'Edit',
ts
}));
const allLinks = store.getAllCrossReferenceLinks();
expect(Array.isArray(allLinks)).toBe(true);
expect(allLinks.length).toBeGreaterThan(0);
// All links should have required fields
allLinks.forEach(link => {
expect(link.id).toBeDefined();
expect(link.sourceType).toBeDefined();
expect(link.targetType).toBeDefined();
expect(link.relationship).toBeDefined();
expect(typeof link.strength).toBe('number');
});
});
});
describe('bead collision detection', () => {
it('should detect collision when multiple workers work on same bead', () => {
const ts = Date.now();
const beadId = 'bd-test';
store.add(createEvent({
worker: 'w1',
bead: beadId,
ts
}));
store.add(createEvent({
worker: 'w2',
bead: beadId,
ts: ts + 5000 // Within 60 second window
}));
const collisions = store.getBeadCollisions();
expect(collisions).toHaveLength(1);
expect(collisions[0].beadId).toBe(beadId);
expect(collisions[0].workers).toContain('w1');
expect(collisions[0].workers).toContain('w2');
expect(collisions[0].isActive).toBe(true);
});
it('should not detect bead collision outside time window', () => {
const ts = Date.now();
const beadId = 'bd-test';
store.add(createEvent({
worker: 'w1',
bead: beadId,
ts
}));
store.add(createEvent({
worker: 'w2',
bead: beadId,
ts: ts + 65000 // Outside 60 second window
}));
const collisions = store.getBeadCollisions();
expect(collisions).toHaveLength(0);
});
it('should set severity to critical when workers use write tools', () => {
const ts = Date.now();
const beadId = 'bd-test';
store.add(createEvent({
worker: 'w1',
bead: beadId,
tool: 'Edit',
path: '/src/test.ts',
ts
}));
store.add(createEvent({
worker: 'w2',
bead: beadId,
tool: 'Write',
path: '/src/test.ts',
ts: ts + 1000
}));
const collisions = store.getBeadCollisions();
expect(collisions).toHaveLength(1);
expect(collisions[0].severity).toBe('critical');
});
it('should set severity to warning for non-write operations', () => {
const ts = Date.now();
const beadId = 'bd-test';
store.add(createEvent({
worker: 'w1',
bead: beadId,
tool: 'Read',
ts
}));
store.add(createEvent({
worker: 'w2',
bead: beadId,
tool: 'Grep',
ts: ts + 1000
}));
const collisions = store.getBeadCollisions();
expect(collisions).toHaveLength(1);
expect(collisions[0].severity).toBe('warning');
});
it('should get bead collisions for specific worker', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
ts
}));
store.add(createEvent({
worker: 'w2',
bead: 'bd-1',
ts: ts + 1000
}));
const w1Collisions = store.getWorkerBeadCollisions('w1');
expect(w1Collisions).toHaveLength(1);
const w3Collisions = store.getWorkerBeadCollisions('w3');
expect(w3Collisions).toHaveLength(0);
});
it('should update worker collision types for bead collision', () => {
const ts = Date.now();
const beadId = 'bd-test';
store.add(createEvent({
worker: 'w1',
bead: beadId,
ts
}));
store.add(createEvent({
worker: 'w2',
bead: beadId,
ts: ts + 1000
}));
const worker1 = store.getWorker('w1');
const worker2 = store.getWorker('w2');
expect(worker1?.collisionTypes).toContain('bead');
expect(worker2?.collisionTypes).toContain('bead');
expect(worker1?.hasCollision).toBe(true);
expect(worker2?.hasCollision).toBe(true);
});
});
describe('task collision detection', () => {
it('should detect collision when workers work in same directory', () => {
const ts = Date.now();
const directory = '/src';
store.add(createEvent({
worker: 'w1',
path: `${directory}/file1.ts`,
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w2',
path: `${directory}/file2.ts`,
tool: 'Edit',
ts: ts + 1000
}));
const collisions = store.getTaskCollisions();
expect(collisions).toHaveLength(1);
expect(collisions[0].type).toBe('directory');
expect(collisions[0].workers).toContain('w1');
expect(collisions[0].workers).toContain('w2');
expect(collisions[0].affectedResources).toContain(directory);
});
it('should set risk level based on active worker count', () => {
const ts = Date.now();
const directory = '/src';
// Add 2 workers (medium risk)
store.add(createEvent({
worker: 'w1',
path: `${directory}/file1.ts`,
ts
}));
store.add(createEvent({
worker: 'w2',
path: `${directory}/file2.ts`,
ts: ts + 100
}));
let collisions = store.getTaskCollisions();
expect(collisions[0].riskLevel).toBe('medium');
// Add 3rd worker (high risk)
store.add(createEvent({
worker: 'w3',
path: `${directory}/file3.ts`,
ts: ts + 200
}));
collisions = store.getTaskCollisions();
expect(collisions[0].riskLevel).toBe('high');
});
it('should track active directories for workers', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
path: '/src/app/file.ts',
ts
}));
store.add(createEvent({
worker: 'w1',
path: '/src/lib/utils.ts',
ts: ts + 100
}));
const worker = store.getWorker('w1');
expect(worker?.activeDirectories).toContain('/src/app');
expect(worker?.activeDirectories).toContain('/src/lib');
});
it('should get task collisions for specific worker', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
path: '/src/file1.ts',
ts
}));
store.add(createEvent({
worker: 'w2',
path: '/src/file2.ts',
ts: ts + 100
}));
const w1Collisions = store.getWorkerTaskCollisions('w1');
expect(w1Collisions).toHaveLength(1);
const w3Collisions = store.getWorkerTaskCollisions('w3');
expect(w3Collisions).toHaveLength(0);
});
});
describe('file heatmap', () => {
beforeEach(() => {
const ts = Date.now();
// Create modification pattern
store.add(createEvent({
worker: 'w1',
path: '/src/hot.ts',
tool: 'Edit',
ts
}));
for (let i = 0; i < 10; i++) {
store.add(createEvent({
worker: 'w1',
path: '/src/hot.ts',
tool: 'Edit',
ts: ts + (i + 1) * 1000
}));
}
// Add 4 modifications for warm level (3-5 modifications)
for (let i = 0; i < 4; i++) {
store.add(createEvent({
worker: 'w2',
path: '/src/warm.ts',
tool: 'Edit',
ts: ts + 500 + i * 100
}));
}
store.add(createEvent({
worker: 'w3',
path: '/src/cold.ts',
tool: 'Edit',
ts: ts + 2000
}));
});
it('should classify heat levels correctly', () => {
const heatmap = store.getFileHeatmap();
const hot = heatmap.find(e => e.path === '/src/hot.ts');
const warm = heatmap.find(e => e.path === '/src/warm.ts');
const cold = heatmap.find(e => e.path === '/src/cold.ts');
expect(hot?.heatLevel).toBe('critical'); // 11+ modifications
expect(warm?.heatLevel).toBe('warm'); // 4 modifications (3-5 = warm)
expect(cold?.heatLevel).toBe('cold'); // 1 modification
});
it('should sort by modification count', () => {
const heatmap = store.getFileHeatmap({ sortBy: 'modifications' });
expect(heatmap[0].path).toBe('/src/hot.ts');
expect(heatmap[0].modifications).toBeGreaterThan(heatmap[1].modifications);
});
it('should filter by directory', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
path: '/lib/utils.ts',
tool: 'Edit',
ts
}));
const srcHeatmap = store.getFileHeatmap({ directoryFilter: '/src' });
expect(srcHeatmap.every(e => e.path.startsWith('/src'))).toBe(true);
});
it('should filter collisions only', () => {
const ts = Date.now();
const path = '/src/collision.ts';
// Create collision
store.add(createEvent({
worker: 'w1',
path,
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w2',
path,
tool: 'Edit',
ts: ts + 1000
}));
const collisionHeatmap = store.getFileHeatmap({ collisionsOnly: true });
expect(collisionHeatmap.every(e => e.hasCollision)).toBe(true);
});
it('should limit max entries', () => {
const heatmap = store.getFileHeatmap({ maxEntries: 2 });
expect(heatmap.length).toBeLessThanOrEqual(2);
});
it('should calculate worker contributions', () => {
const heatmap = store.getFileHeatmap();
const hot = heatmap.find(e => e.path === '/src/hot.ts');
expect(hot?.workers.length).toBeGreaterThan(0);
expect(hot?.workers[0].workerId).toBe('w1');
expect(hot?.workers[0].modifications).toBe(11);
expect(hot?.workers[0].percentage).toBe(100);
});
it('should provide heatmap statistics', () => {
const stats = store.getFileHeatmapStats();
expect(stats.totalFiles).toBeGreaterThan(0);
expect(stats.totalModifications).toBeGreaterThan(0);
expect(stats.heatDistribution.critical).toBeGreaterThan(0);
expect(stats.mostActiveDirectory).toBeDefined();
expect(stats.avgModificationsPerFile).toBeGreaterThan(0);
});
it('should get worker files', () => {
const workerFiles = store.getWorkerFiles('w1');
expect(workerFiles.length).toBeGreaterThan(0);
expect(workerFiles.every(f =>
f.workers.some(w => w.workerId === 'w1')
)).toBe(true);
});
it('should identify collision risk files', () => {
const ts = Date.now();
const path = '/src/risky.ts';
// Multiple workers modify same file
for (let i = 0; i < 3; i++) {
store.add(createEvent({
worker: `w${i + 1}`,
path,
tool: 'Edit',
ts: ts + i * 100
}));
}
const riskFiles = store.getCollisionRiskFiles(3);
expect(riskFiles.some(f => f.path === path)).toBe(true);
});
});
describe('collision alerts', () => {
it('should generate collision alerts for all collision types', () => {
const ts = Date.now();
// File collision
store.add(createEvent({
worker: 'w1',
path: '/src/file.ts',
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w2',
path: '/src/file.ts',
tool: 'Edit',
ts: ts + 1000
}));
// Bead collision
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
ts: ts + 2000
}));
store.add(createEvent({
worker: 'w2',
bead: 'bd-1',
ts: ts + 3000
}));
// Task collision
store.add(createEvent({
worker: 'w1',
path: '/src/dir1/a.ts',
ts: ts + 4000
}));
store.add(createEvent({
worker: 'w2',
path: '/src/dir1/b.ts',
ts: ts + 5000
}));
const alerts = store.generateCollisionAlerts();
expect(alerts.length).toBeGreaterThan(0);
expect(alerts.some(a => a.type === 'file')).toBe(true);
expect(alerts.some(a => a.type === 'bead')).toBe(true);
expect(alerts.some(a => a.type === 'task')).toBe(true);
});
it('should sort alerts by severity', () => {
const ts = Date.now();
// Create critical bead collision
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
tool: 'Edit',
path: '/src/file.ts',
ts
}));
store.add(createEvent({
worker: 'w2',
bead: 'bd-1',
tool: 'Write',
path: '/src/file.ts',
ts: ts + 1000
}));
const alerts = store.generateCollisionAlerts();
const severityOrder = ['critical', 'error', 'warning', 'info'];
for (let i = 1; i < alerts.length; i++) {
const prevIndex = severityOrder.indexOf(alerts[i - 1].severity);
const currIndex = severityOrder.indexOf(alerts[i].severity);
expect(prevIndex).toBeLessThanOrEqual(currIndex);
}
});
it('should get collision statistics', () => {
const ts = Date.now();
// Create various collisions
store.add(createEvent({
worker: 'w1',
path: '/src/file.ts',
tool: 'Edit',
ts
}));
store.add(createEvent({
worker: 'w2',
path: '/src/file.ts',
tool: 'Edit',
ts: ts + 1000
}));
const stats = store.getCollisionStats();
expect(stats.totalFileCollisions).toBeGreaterThanOrEqual(0);
expect(stats.totalBeadCollisions).toBeGreaterThanOrEqual(0);
expect(stats.totalTaskCollisions).toBeGreaterThanOrEqual(0);
expect(stats.activeFileCollisions).toBeGreaterThan(0);
expect(stats.workersWithCollisions).toBeGreaterThan(0);
});
});
describe('error grouping', () => {
it('should track error events in error groups', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
level: 'error',
msg: 'Error: File not found',
ts
}));
const groups = store.getErrorGroups();
expect(groups.length).toBeGreaterThan(0);
});
it('should get active error groups', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
level: 'error',
msg: 'Test error',
ts
}));
const activeGroups = store.getActiveErrorGroups();
expect(Array.isArray(activeGroups)).toBe(true);
});
it('should get worker error groups', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
level: 'error',
msg: 'Worker 1 error',
ts
}));
store.add(createEvent({
worker: 'w2',
level: 'error',
msg: 'Worker 2 error',
ts: ts + 1000
}));
const w1Groups = store.getWorkerErrorGroups('w1');
expect(Array.isArray(w1Groups)).toBe(true);
});
it('should provide error statistics', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
level: 'error',
msg: 'Error message',
ts
}));
const stats = store.getErrorStats();
expect(stats.totalGroups).toBeGreaterThanOrEqual(0);
expect(stats.totalErrors).toBeGreaterThanOrEqual(0);
expect(stats.byCategory).toBeDefined();
expect(stats.bySeverity).toBeDefined();
});
});
describe('concurrent access patterns', () => {
it('should handle multiple workers adding events simultaneously', () => {
const ts = Date.now();
const events = [];
// Simulate concurrent event additions
for (let i = 0; i < 100; i++) {
events.push(createEvent({
worker: `w${i % 10}`,
bead: `bd-${i % 5}`,
ts: ts + i
}));
}
events.forEach(event => store.add(event));
expect(store.size).toBe(100);
expect(store.getWorkers().length).toBe(10);
});
it('should maintain data consistency with rapid queries', () => {
const ts = Date.now();
store.add(createEvent({ worker: 'w1', ts }));
store.add(createEvent({ worker: 'w2', ts: ts + 100 }));
// Rapid queries
const results = [];
for (let i = 0; i < 10; i++) {
results.push(store.query());
}
// All results should be consistent
results.forEach(r => {
expect(r.length).toBe(2);
});
});
it('should handle concurrent collision detection', () => {
const ts = Date.now();
const path = '/src/concurrent.ts';
// Add multiple workers modifying same file
for (let i = 0; i < 5; i++) {
store.add(createEvent({
worker: `w${i}`,
path,
tool: 'Edit',
ts: ts + i * 100
}));
}
const collisions = store.getCollisions();
expect(collisions.length).toBeGreaterThan(0);
expect(collisions[0].workers.length).toBe(5);
});
});
describe('event expiration', () => {
it('should respect maxEvents limit during rapid additions', () => {
const smallStore = new InMemoryEventStore(100);
const ts = Date.now();
// Add more events than limit
for (let i = 0; i < 150; i++) {
smallStore.add(createEvent({ ts: ts + i }));
}
expect(smallStore.size).toBe(100);
});
it('should maintain oldest events when at limit', () => {
const smallStore = new InMemoryEventStore(5);
const ts = Date.now();
for (let i = 0; i < 10; i++) {
smallStore.add(createEvent({
ts: ts + i,
msg: `Event ${i}`
}));
}
const events = smallStore.query();
expect(events[0].msg).toBe('Event 5');
expect(events[events.length - 1].msg).toBe('Event 9');
});
});
describe('worker analytics integration', () => {
it('should provide worker analytics instance', () => {
const analytics = store.getWorkerAnalytics();
expect(analytics).toBeDefined();
});
it('should track analytics for events', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w-analytics',
tool: 'Edit',
path: '/src/file.ts',
ts
}));
// Analytics should be available (basic check)
const analytics = store.getWorkerAnalytics();
expect(analytics).toBeDefined();
});
});
describe('recovery suggestions integration', () => {
it('should provide recovery suggestions for errors', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
level: 'error',
msg: 'Error: ENOENT: no such file or directory',
ts
}));
const suggestions = store.getRecoverySuggestions();
expect(Array.isArray(suggestions)).toBe(true);
});
it('should get recovery statistics', () => {
const stats = store.getRecoveryStats();
expect(stats).toBeDefined();
});
it('should clear recovery suggestions', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w1',
level: 'error',
msg: 'Test error',
ts
}));
store.clearRecoverySuggestions();
// Should not throw
expect(true).toBe(true);
});
});
describe('edge cases', () => {
it('should handle event without worker gracefully', () => {
const event = createEvent({ worker: '' });
store.add(event);
expect(store.size).toBe(1);
});
it('should handle event without timestamp', () => {
const event = { ...createEvent(), ts: undefined as any };
store.add(event);
expect(store.size).toBe(1);
});
it('should handle empty path', () => {
store.add(createEvent({
path: '',
tool: 'Edit'
}));
expect(store.size).toBe(1);
});
it('should handle null/undefined fields in filter', () => {
store.add(createEvent());
const events1 = store.query({ worker: undefined } as any);
expect(events1.length).toBeGreaterThan(0);
const events2 = store.query({ bead: undefined });
expect(events2.length).toBeGreaterThan(0);
});
it('should handle root directory path', () => {
store.add(createEvent({
path: '/file.ts',
tool: 'Edit'
}));
const worker = store.getWorker('w-test');
expect(worker?.activeDirectories).toContain('/');
});
it('should handle file path without directory', () => {
store.add(createEvent({
path: 'file.ts',
tool: 'Edit'
}));
expect(store.size).toBe(1);
});
it('should handle multiple simultaneous collisions on same file', () => {
const ts = Date.now();
const path = '/src/busy.ts';
// Create multiple collision events at nearly same time
for (let i = 0; i < 10; i++) {
store.add(createEvent({
worker: `w${i}`,
path,
tool: 'Edit',
ts: ts + i * 10
}));
}
const collisions = store.getCollisions();
expect(collisions.length).toBeGreaterThan(0);
});
it('should handle query with all filters set', () => {
const ts = Date.now();
store.add(createEvent({
worker: 'w-specific',
level: 'info',
bead: 'bd-specific',
path: '/src/specific.ts',
ts
}));
const events = store.query({
worker: 'w-specific',
level: 'info',
bead: 'bd-specific',
path: '/src/specific.ts',
since: ts - 1000,
until: ts + 1000
});
expect(events.length).toBe(1);
});
});
describe('batch processing', () => {
// Skipped: Batch processing uses setTimeout which can cause test timeouts
it.skip('should handle batch buffer for cross-references', async () => {
const ts = Date.now();
// Add multiple events quickly
for (let i = 0; i < 10; i++) {
store.add(createEvent({
worker: 'w1',
bead: 'bd-1',
path: `/src/file${i}.ts`,
tool: 'Edit',
ts: ts + i * 100
}));
}
// Wait for batch processing (1 second timeout + buffer)
await new Promise(resolve => setTimeout(resolve, 1200));
// Cross-references should be processed
const stats = store.getCrossReferenceStats();
expect(stats.totalLinks).toBeGreaterThan(0);
}, 3000); // 3 second timeout for this test
});
});
describe('sequence-based ordering', () => {
let store: InMemoryEventStore;
beforeEach(() => {
store = new InMemoryEventStore();
});
it('queryOrdered returns events sorted by (worker, sequence), not timestamp', () => {
// Inject events with out-of-order timestamps but monotonic sequences per worker.
// Worker A: sequence 0->2 with timestamps 300, 100, 200 (out of order)
// Worker B: sequence 0->1 with timestamps 50, 150
const events: LogEvent[] = [
{ ts: 300, worker: 'w-a', sequence: 0, level: 'info', msg: 'A-seq0' },
{ ts: 50, worker: 'w-b', sequence: 0, level: 'info', msg: 'B-seq0' },
{ ts: 100, worker: 'w-a', sequence: 1, level: 'info', msg: 'A-seq1' },
{ ts: 150, worker: 'w-b', sequence: 1, level: 'info', msg: 'B-seq1' },
{ ts: 200, worker: 'w-a', sequence: 2, level: 'info', msg: 'A-seq2' },
];
for (const e of events) store.add(e);
const ordered = store.queryOrdered();
// Expected order: all w-a events (seq 0,1,2) then all w-b events (seq 0,1)
// because (worker, sequence) sort groups by worker first.
expect(ordered.map(e => e.msg)).toEqual([
'A-seq0', 'A-seq1', 'A-seq2',
'B-seq0', 'B-seq1',
]);
// Verify timestamps are NOT in order (proving sequence-based, not time-based sort)
const timestamps = ordered.map(e => e.ts);
expect(timestamps).not.toEqual([...timestamps].sort((a, b) => a - b));
});
it('queryOrdered falls back to ts for events without sequence', () => {
const events: LogEvent[] = [
{ ts: 300, worker: 'w-a', level: 'info', msg: 'no-seq-300' },
{ ts: 100, worker: 'w-a', level: 'info', msg: 'no-seq-100' },
{ ts: 200, worker: 'w-a', level: 'info', msg: 'no-seq-200' },
];
for (const e of events) store.add(e);
const ordered = store.queryOrdered();
expect(ordered.map(e => e.msg)).toEqual([
'no-seq-100', 'no-seq-200', 'no-seq-300',
]);
});
it('sequence index stores events keyed by (worker, sequence)', () => {
const event: LogEvent = { ts: Date.now(), worker: 'w-idx', sequence: 42, level: 'info', msg: 'idx-test' };
store.add(event);
// queryOrdered should return the event correctly
const ordered = store.queryOrdered();
expect(ordered).toHaveLength(1);
expect(ordered[0].worker).toBe('w-idx');
expect(ordered[0].sequence).toBe(42);
});
it('handles mixed workers with interleaved sequences', () => {
// Simulate multi-host OTLP ingestion: two workers with overlapping timestamps
// but independent monotonic sequences.
const events: LogEvent[] = [
{ ts: 100, worker: 'host-1', sequence: 0, level: 'info', msg: 'h1-0' },
{ ts: 110, worker: 'host-2', sequence: 0, level: 'info', msg: 'h2-0' },
{ ts: 200, worker: 'host-1', sequence: 1, level: 'info', msg: 'h1-1' },
{ ts: 150, worker: 'host-2', sequence: 1, level: 'info', msg: 'h2-1' },
{ ts: 300, worker: 'host-1', sequence: 2, level: 'info', msg: 'h1-2' },
];
for (const e of events) store.add(e);
const ordered = store.queryOrdered();
expect(ordered.map(e => e.msg)).toEqual([
'h1-0', 'h1-1', 'h1-2',
'h2-0', 'h2-1',
]);
});
});
describe('getStore and resetStore', () => {
beforeEach(() => {
resetStore();
});
afterEach(() => {
resetStore();
});
it('should return the same store instance', () => {
const store1 = getStore();
const store2 = getStore();
expect(store1).toBe(store2);
});
it('should create new store after reset', () => {
const store1 = getStore();
resetStore();
const store2 = getStore();
expect(store1).not.toBe(store2);
});
it('should clear store on reset', () => {
const store = getStore();
store.add({
ts: Date.now(),
worker: 'w-test',
level: 'info',
msg: 'Test',
});
expect(store.size).toBe(1);
resetStore();
const newStore = getStore();
expect(newStore.size).toBe(0);
});
});