This commit is contained in:
Ralph Khreish
2025-07-11 13:29:52 +03:00
parent 74232d0e0d
commit 14cc09d241
27 changed files with 5699 additions and 958 deletions

View File

@@ -0,0 +1,207 @@
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '../../../..');
describe('MCP Server - get_tasks tool', () => {
let client;
let transport;
beforeAll(async () => {
// Create transport by spawning the server
transport = new StdioClientTransport({
command: 'node',
args: ['mcp-server/server.js'],
env: process.env,
cwd: projectRoot
});
// Create client
client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: {
sampling: {}
}
}
);
// Connect to server
await client.connect(transport);
});
afterAll(async () => {
if (client) {
await client.close();
}
});
it('should connect to MCP server successfully', async () => {
const tools = await client.listTools();
expect(tools.tools).toBeDefined();
expect(tools.tools.length).toBeGreaterThan(0);
const toolNames = tools.tools.map((t) => t.name);
expect(toolNames).toContain('get_tasks');
expect(toolNames).toContain('initialize_project');
});
it('should initialize project successfully', async () => {
const result = await client.callTool({
name: 'initialize_project',
arguments: {
projectRoot: projectRoot
}
});
expect(result.content).toBeDefined();
expect(result.content[0].type).toBe('text');
expect(result.content[0].text).toContain(
'Project initialized successfully'
);
});
it('should handle missing tasks file gracefully', async () => {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/non-existent-tasks.json'
}
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Error');
});
it('should get tasks with fixture data', async () => {
// Create a temporary tasks file with proper structure
const testTasksPath = join(projectRoot, '.taskmaster/test-tasks.json');
const testTasks = {
tasks: [
{
id: 'test-001',
description: 'Test task 1',
status: 'pending',
priority: 'high',
estimatedMinutes: 30,
actualMinutes: 0,
dependencies: [],
tags: ['test'],
subtasks: [
{
id: 'test-001-1',
description: 'Test subtask 1.1',
status: 'pending',
priority: 'medium',
estimatedMinutes: 15,
actualMinutes: 0
}
]
},
{
id: 'test-002',
description: 'Test task 2',
status: 'in_progress',
priority: 'medium',
estimatedMinutes: 60,
actualMinutes: 15,
dependencies: ['test-001'],
tags: ['test', 'demo'],
subtasks: []
}
]
};
// Write test tasks file
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
try {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/test-tasks.json',
withSubtasks: true
}
});
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toContain('2 tasks found');
expect(result.content[0].text).toContain('Test task 1');
expect(result.content[0].text).toContain('Test task 2');
expect(result.content[0].text).toContain('Test subtask 1.1');
} finally {
// Cleanup
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
}
});
it('should filter tasks by status', async () => {
// Create a temporary tasks file
const testTasksPath = join(
projectRoot,
'.taskmaster/test-status-tasks.json'
);
const testTasks = {
tasks: [
{
id: 'status-001',
description: 'Pending task',
status: 'pending',
priority: 'high',
estimatedMinutes: 30,
actualMinutes: 0,
dependencies: [],
tags: ['test'],
subtasks: []
},
{
id: 'status-002',
description: 'Done task',
status: 'done',
priority: 'medium',
estimatedMinutes: 60,
actualMinutes: 60,
dependencies: [],
tags: ['test'],
subtasks: []
}
]
};
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
try {
// Test filtering by 'done' status
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/test-status-tasks.json',
status: 'done'
}
});
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toContain('1 task found');
expect(result.content[0].text).toContain('Done task');
expect(result.content[0].text).not.toContain('Pending task');
} finally {
// Cleanup
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
}
});
});

View File

@@ -0,0 +1,146 @@
import { mcpTest } from 'mcp-jest';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '../../../..');
// Create test tasks file for testing
const testTasksPath = join(projectRoot, '.taskmaster/test-mcp-tasks.json');
const testTasks = {
tasks: [
{
id: 'mcp-test-001',
description: 'MCP Test task 1',
status: 'pending',
priority: 'high',
estimatedMinutes: 30,
actualMinutes: 0,
dependencies: [],
tags: ['test'],
subtasks: [
{
id: 'mcp-test-001-1',
description: 'MCP Test subtask 1.1',
status: 'pending',
priority: 'medium',
estimatedMinutes: 15,
actualMinutes: 0
}
]
},
{
id: 'mcp-test-002',
description: 'MCP Test task 2',
status: 'done',
priority: 'medium',
estimatedMinutes: 60,
actualMinutes: 60,
dependencies: ['mcp-test-001'],
tags: ['test', 'demo'],
subtasks: []
}
]
};
// Setup test data
fs.mkdirSync(join(projectRoot, '.taskmaster'), { recursive: true });
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
// Run MCP Jest tests
async function runTests() {
try {
const results = await mcpTest(
{
command: 'node',
args: [join(projectRoot, 'mcp-server/server.js')],
env: process.env
},
{
tools: {
initialize_project: {
args: { projectRoot: projectRoot },
expect: (result) =>
result.content[0].text.includes(
'Project initialized successfully'
)
},
get_tasks: [
{
name: 'get all tasks with subtasks',
args: {
projectRoot: projectRoot,
file: '.taskmaster/test-mcp-tasks.json',
withSubtasks: true
},
expect: (result) => {
const text = result.content[0].text;
return (
!result.isError &&
text.includes('2 tasks found') &&
text.includes('MCP Test task 1') &&
text.includes('MCP Test task 2') &&
text.includes('MCP Test subtask 1.1')
);
}
},
{
name: 'filter by done status',
args: {
projectRoot: projectRoot,
file: '.taskmaster/test-mcp-tasks.json',
status: 'done'
},
expect: (result) => {
const text = result.content[0].text;
return (
!result.isError &&
text.includes('1 task found') &&
text.includes('MCP Test task 2') &&
!text.includes('MCP Test task 1')
);
}
},
{
name: 'handle non-existent file',
args: {
projectRoot: projectRoot,
file: '.taskmaster/non-existent.json'
},
expect: (result) =>
result.isError && result.content[0].text.includes('Error')
}
]
}
}
);
console.log('\nTest Results:');
console.log('=============');
console.log(`✅ Passed: ${results.passed}/${results.total}`);
if (results.failed > 0) {
console.error(`❌ Failed: ${results.failed}`);
console.error('\nDetailed Results:');
console.log(JSON.stringify(results, null, 2));
}
// Cleanup
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
// Exit with appropriate code
process.exit(results.failed > 0 ? 1 : 0);
} catch (error) {
console.error('Test execution failed:', error);
// Cleanup on error
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
process.exit(1);
}
}
runTests();

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env node
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '../../../..');
// Create test tasks file for testing
const testTasksPath = join(projectRoot, '.taskmaster/test-tasks.json');
const testTasks = {
tasks: [
{
id: 'test-001',
description: 'Test task 1',
status: 'pending',
priority: 'high',
estimatedMinutes: 30,
actualMinutes: 0,
dependencies: [],
tags: ['test'],
subtasks: [
{
id: 'test-001-1',
description: 'Test subtask 1.1',
status: 'pending',
priority: 'medium',
estimatedMinutes: 15,
actualMinutes: 0
}
]
},
{
id: 'test-002',
description: 'Test task 2',
status: 'done',
priority: 'medium',
estimatedMinutes: 60,
actualMinutes: 60,
dependencies: ['test-001'],
tags: ['test', 'demo'],
subtasks: []
}
]
};
async function runTests() {
console.log('Starting MCP server tests...\n');
// Setup test data
fs.mkdirSync(join(projectRoot, '.taskmaster'), { recursive: true });
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
// Create transport by spawning the server
const transport = new StdioClientTransport({
command: 'node',
args: ['mcp-server/server.js'],
env: process.env,
cwd: projectRoot
});
// Create client
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
capabilities: {
sampling: {}
}
}
);
let testResults = {
total: 0,
passed: 0,
failed: 0,
tests: []
};
async function runTest(name, testFn) {
testResults.total++;
try {
await testFn();
testResults.passed++;
testResults.tests.push({ name, status: 'passed' });
console.log(`${name}`);
} catch (error) {
testResults.failed++;
testResults.tests.push({ name, status: 'failed', error: error.message });
console.error(`${name}`);
console.error(` Error: ${error.message}`);
}
}
try {
// Connect to server
await client.connect(transport);
console.log('Connected to MCP server\n');
// Test 1: List available tools
await runTest('List available tools', async () => {
const tools = await client.listTools();
if (!tools.tools || tools.tools.length === 0) {
throw new Error('No tools found');
}
const toolNames = tools.tools.map((t) => t.name);
if (!toolNames.includes('get_tasks')) {
throw new Error('get_tasks tool not found');
}
console.log(` Found ${tools.tools.length} tools`);
});
// Test 2: Initialize project
await runTest('Initialize project', async () => {
const result = await client.callTool({
name: 'initialize_project',
arguments: {
projectRoot: projectRoot
}
});
if (
!result.content[0].text.includes('Project initialized successfully')
) {
throw new Error('Project initialization failed');
}
});
// Test 3: Get all tasks
await runTest('Get all tasks with subtasks', async () => {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/test-tasks.json',
withSubtasks: true
}
});
if (result.isError) {
throw new Error(`Tool returned error: ${result.content[0].text}`);
}
const text = result.content[0].text;
const data = JSON.parse(text);
if (!data.data || !data.data.tasks) {
throw new Error('Invalid response format');
}
if (data.data.tasks.length !== 2) {
throw new Error(`Expected 2 tasks, got ${data.data.tasks.length}`);
}
const taskDescriptions = data.data.tasks.map((t) => t.description);
if (
!taskDescriptions.includes('Test task 1') ||
!taskDescriptions.includes('Test task 2')
) {
throw new Error('Expected tasks not found');
}
// Check for subtask
const task1 = data.data.tasks.find((t) => t.id === 'test-001');
if (!task1.subtasks || task1.subtasks.length === 0) {
throw new Error('Subtasks not found');
}
if (task1.subtasks[0].description !== 'Test subtask 1.1') {
throw new Error('Expected subtask not found');
}
});
// Test 4: Filter by status
await runTest('Filter tasks by done status', async () => {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/test-tasks.json',
status: 'done'
}
});
if (result.isError) {
throw new Error(`Tool returned error: ${result.content[0].text}`);
}
const text = result.content[0].text;
const data = JSON.parse(text);
if (!data.data || !data.data.tasks) {
throw new Error('Invalid response format');
}
if (data.data.tasks.length !== 1) {
throw new Error(
`Expected 1 task with done status, got ${data.data.tasks.length}`
);
}
const task = data.data.tasks[0];
if (task.description !== 'Test task 2') {
throw new Error(`Expected 'Test task 2', got '${task.description}'`);
}
if (task.status !== 'done') {
throw new Error(`Expected status 'done', got '${task.status}'`);
}
});
// Test 5: Handle non-existent file
await runTest('Handle non-existent file gracefully', async () => {
const result = await client.callTool({
name: 'get_tasks',
arguments: {
projectRoot: projectRoot,
file: '.taskmaster/non-existent.json'
}
});
if (!result.isError) {
throw new Error('Expected error for non-existent file');
}
if (!result.content[0].text.includes('Error')) {
throw new Error('Expected error message');
}
});
} catch (error) {
console.error('\nConnection error:', error.message);
testResults.failed = testResults.total;
} finally {
// Clean up
await client.close();
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
// Print summary
console.log('\n' + '='.repeat(50));
console.log('Test Summary:');
console.log(`Total: ${testResults.total}`);
console.log(`Passed: ${testResults.passed}`);
console.log(`Failed: ${testResults.failed}`);
console.log('='.repeat(50));
// Exit with appropriate code
process.exit(testResults.failed > 0 ? 1 : 0);
}
}
runTests().catch(console.error);