feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
373
dexto/packages/webui/lib/events/EventBus.test.ts
Normal file
373
dexto/packages/webui/lib/events/EventBus.test.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ClientEventBus } from './EventBus.js';
|
||||
import type { ClientEvent, EventMiddleware } from './types.js';
|
||||
|
||||
// Helper to create a test event
|
||||
function createTestEvent(
|
||||
name: 'llm:thinking' | 'llm:chunk' | 'llm:response' = 'llm:thinking',
|
||||
sessionId = 'test-session'
|
||||
): ClientEvent {
|
||||
if (name === 'llm:thinking') {
|
||||
return { name: 'llm:thinking', sessionId };
|
||||
}
|
||||
if (name === 'llm:chunk') {
|
||||
return { name: 'llm:chunk', sessionId, content: 'test', chunkType: 'text' as const };
|
||||
}
|
||||
return {
|
||||
name: 'llm:response',
|
||||
sessionId,
|
||||
content: 'test response',
|
||||
tokenUsage: { totalTokens: 100 },
|
||||
};
|
||||
}
|
||||
|
||||
describe('ClientEventBus', () => {
|
||||
let bus: ClientEventBus;
|
||||
|
||||
beforeEach(() => {
|
||||
bus = new ClientEventBus();
|
||||
});
|
||||
|
||||
describe('dispatch and handlers', () => {
|
||||
it('should dispatch events to registered handlers', () => {
|
||||
const handler = vi.fn();
|
||||
bus.on('llm:thinking', handler);
|
||||
|
||||
const event = createTestEvent('llm:thinking');
|
||||
bus.dispatch(event);
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should dispatch events to wildcard handlers', () => {
|
||||
const wildcardHandler = vi.fn();
|
||||
bus.on('*', wildcardHandler);
|
||||
|
||||
const event = createTestEvent('llm:thinking');
|
||||
bus.dispatch(event);
|
||||
|
||||
expect(wildcardHandler).toHaveBeenCalledTimes(1);
|
||||
expect(wildcardHandler).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should call both specific and wildcard handlers', () => {
|
||||
const specificHandler = vi.fn();
|
||||
const wildcardHandler = vi.fn();
|
||||
|
||||
bus.on('llm:thinking', specificHandler);
|
||||
bus.on('*', wildcardHandler);
|
||||
|
||||
const event = createTestEvent('llm:thinking');
|
||||
bus.dispatch(event);
|
||||
|
||||
expect(specificHandler).toHaveBeenCalledTimes(1);
|
||||
expect(wildcardHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call handlers for different event types', () => {
|
||||
const handler = vi.fn();
|
||||
bus.on('llm:response', handler);
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should support multiple handlers for the same event', () => {
|
||||
const handler1 = vi.fn();
|
||||
const handler2 = vi.fn();
|
||||
|
||||
bus.on('llm:thinking', handler1);
|
||||
bus.on('llm:thinking', handler2);
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscriptions', () => {
|
||||
it('should unsubscribe when subscription.unsubscribe() is called', () => {
|
||||
const handler = vi.fn();
|
||||
const subscription = bus.on('llm:thinking', handler);
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
subscription.unsubscribe();
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
expect(handler).toHaveBeenCalledTimes(1); // Still 1, not 2
|
||||
});
|
||||
|
||||
it('should support once() for single invocation', () => {
|
||||
const handler = vi.fn();
|
||||
bus.once('llm:thinking', handler);
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support off() to remove all handlers for an event', () => {
|
||||
const handler1 = vi.fn();
|
||||
const handler2 = vi.fn();
|
||||
|
||||
bus.on('llm:thinking', handler1);
|
||||
bus.on('llm:thinking', handler2);
|
||||
|
||||
bus.off('llm:thinking');
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
|
||||
expect(handler1).not.toHaveBeenCalled();
|
||||
expect(handler2).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('middleware', () => {
|
||||
it('should execute middleware in order before handlers', () => {
|
||||
const order: string[] = [];
|
||||
|
||||
const middleware1: EventMiddleware = (event, next) => {
|
||||
order.push('middleware1-before');
|
||||
next(event);
|
||||
order.push('middleware1-after');
|
||||
};
|
||||
|
||||
const middleware2: EventMiddleware = (event, next) => {
|
||||
order.push('middleware2-before');
|
||||
next(event);
|
||||
order.push('middleware2-after');
|
||||
};
|
||||
|
||||
bus.use(middleware1);
|
||||
bus.use(middleware2);
|
||||
bus.on('llm:thinking', () => order.push('handler'));
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
|
||||
expect(order).toEqual([
|
||||
'middleware1-before',
|
||||
'middleware2-before',
|
||||
'handler',
|
||||
'middleware2-after',
|
||||
'middleware1-after',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow middleware to modify events', () => {
|
||||
const handler = vi.fn();
|
||||
|
||||
const modifyingMiddleware: EventMiddleware = (event, next) => {
|
||||
if (event.name === 'llm:chunk') {
|
||||
next({
|
||||
...event,
|
||||
content: 'modified content',
|
||||
} as ClientEvent);
|
||||
} else {
|
||||
next(event);
|
||||
}
|
||||
};
|
||||
|
||||
bus.use(modifyingMiddleware);
|
||||
bus.on('llm:chunk', handler);
|
||||
|
||||
bus.dispatch(createTestEvent('llm:chunk'));
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: 'modified content' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow middleware to block events', () => {
|
||||
const handler = vi.fn();
|
||||
|
||||
const blockingMiddleware: EventMiddleware = (_event, _next) => {
|
||||
// Don't call next() - event is blocked
|
||||
};
|
||||
|
||||
bus.use(blockingMiddleware);
|
||||
bus.on('llm:thinking', handler);
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent duplicate middleware registration', () => {
|
||||
const middleware: EventMiddleware = (event, next) => next(event);
|
||||
|
||||
bus.use(middleware);
|
||||
bus.use(middleware); // Duplicate
|
||||
|
||||
expect(bus.middlewareCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow removing middleware', () => {
|
||||
const callCount = vi.fn();
|
||||
const middleware: EventMiddleware = (event, next) => {
|
||||
callCount();
|
||||
next(event);
|
||||
};
|
||||
|
||||
bus.use(middleware);
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
expect(callCount).toHaveBeenCalledTimes(1);
|
||||
|
||||
bus.removeMiddleware(middleware);
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
expect(callCount).toHaveBeenCalledTimes(1); // Still 1
|
||||
});
|
||||
});
|
||||
|
||||
describe('event history', () => {
|
||||
it('should store events in history', () => {
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
bus.dispatch(createTestEvent('llm:response'));
|
||||
|
||||
const history = bus.getHistory();
|
||||
expect(history).toHaveLength(2);
|
||||
expect(history[0].name).toBe('llm:thinking');
|
||||
expect(history[1].name).toBe('llm:response');
|
||||
});
|
||||
|
||||
it('should filter history by predicate', () => {
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
bus.dispatch(createTestEvent('llm:response'));
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
|
||||
const filtered = bus.getHistory((e) => e.name === 'llm:thinking');
|
||||
expect(filtered).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should filter history by session ID', () => {
|
||||
bus.dispatch(createTestEvent('llm:thinking', 'session-1'));
|
||||
bus.dispatch(createTestEvent('llm:thinking', 'session-2'));
|
||||
bus.dispatch(createTestEvent('llm:thinking', 'session-1'));
|
||||
|
||||
const filtered = bus.getHistoryBySession('session-1');
|
||||
expect(filtered).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should clear history', () => {
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
bus.dispatch(createTestEvent('llm:response'));
|
||||
|
||||
bus.clearHistory();
|
||||
|
||||
expect(bus.getHistory()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should limit history to max size', () => {
|
||||
// Dispatch more than MAX_HISTORY_SIZE events
|
||||
for (let i = 0; i < 1050; i++) {
|
||||
bus.dispatch(createTestEvent('llm:thinking', `session-${i}`));
|
||||
}
|
||||
|
||||
const history = bus.getHistory();
|
||||
expect(history.length).toBeLessThanOrEqual(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replay', () => {
|
||||
it('should replay events through the bus', () => {
|
||||
const handler = vi.fn();
|
||||
bus.on('llm:thinking', handler);
|
||||
|
||||
const events = [
|
||||
createTestEvent('llm:thinking', 'session-1'),
|
||||
createTestEvent('llm:thinking', 'session-2'),
|
||||
];
|
||||
|
||||
bus.replay(events);
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should set isReplaying flag during replay', () => {
|
||||
const replayingStates: boolean[] = [];
|
||||
|
||||
const trackingMiddleware: EventMiddleware = (event, next) => {
|
||||
replayingStates.push(bus.isReplaying);
|
||||
next(event);
|
||||
};
|
||||
|
||||
bus.use(trackingMiddleware);
|
||||
|
||||
// Normal dispatch
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
expect(replayingStates).toEqual([false]);
|
||||
|
||||
// Replay
|
||||
bus.replay([createTestEvent('llm:thinking')]);
|
||||
expect(replayingStates).toEqual([false, true]);
|
||||
|
||||
// After replay
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
expect(replayingStates).toEqual([false, true, false]);
|
||||
});
|
||||
|
||||
it('should reset isReplaying flag even if replay throws', () => {
|
||||
const throwingMiddleware: EventMiddleware = () => {
|
||||
throw new Error('Test error');
|
||||
};
|
||||
|
||||
bus.use(throwingMiddleware);
|
||||
|
||||
expect(() => bus.replay([createTestEvent('llm:thinking')])).toThrow();
|
||||
expect(bus.isReplaying).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should catch handler errors and continue to other handlers', () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const throwingHandler = vi.fn(() => {
|
||||
throw new Error('Handler error');
|
||||
});
|
||||
const normalHandler = vi.fn();
|
||||
|
||||
bus.on('llm:thinking', throwingHandler);
|
||||
bus.on('llm:thinking', normalHandler);
|
||||
|
||||
bus.dispatch(createTestEvent('llm:thinking'));
|
||||
|
||||
expect(throwingHandler).toHaveBeenCalled();
|
||||
expect(normalHandler).toHaveBeenCalled();
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('counts', () => {
|
||||
it('should track handler count', () => {
|
||||
expect(bus.handlerCount).toBe(0);
|
||||
|
||||
const sub1 = bus.on('llm:thinking', () => {});
|
||||
expect(bus.handlerCount).toBe(1);
|
||||
|
||||
bus.on('llm:response', () => {});
|
||||
expect(bus.handlerCount).toBe(2);
|
||||
|
||||
sub1.unsubscribe();
|
||||
expect(bus.handlerCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should track middleware count', () => {
|
||||
expect(bus.middlewareCount).toBe(0);
|
||||
|
||||
const mw: EventMiddleware = (e, n) => n(e);
|
||||
bus.use(mw);
|
||||
expect(bus.middlewareCount).toBe(1);
|
||||
|
||||
bus.removeMiddleware(mw);
|
||||
expect(bus.middlewareCount).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user