@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
329 lines ⢠12.5 kB
JavaScript
/**
* Dry Run Executor
*
* Simulates template execution without making actual API calls.
* Shows what entities would be created and in what order.
*/
import { ReferenceResolver } from './ReferenceResolver';
import { OrchestrationRulesLayer } from './OrchestrationRulesLayer';
export class DryRunExecutor {
referenceResolver;
orchestrationRules;
simulatedEntities;
constructor() {
this.referenceResolver = new ReferenceResolver();
this.orchestrationRules = new OrchestrationRulesLayer();
this.simulatedEntities = new Map();
}
/**
* Execute a dry run of a template
*/
async execute(template, options) {
const steps = [];
const errors = [];
const warnings = [];
let totalEstimatedTime = 0;
// Reset simulated entities
this.simulatedEntities.clear();
// Process parameters
const resolvedParams = this.resolveParameters(template.parameters || {}, options.parameters || {});
// Process each step
for (const step of template.steps || []) {
try {
const dryRunStep = await this.processStep(step, {
...options,
template,
resolvedParams,
previousSteps: steps
});
steps.push(dryRunStep);
totalEstimatedTime += dryRunStep.estimatedTime;
// Stop on error if configured
if (options.stopOnError && dryRunStep.action === 'skip' && dryRunStep.reason?.includes('Error')) {
errors.push(`Step ${step.id} failed: ${dryRunStep.reason}`);
break;
}
}
catch (error) {
errors.push(`Error processing step ${step.id}: ${error instanceof Error ? error.message : String(error)}`);
if (options.stopOnError)
break;
}
}
// Generate summary
const summary = {
totalSteps: steps.length,
createActions: steps.filter(s => s.action === 'create').length,
updateActions: steps.filter(s => s.action === 'update').length,
lookupActions: steps.filter(s => s.action === 'lookup').length,
skippedSteps: steps.filter(s => s.action === 'skip').length
};
return {
success: errors.length === 0,
steps,
totalEstimatedTime,
errors,
warnings,
summary
};
}
/**
* Process a single step
*/
async processStep(step, context) {
const entityType = this.extractEntityType(step.template?.system_template_id);
if (!entityType) {
return {
stepId: step.id,
action: 'skip',
entityType: 'unknown',
entityName: 'Unknown',
reason: 'Could not determine entity type',
inputs: {},
outputs: {},
dependencies: [],
warnings: ['Invalid system_template_id'],
estimatedTime: 0
};
}
// Resolve inputs
const resolvedInputs = await this.resolveInputs(step.template?.inputs || {}, context);
// Check dependencies
const dependencies = this.extractDependencies(step.template?.inputs || {});
// Determine action
let action = 'create';
let reason = '';
// Check if entity would be looked up
if (resolvedInputs.ref) {
action = 'lookup';
reason = 'Entity reference provided';
}
// Simulate entity creation
const simulatedEntity = {
id: `simulated_${step.id}_${Date.now()}`,
type: entityType,
name: resolvedInputs.name || step.id,
...resolvedInputs
};
// Store for future reference resolution
this.simulatedEntities.set(step.id, simulatedEntity);
// Extract warnings from orchestration rules
const warnings = await this.checkOrchestrationWarnings(entityType, resolvedInputs, context.template.platform || 'web');
return {
stepId: step.id,
action,
entityType,
entityName: resolvedInputs.name || step.id,
reason,
inputs: resolvedInputs,
outputs: {
entity_id: simulatedEntity.id,
entity_type: entityType,
entity_name: simulatedEntity.name
},
dependencies,
warnings,
estimatedTime: this.estimateExecutionTime(action, entityType)
};
}
/**
* Resolve template parameters
*/
resolveParameters(templateParams, providedParams) {
const resolved = {};
for (const [key, config] of Object.entries(templateParams)) {
if (providedParams[key] !== undefined) {
resolved[key] = providedParams[key];
}
else if (config.default !== undefined) {
resolved[key] = config.default;
}
else if (config.required) {
resolved[key] = `<MISSING_REQUIRED_PARAM:${key}>`;
}
}
return { ...providedParams, ...resolved };
}
/**
* Resolve step inputs (replace parameter references)
*/
async resolveInputs(inputs, context) {
const resolved = {};
for (const [key, value] of Object.entries(inputs)) {
if (typeof value === 'string') {
// Parameter reference
if (value.startsWith('${') && value.endsWith('}') && !value.includes('.')) {
const paramName = value.slice(2, -1);
resolved[key] = context.resolvedParams[paramName] || value;
}
// Step reference
else if (value.startsWith('${') && value.endsWith('}')) {
const [stepId, field] = value.slice(2, -1).split('.');
const previousStep = context.previousSteps.find((s) => s.stepId === stepId);
if (previousStep) {
resolved[key] = previousStep.outputs[field] || `<REF:${stepId}.${field}>`;
}
else {
resolved[key] = `<UNRESOLVED_REF:${value}>`;
}
}
else {
resolved[key] = value;
}
}
else if (Array.isArray(value)) {
resolved[key] = await Promise.all(value.map(item => this.resolveInputs(typeof item === 'object' ? item : { value: item }, context)));
}
else if (typeof value === 'object' && value !== null) {
resolved[key] = await this.resolveInputs(value, context);
}
else {
resolved[key] = value;
}
}
return resolved;
}
/**
* Extract dependencies from inputs
*/
extractDependencies(inputs) {
const deps = new Set();
const extract = (obj) => {
if (typeof obj === 'string' && obj.startsWith('${') && obj.endsWith('}')) {
const [stepId] = obj.slice(2, -1).split('.');
if (stepId)
deps.add(stepId);
}
else if (Array.isArray(obj)) {
obj.forEach(extract);
}
else if (typeof obj === 'object' && obj !== null) {
Object.values(obj).forEach(extract);
}
};
extract(inputs);
return Array.from(deps);
}
/**
* Check for orchestration warnings
*/
async checkOrchestrationWarnings(entityType, inputs, platform) {
const warnings = [];
// Platform compatibility warnings
const webOnly = ['experiment', 'campaign', 'page'];
const featureOnly = ['flag', 'ruleset', 'rule'];
if (webOnly.includes(entityType) && platform === 'feature') {
warnings.push(`${entityType} is not typically used on feature platform`);
}
if (featureOnly.includes(entityType) && platform === 'web') {
warnings.push(`${entityType} is not supported on web platform`);
}
// Check for common issues
if (entityType === 'experiment' && !inputs.variations) {
warnings.push('Experiment missing variations');
}
if (entityType === 'campaign' && !inputs.metrics) {
warnings.push('Campaign missing metrics');
}
return warnings;
}
/**
* Estimate execution time for an action
*/
estimateExecutionTime(action, entityType) {
const baseTime = {
create: 2000,
update: 1500,
lookup: 500,
skip: 0
};
const entityComplexity = {
experiment: 1.5,
campaign: 1.5,
audience: 1.2,
flag: 1.0,
ruleset: 1.3,
page: 0.8,
event: 0.7
};
const base = baseTime[action] || 1000;
const complexity = entityComplexity[entityType] || 1.0;
return Math.round(base * complexity);
}
/**
* Extract entity type from system template ID
*/
extractEntityType(systemTemplateId) {
if (!systemTemplateId)
return null;
const patterns = [
{ regex: /optimizely_experiment_/, type: 'experiment' },
{ regex: /optimizely_campaign_/, type: 'campaign' },
{ regex: /optimizely_audience_/, type: 'audience' },
{ regex: /optimizely_event_/, type: 'event' },
{ regex: /optimizely_flag_/, type: 'flag' },
{ regex: /optimizely_ruleset_/, type: 'ruleset' },
{ regex: /optimizely_rule_/, type: 'rule' },
{ regex: /optimizely_page_/, type: 'page' },
{ regex: /optimizely_environment_/, type: 'environment' },
{ regex: /optimizely_project_/, type: 'project' }
];
for (const pattern of patterns) {
if (pattern.regex.test(systemTemplateId)) {
return pattern.type;
}
}
return null;
}
/**
* Format dry run results for display
*/
formatResults(result) {
const lines = [];
lines.push('TEMPLATE DRY RUN PREVIEW');
lines.push('========================\n');
if (!result.success) {
lines.push('â Dry run failed with errors:');
result.errors.forEach(err => lines.push(` ⢠${err}`));
lines.push('');
}
lines.push('EXECUTION PLAN:');
lines.push('---------------\n');
for (const step of result.steps) {
const icon = {
create: 'â¨',
update: 'đ',
lookup: 'đ',
skip: 'âď¸'
}[step.action];
lines.push(`${icon} Step ${step.stepId}: ${step.action.toUpperCase()} ${step.entityType}`);
lines.push(` Name: ${step.entityName}`);
if (step.reason) {
lines.push(` Reason: ${step.reason}`);
}
if (step.dependencies.length > 0) {
lines.push(` Dependencies: ${step.dependencies.join(', ')}`);
}
if (step.warnings.length > 0) {
lines.push(` â ď¸ Warnings:`);
step.warnings.forEach(w => lines.push(` ⢠${w}`));
}
lines.push(` Estimated time: ${step.estimatedTime}ms`);
lines.push('');
}
lines.push('SUMMARY:');
lines.push('--------');
lines.push(`Total steps: ${result.summary.totalSteps}`);
lines.push(` ⢠Create: ${result.summary.createActions}`);
lines.push(` ⢠Update: ${result.summary.updateActions}`);
lines.push(` ⢠Lookup: ${result.summary.lookupActions}`);
lines.push(` ⢠Skip: ${result.summary.skippedSteps}`);
lines.push(`\nEstimated total time: ${(result.totalEstimatedTime / 1000).toFixed(1)}s`);
if (result.warnings.length > 0) {
lines.push('\nâ ď¸ WARNINGS:');
result.warnings.forEach(w => lines.push(` ⢠${w}`));
}
return lines.join('\n');
}
}
//# sourceMappingURL=DryRunExecutor.js.map