Skip to main content
Glama
template-api.test.js20.4 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import http from 'http'; import path from 'path'; // Mock os module first vi.mock('os', () => ({ default: { homedir: () => '/mock/home', tmpdir: () => '/mock/tmp' } })); // Mock fs module const mockFs = { readFile: vi.fn(), writeFile: vi.fn(), mkdir: vi.fn(), readdir: vi.fn(), stat: vi.fn(), unlink: vi.fn() }; vi.mock('fs', async () => { const actual = await vi.importActual('fs'); return { ...actual, promises: mockFs, default: actual }; }); // Mock glob module vi.mock('glob', () => ({ glob: vi.fn() })); // Import server after mocks are set up const { startServer } = await import('../server.js'); describe('Template API Endpoints', () => { let server; const mockSettingsFile = path.join('/mock/home', '.shrimp-task-viewer-settings.json'); const mockTemplatesDir = path.join('/mock/home', '.shrimp-task-viewer-templates'); const mockDefaultTemplatesDir = '/mock/project/src/prompts/templates_en'; const mockDefaultTemplate = `## Task Analysis You must analyze the following task: {description} Requirements: {requirements}`; const mockCustomTemplate = `## Custom Task Analysis Custom implementation for: {description} With requirements: {requirements} Additional context: {summary}`; beforeEach(async () => { // Reset all mocks vi.clearAllMocks(); // Default settings mockFs.readFile.mockImplementation((filePath) => { if (filePath === mockSettingsFile) { return Promise.resolve(JSON.stringify({ agents: [] })); } // Default template files if (filePath.includes('planTask.txt')) { return Promise.resolve(mockDefaultTemplate); } // Custom template files if (filePath === path.join(mockTemplatesDir, 'planTask.txt')) { return Promise.resolve(mockCustomTemplate); } const error = new Error('ENOENT: no such file or directory'); error.code = 'ENOENT'; return Promise.reject(error); }); mockFs.writeFile.mockResolvedValue(); mockFs.mkdir.mockResolvedValue(); mockFs.stat.mockImplementation((filePath) => { if (filePath.includes('templates_en') || filePath.includes('.shrimp-task-viewer-templates')) { return Promise.resolve({ isDirectory: () => true }); } const error = new Error('ENOENT'); error.code = 'ENOENT'; return Promise.reject(error); }); // Mock directory scanning mockFs.readdir.mockImplementation((dirPath) => { if (dirPath.includes('templates_en')) { return Promise.resolve(['planTask.txt', 'executeTask.txt', 'analyzeTask.txt']); } if (dirPath.includes('.shrimp-task-viewer-templates')) { return Promise.resolve(['planTask.txt']); } return Promise.resolve([]); }); server = await startServer(); }); afterEach(async () => { if (server) { await new Promise((resolve) => { server.close(resolve); }); server = null; } }); describe('GET /api/templates', () => { it('should return list of all templates with status information', async () => { // Mock environment variables process.env.MCP_PROMPT_EXECUTE_TASK = 'Environment override content'; const response = await makeRequest(server, '/api/templates'); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); expect(Array.isArray(data)).toBe(true); expect(data.length).toBeGreaterThan(0); // Check template structure const template = data.find(t => t.functionName === 'planTask'); expect(template).toMatchObject({ functionName: 'planTask', name: 'planTask', status: 'custom', source: 'user-custom', contentLength: expect.any(Number), category: expect.any(String) }); }); it('should detect environment overrides correctly', async () => { process.env.MCP_PROMPT_PLAN_TASK = 'Environment content'; const response = await makeRequest(server, '/api/templates'); const data = JSON.parse(response.body); const planTaskTemplate = data.find(t => t.functionName === 'planTask'); expect(planTaskTemplate.status).toBe('env-override'); expect(planTaskTemplate.source).toBe('environment'); }); it('should detect append mode from environment', async () => { process.env.MCP_PROMPT_APPEND_PLAN_TASK = 'Append content'; const response = await makeRequest(server, '/api/templates'); const data = JSON.parse(response.body); const planTaskTemplate = data.find(t => t.functionName === 'planTask'); expect(planTaskTemplate.status).toBe('env-append'); expect(planTaskTemplate.source).toBe('environment+built-in'); }); it('should handle empty template directories', async () => { mockFs.readdir.mockResolvedValue([]); const response = await makeRequest(server, '/api/templates'); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); expect(Array.isArray(data)).toBe(true); }); it('should handle directory read errors', async () => { mockFs.readdir.mockRejectedValue(new Error('Permission denied')); const response = await makeRequest(server, '/api/templates'); expect(response.statusCode).toBe(500); expect(response.body).toContain('Error loading templates'); }); }); describe('GET /api/templates/:functionName', () => { it('should return specific template content', async () => { const response = await makeRequest(server, '/api/templates/planTask'); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); expect(data).toMatchObject({ functionName: 'planTask', name: 'planTask', content: expect.stringContaining('Custom Task Analysis'), status: 'custom', source: 'user-custom' }); }); it('should return default template when no custom version exists', async () => { const response = await makeRequest(server, '/api/templates/executeTask'); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); expect(data.status).toBe('default'); expect(data.source).toBe('built-in'); }); it('should return environment override when available', async () => { process.env.MCP_PROMPT_EXECUTE_TASK = 'Environment override content'; const response = await makeRequest(server, '/api/templates/executeTask'); const data = JSON.parse(response.body); expect(data.content).toBe('Environment override content'); expect(data.status).toBe('env-override'); expect(data.source).toBe('environment'); }); it('should return 404 for non-existent template', async () => { const response = await makeRequest(server, '/api/templates/nonExistentTemplate'); expect(response.statusCode).toBe(404); expect(response.body).toBe('Template not found'); }); it('should handle file read errors', async () => { mockFs.readFile.mockImplementation((filePath) => { if (filePath === mockSettingsFile) { return Promise.resolve(JSON.stringify({ agents: [] })); } return Promise.reject(new Error('File read error')); }); const response = await makeRequest(server, '/api/templates/planTask'); expect(response.statusCode).toBe(500); expect(response.body).toContain('Error loading template'); }); }); describe('PUT /api/templates/:functionName', () => { it('should save template with override mode', async () => { const templateData = { content: '## Updated Template\n\n{description}', mode: 'override' }; const response = await makeRequest(server, '/api/templates/planTask', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(templateData) }); expect(response.statusCode).toBe(200); expect(mockFs.writeFile).toHaveBeenCalledWith( path.join(mockTemplatesDir, 'planTask.txt'), templateData.content ); const data = JSON.parse(response.body); expect(data.success).toBe(true); }); it('should save template with append mode', async () => { const templateData = { content: 'Additional content', mode: 'append' }; const response = await makeRequest(server, '/api/templates/planTask', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(templateData) }); expect(response.statusCode).toBe(200); expect(mockFs.writeFile).toHaveBeenCalledWith( path.join(mockTemplatesDir, 'planTask_append.txt'), templateData.content ); }); it('should create templates directory if it does not exist', async () => { mockFs.stat.mockRejectedValue(new Error('ENOENT')); const templateData = { content: 'New template content', mode: 'override' }; const response = await makeRequest(server, '/api/templates/newTemplate', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(templateData) }); expect(response.statusCode).toBe(200); expect(mockFs.mkdir).toHaveBeenCalledWith(mockTemplatesDir, { recursive: true }); }); it('should validate required fields', async () => { const response = await makeRequest(server, '/api/templates/planTask', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) // Missing content and mode }); expect(response.statusCode).toBe(400); expect(response.body).toContain('Content and mode are required'); }); it('should handle file write errors', async () => { mockFs.writeFile.mockRejectedValue(new Error('Permission denied')); const templateData = { content: 'Template content', mode: 'override' }; const response = await makeRequest(server, '/api/templates/planTask', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(templateData) }); expect(response.statusCode).toBe(500); expect(response.body).toContain('Error saving template'); }); }); describe('POST /api/templates/:functionName/duplicate', () => { it('should create duplicate template with new name', async () => { const duplicateData = { newName: 'planTaskCopy' }; const response = await makeRequest(server, '/api/templates/planTask/duplicate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(duplicateData) }); expect(response.statusCode).toBe(200); expect(mockFs.writeFile).toHaveBeenCalledWith( path.join(mockTemplatesDir, 'planTaskCopy.txt'), mockCustomTemplate ); const data = JSON.parse(response.body); expect(data.success).toBe(true); expect(data.newTemplate.functionName).toBe('planTaskCopy'); }); it('should validate new name', async () => { const response = await makeRequest(server, '/api/templates/planTask/duplicate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) // Missing newName }); expect(response.statusCode).toBe(400); expect(response.body).toContain('New name is required'); }); it('should handle source template not found', async () => { const duplicateData = { newName: 'newTemplate' }; const response = await makeRequest(server, '/api/templates/nonExistentTemplate/duplicate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(duplicateData) }); expect(response.statusCode).toBe(404); expect(response.body).toContain('Source template not found'); }); }); describe('DELETE /api/templates/:functionName', () => { it('should delete custom template', async () => { mockFs.unlink.mockResolvedValue(); const response = await makeRequest(server, '/api/templates/planTask', { method: 'DELETE' }); expect(response.statusCode).toBe(200); expect(mockFs.unlink).toHaveBeenCalledWith( path.join(mockTemplatesDir, 'planTask.txt') ); const data = JSON.parse(response.body); expect(data.success).toBe(true); }); it('should delete append template', async () => { mockFs.unlink.mockResolvedValue(); const response = await makeRequest(server, '/api/templates/planTask?mode=append', { method: 'DELETE' }); expect(response.statusCode).toBe(200); expect(mockFs.unlink).toHaveBeenCalledWith( path.join(mockTemplatesDir, 'planTask_append.txt') ); }); it('should handle file not found during delete', async () => { const error = new Error('ENOENT'); error.code = 'ENOENT'; mockFs.unlink.mockRejectedValue(error); const response = await makeRequest(server, '/api/templates/planTask', { method: 'DELETE' }); expect(response.statusCode).toBe(404); expect(response.body).toContain('Template file not found'); }); it('should handle delete errors', async () => { mockFs.unlink.mockRejectedValue(new Error('Permission denied')); const response = await makeRequest(server, '/api/templates/planTask', { method: 'DELETE' }); expect(response.statusCode).toBe(500); expect(response.body).toContain('Error deleting template'); }); }); describe('POST /api/templates/export', () => { beforeEach(() => { // Mock templates for export mockFs.readdir.mockImplementation((dirPath) => { if (dirPath.includes('templates_en')) { return Promise.resolve(['planTask.txt', 'executeTask.txt']); } if (dirPath.includes('.shrimp-task-viewer-templates')) { return Promise.resolve(['planTask.txt']); } return Promise.resolve([]); }); }); it('should export templates in .env format', async () => { const exportData = { format: 'env', customOnly: false, preview: true }; const response = await makeRequest(server, '/api/templates/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(exportData) }); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); expect(data.content).toContain('MCP_PROMPT_PLAN_TASK='); expect(data.content).toContain('MCP_PROMPT_EXECUTE_TASK='); expect(data.filename).toBe('templates.env'); }); it('should export templates in mcp.json format', async () => { const exportData = { format: 'mcp.json', customOnly: false, preview: true }; const response = await makeRequest(server, '/api/templates/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(exportData) }); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); const jsonContent = JSON.parse(data.content); expect(jsonContent).toHaveProperty('mcpServers'); expect(jsonContent.mcpServers).toHaveProperty('shrimp-task-manager'); expect(data.filename).toBe('mcp.json'); }); it('should export only custom templates when customOnly is true', async () => { const exportData = { format: 'env', customOnly: true, preview: true }; const response = await makeRequest(server, '/api/templates/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(exportData) }); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); // Should only contain custom templates expect(data.content).toContain('MCP_PROMPT_PLAN_TASK='); expect(data.content).not.toContain('MCP_PROMPT_EXECUTE_TASK='); }); it('should validate export format', async () => { const exportData = { format: 'invalid', customOnly: false, preview: true }; const response = await makeRequest(server, '/api/templates/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(exportData) }); expect(response.statusCode).toBe(400); expect(response.body).toContain('Invalid export format'); }); it('should handle export errors', async () => { mockFs.readdir.mockRejectedValue(new Error('Permission denied')); const exportData = { format: 'env', customOnly: false, preview: true }; const response = await makeRequest(server, '/api/templates/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(exportData) }); expect(response.statusCode).toBe(500); expect(response.body).toContain('Error exporting templates'); }); }); describe('Edge Cases and Error Handling', () => { it('should handle malformed JSON in requests', async () => { const response = await makeRequest(server, '/api/templates/planTask', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{ invalid json }' }); expect(response.statusCode).toBe(400); expect(response.body).toContain('Invalid JSON'); }); it('should handle missing Content-Type header', async () => { const response = await makeRequest(server, '/api/templates/planTask', { method: 'PUT', body: JSON.stringify({ content: 'test', mode: 'override' }) }); expect(response.statusCode).toBe(400); expect(response.body).toContain('Content-Type must be application/json'); }); it('should handle very large template content', async () => { const largeContent = 'x'.repeat(100000); const templateData = { content: largeContent, mode: 'override' }; const response = await makeRequest(server, '/api/templates/planTask', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(templateData) }); expect(response.statusCode).toBe(200); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.any(String), largeContent ); }); it('should sanitize template function names', async () => { const templateData = { content: 'Template content', mode: 'override' }; const response = await makeRequest(server, '/api/templates/../../../etc/passwd', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(templateData) }); // Should sanitize the path and not create files outside templates directory expect(response.statusCode).toBe(200); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringContaining(mockTemplatesDir), templateData.content ); }); }); }); // Helper function to make HTTP requests to the test server function makeRequest(server, path, options = {}) { return new Promise((resolve) => { const port = server.address().port; const reqOptions = { hostname: '127.0.0.1', port, path, method: options.method || 'GET', headers: options.headers || {} }; const req = http.request(reqOptions, (res) => { let body = ''; res.on('data', (chunk) => body += chunk.toString()); res.on('end', () => { resolve({ statusCode: res.statusCode, headers: res.headers, body }); }); }); if (options.body) { req.write(options.body); } req.end(); }); }

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/cjo4m06/mcp-shrimp-task-manager'

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