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:
512
dexto/packages/webui/lib/events/integration.test.ts
Normal file
512
dexto/packages/webui/lib/events/integration.test.ts
Normal file
@@ -0,0 +1,512 @@
|
||||
/**
|
||||
* EventBus Integration Tests
|
||||
*
|
||||
* Tests the full flow of events through the EventBus to stores:
|
||||
* Event → EventBus → Handlers → Store Actions → State Updates
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { ClientEventBus } from './EventBus.js';
|
||||
import { setupEventHandlers } from './handlers.js';
|
||||
import { useChatStore } from '../stores/chatStore.js';
|
||||
import { useAgentStore } from '../stores/agentStore.js';
|
||||
import { ApprovalType, ApprovalStatus } from '@dexto/core';
|
||||
|
||||
describe('EventBus Integration', () => {
|
||||
let bus: ClientEventBus;
|
||||
let cleanup: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
bus = new ClientEventBus();
|
||||
cleanup = setupEventHandlers(bus);
|
||||
|
||||
// Reset stores to clean state
|
||||
useChatStore.setState({ sessions: new Map() });
|
||||
useAgentStore.setState({
|
||||
status: 'idle',
|
||||
connectionStatus: 'disconnected',
|
||||
lastHeartbeat: null,
|
||||
activeSessionId: null,
|
||||
currentToolName: null,
|
||||
connectionError: null,
|
||||
reconnectAttempts: 0,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// LLM Events
|
||||
// =========================================================================
|
||||
|
||||
describe('LLM Events', () => {
|
||||
it('should process llm:thinking and update stores', () => {
|
||||
bus.dispatch({
|
||||
name: 'llm:thinking',
|
||||
sessionId: 'test-session',
|
||||
});
|
||||
|
||||
// Check agent status
|
||||
expect(useAgentStore.getState().status).toBe('thinking');
|
||||
expect(useAgentStore.getState().activeSessionId).toBe('test-session');
|
||||
|
||||
// Check chat processing state
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.processing).toBe(true);
|
||||
});
|
||||
|
||||
it('should process llm:chunk and create streaming message', () => {
|
||||
bus.dispatch({
|
||||
name: 'llm:chunk',
|
||||
sessionId: 'test-session',
|
||||
content: 'Hello',
|
||||
chunkType: 'text',
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.streamingMessage).not.toBeNull();
|
||||
expect(sessionState.streamingMessage?.content).toBe('Hello');
|
||||
expect(sessionState.streamingMessage?.role).toBe('assistant');
|
||||
});
|
||||
|
||||
it('should append chunks to streaming message', () => {
|
||||
// First chunk
|
||||
bus.dispatch({
|
||||
name: 'llm:chunk',
|
||||
sessionId: 'test-session',
|
||||
content: 'Hello',
|
||||
chunkType: 'text',
|
||||
});
|
||||
|
||||
// Second chunk
|
||||
bus.dispatch({
|
||||
name: 'llm:chunk',
|
||||
sessionId: 'test-session',
|
||||
content: ' world',
|
||||
chunkType: 'text',
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.streamingMessage?.content).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('should handle reasoning chunks separately', () => {
|
||||
// Text chunk
|
||||
bus.dispatch({
|
||||
name: 'llm:chunk',
|
||||
sessionId: 'test-session',
|
||||
content: 'Answer',
|
||||
chunkType: 'text',
|
||||
});
|
||||
|
||||
// Reasoning chunk
|
||||
bus.dispatch({
|
||||
name: 'llm:chunk',
|
||||
sessionId: 'test-session',
|
||||
content: 'Thinking...',
|
||||
chunkType: 'reasoning',
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.streamingMessage?.content).toBe('Answer');
|
||||
expect(sessionState.streamingMessage?.reasoning).toBe('Thinking...');
|
||||
});
|
||||
|
||||
it('should finalize streaming message on llm:response', () => {
|
||||
// Create streaming message
|
||||
bus.dispatch({
|
||||
name: 'llm:chunk',
|
||||
sessionId: 'test-session',
|
||||
content: 'Complete response',
|
||||
chunkType: 'text',
|
||||
});
|
||||
|
||||
// Finalize
|
||||
bus.dispatch({
|
||||
name: 'llm:response',
|
||||
sessionId: 'test-session',
|
||||
content: 'Complete response',
|
||||
model: 'gpt-4',
|
||||
provider: 'openai',
|
||||
tokenUsage: {
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
totalTokens: 30,
|
||||
},
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.streamingMessage).toBeNull();
|
||||
expect(sessionState.messages).toHaveLength(1);
|
||||
expect(sessionState.messages[0].content).toBe('Complete response');
|
||||
expect(sessionState.messages[0].model).toBe('gpt-4');
|
||||
expect(sessionState.messages[0].tokenUsage?.totalTokens).toBe(30);
|
||||
});
|
||||
|
||||
it('should handle llm:error and update stores', () => {
|
||||
bus.dispatch({
|
||||
name: 'llm:error',
|
||||
sessionId: 'test-session',
|
||||
error: {
|
||||
name: 'TestError',
|
||||
message: 'Something went wrong',
|
||||
},
|
||||
context: 'During generation',
|
||||
recoverable: true,
|
||||
});
|
||||
|
||||
// Check agent status
|
||||
expect(useAgentStore.getState().status).toBe('idle');
|
||||
|
||||
// Check error state
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.error).not.toBeNull();
|
||||
expect(sessionState.error?.message).toBe('Something went wrong');
|
||||
expect(sessionState.error?.recoverable).toBe(true);
|
||||
expect(sessionState.processing).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Tool Events
|
||||
// =========================================================================
|
||||
|
||||
describe('Tool Events', () => {
|
||||
it('should process llm:tool-call and create tool message', () => {
|
||||
bus.dispatch({
|
||||
name: 'llm:tool-call',
|
||||
sessionId: 'test-session',
|
||||
toolName: 'read_file',
|
||||
args: { path: '/test.txt' },
|
||||
callId: 'call-123',
|
||||
});
|
||||
|
||||
// Check agent status
|
||||
expect(useAgentStore.getState().status).toBe('executing_tool');
|
||||
expect(useAgentStore.getState().currentToolName).toBe('read_file');
|
||||
|
||||
// Check tool message
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.messages).toHaveLength(1);
|
||||
expect(sessionState.messages[0].toolName).toBe('read_file');
|
||||
expect(sessionState.messages[0].toolCallId).toBe('call-123');
|
||||
});
|
||||
|
||||
it('should update tool message with result', () => {
|
||||
// Create tool call
|
||||
bus.dispatch({
|
||||
name: 'llm:tool-call',
|
||||
sessionId: 'test-session',
|
||||
toolName: 'read_file',
|
||||
args: { path: '/test.txt' },
|
||||
callId: 'call-123',
|
||||
});
|
||||
|
||||
// Add result
|
||||
const sanitizedResult = {
|
||||
content: [{ type: 'text' as const, text: 'File contents' }],
|
||||
meta: { toolName: 'read_file', toolCallId: 'call-123', success: true },
|
||||
};
|
||||
bus.dispatch({
|
||||
name: 'llm:tool-result',
|
||||
sessionId: 'test-session',
|
||||
toolName: 'read_file',
|
||||
callId: 'call-123',
|
||||
success: true,
|
||||
sanitized: sanitizedResult,
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
const toolMessage = sessionState.messages[0];
|
||||
expect(toolMessage.toolResult).toEqual(sanitizedResult);
|
||||
expect(toolMessage.toolResultSuccess).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle tool result with approval requirements', () => {
|
||||
// Create tool call
|
||||
bus.dispatch({
|
||||
name: 'llm:tool-call',
|
||||
sessionId: 'test-session',
|
||||
toolName: 'write_file',
|
||||
args: { path: '/test.txt', content: 'data' },
|
||||
callId: 'call-456',
|
||||
});
|
||||
|
||||
// Add result with approval
|
||||
const sanitizedResult = {
|
||||
content: [{ type: 'text' as const, text: 'File written' }],
|
||||
meta: { toolName: 'write_file', toolCallId: 'call-456', success: true },
|
||||
};
|
||||
bus.dispatch({
|
||||
name: 'llm:tool-result',
|
||||
sessionId: 'test-session',
|
||||
toolName: 'write_file',
|
||||
callId: 'call-456',
|
||||
success: true,
|
||||
sanitized: sanitizedResult,
|
||||
requireApproval: true,
|
||||
approvalStatus: 'approved',
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
const toolMessage = sessionState.messages[0];
|
||||
expect(toolMessage.requireApproval).toBe(true);
|
||||
expect(toolMessage.approvalStatus).toBe('approved');
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Approval Events
|
||||
// =========================================================================
|
||||
|
||||
describe('Approval Events', () => {
|
||||
it('should process approval:request', () => {
|
||||
bus.dispatch({
|
||||
name: 'approval:request',
|
||||
sessionId: 'test-session',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
approvalId: 'approval-123',
|
||||
timeout: 30000,
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
toolName: 'write_file',
|
||||
toolCallId: 'call-write-123',
|
||||
args: { path: '/test.txt' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(useAgentStore.getState().status).toBe('awaiting_approval');
|
||||
expect(useAgentStore.getState().activeSessionId).toBe('test-session');
|
||||
});
|
||||
|
||||
it('should process approval:response with approved status', () => {
|
||||
// Set awaiting approval
|
||||
bus.dispatch({
|
||||
name: 'approval:request',
|
||||
sessionId: 'test-session',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
approvalId: 'approval-123',
|
||||
timeout: 30000,
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
toolName: 'write_file',
|
||||
toolCallId: 'call-write-123',
|
||||
args: { path: '/test.txt' },
|
||||
},
|
||||
});
|
||||
|
||||
// Approve
|
||||
bus.dispatch({
|
||||
name: 'approval:response',
|
||||
sessionId: 'test-session',
|
||||
approvalId: 'approval-123',
|
||||
status: ApprovalStatus.APPROVED,
|
||||
});
|
||||
|
||||
// Status transitions to 'thinking' - agent is resuming execution after approval
|
||||
expect(useAgentStore.getState().status).toBe('thinking');
|
||||
});
|
||||
|
||||
it('should process approval:response with rejected status', () => {
|
||||
// Set awaiting approval
|
||||
bus.dispatch({
|
||||
name: 'approval:request',
|
||||
sessionId: 'test-session',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
approvalId: 'approval-456',
|
||||
timeout: 30000,
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
toolName: 'write_file',
|
||||
toolCallId: 'call-write-456',
|
||||
args: { path: '/test.txt' },
|
||||
},
|
||||
});
|
||||
|
||||
// Reject
|
||||
bus.dispatch({
|
||||
name: 'approval:response',
|
||||
sessionId: 'test-session',
|
||||
approvalId: 'approval-456',
|
||||
status: ApprovalStatus.DENIED,
|
||||
});
|
||||
|
||||
expect(useAgentStore.getState().status).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Run Events
|
||||
// =========================================================================
|
||||
|
||||
describe('Run Events', () => {
|
||||
it('should process run:complete', () => {
|
||||
// Set processing state
|
||||
useChatStore.getState().setProcessing('test-session', true);
|
||||
useAgentStore.getState().setThinking('test-session');
|
||||
|
||||
// Complete run
|
||||
bus.dispatch({
|
||||
name: 'run:complete',
|
||||
sessionId: 'test-session',
|
||||
finishReason: 'stop',
|
||||
stepCount: 3,
|
||||
durationMs: 1500,
|
||||
});
|
||||
|
||||
// Check states reset
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.processing).toBe(false);
|
||||
expect(useAgentStore.getState().status).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Message Events
|
||||
// =========================================================================
|
||||
|
||||
describe('Message Events', () => {
|
||||
it('should process message:dequeued with text content', () => {
|
||||
bus.dispatch({
|
||||
name: 'message:dequeued',
|
||||
sessionId: 'test-session',
|
||||
count: 1,
|
||||
ids: ['queued-1'],
|
||||
coalesced: false,
|
||||
content: [{ type: 'text', text: 'Hello from queue' }],
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.messages).toHaveLength(1);
|
||||
expect(sessionState.messages[0].role).toBe('user');
|
||||
expect(sessionState.messages[0].content).toBe('Hello from queue');
|
||||
});
|
||||
|
||||
it('should process message:dequeued with image attachment', () => {
|
||||
bus.dispatch({
|
||||
name: 'message:dequeued',
|
||||
sessionId: 'test-session',
|
||||
count: 1,
|
||||
ids: ['queued-2'],
|
||||
coalesced: false,
|
||||
content: [
|
||||
{ type: 'text', text: 'Check this image' },
|
||||
{ type: 'image', image: 'base64data', mimeType: 'image/png' },
|
||||
],
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.messages).toHaveLength(1);
|
||||
expect(sessionState.messages[0].content).toBe('Check this image');
|
||||
expect(sessionState.messages[0].imageData).toEqual({
|
||||
image: 'base64data',
|
||||
mimeType: 'image/png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process message:dequeued with file attachment', () => {
|
||||
bus.dispatch({
|
||||
name: 'message:dequeued',
|
||||
sessionId: 'test-session',
|
||||
count: 1,
|
||||
ids: ['queued-3'],
|
||||
coalesced: false,
|
||||
content: [
|
||||
{ type: 'text', text: 'Here is the file' },
|
||||
{
|
||||
type: 'file',
|
||||
data: 'filedata',
|
||||
mimeType: 'text/plain',
|
||||
filename: 'test.txt',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const sessionState = useChatStore.getState().getSessionState('test-session');
|
||||
expect(sessionState.messages).toHaveLength(1);
|
||||
expect(sessionState.messages[0].fileData).toEqual({
|
||||
data: 'filedata',
|
||||
mimeType: 'text/plain',
|
||||
filename: 'test.txt',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Multi-Session Support
|
||||
// =========================================================================
|
||||
|
||||
describe('Multi-Session Support', () => {
|
||||
it('should handle events for multiple sessions independently', () => {
|
||||
// Session 1
|
||||
bus.dispatch({
|
||||
name: 'llm:thinking',
|
||||
sessionId: 'session-1',
|
||||
});
|
||||
|
||||
bus.dispatch({
|
||||
name: 'llm:chunk',
|
||||
sessionId: 'session-1',
|
||||
content: 'Response 1',
|
||||
chunkType: 'text',
|
||||
});
|
||||
|
||||
// Session 2
|
||||
bus.dispatch({
|
||||
name: 'llm:thinking',
|
||||
sessionId: 'session-2',
|
||||
});
|
||||
|
||||
bus.dispatch({
|
||||
name: 'llm:chunk',
|
||||
sessionId: 'session-2',
|
||||
content: 'Response 2',
|
||||
chunkType: 'text',
|
||||
});
|
||||
|
||||
// Verify isolation
|
||||
const session1 = useChatStore.getState().getSessionState('session-1');
|
||||
const session2 = useChatStore.getState().getSessionState('session-2');
|
||||
|
||||
expect(session1.streamingMessage?.content).toBe('Response 1');
|
||||
expect(session2.streamingMessage?.content).toBe('Response 2');
|
||||
expect(session1.processing).toBe(true);
|
||||
expect(session2.processing).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Error Handling
|
||||
// =========================================================================
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle unknown events gracefully', () => {
|
||||
// Dispatch unknown event (should not throw)
|
||||
expect(() => {
|
||||
bus.dispatch({
|
||||
// @ts-expect-error Testing unknown event
|
||||
name: 'unknown:event',
|
||||
sessionId: 'test-session',
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle events with missing sessionId', () => {
|
||||
// Some events might not have sessionId
|
||||
expect(() => {
|
||||
bus.dispatch({
|
||||
name: 'context:compacted',
|
||||
sessionId: 'test-session',
|
||||
originalTokens: 1000,
|
||||
compactedTokens: 500,
|
||||
originalMessages: 10,
|
||||
compactedMessages: 5,
|
||||
strategy: 'auto',
|
||||
reason: 'overflow',
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user