UNPKG

@ooples/token-optimizer-mcp

Version:

Intelligent context window optimization for Claude Code - store content externally via caching and compression, freeing up your context window for what matters

443 lines โ€ข 16.3 kB
/** * Smart Test Tool - 80% Token Reduction * * Wraps Jest to provide: * - Incremental test runs (only affected tests) * - Cached test results * - Failure summarization (not full logs) * - Coverage delta tracking */ import { spawn } from 'child_process'; import { CacheEngine } from '../../core/cache-engine.js'; import { TokenCounter } from '../../core/token-counter.js'; import { MetricsCollector } from '../../core/metrics.js'; import { createHash } from 'crypto'; import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; export class SmartTest { cache; cacheNamespace = 'smart_test'; projectRoot; constructor(cache, _tokenCounter, _metrics, projectRoot) { this.cache = cache; this.projectRoot = projectRoot || process.cwd(); } /** * Run tests with smart caching and output reduction */ async run(options = {}) { const { pattern, onlyChanged = false, force = false, coverage = false, watch = false, maxCacheAge = 3600, } = options; // Generate cache key based on test files and their content const cacheKey = await this.generateCacheKey(pattern); // Check cache first (unless force or watch mode) if (!force && !watch) { const cached = this.getCachedResult(cacheKey, maxCacheAge); if (cached) { return this.formatCachedOutput(cached); } } // Run Jest const result = await this.runJest({ pattern, onlyChanged, coverage, watch, }); // Cache the result if (!watch) { this.cacheResult(cacheKey, result); } // Transform to smart output return this.transformOutput(result); } /** * Run Jest and capture results */ async runJest(options) { const args = ['--json']; if (options.pattern) { // Convert Windows backslashes to forward slashes let normalizedPattern = options.pattern.replace(/\\/g, '/'); // Escape regex special characters for Jest pattern matching // Preserve wildcards (*) as .* for regex, but escape other special chars normalizedPattern = normalizedPattern .replace(/\./g, '\\.') // Escape dots .replace(/\+/g, '\\+') // Escape plus .replace(/\?/g, '\\?') // Escape question mark .replace(/\[/g, '\\[') // Escape square brackets .replace(/\]/g, '\\]') .replace(/\(/g, '\\(') // Escape parentheses .replace(/\)/g, '\\)') .replace(/\{/g, '\\{') // Escape curly braces .replace(/\}/g, '\\}') .replace(/\^/g, '\\^') // Escape caret .replace(/\$/g, '\\$') // Escape dollar .replace(/\|/g, '\\|') // Escape pipe .replace(/\*/g, '.*'); // Convert wildcard * to .* for regex args.push('--testPathPattern=' + normalizedPattern); } if (options.onlyChanged) { args.push('--onlyChanged'); } if (options.coverage) { args.push('--coverage', '--coverageReporters=json-summary'); } if (options.watch) { args.push('--watch'); } args.push('--no-colors'); return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; const jest = spawn('npm', ['run', 'test', '--', ...args], { cwd: this.projectRoot, shell: true, }); jest.stdout.on('data', (data) => { stdout += data.toString(); }); jest.stderr.on('data', (data) => { stderr += data.toString(); }); jest.on('close', (_code) => { try { // Jest writes JSON to stdout even on failure const jsonMatch = stdout.match(/\{[\s\S]*\}/); if (jsonMatch) { const result = JSON.parse(jsonMatch[0]); resolve(result); } else { reject(new Error(`Failed to parse Jest output: ${stderr || stdout}`)); } } catch (err) { reject(new Error(`Failed to parse Jest output: ${err instanceof Error ? err.message : String(err)}`)); } }); jest.on('error', (err) => { reject(err); }); }); } /** * Generate cache key based on test file contents */ async generateCacheKey(pattern) { const hash = createHash('sha256'); hash.update(this.cacheNamespace); hash.update(pattern || 'all'); // Hash package.json to detect dependency changes const packageJsonPath = join(this.projectRoot, 'package.json'); if (existsSync(packageJsonPath)) { const packageJson = readFileSync(packageJsonPath, 'utf-8'); hash.update(packageJson); } // Hash jest config to detect config changes const jestConfigPath = join(this.projectRoot, 'jest.config.js'); if (existsSync(jestConfigPath)) { const jestConfig = readFileSync(jestConfigPath, 'utf-8'); hash.update(jestConfig); } return `${this.cacheNamespace}:${hash.digest('hex')}`; } /** * Get cached result if available and fresh */ getCachedResult(key, maxAge) { const cached = this.cache.get(key); if (!cached) { return null; } try { const result = JSON.parse(cached); const age = (Date.now() - result.cachedAt) / 1000; if (age <= maxAge) { return result; } } catch (err) { // Invalid cache entry return null; } return null; } /** * Cache test result */ cacheResult(key, result) { const toCache = { ...result, cachedAt: Date.now(), }; const dataToCache = JSON.stringify(toCache); const originalSize = JSON.stringify(result).length; const compactSize = this.estimateCompactSize(result); this.cache.set(key, dataToCache, originalSize, compactSize); } /** * Transform full Jest output to smart output */ transformOutput(result, fromCache = false) { const failures = this.extractFailures(result); const newTests = this.detectNewTests(result); const coverageDelta = this.calculateCoverageDelta(result); const originalSize = JSON.stringify(result).length; const compactSize = this.estimateCompactSize(result); return { summary: { total: result.numTotalTests, passed: result.numPassedTests, failed: result.numFailedTests, skipped: result.numPendingTests, duration: result.endTime - result.startTime, fromCache, }, failures, coverageDelta, newTests, metrics: { originalTokens: Math.ceil(originalSize / 4), compactedTokens: Math.ceil(compactSize / 4), reductionPercentage: Math.round(((originalSize - compactSize) / originalSize) * 100), }, }; } /** * Format cached output */ formatCachedOutput(result) { return this.transformOutput(result, true); } /** * Extract only failures with concise error messages */ extractFailures(result) { const failures = []; for (const testFile of result.testResults || []) { if (testFile.status === 'failed') { for (const assertion of testFile.assertionResults || []) { if (assertion.status === 'failed') { // Extract concise error message const error = this.extractConciseError(assertion.failureMessages); failures.push({ testFile: testFile.name, testName: assertion.title, error, location: this.extractErrorLocation(assertion.failureMessages), }); } } } } return failures; } /** * Extract concise error message from Jest failure */ extractConciseError(messages) { if (!messages || messages.length === 0) { return 'Unknown error'; } // Join all messages const fullMessage = messages.join('\n'); // Extract the most important lines const lines = fullMessage.split('\n'); const importantLines = lines.filter((line) => { // Keep expect() lines, received/expected, and error messages return (line.includes('expect') || line.includes('Received:') || line.includes('Expected:') || line.includes('Error:') || line.includes('at ')); }); // Limit to first 5 important lines return (importantLines.slice(0, 5).join('\n').trim() || fullMessage.slice(0, 200)); } /** * Extract error location from stack trace */ extractErrorLocation(messages) { const fullMessage = messages.join('\n'); const lines = fullMessage.split('\n'); for (const line of lines) { if (line.trim().startsWith('at ') && !line.includes('node_modules')) { // Extract file:line:column const match = line.match(/\(([^)]+):(\d+):(\d+)\)/); if (match) { return `${match[1]}:${match[2]}:${match[3]}`; } } } return undefined; } /** * Detect new tests (simplified version - would need test history) */ detectNewTests(_result) { // In a real implementation, we'd compare with previous run // For now, return empty array return []; } /** * Calculate coverage delta (simplified version - would need previous coverage) */ calculateCoverageDelta(result) { // Guard against missing coverage data if (!result.coverageMap || !result.coverageMap.total) { return undefined; } const total = result.coverageMap.total; // Verify all coverage metrics exist if (!total.statements || !total.branches || !total.functions || !total.lines) { return undefined; } // In a real implementation, we'd compare with previous run // For now, return current coverage as delta return { statements: total.statements.pct, branches: total.branches.pct, functions: total.functions.pct, lines: total.lines.pct, }; } /** * Estimate compact output size for token calculation */ estimateCompactSize(result) { // Count only summary and failures, not full test results const summary = { total: result.numTotalTests, passed: result.numPassedTests, failed: result.numFailedTests, skipped: result.numPendingTests, }; const failures = this.extractFailures(result); return JSON.stringify({ summary, failures }).length; } /** * Close cache connection */ close() { this.cache.close(); } } /** * Factory function for creating SmartTest with shared resources (benchmark usage) */ export function getSmartTestTool(cache, tokenCounter, metrics, projectRoot) { return new SmartTest(cache, tokenCounter, metrics, projectRoot); } /** * CLI-friendly function for running smart tests */ export async function runSmartTest(options = {}) { // Create standalone resources for CLI usage const cache = new CacheEngine(join(homedir(), '.token-optimizer-cache', 'cache.db')); const tokenCounter = new TokenCounter(); const metrics = new MetricsCollector(); const smartTest = new SmartTest(cache, tokenCounter, metrics, options.projectRoot); try { const result = await smartTest.run(options); // Format as human-readable output let output = `\n๐Ÿงช Smart Test Results ${result.summary.fromCache ? '(cached)' : ''}\n`; output += `${'='.repeat(50)}\n\n`; // Summary output += `Summary:\n`; output += ` Total: ${result.summary.total}\n`; output += ` โœ“ Passed: ${result.summary.passed}\n`; output += ` โœ— Failed: ${result.summary.failed}\n`; output += ` โŠ˜ Skipped: ${result.summary.skipped}\n`; output += ` Duration: ${(result.summary.duration / 1000).toFixed(2)}s\n\n`; // Failures if (result.failures.length > 0) { output += `Failures:\n`; for (const failure of result.failures) { output += `\n โœ— ${failure.testName}\n`; output += ` File: ${failure.testFile}\n`; if (failure.location) { output += ` Location: ${failure.location}\n`; } output += ` Error:\n`; const errorLines = failure.error.split('\n'); for (const line of errorLines) { output += ` ${line}\n`; } } output += '\n'; } // Coverage delta if (result.coverageDelta) { output += `Coverage:\n`; output += ` Statements: ${result.coverageDelta.statements.toFixed(2)}%\n`; output += ` Branches: ${result.coverageDelta.branches.toFixed(2)}%\n`; output += ` Functions: ${result.coverageDelta.functions.toFixed(2)}%\n`; output += ` Lines: ${result.coverageDelta.lines.toFixed(2)}%\n\n`; } // New tests if (result.newTests.length > 0) { output += `New Tests:\n`; for (const test of result.newTests) { output += ` + ${test}\n`; } output += '\n'; } // Metrics output += `Token Reduction:\n`; output += ` Original: ${result.metrics.originalTokens} tokens\n`; output += ` Compacted: ${result.metrics.compactedTokens} tokens\n`; output += ` Reduction: ${result.metrics.reductionPercentage}%\n`; return output; } finally { smartTest.close(); } } // MCP Tool definition export const SMART_TEST_TOOL_DEFINITION = { name: 'smart_test', description: 'Run tests with intelligent caching, coverage tracking, and incremental test execution', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Pattern to match test files', }, onlyChanged: { type: 'boolean', description: 'Run only tests that changed since last run', default: false, }, force: { type: 'boolean', description: 'Force full test run (ignore cache)', default: false, }, coverage: { type: 'boolean', description: 'Collect coverage information', default: false, }, watch: { type: 'boolean', description: 'Watch mode for continuous testing', default: false, }, projectRoot: { type: 'string', description: 'Project root directory', }, maxCacheAge: { type: 'number', description: 'Maximum cache age in seconds (default: 300)', default: 300, }, }, }, }; //# sourceMappingURL=smart-test.js.map