Skip to main content
Glama
run-tests-basic.test.ts24.4 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { runTestsTool, handleRunTests, determineFormat, createExecutionContext, type RunTestsArgs, type TestExecutionContext } from '../run-tests.js'; import { projectContext } from '../../context/project-context.js'; import { processTestResult } from '../../utils/output-processor.js'; import * as fileUtils from '../../utils/file-utils.js'; import * as configLoader from '../../config/config-loader.js'; import * as versionChecker from '../../utils/version-checker.js'; import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import { writeFileSync, readFileSync, existsSync, unlinkSync } from 'fs'; // Mock external dependencies vi.mock('child_process'); vi.mock('../../utils/file-utils.js'); vi.mock('../../config/config-loader.js'); vi.mock('../../utils/version-checker.js'); vi.mock('../../utils/output-processor.js'); vi.mock('../../context/project-context.js'); vi.mock('fs'); // Create mock child process function createMockChildProcess() { const mockChild = new EventEmitter() as any; mockChild.stdout = new EventEmitter(); mockChild.stderr = new EventEmitter(); mockChild.kill = vi.fn(); mockChild.killed = false; return mockChild; } describe('run-tests (basic functionality)', () => { beforeEach(() => { vi.clearAllMocks(); // Setup default mocks vi.mocked(projectContext.getProjectRoot).mockReturnValue('/test/project'); vi.mocked(fileUtils.fileExists).mockResolvedValue(true); vi.mocked(fileUtils.isDirectory).mockResolvedValue(false); vi.mocked(configLoader.getConfig).mockResolvedValue({ testDefaults: { format: 'summary' as const, timeout: 30000 } } as any); vi.mocked(versionChecker.checkAllVersions).mockResolvedValue({ errors: [], warnings: [] } as any); vi.mocked(processTestResult).mockImplementation(async (result, format, _context) => ({ command: result.command, success: result.success, summary: { totalTests: 1, passed: 1, failed: 0 }, format: format, executionTimeMs: 100 })); // Setup fs mocks vi.mocked(writeFileSync).mockImplementation(() => {}); vi.mocked(readFileSync).mockReturnValue(''); vi.mocked(existsSync).mockReturnValue(true); vi.mocked(unlinkSync).mockImplementation(() => {}); }); afterEach(() => { vi.restoreAllMocks(); }); describe('Tool Definition', () => { it('should have correct name and description', () => { // Arrange & Act const tool = runTestsTool; // Assert expect(tool.name).toBe('run_tests'); expect(tool.description).toContain('Execute Vitest tests'); expect(tool.description).toContain('structured JSON output'); expect(tool.description).toContain('safety guards'); }); it('should define proper input schema', () => { // Arrange & Act const schema = runTestsTool.inputSchema; // Assert expect(schema.type).toBe('object'); expect(schema.properties).toBeDefined(); expect(schema.properties.target).toBeDefined(); expect(schema.properties.format).toBeDefined(); expect(schema.properties.project).toBeDefined(); expect(schema.properties.showLogs).toBeDefined(); }); it('should validate required parameters', () => { // Arrange & Act const schema = runTestsTool.inputSchema; // Assert expect(schema.required).toContain('target'); expect(schema.required).toHaveLength(1); }); it('should provide helpful parameter descriptions', () => { // Arrange & Act const props = runTestsTool.inputSchema.properties; // Assert expect(props.target.description).toContain('File path or directory to test'); expect(props.format.description).toContain('Output format'); expect(props.format.enum).toEqual(['summary', 'detailed']); expect(props.project.description).toContain('monorepo'); expect(props.showLogs.description).toContain('console output'); }); }); describe('Basic Test Execution', () => { beforeEach(() => { const mockChild = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(mockChild as any); // Reset the processTestResult mock to return success for these tests vi.mocked(processTestResult).mockImplementation(async (result, format, _context) => ({ command: result.command, success: true, summary: { totalTests: 1, passed: 1, failed: 0 }, format: format, executionTimeMs: 100 })); // Simulate successful test execution setTimeout(() => { mockChild.stdout.emit('data', Buffer.from('{"numTotalTests":1,"numPassedTests":1,"numFailedTests":0,"success":true}')); mockChild.emit('close', 0); }, 10); }); it('should run single test file successfully', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(true); expect(result.command).toContain('test.ts'); expect(spawn).toHaveBeenCalledWith( 'npx', expect.arrayContaining(['vitest', 'run', 'test.ts', '--reporter=json']), expect.any(Object) ); }); it('should run multiple test files', async () => { // Arrange vi.mocked(fileUtils.isDirectory).mockResolvedValue(true); const args: RunTestsArgs = { target: './tests' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(true); expect(result.command).toContain('tests'); expect(spawn).toHaveBeenCalledWith( 'npx', expect.arrayContaining(['vitest', 'run', 'tests', '--reporter=json']), expect.any(Object) ); }); it('should run tests in specific directory', async () => { // Arrange vi.mocked(fileUtils.isDirectory).mockResolvedValue(true); const args: RunTestsArgs = { target: './src/components' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(true); expect(result.command).toContain('src/components'); expect(fileUtils.isDirectory).toHaveBeenCalledWith('/test/project/src/components'); }); it('should handle glob patterns correctly', async () => { // Arrange const args: RunTestsArgs = { target: './**/*.test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(true); expect(result.command).toContain('**/*.test.ts'); }); it('should pass through vitest options', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts', project: 'web' }; // Act await handleRunTests(args); // Assert expect(spawn).toHaveBeenCalledWith( 'npx', expect.arrayContaining(['vitest', 'run', 'test.ts', '--reporter=json', '--project', 'web']), expect.any(Object) ); }); }); describe('Output Format Selection', () => { it('should auto-select summary format for single file with all passing', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; const context: TestExecutionContext = { isMultiFile: false, targetType: 'file' }; // Act const format = await determineFormat(args, context, false); // Assert expect(format).toBe('summary'); }); it('should auto-select detailed format for failures', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; const context: TestExecutionContext = { isMultiFile: false, targetType: 'file' }; // Act const format = await determineFormat(args, context, true); // Assert expect(format).toBe('detailed'); }); it('should auto-select detailed format for multiple files', async () => { // Arrange const args: RunTestsArgs = { target: './tests' }; const context: TestExecutionContext = { isMultiFile: true, targetType: 'directory' }; // Act const format = await determineFormat(args, context, false); // Assert expect(format).toBe('detailed'); }); it('should respect explicit format parameter', async () => { // Arrange const args: RunTestsArgs = { target: './tests', format: 'summary' }; const context: TestExecutionContext = { isMultiFile: true, targetType: 'directory' }; // Act const format = await determineFormat(args, context, true); // Assert expect(format).toBe('summary'); }); it('should validate format parameter values', () => { // Arrange & Act const formatEnum = runTestsTool.inputSchema.properties.format.enum; // Assert expect(formatEnum).toEqual(['summary', 'detailed']); }); }); describe('Project Context Integration', () => { beforeEach(() => { // Reset to default success mock for most tests in this group vi.mocked(processTestResult).mockImplementation(async (result, format, _context) => ({ command: result.command, success: true, summary: { totalTests: 1, passed: 1, failed: 0 }, format: format, executionTimeMs: 100 })); }); it('should validate project root is set', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; // Act & Assert await expect(async () => { await handleRunTests(args); }).not.toThrow(); expect(projectContext.getProjectRoot).toHaveBeenCalled(); }); it('should return helpful error when project root not set', async () => { // Arrange vi.mocked(projectContext.getProjectRoot).mockImplementation(() => { throw new Error('Project root has not been set'); }); vi.mocked(processTestResult).mockImplementation(async (result, _format, _context) => ({ command: result.command, success: false, summary: { totalTests: 0, passed: 0, failed: 0 }, format: 'detailed' as const, executionTimeMs: 100 })); const args: RunTestsArgs = { target: './test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(false); expect(result.command).toBeDefined(); expect(processTestResult).toHaveBeenCalledWith( expect.objectContaining({ success: false, stderr: expect.stringContaining('Invalid target path') }), 'detailed', expect.any(Object) ); }); it('should use project root for relative path resolution', async () => { // Arrange const args: RunTestsArgs = { target: './src/test.ts' }; // Act await handleRunTests(args); // Assert expect(fileUtils.fileExists).toHaveBeenCalledWith('/test/project/src/test.ts'); }); it('should handle project root changes between calls', async () => { // Arrange const args1: RunTestsArgs = { target: './test1.ts' }; const args2: RunTestsArgs = { target: './test2.ts' }; // Act await handleRunTests(args1); vi.mocked(projectContext.getProjectRoot).mockReturnValue('/different/project'); await handleRunTests(args2); // Assert expect(projectContext.getProjectRoot).toHaveBeenCalledTimes(2); expect(fileUtils.fileExists).toHaveBeenCalledWith('/test/project/test1.ts'); expect(fileUtils.fileExists).toHaveBeenCalledWith('/different/project/test2.ts'); }); }); describe('Basic Error Handling', () => { it('should handle non-existent test files gracefully', async () => { // Arrange vi.mocked(fileUtils.fileExists).mockResolvedValue(false); vi.mocked(processTestResult).mockImplementation(async (result, _format, _context) => ({ command: result.command, success: false, summary: { totalTests: 0, passed: 0, failed: 0 }, format: 'detailed' as const, executionTimeMs: 100 })); const args: RunTestsArgs = { target: './nonexistent.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(false); expect(processTestResult).toHaveBeenCalledWith( expect.objectContaining({ success: false, stderr: expect.stringContaining('Target does not exist') }), 'detailed', expect.any(Object) ); }); it('should handle invalid path arguments', async () => { // Arrange vi.mocked(processTestResult).mockImplementation(async (result, _format, _context) => ({ command: result.command, success: false, summary: { totalTests: 0, passed: 0, failed: 0 }, format: 'detailed' as const, executionTimeMs: 100 })); const args: RunTestsArgs = { target: '' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(false); expect(processTestResult).toHaveBeenCalledWith( expect.objectContaining({ success: false, stderr: expect.stringContaining('Target parameter is required') }), 'detailed', expect.any(Object) ); }); it('should provide clear error messages for common issues', async () => { // Arrange vi.mocked(fileUtils.isDirectory).mockResolvedValue(true); vi.mocked(processTestResult).mockImplementation(async (result, _format, _context) => ({ command: result.command, success: false, summary: { totalTests: 0, passed: 0, failed: 0 }, format: 'detailed' as const, executionTimeMs: 100 })); const args: RunTestsArgs = { target: './' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(false); expect(processTestResult).toHaveBeenCalledWith( expect.objectContaining({ success: false, stderr: expect.stringContaining('Cannot run tests on entire project root') }), 'detailed', expect.any(Object) ); }); it('should handle permission errors', async () => { // Arrange const permissionError = new Error('EACCES: permission denied'); vi.mocked(fileUtils.fileExists).mockRejectedValue(permissionError); vi.mocked(processTestResult).mockImplementation(async (result, _format, _context) => ({ command: result.command, success: false, summary: { totalTests: 0, passed: 0, failed: 0 }, format: 'detailed' as const, executionTimeMs: 100 })); const args: RunTestsArgs = { target: './test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.success).toBe(false); expect(processTestResult).toHaveBeenCalledWith( expect.objectContaining({ success: false, stderr: expect.stringContaining('EACCES: permission denied') }), 'detailed', expect.any(Object) ); }); }); describe('Response Structure', () => { beforeEach(() => { // Reset to default success mock vi.mocked(processTestResult).mockImplementation(async (result, format, _context) => ({ command: result.command, success: true, summary: { totalTests: 1, passed: 1, failed: 0 }, format: format, executionTimeMs: 100 })); }); it('should return consistent response structure', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result).toHaveProperty('command'); expect(result).toHaveProperty('success'); expect(result).toHaveProperty('summary'); expect(result).toHaveProperty('format'); expect(result).toHaveProperty('executionTimeMs'); }); it('should include command information', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts', project: 'web' }; // Act const result = await handleRunTests(args); // Assert expect(result.command).toContain('vitest run'); expect(result.command).toContain('test.ts'); }); it('should include execution context', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.format).toBeDefined(); expect(['summary', 'detailed']).toContain(result.format); expect(result.executionTimeMs).toBeTypeOf('number'); }); it('should include test summary', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.summary).toHaveProperty('totalTests'); expect(result.summary).toHaveProperty('passed'); expect(result.summary).toHaveProperty('failed'); expect(result.summary.totalTests).toBeTypeOf('number'); expect(result.summary.passed).toBeTypeOf('number'); expect(result.summary.failed).toBeTypeOf('number'); }); it('should conditionally include detailed results', async () => { // Arrange vi.mocked(processTestResult).mockResolvedValueOnce({ command: 'vitest run test.ts', success: false, summary: { totalTests: 1, passed: 0, failed: 1 }, format: 'detailed' as const, executionTimeMs: 100, testResults: { failedTests: [{ file: 'test.ts', tests: [{ testName: 'failing test', errorType: 'AssertionError', message: 'Test failed' }] }] } }); const args: RunTestsArgs = { target: './test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.testResults).toBeDefined(); expect(result.testResults?.failedTests).toBeDefined(); }); }); describe('Path Handling', () => { beforeEach(() => { // Reset to default success mock vi.mocked(processTestResult).mockImplementation(async (result, format, _context) => ({ command: result.command, success: true, summary: { totalTests: 1, passed: 1, failed: 0 }, format: format, executionTimeMs: 100 })); }); it('should resolve relative paths against project root', async () => { // Arrange const args: RunTestsArgs = { target: './src/test.ts' }; // Act await handleRunTests(args); // Assert expect(fileUtils.fileExists).toHaveBeenCalledWith('/test/project/src/test.ts'); }); it('should handle absolute paths correctly', async () => { // Arrange const args: RunTestsArgs = { target: '/absolute/path/test.ts' }; // Act await handleRunTests(args); // Assert expect(fileUtils.fileExists).toHaveBeenCalledWith('/absolute/path/test.ts'); }); it('should normalize path separators', async () => { // Arrange const args: RunTestsArgs = { target: './src\\test.ts' }; // Act const result = await handleRunTests(args); // Assert expect(result.command).toContain('src'); expect(result.command).toContain('test.ts'); }); it('should validate path security', async () => { // Arrange - Trying to use path traversal const args: RunTestsArgs = { target: '../../../etc/passwd' }; // Act await handleRunTests(args); // Assert expect(fileUtils.fileExists).toHaveBeenCalled(); // The specific path validation is handled by file-utils }); }); describe('Vitest Integration', () => { beforeEach(() => { const mockChild = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(mockChild as any); // Reset the processTestResult mock to return success for these tests vi.mocked(processTestResult).mockImplementation(async (result, format, _context) => ({ command: result.command, success: true, summary: { totalTests: 1, passed: 1, failed: 0 }, format: format, executionTimeMs: 100 })); setTimeout(() => { mockChild.stdout.emit('data', Buffer.from('{"success":true}')); mockChild.emit('close', 0); }, 10); }); it('should execute vitest with correct arguments', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; // Act await handleRunTests(args); // Assert expect(spawn).toHaveBeenCalledWith( 'npx', ['vitest', 'run', 'test.ts', '--reporter=json'], expect.objectContaining({ cwd: '/test/project', stdio: ['ignore', 'pipe', 'pipe'] }) ); }); it('should use JSON reporter by default', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts' }; // Act await handleRunTests(args); // Assert expect(spawn).toHaveBeenCalledWith( 'npx', expect.arrayContaining(['--reporter=json']), expect.any(Object) ); }); it('should handle vitest configuration files', async () => { // Arrange const mockChild = createMockChildProcess(); vi.mocked(spawn).mockReturnValue(mockChild as any); // Create a promise that resolves when the mock child closes const childProcess = new Promise<void>((resolve) => { setTimeout(() => { mockChild.stdout.emit('data', Buffer.from('{"success":true}')); mockChild.emit('close', 0); resolve(); }, 10); }); const args: RunTestsArgs = { target: './test.ts', showLogs: true }; // Act const resultPromise = handleRunTests(args); await childProcess; // Wait for the mock child process to emit events await resultPromise; // Assert expect(spawn).toHaveBeenCalledWith( 'npx', expect.arrayContaining(['--config']), expect.any(Object) ); }); it('should respect vitest workspace configurations', async () => { // Arrange const args: RunTestsArgs = { target: './test.ts', project: 'web' }; // Act await handleRunTests(args); // Assert expect(spawn).toHaveBeenCalledWith( 'npx', expect.arrayContaining(['--project', 'web']), expect.any(Object) ); }); }); describe('Utility Functions', () => { it('should create execution context for file', async () => { // Arrange vi.mocked(fileUtils.isDirectory).mockResolvedValue(false); // Act const context = await createExecutionContext('/test/project/test.ts'); // Assert expect(context.isMultiFile).toBe(false); expect(context.targetType).toBe('file'); expect(context.estimatedTestCount).toBeUndefined(); }); it('should create execution context for directory', async () => { // Arrange vi.mocked(fileUtils.isDirectory).mockResolvedValue(true); // Act const context = await createExecutionContext('/test/project/tests'); // Assert expect(context.isMultiFile).toBe(true); expect(context.targetType).toBe('directory'); expect(context.estimatedTestCount).toBeUndefined(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/djankies/vitest-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server