Files
wifi-densepose/npm/packages/ruvbot/tests/e2e/conversations/conversation-flow.test.ts
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

438 lines
12 KiB
TypeScript

/**
* Conversation Flow - E2E Tests
*
* End-to-end tests for complete agent conversation flows
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createSession, createAgent, createTenant } from '../../factories';
import { createMockSlackApp, type MockSlackBoltApp } from '../../mocks/slack.mock';
import { createMockPool, type MockPool } from '../../mocks/postgres.mock';
import { createMockRuVectorBindings } from '../../mocks/wasm.mock';
// Mock RuvBot for E2E testing
class MockRuvBot {
private app: MockSlackBoltApp;
private pool: MockPool;
private ruvector: ReturnType<typeof createMockRuVectorBindings>;
private sessions: Map<string, any> = new Map();
private agents: Map<string, any> = new Map();
constructor() {
this.app = createMockSlackApp();
this.pool = createMockPool();
this.ruvector = createMockRuVectorBindings();
this.setupHandlers();
}
async start(): Promise<void> {
await this.pool.connect();
await this.app.start(3000);
}
async stop(): Promise<void> {
await this.app.stop();
await this.pool.end();
}
getApp(): MockSlackBoltApp {
return this.app;
}
getPool(): MockPool {
return this.pool;
}
getSession(key: string): any {
return this.sessions.get(key);
}
async processMessage(message: {
text: string;
channel: string;
user: string;
ts: string;
thread_ts?: string;
}): Promise<void> {
await this.app.processMessage(message);
}
private setupHandlers(): void {
// Handle greetings
this.app.message(/^(hi|hello|hey)/i, async ({ message, say }) => {
const sessionKey = `${(message as any).channel}:${(message as any).thread_ts || (message as any).ts}`;
// Create or get session
if (!this.sessions.has(sessionKey)) {
this.sessions.set(sessionKey, {
id: `session-${Date.now()}`,
channelId: (message as any).channel,
threadTs: (message as any).thread_ts || (message as any).ts,
userId: (message as any).user,
messages: [],
startedAt: new Date()
});
}
const session = this.sessions.get(sessionKey);
session.messages.push({ role: 'user', content: (message as any).text, timestamp: new Date() });
await say({
channel: (message as any).channel,
text: 'Hello! I\'m RuvBot. How can I help you today?',
thread_ts: (message as any).ts
});
session.messages.push({ role: 'assistant', content: 'Hello! I\'m RuvBot. How can I help you today?', timestamp: new Date() });
});
// Handle code generation requests
this.app.message(/generate.*code|write.*function/i, async ({ message, say }) => {
const sessionKey = `${(message as any).channel}:${(message as any).thread_ts || (message as any).ts}`;
await say({
channel: (message as any).channel,
text: 'I\'ll generate that code for you. Give me a moment...',
thread_ts: (message as any).ts
});
// Simulate code generation
await new Promise(resolve => setTimeout(resolve, 100));
await say({
channel: (message as any).channel,
text: '```javascript\nfunction example() {\n console.log("Generated code");\n}\n```',
thread_ts: (message as any).ts
});
const session = this.sessions.get(sessionKey);
if (session) {
session.messages.push({
role: 'user',
content: (message as any).text,
timestamp: new Date()
});
session.messages.push({
role: 'assistant',
content: 'Code generated',
artifact: { type: 'code', language: 'javascript' },
timestamp: new Date()
});
}
});
// Handle help requests
this.app.message(/help|what can you do/i, async ({ message, say }) => {
await say({
channel: (message as any).channel,
text: 'I can help you with:\n- Code generation\n- Code review\n- Testing\n- Documentation\n\nJust ask me what you need!',
thread_ts: (message as any).ts
});
});
// Handle thank you
this.app.message(/thanks|thank you/i, async ({ message, say }) => {
const sessionKey = `${(message as any).channel}:${(message as any).thread_ts || (message as any).ts}`;
await say({
channel: (message as any).channel,
text: 'You\'re welcome! Let me know if you need anything else.',
thread_ts: (message as any).ts
});
// Mark session as potentially complete
const session = this.sessions.get(sessionKey);
if (session) {
session.status = 'satisfied';
}
});
// Handle search requests
this.app.message(/search|find|look up/i, async ({ message, say }) => {
await say({
channel: (message as any).channel,
text: 'Searching through the knowledge base...',
thread_ts: (message as any).ts
});
// Simulate vector search
const results = await this.ruvector.search((message as any).text, 3);
if (results.length > 0) {
await say({
channel: (message as any).channel,
text: `Found ${results.length} relevant results.`,
thread_ts: (message as any).ts
});
} else {
await say({
channel: (message as any).channel,
text: 'No relevant results found.',
thread_ts: (message as any).ts
});
}
});
}
}
describe('E2E: Conversation Flow', () => {
let bot: MockRuvBot;
beforeEach(async () => {
bot = new MockRuvBot();
await bot.start();
});
afterEach(async () => {
await bot.stop();
});
describe('Basic Conversation', () => {
it('should handle greeting and establish session', async () => {
const channel = 'C12345678';
const ts = '1234567890.123456';
await bot.processMessage({
text: 'Hello!',
channel,
user: 'U12345678',
ts
});
const messages = bot.getApp().client.getMessageLog();
expect(messages).toHaveLength(1);
expect(messages[0].text).toContain('RuvBot');
const session = bot.getSession(`${channel}:${ts}`);
expect(session).toBeDefined();
expect(session.messages).toHaveLength(2);
});
it('should maintain conversation context in thread', async () => {
const channel = 'C12345678';
const parentTs = '1234567890.111111';
// Start conversation
await bot.processMessage({
text: 'Hi there',
channel,
user: 'U12345678',
ts: parentTs
});
// Continue in thread
await bot.processMessage({
text: 'Help me generate code',
channel,
user: 'U12345678',
ts: '1234567890.222222',
thread_ts: parentTs
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.length).toBeGreaterThanOrEqual(2);
});
});
describe('Code Generation Flow', () => {
it('should generate code on request', async () => {
await bot.processMessage({
text: 'Generate code for a hello world function',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.length).toBeGreaterThanOrEqual(2);
// Should have progress message and code block
expect(messages.some(m => m.text?.includes('generating') || m.text?.includes('moment'))).toBe(true);
expect(messages.some(m => m.text?.includes('```'))).toBe(true);
});
it('should handle follow-up questions about generated code', async () => {
const channel = 'C12345678';
const parentTs = '1234567890.111111';
// Request code
await bot.processMessage({
text: 'Write a function to sort an array',
channel,
user: 'U12345678',
ts: parentTs
});
// Ask for help about the code
await bot.processMessage({
text: 'Help me understand this',
channel,
user: 'U12345678',
ts: '1234567890.222222',
thread_ts: parentTs
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.length).toBeGreaterThanOrEqual(3);
});
});
describe('Help Flow', () => {
it('should provide help information', async () => {
await bot.processMessage({
text: 'What can you do?',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages).toHaveLength(1);
expect(messages[0].text).toContain('Code generation');
expect(messages[0].text).toContain('Code review');
});
});
describe('Multi-turn Conversation', () => {
it('should handle complete conversation lifecycle', async () => {
const channel = 'C12345678';
const parentTs = '1234567890.000001';
// 1. Greeting
await bot.processMessage({
text: 'Hey',
channel,
user: 'U12345678',
ts: parentTs
});
// 2. Request
await bot.processMessage({
text: 'Generate code for a calculator',
channel,
user: 'U12345678',
ts: '1234567890.000002',
thread_ts: parentTs
});
// 3. Thank you
await bot.processMessage({
text: 'Thank you!',
channel,
user: 'U12345678',
ts: '1234567890.000003',
thread_ts: parentTs
});
const session = bot.getSession(`${channel}:${parentTs}`);
expect(session).toBeDefined();
expect(session.messages.length).toBeGreaterThan(2);
expect(session.status).toBe('satisfied');
});
});
describe('Error Recovery', () => {
it('should handle unknown requests gracefully', async () => {
await bot.processMessage({
text: 'asdfghjkl random gibberish',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
// Should not crash
expect(true).toBe(true);
});
});
});
describe('E2E: Multi-user Conversations', () => {
let bot: MockRuvBot;
beforeEach(async () => {
bot = new MockRuvBot();
await bot.start();
});
afterEach(async () => {
await bot.stop();
});
it('should handle multiple concurrent users', async () => {
const users = ['U11111111', 'U22222222', 'U33333333'];
const channel = 'C12345678';
// All users send messages
for (let i = 0; i < users.length; i++) {
await bot.processMessage({
text: 'Hello',
channel,
user: users[i],
ts: `${Date.now()}.${i}`
});
}
const messages = bot.getApp().client.getMessageLog();
expect(messages).toHaveLength(3); // One response per user
});
it('should maintain separate sessions per thread', async () => {
const channel = 'C12345678';
// User 1 starts thread
await bot.processMessage({
text: 'Hi',
channel,
user: 'U11111111',
ts: '1234567890.111111'
});
// User 2 starts different thread
await bot.processMessage({
text: 'Hello',
channel,
user: 'U22222222',
ts: '1234567890.222222'
});
const session1 = bot.getSession(`${channel}:1234567890.111111`);
const session2 = bot.getSession(`${channel}:1234567890.222222`);
expect(session1.userId).toBe('U11111111');
expect(session2.userId).toBe('U22222222');
});
});
describe('E2E: Cross-channel Conversations', () => {
let bot: MockRuvBot;
beforeEach(async () => {
bot = new MockRuvBot();
await bot.start();
});
afterEach(async () => {
await bot.stop();
});
it('should handle messages from different channels', async () => {
const channels = ['C11111111', 'C22222222', 'C33333333'];
for (const channel of channels) {
await bot.processMessage({
text: 'Hello',
channel,
user: 'U12345678',
ts: `${Date.now()}.000000`
});
}
const messages = bot.getApp().client.getMessageLog();
expect(messages).toHaveLength(3);
// Each response should be in the correct channel
const responseChannels = new Set(messages.map(m => m.channel));
expect(responseChannels.size).toBe(3);
});
});