Overview
Conductor provides a comprehensive testing framework withTestConductor, custom Vitest matchers, and mock providers. Write tests that are fast, reliable, and easy to maintain.
Testing Philosophy
Test Ensembles, Not Implementation
Focus on workflow behavior and outputs, not internal details
Mock External Services
Mock AI providers, databases, APIs for fast, deterministic tests
Use Real Member Logic
Test actual member implementations, not mocks
Test Edge Cases
Error handling, retries, scoring, state management
Installation
TestConductor is included with Conductor:Copy
npm install --save-dev vitest @ensemble-edge/conductor
Basic Test Structure
Copy
import { describe, it, expect } from 'vitest';
import { TestConductor } from '@ensemble-edge/conductor/testing';
describe('hello-world ensemble', () => {
it('should greet user by name', async () => {
// Create test conductor
const conductor = await TestConductor.create();
// Load your project
await conductor.loadProject('./');
// Execute ensemble
const result = await conductor.executeEnsemble('hello-world', {
name: 'World'
});
// Assertions
expect(result).toBeSuccessful();
expect(result.output.greeting).toBe('Hello, World! Welcome to Conductor.');
});
});
TestConductor API
Creating Test Instance
Copy
// Basic creation
const conductor = await TestConductor.create();
// With mocks
const conductor = await TestConductor.create({
mocks: {
ai: {
responses: {
'analyze-sentiment': { sentiment: 'positive', confidence: 0.95 }
}
}
}
});
// With project path
const conductor = await TestConductor.create({
projectPath: './' // Loads all ensembles and members
});
Executing Ensembles
Copy
// Execute with input
const result = await conductor.executeEnsemble('sentiment-analysis', {
text: 'I love this product!'
});
// Access results
console.log(result.success); // true
console.log(result.output); // { sentiment: 'positive', ... }
console.log(result.executionTime); // 1203 (ms)
console.log(result.error); // undefined (on success)
Executing Members
Test individual members:Copy
const result = await conductor.executeMember('greet', {
name: 'Alice'
});
expect(result).toBeSuccessful();
expect(result.output.message).toContain('Alice');
Custom Matchers
Conductor extends Vitest with custom matchers for cleaner assertions.Setup Matchers
Copy
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['@ensemble-edge/conductor/testing/matchers']
}
});
Available Matchers
toBeSuccessful()
Copy
it('should succeed', async () => {
const result = await conductor.executeEnsemble('hello-world', { name: 'Test' });
expect(result).toBeSuccessful();
// Equivalent to: expect(result.success).toBe(true)
});
toHaveError()
Copy
it('should fail with invalid input', async () => {
const result = await conductor.executeEnsemble('analyze', {});
expect(result).toHaveError();
expect(result).toHaveError(/required/i); // Match error message
});
toHaveExecutedMember()
Copy
it('should execute analyze-sentiment member', async () => {
const result = await conductor.executeEnsemble('sentiment-analysis', input);
expect(result).toHaveExecutedMember('analyze-sentiment');
});
toHaveCachedResult()
Copy
it('should cache results', async () => {
// First call
await conductor.executeEnsemble('expensive-call', input);
// Second call
const result = await conductor.executeEnsemble('expensive-call', input);
expect(result).toHaveCachedResult();
});
toHaveState()
Copy
it('should set state correctly', async () => {
const result = await conductor.executeEnsemble('multi-step', input);
expect(result).toHaveState('companyData');
expect(result).toHaveState('analysis', { confidence: 0.9 });
});
toHaveOutputMatching()
Copy
it('should have expected output structure', async () => {
const result = await conductor.executeEnsemble('analyze', input);
expect(result).toHaveOutputMatching({
sentiment: expect.any(String),
confidence: expect.any(Number)
});
});
Mocking Strategies
Mock AI Providers
Copy
const conductor = await TestConductor.create({
mocks: {
ai: {
// Mock specific members
responses: {
'analyze-sentiment': {
sentiment: 'positive',
confidence: 0.95
},
'generate-summary': {
summary: 'Test summary'
}
},
// Or use a function for dynamic responses
handler: async (memberName, input) => {
if (memberName === 'analyze-sentiment') {
return {
sentiment: input.text.includes('love') ? 'positive' : 'negative',
confidence: 0.8
};
}
}
}
}
});
Mock Databases
Copy
const conductor = await TestConductor.create({
mocks: {
database: {
// Mock query results
responses: {
'get-user': {
id: 1,
name: 'Test User',
email: 'test@example.com'
}
},
// Or use a function
handler: async (operation, query, params) => {
if (query.includes('SELECT * FROM users')) {
return [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
}
}
}
}
});
Mock HTTP Requests
Copy
const conductor = await TestConductor.create({
mocks: {
http: {
// Mock by URL pattern
responses: {
'https://api.example.com/pricing': {
price: 99.99,
currency: 'USD'
}
},
// Or use a function
handler: async (url, options) => {
if (url.includes('/users/')) {
return {
status: 200,
data: { id: 1, name: 'Alice' }
};
}
}
}
}
});
Mock Vectorize
Copy
const conductor = await TestConductor.create({
mocks: {
vectorize: {
// Mock search results
searches: {
'documentation': [
{ id: 'doc1', score: 0.95, metadata: { title: 'Getting Started' } },
{ id: 'doc2', score: 0.87, metadata: { title: 'API Reference' } }
]
}
}
}
});
Testing Patterns
Test Successful Execution
Copy
describe('company-intelligence ensemble', () => {
it('should analyze company successfully', async () => {
const conductor = await TestConductor.create({
mocks: {
ai: {
responses: {
'analyze-company': {
summary: 'Growing tech company',
confidence: 0.9
}
}
}
}
});
const result = await conductor.executeEnsemble('company-intelligence', {
domain: 'example.com'
});
expect(result).toBeSuccessful();
expect(result.output.summary).toBe('Growing tech company');
expect(result.output.confidence).toBe(0.9);
});
});
Test Error Handling
Copy
it('should handle invalid domain', async () => {
const conductor = await TestConductor.create();
const result = await conductor.executeEnsemble('company-intelligence', {
domain: 'invalid-domain'
});
expect(result).toHaveError();
expect(result.error.message).toContain('Invalid domain');
});
Test State Management
Copy
it('should share state between members', async () => {
const conductor = await TestConductor.create();
const result = await conductor.executeEnsemble('multi-step', input);
expect(result).toHaveState('companyData');
expect(result).toHaveState('analysis');
expect(result).toHaveExecutedMember('fetch-data');
expect(result).toHaveExecutedMember('analyze-data');
});
Test Caching
Copy
it('should cache expensive operations', async () => {
const conductor = await TestConductor.create();
// First call - cache miss
const result1 = await conductor.executeEnsemble('analyze', input);
expect(result1).not.toHaveCachedResult();
// Second call - cache hit
const result2 = await conductor.executeEnsemble('analyze', input);
expect(result2).toHaveCachedResult();
expect(result2.executionTime).toBeLessThan(result1.executionTime);
});
Test Scoring and Retry
Copy
it('should retry on low quality scores', async () => {
let attempt = 0;
const conductor = await TestConductor.create({
mocks: {
ai: {
handler: async (memberName, input) => {
attempt++;
return {
content: attempt === 1 ? 'bad' : 'good content',
quality: attempt === 1 ? 0.3 : 0.9 // Low then high
};
}
}
}
});
const result = await conductor.executeEnsemble('generate-content', input);
expect(result).toBeSuccessful();
expect(result.metadata.attempts).toBe(2); // Retried once
});
Test Conditional Flows
Copy
it('should execute conditional branch', async () => {
const conductor = await TestConductor.create();
const result = await conductor.executeEnsemble('conditional-flow', {
type: 'premium'
});
expect(result).toHaveExecutedMember('premium-handler');
expect(result).not.toHaveExecutedMember('basic-handler');
});
Test Parallel Execution
Copy
it('should execute steps in parallel', async () => {
const conductor = await TestConductor.create();
const startTime = performance.now();
const result = await conductor.executeEnsemble('parallel-flow', input);
const duration = performance.now() - startTime;
expect(result).toBeSuccessful();
// Should be faster than sequential
expect(duration).toBeLessThan(5000);
});
Testing Built-In Members
Test Scrape Member
Copy
it('should scrape website', async () => {
const conductor = await TestConductor.create({
mocks: {
http: {
responses: {
'https://example.com': {
status: 200,
body: '<html><body><h1>Test Page</h1></body></html>'
}
}
}
}
});
const result = await conductor.executeMember('scrape', {
url: 'https://example.com',
output: 'markdown'
});
expect(result).toBeSuccessful();
expect(result.output.content).toContain('Test Page');
});
Test Validate Member
Copy
it('should validate content quality', async () => {
const conductor = await TestConductor.create();
const result = await conductor.executeMember('validate', {
content: 'This is high-quality content with proper grammar.',
criteria: {
grammar: 'Must have proper grammar',
length: 'Must be at least 10 characters'
},
thresholds: {
minimum: 0.7
}
});
expect(result).toBeSuccessful();
expect(result.output.score).toBeGreaterThan(0.7);
expect(result.output.passed).toBe(true);
});
Test RAG Member
Copy
it('should search and retrieve documents', async () => {
const conductor = await TestConductor.create({
mocks: {
vectorize: {
searches: {
'how to deploy': [
{ id: 'doc1', score: 0.95, metadata: { title: 'Deployment Guide' } }
]
}
}
}
});
const result = await conductor.executeMember('rag', {
operation: 'search',
query: 'how to deploy',
topK: 5
});
expect(result).toBeSuccessful();
expect(result.output.results).toHaveLength(1);
expect(result.output.results[0].score).toBe(0.95);
});
Testing Best Practices
1. Use Descriptive Test Names
Copy
// ✅ Good - clear what's being tested
it('should return positive sentiment for text containing "love"', async () => {
// ...
});
// ❌ Bad - unclear what's being tested
it('test sentiment', async () => {
// ...
});
2. Test One Thing Per Test
Copy
// ✅ Good - focused test
it('should cache AI responses', async () => {
// Test only caching behavior
});
it('should handle errors gracefully', async () => {
// Test only error handling
});
// ❌ Bad - testing multiple concerns
it('should cache and handle errors', async () => {
// Too much in one test
});
3. Use Arrange-Act-Assert Pattern
Copy
it('should analyze sentiment', async () => {
// Arrange - setup
const conductor = await TestConductor.create();
const input = { text: 'I love this!' };
// Act - execute
const result = await conductor.executeEnsemble('sentiment-analysis', input);
// Assert - verify
expect(result).toBeSuccessful();
expect(result.output.sentiment).toBe('positive');
});
4. Mock External Dependencies
Copy
// ✅ Good - mocked for fast, reliable tests
const conductor = await TestConductor.create({
mocks: {
ai: { responses: { ... } },
http: { responses: { ... } }
}
});
// ❌ Bad - real API calls (slow, flaky)
const conductor = await TestConductor.create();
// Uses real OpenAI, real databases
5. Test Error Paths
Copy
describe('error handling', () => {
it('should handle missing required input', async () => {
const result = await conductor.executeEnsemble('analyze', {});
expect(result).toHaveError(/required/);
});
it('should handle invalid domain format', async () => {
const result = await conductor.executeEnsemble('analyze', {
domain: 'not-a-domain'
});
expect(result).toHaveError(/invalid domain/);
});
});
Running Tests
Run All Tests
Copy
npm test
Run Specific Test File
Copy
npm test -- src/ensembles/sentiment.test.ts
Run with Coverage
Copy
npm test -- --coverage
Run in Watch Mode
Copy
npm test -- --watch
Run with UI
Copy
npm test -- --ui
CI/CD Integration
GitHub Actions
Copy
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
Example Test Suite
Complete example testing a sentiment analysis ensemble:Copy
import { describe, it, expect, beforeEach } from 'vitest';
import { TestConductor } from '@ensemble-edge/conductor/testing';
describe('sentiment-analysis ensemble', () => {
let conductor: TestConductor;
beforeEach(async () => {
conductor = await TestConductor.create({
projectPath: './',
mocks: {
ai: {
responses: {
'analyze-sentiment': {
sentiment: 'positive',
confidence: 0.95
}
}
}
}
});
});
describe('successful execution', () => {
it('should analyze positive sentiment', async () => {
const result = await conductor.executeEnsemble('sentiment-analysis', {
text: 'I love this product!',
name: 'Alice'
});
expect(result).toBeSuccessful();
expect(result.output.sentiment).toBe('positive');
expect(result.output.confidence).toBe(0.95);
expect(result.output.greeting).toContain('Alice');
});
it('should cache results', async () => {
const input = { text: 'Great!', name: 'Bob' };
const result1 = await conductor.executeEnsemble('sentiment-analysis', input);
const result2 = await conductor.executeEnsemble('sentiment-analysis', input);
expect(result1).not.toHaveCachedResult();
expect(result2).toHaveCachedResult();
});
it('should execute both members', async () => {
const result = await conductor.executeEnsemble('sentiment-analysis', {
text: 'Amazing!',
name: 'Charlie'
});
expect(result).toHaveExecutedMember('analyze-sentiment');
expect(result).toHaveExecutedMember('greet');
});
});
describe('error handling', () => {
it('should handle missing text', async () => {
const result = await conductor.executeEnsemble('sentiment-analysis', {
name: 'Alice'
});
expect(result).toHaveError();
expect(result.error.message).toContain('text');
});
it('should handle AI provider failure', async () => {
conductor = await TestConductor.create({
mocks: {
ai: {
handler: async () => {
throw new Error('AI provider unavailable');
}
}
}
});
const result = await conductor.executeEnsemble('sentiment-analysis', {
text: 'Test',
name: 'Bob'
});
expect(result).toHaveError(/provider unavailable/);
});
});
describe('performance', () => {
it('should execute within time limit', async () => {
const result = await conductor.executeEnsemble('sentiment-analysis', {
text: 'Test',
name: 'Alice'
});
expect(result.executionTime).toBeLessThan(2000); // 2 seconds
});
});
});

