Skip to main content
Glama
2389-research

MCP Agent Social Media Server

roots.test.ts30.2 kB
// ABOUTME: Unit tests for roots system and workspace boundaries // ABOUTME: Tests RootsManager functionality, session assignments, and MCP resource registration import { jest } from '@jest/globals'; // Mock logger jest.mock('../src/logger.js', () => ({ logger: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn(), }, })); import { RootsManager, registerRoots } from '../src/roots/index.js'; import type { RootDefinition, RootLimits, RootPermissions } from '../src/roots/types.js'; // Test type interfaces interface MockServer { resource: jest.MockedFunction<(...args: any[]) => any>; } interface MockContext { apiClient: object; sessionManager: object; rootsManager?: RootsManager; } describe('Roots System', () => { let rootsManager: RootsManager; let mockServer: MockServer; let mockContext: MockContext; beforeEach(() => { jest.clearAllMocks(); rootsManager = new RootsManager(); mockServer = { resource: jest.fn(), } as MockServer; mockContext = { apiClient: {}, sessionManager: {}, } as MockContext; }); describe('RootsManager', () => { describe('constructor', () => { it('should initialize with default workspace root', () => { expect(rootsManager).toBeInstanceOf(RootsManager); const defaultRoot = rootsManager.getRootForSession('any-session'); expect(defaultRoot).toBeDefined(); expect(defaultRoot?.uri).toBe('social://workspace'); expect(defaultRoot?.name).toBe('Social Media Workspace'); }); it('should set up default permissions and limits', () => { const defaultRoot = rootsManager.getRootForSession('test-session'); expect(defaultRoot?.limits).toEqual( expect.objectContaining({ maxPostsPerHour: 10, maxReadRequestsPerMinute: 30, maxConcurrentSessions: 5, maxContentLength: 2000, rateLimitWindow: 3600000, }), ); expect(defaultRoot?.permissions).toEqual( expect.objectContaining({ canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: true, canUsePrompts: true, canUseSampling: true, }), ); }); it('should include allowed operations', () => { const defaultRoot = rootsManager.getRootForSession('test-session'); expect(defaultRoot?.limits.allowedOperations).toEqual( expect.arrayContaining(['read_posts', 'create_post', 'login']), ); }); }); describe('getRootForSession', () => { it('should return default root for unassigned session', () => { const root = rootsManager.getRootForSession('unassigned-session'); expect(root).toBeDefined(); expect(root?.uri).toBe('social://workspace'); }); it('should return assigned root for session', () => { const customRoot: RootDefinition = { uri: 'social://custom-workspace', name: 'Custom Workspace', description: 'Test workspace', limits: { maxPostsPerHour: 5, maxReadRequestsPerMinute: 15, maxConcurrentSessions: 2, allowedOperations: ['read_posts'], maxContentLength: 1000, rateLimitWindow: 1800000, }, permissions: { canCreatePosts: false, canReadPosts: true, canAccessFeed: false, canAccessAgentProfiles: false, canUsePrompts: false, canUseSampling: false, }, }; rootsManager.addRoot(customRoot); rootsManager.assignRootToSession('test-session', 'social://custom-workspace'); const assignedRoot = rootsManager.getRootForSession('test-session'); expect(assignedRoot?.uri).toBe('social://custom-workspace'); expect(assignedRoot?.name).toBe('Custom Workspace'); }); it('should return undefined for non-existent root assignment', () => { rootsManager.assignRootToSession('test-session', 'social://non-existent'); // Should fall back to default since assignment failed const root = rootsManager.getRootForSession('test-session'); expect(root?.uri).toBe('social://workspace'); }); }); describe('assignRootToSession', () => { it('should successfully assign existing root to session', () => { const result = rootsManager.assignRootToSession('test-session', 'social://workspace'); expect(result).toBe(true); const assignedRoot = rootsManager.getRootForSession('test-session'); expect(assignedRoot?.uri).toBe('social://workspace'); }); it('should fail to assign non-existent root', () => { const result = rootsManager.assignRootToSession('test-session', 'social://non-existent'); expect(result).toBe(false); // Session should still get default root const root = rootsManager.getRootForSession('test-session'); expect(root?.uri).toBe('social://workspace'); }); it('should overwrite previous root assignment', () => { const customRoot: RootDefinition = { uri: 'social://new-workspace', name: 'New Workspace', description: 'Another test workspace', limits: { maxPostsPerHour: 20, maxReadRequestsPerMinute: 60, maxConcurrentSessions: 10, allowedOperations: ['read_posts', 'create_post'], maxContentLength: 3000, rateLimitWindow: 7200000, }, permissions: { canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: true, canUsePrompts: true, canUseSampling: false, }, }; rootsManager.addRoot(customRoot); // First assignment rootsManager.assignRootToSession('test-session', 'social://workspace'); expect(rootsManager.getRootForSession('test-session')?.uri).toBe('social://workspace'); // Second assignment should overwrite rootsManager.assignRootToSession('test-session', 'social://new-workspace'); expect(rootsManager.getRootForSession('test-session')?.uri).toBe('social://new-workspace'); }); }); describe('isOperationAllowed', () => { it('should allow operations in allowedOperations list', () => { const allowed = rootsManager.isOperationAllowed('test-session', 'read_posts'); expect(allowed).toBe(true); const alsoAllowed = rootsManager.isOperationAllowed('test-session', 'create_post'); expect(alsoAllowed).toBe(true); }); it('should deny operations not in allowedOperations list', () => { const denied = rootsManager.isOperationAllowed('test-session', 'delete_post'); expect(denied).toBe(false); const alsoDenied = rootsManager.isOperationAllowed('test-session', 'admin_operation'); expect(alsoDenied).toBe(false); }); it('should deny all operations for session without root', () => { // Create a session with non-existent root rootsManager.assignRootToSession('orphan-session', 'social://non-existent'); // Since assignRootToSession returns false for non-existent roots, // the session will fall back to default root const denied = rootsManager.isOperationAllowed('orphan-session', 'read_posts'); expect(denied).toBe(true); // Falls back to default root which allows read_posts }); it('should respect custom root operation restrictions', () => { const restrictiveRoot: RootDefinition = { uri: 'social://restrictive', name: 'Restrictive Workspace', description: 'Read-only workspace', limits: { maxPostsPerHour: 0, maxReadRequestsPerMinute: 10, maxConcurrentSessions: 1, allowedOperations: ['read_posts'], maxContentLength: 500, rateLimitWindow: 3600000, }, permissions: { canCreatePosts: false, canReadPosts: true, canAccessFeed: false, canAccessAgentProfiles: false, canUsePrompts: false, canUseSampling: false, }, }; rootsManager.addRoot(restrictiveRoot); rootsManager.assignRootToSession('restricted-session', 'social://restrictive'); expect(rootsManager.isOperationAllowed('restricted-session', 'read_posts')).toBe(true); expect(rootsManager.isOperationAllowed('restricted-session', 'create_post')).toBe(false); expect(rootsManager.isOperationAllowed('restricted-session', 'login')).toBe(false); }); }); describe('isContentLengthValid', () => { it('should allow content within length limits', () => { const valid = rootsManager.isContentLengthValid('test-session', 100); expect(valid).toBe(true); const atLimit = rootsManager.isContentLengthValid('test-session', 2000); expect(atLimit).toBe(true); }); it('should deny content exceeding length limits', () => { const invalid = rootsManager.isContentLengthValid('test-session', 2001); expect(invalid).toBe(false); const wayTooLong = rootsManager.isContentLengthValid('test-session', 10000); expect(wayTooLong).toBe(false); }); it('should deny content for session without root', () => { // Force a session with no valid root rootsManager.assignRootToSession('orphan-session', 'social://non-existent'); // Since assignRootToSession returns false for non-existent roots, // the session will fall back to default root const denied = rootsManager.isContentLengthValid('orphan-session', 100); expect(denied).toBe(true); // Falls back to default root with maxContentLength 2000 }); it('should respect custom root content length limits', () => { const shortLimitRoot: RootDefinition = { uri: 'social://short-content', name: 'Short Content Workspace', description: 'Workspace with short content limits', limits: { maxPostsPerHour: 10, maxReadRequestsPerMinute: 30, maxConcurrentSessions: 5, allowedOperations: ['read_posts', 'create_post'], maxContentLength: 280, // Twitter-style limit rateLimitWindow: 3600000, }, permissions: { canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: true, canUsePrompts: true, canUseSampling: true, }, }; rootsManager.addRoot(shortLimitRoot); rootsManager.assignRootToSession('short-session', 'social://short-content'); expect(rootsManager.isContentLengthValid('short-session', 280)).toBe(true); expect(rootsManager.isContentLengthValid('short-session', 281)).toBe(false); }); }); describe('getAllRoots', () => { it('should return all registered roots', () => { const roots = rootsManager.getAllRoots(); expect(roots).toHaveLength(1); // Default root expect(roots[0].uri).toBe('social://workspace'); }); it('should return all roots after adding custom ones', () => { const customRoot1: RootDefinition = { uri: 'social://custom1', name: 'Custom 1', description: 'First custom root', limits: { maxPostsPerHour: 5, maxReadRequestsPerMinute: 15, maxConcurrentSessions: 2, allowedOperations: ['read_posts'], maxContentLength: 1000, rateLimitWindow: 1800000, }, permissions: { canCreatePosts: false, canReadPosts: true, canAccessFeed: false, canAccessAgentProfiles: false, canUsePrompts: false, canUseSampling: false, }, }; const customRoot2: RootDefinition = { uri: 'social://custom2', name: 'Custom 2', description: 'Second custom root', limits: { maxPostsPerHour: 15, maxReadRequestsPerMinute: 45, maxConcurrentSessions: 8, allowedOperations: ['read_posts', 'create_post', 'delete_post'], maxContentLength: 5000, rateLimitWindow: 3600000, }, permissions: { canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: true, canUsePrompts: true, canUseSampling: true, }, }; rootsManager.addRoot(customRoot1); rootsManager.addRoot(customRoot2); const allRoots = rootsManager.getAllRoots(); expect(allRoots).toHaveLength(3); const uris = allRoots.map((r: RootDefinition) => r.uri); expect(uris).toContain('social://workspace'); expect(uris).toContain('social://custom1'); expect(uris).toContain('social://custom2'); }); }); describe('addRoot', () => { it('should add new root successfully', () => { const newRoot: RootDefinition = { uri: 'social://test-workspace', name: 'Test Workspace', description: 'Workspace for testing', limits: { maxPostsPerHour: 25, maxReadRequestsPerMinute: 100, maxConcurrentSessions: 15, allowedOperations: ['read_posts', 'create_post', 'admin_operation'], maxContentLength: 4000, rateLimitWindow: 1800000, }, permissions: { canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: true, canUsePrompts: true, canUseSampling: true, }, }; rootsManager.addRoot(newRoot); const allRoots = rootsManager.getAllRoots(); expect(allRoots).toHaveLength(2); const addedRoot = allRoots.find((r) => r.uri === 'social://test-workspace'); expect(addedRoot).toBeDefined(); expect(addedRoot?.name).toBe('Test Workspace'); expect(addedRoot?.limits.maxPostsPerHour).toBe(25); }); it('should overwrite existing root with same URI', () => { const originalRoot: RootDefinition = { uri: 'social://duplicate', name: 'Original Name', description: 'Original description', limits: { maxPostsPerHour: 5, maxReadRequestsPerMinute: 10, maxConcurrentSessions: 1, allowedOperations: ['read_posts'], maxContentLength: 500, rateLimitWindow: 3600000, }, permissions: { canCreatePosts: false, canReadPosts: true, canAccessFeed: false, canAccessAgentProfiles: false, canUsePrompts: false, canUseSampling: false, }, }; const updatedRoot: RootDefinition = { uri: 'social://duplicate', name: 'Updated Name', description: 'Updated description', limits: { maxPostsPerHour: 15, maxReadRequestsPerMinute: 30, maxConcurrentSessions: 5, allowedOperations: ['read_posts', 'create_post'], maxContentLength: 2000, rateLimitWindow: 1800000, }, permissions: { canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: true, canUsePrompts: true, canUseSampling: true, }, }; rootsManager.addRoot(originalRoot); expect(rootsManager.getAllRoots()).toHaveLength(2); // Default + original rootsManager.addRoot(updatedRoot); expect(rootsManager.getAllRoots()).toHaveLength(2); // Default + updated (overwrote original) const finalRoot = rootsManager.getAllRoots().find((r) => r.uri === 'social://duplicate'); expect(finalRoot?.name).toBe('Updated Name'); expect(finalRoot?.limits.maxPostsPerHour).toBe(15); }); }); describe('clearSessionRoot', () => { it('should remove session root assignment', () => { // Assign a root to session rootsManager.assignRootToSession('test-session', 'social://workspace'); expect(rootsManager.getRootForSession('test-session')?.uri).toBe('social://workspace'); // Clear the assignment rootsManager.clearSessionRoot('test-session'); // Should fall back to default root expect(rootsManager.getRootForSession('test-session')?.uri).toBe('social://workspace'); }); it('should handle clearing non-existent session gracefully', () => { // Should not throw error expect(() => { rootsManager.clearSessionRoot('non-existent-session'); }).not.toThrow(); }); it('should only affect target session', () => { const customRoot: RootDefinition = { uri: 'social://custom', name: 'Custom', description: 'Custom workspace', limits: { maxPostsPerHour: 5, maxReadRequestsPerMinute: 15, maxConcurrentSessions: 2, allowedOperations: ['read_posts'], maxContentLength: 1000, rateLimitWindow: 1800000, }, permissions: { canCreatePosts: false, canReadPosts: true, canAccessFeed: false, canAccessAgentProfiles: false, canUsePrompts: false, canUseSampling: false, }, }; rootsManager.addRoot(customRoot); // Assign custom root to both sessions rootsManager.assignRootToSession('session1', 'social://custom'); rootsManager.assignRootToSession('session2', 'social://custom'); // Clear only session1 rootsManager.clearSessionRoot('session1'); // session1 should fall back to default, session2 should keep custom expect(rootsManager.getRootForSession('session1')?.uri).toBe('social://workspace'); expect(rootsManager.getRootForSession('session2')?.uri).toBe('social://custom'); }); }); }); describe('registerRoots', () => { it('should register roots resource with MCP server', () => { const returnedManager = registerRoots(mockServer, mockContext); expect(mockServer.resource).toHaveBeenCalledTimes(1); expect(mockServer.resource).toHaveBeenCalledWith( 'workspace-roots', 'social://roots', expect.objectContaining({ description: 'Available workspace boundaries and operational limits', mimeType: 'application/json', }), expect.any(Function), ); expect(returnedManager).toBeInstanceOf(RootsManager); }); it('should add roots manager to context', () => { registerRoots(mockServer, mockContext); expect(mockContext.rootsManager).toBeInstanceOf(RootsManager); }); it('should create working roots resource handler', async () => { registerRoots(mockServer, mockContext); // Get the registered handler function const resourceHandler = mockServer.resource.mock.calls[0][3]; // Call the handler const result = await resourceHandler(); expect(result.contents).toHaveLength(1); expect(result.contents[0].uri).toBe('social://roots'); expect(result.contents[0].mimeType).toBe('application/json'); const content = JSON.parse(result.contents[0].text); expect(content.roots).toBeInstanceOf(Array); expect(content.roots).toHaveLength(1); // Default root expect(content.roots[0]).toEqual( expect.objectContaining({ uri: 'social://workspace', name: 'Social Media Workspace', description: 'Default workspace for social media operations', }), ); }); it('should handle multiple roots in resource response', async () => { const manager = registerRoots(mockServer, mockContext); // Add additional roots manager.addRoot({ uri: 'social://premium', name: 'Premium Workspace', description: 'Premium features workspace', limits: { maxPostsPerHour: 50, maxReadRequestsPerMinute: 100, maxConcurrentSessions: 20, allowedOperations: ['read_posts', 'create_post', 'premium_feature'], maxContentLength: 5000, rateLimitWindow: 3600000, }, permissions: { canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: true, canUsePrompts: true, canUseSampling: true, }, }); const resourceHandler = mockServer.resource.mock.calls[0][3]; const result = await resourceHandler(); const content = JSON.parse(result.contents[0].text); expect(content.roots).toHaveLength(2); const uris = content.roots.map((r: RootDefinition) => r.uri); expect(uris).toContain('social://workspace'); expect(uris).toContain('social://premium'); }); it('should handle resource handler errors gracefully', async () => { const manager = registerRoots(mockServer, mockContext); // Mock getAllRoots to throw an error jest.spyOn(manager, 'getAllRoots').mockImplementation(() => { throw new Error('Test error'); }); const resourceHandler = mockServer.resource.mock.calls[0][3]; await expect(resourceHandler()).rejects.toThrow('Test error'); }); }); describe('Integration Tests', () => { it('should support complete session workflow', () => { const manager = registerRoots(mockServer, mockContext); // Add custom workspace const customRoot: RootDefinition = { uri: 'social://workflow-test', name: 'Workflow Test', description: 'Testing complete workflow', limits: { maxPostsPerHour: 8, maxReadRequestsPerMinute: 25, maxConcurrentSessions: 3, allowedOperations: ['read_posts', 'create_post'], maxContentLength: 1500, rateLimitWindow: 3600000, }, permissions: { canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: false, canUsePrompts: false, canUseSampling: false, }, }; manager.addRoot(customRoot); // Session starts with default root expect(manager.getRootForSession('workflow-session')?.uri).toBe('social://workspace'); expect(manager.isOperationAllowed('workflow-session', 'login')).toBe(true); // Assign custom root const assigned = manager.assignRootToSession('workflow-session', 'social://workflow-test'); expect(assigned).toBe(true); // Verify new constraints expect(manager.getRootForSession('workflow-session')?.uri).toBe('social://workflow-test'); expect(manager.isOperationAllowed('workflow-session', 'read_posts')).toBe(true); expect(manager.isOperationAllowed('workflow-session', 'create_post')).toBe(true); expect(manager.isOperationAllowed('workflow-session', 'login')).toBe(false); // Not in custom allowed operations expect(manager.isContentLengthValid('workflow-session', 1500)).toBe(true); expect(manager.isContentLengthValid('workflow-session', 1501)).toBe(false); // Clear assignment manager.clearSessionRoot('workflow-session'); expect(manager.getRootForSession('workflow-session')?.uri).toBe('social://workspace'); expect(manager.isOperationAllowed('workflow-session', 'login')).toBe(true); // Back to default }); it('should maintain session isolation', () => { const manager = registerRoots(mockServer, mockContext); const restrictiveRoot: RootDefinition = { uri: 'social://restrictive', name: 'Restrictive', description: 'Very restrictive workspace', limits: { maxPostsPerHour: 1, maxReadRequestsPerMinute: 5, maxConcurrentSessions: 1, allowedOperations: ['read_posts'], maxContentLength: 100, rateLimitWindow: 3600000, }, permissions: { canCreatePosts: false, canReadPosts: true, canAccessFeed: false, canAccessAgentProfiles: false, canUsePrompts: false, canUseSampling: false, }, }; manager.addRoot(restrictiveRoot); // Assign restrictive root to session1, leave session2 with default manager.assignRootToSession('session1', 'social://restrictive'); // Verify isolation expect(manager.isOperationAllowed('session1', 'create_post')).toBe(false); expect(manager.isOperationAllowed('session2', 'create_post')).toBe(true); expect(manager.isContentLengthValid('session1', 200)).toBe(false); expect(manager.isContentLengthValid('session2', 200)).toBe(true); expect(manager.getRootForSession('session1')?.limits.maxPostsPerHour).toBe(1); expect(manager.getRootForSession('session2')?.limits.maxPostsPerHour).toBe(10); }); it('should handle concurrent operations safely', () => { const manager = registerRoots(mockServer, mockContext); // Simulate concurrent session assignments const sessions = ['session1', 'session2', 'session3', 'session4', 'session5']; const customRoot: RootDefinition = { uri: 'social://concurrent-test', name: 'Concurrent Test', description: 'Testing concurrent operations', limits: { maxPostsPerHour: 12, maxReadRequestsPerMinute: 40, maxConcurrentSessions: 8, allowedOperations: ['read_posts', 'create_post'], maxContentLength: 1800, rateLimitWindow: 3600000, }, permissions: { canCreatePosts: true, canReadPosts: true, canAccessFeed: true, canAccessAgentProfiles: true, canUsePrompts: true, canUseSampling: false, }, }; manager.addRoot(customRoot); // Assign all sessions to custom root for (const sessionId of sessions) { const result = manager.assignRootToSession(sessionId, 'social://concurrent-test'); expect(result).toBe(true); } // Verify all sessions have correct assignment for (const sessionId of sessions) { expect(manager.getRootForSession(sessionId)?.uri).toBe('social://concurrent-test'); expect(manager.isOperationAllowed(sessionId, 'create_post')).toBe(true); expect(manager.isContentLengthValid(sessionId, 1800)).toBe(true); } // Clear some sessions manager.clearSessionRoot('session2'); manager.clearSessionRoot('session4'); // Verify selective clearing expect(manager.getRootForSession('session1')?.uri).toBe('social://concurrent-test'); expect(manager.getRootForSession('session2')?.uri).toBe('social://workspace'); // Cleared expect(manager.getRootForSession('session3')?.uri).toBe('social://concurrent-test'); expect(manager.getRootForSession('session4')?.uri).toBe('social://workspace'); // Cleared expect(manager.getRootForSession('session5')?.uri).toBe('social://concurrent-test'); }); }); describe('Edge Cases', () => { it('should handle empty operation strings', () => { expect(rootsManager.isOperationAllowed('test-session', '')).toBe(false); expect(rootsManager.isOperationAllowed('test-session', ' ')).toBe(false); }); it('should handle zero and negative content lengths', () => { expect(rootsManager.isContentLengthValid('test-session', 0)).toBe(true); expect(rootsManager.isContentLengthValid('test-session', -1)).toBe(true); // Technically valid since -1 <= 2000 }); it('should handle very large content lengths', () => { expect(rootsManager.isContentLengthValid('test-session', Number.MAX_SAFE_INTEGER)).toBe( false, ); }); it('should handle special characters in session IDs', () => { const specialSessions = [ 'session@123', 'session.with.dots', 'session-with-dashes', 'session_with_underscores', ]; for (const sessionId of specialSessions) { const result = rootsManager.assignRootToSession(sessionId, 'social://workspace'); expect(result).toBe(true); const root = rootsManager.getRootForSession(sessionId); expect(root?.uri).toBe('social://workspace'); } }); it('should handle unicode characters in root URIs and names', () => { const unicodeRoot: RootDefinition = { uri: 'social://测试工作区', name: 'Test Workspace 测试', description: 'Unicode test workspace with émojis 🚀', limits: { maxPostsPerHour: 10, maxReadRequestsPerMinute: 30, maxConcurrentSessions: 5, allowedOperations: ['read_posts'], maxContentLength: 1000, rateLimitWindow: 3600000, }, permissions: { canCreatePosts: false, canReadPosts: true, canAccessFeed: false, canAccessAgentProfiles: false, canUsePrompts: false, canUseSampling: false, }, }; rootsManager.addRoot(unicodeRoot); const assigned = rootsManager.assignRootToSession('unicode-session', 'social://测试工作区'); expect(assigned).toBe(true); expect(rootsManager.getRootForSession('unicode-session')?.name).toBe('Test Workspace 测试'); }); }); });

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