Skip to main content
Glama
validation-bugs.test.ts14.3 kB
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { createMcpServer, createServices } from '../../src/server/index.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as crypto from 'crypto'; /** * RED PHASE - REQ-8: Validation Bug Fixes * * E2E tests for 6 validation bugs: * - BUG-018: Negative limit/offset accepted * - BUG-019: limit=0 returns empty with hasMore=true * - BUG-022: Invalid filter values return empty * - BUG-035: Whitespace-only tag keys accepted * - BUG-042: Empty description accepted * - BUG-012: No length limits on title/description * * These tests should FAIL initially, then PASS after GREEN phase fixes. */ // Helper to retry directory removal on Windows async function removeDirectoryWithRetry(dir: string, maxRetries = 3): Promise<void> { for (let i = 0; i < maxRetries; i++) { try { await fs.rm(dir, { recursive: true, force: true }); return; } catch (error: unknown) { if (i === maxRetries - 1) throw error; await new Promise((resolve) => setTimeout(resolve, 100 * (i + 1))); } } } // Helper to parse MCP tool result // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters function parseResult<T>(result: unknown): T { const r = result as { content: { type: string; text: string }[] }; return JSON.parse(r.content[0].text) as T; } describe('E2E: Validation Bug Fixes (RED Phase)', () => { let client: Client; let storagePath: string; let cleanup: () => Promise<void>; let planId: string; beforeAll(async () => { storagePath = path.join( process.cwd(), '.test-temp', 'validation-bugs-' + String(Date.now()) + '-' + crypto.randomUUID() ); await fs.mkdir(storagePath, { recursive: true }); const services = await createServices(storagePath); const { server } = createMcpServer(services); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); client = new Client( { name: 'validation-bugs-test', version: '1.0.0' }, { capabilities: {} } ); await server.connect(serverTransport); await client.connect(clientTransport); cleanup = async (): Promise<void> => { await client.close(); await server.close(); await removeDirectoryWithRetry(storagePath); }; // Setup: Create test plan const planResult = await client.callTool({ name: 'plan', arguments: { action: 'create', name: 'Validation Bugs Test Plan', description: 'Testing validation bug fixes', }, }); const plan = parseResult<{ planId: string }>(planResult); planId = plan.planId; }); afterAll(async () => { await cleanup(); }); // ============================================================ // RED: BUG-018 - Negative limit/offset Accepted // ============================================================ describe('BUG-018: negative limit/offset accepted', () => { it('RED: should reject negative limit in list operations', async () => { const listResult = await client.callTool({ name: 'requirement', arguments: { action: 'list', planId, limit: -1, }, }); // RED: This should FAIL because negative limit is not validated expect((listResult as { isError?: boolean }).isError).toBe(true); }); it('RED: should reject negative offset in list operations', async () => { const listResult = await client.callTool({ name: 'requirement', arguments: { action: 'list', planId, offset: -5, }, }); // RED: This should FAIL because negative offset is not validated expect((listResult as { isError?: boolean }).isError).toBe(true); }); }); // ============================================================ // RED: BUG-019 - limit=0 Returns Empty with hasMore=true // ============================================================ describe('BUG-019: limit=0 behavior', () => { it('RED: should reject limit=0 in list operations', async () => { const listResult = await client.callTool({ name: 'plan', arguments: { action: 'list', limit: 0, }, }); // RED: This should FAIL - limit=0 should be rejected expect((listResult as { isError?: boolean }).isError).toBe(true); }); }); // ============================================================ // RED: BUG-022 - Invalid Filter Values Return Empty // ============================================================ describe('BUG-022: invalid filter values', () => { it('RED: should reject invalid priority filter', async () => { const listResult = await client.callTool({ name: 'requirement', arguments: { action: 'list', planId, filters: { priority: 'super_critical', // Invalid! }, }, }); // RED: This should FAIL - invalid priority should be rejected expect((listResult as { isError?: boolean }).isError).toBe(true); }); it('RED: should reject invalid category filter', async () => { const listResult = await client.callTool({ name: 'requirement', arguments: { action: 'list', planId, filters: { category: 'invalid-category', }, }, }); // RED: This should FAIL - invalid category should be rejected expect((listResult as { isError?: boolean }).isError).toBe(true); }); it('RED: should reject invalid status filter', async () => { const listResult = await client.callTool({ name: 'requirement', arguments: { action: 'list', planId, filters: { status: 'invalid-status', }, }, }); // RED: This should FAIL - invalid status should be rejected expect((listResult as { isError?: boolean }).isError).toBe(true); }); }); // ============================================================ // RED: BUG-035 - Whitespace-Only Tag Keys Accepted // ============================================================ describe('BUG-035: whitespace-only tag keys', () => { it('RED: should reject tag with whitespace-only key', async () => { const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Test Requirement', description: 'Testing whitespace tags', source: { type: 'user-request' }, category: 'functional', priority: 'medium', tags: [{ key: ' ', value: 'whitespace key' }], }, }, }); // RED: This should FAIL - whitespace-only key should be rejected expect((addResult as { isError?: boolean }).isError).toBe(true); }); it('RED: should reject tag with tab-only key', async () => { const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Test Requirement 2', description: 'Testing tab tags', source: { type: 'user-request' }, category: 'functional', priority: 'medium', tags: [{ key: '\t\t\t', value: 'tab key' }], }, }, }); // RED: This should FAIL - tab-only key should be rejected expect((addResult as { isError?: boolean }).isError).toBe(true); }); it('RED: should reject tag with whitespace-only value', async () => { const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Test Requirement 3', description: 'Testing whitespace tag value', source: { type: 'user-request' }, category: 'functional', priority: 'medium', tags: [{ key: 'valid-key', value: ' ' }], }, }, }); // RED: This should FAIL - whitespace-only value should be rejected expect((addResult as { isError?: boolean }).isError).toBe(true); }); }); // ============================================================ // RED: BUG-042 - Empty Description Accepted // ============================================================ describe('BUG-042: empty description accepted', () => { it('RED: should reject empty string description', async () => { const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Test Requirement', description: '', // Empty string source: { type: 'user-request' }, category: 'functional', priority: 'medium', }, }, }); // RED: This should FAIL - empty description should be rejected expect((addResult as { isError?: boolean }).isError).toBe(true); }); it('RED: should allow undefined description', async () => { const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Test Requirement Without Description', // description intentionally omitted (undefined) source: { type: 'user-request' }, category: 'functional', priority: 'medium', }, }, }); // This should SUCCEED - undefined description is OK const result = parseResult<{ requirementId: string }>(addResult); expect(result.requirementId).toBeDefined(); }); it('RED: should reject empty string rationale', async () => { const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Test Requirement', description: 'Valid description', rationale: '', // Empty string source: { type: 'user-request' }, category: 'functional', priority: 'medium', }, }, }); // RED: This should FAIL - empty rationale should be rejected expect((addResult as { isError?: boolean }).isError).toBe(true); }); }); // ============================================================ // RED: BUG-012 - No Length Limits // ============================================================ describe('BUG-012: no length limits on text fields', () => { it('RED: should reject title exceeding 200 characters', async () => { const longTitle = 'A'.repeat(201); const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: longTitle, description: 'Valid description', source: { type: 'user-request' }, category: 'functional', priority: 'medium', }, }, }); // RED: This should FAIL - title too long expect((addResult as { isError?: boolean }).isError).toBe(true); }); it('RED: should accept title with exactly 200 characters', async () => { const title200 = 'A'.repeat(200); const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: title200, description: 'Valid description', source: { type: 'user-request' }, category: 'functional', priority: 'medium', }, }, }); // This should SUCCEED - exactly at limit const result = parseResult<{ requirementId: string }>(addResult); expect(result.requirementId).toBeDefined(); }); it('RED: should reject description exceeding 2000 characters', async () => { const longDesc = 'B'.repeat(2001); const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Valid Title', description: longDesc, source: { type: 'user-request' }, category: 'functional', priority: 'medium', }, }, }); // RED: This should FAIL - description too long expect((addResult as { isError?: boolean }).isError).toBe(true); }); it('RED: should accept description with exactly 2000 characters', async () => { const desc2000 = 'B'.repeat(2000); const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Valid Title', description: desc2000, source: { type: 'user-request' }, category: 'functional', priority: 'medium', }, }, }); // This should SUCCEED - exactly at limit const result = parseResult<{ requirementId: string }>(addResult); expect(result.requirementId).toBeDefined(); }); it('RED: should reject rationale exceeding 1000 characters', async () => { const longRationale = 'C'.repeat(1001); const addResult = await client.callTool({ name: 'requirement', arguments: { action: 'add', planId, requirement: { title: 'Valid Title', description: 'Valid description', rationale: longRationale, source: { type: 'user-request' }, category: 'functional', priority: 'medium', }, }, }); // RED: This should FAIL - rationale too long expect((addResult as { isError?: boolean }).isError).toBe(true); }); }); });

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/cppmyjob/cpp-mcp-planner'

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