UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

619 lines (528 loc) 15.7 kB
const fs = require('fs').promises; const path = require('path'); let execa; try { execa = require('execa').execa; } catch { execa = null; } let chalk; try { chalk = require('chalk'); } catch { chalk = { blue: s => s, green: s => s, red: s => s, yellow: s => s }; } let tmp; try { tmp = require('tmp-promise'); } catch { tmp = null; } /** * SandboxTester - Test improvements in isolated environment * * Refactored to use execa for cross-platform compatibility */ class SandboxTester { constructor(options = {}) { this.verbose = options.verbose || false; this.keepSandbox = options.keepSandbox || false; this.sandboxPath = null; } /** * Create isolated sandbox environment */ async createSandbox() { try { if (!tmp) { throw new Error('tmp-promise module not available - install with: npm install tmp-promise'); } const tmpDir = await tmp.dir({ unsafeCleanup: !this.keepSandbox }); this.sandboxPath = tmpDir.path; if (this.verbose) { console.log(chalk.blue(`Sandbox created: ${this.sandboxPath}`)); } return this.sandboxPath; } catch (error) { throw new Error(`Failed to create sandbox: ${error.message}`); } } /** * Copy project files to sandbox */ async copyProject(sourcePath, options = {}) { const { exclude = ['node_modules', '.git', 'dist', 'build'] } = options; try { if (!this.sandboxPath) { await this.createSandbox(); } // Get list of files to copy const files = await this.getProjectFiles(sourcePath, exclude); // Copy files maintaining structure for (const file of files) { const relativePath = path.relative(sourcePath, file); const destPath = path.join(this.sandboxPath, relativePath); await fs.mkdir(path.dirname(destPath), { recursive: true }); await fs.copyFile(file, destPath); } if (this.verbose) { console.log(chalk.green(`Copied ${files.length} files to sandbox`)); } return files.length; } catch (error) { throw new Error(`Failed to copy project: ${error.message}`); } } /** * Get list of project files excluding specified patterns */ async getProjectFiles(rootPath, exclude = []) { const files = []; async function walk(dir) { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); const relativePath = path.relative(rootPath, fullPath); // Check if excluded const isExcluded = exclude.some(pattern => relativePath.includes(pattern) || entry.name === pattern, ); if (isExcluded) continue; if (entry.isDirectory()) { await walk(fullPath); } else { files.push(fullPath); } } } await walk(rootPath); return files; } /** * Apply improvement to sandbox */ async applyImprovement(improvement) { try { if (!this.sandboxPath) { throw new Error('Sandbox not initialized'); } const { files, changes } = improvement; // Apply file changes for (const change of changes) { const filePath = path.join(this.sandboxPath, change.file); if (change.type === 'create') { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, change.content); } else if (change.type === 'modify') { await fs.writeFile(filePath, change.content); } else if (change.type === 'delete') { await fs.unlink(filePath); } } if (this.verbose) { console.log(chalk.green(`Applied ${changes.length} changes to sandbox`)); } return true; } catch (error) { throw new Error(`Failed to apply improvement: ${error.message}`); } } /** * Run tests in sandbox */ async runTests(options = {}) { const { testCommand = 'npm test', timeout = 60000, } = options; try { if (!this.sandboxPath) { throw new Error('Sandbox not initialized'); } if (this.verbose) { console.log(chalk.blue('Running tests in sandbox...')); } const output = await this.runCommand(this.sandboxPath, testCommand, { timeout }); return { success: true, output, }; } catch (error) { return { success: false, error: error.message, stdout: error.stdout || '', stderr: error.stderr || '', }; } } /** * Run lint checks in sandbox */ async runLint(options = {}) { const { lintCommand = 'npm run lint', timeout = 30000, } = options; try { if (!this.sandboxPath) { throw new Error('Sandbox not initialized'); } if (this.verbose) { console.log(chalk.blue('Running lint in sandbox...')); } const output = await this.runCommand(this.sandboxPath, lintCommand, { timeout }); return { success: true, output, }; } catch (error) { return { success: false, error: error.message, stdout: error.stdout || '', stderr: error.stderr || '', }; } } /** * Validate improvement before applying */ async validateImprovement(improvement) { const validation = { success: true, errors: [], warnings: [], checks: { syntax: { passed: false, errors: [] }, structure: { passed: false, errors: [] }, dependencies: { passed: false, errors: [] }, }, }; try { const { files, changes, metadata = {} } = improvement; // Check required fields if (!changes || !Array.isArray(changes)) { validation.success = false; validation.errors.push('Missing or invalid changes array'); return validation; } // Check affected files exist in sandbox const plan = metadata.plan || { affectedFiles: [] }; // Syntax validation for (const file of plan.affectedFiles) { const filePath = path.join(this.sandboxPath, file); try { await execa('node', ['--check', filePath], { encoding: 'utf8', }); validation.checks.syntax.passed = true; } catch (error) { validation.success = false; validation.checks.syntax.errors.push({ file, error: error.message, }); } } // Structure validation const requiredDirs = ['src', 'tests']; for (const dir of requiredDirs) { const dirPath = path.join(this.sandboxPath, dir); try { await fs.access(dirPath); validation.checks.structure.passed = true; } catch (error) { validation.warnings.push(`Missing directory: ${dir}`); } } // Dependencies validation const packageJsonPath = path.join(this.sandboxPath, 'package.json'); try { await fs.access(packageJsonPath); const content = await fs.readFile(packageJsonPath, 'utf-8'); const pkg = JSON.parse(content); if (pkg.dependencies || pkg.devDependencies) { validation.checks.dependencies.passed = true; } } catch (error) { validation.warnings.push('Could not validate dependencies'); } return validation; } catch (error) { validation.success = false; validation.errors.push(`Validation error: ${error.message}`); return validation; } } /** * Compare sandbox results with original */ async compareResults(originalPath) { try { if (!this.sandboxPath) { throw new Error('Sandbox not initialized'); } const comparison = { filesChanged: [], filesAdded: [], filesRemoved: [], metrics: { original: {}, sandbox: {}, }, }; // Get file lists const originalFiles = await this.getProjectFiles(originalPath); const sandboxFiles = await this.getProjectFiles(this.sandboxPath); const originalSet = new Set(originalFiles.map(f => path.relative(originalPath, f), )); const sandboxSet = new Set(sandboxFiles.map(f => path.relative(this.sandboxPath, f), )); // Find changes for (const file of sandboxSet) { if (!originalSet.has(file)) { comparison.filesAdded.push(file); } else { const originalContent = await fs.readFile( path.join(originalPath, file), 'utf-8', ); const sandboxContent = await fs.readFile( path.join(this.sandboxPath, file), 'utf-8', ); if (originalContent !== sandboxContent) { comparison.filesChanged.push(file); } } } for (const file of originalSet) { if (!sandboxSet.has(file)) { comparison.filesRemoved.push(file); } } // Collect metrics comparison.metrics.original = { fileCount: originalFiles.length, }; comparison.metrics.sandbox = { fileCount: sandboxFiles.length, }; if (this.verbose) { console.log(chalk.blue('Comparison results:')); console.log(` Changed: ${comparison.filesChanged.length}`); console.log(` Added: ${comparison.filesAdded.length}`); console.log(` Removed: ${comparison.filesRemoved.length}`); } return comparison; } catch (error) { throw new Error(`Failed to compare results: ${error.message}`); } } /** * Run full improvement test cycle */ async testImprovement(improvement, options = {}) { const { runTests = true, runLint = true, compareWithOriginal = false, originalPath = null, } = options; const results = { success: true, validation: null, tests: null, lint: null, comparison: null, errors: [], }; try { // Validate improvement results.validation = await this.validateImprovement(improvement); if (!results.validation.success) { results.success = false; results.errors.push('Validation failed'); } // Apply improvement await this.applyImprovement(improvement); // Run tests if (runTests) { results.tests = await this.runTests(); if (!results.tests.success) { results.success = false; results.errors.push('Tests failed'); } } // Run lint if (runLint) { results.lint = await this.runLint(); if (!results.lint.success) { results.success = false; results.errors.push('Lint failed'); } } // Compare with original if (compareWithOriginal && originalPath) { results.comparison = await this.compareResults(originalPath); } return results; } catch (error) { results.success = false; results.errors.push(error.message); return results; } } /** * Execute command in sandbox with timeout * Refactored to use execa for cross-platform compatibility */ async runCommand(cwd, command, options = {}) { const timeout = options.timeout || 30000; try { // Split command into program and arguments const parts = command.split(' '); const program = parts[0]; const args = parts.slice(1); const { stdout } = await execa(program, args, { cwd, shell: true, timeout, encoding: 'utf8', env: { ...process.env, CI: 'true', NODE_ENV: 'test' }, all: true, reject: false, }); if (this.verbose && stdout) { process.stdout.write(stdout); } return stdout || ''; } catch (error) { // Handle timeout if (error.timedOut) { throw new Error(`Command timed out after ${timeout}ms`); } // Handle command failure if (error.exitCode !== undefined && error.exitCode !== 0) { const cmdError = new Error(`Command failed with code ${error.exitCode}`); cmdError.stdout = error.stdout || ''; cmdError.stderr = error.stderr || ''; throw cmdError; } // Handle other errors throw error; } } /** * Clean up sandbox */ async cleanup() { try { if (this.sandboxPath && !this.keepSandbox) { await fs.rm(this.sandboxPath, { recursive: true, force: true }); if (this.verbose) { console.log(chalk.blue('Sandbox cleaned up')); } } this.sandboxPath = null; } catch (error) { console.error(chalk.red(`Failed to cleanup sandbox: ${error.message}`)); } } /** * Get sandbox statistics */ async getStats() { try { if (!this.sandboxPath) { throw new Error('Sandbox not initialized'); } const files = await this.getProjectFiles(this.sandboxPath); let totalSize = 0; const fileTypes = {}; for (const file of files) { const stats = await fs.stat(file); totalSize += stats.size; const ext = path.extname(file) || 'no-extension'; fileTypes[ext] = (fileTypes[ext] || 0) + 1; } return { path: this.sandboxPath, fileCount: files.length, totalSize, fileTypes, sizeFormatted: this.formatBytes(totalSize), }; } catch (error) { throw new Error(`Failed to get stats: ${error.message}`); } } /** * Format bytes to human readable */ formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Take snapshot of sandbox state */ async snapshot() { try { if (!this.sandboxPath) { throw new Error('Sandbox not initialized'); } const stats = await this.getStats(); const files = await this.getProjectFiles(this.sandboxPath); const fileContents = {}; for (const file of files) { const relativePath = path.relative(this.sandboxPath, file); fileContents[relativePath] = await fs.readFile(file, 'utf-8'); } return { timestamp: new Date().toISOString(), stats, files: fileContents, }; } catch (error) { throw new Error(`Failed to create snapshot: ${error.message}`); } } /** * Restore sandbox from snapshot */ async restore(snapshot) { try { if (!this.sandboxPath) { await this.createSandbox(); } // Clear sandbox const entries = await fs.readdir(this.sandboxPath); for (const entry of entries) { await fs.rm(path.join(this.sandboxPath, entry), { recursive: true, force: true, }); } // Restore files for (const [relativePath, content] of Object.entries(snapshot.files)) { const filePath = path.join(this.sandboxPath, relativePath); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, content); } if (this.verbose) { console.log(chalk.green(`Restored ${Object.keys(snapshot.files).length} files`)); } return true; } catch (error) { throw new Error(`Failed to restore snapshot: ${error.message}`); } } } module.exports = SandboxTester;