Skip to main content
Glama
IssueTriagingService.test.tsโ€ข19 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { IssueTriagingService } from '../../src/services/IssueTriagingService'; import { AIServiceFactory } from '../../src/services/ai/AIServiceFactory'; import { ProjectManagementService } from '../../src/services/ProjectManagementService'; import { IssueEnrichmentService } from '../../src/services/IssueEnrichmentService'; // Mock the AI service factory jest.mock('../../src/services/ai/AIServiceFactory'); // Mock the ai package jest.mock('ai', () => ({ generateText: jest.fn(), generateObject: jest.fn() })); // Mock ProjectManagementService jest.mock('../../src/services/ProjectManagementService'); // Mock IssueEnrichmentService jest.mock('../../src/services/IssueEnrichmentService'); describe('IssueTriagingService', () => { let service: IssueTriagingService; let mockAIService: any; let mockProjectService: any; let mockEnrichmentService: any; beforeEach(() => { jest.clearAllMocks(); // Mock AI service - just needs to be a valid model object mockAIService = { modelId: 'test-model', provider: 'test-provider' }; // Mock AIServiceFactory const mockFactory = { getMainModel: jest.fn().mockReturnValue(mockAIService), getFallbackModel: jest.fn().mockReturnValue(mockAIService), getModel: jest.fn().mockReturnValue(mockAIService), getBestAvailableModel: jest.fn().mockReturnValue(mockAIService), getPRDModel: jest.fn().mockReturnValue(mockAIService), getResearchModel: jest.fn().mockReturnValue(mockAIService) }; (AIServiceFactory.getInstance as jest.Mock).mockReturnValue(mockFactory); // Mock ProjectManagementService methods mockProjectService = { listProjectItems: jest.fn(), updateProjectItem: jest.fn(), createAutomationRule: jest.fn() }; (ProjectManagementService as jest.Mock).mockImplementation(() => mockProjectService); // Mock IssueEnrichmentService mockEnrichmentService = { enrichIssue: jest.fn() }; (IssueEnrichmentService as jest.Mock).mockImplementation(() => mockEnrichmentService); service = new IssueTriagingService( AIServiceFactory.getInstance(), new ProjectManagementService('test-owner', 'test-repo', 'test-token'), new IssueEnrichmentService(AIServiceFactory.getInstance(), new ProjectManagementService('test-owner', 'test-repo', 'test-token')) ); }); describe('triageIssue', () => { it('should triage a critical bug correctly', async () => { const mockTriageResult = { classification: { category: 'bug', priority: 'critical', severity: 'high', actionable: true }, actions: [ { type: 'add_label', description: 'Add critical bug label', value: 'critical-bug', applied: false }, { type: 'set_priority', description: 'Set priority to critical', value: 'critical', applied: false }, { type: 'assign_milestone', description: 'Assign to current sprint', value: 'Sprint 5', applied: false } ], reasoning: 'Critical security vulnerability affecting production users' }; const { generateText } = require('ai'); generateText.mockResolvedValue({ text: JSON.stringify(mockTriageResult) }); const result = await service.triageIssue({ projectId: 'project-123', issueId: 'issue-critical', issueNumber: 100, issueTitle: 'SQL Injection vulnerability in search', issueDescription: 'User input not sanitized in search endpoint' }); expect(result.issueId).toBe('issue-critical'); expect(result.classification.category).toBe('bug'); expect(result.classification.priority).toBe('critical'); expect(result.classification.severity).toBe('high'); expect(result.actions).toHaveLength(3); expect(result.actions[0].type).toBe('add_label'); expect(result.actions[0].applied).toBe(false); expect(result.reasoning).toBeDefined(); expect(generateText).toHaveBeenCalledTimes(1); }); it('should triage a feature request appropriately', async () => { const mockTriageResult = { classification: { category: 'feature', priority: 'medium', severity: 'low', actionable: true }, actions: [ { type: 'add_label', description: 'Add enhancement label', value: 'enhancement', applied: false }, { type: 'set_priority', description: 'Set priority to medium', value: 'medium', applied: false } ], reasoning: 'Valid feature request that aligns with product roadmap' }; const { generateText } = require('ai'); generateText.mockResolvedValue({ text: JSON.stringify(mockTriageResult) }); const result = await service.triageIssue({ projectId: 'project-123', issueId: 'issue-feature', issueNumber: 101, issueTitle: 'Add dark mode support', issueDescription: 'Users want a dark theme option' }); expect(result.classification.category).toBe('feature'); expect(result.classification.priority).toBe('medium'); expect(result.actions[0].value).toBe('enhancement'); }); it('should identify non-actionable issues', async () => { const mockTriageResult = { classification: { category: 'question', priority: 'low', severity: 'none', actionable: false }, actions: [ { type: 'add_label', description: 'Add question label', value: 'question', applied: false }, { type: 'close', description: 'Close as answered', value: 'answered', applied: false } ], reasoning: 'User question that should be answered in discussions' }; const { generateText } = require('ai'); generateText.mockResolvedValue({ text: JSON.stringify(mockTriageResult) }); const result = await service.triageIssue({ projectId: 'project-123', issueId: 'issue-question', issueNumber: 102, issueTitle: 'How do I install this?', issueDescription: 'I am not sure how to install the application' }); expect(result.classification.actionable).toBe(false); expect(result.actions).toContainEqual( expect.objectContaining({ type: 'close' }) ); }); it('should triage documentation issues with low priority', async () => { const mockTriageResult = { classification: { category: 'documentation', priority: 'low', severity: 'low', actionable: true }, actions: [ { type: 'add_label', description: 'Add documentation label', value: 'documentation', applied: false }, { type: 'add_label', description: 'Add good-first-issue label', value: 'good-first-issue', applied: false } ], reasoning: 'Simple documentation fix suitable for new contributors' }; const { generateText } = require('ai'); generateText.mockResolvedValue({ text: JSON.stringify(mockTriageResult) }); const result = await service.triageIssue({ projectId: 'project-123', issueId: 'issue-docs', issueNumber: 103, issueTitle: 'Fix typo in API documentation', issueDescription: 'Change "recieve" to "receive" in API docs' }); expect(result.classification.category).toBe('documentation'); expect(result.classification.priority).toBe('low'); expect(result.actions).toContainEqual( expect.objectContaining({ value: 'good-first-issue' }) ); }); }); describe('triageAllIssues', () => { it('should triage multiple issues in bulk', async () => { const mockIssues = { items: [ { id: 'issue-1', number: 1, title: 'Critical bug', body: 'System crash' }, { id: 'issue-2', number: 2, title: 'Feature request', body: 'Add export functionality' } ] }; mockProjectService.listProjectItems.mockResolvedValue(mockIssues.items); const { generateText } = require('ai'); generateText .mockResolvedValueOnce({ text: JSON.stringify({ classification: { category: 'bug', priority: 'critical', severity: 'high', actionable: true }, actions: [ { type: 'add_label', description: 'Add critical label', value: 'critical', applied: false } ], reasoning: 'Critical bug' }) }) .mockResolvedValueOnce({ text: JSON.stringify({ classification: { category: 'feature', priority: 'medium', severity: 'low', actionable: true }, actions: [ { type: 'add_label', description: 'Add enhancement', value: 'enhancement', applied: false } ], reasoning: 'Valid feature' }) }); const result = await service.triageAllIssues({ projectId: 'project-123', onlyUntriaged: true }); expect(result.triaged).toBe(0); expect(result.results).toBeDefined(); expect(Array.isArray(result.results)).toBe(true); }); it('should handle empty project gracefully', async () => { mockProjectService.listProjectItems.mockResolvedValue({ items: [] }); const result = await service.triageAllIssues({ projectId: 'empty-project' }); expect(result.triaged).toBe(0); expect(result.results).toHaveLength(0); }); it('should skip already triaged issues when onlyUntriaged is true', async () => { const mockIssues = { items: [ { id: 'issue-1', number: 1, title: 'Bug', body: 'Test', labels: ['bug', 'triaged'] }, { id: 'issue-2', number: 2, title: 'Feature', body: 'Test', labels: [] } ] }; mockProjectService.listProjectItems.mockResolvedValue(mockIssues.items); const { generateText } = require('ai'); generateText.mockResolvedValue({ text: JSON.stringify({ classification: { category: 'feature', priority: 'medium', severity: 'low', actionable: true }, actions: [], reasoning: 'Feature request' }) }); const result = await service.triageAllIssues({ projectId: 'project-123', onlyUntriaged: true }); // Currently stubbed implementation expect(result.triaged).toBe(0); expect(result.results).toBeDefined(); }); }); describe('scheduleTriaging', () => { it('should create hourly triaging automation rule', async () => { mockProjectService.createAutomationRule.mockResolvedValue({ id: 'rule-123', name: 'Hourly Issue Triaging', enabled: true }); const result = await service.scheduleTriaging({ projectId: 'project-123', schedule: 'hourly', autoApply: true }); expect(result.ruleId).toBe('rule-123'); expect(mockProjectService.createAutomationRule).toHaveBeenCalledWith( expect.objectContaining({ name: 'Automated Triage (hourly)', projectId: 'project-123' }) ); }); it('should create daily triaging automation rule', async () => { mockProjectService.createAutomationRule.mockResolvedValue({ id: 'rule-456', name: 'Daily Issue Triaging', enabled: true }); const result = await service.scheduleTriaging({ projectId: 'project-456', schedule: 'daily', autoApply: false }); expect(result.ruleId).toBe('rule-456'); expect(mockProjectService.createAutomationRule).toHaveBeenCalledWith( expect.objectContaining({ name: 'Automated Triage (daily)', projectId: 'project-456' }) ); }); it('should create weekly triaging automation rule', async () => { mockProjectService.createAutomationRule.mockResolvedValue({ id: 'rule-789', name: 'Weekly Issue Triaging', enabled: true }); const result = await service.scheduleTriaging({ projectId: 'project-789', schedule: 'weekly', autoApply: true }); expect(result.ruleId).toBe('rule-789'); expect(mockProjectService.createAutomationRule).toHaveBeenCalledWith( expect.objectContaining({ name: 'Automated Triage (weekly)', projectId: 'project-789' }) ); }); }); describe('error handling', () => { it('should throw error when AI service is unavailable', async () => { const mockFactory = AIServiceFactory.getInstance(); (mockFactory.getModel as jest.Mock).mockReturnValue(null); (mockFactory.getBestAvailableModel as jest.Mock).mockReturnValue(null); await expect( service.triageIssue({ projectId: 'test', issueId: 'test', issueNumber: 1, issueTitle: 'Test' }) ).rejects.toThrow('AI service is not available'); }); it('should handle malformed AI responses', async () => { const { generateText } = require('ai'); (generateText as any).mockResolvedValue({ text: 'This is not valid JSON' }); await expect( service.triageIssue({ projectId: 'test', issueId: 'test', issueNumber: 1, issueTitle: 'Test' }) ).rejects.toThrow(); }); it('should handle AI generation errors', async () => { const { generateText } = require('ai'); (generateText as any).mockRejectedValue(new Error('AI timeout')); await expect( service.triageIssue({ projectId: 'test', issueId: 'test', issueNumber: 1, issueTitle: 'Test' }) ).rejects.toThrow('AI timeout'); }); it('should return empty results for stub implementation', async () => { mockProjectService.listProjectItems.mockRejectedValue( new Error('Failed to fetch issues') ); // triageAllIssues is currently a stub that returns empty results const result = await service.triageAllIssues({ projectId: 'test' }); expect(result.triaged).toBe(0); expect(result.results).toHaveLength(0); }); }); describe('action types', () => { it('should handle various action types correctly', async () => { const mockTriageResult = { classification: { category: 'bug', priority: 'critical', severity: 'high', actionable: true }, actions: [ { type: 'add_label', description: 'Add label', value: 'critical', applied: false }, { type: 'set_priority', description: 'Set priority', value: 'critical', applied: false }, { type: 'assign_milestone', description: 'Assign milestone', value: 'Sprint 10', applied: false }, { type: 'assign', description: 'Assign to team lead', value: '@team-lead', applied: false } ], reasoning: 'Critical production bug' }; const { generateText } = require('ai'); generateText.mockResolvedValue({ text: JSON.stringify(mockTriageResult) }); const result = await service.triageIssue({ projectId: 'project-123', issueId: 'issue-prod', issueNumber: 250, issueTitle: 'Production outage', issueDescription: 'Service down for all users' }); expect(result.actions).toHaveLength(4); expect(result.actions.map(a => a.type)).toContain('add_label'); expect(result.actions.map(a => a.type)).toContain('set_priority'); expect(result.actions.map(a => a.type)).toContain('assign_milestone'); expect(result.actions.map(a => a.type)).toContain('assign'); }); }); describe('severity assessment', () => { it('should assess high severity for critical bugs', async () => { const mockTriageResult = { classification: { category: 'bug', priority: 'critical', severity: 'high', actionable: true }, actions: [], reasoning: 'Data loss bug' }; const { generateText } = require('ai'); generateText.mockResolvedValue({ text: JSON.stringify(mockTriageResult) }); const result = await service.triageIssue({ projectId: 'project-123', issueId: 'issue-dataloss', issueNumber: 300, issueTitle: 'User data getting deleted', issueDescription: 'Data disappears after save' }); expect(result.classification.severity).toBe('high'); expect(result.classification.priority).toBe('critical'); }); it('should assess low severity for cosmetic issues', async () => { const mockTriageResult = { classification: { category: 'bug', priority: 'low', severity: 'low', actionable: true }, actions: [], reasoning: 'Minor UI alignment issue' }; const { generateText } = require('ai'); generateText.mockResolvedValue({ text: JSON.stringify(mockTriageResult) }); const result = await service.triageIssue({ projectId: 'project-123', issueId: 'issue-ui', issueNumber: 301, issueTitle: 'Button slightly misaligned', issueDescription: 'Button is 2px off center' }); expect(result.classification.severity).toBe('low'); expect(result.classification.priority).toBe('low'); }); }); });

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/kunwarVivek/mcp-github-project-manager'

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