@1mcp/agent 0.2.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ServerManager } from './serverManager.js';
3
+ import logger from './logger/logger.js';
4
+ import configReloadService from './services/configReloadService.js';
5
+ import { setupCapabilities } from './capabilities/capabilityManager.js';
6
+ import { enhanceServerWithLogging } from './middleware/loggingMiddleware.js';
7
+ // Mock dependencies
8
+ jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
9
+ Server: jest.fn(),
10
+ }));
11
+ jest.mock('@modelcontextprotocol/sdk/shared/transport.js', () => ({
12
+ Transport: jest.fn(),
13
+ }));
14
+ jest.mock('./logger/logger.js', () => {
15
+ const mockLogger = {
16
+ info: jest.fn(),
17
+ error: jest.fn(),
18
+ };
19
+ return {
20
+ __esModule: true,
21
+ default: mockLogger,
22
+ };
23
+ });
24
+ jest.mock('./services/configReloadService.js', () => ({
25
+ __esModule: true,
26
+ default: {
27
+ initialize: jest.fn(),
28
+ },
29
+ }));
30
+ jest.mock('./capabilities/capabilityManager.js', () => ({
31
+ setupCapabilities: jest.fn(),
32
+ }));
33
+ jest.mock('./middleware/loggingMiddleware.js', () => ({
34
+ enhanceServerWithLogging: jest.fn(),
35
+ }));
36
+ describe('ServerManager', () => {
37
+ let mockConfig;
38
+ let mockCapabilities;
39
+ let mockClients;
40
+ let mockTransports;
41
+ let mockTransport;
42
+ let mockServer;
43
+ beforeEach(() => {
44
+ // Reset all mocks
45
+ jest.clearAllMocks();
46
+ // Setup test data
47
+ mockConfig = { name: 'test-server', version: '1.0.0' };
48
+ mockCapabilities = { capabilities: { test: true } };
49
+ mockClients = {};
50
+ mockTransports = {};
51
+ mockTransport = {
52
+ // Add any required Transport properties here
53
+ };
54
+ mockServer = {
55
+ connect: jest.fn().mockResolvedValue(undefined),
56
+ transport: mockTransport,
57
+ };
58
+ // Setup mocks
59
+ Server.mockImplementation(() => mockServer);
60
+ setupCapabilities.mockResolvedValue(undefined);
61
+ enhanceServerWithLogging.mockReturnValue(undefined);
62
+ configReloadService.initialize.mockImplementation(() => undefined);
63
+ });
64
+ describe('getInstance', () => {
65
+ it('should create a singleton instance', () => {
66
+ const instance1 = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
67
+ const instance2 = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
68
+ expect(instance1).toBe(instance2);
69
+ });
70
+ });
71
+ describe('connectTransport', () => {
72
+ let serverManager;
73
+ const sessionId = 'test-session';
74
+ const tags = ['tag1', 'tag2'];
75
+ beforeEach(() => {
76
+ serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
77
+ });
78
+ it('should successfully connect a transport', async () => {
79
+ await serverManager.connectTransport(mockTransport, sessionId, tags);
80
+ expect(Server).toHaveBeenCalledWith(mockConfig, mockCapabilities);
81
+ expect(enhanceServerWithLogging).toHaveBeenCalledWith(mockServer);
82
+ expect(setupCapabilities).toHaveBeenCalled();
83
+ expect(configReloadService.initialize).toHaveBeenCalled();
84
+ expect(mockServer.connect).toHaveBeenCalledWith(mockTransport);
85
+ expect(logger.info).toHaveBeenCalledWith(`Connected transport for session ${sessionId}`);
86
+ });
87
+ it('should handle connection errors', async () => {
88
+ const error = new Error('Connection failed');
89
+ mockServer.connect.mockRejectedValueOnce(error);
90
+ await expect(serverManager.connectTransport(mockTransport, sessionId, tags)).rejects.toThrow('Connection failed');
91
+ expect(logger.error).toHaveBeenCalled();
92
+ });
93
+ });
94
+ describe('disconnectTransport', () => {
95
+ let serverManager;
96
+ const sessionId = 'test-session';
97
+ beforeEach(() => {
98
+ serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
99
+ });
100
+ it('should successfully disconnect a transport', async () => {
101
+ await serverManager.connectTransport(mockTransport, sessionId);
102
+ jest.clearAllMocks(); // Clear the logs from connectTransport
103
+ serverManager.disconnectTransport(sessionId);
104
+ expect(logger.info).toHaveBeenCalledWith(`Disconnected transport for session ${sessionId}`);
105
+ });
106
+ it('should handle non-existent session gracefully', () => {
107
+ serverManager.disconnectTransport('non-existent');
108
+ expect(logger.info).not.toHaveBeenCalled();
109
+ });
110
+ });
111
+ describe('transport management methods', () => {
112
+ let serverManager;
113
+ const sessionId = 'test-session';
114
+ beforeEach(async () => {
115
+ serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
116
+ await serverManager.connectTransport(mockTransport, sessionId);
117
+ });
118
+ it('should get transport by session id', () => {
119
+ const transport = serverManager.getTransport(sessionId);
120
+ expect(transport).toBe(mockTransport);
121
+ });
122
+ it('should return undefined for non-existent session', () => {
123
+ const transport = serverManager.getTransport('non-existent');
124
+ expect(transport).toBeUndefined();
125
+ });
126
+ it('should get all transports', () => {
127
+ const transports = serverManager.getTransports();
128
+ expect(transports.size).toBe(1);
129
+ expect(transports.get(sessionId)).toBe(mockTransport);
130
+ });
131
+ it('should get client transports', () => {
132
+ const clientTransports = serverManager.getClientTransports();
133
+ expect(clientTransports).toEqual(mockTransports);
134
+ });
135
+ it('should get active transports count', () => {
136
+ expect(serverManager.getActiveTransportsCount()).toBe(1);
137
+ });
138
+ });
139
+ describe('getServer', () => {
140
+ let serverManager;
141
+ const sessionId = 'test-session';
142
+ const tags = ['tag1', 'tag2'];
143
+ beforeEach(async () => {
144
+ serverManager = ServerManager.getInstance(mockConfig, mockCapabilities, mockClients, mockTransports);
145
+ await serverManager.connectTransport(mockTransport, sessionId, tags);
146
+ });
147
+ it('should return server info for existing session', () => {
148
+ const serverInfo = serverManager.getServer(sessionId);
149
+ expect(serverInfo).toBeDefined();
150
+ expect(serverInfo?.server).toBe(mockServer);
151
+ expect(serverInfo?.tags).toEqual(tags);
152
+ });
153
+ it('should return undefined for non-existent session', () => {
154
+ const serverInfo = serverManager.getServer('non-existent');
155
+ expect(serverInfo).toBeUndefined();
156
+ });
157
+ });
158
+ });
@@ -1,14 +1,14 @@
1
1
  import logger from '../logger/logger.js';
2
- import configManager, { ConfigChangeEvent } from '../config/configManager.js';
3
- import { createTransports } from '../config/transportConfig.js';
2
+ import { ConfigChangeEvent, ConfigManager } from '../config/configManager.js';
4
3
  import { createClients } from '../clients/clientManager.js';
5
4
  import { setupCapabilities } from '../capabilities/capabilityManager.js';
5
+ import { createTransports } from '../config/transportConfig.js';
6
6
  /**
7
7
  * Service to handle dynamic configuration reloading
8
8
  */
9
9
  export class ConfigReloadService {
10
10
  static instance;
11
- server = null;
11
+ serverInfo = null;
12
12
  currentTransports = {};
13
13
  isReloading = false;
14
14
  /**
@@ -27,12 +27,13 @@ export class ConfigReloadService {
27
27
  }
28
28
  /**
29
29
  * Initialize the service with the server instance
30
- * @param server The MCP server instance
30
+ * @param serverInfo The MCP server instance
31
31
  * @param initialTransports The initial transports
32
32
  */
33
- initialize(server, initialTransports) {
34
- this.server = server;
33
+ initialize(serverInfo, initialTransports) {
34
+ this.serverInfo = serverInfo;
35
35
  this.currentTransports = initialTransports;
36
+ const configManager = ConfigManager.getInstance();
36
37
  // Set up configuration change listener
37
38
  configManager.on(ConfigChangeEvent.TRANSPORT_CONFIG_CHANGED, this.handleConfigChange.bind(this));
38
39
  // Start watching for configuration changes
@@ -44,7 +45,7 @@ export class ConfigReloadService {
44
45
  * @param newConfig The new transport configuration
45
46
  */
46
47
  async handleConfigChange(newConfig) {
47
- if (!this.server || this.isReloading) {
48
+ if (!this.serverInfo || this.isReloading) {
48
49
  return;
49
50
  }
50
51
  this.isReloading = true;
@@ -67,7 +68,7 @@ export class ConfigReloadService {
67
68
  // Create clients for the new transports
68
69
  const newClients = await createClients(newTransports);
69
70
  // Register new capabilities with the server
70
- await setupCapabilities(newClients, this.server);
71
+ await setupCapabilities(newClients, this.serverInfo);
71
72
  // Update current transports
72
73
  this.currentTransports = newTransports;
73
74
  logger.info('Configuration reload completed successfully');
@@ -83,6 +84,7 @@ export class ConfigReloadService {
83
84
  * Stop the service and clean up resources
84
85
  */
85
86
  stop() {
87
+ const configManager = ConfigManager.getInstance();
86
88
  configManager.stopWatching();
87
89
  configManager.removeAllListeners(ConfigChangeEvent.TRANSPORT_CONFIG_CHANGED);
88
90
  logger.info('Config reload service stopped');
@@ -0,0 +1,44 @@
1
+ import { createMockTransport } from '../mocks/commonMocks.js';
2
+ // Common test clients fixture
3
+ export const createTestClients = () => ({
4
+ client1: {
5
+ capabilities: {
6
+ resources: {},
7
+ tools: {},
8
+ },
9
+ transport: createMockTransport(),
10
+ },
11
+ client2: {
12
+ capabilities: {
13
+ resources: {},
14
+ },
15
+ transport: createMockTransport(),
16
+ },
17
+ client3: {
18
+ capabilities: {
19
+ tools: {},
20
+ },
21
+ transport: createMockTransport(),
22
+ },
23
+ clientNoCapabilities: {
24
+ transport: createMockTransport(),
25
+ },
26
+ clientNoTags: {
27
+ capabilities: {
28
+ resources: {},
29
+ },
30
+ transport: createMockTransport(),
31
+ },
32
+ });
33
+ // Common test config fixture
34
+ export const testConfig = {
35
+ mcpServers: {
36
+ test: { url: 'test-url' },
37
+ },
38
+ };
39
+ // Common test capabilities fixture
40
+ export const testCapabilities = {
41
+ resources: {},
42
+ tools: {},
43
+ test: true,
44
+ };
@@ -0,0 +1,23 @@
1
+ // Helper to clear all mocks in a test
2
+ export const clearAllMocks = () => {
3
+ jest.clearAllMocks();
4
+ jest.useRealTimers();
5
+ };
6
+ // Helper to setup fake timers
7
+ export const setupFakeTimers = () => {
8
+ jest.useFakeTimers();
9
+ };
10
+ // Helper to advance timers by time
11
+ export const advanceTimersByTime = async (ms) => {
12
+ await jest.advanceTimersByTimeAsync(ms);
13
+ };
14
+ // Helper to run all timers
15
+ export const runAllTimers = async () => {
16
+ await jest.runAllTimersAsync();
17
+ };
18
+ // Helper to create a mock event emitter spy
19
+ export const createEventEmitterSpy = (instance) => {
20
+ return jest.spyOn(instance, 'emit');
21
+ };
22
+ // Helper to wait for next tick
23
+ export const waitForNextTick = () => new Promise((resolve) => process.nextTick(resolve));
@@ -0,0 +1,65 @@
1
+ // Mock Logger
2
+ export const mockLogger = {
3
+ info: jest.fn(),
4
+ error: jest.fn(),
5
+ warn: jest.fn(),
6
+ debug: jest.fn(),
7
+ level: 'info',
8
+ transports: [],
9
+ };
10
+ // Mock Transport
11
+ export const createMockTransport = () => ({
12
+ start: jest.fn(),
13
+ send: jest.fn(),
14
+ close: jest.fn(),
15
+ });
16
+ // Mock Server
17
+ export const createMockServer = (overrides = {}) => ({
18
+ connect: jest.fn().mockResolvedValue(undefined),
19
+ transport: createMockTransport(),
20
+ ...overrides,
21
+ });
22
+ // Mock Client
23
+ export const createMockClient = (overrides = {}) => ({
24
+ connect: jest.fn(),
25
+ getServerVersion: jest.fn(),
26
+ ...overrides,
27
+ });
28
+ // Mock File System
29
+ export const createMockFs = () => ({
30
+ existsSync: jest.fn(),
31
+ mkdirSync: jest.fn(),
32
+ writeFileSync: jest.fn(),
33
+ readFileSync: jest.fn(),
34
+ watch: jest.fn(() => ({ close: jest.fn() })),
35
+ });
36
+ // Mock Config
37
+ export const createMockConfig = (overrides = {}) => ({
38
+ name: 'test-server',
39
+ version: '1.0.0',
40
+ ...overrides,
41
+ });
42
+ // Mock Capabilities
43
+ export const createMockCapabilities = (overrides = {}) => ({
44
+ capabilities: {
45
+ resources: {},
46
+ tools: {},
47
+ ...overrides,
48
+ },
49
+ });
50
+ // Jest module mocks setup helper
51
+ export const setupCommonJestMocks = () => {
52
+ jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
53
+ Server: jest.fn(),
54
+ }));
55
+ jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
56
+ Client: jest.fn(),
57
+ }));
58
+ jest.mock('@modelcontextprotocol/sdk/shared/transport.js', () => ({
59
+ Transport: jest.fn(),
60
+ }));
61
+ jest.mock('../../logger/logger.js', () => ({
62
+ __esModule: true,
63
+ default: mockLogger,
64
+ }));
65
+ };
package/build/types.js ADDED
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Zod schema for transport configuration
4
+ */
5
+ export const transportConfigSchema = z.object({
6
+ type: z.enum(['stdio', 'sse', 'http']).optional(),
7
+ disabled: z.boolean().optional(),
8
+ timeout: z.number().optional(),
9
+ tags: z.array(z.string()).optional(),
10
+ // HTTP/SSE Parameters
11
+ url: z.string().url().optional(),
12
+ headers: z.record(z.string()).optional(),
13
+ // StdioServerParameters fields
14
+ command: z.string().optional(),
15
+ args: z.array(z.string()).optional(),
16
+ stderr: z.union([z.string(), z.number()]).optional(),
17
+ cwd: z.string().optional(),
18
+ env: z.record(z.string()).optional(),
19
+ });
20
+ /**
21
+ * Enum representing possible client connection states
22
+ */
23
+ export var ClientStatus;
24
+ (function (ClientStatus) {
25
+ /** Client is successfully connected */
26
+ ClientStatus["Connected"] = "connected";
27
+ /** Client is disconnected */
28
+ ClientStatus["Disconnected"] = "disconnected";
29
+ /** Client encountered an error */
30
+ ClientStatus["Error"] = "error";
31
+ })(ClientStatus || (ClientStatus = {}));
@@ -0,0 +1,101 @@
1
+ import logger from '../logger/logger.js';
2
+ /**
3
+ * Filters clients by tags
4
+ * @param clients Record of client instances
5
+ * @param tags Array of tags to filter by
6
+ * @returns Filtered record of client instances
7
+ */
8
+ export function filterClientsByTags(clients, tags) {
9
+ if (!tags || tags.length === 0) {
10
+ return clients;
11
+ }
12
+ const filteredClients = {};
13
+ let matchedClients = 0;
14
+ for (const [name, clientInfo] of Object.entries(clients)) {
15
+ const clientTags = clientInfo.transport.tags || [];
16
+ const hasMatchingTags = tags.every((tag) => clientTags.includes(tag));
17
+ if (hasMatchingTags) {
18
+ filteredClients[name] = clientInfo;
19
+ matchedClients++;
20
+ }
21
+ }
22
+ if (matchedClients === 0) {
23
+ logger.warn(`No clients found matching tags: ${tags.join(', ')}`);
24
+ }
25
+ else {
26
+ logger.debug(`Found ${matchedClients} clients matching tags: ${tags.join(', ')}`);
27
+ }
28
+ return Object.freeze(filteredClients);
29
+ }
30
+ /**
31
+ * Filters clients by capabilities
32
+ * @param clients Record of client instances
33
+ * @param capabilities Object containing capabilities to filter by
34
+ * @returns Filtered record of client instances
35
+ */
36
+ export function filterClientsByCapabilities(clients, capabilities) {
37
+ const filteredClients = {};
38
+ let matchedClients = 0;
39
+ for (const [name, clientInfo] of Object.entries(clients)) {
40
+ const clientCapabilities = clientInfo.capabilities || {};
41
+ const hasMatchingCapabilities = Object.keys(capabilities).every((capability) => {
42
+ const clientCapability = clientCapabilities[capability];
43
+ return clientCapability !== undefined;
44
+ });
45
+ if (hasMatchingCapabilities) {
46
+ filteredClients[name] = clientInfo;
47
+ matchedClients++;
48
+ }
49
+ }
50
+ if (matchedClients === 0) {
51
+ logger.warn(`No clients found matching capabilities: ${JSON.stringify(capabilities)}`);
52
+ }
53
+ else {
54
+ logger.debug(`Found ${matchedClients} clients matching capabilities: ${JSON.stringify(capabilities)}`);
55
+ }
56
+ return Object.freeze(filteredClients);
57
+ }
58
+ /**
59
+ * Filters clients by multiple criteria
60
+ * @param filters Array of client filters
61
+ * @returns Filtered record of client instances
62
+ */
63
+ export function filterClients(...filters) {
64
+ return (clients) => {
65
+ return filters.reduce((filteredClients, filter) => filter(filteredClients), clients);
66
+ };
67
+ }
68
+ /**
69
+ * Filters clients by capabilities
70
+ * @param requiredCapabilities Object containing capabilities to filter by
71
+ * @returns Filtered record of client instances
72
+ */
73
+ export function byCapabilities(requiredCapabilities) {
74
+ return (clients) => {
75
+ return Object.entries(clients).reduce((filtered, [name, clientInfo]) => {
76
+ const hasCapabilities = Object.keys(requiredCapabilities).every((cap) => clientInfo.capabilities && cap in clientInfo.capabilities);
77
+ if (hasCapabilities) {
78
+ filtered[name] = clientInfo;
79
+ }
80
+ return filtered;
81
+ }, {});
82
+ };
83
+ }
84
+ /**
85
+ * Filters clients by tags
86
+ * @param tags Array of tags to filter by
87
+ * @returns Filtered record of client instances
88
+ */
89
+ export function byTags(tags) {
90
+ return (clients) => {
91
+ if (!tags || tags.length === 0)
92
+ return clients;
93
+ return Object.entries(clients).reduce((filtered, [name, clientInfo]) => {
94
+ const hasMatchingTags = tags.every((tag) => clientInfo.transport.tags?.includes(tag));
95
+ if (hasMatchingTags) {
96
+ filtered[name] = clientInfo;
97
+ }
98
+ return filtered;
99
+ }, {});
100
+ };
101
+ }
@@ -0,0 +1,124 @@
1
+ import { filterClients, byCapabilities, byTags, filterClientsByCapabilities, filterClientsByTags, } from './clientFiltering.js';
2
+ describe('Client Filtering Utils', () => {
3
+ const mockClients = {
4
+ client1: {
5
+ capabilities: {
6
+ resources: {},
7
+ tools: {},
8
+ },
9
+ transport: {
10
+ tags: ['tag1', 'tag2'],
11
+ },
12
+ },
13
+ client2: {
14
+ capabilities: {
15
+ resources: {},
16
+ },
17
+ transport: {
18
+ tags: ['tag1'],
19
+ },
20
+ },
21
+ client3: {
22
+ capabilities: {
23
+ tools: {},
24
+ },
25
+ transport: {
26
+ tags: ['tag3'],
27
+ },
28
+ },
29
+ clientNoCapabilities: {
30
+ transport: {
31
+ tags: ['tag1'],
32
+ },
33
+ },
34
+ clientNoTags: {
35
+ capabilities: {
36
+ resources: {},
37
+ },
38
+ transport: {},
39
+ },
40
+ };
41
+ describe('filterClientsByCapabilities', () => {
42
+ it('should filter clients by single capability', () => {
43
+ const filtered = filterClientsByCapabilities(mockClients, { resources: {} });
44
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2', 'clientNoTags']);
45
+ });
46
+ it('should filter clients by multiple capabilities', () => {
47
+ const filtered = filterClientsByCapabilities(mockClients, { resources: {}, tools: {} });
48
+ expect(Object.keys(filtered)).toEqual(['client1']);
49
+ });
50
+ it('should handle clients with no capabilities', () => {
51
+ const filtered = filterClientsByCapabilities(mockClients, { resources: {} });
52
+ expect(filtered).not.toHaveProperty('clientNoCapabilities');
53
+ });
54
+ it('should return empty object when no matches found', () => {
55
+ const filtered = filterClientsByCapabilities(mockClients, { nonexistent: {} });
56
+ expect(Object.keys(filtered)).toHaveLength(0);
57
+ });
58
+ });
59
+ describe('filterClientsByTags', () => {
60
+ it('should return all clients when no tags provided', () => {
61
+ const filtered = filterClientsByTags(mockClients, undefined);
62
+ expect(Object.keys(filtered)).toEqual(Object.keys(mockClients));
63
+ });
64
+ it('should filter clients by single tag', () => {
65
+ const filtered = filterClientsByTags(mockClients, ['tag1']);
66
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2', 'clientNoCapabilities']);
67
+ });
68
+ it('should filter clients by multiple tags', () => {
69
+ const filtered = filterClientsByTags(mockClients, ['tag1', 'tag2']);
70
+ expect(Object.keys(filtered)).toEqual(['client1']);
71
+ });
72
+ it('should handle clients with no tags', () => {
73
+ const filtered = filterClientsByTags(mockClients, ['tag1']);
74
+ expect(filtered).not.toHaveProperty('clientNoTags');
75
+ });
76
+ it('should return empty object when no matches found', () => {
77
+ const filtered = filterClientsByTags(mockClients, ['nonexistent']);
78
+ expect(Object.keys(filtered)).toHaveLength(0);
79
+ });
80
+ });
81
+ describe('filterClients (composed filters)', () => {
82
+ it('should chain multiple filters together', () => {
83
+ const filtered = filterClients(byCapabilities({ resources: {} }), byTags(['tag1']))(mockClients);
84
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2']);
85
+ });
86
+ it('should handle empty filters array', () => {
87
+ const filtered = filterClients()(mockClients);
88
+ expect(Object.keys(filtered)).toEqual(Object.keys(mockClients));
89
+ });
90
+ it('should handle no matching results', () => {
91
+ const filtered = filterClients(byCapabilities({ resources: {} }), byTags(['tag3']))(mockClients);
92
+ expect(Object.keys(filtered)).toHaveLength(0);
93
+ });
94
+ });
95
+ describe('byCapabilities', () => {
96
+ it('should create a filter function for capabilities', () => {
97
+ const filter = byCapabilities({ resources: {} });
98
+ const filtered = filter(mockClients);
99
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2', 'clientNoTags']);
100
+ });
101
+ it('should handle undefined capabilities safely', () => {
102
+ const filter = byCapabilities({ resources: {} });
103
+ const filtered = filter({ ...mockClients, unsafeClient: { transport: {} } });
104
+ expect(filtered).not.toHaveProperty('unsafeClient');
105
+ });
106
+ });
107
+ describe('byTags', () => {
108
+ it('should create a filter function for tags', () => {
109
+ const filter = byTags(['tag1']);
110
+ const filtered = filter(mockClients);
111
+ expect(Object.keys(filtered)).toEqual(['client1', 'client2', 'clientNoCapabilities']);
112
+ });
113
+ it('should handle undefined tags array', () => {
114
+ const filter = byTags(undefined);
115
+ const filtered = filter(mockClients);
116
+ expect(Object.keys(filtered)).toEqual(Object.keys(mockClients));
117
+ });
118
+ it('should handle empty tags array', () => {
119
+ const filter = byTags([]);
120
+ const filtered = filter(mockClients);
121
+ expect(Object.keys(filtered)).toEqual(Object.keys(mockClients));
122
+ });
123
+ });
124
+ });