Squashed 'vendor/ruvector/' content from commit b64c2172

git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
commit d803bfe2b1
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,437 @@
/**
* 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);
});
});

View File

@@ -0,0 +1,545 @@
/**
* Skill Execution - E2E Tests
*
* End-to-end tests for skill execution flows
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createSkill } from '../../factories';
import { createMockSlackApp, type MockSlackBoltApp } from '../../mocks/slack.mock';
import { createMockRuVectorBindings } from '../../mocks/wasm.mock';
// Skill execution types
interface SkillInput {
skill: string;
params: Record<string, unknown>;
}
interface SkillOutput {
success: boolean;
result: unknown;
error?: string;
executionTime: number;
}
// Mock Skill Executor
class MockSkillExecutor {
private skills: Map<string, {
handler: (params: Record<string, unknown>) => Promise<unknown>;
timeout: number;
}> = new Map();
registerSkill(
name: string,
handler: (params: Record<string, unknown>) => Promise<unknown>,
timeout: number = 30000
): void {
this.skills.set(name, { handler, timeout });
}
async execute(input: SkillInput): Promise<SkillOutput> {
const skill = this.skills.get(input.skill);
if (!skill) {
return {
success: false,
result: null,
error: `Skill '${input.skill}' not found`,
executionTime: 0
};
}
const startTime = Date.now();
try {
const result = await Promise.race([
skill.handler(input.params),
this.createTimeout(skill.timeout)
]);
return {
success: true,
result,
executionTime: Date.now() - startTime
};
} catch (error) {
return {
success: false,
result: null,
error: error instanceof Error ? error.message : 'Unknown error',
executionTime: Date.now() - startTime
};
}
}
private createTimeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Skill execution timed out')), ms);
});
}
}
// Mock Skill-enabled Bot
class MockSkillBot {
private app: MockSlackBoltApp;
private executor: MockSkillExecutor;
private ruvector: ReturnType<typeof createMockRuVectorBindings>;
constructor() {
this.app = createMockSlackApp();
this.executor = new MockSkillExecutor();
this.ruvector = createMockRuVectorBindings();
this.registerSkills();
this.setupHandlers();
}
getApp(): MockSlackBoltApp {
return this.app;
}
getExecutor(): MockSkillExecutor {
return this.executor;
}
async processMessage(message: {
text: string;
channel: string;
user: string;
ts: string;
thread_ts?: string;
}): Promise<void> {
await this.app.processMessage(message);
}
private registerSkills(): void {
// Code generation skill
this.executor.registerSkill('code-generation', async (params) => {
const { language, description } = params;
await new Promise(resolve => setTimeout(resolve, 50)); // Simulate processing
const templates: Record<string, string> = {
javascript: `// ${description}\nfunction example() {\n // Implementation\n}`,
python: `# ${description}\ndef example():\n # Implementation\n pass`,
typescript: `// ${description}\nfunction example(): void {\n // Implementation\n}`
};
return {
code: templates[language as string] || templates.javascript,
language
};
});
// Test generation skill
this.executor.registerSkill('test-generation', async (params) => {
const { code, framework } = params;
await new Promise(resolve => setTimeout(resolve, 50));
return {
tests: `describe('Generated Tests', () => {\n it('should work', () => {\n expect(true).toBe(true);\n });\n});`,
framework: framework || 'jest',
coverage: 85
};
});
// Vector search skill
this.executor.registerSkill('vector-search', async (params) => {
const { query, topK } = params;
const results = await this.ruvector.search(query as string, topK as number || 5);
return {
results,
query,
count: results.length
};
});
// Code review skill
this.executor.registerSkill('code-review', async (params) => {
const { code } = params;
await new Promise(resolve => setTimeout(resolve, 100));
return {
issues: [
{ type: 'warning', message: 'Consider adding error handling', line: 5 },
{ type: 'suggestion', message: 'Variable could be const', line: 2 }
],
score: 85,
summary: 'Code looks good with minor improvements suggested'
};
});
// Documentation skill
this.executor.registerSkill('generate-docs', async (params) => {
const { code, format } = params;
await new Promise(resolve => setTimeout(resolve, 75));
return {
documentation: `## Function Documentation\n\nThis function does something useful.\n\n### Parameters\n- param1: Description`,
format: format || 'markdown'
};
});
}
private setupHandlers(): void {
// Handle code generation
this.app.message(/generate.*code.*in\s+(\w+)/i, async ({ message, say }) => {
const languageMatch = (message as any).text.match(/in\s+(\w+)/i);
const language = languageMatch ? languageMatch[1].toLowerCase() : 'javascript';
await say({
channel: (message as any).channel,
text: `Generating ${language} code...`,
thread_ts: (message as any).ts
});
const result = await this.executor.execute({
skill: 'code-generation',
params: {
language,
description: (message as any).text
}
});
if (result.success) {
const output = result.result as { code: string };
await say({
channel: (message as any).channel,
text: `\`\`\`${language}\n${output.code}\n\`\`\``,
thread_ts: (message as any).ts
});
} else {
await say({
channel: (message as any).channel,
text: `Error: ${result.error}`,
thread_ts: (message as any).ts
});
}
});
// Handle test generation
this.app.message(/generate.*tests?|write.*tests?/i, async ({ message, say }) => {
await say({
channel: (message as any).channel,
text: 'Generating tests...',
thread_ts: (message as any).ts
});
const result = await this.executor.execute({
skill: 'test-generation',
params: {
code: 'function example() {}',
framework: 'vitest'
}
});
if (result.success) {
const output = result.result as { tests: string; coverage: number };
await say({
channel: (message as any).channel,
text: `\`\`\`typescript\n${output.tests}\n\`\`\`\nEstimated coverage: ${output.coverage}%`,
thread_ts: (message as any).ts
});
}
});
// Handle code review
this.app.message(/review.*code|check.*code/i, async ({ message, say }) => {
await say({
channel: (message as any).channel,
text: 'Reviewing code...',
thread_ts: (message as any).ts
});
const result = await this.executor.execute({
skill: 'code-review',
params: {
code: '// Sample code for review'
}
});
if (result.success) {
const output = result.result as { summary: string; score: number; issues: unknown[] };
await say({
channel: (message as any).channel,
text: `Code Review Results:\n- Score: ${output.score}/100\n- Issues: ${output.issues.length}\n\n${output.summary}`,
thread_ts: (message as any).ts
});
}
});
// Handle search
this.app.message(/search.*for|find.*about/i, async ({ message, say }) => {
const result = await this.executor.execute({
skill: 'vector-search',
params: {
query: (message as any).text,
topK: 5
}
});
if (result.success) {
const output = result.result as { count: number };
await say({
channel: (message as any).channel,
text: `Found ${output.count} results`,
thread_ts: (message as any).ts
});
}
});
// Handle documentation
this.app.message(/generate.*docs|document.*this/i, async ({ message, say }) => {
await say({
channel: (message as any).channel,
text: 'Generating documentation...',
thread_ts: (message as any).ts
});
const result = await this.executor.execute({
skill: 'generate-docs',
params: {
code: 'function example() {}',
format: 'markdown'
}
});
if (result.success) {
const output = result.result as { documentation: string };
await say({
channel: (message as any).channel,
text: output.documentation,
thread_ts: (message as any).ts
});
}
});
}
}
describe('E2E: Skill Execution', () => {
let bot: MockSkillBot;
beforeEach(() => {
bot = new MockSkillBot();
});
describe('Code Generation Skill', () => {
it('should generate JavaScript code', async () => {
await bot.processMessage({
text: 'Generate code in JavaScript for a hello world function',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('Generating'))).toBe(true);
expect(messages.some(m => m.text?.includes('```javascript'))).toBe(true);
});
it('should generate Python code', async () => {
await bot.processMessage({
text: 'Generate code in Python for data processing',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('```python'))).toBe(true);
});
it('should generate TypeScript code', async () => {
await bot.processMessage({
text: 'Generate code in TypeScript for a type-safe function',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('```typescript'))).toBe(true);
});
});
describe('Test Generation Skill', () => {
it('should generate tests', async () => {
await bot.processMessage({
text: 'Generate tests for this function',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('describe'))).toBe(true);
expect(messages.some(m => m.text?.includes('coverage'))).toBe(true);
});
});
describe('Code Review Skill', () => {
it('should review code and provide feedback', async () => {
await bot.processMessage({
text: 'Review this code for me',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('Review Results'))).toBe(true);
expect(messages.some(m => m.text?.includes('Score'))).toBe(true);
});
});
describe('Vector Search Skill', () => {
it('should search and return results', async () => {
await bot.processMessage({
text: 'Search for React patterns',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('results'))).toBe(true);
});
});
describe('Documentation Skill', () => {
it('should generate documentation', async () => {
await bot.processMessage({
text: 'Generate docs for this function',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
const messages = bot.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('Documentation'))).toBe(true);
});
});
});
describe('E2E: Skill Chaining', () => {
let executor: MockSkillExecutor;
beforeEach(() => {
executor = new MockSkillExecutor();
// Register skills for chaining
executor.registerSkill('analyze', async (params) => {
return { analyzed: true, data: params.input };
});
executor.registerSkill('transform', async (params) => {
return { transformed: true, original: params.data };
});
executor.registerSkill('output', async (params) => {
return { result: `Processed: ${JSON.stringify(params.transformed)}` };
});
});
it('should chain multiple skills together', async () => {
// Step 1: Analyze
const step1 = await executor.execute({
skill: 'analyze',
params: { input: 'raw data' }
});
expect(step1.success).toBe(true);
// Step 2: Transform
const step2 = await executor.execute({
skill: 'transform',
params: { data: step1.result }
});
expect(step2.success).toBe(true);
// Step 3: Output
const step3 = await executor.execute({
skill: 'output',
params: { transformed: step2.result }
});
expect(step3.success).toBe(true);
expect((step3.result as any).result).toContain('Processed');
});
});
describe('E2E: Skill Error Handling', () => {
let executor: MockSkillExecutor;
beforeEach(() => {
executor = new MockSkillExecutor();
executor.registerSkill('failing-skill', async () => {
throw new Error('Skill failed intentionally');
});
executor.registerSkill('slow-skill', async () => {
await new Promise(resolve => setTimeout(resolve, 5000));
return { result: 'Should not reach' };
}, 100); // 100ms timeout
});
it('should handle skill errors gracefully', async () => {
const result = await executor.execute({
skill: 'failing-skill',
params: {}
});
expect(result.success).toBe(false);
expect(result.error).toBe('Skill failed intentionally');
});
it('should handle skill timeout', async () => {
const result = await executor.execute({
skill: 'slow-skill',
params: {}
});
expect(result.success).toBe(false);
expect(result.error).toContain('timed out');
});
it('should handle non-existent skill', async () => {
const result = await executor.execute({
skill: 'non-existent',
params: {}
});
expect(result.success).toBe(false);
expect(result.error).toContain('not found');
});
});
describe('E2E: Skill Execution Metrics', () => {
let executor: MockSkillExecutor;
beforeEach(() => {
executor = new MockSkillExecutor();
executor.registerSkill('timed-skill', async (params) => {
const delay = (params.delay as number) || 50;
await new Promise(resolve => setTimeout(resolve, delay));
return { executed: true };
});
});
it('should track execution time', async () => {
const result = await executor.execute({
skill: 'timed-skill',
params: { delay: 100 }
});
expect(result.executionTime).toBeGreaterThanOrEqual(100);
expect(result.executionTime).toBeLessThan(200);
});
it('should report zero execution time for immediate failures', async () => {
const result = await executor.execute({
skill: 'non-existent',
params: {}
});
expect(result.executionTime).toBe(0);
});
});

View File

@@ -0,0 +1,464 @@
/**
* Long-running Tasks - E2E Tests
*
* End-to-end tests for long-running task completion
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createAgent, createSession } from '../../factories';
import { createMockSlackApp, type MockSlackBoltApp } from '../../mocks/slack.mock';
// Task types
interface Task {
id: string;
type: string;
status: 'pending' | 'running' | 'completed' | 'failed';
progress: number;
result?: unknown;
error?: string;
startedAt?: Date;
completedAt?: Date;
}
// Mock Task Manager
class MockTaskManager {
private tasks: Map<string, Task> = new Map();
private eventHandlers: Map<string, Array<(task: Task) => void>> = new Map();
async createTask(type: string, payload: unknown): Promise<Task> {
const task: Task = {
id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
type,
status: 'pending',
progress: 0
};
this.tasks.set(task.id, task);
this.emit('created', task);
return task;
}
async startTask(taskId: string): Promise<void> {
const task = this.tasks.get(taskId);
if (!task) throw new Error(`Task ${taskId} not found`);
task.status = 'running';
task.startedAt = new Date();
this.emit('started', task);
}
async updateProgress(taskId: string, progress: number): Promise<void> {
const task = this.tasks.get(taskId);
if (!task) throw new Error(`Task ${taskId} not found`);
task.progress = progress;
this.emit('progress', task);
}
async completeTask(taskId: string, result: unknown): Promise<void> {
const task = this.tasks.get(taskId);
if (!task) throw new Error(`Task ${taskId} not found`);
task.status = 'completed';
task.progress = 100;
task.result = result;
task.completedAt = new Date();
this.emit('completed', task);
}
async failTask(taskId: string, error: string): Promise<void> {
const task = this.tasks.get(taskId);
if (!task) throw new Error(`Task ${taskId} not found`);
task.status = 'failed';
task.error = error;
task.completedAt = new Date();
this.emit('failed', task);
}
getTask(taskId: string): Task | undefined {
return this.tasks.get(taskId);
}
on(event: string, handler: (task: Task) => void): void {
const handlers = this.eventHandlers.get(event) || [];
handlers.push(handler);
this.eventHandlers.set(event, handlers);
}
private emit(event: string, task: Task): void {
const handlers = this.eventHandlers.get(event) || [];
handlers.forEach(h => h(task));
}
// Simulate long-running task execution
async executeTask(taskId: string, duration: number, steps: number): Promise<void> {
await this.startTask(taskId);
const stepDuration = duration / steps;
for (let i = 1; i <= steps; i++) {
await new Promise(resolve => setTimeout(resolve, stepDuration));
await this.updateProgress(taskId, (i / steps) * 100);
}
await this.completeTask(taskId, { message: 'Task completed successfully' });
}
}
// Mock Orchestrator for E2E testing
class MockTaskOrchestrator {
private app: MockSlackBoltApp;
private taskManager: MockTaskManager;
private activeTasks: Map<string, { channel: string; threadTs: string }> = new Map();
constructor() {
this.app = createMockSlackApp();
this.taskManager = new MockTaskManager();
this.setupHandlers();
this.setupTaskEvents();
}
getApp(): MockSlackBoltApp {
return this.app;
}
getTaskManager(): MockTaskManager {
return this.taskManager;
}
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 long task requests
this.app.message(/run.*long.*task|execute.*batch/i, async ({ message, say }) => {
const task = await this.taskManager.createTask('long-running', {
request: (message as any).text
});
this.activeTasks.set(task.id, {
channel: (message as any).channel,
threadTs: (message as any).ts
});
await say({
channel: (message as any).channel,
text: `Starting task ${task.id}. I'll update you on progress...`,
thread_ts: (message as any).ts
});
// Execute task in background
this.taskManager.executeTask(task.id, 500, 5);
});
// Handle analysis requests
this.app.message(/analyze|process.*data/i, async ({ message, say }) => {
const task = await this.taskManager.createTask('analysis', {
request: (message as any).text
});
this.activeTasks.set(task.id, {
channel: (message as any).channel,
threadTs: (message as any).ts
});
await say({
channel: (message as any).channel,
text: `Beginning analysis (Task: ${task.id})...`,
thread_ts: (message as any).ts
});
// Execute analysis task
this.taskManager.executeTask(task.id, 300, 3);
});
// Handle code refactoring requests
this.app.message(/refactor.*code|rewrite/i, async ({ message, say }) => {
const task = await this.taskManager.createTask('refactoring', {
request: (message as any).text
});
this.activeTasks.set(task.id, {
channel: (message as any).channel,
threadTs: (message as any).ts
});
await say({
channel: (message as any).channel,
text: `Starting code refactoring (Task: ${task.id}). This may take a while...`,
thread_ts: (message as any).ts
});
// Execute refactoring task
this.taskManager.executeTask(task.id, 800, 8);
});
}
private setupTaskEvents(): void {
this.taskManager.on('progress', async (task) => {
const context = this.activeTasks.get(task.id);
if (!context) return;
// Only send updates at 25%, 50%, 75%
if ([25, 50, 75].includes(task.progress)) {
await this.app.client.chat.postMessage({
channel: context.channel,
text: `Task ${task.id} progress: ${task.progress}%`,
thread_ts: context.threadTs
});
}
});
this.taskManager.on('completed', async (task) => {
const context = this.activeTasks.get(task.id);
if (!context) return;
const duration = task.completedAt!.getTime() - task.startedAt!.getTime();
await this.app.client.chat.postMessage({
channel: context.channel,
text: `Task ${task.id} completed successfully in ${duration}ms!`,
thread_ts: context.threadTs
});
this.activeTasks.delete(task.id);
});
this.taskManager.on('failed', async (task) => {
const context = this.activeTasks.get(task.id);
if (!context) return;
await this.app.client.chat.postMessage({
channel: context.channel,
text: `Task ${task.id} failed: ${task.error}`,
thread_ts: context.threadTs
});
this.activeTasks.delete(task.id);
});
}
}
describe('E2E: Long-running Tasks', () => {
let orchestrator: MockTaskOrchestrator;
beforeEach(() => {
orchestrator = new MockTaskOrchestrator();
});
describe('Task Execution', () => {
it('should start and complete long-running task', async () => {
await orchestrator.processMessage({
text: 'Run a long task for me',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
// Wait for task to complete
await new Promise(resolve => setTimeout(resolve, 600));
const messages = orchestrator.getApp().client.getMessageLog();
// Should have start message, progress updates, and completion
expect(messages.some(m => m.text?.includes('Starting task'))).toBe(true);
expect(messages.some(m => m.text?.includes('completed'))).toBe(true);
});
it('should send progress updates', async () => {
await orchestrator.processMessage({
text: 'Run a long task',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
// Wait for task to complete
await new Promise(resolve => setTimeout(resolve, 600));
const messages = orchestrator.getApp().client.getMessageLog();
const progressMessages = messages.filter(m => m.text?.includes('progress'));
// Should have multiple progress updates
expect(progressMessages.length).toBeGreaterThan(0);
});
it('should report completion time', async () => {
await orchestrator.processMessage({
text: 'Execute batch process',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
await new Promise(resolve => setTimeout(resolve, 600));
const messages = orchestrator.getApp().client.getMessageLog();
const completionMessage = messages.find(m => m.text?.includes('completed'));
expect(completionMessage?.text).toMatch(/\d+ms/);
});
});
describe('Multiple Concurrent Tasks', () => {
it('should handle multiple tasks concurrently', async () => {
// Start multiple tasks
await orchestrator.processMessage({
text: 'Run a long task',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.111111'
});
await orchestrator.processMessage({
text: 'Analyze this data',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.222222'
});
// Wait for both to complete
await new Promise(resolve => setTimeout(resolve, 700));
const messages = orchestrator.getApp().client.getMessageLog();
const completedMessages = messages.filter(m => m.text?.includes('completed'));
expect(completedMessages.length).toBe(2);
});
it('should track tasks independently', async () => {
await orchestrator.processMessage({
text: 'Run a long task',
channel: 'C11111111',
user: 'U12345678',
ts: '1234567890.111111'
});
await orchestrator.processMessage({
text: 'Process data',
channel: 'C22222222',
user: 'U12345678',
ts: '1234567890.222222'
});
await new Promise(resolve => setTimeout(resolve, 700));
const messages = orchestrator.getApp().client.getMessageLog();
// Each channel should have its own completion message
const channel1Completed = messages.some(
m => m.channel === 'C11111111' && m.text?.includes('completed')
);
const channel2Completed = messages.some(
m => m.channel === 'C22222222' && m.text?.includes('completed')
);
expect(channel1Completed).toBe(true);
expect(channel2Completed).toBe(true);
});
});
describe('Task Types', () => {
it('should handle analysis tasks', async () => {
await orchestrator.processMessage({
text: 'Analyze the codebase',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
await new Promise(resolve => setTimeout(resolve, 400));
const messages = orchestrator.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('analysis'))).toBe(true);
});
it('should handle refactoring tasks', async () => {
await orchestrator.processMessage({
text: 'Refactor the code in src/main.ts',
channel: 'C12345678',
user: 'U12345678',
ts: '1234567890.123456'
});
await new Promise(resolve => setTimeout(resolve, 900));
const messages = orchestrator.getApp().client.getMessageLog();
expect(messages.some(m => m.text?.includes('refactoring'))).toBe(true);
});
});
});
describe('E2E: Task Manager', () => {
let taskManager: MockTaskManager;
beforeEach(() => {
taskManager = new MockTaskManager();
});
describe('Task Lifecycle', () => {
it('should create task in pending state', async () => {
const task = await taskManager.createTask('test', {});
expect(task.status).toBe('pending');
expect(task.progress).toBe(0);
});
it('should transition through states correctly', async () => {
const states: string[] = [];
taskManager.on('created', (t) => states.push(t.status));
taskManager.on('started', (t) => states.push(t.status));
taskManager.on('completed', (t) => states.push(t.status));
const task = await taskManager.createTask('test', {});
await taskManager.executeTask(task.id, 100, 2);
expect(states).toEqual(['pending', 'running', 'completed']);
});
it('should track progress correctly', async () => {
const progressValues: number[] = [];
taskManager.on('progress', (t) => progressValues.push(t.progress));
const task = await taskManager.createTask('test', {});
await taskManager.executeTask(task.id, 100, 4);
expect(progressValues).toEqual([25, 50, 75, 100]);
});
it('should record timing information', async () => {
const task = await taskManager.createTask('test', {});
await taskManager.executeTask(task.id, 100, 2);
const completed = taskManager.getTask(task.id)!;
expect(completed.startedAt).toBeDefined();
expect(completed.completedAt).toBeDefined();
expect(completed.completedAt!.getTime()).toBeGreaterThan(completed.startedAt!.getTime());
});
});
describe('Task Failure', () => {
it('should handle task failure', async () => {
const task = await taskManager.createTask('test', {});
await taskManager.startTask(task.id);
await taskManager.failTask(task.id, 'Something went wrong');
const failed = taskManager.getTask(task.id)!;
expect(failed.status).toBe('failed');
expect(failed.error).toBe('Something went wrong');
});
});
});