cmte
Version:
Design by Committee™ except it's just you and LLMs
281 lines (258 loc) • 8.43 kB
JavaScript
/**
* Test file for the execution engine
*/
import path from 'path';
import fs from 'fs/promises';
import { fileURLToPath } from 'url';
import { ComponentRegistry } from './components/registry.js';
import { WorkflowExecutor } from './execution/workflow-executor.js';
import { createEmptyResumeState, saveResumeState, loadResumeState, markFileAsReviewed } from './state/index.js';
import { logger } from '../utils/logger.js';
// Get dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Create the output directory path
const outputDir = path.resolve(__dirname, '../../_output/test-workflow');
// Create a test workflow
const testWorkflow = {
name: 'test-workflow',
description: 'Test workflow for execution engine',
outputPath: outputDir,
phases: [{
usePhase: 'phase1',
dependsOn: []
}, {
usePhase: 'phase2',
dependsOn: ['phase1'],
humanInputRequired: [path.join(outputDir, 'phase1/example-output.o.md')]
}]
};
// Create test phases
const phase1 = {
name: 'phase1',
description: 'Test phase 1',
execution: 'sequential',
set: [{
useSet: 'set1'
}]
};
const phase2 = {
name: 'phase2',
description: 'Test phase 2',
execution: 'sequential',
set: [{
useSet: 'set2'
}]
};
// Create test sets
const set1 = {
name: 'set1',
description: 'Test set 1',
execution: 'sequential',
tasks: [{
description: 'Test task 1',
useTask: 'task1'
}]
};
const set2 = {
name: 'set2',
description: 'Test set 2',
execution: 'sequential',
tasks: [{
description: 'Test task 2',
useTask: 'task2'
}]
};
// Create test tasks
const task1 = {
name: 'task1',
description: 'Test task 1'
};
const task2 = {
name: 'task2',
description: 'Test task 2'
};
/**
* Run the test
*/
async function runTest() {
try {
// Create a mock registry
const registry = new ComponentRegistry(outputDir);
// Add test components to registry
await registry.addTask('task1', task1, 'Test task 1 content');
await registry.addTask('task2', task2, 'Test task 2 content');
await registry.addSet('set1', set1);
await registry.addSet('set2', set2);
await registry.addPhase('phase1', phase1);
await registry.addPhase('phase2', phase2);
await registry.addWorkflow('test-workflow', testWorkflow);
// Create the workflow executor
const workflowPath = outputDir;
const executor = new WorkflowExecutor({
registry,
workflowPath,
outputPath: testWorkflow.outputPath,
initialContext: {
testVar: 'test value'
}
});
// Create a test output file to simulate phase1 output
const phase1Dir = path.join(workflowPath, 'phase1');
await fs.mkdir(phase1Dir, {
recursive: true
});
const exampleOutputPath = path.join(phase1Dir, 'example-output.o.md');
await fs.writeFile(exampleOutputPath, '# Example Output\n\nThis is a test output file.');
logger.info(`Created test file at: ${exampleOutputPath}`);
// Execute the workflow
logger.info('Starting workflow execution');
const completed = await executor.executeWorkflow(testWorkflow);
if (completed) {
logger.info('Workflow completed successfully');
} else {
logger.info('Workflow awaiting human input');
// List the files awaiting review
const state = await loadResumeState(workflowPath);
logger.info('Files awaiting review:');
state.filesAwaitingReview.forEach(file => {
logger.info(`- ${file}`);
});
// Properly mark the file as reviewed, which will also add it to processedHumanInputPaths
const fileToReview = state.filesAwaitingReview[0];
const updatedState = markFileAsReviewed(state, fileToReview);
logger.info(`Updated state with markFileAsReviewed: ${JSON.stringify(updatedState)}`);
// Save the updated state
await saveResumeState(workflowPath, updatedState);
// Resume the workflow
logger.info('Resuming workflow execution');
const resumeCompleted = await executor.resumeWorkflow(testWorkflow);
if (resumeCompleted) {
logger.info('Workflow completed successfully after resuming');
} else {
logger.error('Workflow still awaiting human input after resuming');
}
}
} catch (error) {
logger.error(`Test failed: ${error}`);
console.error(error);
}
}
// Run the test if this file is executed directly
if (import.meta.url.startsWith('file:')) {
const modulePath = fileURLToPath(import.meta.url);
if (process.argv[1] === modulePath) {
runTest().catch(error => {
logger.error(`Test failed: ${error}`);
console.error(error);
process.exit(1);
});
}
}
export { runTest };
/**
* Run a workflow from the beginning
*/
export async function runWorkflow(workflowFolder, options = {}) {
logger.info('Starting workflow execution', {
workflowFolder,
options,
cwd: process.cwd()
});
try {
// Create registry and load workflow
const registry = new ComponentRegistry(process.cwd());
logger.info('Created component registry');
// Load the workflow
logger.info(`Loading workflow from ${workflowFolder}`);
const workflow = await registry.loadWorkflow(workflowFolder);
logger.info('Loaded workflow', {
name: workflow.name,
phases: workflow.phases.length,
outputPath: workflow.outputPath
});
// Create the workflow executor
const executor = new WorkflowExecutor({
registry,
workflowPath: workflowFolder,
outputPath: workflow.outputPath,
savePrompts: options.savePrompts || options.dryRun,
// Always save prompts in dry run mode
dryRun: options.dryRun,
apiDryRun: options.apiDryRun,
useHaikuModel: options.useHaikuModel,
useLocalLLM: options.useLocalLLM ?? false,
initialContext: options.initialContext || {}
});
logger.info('Created workflow executor', {
savePrompts: options.savePrompts || options.dryRun,
dryRun: options.dryRun,
apiDryRun: options.apiDryRun,
useHaikuModel: options.useHaikuModel,
useLocalLLM: options.useLocalLLM ?? false
});
// Execute the workflow
const completed = await executor.executeWorkflow(workflow);
if (!completed) {
logger.info('Workflow paused for human input');
// Get the state to show what files need review
const state = await loadResumeState(workflowFolder);
if (state.filesAwaitingReview.length > 0) {
logger.info('Files awaiting review:', {
files: state.filesAwaitingReview
});
}
} else {
logger.info('Workflow completed successfully');
}
} catch (error) {
logger.error('Error executing workflow', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
throw error;
}
}
/**
* Continue a workflow from where it left off
*/
export async function nextStep(workflowFolder, options = {}) {
const registry = new ComponentRegistry(workflowFolder);
const workflow = await registry.loadWorkflow('.');
const executor = new WorkflowExecutor({
registry,
workflowPath: workflowFolder,
outputPath: workflow.outputPath,
savePrompts: options.savePrompts,
dryRun: options.dryRun,
apiDryRun: options.apiDryRun,
useHaikuModel: options.useHaikuModel,
useLocalLLM: options.useLocalLLM ?? false,
initialContext: options.initialContext
});
const completed = await executor.resumeWorkflow(workflow);
if (!completed) {
logger.info('Workflow still requires human input');
}
}
/**
* Review a file in a workflow
*/
export async function reviewFile(workflowFolder, filePath) {
const state = await loadResumeState(workflowFolder);
if (!state) {
throw new Error('No workflow state found');
}
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(workflowFolder, filePath);
const updatedState = markFileAsReviewed(state, absolutePath);
await saveResumeState(workflowFolder, updatedState);
logger.info(`Marked ${filePath} as reviewed`);
}
/**
* Initialize a new workflow
*/
export async function initWorkflow(workflowFolder, options = {}) {
const state = createEmptyResumeState();
await saveResumeState(workflowFolder, state);
logger.info(`Initialized workflow in ${workflowFolder}`);
}