@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
545 lines • 20.5 kB
JavaScript
/**
* Test Suite for Multi-Entity Query Planning - Phase 4A Implementation
*
* Tests the complete multi-entity join planning system including:
* - JoinPathPlanner for optimal join path discovery
* - MultiTableSQLBuilder for complex SQL generation
* - QueryPlanner multi-entity planning capabilities
*
* This validates the critical user requirement:
* "experiments that have certain pages and are using specific event metrics"
*/
import { getLogger } from '../../logging/Logger.js';
import { OptimizelyAdapter } from './adapters/OptimizelyAdapter.js';
import { JoinPathPlanner } from './JoinPathPlanner.js';
import { MultiTableSQLBuilder } from './MultiTableSQLBuilder.js';
import { QueryPlanner } from './QueryPlanner.js';
import { FieldCatalog } from './FieldCatalog.js';
import Database from 'better-sqlite3';
const logger = getLogger();
/**
* Test Cases for Multi-Entity Queries
*/
const MULTI_ENTITY_TEST_QUERIES = [
{
name: 'Experiments with Pages',
description: 'Basic two-entity join between experiments and pages',
query: {
find: 'experiments',
select: ['experiments.name', 'pages.edit_url'],
where: [{ field: 'experiments.status', operator: '=', value: 'running' }],
limit: 10
},
expectedEntities: ['experiments', 'pages'],
expectedJoinCount: 1
},
{
name: 'Experiments with Pages and Events',
description: 'Three-entity join: experiments → pages → events',
query: {
find: 'experiments',
select: ['experiments.name', 'pages.edit_url', 'events.name'],
where: [
{ field: 'experiments.status', operator: '=', value: 'running' },
{ field: 'events.event_type', operator: '=', value: 'click' }
],
limit: 5
},
expectedEntities: ['experiments', 'pages', 'events'],
expectedJoinCount: 2
},
{
name: 'Experiments with Audiences and Events',
description: 'Complex many-to-many relationships through junction tables',
query: {
find: 'experiments',
select: ['experiments.name', 'audiences.name', 'events.name'],
where: [
{ field: 'audiences.archived', operator: '=', value: false },
{ field: 'events.event_type', operator: '=', value: 'conversion' }
],
groupBy: ['experiments.name'],
limit: 10
},
expectedEntities: ['experiments', 'audiences', 'events'],
expectedJoinCount: 2
},
{
name: 'Flags with Environments and Variations',
description: 'Feature flag relationships with environment-specific data',
query: {
find: 'flags',
select: ['flags.key', 'flag_environments.environment_key', 'variations.key'],
where: [
{ field: 'flag_environments.environment_key', operator: 'IN', value: ['production', 'staging'] },
{ field: 'variations.archived', operator: '=', value: false }
],
orderBy: [{ field: 'flags.created_time', direction: 'DESC' }],
limit: 20
},
expectedEntities: ['flags', 'flag_environments', 'variations'],
expectedJoinCount: 2
},
{
name: 'Pages with Events and Experiments',
description: 'Page-centric analysis across multiple entities',
query: {
find: 'pages',
select: ['pages.name', 'events.name', 'experiments.name', 'COUNT(*) as total_experiments'],
where: [
{ field: 'pages.activation_mode', operator: '=', value: 'immediate' },
{ field: 'events.event_type', operator: '!=', value: 'custom' }
],
groupBy: ['pages.name', 'events.name'],
orderBy: [{ field: 'total_experiments', direction: 'DESC' }],
limit: 15
},
expectedEntities: ['pages', 'events', 'experiments'],
expectedJoinCount: 2
},
{
name: 'Complex Aggregation Query',
description: 'Multi-entity query with complex aggregations and grouping',
query: {
find: 'experiments',
select: [
'experiments.name',
'pages.edit_url',
'events.name as conversion_event',
'COUNT(*) as conversions',
'COUNT(DISTINCT pages.id) as unique_pages'
],
where: [
{ field: 'experiments.status', operator: '=', value: 'running' },
{ field: 'pages.activation_mode', operator: '=', value: 'immediate' },
{ field: 'events.event_type', operator: '=', value: 'click' }
],
groupBy: ['experiments.name', 'pages.edit_url', 'events.name'],
orderBy: [{ field: 'conversions', direction: 'DESC' }],
limit: 10
},
expectedEntities: ['experiments', 'pages', 'events'],
expectedJoinCount: 2
}
];
/**
* Run comprehensive multi-entity query tests
*/
export async function runMultiEntityTests() {
console.info('Starting Multi-Entity Query Planning Tests');
const results = [];
try {
// Initialize database connection
const db = new Database('./data/optimizely-cache.db', { readonly: true });
// Initialize test components
const fieldCatalog = new FieldCatalog();
const joinPathPlanner = new JoinPathPlanner();
const multiTableSQLBuilder = new MultiTableSQLBuilder(fieldCatalog);
// Initialize adapters
const optimizelyAdapter = new OptimizelyAdapter({ database: db });
const adapters = new Map();
adapters.set('optimizely', optimizelyAdapter);
const queryPlanner = new QueryPlanner(fieldCatalog, adapters);
// Test 1: JoinPathPlanner Basic Functionality
results.push(await testJoinPathPlanner(joinPathPlanner));
// Test 2: Multi-Entity Discovery
results.push(await testEntityDiscovery(queryPlanner));
// Test 3: Join Path Planning
results.push(await testJoinPathPlanning(joinPathPlanner));
// Test 4: Multi-Table SQL Generation
results.push(await testMultiTableSQLGeneration(multiTableSQLBuilder, fieldCatalog));
// Test 5: End-to-End Multi-Entity Queries
for (const testCase of MULTI_ENTITY_TEST_QUERIES) {
results.push(await testMultiEntityQuery(queryPlanner, testCase));
}
// Test 6: Performance and Cost Analysis
results.push(await testPerformanceAnalysis(queryPlanner));
// Test 7: Error Handling
results.push(await testErrorHandling(queryPlanner));
// Close database
db.close();
}
catch (error) {
console.error(`Test suite failed to initialize: ${error}`);
results.push({
name: 'Test Suite Initialization',
status: 'FAIL',
duration: 0,
error: error instanceof Error ? error.message : String(error)
});
}
// Print summary
printTestSummary(results);
return results;
}
async function testJoinPathPlanner(planner) {
const startTime = Date.now();
try {
// Test basic join path planning
const entities = ['experiments', 'pages', 'events'];
const joinPaths = await planner.findOptimalJoinPath(entities);
if (joinPaths.length === 0) {
throw new Error('No join paths found for basic entities');
}
// Validate join paths
const isValid = planner.validateJoinPath(joinPaths);
if (!isValid) {
throw new Error('Generated join paths failed validation');
}
// Test cost calculation
const cost = planner.calculateJoinCost(joinPaths);
if (cost <= 0) {
throw new Error('Invalid join cost calculation');
}
// Test statistics
const stats = planner.getStatistics();
if (stats.totalEntities === 0) {
throw new Error('Join planner statistics are empty');
}
return {
name: 'JoinPathPlanner Basic Functionality',
status: 'PASS',
duration: Date.now() - startTime,
details: {
joinPathCount: joinPaths.length,
totalCost: cost,
isValid,
statistics: stats
}
};
}
catch (error) {
return {
name: 'JoinPathPlanner Basic Functionality',
status: 'FAIL',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error)
};
}
}
async function testEntityDiscovery(planner) {
const startTime = Date.now();
try {
const query = {
find: 'experiments',
select: ['experiments.name', 'pages.edit_url', 'events.name'],
where: [{ field: 'experiments.status', operator: '=', value: 'running' }]
};
// This tests the private method through createMultiEntityPlan
const plan = await planner.createMultiEntityPlan(query);
if (!plan) {
throw new Error('Failed to create multi-entity plan');
}
if (plan.strategy === 'pure-sql' || plan.strategy === 'hybrid-sql-first') {
// Success - indicates multi-entity capabilities detected
}
else {
console.warn(`Unexpected strategy for multi-entity query: ${plan.strategy}`);
}
return {
name: 'Multi-Entity Discovery',
status: 'PASS',
duration: Date.now() - startTime,
details: {
strategy: plan.strategy,
phaseCount: plan.phases.length,
estimatedCost: plan.estimatedCost
}
};
}
catch (error) {
return {
name: 'Multi-Entity Discovery',
status: 'FAIL',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error)
};
}
}
async function testJoinPathPlanning(planner) {
const startTime = Date.now();
try {
const testCases = [
{ entities: ['experiments', 'pages'], expectedJoins: 1 },
{ entities: ['experiments', 'pages', 'events'], expectedJoins: 2 },
{ entities: ['flags', 'flag_environments'], expectedJoins: 1 },
{ entities: ['experiments', 'audiences'], expectedJoins: 1 }
];
const results = [];
for (const testCase of testCases) {
const joinPaths = await planner.findOptimalJoinPath(testCase.entities);
const isValid = planner.validateJoinPath(joinPaths);
results.push({
entities: testCase.entities,
joinPathCount: joinPaths.length,
expectedJoins: testCase.expectedJoins,
isValid,
cost: planner.calculateJoinCost(joinPaths)
});
if (!isValid) {
throw new Error(`Invalid join path for entities: ${testCase.entities.join(', ')}`);
}
}
return {
name: 'Join Path Planning',
status: 'PASS',
duration: Date.now() - startTime,
details: { testResults: results }
};
}
catch (error) {
return {
name: 'Join Path Planning',
status: 'FAIL',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error)
};
}
}
async function testMultiTableSQLGeneration(builder, fieldCatalog) {
const startTime = Date.now();
try {
const query = {
find: 'experiments',
select: ['experiments.name', 'pages.edit_url'],
where: [{ field: 'experiments.status', operator: '=', value: 'running' }],
limit: 10
};
const sql = await builder.buildMultiEntitySQL(query);
if (!sql) {
throw new Error('Failed to generate SQL');
}
// Validate SQL contains expected elements
if (!sql.includes('SELECT')) {
throw new Error('Generated SQL missing SELECT clause');
}
if (!sql.includes('FROM')) {
throw new Error('Generated SQL missing FROM clause');
}
// For multi-entity queries, should contain JOINs
if (sql.includes('pages.edit_url') && !sql.includes('JOIN')) {
console.warn('Multi-entity query generated without JOINs - may need field resolution enhancement');
}
const stats = builder.getJoinStatistics();
return {
name: 'Multi-Table SQL Generation',
status: 'PASS',
duration: Date.now() - startTime,
details: {
generatedSQL: sql,
joinStatistics: stats
}
};
}
catch (error) {
return {
name: 'Multi-Table SQL Generation',
status: 'FAIL',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error)
};
}
}
async function testMultiEntityQuery(planner, testCase) {
const startTime = Date.now();
try {
const plan = await planner.createMultiEntityPlan(testCase.query);
if (!plan) {
throw new Error('Failed to create execution plan');
}
// Validate plan structure
if (plan.phases.length === 0) {
throw new Error('Execution plan has no phases');
}
if (plan.estimatedCost <= 0) {
throw new Error('Invalid estimated cost');
}
// Check if strategy is appropriate for multi-entity query
const isMultiEntityStrategy = plan.strategy === 'pure-sql' ||
plan.strategy === 'hybrid-sql-first' ||
plan.strategy === 'hybrid-jsonata-first';
return {
name: testCase.name,
status: 'PASS',
duration: Date.now() - startTime,
details: {
description: testCase.description,
strategy: plan.strategy,
isMultiEntityStrategy,
phaseCount: plan.phases.length,
estimatedCost: plan.estimatedCost,
estimatedRows: plan.estimatedRows
}
};
}
catch (error) {
return {
name: testCase.name,
status: 'FAIL',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error)
};
}
}
async function testPerformanceAnalysis(planner) {
const startTime = Date.now();
try {
const performanceQueries = [
{
name: 'Simple two-entity',
query: {
find: 'experiments',
select: ['experiments.name', 'pages.edit_url'],
limit: 10
}
},
{
name: 'Complex three-entity',
query: {
find: 'experiments',
select: ['experiments.name', 'pages.edit_url', 'events.name'],
where: [
{ field: 'experiments.status', operator: '=', value: 'running' },
{ field: 'events.event_type', operator: '=', value: 'click' }
],
groupBy: ['experiments.name'],
limit: 5
}
}
];
const results = [];
for (const perfTest of performanceQueries) {
const testStart = Date.now();
const plan = await planner.createMultiEntityPlan(perfTest.query);
const planningTime = Date.now() - testStart;
results.push({
name: perfTest.name,
planningTime,
estimatedCost: plan.estimatedCost,
strategy: plan.strategy,
phaseCount: plan.phases.length
});
}
// Check join planner statistics
const joinStats = planner.getJoinPlannerStatistics();
return {
name: 'Performance Analysis',
status: 'PASS',
duration: Date.now() - startTime,
details: {
performanceTests: results,
joinPlannerStats: joinStats
}
};
}
catch (error) {
return {
name: 'Performance Analysis',
status: 'FAIL',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error)
};
}
}
async function testErrorHandling(planner) {
const startTime = Date.now();
try {
const errorTestCases = [
{
name: 'Unknown entity',
query: {
find: 'unknown_entity',
select: ['unknown_entity.name'],
limit: 1
},
expectError: true
},
{
name: 'Invalid field reference',
query: {
find: 'experiments',
select: ['experiments.nonexistent_field'],
limit: 1
},
expectError: false // Should handle gracefully
}
];
const results = [];
for (const errorTest of errorTestCases) {
try {
const plan = await planner.createMultiEntityPlan(errorTest.query);
results.push({
name: errorTest.name,
expectedError: errorTest.expectError,
actualError: false,
plan: plan ? { strategy: plan.strategy, phases: plan.phases.length } : null
});
}
catch (error) {
results.push({
name: errorTest.name,
expectedError: errorTest.expectError,
actualError: true,
error: error instanceof Error ? error.message : String(error)
});
}
}
return {
name: 'Error Handling',
status: 'PASS',
duration: Date.now() - startTime,
details: { errorTests: results }
};
}
catch (error) {
return {
name: 'Error Handling',
status: 'FAIL',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error)
};
}
}
function printTestSummary(results) {
const passed = results.filter(r => r.status === 'PASS').length;
const failed = results.filter(r => r.status === 'FAIL').length;
const skipped = results.filter(r => r.status === 'SKIP').length;
const total = results.length;
console.log('\n' + '='.repeat(80));
console.log('MULTI-ENTITY QUERY PLANNING TEST SUMMARY');
console.log('='.repeat(80));
console.log(`Total Tests: ${total}`);
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
console.log(`⏭️ Skipped: ${skipped}`);
console.log(`Success Rate: ${((passed / total) * 100).toFixed(1)}%`);
console.log('='.repeat(80));
// Show failed tests
const failedTests = results.filter(r => r.status === 'FAIL');
if (failedTests.length > 0) {
console.log('\nFAILED TESTS:');
for (const test of failedTests) {
console.log(`${test.name}: ${test.error}`);
}
}
// Show timing summary
const totalTime = results.reduce((sum, r) => sum + r.duration, 0);
console.log(`\nTotal Execution Time: ${totalTime}ms`);
console.log(`Average Test Time: ${(totalTime / total).toFixed(1)}ms`);
console.log('\n' + '='.repeat(80));
}
// Export for standalone testing
export { MULTI_ENTITY_TEST_QUERIES };
// Run tests if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
runMultiEntityTests()
.then(results => {
const failed = results.filter(r => r.status === 'FAIL').length;
process.exit(failed > 0 ? 1 : 0);
})
.catch(error => {
console.error('Test suite failed:', error);
process.exit(1);
});
}
//# sourceMappingURL=test-multi-entity-queries.js.map