#!/usr/bin/env node
/**
* Comprehensive test suite for the Code Review MCP Server
* Tests build process, environment validation, and server initialization
* Can be run without actual GitLab credentials
*/
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
class TestRunner {
constructor() {
this.passed = 0;
this.failed = 0;
this.tests = [];
}
async test(name, testFn) {
console.log(`\n๐งช Testing: ${name}`);
try {
await testFn();
console.log(`โ
PASS: ${name}`);
this.passed++;
} catch (error) {
console.log(`โ FAIL: ${name}`);
console.log(` Error: ${error.message}`);
this.failed++;
}
this.tests.push({ name, passed: this.failed === 0 });
}
summary() {
console.log('\n' + '='.repeat(50));
console.log('๐ TEST SUMMARY');
console.log('='.repeat(50));
console.log(`โ
Passed: ${this.passed}`);
console.log(`โ Failed: ${this.failed}`);
console.log(`๐ Total: ${this.passed + this.failed}`);
if (this.failed > 0) {
console.log('\nโ Some tests failed. Please review the output above.');
process.exit(1);
} else {
console.log('\n๐ All tests passed!');
}
}
}
async function runCommand(command, args = [], options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: ['pipe', 'pipe', 'pipe'],
...options
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
resolve({ code, stdout, stderr });
});
child.on('error', (error) => {
reject(error);
});
});
}
async function fileExists(path) {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
async function isExecutable(path) {
try {
const stats = await fs.stat(path);
// Check if file has execute permissions (owner, group, or other)
return (stats.mode & parseInt('111', 8)) !== 0;
} catch {
return false;
}
}
async function testBuildProcess() {
console.log(' Building TypeScript project...');
// Clean build directory first
try {
await fs.rm('build', { recursive: true, force: true });
} catch {
// Directory might not exist, that's fine
}
const result = await runCommand('pnpm', ['run', 'build']);
if (result.code !== 0) {
throw new Error(`Build failed with code ${result.code}: ${result.stderr}`);
}
console.log(' โ TypeScript compilation successful');
}
async function testOutputFileGeneration() {
console.log(' Checking build output...');
const buildIndexPath = join(__dirname, 'build', 'index.js');
const buildEnvPath = join(__dirname, 'build', 'env.js');
if (!(await fileExists(buildIndexPath))) {
throw new Error('build/index.js was not generated');
}
if (!(await fileExists(buildEnvPath))) {
throw new Error('build/env.js was not generated');
}
console.log(' โ Build files generated successfully');
}
async function testExecutablePermissions() {
console.log(' Checking executable permissions...');
const buildIndexPath = join(__dirname, 'build', 'index.js');
if (!(await isExecutable(buildIndexPath))) {
throw new Error('build/index.js is not executable');
}
console.log(' โ Executable permissions set correctly');
}
async function testEnvironmentValidation() {
console.log(' Testing environment variable validation...');
// Test with missing environment variables
const result = await runCommand('node', ['build/index.js'], {
env: {
...process.env,
// Remove required environment variables
GITLAB_PROJECT_ID: undefined,
GITLAB_PAT: undefined,
GITLAB_API_URL: undefined,
SERVER_NAME: undefined,
SERVER_VERSION: undefined
}
});
// Should fail with missing environment variables
if (result.code === 0) {
throw new Error('Server should have failed with missing environment variables');
}
if (!result.stderr.includes('Invalid environment variables')) {
throw new Error('Expected environment validation error message not found');
}
console.log(' โ Environment validation working correctly');
}
async function testServerInitialization() {
console.log(' Testing MCP server initialization...');
// Test with valid environment variables
const child = spawn('node', ['build/index.js'], {
env: {
...process.env,
GITLAB_PROJECT_ID: '123',
GITLAB_PAT: 'test-token',
GITLAB_API_URL: 'https://gitlab.example.com',
SERVER_NAME: 'test-server',
SERVER_VERSION: '1.0.0'
},
stdio: ['pipe', 'pipe', 'pipe']
});
let stderr = '';
child.stderr.on('data', (data) => {
stderr += data.toString();
});
// Give the server a moment to initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Send a test MCP request
const testRequest = JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/list"
}) + '\n';
child.stdin.write(testRequest);
// Wait for response
let stdout = '';
const responsePromise = new Promise((resolve) => {
child.stdout.on('data', (data) => {
stdout += data.toString();
if (stdout.includes('"jsonrpc"')) {
resolve();
}
});
});
await Promise.race([
responsePromise,
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for server response')), 5000))
]);
child.kill();
// Check if we got a valid JSON-RPC response
if (!stdout.includes('"jsonrpc"') || !stdout.includes('"result"')) {
throw new Error(`Invalid server response: ${stdout}`);
}
// Parse the response to verify it contains our tools
try {
const lines = stdout.trim().split('\n');
const response = JSON.parse(lines[lines.length - 1]);
if (!response.result || !response.result.tools) {
throw new Error('Response missing tools list');
}
const toolNames = response.result.tools.map(tool => tool.name);
const expectedTools = ['get-projects', 'get-merge-requests', 'get-merge-request-diffs', 'create-draft-note'];
for (const expectedTool of expectedTools) {
if (!toolNames.includes(expectedTool)) {
throw new Error(`Missing expected tool: ${expectedTool}`);
}
}
} catch (parseError) {
throw new Error(`Failed to parse server response: ${parseError.message}`);
}
console.log(' โ MCP server initialized and responding correctly');
}
async function testPackageJsonConfiguration() {
console.log(' Testing package.json configuration...');
const packageJsonPath = join(__dirname, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
// Verify essential package.json fields
if (packageJson.type !== 'module') {
throw new Error('package.json should specify "type": "module"');
}
if (!packageJson.bin || !packageJson.bin['code-review-mcp']) {
throw new Error('package.json missing binary configuration');
}
if (packageJson.bin['code-review-mcp'] !== './build/index.js') {
throw new Error('Binary path should point to ./build/index.js');
}
console.log(' โ Package configuration is correct');
}
async function testTypeScriptConfiguration() {
console.log(' Testing TypeScript configuration...');
const tsconfigPath = join(__dirname, 'tsconfig.json');
const tsconfig = JSON.parse(await fs.readFile(tsconfigPath, 'utf8'));
// Verify essential TypeScript settings
if (tsconfig.compilerOptions.target !== 'ES2022') {
throw new Error('TypeScript target should be ES2022');
}
if (tsconfig.compilerOptions.module !== 'Node16') {
throw new Error('TypeScript module should be Node16');
}
if (tsconfig.compilerOptions.outDir !== './build') {
throw new Error('TypeScript outDir should be ./build');
}
console.log(' โ TypeScript configuration is correct');
}
async function main() {
console.log('๐ Starting Code Review MCP Server Test Suite');
console.log('=' .repeat(50));
const runner = new TestRunner();
// Test build process
await runner.test('Build Process', testBuildProcess);
await runner.test('Output File Generation', testOutputFileGeneration);
await runner.test('Executable Permissions', testExecutablePermissions);
// Test configuration
await runner.test('Package.json Configuration', testPackageJsonConfiguration);
await runner.test('TypeScript Configuration', testTypeScriptConfiguration);
// Test runtime behavior
await runner.test('Environment Variable Validation', testEnvironmentValidation);
await runner.test('MCP Server Initialization', testServerInitialization);
runner.summary();
}
// Run the tests
main().catch((error) => {
console.error('๐ฅ Test suite failed:', error);
process.exit(1);
});