UNPKG

@uplinq/mcp-vitest

Version:

MCP server for Vitest with watch-mode support for fast test feedback

290 lines 11.2 kB
import { createVitest } from 'vitest/node'; import { TestStateTracker } from './test-state-tracker.js'; import { log, error } from './utils/logger.js'; /** * Manages the Vitest instance lifecycle and test execution */ export class VitestManager { vitest = null; testStateTracker; isInitialized = false; isShuttingDown = false; statePollingInterval = null; hasInitialRunCompleted = false; isStdioTransport = false; constructor(isStdioTransport = false) { this.testStateTracker = new TestStateTracker(); this.isStdioTransport = isStdioTransport; } /** * Initializes the Vitest instance with watch mode enabled */ async initialize() { if (this.isInitialized || this.isShuttingDown) { return; } try { log('Initializing Vitest...'); // For stdio transport, redirect stdout to stderr during Vitest operations let originalStdoutWrite; if (this.isStdioTransport) { originalStdoutWrite = process.stdout.write; process.stdout.write = process.stderr.write.bind(process.stderr); } try { this.vitest = await createVitest('test', { watch: true, reporters: [], // Disable default reporters to avoid console noise config: false, // Use project's vitest config ui: false, // Disable UI coverage: { enabled: false }, // Disable coverage for performance silent: this.isStdioTransport, // Completely silent for stdio transport }); } finally { // Restore original stdout if (this.isStdioTransport && originalStdoutWrite) { process.stdout.write = originalStdoutWrite; } } this.setupEventListeners(); // Start Vitest in the background log('Starting Vitest...'); // For stdio transport, redirect stdout to stderr during Vitest start if (this.isStdioTransport) { originalStdoutWrite = process.stdout.write; process.stdout.write = process.stderr.write.bind(process.stderr); } try { await this.vitest.start(); } finally { // Restore original stdout if (this.isStdioTransport && originalStdoutWrite) { process.stdout.write = originalStdoutWrite; } } this.isInitialized = true; log('Vitest initialized successfully'); // Start polling Vitest state to detect completion this.startStatePolling(); } catch (err) { this.testStateTracker.markDiscoveryError(); throw new Error(`Failed to initialize Vitest: ${err instanceof Error ? err.message : String(err)}`); } } /** * Starts polling Vitest state to detect when tests complete */ startStatePolling() { if (this.statePollingInterval || !this.vitest) return; this.statePollingInterval = setInterval(() => { this.checkVitestState(); }, 1000); // Poll every second log('Started state polling for Vitest completion detection'); } /** * Checks the current Vitest state and updates our tracker accordingly */ checkVitestState() { if (!this.vitest || !this.isInitialized) return; try { // Get current files from Vitest state const files = this.vitest.state.getFiles(); if (files && files.length > 0) { // Process the collected files this.testStateTracker.processCollectedFiles(files); // Check if initial run has completed if (!this.hasInitialRunCompleted) { const hasRunResults = files.some(file => file.result && (file.result.state === 'pass' || file.result.state === 'fail')); if (hasRunResults) { log('Detected initial Vitest run completion'); this.hasInitialRunCompleted = true; this.testStateTracker.markDiscoveryComplete(); // Stop polling once we've detected the initial completion if (this.statePollingInterval) { clearInterval(this.statePollingInterval); this.statePollingInterval = null; log('Stopped state polling - initial run detected'); } } } } } catch (err) { error('Error checking Vitest state:', err); } } /** * Sets up event listeners for Vitest state changes */ setupEventListeners() { if (!this.vitest) return; // Listen to test state changes - using a more compatible approach const handleTaskUpdate = (packs) => { try { this.testStateTracker.updateFromTaskPacks(packs); // Also trigger a state check when tasks update this.checkVitestState(); } catch (err) { error('Error updating test state:', err); } }; const handleFinished = (files, errors) => { try { log('Vitest onFinished event triggered'); if (errors && errors.length > 0) { error('Vitest execution errors:', errors); this.testStateTracker.markDiscoveryError(); } else { this.hasInitialRunCompleted = true; this.testStateTracker.markDiscoveryComplete(); log('Marked discovery as complete from onFinished event'); } } catch (err) { error('Error handling test completion:', err); } }; const handleCollected = (files) => { try { log(`Vitest onCollected event triggered with ${files.length} files`); this.testStateTracker.processCollectedFiles(files); } catch (err) { error('Error processing collected files:', err); } }; // Set up listeners with proper error handling try { // Log what methods are available on the state object log('Available Vitest state methods:', Object.getOwnPropertyNames(this.vitest.state)); // These may not be available in all Vitest versions, so we wrap in try-catch if ('onTaskUpdate' in this.vitest.state && typeof this.vitest.state.onTaskUpdate === 'function') { this.vitest.state.onTaskUpdate(handleTaskUpdate); log('Set up onTaskUpdate listener'); } else { log('onTaskUpdate not available on Vitest state'); } if ('onFinished' in this.vitest.state && typeof this.vitest.state.onFinished === 'function') { this.vitest.state.onFinished(handleFinished); log('Set up onFinished listener'); } else { log('onFinished not available on Vitest state'); } if ('onCollected' in this.vitest.state && typeof this.vitest.state.onCollected === 'function') { this.vitest.state.onCollected(handleCollected); log('Set up onCollected listener'); } else { log('onCollected not available on Vitest state'); } // Try alternative event listeners if available if ('on' in this.vitest.state && typeof this.vitest.state.on === 'function') { log('Setting up generic event listeners via .on()'); this.vitest.state.on('taskUpdate', handleTaskUpdate); this.vitest.state.on('finished', handleFinished); this.vitest.state.on('collected', handleCollected); } } catch (err) { log('Some Vitest event listeners could not be set up:', err); log('Relying on state polling for completion detection'); } } /** * Gets the current test state from the tracker */ getTestState() { // Ensure we have the latest state before returning if (this.vitest && this.isInitialized) { this.checkVitestState(); } return this.testStateTracker.getAggregatedState(); } /** * Gets all currently failing tests */ getFailingTests() { return this.testStateTracker.getFailingTests(); } /** * Checks if Vitest is initialized and ready */ isReady() { return this.isInitialized && this.vitest !== null && !this.isShuttingDown; } /** * Gets the current initialization status */ getStatus() { if (this.isShuttingDown) return 'shutting-down'; if (!this.isInitialized && this.vitest === null) return 'uninitialized'; if (!this.isInitialized && this.vitest !== null) return 'initializing'; if (this.isInitialized && this.vitest !== null) return 'ready'; return 'error'; } /** * Resets the test state tracker */ resetTestState() { this.testStateTracker.reset(); this.hasInitialRunCompleted = false; } /** * Closes the Vitest instance and cleans up resources */ async close() { if (this.isShuttingDown || !this.vitest) { return; } this.isShuttingDown = true; // Stop state polling if (this.statePollingInterval) { clearInterval(this.statePollingInterval); this.statePollingInterval = null; } try { log('Closing Vitest...'); await this.vitest.close(); log('Vitest closed successfully'); } catch (err) { error('Error closing Vitest:', err); } finally { this.vitest = null; this.isInitialized = false; this.isShuttingDown = false; this.hasInitialRunCompleted = false; this.testStateTracker.reset(); } } /** * Gets statistics about the managed Vitest instance */ getStatistics() { return { isInitialized: this.isInitialized, isReady: this.isReady(), status: this.getStatus(), fileCount: this.testStateTracker.getFileCount(), discoveryStatus: this.testStateTracker.getDiscoveryStatus(), hasInitialRunCompleted: this.hasInitialRunCompleted, isPolling: this.statePollingInterval !== null, }; } } //# sourceMappingURL=vitest-manager.js.map