Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
545
npm/packages/ruvbot/tests/e2e/skills/skill-execution.test.ts
Normal file
545
npm/packages/ruvbot/tests/e2e/skills/skill-execution.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user