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