UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

305 lines 18.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Watcher = void 0; const chokidar_1 = __importDefault(require("chokidar")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); const promises_1 = __importDefault(require("fs/promises")); const events_1 = require("events"); const testGenerator_1 = require("./testGenerator"); const testRunner_1 = require("./testRunner"); const checklistGenerator_1 = require("./checklistGenerator"); const diffUtils_1 = require("../utils/diffUtils"); const securityRiskUtils_1 = require("../utils/securityRiskUtils"); /** * File watcher for auto-running tests and security analysis * Watches source files for changes and automatically generates tests, * runs those tests, and performs security analysis. */ class Watcher extends events_1.EventEmitter { /** * Create a new watcher instance * @param options Configuration options */ constructor(options) { super(); this.watcher = null; this.isProcessing = false; this.fileContents = new Map(); this.securityIssues = new Map(); this.options = { debounceMs: options.debounceMs || 1000, ignorePatterns: options.ignorePatterns || ['node_modules', 'dist', 'build', '*.test.*'], runTests: options.runTests ?? true, generateChecklists: options.generateChecklists ?? true, showDiffs: options.showDiffs ?? true, browser: options.browser || 'chromium', headless: options.headless ?? true, workers: options.workers || 1 }; this.testGenerator = new testGenerator_1.TestGenerator({ format: 'playwright', timeout: 60 }); this.testRunner = new testRunner_1.TestRunner({ browser: this.options.browser, headless: this.options.headless, timeout: 60, reporter: 'list', workers: this.options.workers }); this.checklistGenerator = new checklistGenerator_1.ChecklistGenerator(); // Setup test runner events for real-time feedback this.testRunner.on('test:end', (result) => { this.emit('test:end', result); }); this.testRunner.on('error', (error) => { this.emit('error', error); }); } /** * Watch source files and generate tests on changes * @param sourcePath Source path to watch * @param outputDir Output directory for generated tests */ async watch(sourcePath, outputDir) { // Initialize watcher this.watcher = chokidar_1.default.watch(sourcePath, { ignored: this.options.ignorePatterns, ignoreInitial: false, persistent: true }); // Setup event handlers this.watcher .on('add', (filePath) => this.handleFileChange('add', filePath, outputDir)) .on('change', (filePath) => this.handleFileChange('change', filePath, outputDir)) .on('unlink', (filePath) => this.handleFileChange('unlink', filePath, outputDir)) .on('ready', () => { console.log(chalk_1.default.green('Initial scan complete. Watching for changes...')); }) .on('error', (error) => { console.error(chalk_1.default.red(`Watcher error: ${error}`)); }); } /** * Stop watching files and clean up resources */ stop() { if (this.watcher) { this.watcher.close(); this.watcher = null; } } /** * Handle file change events * @param event File event type (add, change, unlink) * @param filePath Path to the changed file * @param outputDir Output directory for generated tests */ handleFileChange(event, filePath, outputDir) { // Skip non-source files if (!this.isSourceFile(filePath)) { return; } console.log(chalk_1.default.blue(`${event}: ${filePath}`)); // Debounce multiple changes setTimeout(async () => { if (this.isProcessing) { return; } this.isProcessing = true; this.emit('processing:start', { filePath, event }); try { if (event === 'add' || event === 'change') { // Store file content for diff analysis let previousContent = null; if (this.options.showDiffs && event === 'change') { previousContent = this.fileContents.get(filePath) || null; } // Read and store new content const newContent = await promises_1.default.readFile(filePath, 'utf8'); this.fileContents.set(filePath, newContent); // Show diff if enabled and we have previous content if (this.options.showDiffs && previousContent !== null) { const changes = (0, diffUtils_1.diff)(previousContent, newContent); if (changes.length > 1) { // More than just the unchanged content console.log(chalk_1.default.yellow('Code changes:')); changes.forEach((change) => { if (change.added) { console.log(chalk_1.default.green(`+ ${change.value}`)); } else if (change.removed) { console.log(chalk_1.default.red(`- ${change.value}`)); } }); this.emit('diff', { filePath, changes }); } } // Generate tests console.log(chalk_1.default.yellow(`Generating tests for: ${filePath}`)); try { const result = await this.testGenerator.generateTests(filePath, outputDir); if (result.testCount > 0) { console.log(chalk_1.default.green(`✓ Generated ${result.testCount} tests for ${filePath}`)); console.log(chalk_1.default.gray(` Output: ${result.files.join(', ')}`)); this.emit('tests:generated', { filePath, testFiles: result.files, testCount: result.testCount }); // Run tests if enabled if (this.options.runTests && result.files.length > 0) { console.log(chalk_1.default.yellow(`Running tests for: ${filePath}`)); try { const testResults = await this.testRunner.runTests(result.files[0]); this.emit('tests:run', { filePath, results: testResults }); // Report test results if (testResults.passed === testResults.total) { console.log(chalk_1.default.green(`✓ All tests passed (${testResults.passed}/${testResults.total})`)); } else { console.log(chalk_1.default.red(`✗ Tests failed: ${testResults.failed}/${testResults.total}`)); // Log failed tests if (testResults.errors && testResults.errors.length > 0) { console.log(chalk_1.default.red('Failures:')); testResults.errors.forEach(error => { console.log(chalk_1.default.red(` - ${error.message}`)); }); } } } catch (testError) { console.error(chalk_1.default.red(`Error running tests: ${testError.message}`)); } } // Generate security and QA checklist if enabled if (this.options.generateChecklists) { console.log(chalk_1.default.yellow(`Generating security checklist for: ${filePath}`)); try { const checklistResult = await this.checklistGenerator.generateChecklist(filePath, path_1.default.join(outputDir, '../checklists')); if (checklistResult.itemCount > 0) { console.log(chalk_1.default.green(`✓ Generated checklist with ${checklistResult.itemCount} items`)); console.log(chalk_1.default.gray(` Output: ${checklistResult.file}`)); const securityItems = checklistResult.items.filter(item => item.category === 'Security' && (item.status === 'failed')); const previousIssues = this.securityIssues.get(filePath) || []; const currentIssues = securityItems.map(item => item.description); this.securityIssues.set(filePath, currentIssues); // Find new security issues const newIssues = currentIssues.filter(issue => !previousIssues.includes(issue)); if (newIssues.length > 0) { console.log(chalk_1.default.red(`⚠️ ${newIssues.length} new security issues detected`)); // Get full security items with severity and risk scores const newSecurityItems = securityItems.filter(item => item.description && newIssues.includes(item.description)); // Display detailed security risk information if (newSecurityItems.length > 0) { // Count by severity const criticalCount = newSecurityItems.filter(item => item.severity === 'critical').length; const highCount = newSecurityItems.filter(item => item.severity === 'high').length; const mediumCount = newSecurityItems.filter(item => item.severity === 'medium').length; const lowCount = newSecurityItems.filter(item => item.severity === 'low').length; // Show security severity breakdown if (criticalCount > 0) console.log(` ${(0, securityRiskUtils_1.formatSeverityBadge)('critical')} ${criticalCount}`); if (highCount > 0) console.log(` ${(0, securityRiskUtils_1.formatSeverityBadge)('high')} ${highCount}`); if (mediumCount > 0) console.log(` ${(0, securityRiskUtils_1.formatSeverityBadge)('medium')} ${mediumCount}`); if (lowCount > 0) console.log(` ${(0, securityRiskUtils_1.formatSeverityBadge)('low')} ${lowCount}`); // Generate concise risk report for detected issues newSecurityItems.forEach(item => { const score = item.riskScore?.score ? chalk_1.default.yellow(item.riskScore.score.toFixed(1)) : ''; const severity = (0, securityRiskUtils_1.formatSeverityBadge)(item.severity || 'medium'); console.log(chalk_1.default.red(` - ${severity} ${item.title || 'Security Issue'} ${score}`)); console.log(chalk_1.default.gray(` ${item.description}`)); if (item.remediation?.description) { console.log(chalk_1.default.green(` ✓ Remediation: ${item.remediation.description}`)); if (item.remediation.codeExample) { console.log(chalk_1.default.blue(` Example: ${item.remediation.codeExample}`)); } } // Show security references if (item.references && item.references.length > 0) { const ref = item.references[0]; const refText = []; if (ref.cwe) refText.push(ref.cwe); if (ref.owasp) refText.push(ref.owasp); if (refText.length > 0) { console.log(chalk_1.default.gray(` References: ${refText.join(', ')}`)); } } console.log(); // Add spacing between issues }); } else { // Fallback to simple issue list if no detailed items newIssues.forEach((issue) => { console.log(chalk_1.default.red(` - ${issue}`)); }); } this.emit('security:new-issues', { filePath, issues: newIssues }); } // Find resolved security issues const resolvedIssues = previousIssues.filter((issue) => !currentIssues.includes(issue)); if (resolvedIssues.length > 0) { console.log(chalk_1.default.green(`✓ Security issues resolved:`)); resolvedIssues.forEach((issue) => { console.log(chalk_1.default.green(` - ${issue}`)); }); this.emit('security:resolved-issues', { filePath, issues: resolvedIssues }); } } else { console.log(chalk_1.default.gray(`No checklist items generated for ${filePath}`)); } } catch (checklistError) { console.error(chalk_1.default.red(`Error generating checklist: ${checklistError.message}`)); } } } else { console.log(chalk_1.default.gray(`No tests generated for ${filePath}`)); } } catch (genError) { console.error(chalk_1.default.red(`Error generating tests: ${genError.message}`)); } } } catch (error) { console.error(chalk_1.default.red(`Error processing ${filePath}: ${error.message}`)); this.emit('error', { filePath, error }); } finally { this.isProcessing = false; this.emit('processing:end', { filePath }); } }, this.options.debounceMs); } /** * Check if file is a source file that should trigger test generation * @param filePath Path to file to check * @returns True if the file is a source file that should trigger test generation */ isSourceFile(filePath) { const ext = path_1.default.extname(filePath).toLowerCase(); const validExtensions = ['.js', '.jsx', '.ts', '.tsx']; if (!validExtensions.includes(ext)) { return false; } const fileName = path_1.default.basename(filePath).toLowerCase(); // Skip test files if (fileName.includes('.test.') || fileName.includes('.spec.')) { return false; } return true; } } exports.Watcher = Watcher; //# sourceMappingURL=watcher.js.map