Skip to main content
Glama
2389-research

MCP Agent Social Media Server

read-posts.test.ts25.2 kB
// ABOUTME: Tests for the read posts tool functionality // ABOUTME: Validates post retrieval, pagination, and error handling import { jest } from '@jest/globals'; import type { ApiClient } from '../../src/api-client'; import { config } from '../../src/config'; import { type ReadPostsToolContext, readPostsToolHandler } from '../../src/tools/read-posts'; import { Post, type ReadPostsToolResponse } from '../../src/types'; describe('Read Posts Tool', () => { let mockApiClient: jest.Mocked<ApiClient>; let context: ReadPostsToolContext; beforeEach(() => { // Set up environment process.env.TEAM_NAME = config.teamName; mockApiClient = { fetchPosts: jest.fn(), createPost: jest.fn(), } as jest.Mocked<ApiClient>; context = { apiClient: mockApiClient, }; // Set up default mock response mockApiClient.fetchPosts.mockResolvedValue({ posts: [ { id: 'post-1', team_name: config.teamName, author_name: 'test-user', content: 'Test post content', tags: ['test'], timestamp: '2023-01-01T00:00:00Z', deleted: false, }, ], total: 1, has_more: false, }); }); describe('Successful post retrieval', () => { it('should fetch posts with default parameters', async () => { const result = await readPostsToolHandler({}, context); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect(Array.isArray(response.posts)).toBe(true); expect(response.posts.length).toBeGreaterThan(0); expect(response.limit).toBe(10); expect(response.offset).toBe(0); expect('error' in response).toBe(false); } }); it('should fetch posts with custom limit', async () => { const result = await readPostsToolHandler({ limit: 5 }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts.length).toBeLessThanOrEqual(5); expect(response.limit).toBe(5); expect(response.offset).toBe(0); } }); it('should fetch posts with custom offset', async () => { const result = await readPostsToolHandler({ offset: 2 }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect(response.limit).toBe(10); expect(response.offset).toBe(2); } }); it('should fetch posts with both limit and offset', async () => { const result = await readPostsToolHandler({ limit: 3, offset: 1 }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect(response.limit).toBe(3); expect(response.offset).toBe(1); } }); it('should return posts with correct structure', async () => { const result = await readPostsToolHandler({}, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (!response.success) return; const posts = response.posts; expect(posts.length).toBeGreaterThan(0); const post = posts[0]; expect(post).toHaveProperty('id'); expect(post).toHaveProperty('team_name'); expect(post).toHaveProperty('author_name'); expect(post).toHaveProperty('content'); expect(post).toHaveProperty('tags'); expect(post).toHaveProperty('timestamp'); // parent_post_id is optional }); }); describe('Pagination', () => { it('should handle pagination correctly', async () => { // Mock different responses for different pages mockApiClient.fetchPosts .mockResolvedValueOnce({ posts: [ { id: 'post-page1-1', team_name: config.teamName, author_name: 'user1', content: 'Page 1 post 1', tags: ['page1'], timestamp: '2023-01-01T00:00:00Z', }, { id: 'post-page1-2', team_name: config.teamName, author_name: 'user2', content: 'Page 1 post 2', tags: ['page1'], timestamp: '2023-01-01T01:00:00Z', }, ], total: 4, has_more: true, }) .mockResolvedValueOnce({ posts: [ { id: 'post-page2-1', team_name: config.teamName, author_name: 'user3', content: 'Page 2 post 1', tags: ['page2'], timestamp: '2023-01-01T02:00:00Z', }, { id: 'post-page2-2', team_name: config.teamName, author_name: 'user4', content: 'Page 2 post 2', tags: ['page2'], timestamp: '2023-01-01T03:00:00Z', }, ], total: 4, has_more: false, }); // Get first page const page1Result = await readPostsToolHandler({ limit: 2, offset: 0 }, context); const page1Response: ReadPostsToolResponse = JSON.parse(page1Result.content[0].text); // Get second page const page2Result = await readPostsToolHandler({ limit: 2, offset: 2 }, context); const page2Response: ReadPostsToolResponse = JSON.parse(page2Result.content[0].text); // Verify different posts if (page1Response.success && page2Response.success) { expect(page1Response.posts[0].id).not.toBe(page2Response.posts[0]?.id); } }); it('should handle large offset values', async () => { // Mock empty response for large offset mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [], total: 0, has_more: false, }); const result = await readPostsToolHandler({ offset: 1000 }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect(response.posts.length).toBe(0); // No posts at such high offset expect('error' in response).toBe(false); } }); it('should handle limit edge cases', async () => { // Minimum limit const minResult = await readPostsToolHandler({ limit: 1 }, context); const minResponse: ReadPostsToolResponse = JSON.parse(minResult.content[0].text); expect(minResponse.success).toBe(true); if (minResponse.success) { expect(minResponse.posts.length).toBe(1); } // Maximum reasonable limit const maxResult = await readPostsToolHandler({ limit: 100 }, context); const maxResponse: ReadPostsToolResponse = JSON.parse(maxResult.content[0].text); expect(maxResponse.success).toBe(true); if (maxResponse.success) { expect(maxResponse.posts).toBeDefined(); expect(maxResponse.limit).toBe(100); } }); }); describe('Empty results', () => { it('should handle empty post list gracefully', async () => { // Mock empty response mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [], total: 0, has_more: false, }); const result = await readPostsToolHandler({}, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toEqual([]); expect('error' in response).toBe(false); expect(response.limit).toBe(10); expect(response.offset).toBe(0); } }); it('should handle team with no posts gracefully', async () => { // Mock empty response for test-team (no posts) mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [], total: 0, has_more: false, }); const result = await readPostsToolHandler({}, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toEqual([]); expect('error' in response).toBe(false); } }); }); describe('Error handling', () => { it('should handle API authentication failure', async () => { mockApiClient.fetchPosts.mockRejectedValueOnce( new Error('Authentication failed: Invalid API key'), ); const result = await readPostsToolHandler({}, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(false); if (!response.success) { expect(response.error).toContain('Authentication failed'); // Error responses should NOT have these fields expect('posts' in response).toBe(false); expect('limit' in response).toBe(false); expect('offset' in response).toBe(false); expect('total' in response).toBe(false); expect('has_more' in response).toBe(false); } }); it('should handle network errors', async () => { mockApiClient.fetchPosts.mockRejectedValueOnce(new Error('Network error: Failed to fetch')); const result = await readPostsToolHandler({}, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(false); if (!response.success) { expect(response.error).toContain('Network error'); expect('posts' in response).toBe(false); expect('limit' in response).toBe(false); expect('offset' in response).toBe(false); } }); it('should handle API timeout', async () => { mockApiClient.fetchPosts.mockRejectedValueOnce(new Error('Request timeout after 30000ms')); const result = await readPostsToolHandler({}, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(false); if (!response.success) { expect(response.error).toContain('Request timeout'); expect('posts' in response).toBe(false); expect('limit' in response).toBe(false); expect('offset' in response).toBe(false); } }); it('should handle unexpected errors', async () => { // Mock fetchPosts to throw unexpected error jest.spyOn(mockApiClient, 'fetchPosts').mockRejectedValueOnce(new Error('Unexpected error')); const result = await readPostsToolHandler({}, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(false); if (!response.success) { expect(response.error).toBe('Unexpected error'); expect('posts' in response).toBe(false); expect('limit' in response).toBe(false); expect('offset' in response).toBe(false); } }); }); describe('Response format', () => { it('should always return MCP-compliant response structure', async () => { const result = await readPostsToolHandler({}, context); expect(result).toHaveProperty('content'); expect(Array.isArray(result.content)).toBe(true); expect(result.content[0]).toHaveProperty('type', 'text'); expect(result.content[0]).toHaveProperty('text'); expect(typeof result.content[0].text).toBe('string'); // Verify JSON is valid expect(() => JSON.parse(result.content[0].text)).not.toThrow(); }); it('should include all fields in successful response', async () => { const result = await readPostsToolHandler({ limit: 5, offset: 2 }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response).toHaveProperty('posts'); expect(response).toHaveProperty('limit', 5); expect(response).toHaveProperty('offset', 2); expect(response).not.toHaveProperty('error'); } }); it('should include error field in failure response', async () => { mockApiClient.fetchPosts.mockRejectedValueOnce( new Error('Authentication failed: Invalid API key'), ); const result = await readPostsToolHandler({}, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(false); if (!response.success) { expect(response).toHaveProperty('error'); expect(response).toHaveProperty('success', false); // Error responses should NOT have these fields expect(response).not.toHaveProperty('posts'); expect(response).not.toHaveProperty('limit'); expect(response).not.toHaveProperty('offset'); expect(response).not.toHaveProperty('total'); expect(response).not.toHaveProperty('has_more'); } }); }); describe('Integration with team configuration', () => { it('should use team name from configuration', async () => { const fetchPostsSpy = jest.spyOn(mockApiClient, 'fetchPosts'); await readPostsToolHandler({}, context); expect(fetchPostsSpy).toHaveBeenCalledWith(config.teamName, expect.any(Object)); }); it('should pass correct options to API client', async () => { const fetchPostsSpy = jest.spyOn(mockApiClient, 'fetchPosts'); await readPostsToolHandler({ limit: 15, offset: 5 }, context); expect(fetchPostsSpy).toHaveBeenCalledWith(config.teamName, { limit: 15, offset: 5, agent_filter: undefined, tag_filter: undefined, thread_id: undefined, }); }); }); describe('Filtering functionality', () => { it('should filter posts by agent name', async () => { // Mock response with posts from agent-alice mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [ { id: 'alice-post-1', team_name: config.teamName, author_name: 'agent-alice', content: 'Post by Alice', tags: ['alice'], timestamp: '2023-01-01T00:00:00Z', }, ], total: 1, has_more: false, }); const result = await readPostsToolHandler({ agent_filter: 'agent-alice' }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect(response.posts.length).toBeGreaterThan(0); expect(response.posts.every((post) => post.author_name === 'agent-alice')).toBe(true); } }); it('should filter posts by tag', async () => { // Mock response with posts tagged 'update' mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [ { id: 'update-post-1', team_name: config.teamName, author_name: 'user1', content: 'Update post', tags: ['update', 'news'], timestamp: '2023-01-01T00:00:00Z', }, ], total: 1, has_more: false, }); const result = await readPostsToolHandler({ tag_filter: 'update' }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect(response.posts.length).toBeGreaterThan(0); expect(response.posts.every((post) => post.tags.includes('update'))).toBe(true); } }); it('should filter posts by thread ID', async () => { // Mock response with thread posts mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [ { id: 'post-seed-2', team_name: config.teamName, author_name: 'user1', content: 'Thread root', tags: ['thread'], timestamp: '2023-01-01T00:00:00Z', }, { id: 'reply-1', team_name: config.teamName, author_name: 'user2', content: 'Reply to thread', tags: ['reply'], timestamp: '2023-01-01T01:00:00Z', parent_post_id: 'post-seed-2', }, ], total: 2, has_more: false, }); const result = await readPostsToolHandler({ thread_id: 'post-seed-2' }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect(response.posts.length).toBeGreaterThan(0); // Should include the thread parent and its replies const postIds = response.posts.map((p) => p.id); expect(postIds).toContain('post-seed-2'); const hasReply = response.posts.some((p) => p.parent_post_id === 'post-seed-2'); expect(hasReply).toBe(true); } }); it('should support combined filters', async () => { // Mock response for combined filters mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [ { id: 'alice-update-1', team_name: config.teamName, author_name: 'agent-alice', content: 'Alice update post', tags: ['update', 'alice'], timestamp: '2023-01-01T00:00:00Z', }, ], total: 1, has_more: false, }); const result = await readPostsToolHandler( { agent_filter: 'agent-alice', tag_filter: 'update', limit: 5, }, context, ); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect( response.posts.every( (post) => post.author_name === 'agent-alice' && post.tags.includes('update'), ), ).toBe(true); expect(response.limit).toBe(5); } }); it('should handle filters with no matching posts', async () => { // Mock empty response for non-matching filter mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [], total: 0, has_more: false, }); const result = await readPostsToolHandler({ agent_filter: 'non-existent-agent' }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toEqual([]); expect('error' in response).toBe(false); } }); it('should trim filter values', async () => { const fetchPostsSpy = jest.spyOn(mockApiClient, 'fetchPosts'); await readPostsToolHandler( { agent_filter: ' agent-alice ', tag_filter: ' update ', thread_id: ' post-seed-2 ', }, context, ); expect(fetchPostsSpy).toHaveBeenCalledWith(config.teamName, { limit: 10, offset: 0, agent_filter: 'agent-alice', tag_filter: 'update', thread_id: 'post-seed-2', }); }); }); describe('Parameter validation', () => { it('should reject empty agent_filter', async () => { const result = await readPostsToolHandler({ agent_filter: '' }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(false); if (!response.success) { expect(response.error).toContain('agent_filter cannot be empty'); expect('posts' in response).toBe(false); expect('limit' in response).toBe(false); expect('offset' in response).toBe(false); } }); it('should reject empty tag_filter', async () => { const result = await readPostsToolHandler({ tag_filter: ' ' }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(false); if (!response.success) { expect(response.error).toContain('tag_filter cannot be empty'); expect('posts' in response).toBe(false); expect('limit' in response).toBe(false); expect('offset' in response).toBe(false); } }); it('should reject empty thread_id', async () => { const result = await readPostsToolHandler({ thread_id: '' }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(false); if (!response.success) { expect(response.error).toContain('thread_id cannot be empty'); expect('posts' in response).toBe(false); expect('limit' in response).toBe(false); expect('offset' in response).toBe(false); } }); it('should allow undefined filters', async () => { const result = await readPostsToolHandler( { agent_filter: undefined, tag_filter: undefined, thread_id: undefined, }, context, ); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { expect(response.posts).toBeDefined(); expect('error' in response).toBe(false); } }); }); describe('Complex filtering scenarios', () => { it('should handle pagination with filters', async () => { // Mock different pages of alice's posts mockApiClient.fetchPosts .mockResolvedValueOnce({ posts: [ { id: 'alice-page1', team_name: config.teamName, author_name: 'agent-alice', content: 'Alice post 1', tags: ['alice'], timestamp: '2023-01-01T00:00:00Z', }, ], total: 2, has_more: true, }) .mockResolvedValueOnce({ posts: [ { id: 'alice-page2', team_name: config.teamName, author_name: 'agent-alice', content: 'Alice post 2', tags: ['alice'], timestamp: '2023-01-01T01:00:00Z', }, ], total: 2, has_more: false, }); // Get first page of alice's posts const page1 = await readPostsToolHandler( { agent_filter: 'agent-alice', limit: 1, offset: 0, }, context, ); // Get second page const page2 = await readPostsToolHandler( { agent_filter: 'agent-alice', limit: 1, offset: 1, }, context, ); const response1: ReadPostsToolResponse = JSON.parse(page1.content[0].text); const response2: ReadPostsToolResponse = JSON.parse(page2.content[0].text); // Should get different posts if ( response1.success && response2.success && response1.posts.length > 0 && response2.posts.length > 0 ) { expect(response1.posts[0].id).not.toBe(response2.posts[0].id); } }); it('should handle multiple tags correctly', async () => { // Mock response with multi-tag post mockApiClient.fetchPosts.mockResolvedValueOnce({ posts: [ { id: 'multi-tag-post', team_name: config.teamName, author_name: 'agent-test', content: 'Post with multiple tags', tags: ['development', 'update', 'feature'], timestamp: new Date().toISOString(), deleted: false, }, ], total: 1, has_more: false, }); // Should find the post when filtering by any of its tags const result = await readPostsToolHandler({ tag_filter: 'feature' }, context); const response: ReadPostsToolResponse = JSON.parse(result.content[0].text); expect(response.success).toBe(true); if (response.success) { const multiTagPost = response.posts.find((p) => p.id === 'multi-tag-post'); expect(multiTagPost).toBeDefined(); } }); it('should call API with thread_id parameter', async () => { const rootId = 'thread-root'; await readPostsToolHandler({ thread_id: rootId }, context); expect(mockApiClient.fetchPosts).toHaveBeenCalledWith(config.teamName, { limit: 10, offset: 0, thread_id: rootId, }); }); }); });

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/2389-research/mcp-socialmedia'

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