Skip to main content
Glama

Linear MCP Server

by cosmix
linear-api-utils.test.ts9.49 kB
import { describe, test, expect } from 'bun:test'; import { extractMentions, cleanDescription, getComments, getRelationships } from '../linear/utils'; describe('LinearAPIService - Utils', () => { describe('extractMentions', () => { test('extracts issue mentions correctly', () => { const text = 'Related to ABC-123 and DEF-456, also XYZ-789'; const result = extractMentions(text); expect(result.issues).toEqual(['ABC-123', 'DEF-456', 'XYZ-789']); expect(result.users).toEqual([]); }); test('extracts user mentions correctly', () => { const text = 'CC @john and @jane_doe, also @user-123'; const result = extractMentions(text); expect(result.issues).toEqual([]); expect(result.users).toEqual(['john', 'jane_doe', 'user-123']); }); test('extracts both issue and user mentions', () => { const text = 'Related to ABC-123. CC @john and @jane. Also see DEF-456.'; const result = extractMentions(text); expect(result.issues).toEqual(['ABC-123', 'DEF-456']); expect(result.users).toEqual(['john', 'jane']); }); test('deduplicates mentions', () => { const text = 'ABC-123 and ABC-123 again. CC @john and @john again.'; const result = extractMentions(text); expect(result.issues).toEqual(['ABC-123']); expect(result.users).toEqual(['john']); }); test('handles empty or null text', () => { expect(extractMentions('')).toEqual({ issues: [], users: [] }); expect(extractMentions(null)).toEqual({ issues: [], users: [] }); expect(extractMentions(undefined)).toEqual({ issues: [], users: [] }); }); test('handles text with no mentions', () => { const text = 'This is just regular text with no mentions'; const result = extractMentions(text); expect(result.issues).toEqual([]); expect(result.users).toEqual([]); }); }); describe('cleanDescription', () => { test('normalizes whitespace and preserves text content', () => { const text = '# Main Header\n## Sub Header\nRegular text'; const result = cleanDescription(text); // The current implementation collapses whitespace but doesn't fully remove headers expect(result).toBe('# Main Header ## Sub Header Regular text'); }); test('removes bold and italic formatting', () => { const text = '**Bold text** and *italic text* and __bold__ and _italic_'; const result = cleanDescription(text); expect(result).toBe('Bold text and italic text and bold and italic'); }); test('removes inline code formatting', () => { const text = 'Here is some `inline code` in text'; const result = cleanDescription(text); expect(result).toBe('Here is some inline code in text'); }); test('converts markdown links to text', () => { const text = 'Check out [this link](https://example.com) for more info'; const result = cleanDescription(text); expect(result).toBe('Check out this link for more info'); }); test('removes excessive whitespace', () => { const text = 'Text with lots of spaces'; const result = cleanDescription(text); expect(result).toBe('Text with lots of spaces'); }); test('handles complex markdown formatting', () => { const text = ` # Header **Bold** and *italic* text [Link](https://example.com) \`code\` Multiple spaces `; const result = cleanDescription(text); expect(result).toBe('Bold and italic text Link code Multiple spaces'); }); test('handles null and empty strings', () => { expect(cleanDescription(null)).toBe(null); expect(cleanDescription(undefined)).toBe(null); expect(cleanDescription('')).toBe(null); // Empty string becomes null due to the !description check expect(cleanDescription(' ')).toBe(''); // Whitespace gets trimmed to empty string }); test('preserves regular text content', () => { const text = 'This is regular text without any markdown'; const result = cleanDescription(text); expect(result).toBe('This is regular text without any markdown'); }); }); describe('getComments', () => { test('formats comments correctly', async () => { const mockIssue = { comments: () => Promise.resolve({ nodes: [ { id: 'comment-1', body: 'First comment', createdAt: new Date('2025-01-24T10:00:00Z'), updatedAt: new Date('2025-01-24T10:30:00Z'), user: Promise.resolve({ id: 'user-1', name: 'John Doe' }) }, { id: 'comment-2', body: 'Second comment', createdAt: new Date('2025-01-24T11:00:00Z'), updatedAt: new Date('2025-01-24T11:15:00Z'), user: Promise.resolve({ id: 'user-2', name: 'Jane Smith' }) } ] }) }; const result = await getComments(mockIssue as any); expect(result).toEqual([ { id: 'comment-1', body: 'First comment', userId: 'user-1', userName: 'John Doe', createdAt: '2025-01-24T10:00:00.000Z', updatedAt: '2025-01-24T10:30:00.000Z' }, { id: 'comment-2', body: 'Second comment', userId: 'user-2', userName: 'Jane Smith', createdAt: '2025-01-24T11:00:00.000Z', updatedAt: '2025-01-24T11:15:00.000Z' } ]); }); test('handles comments with missing user data', async () => { const mockIssue = { comments: () => Promise.resolve({ nodes: [ { id: 'comment-1', body: 'Comment without user', createdAt: new Date('2025-01-24T10:00:00Z'), updatedAt: new Date('2025-01-24T10:30:00Z'), user: Promise.resolve(null) } ] }) }; const result = await getComments(mockIssue as any); expect(result).toEqual([ { id: 'comment-1', body: 'Comment without user', userId: '', userName: undefined, createdAt: '2025-01-24T10:00:00.000Z', updatedAt: '2025-01-24T10:30:00.000Z' } ]); }); test('handles empty comments list', async () => { const mockIssue = { comments: () => Promise.resolve({ nodes: [] }) }; const result = await getComments(mockIssue as any); expect(result).toEqual([]); }); }); describe('getRelationships', () => { test('extracts all relationship types', async () => { const mockParent = { id: 'parent-1', identifier: 'PAR-1', title: 'Parent Issue' }; const mockChild = { id: 'child-1', identifier: 'CHI-1', title: 'Child Issue' }; const mockRelatedIssue = { id: 'related-1', identifier: 'REL-1', title: 'Related Issue' }; const mockIssue = { parent: Promise.resolve(mockParent), children: () => Promise.resolve({ nodes: [mockChild] }), relations: () => Promise.resolve({ nodes: [ { type: 'RELATED', relatedIssue: Promise.resolve(mockRelatedIssue) }, { type: 'BLOCKED', relatedIssue: Promise.resolve({ id: 'blocked-1', identifier: 'BLO-1', title: 'Blocked Issue' }) } ] }) }; const result = await getRelationships(mockIssue as any); expect(result).toEqual([ { type: 'parent', issueId: 'parent-1', identifier: 'PAR-1', title: 'Parent Issue' }, { type: 'sub', issueId: 'child-1', identifier: 'CHI-1', title: 'Child Issue' }, { type: 'related', issueId: 'related-1', identifier: 'REL-1', title: 'Related Issue' }, { type: 'blocked', issueId: 'blocked-1', identifier: 'BLO-1', title: 'Blocked Issue' } ]); }); test('handles issue with no relationships', async () => { const mockIssue = { parent: Promise.resolve(null), children: () => Promise.resolve({ nodes: [] }), relations: () => Promise.resolve({ nodes: [] }) }; const result = await getRelationships(mockIssue as any); expect(result).toEqual([]); }); test('handles relations with missing related issues', async () => { const mockIssue = { parent: Promise.resolve(null), children: () => Promise.resolve({ nodes: [] }), relations: () => Promise.resolve({ nodes: [ { type: 'RELATED', relatedIssue: Promise.resolve(null) } ] }) }; const result = await getRelationships(mockIssue as any); expect(result).toEqual([]); }); }); });

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/cosmix/linear-mcp'

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