Skip to main content
Glama

Targetprocess MCP Server

add-comment.test.ts25 kB
import { jest } from '@jest/globals'; import { AddCommentOperation } from '../../operations/work/add-comment.js'; import { TPService } from '../../api/client/tp.service.js'; import { ExecutionContext } from '../../core/interfaces/semantic-operation.interface.js'; import { testConfig } from '../config/test-config.js'; // Mock logger to avoid console output during tests jest.mock('../../utils/logger.js', () => ({ logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } })); // Mock TPService const mockService = { getEntity: jest.fn(), createComment: jest.fn(), searchEntities: jest.fn(), } as unknown as jest.Mocked<TPService>; // Helper to create mock execution context with different roles const createMockContext = (role: string = 'developer'): ExecutionContext => ({ user: { id: parseInt(testConfig.userId), name: 'Test User', email: testConfig.userEmail, role: role, teams: [], permissions: [] }, workspace: { recentEntities: [] }, personality: { mode: role, features: [], restrictions: {} }, conversation: { mentionedEntities: [], previousOperations: [], intent: 'test' }, config: { apiUrl: testConfig.apiUrl, maxResults: 25, timeout: 30000 } }); // Mock entities for testing const mockTask = { Id: 123, Name: 'Test Task', EntityState: { Name: 'In Progress', IsInitial: false, IsFinal: false, ModifyDate: new Date().toISOString() }, Project: { Id: 1, Name: 'Test Project' }, AssignedUser: { Id: parseInt(testConfig.userId), FirstName: 'Test', LastName: 'User', Items: [{ Id: parseInt(testConfig.userId), FirstName: 'Test', LastName: 'User' }] }, Priority: { Name: 'High', Importance: 1 }, Tags: { Items: [] }, CreateDate: new Date().toISOString(), ModifyDate: new Date().toISOString() }; const mockBug = { ...mockTask, Id: 456, Name: 'Test Bug', Severity: { Name: 'Critical', Importance: 1 } }; const mockBlockedTask = { ...mockTask, Id: 789, Name: 'Blocked Task', Tags: { Items: [{ Name: 'blocked' }] } }; describe('AddCommentOperation', () => { let operation: AddCommentOperation; beforeEach(() => { operation = new AddCommentOperation(mockService); jest.clearAllMocks(); }); describe('metadata', () => { it('should have correct metadata', () => { const metadata = operation.metadata; expect(metadata.id).toBe('add-comment'); expect(metadata.name).toBe('Add Comment'); expect(metadata.description).toContain('smart context awareness'); expect(metadata.category).toBe('collaboration'); expect(metadata.requiredPersonalities).toEqual(['default', 'developer', 'tester', 'project-manager', 'product-owner']); expect(metadata.examples).toBeInstanceOf(Array); expect(metadata.examples.length).toBeGreaterThan(0); expect(metadata.tags).toContain('comment'); }); }); describe('schema validation', () => { it('should validate required parameters', () => { const schema = operation.getSchema(); // Valid params const validParams = { entityType: 'Task', entityId: 123, comment: 'Test comment' }; expect(() => schema.parse(validParams)).not.toThrow(); // Missing required params expect(() => schema.parse({ entityType: 'Task' })).toThrow(); expect(() => schema.parse({ entityId: 123 })).toThrow(); expect(() => schema.parse({ comment: '' })).toThrow(); }); it('should handle optional parameters', () => { const schema = operation.getSchema(); const params = { entityType: 'Task', entityId: 123, comment: 'Test', isPrivate: true, parentCommentId: 456, mentions: ['john', 'jane'], attachments: [{ path: 'file.txt', description: 'Test file' }], useTemplate: 'Bug Fixed', codeLanguage: 'javascript', linkedCommit: 'abc123', linkedPR: 'https://github.com/pr/1' }; const parsed = schema.parse(params); expect(parsed.isPrivate).toBe(true); expect(parsed.mentions).toEqual(['john', 'jane']); expect(parsed.codeLanguage).toBe('javascript'); }); }); describe('execute - basic functionality', () => { it('should successfully add a comment to a task', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); // No special entities found mockService.createComment.mockResolvedValue({ Id: 999, Description: 'Test comment', CreateDate: new Date().toISOString() }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'This is a test comment', isPrivate: false }); expect(mockService.getEntity).toHaveBeenCalledWith('Task', 123, expect.any(Array)); expect(mockService.createComment).toHaveBeenCalled(); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('✅ Comment added to Test Task'); expect(result.suggestions).toBeDefined(); expect(result.suggestions!.length).toBeGreaterThan(0); }); it('should handle entity not found gracefully', async () => { mockService.getEntity.mockResolvedValue(null); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 999, comment: 'Test', isPrivate: false }); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Entity Discovery'); expect(result.content[1].text).toContain('Smart Suggestions'); expect(result.suggestions).toContain('search_entities type:Task - Find available Tasks'); }); it('should handle comment creation failure with helpful guidance', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockRejectedValue(new Error('Comments disabled')); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Test', isPrivate: false }); expect(result.content[0].text).toContain('Comment Creation Discovery'); expect(result.content[1].text).toContain('What we learned'); expect(result.content[1].text).toContain('Comments disabled'); expect(result.suggestions).toContain('show-comments entityType:Task entityId:123 - View existing comments'); }); }); describe('execute - role-based behavior', () => { it('should use developer role formatting and suggestions', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext('developer'), { entityType: 'Task', entityId: 123, comment: 'Fixed the bug', isPrivate: false }); const structuredData = result.content[1].data; expect(structuredData.comment.preview).toContain('Developer Update'); // Developer-specific suggestions const suggestions = result.suggestions!.join(' '); expect(suggestions).toContain('start-working-on'); }); it('should use tester role formatting and suggestions', async () => { mockService.getEntity.mockResolvedValue(mockBug); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext('tester'), { entityType: 'Bug', entityId: 456, comment: 'Verified on staging', isPrivate: false }); const structuredData = result.content[1].data; expect(structuredData.comment.preview).toContain('QA Update'); // Tester-specific suggestions for bugs const suggestions = result.suggestions!.join(' '); expect(suggestions).toContain('attachments:[{path:"screenshot.png"}]'); }); it('should use project-manager role formatting', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext('project-manager'), { entityType: 'Task', entityId: 123, comment: 'Status update', isPrivate: false }); const structuredData = result.content[1].data; expect(structuredData.comment.preview).toContain('Project Update'); }); it('should use product-owner role formatting', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext('product-owner'), { entityType: 'Task', entityId: 123, comment: 'Approved for release', isPrivate: false }); const structuredData = result.content[1].data; expect(structuredData.comment.preview).toContain('Product Update'); }); }); describe('execute - entity context detection', () => { it('should detect blocked entities and provide relevant suggestions', async () => { mockService.getEntity.mockResolvedValue(mockBlockedTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 789, comment: 'Working to unblock this', isPrivate: false }); expect(result.content[0].text).toContain('In Progress 🚧 (Blocked)'); expect(result.suggestions!.some(s => s.includes('escalate-to-manager'))).toBe(true); }); it('should detect overdue entities', async () => { const overdueTask = { ...mockTask, EndDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days ago }; mockService.getEntity.mockResolvedValue(overdueTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Update on delay', isPrivate: false }); expect(result.content[0].text).toContain('⚠️ (Overdue)'); }); it('should handle unassigned entities', async () => { const unassignedTask = { ...mockTask, AssignedUser: null }; mockService.getEntity.mockResolvedValue(unassignedTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Taking this task', isPrivate: false }); expect(result.content[0].text).toContain('This Task is currently unassigned'); // The operation might suggest different actions for unassigned tasks expect(result.suggestions).toBeDefined(); expect(result.suggestions!.length).toBeGreaterThan(0); // Check that at least one suggestion relates to the unassigned state const hasRelevantSuggestion = result.suggestions!.some(s => s.includes('assign') || s.includes('show-comments') || s.includes('start-working') ); expect(hasRelevantSuggestion).toBe(true); }); }); describe('execute - rich text formatting', () => { it('should handle markdown to HTML conversion', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const markdown = '**Bold** *italic* `code` \n- Item 1\n- Item 2'; const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: markdown, isPrivate: false }); // Check that createComment was called with HTML const commentCall = mockService.createComment.mock.calls[0]; const htmlContent = commentCall[1]; expect(htmlContent).toContain('<strong>Bold</strong>'); expect(htmlContent).toContain('<em>italic</em>'); expect(htmlContent).toContain('<code>code</code>'); expect(htmlContent).toContain('Item 1'); expect(htmlContent).toContain('Item 2'); }); it('should handle code blocks with syntax highlighting', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: '```javascript\nconst x = 42;\n```', codeLanguage: 'javascript', isPrivate: false }); const commentCall = mockService.createComment.mock.calls[0]; const htmlContent = commentCall[1]; expect(htmlContent).toContain('data-language="javascript"'); expect(htmlContent).toContain('<pre><code>'); }); it('should handle tables', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const tableMarkdown = ` | Header 1 | Header 2 | |----------|----------| | Cell 1 | Cell 2 |`; const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: tableMarkdown, isPrivate: false }); const commentCall = mockService.createComment.mock.calls[0]; const htmlContent = commentCall[1]; expect(htmlContent).toContain('<table>'); expect(htmlContent).toContain('<th>Header 1</th>'); expect(htmlContent).toContain('<td>Cell 1</td>'); }); }); describe('execute - mentions', () => { it('should resolve user mentions', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities .mockResolvedValueOnce([]) // CommentType search .mockResolvedValueOnce([]) // NotificationRule search .mockResolvedValueOnce([{ // User search for 'john' Id: 555, FirstName: 'John', LastName: 'Doe', Login: 'jdoe', Email: 'john@example.com' }]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Hey @john, please review', mentions: ['john'], isPrivate: false }); // Verify user search was performed expect(mockService.searchEntities).toHaveBeenCalledWith( 'GeneralUser', expect.stringContaining('john'), expect.any(Array), 5 ); // Verify mention was formatted in HTML const commentCall = mockService.createComment.mock.calls[0]; const htmlContent = commentCall[1]; // The semantic operation may not add user data attributes in this version expect(htmlContent).toContain('@john'); }); it('should handle failed mention resolution gracefully', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); // No users found mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Hey @nonexistent, please review', mentions: ['nonexistent'], isPrivate: false }); // Should still create comment even if mention not found expect(mockService.createComment).toHaveBeenCalled(); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('✅ Comment added'); }); }); describe('execute - templates', () => { it('should discover and suggest templates based on context', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); // No template entities mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext('developer'), { entityType: 'Task', entityId: 123, comment: 'Update', isPrivate: false }); // Should suggest using templates expect(result.suggestions!.some(s => s.includes('useTemplate:'))).toBe(true); // Should include available template names in response const structuredData = result.content[1].data; expect(structuredData.templates.available).toBeDefined(); expect(structuredData.templates.count).toBeGreaterThan(0); }); it('should apply template when requested', async () => { mockService.getEntity.mockResolvedValue(mockBug); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); // First get templates to know what's available const templates = await operation.getTemplates('developer', 'Bug', { workflowStage: { isBlocked: false } }); const bugFixedTemplate = templates.find(t => t.name === 'Bug Fixed'); expect(bugFixedTemplate).toBeDefined(); const result = await operation.execute(createMockContext('developer'), { entityType: 'Bug', entityId: 456, comment: 'Memory leak in auth module', useTemplate: 'Bug Fixed', isPrivate: false }); // Verify template was applied const commentCall = mockService.createComment.mock.calls[0]; const htmlContent = commentCall[1]; // Check that the template was applied and content included expect(htmlContent).toContain('Memory leak in auth module'); }); }); describe('execute - performance', () => { it('should complete within 500ms', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const startTime = Date.now(); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Performance test', isPrivate: false }); const executionTime = Date.now() - startTime; expect(executionTime).toBeLessThan(500); // Check execution time in metadata expect(result.metadata?.executionTime).toBeDefined(); expect(result.metadata?.executionTime).toBeLessThan(500); }); }); describe('execute - attachments and links', () => { it('should handle attachments notation', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'See attached logs', attachments: [ { path: 'error.log', description: 'Error logs' }, { path: 'screenshot.png', description: 'UI screenshot' } ], isPrivate: false }); const commentCall = mockService.createComment.mock.calls[0]; const htmlContent = commentCall[1]; expect(htmlContent).toContain('Attachments:'); expect(htmlContent).toContain('Error logs'); expect(htmlContent).toContain('UI screenshot'); const structuredData = result.content[1].data; expect(structuredData.comment.hasAttachments).toBe(true); }); it('should handle linked commits and PRs', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext('developer'), { entityType: 'Task', entityId: 123, comment: 'Fixed in commit:abc123 PR#42', linkedCommit: 'abc123def456', linkedPR: 'https://github.com/org/repo/pull/42', isPrivate: false }); const commentCall = mockService.createComment.mock.calls[0]; const htmlContent = commentCall[1]; // Check that commits and PR links are included expect(htmlContent).toContain('commit'); expect(htmlContent).toContain('PR#42'); }); }); describe('execute - parent/child threading', () => { it('should handle comment replies', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 1000 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'I agree with the above', parentCommentId: 999, isPrivate: false }); expect(mockService.createComment).toHaveBeenCalledWith( 123, expect.any(String), false, 999 // Parent comment ID ); // Check that it's marked as a reply expect(result.content[0].text).toContain('added'); const structuredData = result.content[1].data; expect(structuredData.comment.parentId).toBe(999); }); it('should handle private replies', async () => { mockService.getEntity.mockResolvedValue(mockTask); mockService.searchEntities.mockResolvedValue([]); mockService.createComment.mockResolvedValue({ Id: 1000 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Internal note', parentCommentId: 999, isPrivate: true }); expect(mockService.createComment).toHaveBeenCalledWith( 123, expect.any(String), true, // Private 999 ); // Check that it's marked as private expect(result.content[0].text).toContain('Private'); }); }); describe('execute - dynamic discovery', () => { it('should attempt to discover comment types', async () => { mockService.getEntity.mockResolvedValue(mockTask); // Mock finding comment types mockService.searchEntities .mockResolvedValueOnce([{ // CommentType search Id: 1, Name: 'Technical Discussion', Description: 'For technical discussions' }]) .mockResolvedValueOnce([]); // NotificationRule search mockService.createComment.mockResolvedValue({ Id: 999 }); await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Test', isPrivate: false }); expect(mockService.searchEntities).toHaveBeenCalledWith( 'CommentType', expect.stringContaining('Task'), expect.any(Array), 10 ); }); it('should handle discovery failures gracefully', async () => { mockService.getEntity.mockResolvedValue(mockTask); // Mock discovery failures mockService.searchEntities.mockRejectedValue(new Error('Entity type not found')); mockService.createComment.mockResolvedValue({ Id: 999 }); const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 123, comment: 'Test', isPrivate: false }); // Should still work despite discovery failures expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('✅ Comment added'); }); }); describe('execute - validation errors', () => { it('should provide helpful validation error messages', async () => { const result = await operation.execute(createMockContext(), { entityType: 'Task', entityId: 'not-a-number' as any, comment: '', isPrivate: false }); expect(result.content[0].text).toContain('Validation Error'); expect(result.content[1].text).toContain('Issues found:'); expect(result.content[1].text).toContain('comment:'); expect(result.suggestions).toContain('show-my-tasks - View your tasks to get valid IDs'); }); }); });

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/aaronsb/apptio-target-process-mcp'

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