UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

545 lines 20.5 kB
/** * 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