UNPKG

mini-todo-list-mcp

Version:

A streamlined Model Context Protocol (MCP) server for todo management with essential CRUD operations, bulk functionality, and workflow support

341 lines (280 loc) 12.3 kB
/** * Integration tests for Rules MCP tools */ import { describe, it, expect, beforeEach } from '@jest/globals'; import { join } from 'path'; import { mkdirSync, existsSync, writeFileSync, rmSync } from 'fs'; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { ruleService } from '../../src/services/RuleService.js'; // Mock the MCP server tools for testing describe('Rules MCP Tools Integration', () => { let tempDir: string; beforeEach(() => { tempDir = join(process.cwd(), 'tests', 'temp', 'rules-integration'); if (existsSync(tempDir)) { rmSync(tempDir, { recursive: true, force: true }); } mkdirSync(tempDir, { recursive: true }); ruleService.clearAllRules(); }); describe('add-rules tool', () => { it('should validate required parameters', () => { const AddRulesSchema = z.object({ filePath: z.string().min(1, "File path is required"), clearAll: z.boolean().optional().default(false), }); // Test valid parameters expect(() => AddRulesSchema.parse({ filePath: '/path/to/file.txt', clearAll: false })).not.toThrow(); // Test missing filePath expect(() => AddRulesSchema.parse({ clearAll: false })).toThrow('Required'); // Test empty filePath expect(() => AddRulesSchema.parse({ filePath: '', clearAll: false })).toThrow('File path is required'); // Test default clearAll const result = AddRulesSchema.parse({ filePath: '/path/to/file.txt' }); expect(result.clearAll).toBe(false); }); it('should handle successful rule addition', async () => { const filePath = join(tempDir, 'success-rule.txt'); const ruleContent = 'This is a successful rule addition test'; writeFileSync(filePath, ruleContent); const rules = await ruleService.addRules({ filePath: filePath, clearAll: false }); expect(rules).toHaveLength(1); expect(rules[0].description).toBe(ruleContent); expect(rules[0].filePath).toBe(filePath); }); it('should handle file not found error', async () => { const nonExistentPath = join(tempDir, 'non-existent-file.txt'); await expect(ruleService.addRules({ filePath: nonExistentPath, clearAll: false })).rejects.toThrow(`File does not exist: ${nonExistentPath}`); }); it('should handle directory instead of file error', async () => { const dirPath = join(tempDir, 'test-directory'); mkdirSync(dirPath); await expect(ruleService.addRules({ filePath: dirPath, clearAll: false })).rejects.toThrow(`Path is not a file: ${dirPath}`); }); it('should handle empty file error', async () => { const emptyFilePath = join(tempDir, 'empty-file.txt'); writeFileSync(emptyFilePath, ''); await expect(ruleService.addRules({ filePath: emptyFilePath, clearAll: false })).rejects.toThrow(`File is empty: ${emptyFilePath}`); }); it('should handle clearAll functionality', async () => { // Create first rule const firstFile = join(tempDir, 'first-rule.txt'); writeFileSync(firstFile, 'First rule content'); await ruleService.addRules({ filePath: firstFile, clearAll: false }); // Verify first rule exists let allRules = ruleService.getRules(); expect(allRules).toHaveLength(1); // Add second rule with clearAll=true const secondFile = join(tempDir, 'second-rule.txt'); writeFileSync(secondFile, 'Second rule content'); await ruleService.addRules({ filePath: secondFile, clearAll: true }); // Verify only second rule exists allRules = ruleService.getRules(); expect(allRules).toHaveLength(1); expect(allRules[0].description).toBe('Second rule content'); }); }); describe('get-rules tool', () => { it('should validate optional parameters', () => { const GetRulesSchema = z.object({ id: z.number().int().positive("Invalid Rule ID").optional(), }); // Test valid parameters expect(() => GetRulesSchema.parse({})).not.toThrow(); expect(() => GetRulesSchema.parse({ id: 5 })).not.toThrow(); // Test invalid ID types expect(() => GetRulesSchema.parse({ id: -1 })).toThrow('Invalid Rule ID'); expect(() => GetRulesSchema.parse({ id: 0 })).toThrow('Invalid Rule ID'); expect(() => GetRulesSchema.parse({ id: 1.5 })).toThrow(); }); it('should return empty array when no rules exist', () => { const rules = ruleService.getRules(); expect(rules).toHaveLength(0); expect(Array.isArray(rules)).toBe(true); }); it('should return all rules', async () => { // Create multiple rules const files = ['rule1.txt', 'rule2.txt', 'rule3.txt']; for (let i = 0; i < files.length; i++) { const filePath = join(tempDir, files[i]); writeFileSync(filePath, `Rule ${i + 1} content`); await ruleService.addRules({ filePath }); } const rules = ruleService.getRules(); expect(rules).toHaveLength(3); rules.forEach((rule, index) => { expect(rule.description).toBe(`Rule ${index + 1} content`); expect(rule).toHaveProperty('id'); expect(rule).toHaveProperty('createdAt'); expect(rule).toHaveProperty('updatedAt'); expect(rule).toHaveProperty('filePath'); }); }); it('should return specific rule by ID', async () => { // Create a rule const filePath = join(tempDir, 'specific-rule.txt'); const content = 'Specific rule for ID test'; writeFileSync(filePath, content); const [createdRule] = await ruleService.addRules({ filePath }); // Get specific rule by ID const rules = ruleService.getRules({ id: createdRule.id }); expect(rules).toHaveLength(1); expect(rules[0].id).toBe(createdRule.id); expect(rules[0].description).toBe(content); }); it('should return empty array for non-existent ID', async () => { // Create a rule first const filePath = join(tempDir, 'test-rule.txt'); writeFileSync(filePath, 'Test content'); await ruleService.addRules({ filePath }); // Try to get non-existent rule const rules = ruleService.getRules({ id: 99999 }); expect(rules).toHaveLength(0); }); }); describe('Rules tool error handling', () => { it('should handle various file types appropriately', async () => { const testFiles = [ { name: 'text.txt', content: 'Plain text content', shouldWork: true }, { name: 'markdown.md', content: '# Markdown Rule\nContent here', shouldWork: true }, { name: 'json.json', content: '{"rule": "JSON content"}', shouldWork: true }, { name: 'javascript.js', content: 'console.log("JS rule");', shouldWork: true }, { name: 'python.py', content: 'print("Python rule")', shouldWork: true }, { name: 'no-extension', content: 'File without extension', shouldWork: true } ]; for (const testFile of testFiles) { const filePath = join(tempDir, testFile.name); writeFileSync(filePath, testFile.content); if (testFile.shouldWork) { const rules = await ruleService.addRules({ filePath }); expect(rules).toHaveLength(1); expect(rules[0].description).toBe(testFile.content); } } // Verify all rules were created const allRules = ruleService.getRules(); expect(allRules.length).toBe(testFiles.filter(f => f.shouldWork).length); }); it('should handle binary files gracefully', async () => { // Create a binary file (simulate with non-UTF8 content) const binaryPath = join(tempDir, 'binary.bin'); const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0xFF, 0xFE]); writeFileSync(binaryPath, binaryContent); // Should still work as it reads as string, but content may be garbled const rules = await ruleService.addRules({ filePath: binaryPath }); expect(rules).toHaveLength(1); expect(rules[0].filePath).toBe(binaryPath); }); it('should handle concurrent operations safely', async () => { const promises = []; // Create multiple files and add rules concurrently for (let i = 0; i < 10; i++) { const filePath = join(tempDir, `concurrent-${i}.txt`); writeFileSync(filePath, `Concurrent rule ${i}`); promises.push(ruleService.addRules({ filePath })); } const results = await Promise.all(promises); expect(results).toHaveLength(10); // Verify all rules were created const allRules = ruleService.getRules(); expect(allRules).toHaveLength(10); }); it('should handle very long file paths', async () => { // Create nested directories with long names const longDirName = 'a'.repeat(50); const longFileName = 'b'.repeat(50) + '.txt'; const nestedDir = join(tempDir, longDirName, longDirName); mkdirSync(nestedDir, { recursive: true }); const longFilePath = join(nestedDir, longFileName); writeFileSync(longFilePath, 'Content in long path'); const rules = await ruleService.addRules({ filePath: longFilePath }); expect(rules).toHaveLength(1); expect(rules[0].filePath).toBe(longFilePath); }); }); describe('Tool response formatting', () => { it('should format successful add-rules response', async () => { const filePath = join(tempDir, 'format-test.txt'); writeFileSync(filePath, 'Test content'); const rules = await ruleService.addRules({ filePath, clearAll: false }); // Simulate the tool response formatting const response = { ruleCount: rules.length, summary: `Created ${rules.length} rule from ${filePath}` }; expect(response.ruleCount).toBe(1); expect(response.summary).toBe(`Created 1 rule from ${filePath}`); }); it('should format successful add-rules response with clearAll', async () => { const filePath = join(tempDir, 'format-clear-test.txt'); writeFileSync(filePath, 'Test content'); const rules = await ruleService.addRules({ filePath, clearAll: true }); // Simulate the tool response formatting const clearMessage = " (after clearing all existing rules)"; const response = { ruleCount: rules.length, summary: `Created ${rules.length} rule from ${filePath}${clearMessage}` }; expect(response.summary).toBe(`Created 1 rule from ${filePath} (after clearing all existing rules)`); }); it('should format get-rules response for single rule', async () => { const filePath = join(tempDir, 'single-rule-format.txt'); writeFileSync(filePath, 'Single rule content'); const [rule] = await ruleService.addRules({ filePath }); const rules = ruleService.getRules({ id: rule.id }); // Simulate the formatted response const formattedResponse = `**Rule ${rules[0].id}** Created: ${rules[0].createdAt} Source: ${rules[0].filePath} ${rules[0].description}`; expect(formattedResponse).toContain(`**Rule ${rule.id}**`); expect(formattedResponse).toContain('Single rule content'); expect(formattedResponse).toContain(filePath); }); it('should format get-rules response for multiple rules', async () => { // Create multiple rules const files = ['rule1.txt', 'rule2.txt']; for (let i = 0; i < files.length; i++) { const filePath = join(tempDir, files[i]); writeFileSync(filePath, `Rule ${i + 1} content`); await ruleService.addRules({ filePath }); } const rules = ruleService.getRules(); // Simulate the formatted response for multiple rules const formattedResponse = rules.map(rule => `**Rule ${rule.id}** Created: ${rule.createdAt} Source: ${rule.filePath} ${rule.description} ---` ).join('\n'); expect(formattedResponse).toContain('**Rule'); expect(formattedResponse).toContain('Rule 1 content'); expect(formattedResponse).toContain('Rule 2 content'); expect(formattedResponse).toContain('---'); }); }); });