UNPKG

claude-flow

Version:

Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)

746 lines (637 loc) 25.2 kB
const { TestHarness } = require('../test-harness'); const assert = require('assert'); describe('Rollback Mechanism Tests', () => { let harness; beforeEach(() => { harness = new TestHarness(); harness.createMockProject('standard'); }); afterEach(() => { harness.reset(); }); describe('Transaction Rollback', () => { it('should rollback file operations on failure', async () => { // Track all file operations for rollback const fileOperations = []; const originalFiles = new Map(); // Save original state for (const [path, content] of harness.mockFS.entries()) { originalFiles.set(path, content); } const operations = [ { id: 1, type: 'create', path: 'src/new-file1.js', content: 'export const feature1 = () => {};' }, { id: 2, type: 'update', path: 'src/index.js', content: 'import { feature1 } from "./new-file1";\nconsole.log("Updated");' }, { id: 3, type: 'create', path: 'src/new-file2.js', content: 'export const feature2 = () => {};' }, { id: 4, type: 'delete', path: 'src/utils.js' }, { id: 5, type: 'error', message: 'Simulated failure - should trigger rollback' } ]; const executeWithRollback = async () => { const rollbackStack = []; try { for (const op of operations) { if (op.type === 'error') { throw new Error(op.message); } switch (op.type) { case 'create': await harness.mockWriteFile(op.path, op.content); rollbackStack.push({ action: 'delete', path: op.path }); break; case 'update': const originalContent = await harness.mockReadFile(op.path); await harness.mockWriteFile(op.path, op.content); rollbackStack.push({ action: 'restore', path: op.path, content: originalContent }); break; case 'delete': const deletedContent = await harness.mockReadFile(op.path); harness.mockFS.delete(op.path); rollbackStack.push({ action: 'restore', path: op.path, content: deletedContent }); break; } fileOperations.push({ ...op, status: 'completed' }); } } catch (error) { console.log('Error occurred, rolling back changes...'); // Execute rollback in reverse order for (const rollback of rollbackStack.reverse()) { switch (rollback.action) { case 'delete': harness.mockFS.delete(rollback.path); break; case 'restore': await harness.mockWriteFile(rollback.path, rollback.content); break; } } throw error; } }; // Execute operations with rollback let errorCaught = false; try { await executeWithRollback(); } catch (error) { errorCaught = true; assert(error.message.includes('Simulated failure')); } assert(errorCaught, 'Error should have been thrown'); // Verify rollback completed successfully assert(!harness.mockFS.has('src/new-file1.js'), 'New file 1 should be rolled back'); assert(!harness.mockFS.has('src/new-file2.js'), 'New file 2 should be rolled back'); // Verify original files restored const indexContent = await harness.mockReadFile('src/index.js'); assert.strictEqual(indexContent, originalFiles.get('src/index.js')); assert(harness.mockFS.has('src/utils.js'), 'Deleted file should be restored'); const utilsContent = await harness.mockReadFile('src/utils.js'); assert.strictEqual(utilsContent, originalFiles.get('src/utils.js')); }); it('should handle partial rollback failures gracefully', async () => { const operations = [ { id: 1, type: 'create', path: 'file1.txt', content: 'content1' }, { id: 2, type: 'create', path: 'file2.txt', content: 'content2' }, { id: 3, type: 'create', path: 'file3.txt', content: 'content3' }, { id: 4, type: 'fail' } ]; const rollbackLog = []; let rollbackWithFailure = false; const executeWithFailingRollback = async () => { const createdFiles = []; try { for (const op of operations) { if (op.type === 'fail') { throw new Error('Operation failed'); } await harness.mockWriteFile(op.path, op.content); createdFiles.push(op.path); } } catch (error) { // Attempt rollback with simulated failure for (let i = createdFiles.length - 1; i >= 0; i--) { const file = createdFiles[i]; try { if (file === 'file2.txt') { // Simulate rollback failure for file2 throw new Error('Rollback failed for file2.txt'); } harness.mockFS.delete(file); rollbackLog.push({ file, status: 'rolled back' }); } catch (rollbackError) { rollbackLog.push({ file, status: 'rollback failed', error: rollbackError.message }); rollbackWithFailure = true; } } throw error; } }; try { await executeWithFailingRollback(); } catch (error) { assert(error.message === 'Operation failed'); } // Verify rollback log assert.strictEqual(rollbackLog.length, 3); assert(rollbackLog.some(entry => entry.status === 'rollback failed')); assert(rollbackWithFailure); // Verify partial rollback state assert(!harness.mockFS.has('file3.txt'), 'file3.txt should be rolled back'); assert(harness.mockFS.has('file2.txt'), 'file2.txt rollback failed, should still exist'); assert(!harness.mockFS.has('file1.txt'), 'file1.txt should be rolled back'); }); it('should implement two-phase commit for distributed operations', async () => { // Simulate distributed system with multiple services const services = { database: { transactions: new Map(), prepared: new Set() }, cache: { transactions: new Map(), prepared: new Set() }, queue: { transactions: new Map(), prepared: new Set() } }; const distributedTransaction = async (operations) => { const transactionId = `tx_${Date.now()}`; const preparedServices = []; try { // Phase 1: Prepare console.log(`\nPhase 1: Preparing transaction ${transactionId}`); for (const op of operations) { const service = services[op.service]; // Simulate prepare phase service.transactions.set(transactionId, { operation: op, state: 'prepared', rollbackData: op.rollbackData }); service.prepared.add(transactionId); preparedServices.push(op.service); console.log(` - ${op.service}: prepared`); // Simulate failure during prepare if (op.shouldFailPrepare) { throw new Error(`${op.service} failed to prepare`); } } // Phase 2: Commit console.log(`\nPhase 2: Committing transaction ${transactionId}`); for (const op of operations) { const service = services[op.service]; const tx = service.transactions.get(transactionId); // Simulate commit phase if (op.shouldFailCommit) { throw new Error(`${op.service} failed to commit`); } tx.state = 'committed'; service.prepared.delete(transactionId); console.log(` - ${op.service}: committed`); } return { transactionId, status: 'committed' }; } catch (error) { console.log(`\nError: ${error.message}`); console.log('Rolling back prepared services...'); // Rollback all prepared services for (const serviceName of preparedServices) { const service = services[serviceName]; if (service.prepared.has(transactionId)) { const tx = service.transactions.get(transactionId); tx.state = 'aborted'; service.prepared.delete(transactionId); console.log(` - ${serviceName}: rolled back`); } } throw error; } }; // Test successful two-phase commit const successfulOps = [ { service: 'database', action: 'insert', data: { id: 1, name: 'test' } }, { service: 'cache', action: 'set', key: 'user:1', value: 'test' }, { service: 'queue', action: 'publish', message: 'user.created' } ]; const successResult = await distributedTransaction(successfulOps); assert(successResult.status === 'committed'); // Test rollback on prepare failure const failPrepareOps = [ { service: 'database', action: 'insert', data: { id: 2 } }, { service: 'cache', action: 'set', key: 'user:2', shouldFailPrepare: true } ]; try { await distributedTransaction(failPrepareOps); assert.fail('Should have thrown error'); } catch (error) { assert(error.message.includes('failed to prepare')); } // Verify rollback const dbTransactions = Array.from(services.database.transactions.values()); const abortedDb = dbTransactions.filter(tx => tx.state === 'aborted'); assert(abortedDb.length > 0); }); }); describe('State Recovery', () => { it('should create and restore from checkpoints', async () => { const stateManager = { checkpoints: new Map(), currentState: { files: new Map(), metadata: { version: 1, lastModified: Date.now() } } }; const createCheckpoint = (name) => { const checkpoint = { name, timestamp: Date.now(), state: { files: new Map(harness.mockFS), metadata: { ...stateManager.currentState.metadata } } }; stateManager.checkpoints.set(name, checkpoint); return checkpoint; }; const restoreCheckpoint = (name) => { const checkpoint = stateManager.checkpoints.get(name); if (!checkpoint) { throw new Error(`Checkpoint ${name} not found`); } // Restore file system state harness.mockFS.clear(); for (const [path, content] of checkpoint.state.files) { harness.mockFS.set(path, content); } // Restore metadata stateManager.currentState.metadata = { ...checkpoint.state.metadata }; return checkpoint; }; // Create initial checkpoint createCheckpoint('initial'); // Make changes await harness.mockWriteFile('src/feature1.js', 'export const feature1 = true;'); await harness.mockWriteFile('src/feature2.js', 'export const feature2 = true;'); createCheckpoint('after-features'); // More changes harness.mockFS.delete('src/utils.js'); await harness.mockWriteFile('src/feature3.js', 'export const feature3 = true;'); createCheckpoint('after-deletion'); // Simulate error and restore try { // Attempt risky operation await harness.mockWriteFile('src/broken.js', 'syntax error {'); throw new Error('Build failed due to syntax error'); } catch (error) { console.log('Error detected, restoring from checkpoint...'); // Restore to last known good state const restored = restoreCheckpoint('after-features'); console.log(`Restored to checkpoint: ${restored.name}`); } // Verify restoration assert(harness.mockFS.has('src/feature1.js')); assert(harness.mockFS.has('src/feature2.js')); assert(!harness.mockFS.has('src/feature3.js')); // Should not exist after restore assert(harness.mockFS.has('src/utils.js')); // Should be restored assert(!harness.mockFS.has('src/broken.js')); // Error file should not exist // Verify checkpoint history assert.strictEqual(stateManager.checkpoints.size, 3); assert(stateManager.checkpoints.has('initial')); assert(stateManager.checkpoints.has('after-features')); assert(stateManager.checkpoints.has('after-deletion')); }); it('should implement incremental rollback', async () => { const changeLog = []; const trackChange = (change) => { changeLog.push({ ...change, timestamp: Date.now(), id: changeLog.length }); }; const applyChange = async (change) => { switch (change.type) { case 'create': await harness.mockWriteFile(change.path, change.content); trackChange({ ...change, oldContent: null }); break; case 'update': const oldContent = await harness.mockReadFile(change.path); await harness.mockWriteFile(change.path, change.content); trackChange({ ...change, oldContent }); break; case 'delete': const deletedContent = await harness.mockReadFile(change.path); harness.mockFS.delete(change.path); trackChange({ ...change, oldContent: deletedContent }); break; } }; const rollbackToChange = async (changeId) => { const targetIndex = changeLog.findIndex(c => c.id === changeId); if (targetIndex === -1) { throw new Error(`Change ${changeId} not found`); } // Rollback changes in reverse order for (let i = changeLog.length - 1; i > targetIndex; i--) { const change = changeLog[i]; switch (change.type) { case 'create': harness.mockFS.delete(change.path); break; case 'update': case 'delete': if (change.oldContent !== null) { await harness.mockWriteFile(change.path, change.oldContent); } break; } } // Remove rolled back changes from log changeLog.splice(targetIndex + 1); }; // Apply series of changes await applyChange({ type: 'create', path: 'v1.txt', content: 'Version 1' }); await applyChange({ type: 'create', path: 'v2.txt', content: 'Version 2' }); await applyChange({ type: 'update', path: 'src/index.js', content: 'Updated index' }); await applyChange({ type: 'create', path: 'v3.txt', content: 'Version 3' }); await applyChange({ type: 'delete', path: 'src/utils.js' }); // Verify all changes applied assert.strictEqual(changeLog.length, 5); assert(harness.mockFS.has('v3.txt')); assert(!harness.mockFS.has('src/utils.js')); // Rollback to change 2 (after v2.txt creation) await rollbackToChange(1); // Verify incremental rollback assert(harness.mockFS.has('v1.txt')); assert(harness.mockFS.has('v2.txt')); assert(!harness.mockFS.has('v3.txt')); // Should be rolled back assert(harness.mockFS.has('src/utils.js')); // Should be restored const indexContent = await harness.mockReadFile('src/index.js'); assert(indexContent.includes('console.log')); // Original content assert.strictEqual(changeLog.length, 2); // Only first 2 changes remain }); }); describe('Compensation Patterns', () => { it('should implement saga pattern for complex workflows', async () => { const saga = { steps: [], compensations: [], completed: [], failed: null }; const executeSaga = async (workflow) => { saga.steps = workflow; try { for (let i = 0; i < workflow.length; i++) { const step = workflow[i]; console.log(`Executing step ${i + 1}: ${step.name}`); // Execute step const result = await step.execute(); saga.completed.push({ step: step.name, result }); // Register compensation if (step.compensate) { saga.compensations.unshift({ step: step.name, compensate: step.compensate, data: result }); } // Check for failure if (step.shouldFail) { throw new Error(`Step ${step.name} failed`); } } return { status: 'completed', completed: saga.completed }; } catch (error) { console.log(`\nSaga failed at step: ${error.message}`); console.log('Running compensations...'); saga.failed = error; // Run compensations in reverse order for (const compensation of saga.compensations) { try { console.log(` Compensating: ${compensation.step}`); await compensation.compensate(compensation.data); } catch (compError) { console.log(` Warning: Compensation failed for ${compensation.step}: ${compError.message}`); } } throw error; } }; // Define a complex workflow const orderWorkflow = [ { name: 'reserve-inventory', execute: async () => { await harness.mockWriteFile('inventory-lock.json', JSON.stringify({ orderId: 'order-123', items: ['item1', 'item2'], locked: true })); return { lockId: 'lock-123' }; }, compensate: async (data) => { harness.mockFS.delete('inventory-lock.json'); } }, { name: 'charge-payment', execute: async () => { await harness.mockWriteFile('payment-record.json', JSON.stringify({ orderId: 'order-123', amount: 100, status: 'charged' })); return { paymentId: 'payment-123' }; }, compensate: async (data) => { await harness.mockWriteFile('payment-record.json', JSON.stringify({ orderId: 'order-123', amount: 100, status: 'refunded', refundId: 'refund-123' })); } }, { name: 'create-shipment', execute: async () => { await harness.mockWriteFile('shipment.json', JSON.stringify({ orderId: 'order-123', status: 'pending' })); return { shipmentId: 'ship-123' }; }, compensate: async (data) => { harness.mockFS.delete('shipment.json'); }, shouldFail: true // Simulate failure at this step }, { name: 'send-confirmation', execute: async () => { await harness.mockWriteFile('confirmation.json', JSON.stringify({ orderId: 'order-123', sent: true })); return { confirmationId: 'conf-123' }; } } ]; // Execute saga with expected failure try { await executeSaga(orderWorkflow); assert.fail('Saga should have failed'); } catch (error) { assert(error.message.includes('create-shipment failed')); } // Verify compensations were executed assert(!harness.mockFS.has('inventory-lock.json'), 'Inventory should be released'); assert(harness.mockFS.has('payment-record.json'), 'Payment record should exist'); const paymentRecord = JSON.parse(await harness.mockReadFile('payment-record.json')); assert.strictEqual(paymentRecord.status, 'refunded', 'Payment should be refunded'); assert(!harness.mockFS.has('shipment.json'), 'Shipment should not exist'); assert(!harness.mockFS.has('confirmation.json'), 'Confirmation should not be sent'); // Verify saga state assert.strictEqual(saga.completed.length, 2); // Only first 2 steps completed assert.strictEqual(saga.compensations.length, 2); // 2 compensations registered }); it('should handle compensation failures with fallback strategies', async () => { const compensationLog = []; const executeWithFallback = async (operations) => { const executed = []; try { for (const op of operations) { const result = await op.execute(); executed.push({ operation: op.name, result }); } // Simulate failure after all operations throw new Error('System failure after operations'); } catch (error) { console.log('Attempting compensations with fallback strategies...'); for (const op of operations.reverse()) { if (!op.compensate) continue; try { // Try primary compensation await op.compensate(); compensationLog.push({ operation: op.name, status: 'compensated', method: 'primary' }); } catch (compError) { // Try fallback compensation if (op.fallbackCompensate) { try { await op.fallbackCompensate(); compensationLog.push({ operation: op.name, status: 'compensated', method: 'fallback' }); } catch (fallbackError) { compensationLog.push({ operation: op.name, status: 'failed', error: fallbackError.message }); } } else { compensationLog.push({ operation: op.name, status: 'failed', error: compError.message }); } } } throw error; } }; const operations = [ { name: 'operation1', execute: async () => { await harness.mockWriteFile('op1.txt', 'Operation 1 data'); return { id: 1 }; }, compensate: async () => { harness.mockFS.delete('op1.txt'); } }, { name: 'operation2', execute: async () => { await harness.mockWriteFile('op2.txt', 'Operation 2 data'); return { id: 2 }; }, compensate: async () => { // Simulate compensation failure throw new Error('Primary compensation failed'); }, fallbackCompensate: async () => { // Fallback: Mark for manual cleanup await harness.mockWriteFile('cleanup-required.txt', 'Manual cleanup needed for op2.txt'); } }, { name: 'operation3', execute: async () => { await harness.mockWriteFile('op3.txt', 'Operation 3 data'); return { id: 3 }; }, compensate: async () => { harness.mockFS.delete('op3.txt'); } } ]; try { await executeWithFallback(operations); } catch (error) { assert(error.message.includes('System failure')); } // Verify compensation log assert.strictEqual(compensationLog.length, 3); assert(compensationLog.some(log => log.method === 'fallback')); assert(harness.mockFS.has('cleanup-required.txt'), 'Fallback should create cleanup marker'); // Verify compensations assert(!harness.mockFS.has('op1.txt'), 'Operation 1 should be compensated'); assert(harness.mockFS.has('op2.txt'), 'Operation 2 compensation failed, file remains'); assert(!harness.mockFS.has('op3.txt'), 'Operation 3 should be compensated'); }); }); });