Skip to main content
Glama
iceener

Linear Streamable MCP Server

by iceener
get-issues.test.ts11.3 kB
/** * Tests for get_issues tool. * Verifies: input validation, batch fetching, output shape. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getIssuesTool } from '../../src/shared/tools/linear/get-issues.js'; import { createMockLinearClient, resetMockCalls, type MockLinearClient } from '../mocks/linear-client.js'; import type { ToolContext } from '../../src/shared/tools/types.js'; import getIssuesFixtures from '../fixtures/tool-inputs/get-issues.json'; // ───────────────────────────────────────────────────────────────────────────── // Test Setup // ───────────────────────────────────────────────────────────────────────────── let mockClient: MockLinearClient; const baseContext: ToolContext = { sessionId: 'test-session', providerToken: 'test-token', authStrategy: 'bearer', }; // Mock the getLinearClient function vi.mock('../../src/services/linear/client.js', () => ({ getLinearClient: vi.fn(() => Promise.resolve(mockClient)), })); beforeEach(() => { mockClient = createMockLinearClient(); resetMockCalls(mockClient); }); // ───────────────────────────────────────────────────────────────────────────── // Tool Metadata Tests // ───────────────────────────────────────────────────────────────────────────── describe('get_issues tool metadata', () => { it('has correct name and title', () => { expect(getIssuesTool.name).toBe('get_issues'); expect(getIssuesTool.title).toBe('Get Issues (Batch)'); }); it('has readOnlyHint annotation', () => { expect(getIssuesTool.annotations?.readOnlyHint).toBe(true); expect(getIssuesTool.annotations?.destructiveHint).toBe(false); }); }); // ───────────────────────────────────────────────────────────────────────────── // Input Validation Tests // ───────────────────────────────────────────────────────────────────────────── describe('get_issues input validation', () => { describe('valid inputs', () => { for (const fixture of getIssuesFixtures.valid) { it(`accepts: ${fixture.name}`, () => { const result = getIssuesTool.inputSchema.safeParse(fixture.input); expect(result.success).toBe(true); }); } }); describe('invalid inputs', () => { for (const fixture of getIssuesFixtures.invalid) { it(`rejects: ${fixture.name}`, () => { const result = getIssuesTool.inputSchema.safeParse(fixture.input); expect(result.success).toBe(false); }); } }); }); // ───────────────────────────────────────────────────────────────────────────── // Handler Behavior Tests // ───────────────────────────────────────────────────────────────────────────── describe('get_issues handler', () => { it('fetches single issue by UUID', async () => { const result = await getIssuesTool.handler({ ids: ['issue-001'] }, baseContext); expect(result.isError).toBeFalsy(); expect(result.structuredContent).toBeDefined(); const structured = result.structuredContent as Record<string, unknown>; expect(structured.results).toBeDefined(); const results = structured.results as Array<Record<string, unknown>>; expect(results.length).toBe(1); expect(results[0].success).toBe(true); expect(results[0].issue).toBeDefined(); expect((results[0].issue as Record<string, unknown>).id).toBe('issue-001'); // Verify issue() was called expect(mockClient.issue).toHaveBeenCalledWith('issue-001'); }); it('fetches single issue by identifier', async () => { const result = await getIssuesTool.handler({ ids: ['ENG-123'] }, baseContext); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; const results = structured.results as Array<Record<string, unknown>>; expect(results[0].success).toBe(true); expect((results[0].issue as Record<string, unknown>).identifier).toBe('ENG-123'); // Verify issue() was called with identifier expect(mockClient.issue).toHaveBeenCalledWith('ENG-123'); }); it('fetches multiple issues in batch', async () => { const result = await getIssuesTool.handler( { ids: ['issue-001', 'issue-002', 'issue-003'] }, baseContext, ); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; const results = structured.results as Array<Record<string, unknown>>; expect(results.length).toBe(3); expect(mockClient.issue).toHaveBeenCalledTimes(3); }); it('returns issue details in result', async () => { const result = await getIssuesTool.handler({ ids: ['issue-001'] }, baseContext); const structured = result.structuredContent as Record<string, unknown>; const results = structured.results as Array<Record<string, unknown>>; const issue = results[0].issue as Record<string, unknown>; expect(issue).toBeDefined(); expect(issue.id).toBe('issue-001'); expect(issue.title).toBe('Fix authentication bug'); }); it('includes summary with ok/failed counts', async () => { const result = await getIssuesTool.handler( { ids: ['issue-001', 'issue-002'] }, baseContext, ); const structured = result.structuredContent as Record<string, unknown>; const summary = structured.summary as { succeeded: number; failed: number }; expect(summary.succeeded).toBe(2); expect(summary.failed).toBe(0); }); }); // ───────────────────────────────────────────────────────────────────────────── // Output Shape Tests // ───────────────────────────────────────────────────────────────────────────── describe('get_issues output shape', () => { it('matches GetIssuesOutputSchema', async () => { const result = await getIssuesTool.handler({ ids: ['issue-001'] }, baseContext); const structured = result.structuredContent as Record<string, unknown>; expect(structured.results).toBeDefined(); expect(structured.summary).toBeDefined(); const results = structured.results as Array<Record<string, unknown>>; for (const r of results) { expect(typeof r.requestedId).toBe('string'); expect(typeof r.success).toBe('boolean'); if (r.success) { expect(r.issue).toBeDefined(); } } }); it('issue contains expected fields', async () => { const result = await getIssuesTool.handler({ ids: ['issue-001'] }, baseContext); const structured = result.structuredContent as Record<string, unknown>; const results = structured.results as Array<Record<string, unknown>>; const issue = results[0].issue as Record<string, unknown>; // Required identification fields expect(issue.id).toBeDefined(); expect(issue.identifier).toBeDefined(); expect(issue.title).toBeDefined(); // State info (nested object in GetIssueOutputSchema) expect(issue.state).toBeDefined(); const state = issue.state as Record<string, unknown>; expect(state.id).toBeDefined(); expect(state.name).toBeDefined(); // Labels array expect(issue.labels).toBeDefined(); expect(Array.isArray(issue.labels)).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────────── // Error Handling Tests // ───────────────────────────────────────────────────────────────────────────── describe('get_issues error handling', () => { it('handles not found gracefully', async () => { (mockClient.issue as ReturnType<typeof vi.fn>).mockResolvedValueOnce(null); const result = await getIssuesTool.handler({ ids: ['nonexistent'] }, baseContext); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; const results = structured.results as Array<Record<string, unknown>>; expect(results[0].success).toBe(false); expect(results[0].error).toBeDefined(); }); it('continues batch on partial failure', async () => { (mockClient.issue as ReturnType<typeof vi.fn>) .mockResolvedValueOnce(null) // First fails (not found) .mockResolvedValueOnce({ // Second succeeds - full mock with all required async properties id: 'issue-002', identifier: 'ENG-124', title: 'Test', description: null, branchName: null, state: Promise.resolve({ id: 'state-todo', name: 'Todo', type: 'unstarted' }), project: Promise.resolve(null), assignee: Promise.resolve(null), labels: () => Promise.resolve({ nodes: [] }), attachments: () => Promise.resolve({ nodes: [] }), }); const result = await getIssuesTool.handler( { ids: ['bad-id', 'issue-002'] }, baseContext, ); const structured = result.structuredContent as Record<string, unknown>; const summary = structured.summary as { succeeded: number; failed: number }; expect(summary.succeeded).toBe(1); expect(summary.failed).toBe(1); }); it('handles API error gracefully', async () => { (mockClient.issue as ReturnType<typeof vi.fn>).mockRejectedValueOnce( new Error('Network error'), ); const result = await getIssuesTool.handler({ ids: ['issue-001'] }, baseContext); expect(result.isError).toBeFalsy(); const structured = result.structuredContent as Record<string, unknown>; const results = structured.results as Array<Record<string, unknown>>; expect(results[0].success).toBe(false); expect((results[0].error as Record<string, unknown>).message).toContain('Network error'); }); });

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/iceener/linear-streamable-mcp-server'

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