UNPKG

@uplinq/mcp-vitest

Version:

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

242 lines 7.28 kB
/** * Manages and tracks the state of Vitest test execution */ export class TestStateTracker { discoveryStatus = 'discovering'; testFiles = new Map(); lastUpdate = new Date(); /** * Updates test state from Vitest task packs */ updateFromTaskPacks(packs) { this.lastUpdate = new Date(); for (const pack of packs) { for (const taskResult of pack) { // TaskResultPack contains task results, we need to check if it's actually a Task if (taskResult && typeof taskResult === 'object' && 'type' in taskResult) { this.processTask(taskResult); } } } } /** * Processes a single task from Vitest */ processTask(task) { if (task.type === 'suite' && task.file) { this.testFiles.set(task.file.filepath, task.file); } } /** * Marks test discovery as complete */ markDiscoveryComplete() { this.discoveryStatus = 'complete'; this.lastUpdate = new Date(); } /** * Marks test discovery as failed */ markDiscoveryError() { this.discoveryStatus = 'error'; this.lastUpdate = new Date(); } /** * Processes collected files from Vitest */ processCollectedFiles(files) { for (const file of files) { this.testFiles.set(file.filepath, file); } this.lastUpdate = new Date(); } /** * Gets the aggregated test state */ getAggregatedState() { const summary = this.calculateSummary(); const files = this.buildFileStructure(); return { status: this.discoveryStatus, summary, files, lastUpdate: this.lastUpdate.toISOString(), }; } /** * Gets all currently failing tests with detailed error information */ getFailingTests() { const failing = []; for (const file of this.testFiles.values()) { this.collectFailingTests(file, failing); } return failing; } /** * Calculates summary statistics for all tests */ calculateSummary() { let total = 0, passed = 0, failed = 0, skipped = 0; for (const file of this.testFiles.values()) { const stats = this.getFileStats(file); total += stats.total; passed += stats.passed; failed += stats.failed; skipped += stats.skipped; } return { total, passed, failed, skipped }; } /** * Builds the nested file structure with test results */ buildFileStructure() { const files = {}; for (const file of this.testFiles.values()) { files[file.filepath] = this.buildFileNode(file); } return files; } /** * Builds a node for a test file */ buildFileNode(file) { const stats = this.getFileStats(file); const node = { ...stats }; if (file.tasks) { for (const task of file.tasks) { const taskNode = this.buildTaskNode(task); node[task.name] = taskNode; } } return node; } /** * Builds a node for a task (test or suite) */ buildTaskNode(task) { if (task.type === 'test') { const baseNode = { total: 1, passed: task.result?.state === 'pass' ? 1 : 0, failed: task.result?.state === 'fail' ? 1 : 0, skipped: task.result?.state === 'skip' ? 1 : 0, state: task.result?.state || 'queued', duration: task.result?.duration, }; if (task.result?.errors) { baseNode.errors = task.result.errors.map(err => ({ message: err.message, name: err.name || 'Error', stack: err.stack, })); } return baseNode; } if (task.type === 'suite' && 'tasks' in task && task.tasks) { const stats = this.getTaskStats(task); const suite = { ...stats }; for (const childTask of task.tasks) { const childNode = this.buildTaskNode(childTask); suite[childTask.name] = childNode; } return suite; } return { total: 1, passed: 0, failed: 0, skipped: 0, state: task.result?.state || 'queued', duration: task.result?.duration, }; } /** * Gets statistics for a test file */ getFileStats(file) { return this.getTaskStats(file); } /** * Gets statistics for a task (recursive) */ getTaskStats(task) { let total = 0, passed = 0, failed = 0, skipped = 0; if (task.type === 'test') { total = 1; const state = task.result?.state; if (state === 'pass') passed = 1; else if (state === 'fail') failed = 1; else if (state === 'skip') skipped = 1; } else if ('tasks' in task && task.tasks) { for (const childTask of task.tasks) { const childStats = this.getTaskStats(childTask); total += childStats.total; passed += childStats.passed; failed += childStats.failed; skipped += childStats.skipped; } } return { total, passed, failed, skipped }; } /** * Collects failing tests recursively from a task */ collectFailingTests(task, failing) { if (task.type === 'test' && task.result?.state === 'fail') { failing.push({ name: task.name, file: task.file?.filepath || 'unknown', suite: this.getSuitePath(task), errors: task.result.errors?.map(err => ({ message: err.message, name: err.name || 'Error', stack: err.stack, })) || [], duration: task.result.duration, }); } if ('tasks' in task && task.tasks) { for (const childTask of task.tasks) { this.collectFailingTests(childTask, failing); } } } /** * Gets the suite path for a test (breadcrumb trail) */ getSuitePath(task) { const path = []; let current = task.suite; while (current && current.name) { path.unshift(current.name); current = current.suite; } return path; } /** * Resets the tracker state */ reset() { this.discoveryStatus = 'discovering'; this.testFiles.clear(); this.lastUpdate = new Date(); } /** * Gets the current discovery status */ getDiscoveryStatus() { return this.discoveryStatus; } /** * Gets the number of test files being tracked */ getFileCount() { return this.testFiles.size; } } //# sourceMappingURL=test-state-tracker.js.map