@uplinq/mcp-vitest
Version:
MCP server for Vitest with watch-mode support for fast test feedback
290 lines • 11.2 kB
JavaScript
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