linear-api-utils.test.ts•9.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([]);
});
});
});