| | const originalEnv = { |
| | CREDS_KEY: process.env.CREDS_KEY, |
| | CREDS_IV: process.env.CREDS_IV, |
| | }; |
| |
|
| | process.env.CREDS_KEY = '0123456789abcdef0123456789abcdef'; |
| | process.env.CREDS_IV = '0123456789abcdef'; |
| |
|
| | jest.mock('~/server/services/Config', () => ({ |
| | getCachedTools: jest.fn(), |
| | getMCPServerTools: jest.fn(), |
| | })); |
| |
|
| | const mongoose = require('mongoose'); |
| | const { v4: uuidv4 } = require('uuid'); |
| | const { agentSchema } = require('@librechat/data-schemas'); |
| | const { MongoMemoryServer } = require('mongodb-memory-server'); |
| | const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider'); |
| | const { |
| | getAgent, |
| | loadAgent, |
| | createAgent, |
| | updateAgent, |
| | deleteAgent, |
| | getListAgents, |
| | getListAgentsByAccess, |
| | revertAgentVersion, |
| | updateAgentProjects, |
| | addAgentResourceFile, |
| | removeAgentResourceFiles, |
| | generateActionMetadataHash, |
| | } = require('./Agent'); |
| | const permissionService = require('~/server/services/PermissionService'); |
| | const { getCachedTools, getMCPServerTools } = require('~/server/services/Config'); |
| | const { AclEntry } = require('~/db/models'); |
| |
|
| | |
| | |
| | |
| | let Agent; |
| |
|
| | describe('models/Agent', () => { |
| | describe('Agent Resource File Operations', () => { |
| | let mongoServer; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await mongoose.connect(mongoUri); |
| | }, 20000); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | process.env.CREDS_KEY = originalEnv.CREDS_KEY; |
| | process.env.CREDS_IV = originalEnv.CREDS_IV; |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await Agent.deleteMany({}); |
| | }); |
| |
|
| | test('should add tool_resource to tools if missing', async () => { |
| | const agent = await createBasicAgent(); |
| | const fileId = uuidv4(); |
| | const toolResource = 'file_search'; |
| |
|
| | const updatedAgent = await addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: toolResource, |
| | file_id: fileId, |
| | }); |
| |
|
| | expect(updatedAgent.tools).toContain(toolResource); |
| | expect(Array.isArray(updatedAgent.tools)).toBe(true); |
| | |
| | const count = updatedAgent.tools.filter((t) => t === toolResource).length; |
| | expect(count).toBe(1); |
| | }); |
| |
|
| | test('should not duplicate tool_resource in tools if already present', async () => { |
| | const agent = await createBasicAgent(); |
| | const fileId1 = uuidv4(); |
| | const fileId2 = uuidv4(); |
| | const toolResource = 'file_search'; |
| |
|
| | |
| | await addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: toolResource, |
| | file_id: fileId1, |
| | }); |
| |
|
| | |
| | const updatedAgent = await addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: toolResource, |
| | file_id: fileId2, |
| | }); |
| |
|
| | expect(updatedAgent.tools).toContain(toolResource); |
| | expect(Array.isArray(updatedAgent.tools)).toBe(true); |
| | const count = updatedAgent.tools.filter((t) => t === toolResource).length; |
| | expect(count).toBe(1); |
| | }); |
| |
|
| | test('should handle concurrent file additions', async () => { |
| | const agent = await createBasicAgent(); |
| | const fileIds = Array.from({ length: 10 }, () => uuidv4()); |
| |
|
| | |
| | const additionPromises = createFileOperations(agent.id, fileIds, 'add'); |
| |
|
| | await Promise.all(additionPromises); |
| |
|
| | const updatedAgent = await Agent.findOne({ id: agent.id }); |
| | expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); |
| | expect(updatedAgent.tool_resources.test_tool.file_ids).toHaveLength(10); |
| | expect(new Set(updatedAgent.tool_resources.test_tool.file_ids).size).toBe(10); |
| | }); |
| |
|
| | test('should handle concurrent additions and removals', async () => { |
| | const agent = await createBasicAgent(); |
| | const initialFileIds = Array.from({ length: 5 }, () => uuidv4()); |
| |
|
| | await Promise.all(createFileOperations(agent.id, initialFileIds, 'add')); |
| |
|
| | const newFileIds = Array.from({ length: 5 }, () => uuidv4()); |
| | const operations = [ |
| | ...newFileIds.map((fileId) => |
| | addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: 'test_tool', |
| | file_id: fileId, |
| | }), |
| | ), |
| | ...initialFileIds.map((fileId) => |
| | removeAgentResourceFiles({ |
| | agent_id: agent.id, |
| | files: [{ tool_resource: 'test_tool', file_id: fileId }], |
| | }), |
| | ), |
| | ]; |
| |
|
| | await Promise.all(operations); |
| |
|
| | const updatedAgent = await Agent.findOne({ id: agent.id }); |
| | expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); |
| | expect(updatedAgent.tool_resources.test_tool.file_ids).toHaveLength(5); |
| | }); |
| |
|
| | test('should initialize array when adding to non-existent tool resource', async () => { |
| | const agent = await createBasicAgent(); |
| | const fileId = uuidv4(); |
| |
|
| | const updatedAgent = await addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: 'new_tool', |
| | file_id: fileId, |
| | }); |
| |
|
| | expect(updatedAgent.tool_resources.new_tool.file_ids).toBeDefined(); |
| | expect(updatedAgent.tool_resources.new_tool.file_ids).toHaveLength(1); |
| | expect(updatedAgent.tool_resources.new_tool.file_ids[0]).toBe(fileId); |
| | }); |
| |
|
| | test('should handle rapid sequential modifications to same tool resource', async () => { |
| | const agent = await createBasicAgent(); |
| | const fileId = uuidv4(); |
| |
|
| | for (let i = 0; i < 10; i++) { |
| | await addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: 'test_tool', |
| | file_id: `${fileId}_${i}`, |
| | }); |
| |
|
| | if (i % 2 === 0) { |
| | await removeAgentResourceFiles({ |
| | agent_id: agent.id, |
| | files: [{ tool_resource: 'test_tool', file_id: `${fileId}_${i}` }], |
| | }); |
| | } |
| | } |
| |
|
| | const updatedAgent = await Agent.findOne({ id: agent.id }); |
| | expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); |
| | expect(Array.isArray(updatedAgent.tool_resources.test_tool.file_ids)).toBe(true); |
| | }); |
| |
|
| | test('should handle multiple tool resources concurrently', async () => { |
| | const agent = await createBasicAgent(); |
| | const toolResources = ['tool1', 'tool2', 'tool3']; |
| | const operations = []; |
| |
|
| | toolResources.forEach((tool) => { |
| | const fileIds = Array.from({ length: 5 }, () => uuidv4()); |
| | fileIds.forEach((fileId) => { |
| | operations.push( |
| | addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: tool, |
| | file_id: fileId, |
| | }), |
| | ); |
| | }); |
| | }); |
| |
|
| | await Promise.all(operations); |
| |
|
| | const updatedAgent = await Agent.findOne({ id: agent.id }); |
| | toolResources.forEach((tool) => { |
| | expect(updatedAgent.tool_resources[tool].file_ids).toBeDefined(); |
| | expect(updatedAgent.tool_resources[tool].file_ids).toHaveLength(5); |
| | }); |
| | }); |
| |
|
| | test.each([ |
| | { |
| | name: 'duplicate additions', |
| | operation: 'add', |
| | duplicateCount: 5, |
| | expectedLength: 1, |
| | expectedContains: true, |
| | }, |
| | { |
| | name: 'duplicate removals', |
| | operation: 'remove', |
| | duplicateCount: 5, |
| | expectedLength: 0, |
| | expectedContains: false, |
| | setupFile: true, |
| | }, |
| | ])( |
| | 'should handle concurrent $name', |
| | async ({ operation, duplicateCount, expectedLength, expectedContains, setupFile }) => { |
| | const agent = await createBasicAgent(); |
| | const fileId = uuidv4(); |
| |
|
| | if (setupFile) { |
| | await addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: 'test_tool', |
| | file_id: fileId, |
| | }); |
| | } |
| |
|
| | const promises = Array.from({ length: duplicateCount }).map(() => |
| | operation === 'add' |
| | ? addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: 'test_tool', |
| | file_id: fileId, |
| | }) |
| | : removeAgentResourceFiles({ |
| | agent_id: agent.id, |
| | files: [{ tool_resource: 'test_tool', file_id: fileId }], |
| | }), |
| | ); |
| |
|
| | await Promise.all(promises); |
| |
|
| | const updatedAgent = await Agent.findOne({ id: agent.id }); |
| | const fileIds = updatedAgent.tool_resources?.test_tool?.file_ids ?? []; |
| |
|
| | expect(fileIds).toHaveLength(expectedLength); |
| | if (expectedContains) { |
| | expect(fileIds[0]).toBe(fileId); |
| | } else { |
| | expect(fileIds).not.toContain(fileId); |
| | } |
| | }, |
| | ); |
| |
|
| | test('should handle concurrent add and remove of the same file', async () => { |
| | const agent = await createBasicAgent(); |
| | const fileId = uuidv4(); |
| |
|
| | await addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: 'test_tool', |
| | file_id: fileId, |
| | }); |
| |
|
| | const operations = [ |
| | addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: 'test_tool', |
| | file_id: fileId, |
| | }), |
| | removeAgentResourceFiles({ |
| | agent_id: agent.id, |
| | files: [{ tool_resource: 'test_tool', file_id: fileId }], |
| | }), |
| | ]; |
| |
|
| | await Promise.all(operations); |
| |
|
| | const updatedAgent = await Agent.findOne({ id: agent.id }); |
| | const finalFileIds = updatedAgent.tool_resources.test_tool.file_ids; |
| | const count = finalFileIds.filter((id) => id === fileId).length; |
| |
|
| | expect(count).toBeLessThanOrEqual(1); |
| | if (count === 0) { |
| | expect(finalFileIds).toHaveLength(0); |
| | } else { |
| | expect(finalFileIds).toHaveLength(1); |
| | expect(finalFileIds[0]).toBe(fileId); |
| | } |
| | }); |
| |
|
| | test('should handle concurrent removals of different files', async () => { |
| | const agent = await createBasicAgent(); |
| | const fileIds = Array.from({ length: 10 }, () => uuidv4()); |
| |
|
| | |
| | await Promise.all( |
| | fileIds.map((fileId) => |
| | addAgentResourceFile({ |
| | agent_id: agent.id, |
| | tool_resource: 'test_tool', |
| | file_id: fileId, |
| | }), |
| | ), |
| | ); |
| |
|
| | |
| | const removalPromises = fileIds.map((fileId) => |
| | removeAgentResourceFiles({ |
| | agent_id: agent.id, |
| | files: [{ tool_resource: 'test_tool', file_id: fileId }], |
| | }), |
| | ); |
| |
|
| | await Promise.all(removalPromises); |
| |
|
| | const updatedAgent = await Agent.findOne({ id: agent.id }); |
| | |
| | const finalFileIds = updatedAgent.tool_resources?.test_tool?.file_ids ?? []; |
| | expect(finalFileIds).toHaveLength(0); |
| | }); |
| |
|
| | describe('Edge Cases', () => { |
| | describe.each([ |
| | { |
| | operation: 'add', |
| | name: 'empty file_id', |
| | needsAgent: true, |
| | params: { tool_resource: 'file_search', file_id: '' }, |
| | shouldResolve: true, |
| | }, |
| | { |
| | operation: 'add', |
| | name: 'non-existent agent', |
| | needsAgent: false, |
| | params: { tool_resource: 'file_search', file_id: 'file123' }, |
| | shouldResolve: false, |
| | error: 'Agent not found for adding resource file', |
| | }, |
| | ])('addAgentResourceFile with $name', ({ needsAgent, params, shouldResolve, error }) => { |
| | test(`should ${shouldResolve ? 'resolve' : 'reject'}`, async () => { |
| | const agent = needsAgent ? await createBasicAgent() : null; |
| | const agent_id = needsAgent ? agent.id : `agent_${uuidv4()}`; |
| |
|
| | if (shouldResolve) { |
| | await expect(addAgentResourceFile({ agent_id, ...params })).resolves.toBeDefined(); |
| | } else { |
| | await expect(addAgentResourceFile({ agent_id, ...params })).rejects.toThrow(error); |
| | } |
| | }); |
| | }); |
| |
|
| | describe.each([ |
| | { |
| | name: 'empty files array', |
| | files: [], |
| | needsAgent: true, |
| | shouldResolve: true, |
| | }, |
| | { |
| | name: 'non-existent tool_resource', |
| | files: [{ tool_resource: 'non_existent_tool', file_id: 'file123' }], |
| | needsAgent: true, |
| | shouldResolve: true, |
| | }, |
| | { |
| | name: 'non-existent agent', |
| | files: [{ tool_resource: 'file_search', file_id: 'file123' }], |
| | needsAgent: false, |
| | shouldResolve: false, |
| | error: 'Agent not found for removing resource files', |
| | }, |
| | ])('removeAgentResourceFiles with $name', ({ files, needsAgent, shouldResolve, error }) => { |
| | test(`should ${shouldResolve ? 'resolve' : 'reject'}`, async () => { |
| | const agent = needsAgent ? await createBasicAgent() : null; |
| | const agent_id = needsAgent ? agent.id : `agent_${uuidv4()}`; |
| |
|
| | if (shouldResolve) { |
| | const result = await removeAgentResourceFiles({ agent_id, files }); |
| | expect(result).toBeDefined(); |
| | if (agent) { |
| | expect(result.id).toBe(agent.id); |
| | } |
| | } else { |
| | await expect(removeAgentResourceFiles({ agent_id, files })).rejects.toThrow(error); |
| | } |
| | }); |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Agent CRUD Operations', () => { |
| | let mongoServer; |
| | let AccessRole; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await mongoose.connect(mongoUri); |
| |
|
| | |
| | const dbModels = require('~/db/models'); |
| | AccessRole = dbModels.AccessRole; |
| |
|
| | |
| | await AccessRole.create({ |
| | accessRoleId: AccessRoleIds.AGENT_OWNER, |
| | name: 'Owner', |
| | description: 'Full control over agents', |
| | resourceType: ResourceType.AGENT, |
| | permBits: 15, |
| | }); |
| | }, 20000); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await Agent.deleteMany({}); |
| | }); |
| |
|
| | test('should create and get an agent', async () => { |
| | const { agentId, authorId } = createTestIds(); |
| |
|
| | const newAgent = await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | description: 'Test description', |
| | }); |
| |
|
| | expect(newAgent).toBeDefined(); |
| | expect(newAgent.id).toBe(agentId); |
| | expect(newAgent.name).toBe('Test Agent'); |
| |
|
| | const retrievedAgent = await getAgent({ id: agentId }); |
| | expect(retrievedAgent).toBeDefined(); |
| | expect(retrievedAgent.id).toBe(agentId); |
| | expect(retrievedAgent.name).toBe('Test Agent'); |
| | expect(retrievedAgent.description).toBe('Test description'); |
| | }); |
| |
|
| | test('should delete an agent', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Agent To Delete', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | const agentBeforeDelete = await getAgent({ id: agentId }); |
| | expect(agentBeforeDelete).toBeDefined(); |
| |
|
| | await deleteAgent({ id: agentId }); |
| |
|
| | const agentAfterDelete = await getAgent({ id: agentId }); |
| | expect(agentAfterDelete).toBeNull(); |
| | }); |
| |
|
| | test('should remove ACL entries when deleting an agent', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | const agent = await createAgent({ |
| | id: agentId, |
| | name: 'Agent With Permissions', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | |
| | await permissionService.grantPermission({ |
| | principalType: PrincipalType.USER, |
| | principalId: authorId, |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent._id, |
| | accessRoleId: AccessRoleIds.AGENT_OWNER, |
| | grantedBy: authorId, |
| | }); |
| |
|
| | |
| | const aclEntriesBefore = await AclEntry.find({ |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent._id, |
| | }); |
| | expect(aclEntriesBefore).toHaveLength(1); |
| |
|
| | |
| | await deleteAgent({ id: agentId }); |
| |
|
| | |
| | const agentAfterDelete = await getAgent({ id: agentId }); |
| | expect(agentAfterDelete).toBeNull(); |
| |
|
| | |
| | const aclEntriesAfter = await AclEntry.find({ |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent._id, |
| | }); |
| | expect(aclEntriesAfter).toHaveLength(0); |
| | }); |
| |
|
| | test('should list agents by author', async () => { |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const otherAuthorId = new mongoose.Types.ObjectId(); |
| |
|
| | const agentIds = []; |
| | for (let i = 0; i < 5; i++) { |
| | const id = `agent_${uuidv4()}`; |
| | agentIds.push(id); |
| | await createAgent({ |
| | id, |
| | name: `Agent ${i}`, |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| | } |
| |
|
| | for (let i = 0; i < 3; i++) { |
| | await createAgent({ |
| | id: `other_agent_${uuidv4()}`, |
| | name: `Other Agent ${i}`, |
| | provider: 'test', |
| | model: 'test-model', |
| | author: otherAuthorId, |
| | }); |
| | } |
| |
|
| | const result = await getListAgents({ author: authorId.toString() }); |
| |
|
| | expect(result).toBeDefined(); |
| | expect(result.data).toBeDefined(); |
| | expect(result.data).toHaveLength(5); |
| | expect(result.has_more).toBe(true); |
| |
|
| | for (const agent of result.data) { |
| | expect(agent.author).toBe(authorId.toString()); |
| | } |
| | }); |
| |
|
| | test('should update agent projects', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const projectId1 = new mongoose.Types.ObjectId(); |
| | const projectId2 = new mongoose.Types.ObjectId(); |
| | const projectId3 = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Project Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | projectIds: [projectId1], |
| | }); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { $addToSet: { projectIds: { $each: [projectId2, projectId3] } } }, |
| | ); |
| |
|
| | await updateAgent({ id: agentId }, { $pull: { projectIds: projectId1 } }); |
| |
|
| | await updateAgent({ id: agentId }, { projectIds: [projectId2, projectId3] }); |
| |
|
| | const updatedAgent = await getAgent({ id: agentId }); |
| | expect(updatedAgent.projectIds).toHaveLength(2); |
| | expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); |
| | expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId3.toString()); |
| | expect(updatedAgent.projectIds.map((id) => id.toString())).not.toContain( |
| | projectId1.toString(), |
| | ); |
| |
|
| | await updateAgent({ id: agentId }, { projectIds: [] }); |
| |
|
| | const emptyProjectsAgent = await getAgent({ id: agentId }); |
| | expect(emptyProjectsAgent.projectIds).toHaveLength(0); |
| |
|
| | const nonExistentId = `agent_${uuidv4()}`; |
| | await expect( |
| | updateAgentProjects({ |
| | id: nonExistentId, |
| | projectIds: [projectId1], |
| | }), |
| | ).rejects.toThrow(); |
| | }); |
| |
|
| | test('should handle ephemeral agent loading', async () => { |
| | const agentId = 'ephemeral_test'; |
| | const endpoint = 'openai'; |
| |
|
| | const originalModule = jest.requireActual('librechat-data-provider'); |
| |
|
| | const mockDataProvider = { |
| | ...originalModule, |
| | Constants: { |
| | ...originalModule.Constants, |
| | EPHEMERAL_AGENT_ID: 'ephemeral_test', |
| | }, |
| | }; |
| |
|
| | jest.doMock('librechat-data-provider', () => mockDataProvider); |
| |
|
| | expect(agentId).toBeDefined(); |
| | expect(endpoint).toBeDefined(); |
| |
|
| | jest.dontMock('librechat-data-provider'); |
| | }); |
| |
|
| | test('should handle loadAgent functionality and errors', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Load Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | tools: ['tool1', 'tool2'], |
| | }); |
| |
|
| | const agent = await getAgent({ id: agentId }); |
| |
|
| | expect(agent).toBeDefined(); |
| | expect(agent.id).toBe(agentId); |
| | expect(agent.name).toBe('Test Load Agent'); |
| | expect(agent.tools).toEqual(expect.arrayContaining(['tool1', 'tool2'])); |
| |
|
| | const mockLoadAgent = jest.fn().mockResolvedValue(agent); |
| | const loadedAgent = await mockLoadAgent(); |
| | expect(loadedAgent).toBeDefined(); |
| | expect(loadedAgent.id).toBe(agentId); |
| |
|
| | const nonExistentId = `agent_${uuidv4()}`; |
| | const nonExistentAgent = await getAgent({ id: nonExistentId }); |
| | expect(nonExistentAgent).toBeNull(); |
| |
|
| | const mockLoadAgentError = jest.fn().mockRejectedValue(new Error('No agent found with ID')); |
| | await expect(mockLoadAgentError()).rejects.toThrow('No agent found with ID'); |
| | }); |
| |
|
| | describe('Edge Cases', () => { |
| | test.each([ |
| | { |
| | name: 'getAgent with undefined search parameters', |
| | fn: () => getAgent(undefined), |
| | expected: null, |
| | }, |
| | { |
| | name: 'deleteAgent with non-existent agent', |
| | fn: () => deleteAgent({ id: 'non-existent' }), |
| | expected: null, |
| | }, |
| | ])('$name should return null', async ({ fn, expected }) => { |
| | const result = await fn(); |
| | expect(result).toBe(expected); |
| | }); |
| |
|
| | test('should handle getListAgents with invalid author format', async () => { |
| | try { |
| | const result = await getListAgents({ author: 'invalid-object-id' }); |
| | expect(result.data).toEqual([]); |
| | } catch (error) { |
| | expect(error).toBeDefined(); |
| | } |
| | }); |
| |
|
| | test('should handle getListAgents with no agents', async () => { |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const result = await getListAgents({ author: authorId.toString() }); |
| |
|
| | expect(result).toBeDefined(); |
| | expect(result.data).toEqual([]); |
| | expect(result.has_more).toBe(false); |
| | expect(result.first_id).toBeNull(); |
| | expect(result.last_id).toBeNull(); |
| | }); |
| |
|
| | test('should handle updateAgentProjects with non-existent agent', async () => { |
| | const nonExistentId = `agent_${uuidv4()}`; |
| | const userId = new mongoose.Types.ObjectId(); |
| | const projectId = new mongoose.Types.ObjectId(); |
| |
|
| | const result = await updateAgentProjects({ |
| | user: { id: userId.toString() }, |
| | agentId: nonExistentId, |
| | projectIds: [projectId.toString()], |
| | }); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Agent Version History', () => { |
| | let mongoServer; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await mongoose.connect(mongoUri); |
| | }, 20000); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await Agent.deleteMany({}); |
| | }); |
| |
|
| | test('should create an agent with a single entry in versions array', async () => { |
| | const agent = await createBasicAgent(); |
| |
|
| | expect(agent.versions).toBeDefined(); |
| | expect(Array.isArray(agent.versions)).toBe(true); |
| | expect(agent.versions).toHaveLength(1); |
| | expect(agent.versions[0].name).toBe('Test Agent'); |
| | expect(agent.versions[0].provider).toBe('test'); |
| | expect(agent.versions[0].model).toBe('test-model'); |
| | }); |
| |
|
| | test('should accumulate version history across multiple updates', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const author = new mongoose.Types.ObjectId(); |
| | await createAgent({ |
| | id: agentId, |
| | name: 'First Name', |
| | provider: 'test', |
| | model: 'test-model', |
| | author, |
| | description: 'First description', |
| | }); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { name: 'Second Name', description: 'Second description' }, |
| | ); |
| | await updateAgent({ id: agentId }, { name: 'Third Name', model: 'new-model' }); |
| | const finalAgent = await updateAgent({ id: agentId }, { description: 'Final description' }); |
| |
|
| | expect(finalAgent.versions).toBeDefined(); |
| | expect(Array.isArray(finalAgent.versions)).toBe(true); |
| | expect(finalAgent.versions).toHaveLength(4); |
| |
|
| | expect(finalAgent.versions[0].name).toBe('First Name'); |
| | expect(finalAgent.versions[0].description).toBe('First description'); |
| | expect(finalAgent.versions[0].model).toBe('test-model'); |
| |
|
| | expect(finalAgent.versions[1].name).toBe('Second Name'); |
| | expect(finalAgent.versions[1].description).toBe('Second description'); |
| | expect(finalAgent.versions[1].model).toBe('test-model'); |
| |
|
| | expect(finalAgent.versions[2].name).toBe('Third Name'); |
| | expect(finalAgent.versions[2].description).toBe('Second description'); |
| | expect(finalAgent.versions[2].model).toBe('new-model'); |
| |
|
| | expect(finalAgent.versions[3].name).toBe('Third Name'); |
| | expect(finalAgent.versions[3].description).toBe('Final description'); |
| | expect(finalAgent.versions[3].model).toBe('new-model'); |
| |
|
| | expect(finalAgent.name).toBe('Third Name'); |
| | expect(finalAgent.description).toBe('Final description'); |
| | expect(finalAgent.model).toBe('new-model'); |
| | }); |
| |
|
| | test('should not include metadata fields in version history', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: new mongoose.Types.ObjectId(), |
| | }); |
| |
|
| | const updatedAgent = await updateAgent({ id: agentId }, { description: 'New description' }); |
| |
|
| | expect(updatedAgent.versions).toHaveLength(2); |
| | expect(updatedAgent.versions[0]._id).toBeUndefined(); |
| | expect(updatedAgent.versions[0].__v).toBeUndefined(); |
| | expect(updatedAgent.versions[0].name).toBe('Test Agent'); |
| | expect(updatedAgent.versions[0].author).toBeUndefined(); |
| |
|
| | expect(updatedAgent.versions[1]._id).toBeUndefined(); |
| | expect(updatedAgent.versions[1].__v).toBeUndefined(); |
| | }); |
| |
|
| | test('should not recursively include previous versions', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: new mongoose.Types.ObjectId(), |
| | }); |
| |
|
| | await updateAgent({ id: agentId }, { name: 'Updated Name 1' }); |
| | await updateAgent({ id: agentId }, { name: 'Updated Name 2' }); |
| | const finalAgent = await updateAgent({ id: agentId }, { name: 'Updated Name 3' }); |
| |
|
| | expect(finalAgent.versions).toHaveLength(4); |
| |
|
| | finalAgent.versions.forEach((version) => { |
| | expect(version.versions).toBeUndefined(); |
| | }); |
| | }); |
| |
|
| | test('should handle MongoDB operators and field updates correctly', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const projectId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'MongoDB Operator Test', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | tools: ['tool1'], |
| | }); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { |
| | description: 'Updated description', |
| | $push: { tools: 'tool2' }, |
| | $addToSet: { projectIds: projectId }, |
| | }, |
| | ); |
| |
|
| | const firstUpdate = await getAgent({ id: agentId }); |
| | expect(firstUpdate.description).toBe('Updated description'); |
| | expect(firstUpdate.tools).toContain('tool1'); |
| | expect(firstUpdate.tools).toContain('tool2'); |
| | expect(firstUpdate.projectIds.map((id) => id.toString())).toContain(projectId.toString()); |
| | expect(firstUpdate.versions).toHaveLength(2); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { |
| | tools: ['tool2', 'tool3'], |
| | }, |
| | ); |
| |
|
| | const secondUpdate = await getAgent({ id: agentId }); |
| | expect(secondUpdate.tools).toHaveLength(2); |
| | expect(secondUpdate.tools).toContain('tool2'); |
| | expect(secondUpdate.tools).toContain('tool3'); |
| | expect(secondUpdate.tools).not.toContain('tool1'); |
| | expect(secondUpdate.versions).toHaveLength(3); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { |
| | $push: { tools: 'tool3' }, |
| | }, |
| | ); |
| |
|
| | const thirdUpdate = await getAgent({ id: agentId }); |
| | const toolCount = thirdUpdate.tools.filter((t) => t === 'tool3').length; |
| | expect(toolCount).toBe(2); |
| | expect(thirdUpdate.versions).toHaveLength(4); |
| | }); |
| |
|
| | test('should handle parameter objects correctly', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Parameters Test', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | model_parameters: { temperature: 0.7 }, |
| | }); |
| |
|
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { model_parameters: { temperature: 0.8 } }, |
| | ); |
| |
|
| | expect(updatedAgent.versions).toHaveLength(2); |
| | expect(updatedAgent.model_parameters.temperature).toBe(0.8); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { |
| | model_parameters: { |
| | temperature: 0.8, |
| | max_tokens: 1000, |
| | }, |
| | }, |
| | ); |
| |
|
| | const complexAgent = await getAgent({ id: agentId }); |
| | expect(complexAgent.versions).toHaveLength(3); |
| | expect(complexAgent.model_parameters.temperature).toBe(0.8); |
| | expect(complexAgent.model_parameters.max_tokens).toBe(1000); |
| |
|
| | await updateAgent({ id: agentId }, { model_parameters: {} }); |
| |
|
| | const emptyParamsAgent = await getAgent({ id: agentId }); |
| | expect(emptyParamsAgent.versions).toHaveLength(4); |
| | expect(emptyParamsAgent.model_parameters).toEqual({}); |
| | }); |
| |
|
| | test('should not create new version for duplicate updates', async () => { |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const testCases = generateVersionTestCases(); |
| |
|
| | for (const testCase of testCases) { |
| | const testAgentId = `agent_${uuidv4()}`; |
| |
|
| | await createAgent({ |
| | id: testAgentId, |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | ...testCase.initial, |
| | }); |
| |
|
| | const updatedAgent = await updateAgent({ id: testAgentId }, testCase.update); |
| | expect(updatedAgent.versions).toHaveLength(2); |
| |
|
| | |
| | const duplicateUpdate = await updateAgent({ id: testAgentId }, testCase.duplicate); |
| |
|
| | expect(duplicateUpdate.versions).toHaveLength(2); |
| |
|
| | const agent = await getAgent({ id: testAgentId }); |
| | expect(agent.versions).toHaveLength(2); |
| | } |
| | }); |
| |
|
| | test('should track updatedBy when a different user updates an agent', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const originalAuthor = new mongoose.Types.ObjectId(); |
| | const updatingUser = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Original Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: originalAuthor, |
| | description: 'Original description', |
| | }); |
| |
|
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { name: 'Updated Agent', description: 'Updated description' }, |
| | { updatingUserId: updatingUser.toString() }, |
| | ); |
| |
|
| | expect(updatedAgent.versions).toHaveLength(2); |
| | expect(updatedAgent.versions[1].updatedBy.toString()).toBe(updatingUser.toString()); |
| | expect(updatedAgent.author.toString()).toBe(originalAuthor.toString()); |
| | }); |
| |
|
| | test('should include updatedBy even when the original author updates the agent', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const originalAuthor = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Original Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: originalAuthor, |
| | description: 'Original description', |
| | }); |
| |
|
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { name: 'Updated Agent', description: 'Updated description' }, |
| | { updatingUserId: originalAuthor.toString() }, |
| | ); |
| |
|
| | expect(updatedAgent.versions).toHaveLength(2); |
| | expect(updatedAgent.versions[1].updatedBy.toString()).toBe(originalAuthor.toString()); |
| | expect(updatedAgent.author.toString()).toBe(originalAuthor.toString()); |
| | }); |
| |
|
| | test('should track multiple different users updating the same agent', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const originalAuthor = new mongoose.Types.ObjectId(); |
| | const user1 = new mongoose.Types.ObjectId(); |
| | const user2 = new mongoose.Types.ObjectId(); |
| | const user3 = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Original Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: originalAuthor, |
| | description: 'Original description', |
| | }); |
| |
|
| | |
| | await updateAgent( |
| | { id: agentId }, |
| | { name: 'Updated by User 1', description: 'First update' }, |
| | { updatingUserId: user1.toString() }, |
| | ); |
| |
|
| | |
| | await updateAgent( |
| | { id: agentId }, |
| | { description: 'Updated by original author' }, |
| | { updatingUserId: originalAuthor.toString() }, |
| | ); |
| |
|
| | |
| | await updateAgent( |
| | { id: agentId }, |
| | { name: 'Updated by User 2', model: 'new-model' }, |
| | { updatingUserId: user2.toString() }, |
| | ); |
| |
|
| | |
| | const finalAgent = await updateAgent( |
| | { id: agentId }, |
| | { description: 'Final update by User 3' }, |
| | { updatingUserId: user3.toString() }, |
| | ); |
| |
|
| | expect(finalAgent.versions).toHaveLength(5); |
| | expect(finalAgent.author.toString()).toBe(originalAuthor.toString()); |
| |
|
| | |
| | expect(finalAgent.versions[0].updatedBy).toBeUndefined(); |
| | expect(finalAgent.versions[1].updatedBy.toString()).toBe(user1.toString()); |
| | expect(finalAgent.versions[2].updatedBy.toString()).toBe(originalAuthor.toString()); |
| | expect(finalAgent.versions[3].updatedBy.toString()).toBe(user2.toString()); |
| | expect(finalAgent.versions[4].updatedBy.toString()).toBe(user3.toString()); |
| |
|
| | |
| | expect(finalAgent.name).toBe('Updated by User 2'); |
| | expect(finalAgent.description).toBe('Final update by User 3'); |
| | expect(finalAgent.model).toBe('new-model'); |
| | }); |
| |
|
| | test('should preserve original author during agent restoration', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const originalAuthor = new mongoose.Types.ObjectId(); |
| | const updatingUser = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Original Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: originalAuthor, |
| | description: 'Original description', |
| | }); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { name: 'Updated Agent', description: 'Updated description' }, |
| | { updatingUserId: updatingUser.toString() }, |
| | ); |
| |
|
| | const { revertAgentVersion } = require('./Agent'); |
| | const revertedAgent = await revertAgentVersion({ id: agentId }, 0); |
| |
|
| | expect(revertedAgent.author.toString()).toBe(originalAuthor.toString()); |
| | expect(revertedAgent.name).toBe('Original Agent'); |
| | expect(revertedAgent.description).toBe('Original description'); |
| | }); |
| |
|
| | test('should detect action metadata changes and force version update', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const actionId = 'testActionId123'; |
| |
|
| | |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Agent with Actions', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | actions: [`test.com_action_${actionId}`], |
| | tools: ['listEvents_action_test.com', 'createEvent_action_test.com'], |
| | }); |
| |
|
| | |
| | const firstUpdate = await updateAgent( |
| | { id: agentId }, |
| | { tools: ['listEvents_action_test.com', 'createEvent_action_test.com'] }, |
| | { updatingUserId: authorId.toString(), forceVersion: true }, |
| | ); |
| |
|
| | expect(firstUpdate.versions).toHaveLength(2); |
| |
|
| | |
| | const secondUpdate = await updateAgent( |
| | { id: agentId }, |
| | { tools: ['listEvents_action_test.com', 'createEvent_action_test.com'] }, |
| | { updatingUserId: authorId.toString(), forceVersion: true }, |
| | ); |
| |
|
| | expect(secondUpdate.versions).toHaveLength(3); |
| |
|
| | |
| | const duplicateUpdate = await updateAgent( |
| | { id: agentId }, |
| | { tools: ['listEvents_action_test.com', 'createEvent_action_test.com'] }, |
| | { updatingUserId: authorId.toString(), forceVersion: false }, |
| | ); |
| |
|
| | expect(duplicateUpdate.versions).toHaveLength(3); |
| | }); |
| |
|
| | test('should handle isDuplicateVersion with arrays containing null/undefined values', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | tools: ['tool1', null, 'tool2', undefined], |
| | }); |
| |
|
| | |
| | const updatedAgent = await updateAgent({ id: agentId }, { tools: ['tool1', 'tool2'] }); |
| |
|
| | expect(updatedAgent.versions).toHaveLength(2); |
| | expect(updatedAgent.tools).toEqual(['tool1', 'tool2']); |
| | }); |
| |
|
| | test('should handle isDuplicateVersion with empty objects in tool_kwargs', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | tool_kwargs: [ |
| | { tool: 'tool1', config: { setting: 'value' } }, |
| | {}, |
| | { tool: 'tool2', config: {} }, |
| | ], |
| | }); |
| |
|
| | |
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { |
| | tool_kwargs: [ |
| | { tool: 'tool2', config: {} }, |
| | { tool: 'tool1', config: { setting: 'value' } }, |
| | {}, |
| | ], |
| | }, |
| | ); |
| |
|
| | |
| | expect(updatedAgent.versions).toHaveLength(2); |
| | }); |
| |
|
| | test('should handle isDuplicateVersion with mixed primitive and object arrays', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | mixed_array: [1, 'string', { key: 'value' }, true, null], |
| | }); |
| |
|
| | |
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { mixed_array: ['1', 'string', { key: 'value' }, 'true', null] }, |
| | ); |
| |
|
| | |
| | expect(updatedAgent.versions).toHaveLength(2); |
| | }); |
| |
|
| | test('should handle isDuplicateVersion with deeply nested objects', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | const deepObject = { |
| | level1: { |
| | level2: { |
| | level3: { |
| | level4: { |
| | value: 'deep', |
| | array: [1, 2, { nested: true }], |
| | }, |
| | }, |
| | }, |
| | }, |
| | }; |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | model_parameters: deepObject, |
| | }); |
| |
|
| | |
| | await updateAgent({ id: agentId }, { description: 'Updated' }); |
| |
|
| | |
| | await updateAgent( |
| | { id: agentId }, |
| | { |
| | model_parameters: deepObject, |
| | description: undefined, |
| | }, |
| | ); |
| |
|
| | |
| | |
| | const agent = await getAgent({ id: agentId }); |
| | expect(agent.versions).toHaveLength(3); |
| | }); |
| |
|
| | test('should handle version comparison with special field types', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const projectId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | projectIds: [projectId], |
| | model_parameters: { temperature: 0.7 }, |
| | }); |
| |
|
| | |
| | const firstUpdate = await updateAgent({ id: agentId }, { description: 'New description' }); |
| |
|
| | expect(firstUpdate.versions).toHaveLength(2); |
| |
|
| | |
| | const secondUpdate = await updateAgent( |
| | { id: agentId }, |
| | { model_parameters: { temperature: 0.8 } }, |
| | ); |
| |
|
| | expect(secondUpdate.versions).toHaveLength(3); |
| | }); |
| |
|
| | test('should detect changes in support_contact fields', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Agent with Support Contact', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | support_contact: { |
| | name: 'Initial Support', |
| | email: 'initial@support.com', |
| | }, |
| | }); |
| |
|
| | |
| | const firstUpdate = await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'Updated Support', |
| | email: 'initial@support.com', |
| | }, |
| | }, |
| | ); |
| |
|
| | expect(firstUpdate.versions).toHaveLength(2); |
| | expect(firstUpdate.support_contact.name).toBe('Updated Support'); |
| | expect(firstUpdate.support_contact.email).toBe('initial@support.com'); |
| |
|
| | |
| | const secondUpdate = await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'Updated Support', |
| | email: 'updated@support.com', |
| | }, |
| | }, |
| | ); |
| |
|
| | expect(secondUpdate.versions).toHaveLength(3); |
| | expect(secondUpdate.support_contact.email).toBe('updated@support.com'); |
| |
|
| | |
| | const duplicateUpdate = await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'Updated Support', |
| | email: 'updated@support.com', |
| | }, |
| | }, |
| | ); |
| |
|
| | |
| | expect(duplicateUpdate.versions).toHaveLength(3); |
| | expect(duplicateUpdate.version).toBe(3); |
| | expect(duplicateUpdate.support_contact.email).toBe('updated@support.com'); |
| | }); |
| |
|
| | test('should handle support_contact from empty to populated', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | const agent = await createAgent({ |
| | id: agentId, |
| | name: 'Agent without Support', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | |
| | expect(agent.support_contact).toBeUndefined(); |
| |
|
| | |
| | const updated = await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'New Support Team', |
| | email: 'support@example.com', |
| | }, |
| | }, |
| | ); |
| |
|
| | expect(updated.versions).toHaveLength(2); |
| | expect(updated.support_contact.name).toBe('New Support Team'); |
| | expect(updated.support_contact.email).toBe('support@example.com'); |
| | }); |
| |
|
| | test('should handle support_contact edge cases in isDuplicateVersion', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Edge Case Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | support_contact: { |
| | name: 'Support', |
| | email: 'support@test.com', |
| | }, |
| | }); |
| |
|
| | |
| | const emptyUpdate = await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: {}, |
| | }, |
| | ); |
| |
|
| | expect(emptyUpdate.versions).toHaveLength(2); |
| | expect(emptyUpdate.support_contact).toEqual({}); |
| |
|
| | |
| | const repopulated = await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'Support', |
| | email: 'support@test.com', |
| | }, |
| | }, |
| | ); |
| |
|
| | expect(repopulated.versions).toHaveLength(3); |
| |
|
| | |
| | const finalAgent = await getAgent({ id: agentId }); |
| | expect(finalAgent.versions[0].support_contact).toEqual({ |
| | name: 'Support', |
| | email: 'support@test.com', |
| | }); |
| | expect(finalAgent.versions[1].support_contact).toEqual({}); |
| | expect(finalAgent.versions[2].support_contact).toEqual({ |
| | name: 'Support', |
| | email: 'support@test.com', |
| | }); |
| | }); |
| |
|
| | test('should preserve support_contact in version history', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Version History Test', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | support_contact: { |
| | name: 'Initial Contact', |
| | email: 'initial@test.com', |
| | }, |
| | }); |
| |
|
| | |
| | await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'Second Contact', |
| | email: 'second@test.com', |
| | }, |
| | }, |
| | ); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'Third Contact', |
| | email: 'third@test.com', |
| | }, |
| | }, |
| | ); |
| |
|
| | const finalAgent = await getAgent({ id: agentId }); |
| |
|
| | |
| | expect(finalAgent.versions).toHaveLength(3); |
| | expect(finalAgent.versions[0].support_contact).toEqual({ |
| | name: 'Initial Contact', |
| | email: 'initial@test.com', |
| | }); |
| | expect(finalAgent.versions[1].support_contact).toEqual({ |
| | name: 'Second Contact', |
| | email: 'second@test.com', |
| | }); |
| | expect(finalAgent.versions[2].support_contact).toEqual({ |
| | name: 'Third Contact', |
| | email: 'third@test.com', |
| | }); |
| |
|
| | |
| | expect(finalAgent.support_contact).toEqual({ |
| | name: 'Third Contact', |
| | email: 'third@test.com', |
| | }); |
| | }); |
| |
|
| | test('should handle partial support_contact updates', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Partial Update Test', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | support_contact: { |
| | name: 'Original Name', |
| | email: 'original@email.com', |
| | }, |
| | }); |
| |
|
| | |
| | |
| | const updated = await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'New Name', |
| | email: '', |
| | }, |
| | }, |
| | ); |
| |
|
| | expect(updated.versions).toHaveLength(2); |
| | expect(updated.support_contact.name).toBe('New Name'); |
| | expect(updated.support_contact.email).toBe(''); |
| |
|
| | |
| | const duplicateUpdate = await updateAgent( |
| | { id: agentId }, |
| | { |
| | support_contact: { |
| | name: 'New Name', |
| | email: '', |
| | }, |
| | }, |
| | ); |
| |
|
| | |
| | expect(duplicateUpdate.versions).toHaveLength(2); |
| | expect(duplicateUpdate.version).toBe(2); |
| | expect(duplicateUpdate.support_contact.name).toBe('New Name'); |
| | expect(duplicateUpdate.support_contact.email).toBe(''); |
| | }); |
| |
|
| | |
| | describe.each([ |
| | { |
| | operation: 'add', |
| | name: 'empty file_id', |
| | needsAgent: true, |
| | params: { tool_resource: 'file_search', file_id: '' }, |
| | shouldResolve: true, |
| | }, |
| | { |
| | operation: 'add', |
| | name: 'non-existent agent', |
| | needsAgent: false, |
| | params: { tool_resource: 'file_search', file_id: 'file123' }, |
| | shouldResolve: false, |
| | error: 'Agent not found for adding resource file', |
| | }, |
| | ])('addAgentResourceFile with $name', ({ needsAgent, params, shouldResolve, error }) => { |
| | test(`should ${shouldResolve ? 'resolve' : 'reject'}`, async () => { |
| | const agent = needsAgent ? await createBasicAgent() : null; |
| | const agent_id = needsAgent ? agent.id : `agent_${uuidv4()}`; |
| |
|
| | if (shouldResolve) { |
| | await expect(addAgentResourceFile({ agent_id, ...params })).resolves.toBeDefined(); |
| | } else { |
| | await expect(addAgentResourceFile({ agent_id, ...params })).rejects.toThrow(error); |
| | } |
| | }); |
| | }); |
| |
|
| | describe.each([ |
| | { |
| | name: 'empty files array', |
| | files: [], |
| | needsAgent: true, |
| | shouldResolve: true, |
| | }, |
| | { |
| | name: 'non-existent tool_resource', |
| | files: [{ tool_resource: 'non_existent_tool', file_id: 'file123' }], |
| | needsAgent: true, |
| | shouldResolve: true, |
| | }, |
| | { |
| | name: 'non-existent agent', |
| | files: [{ tool_resource: 'file_search', file_id: 'file123' }], |
| | needsAgent: false, |
| | shouldResolve: false, |
| | error: 'Agent not found for removing resource files', |
| | }, |
| | ])('removeAgentResourceFiles with $name', ({ files, needsAgent, shouldResolve, error }) => { |
| | test(`should ${shouldResolve ? 'resolve' : 'reject'}`, async () => { |
| | const agent = needsAgent ? await createBasicAgent() : null; |
| | const agent_id = needsAgent ? agent.id : `agent_${uuidv4()}`; |
| |
|
| | if (shouldResolve) { |
| | const result = await removeAgentResourceFiles({ agent_id, files }); |
| | expect(result).toBeDefined(); |
| | if (agent) { |
| | expect(result.id).toBe(agent.id); |
| | } |
| | } else { |
| | await expect(removeAgentResourceFiles({ agent_id, files })).rejects.toThrow(error); |
| | } |
| | }); |
| | }); |
| |
|
| | describe('Edge Cases', () => { |
| | test('should handle extremely large version history', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Version Test', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | for (let i = 0; i < 20; i++) { |
| | await updateAgent({ id: agentId }, { description: `Version ${i}` }); |
| | } |
| |
|
| | const agent = await getAgent({ id: agentId }); |
| | expect(agent.versions).toHaveLength(21); |
| | expect(agent.description).toBe('Version 19'); |
| | }); |
| |
|
| | test('should handle revertAgentVersion with invalid version index', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | await expect(revertAgentVersion({ id: agentId }, 5)).rejects.toThrow('Version 5 not found'); |
| | }); |
| |
|
| | test('should handle revertAgentVersion with non-existent agent', async () => { |
| | const nonExistentId = `agent_${uuidv4()}`; |
| |
|
| | await expect(revertAgentVersion({ id: nonExistentId }, 0)).rejects.toThrow( |
| | 'Agent not found', |
| | ); |
| | }); |
| |
|
| | test('should handle updateAgent with empty update object', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | const updatedAgent = await updateAgent({ id: agentId }, {}); |
| |
|
| | expect(updatedAgent).toBeDefined(); |
| | expect(updatedAgent.name).toBe('Test Agent'); |
| | expect(updatedAgent.versions).toHaveLength(1); |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Action Metadata and Hash Generation', () => { |
| | let mongoServer; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await mongoose.connect(mongoUri); |
| | }, 20000); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await Agent.deleteMany({}); |
| | }); |
| |
|
| | test('should generate consistent hash for same action metadata', async () => { |
| | const actionIds = ['test.com_action_123', 'example.com_action_456']; |
| | const actions = [ |
| | { |
| | action_id: '123', |
| | metadata: { version: '1.0', endpoints: ['GET /api/test'], schema: { type: 'object' } }, |
| | }, |
| | { |
| | action_id: '456', |
| | metadata: { |
| | version: '2.0', |
| | endpoints: ['POST /api/example'], |
| | schema: { type: 'string' }, |
| | }, |
| | }, |
| | ]; |
| |
|
| | const hash1 = await generateActionMetadataHash(actionIds, actions); |
| | const hash2 = await generateActionMetadataHash(actionIds, actions); |
| |
|
| | expect(hash1).toBe(hash2); |
| | expect(typeof hash1).toBe('string'); |
| | expect(hash1.length).toBe(64); |
| | }); |
| |
|
| | test('should generate different hashes for different action metadata', async () => { |
| | const actionIds = ['test.com_action_123']; |
| | const actions1 = [ |
| | { action_id: '123', metadata: { version: '1.0', endpoints: ['GET /api/test'] } }, |
| | ]; |
| | const actions2 = [ |
| | { action_id: '123', metadata: { version: '2.0', endpoints: ['GET /api/test'] } }, |
| | ]; |
| |
|
| | const hash1 = await generateActionMetadataHash(actionIds, actions1); |
| | const hash2 = await generateActionMetadataHash(actionIds, actions2); |
| |
|
| | expect(hash1).not.toBe(hash2); |
| | }); |
| |
|
| | test('should handle empty action arrays', async () => { |
| | const hash = await generateActionMetadataHash([], []); |
| | expect(hash).toBe(''); |
| | }); |
| |
|
| | test('should handle null or undefined action arrays', async () => { |
| | const hash1 = await generateActionMetadataHash(null, []); |
| | const hash2 = await generateActionMetadataHash(undefined, []); |
| |
|
| | expect(hash1).toBe(''); |
| | expect(hash2).toBe(''); |
| | }); |
| |
|
| | test('should handle missing action metadata gracefully', async () => { |
| | const actionIds = ['test.com_action_123', 'missing.com_action_999']; |
| | const actions = [ |
| | { action_id: '123', metadata: { version: '1.0' } }, |
| | |
| | ]; |
| |
|
| | const hash = await generateActionMetadataHash(actionIds, actions); |
| | expect(typeof hash).toBe('string'); |
| | expect(hash.length).toBe(64); |
| | }); |
| |
|
| | test('should sort action IDs for consistent hashing', async () => { |
| | const actionIds1 = ['b.com_action_2', 'a.com_action_1']; |
| | const actionIds2 = ['a.com_action_1', 'b.com_action_2']; |
| | const actions = [ |
| | { action_id: '1', metadata: { version: '1.0' } }, |
| | { action_id: '2', metadata: { version: '2.0' } }, |
| | ]; |
| |
|
| | const hash1 = await generateActionMetadataHash(actionIds1, actions); |
| | const hash2 = await generateActionMetadataHash(actionIds2, actions); |
| |
|
| | expect(hash1).toBe(hash2); |
| | }); |
| |
|
| | test('should handle complex nested metadata objects', async () => { |
| | const actionIds = ['complex.com_action_1']; |
| | const actions = [ |
| | { |
| | action_id: '1', |
| | metadata: { |
| | version: '1.0', |
| | schema: { |
| | type: 'object', |
| | properties: { |
| | name: { type: 'string' }, |
| | nested: { |
| | type: 'object', |
| | properties: { |
| | id: { type: 'number' }, |
| | tags: { type: 'array', items: { type: 'string' } }, |
| | }, |
| | }, |
| | }, |
| | }, |
| | endpoints: [ |
| | { path: '/api/test', method: 'GET', params: ['id'] }, |
| | { path: '/api/create', method: 'POST', body: true }, |
| | ], |
| | }, |
| | }, |
| | ]; |
| |
|
| | const hash = await generateActionMetadataHash(actionIds, actions); |
| | expect(typeof hash).toBe('string'); |
| | expect(hash.length).toBe(64); |
| | }); |
| |
|
| | describe('Edge Cases', () => { |
| | test('should handle generateActionMetadataHash with null metadata', async () => { |
| | const hash = await generateActionMetadataHash( |
| | ['test.com_action_1'], |
| | [{ action_id: '1', metadata: null }], |
| | ); |
| | expect(typeof hash).toBe('string'); |
| | }); |
| |
|
| | test('should handle generateActionMetadataHash with deeply nested metadata', async () => { |
| | const deepMetadata = { |
| | level1: { |
| | level2: { |
| | level3: { |
| | level4: { |
| | level5: 'deep value', |
| | array: [1, 2, { nested: true }], |
| | }, |
| | }, |
| | }, |
| | }, |
| | }; |
| |
|
| | const hash = await generateActionMetadataHash( |
| | ['test.com_action_1'], |
| | [{ action_id: '1', metadata: deepMetadata }], |
| | ); |
| |
|
| | expect(typeof hash).toBe('string'); |
| | expect(hash.length).toBe(64); |
| | }); |
| |
|
| | test('should handle generateActionMetadataHash with special characters', async () => { |
| | const specialMetadata = { |
| | unicode: '🚀🎉👍', |
| | symbols: '!@#$%^&*()_+-=[]{}|;:,.<>?', |
| | quotes: 'single\'s and "doubles"', |
| | newlines: 'line1\nline2\r\nline3', |
| | }; |
| |
|
| | const hash = await generateActionMetadataHash( |
| | ['test.com_action_1'], |
| | [{ action_id: '1', metadata: specialMetadata }], |
| | ); |
| |
|
| | expect(typeof hash).toBe('string'); |
| | expect(hash.length).toBe(64); |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Load Agent Functionality', () => { |
| | let mongoServer; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await mongoose.connect(mongoUri); |
| | }, 20000); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await Agent.deleteMany({}); |
| | }); |
| |
|
| | test('should return null when agent_id is not provided', async () => { |
| | const mockReq = { user: { id: 'user123' } }; |
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: null, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | test('should return null when agent_id is empty string', async () => { |
| | const mockReq = { user: { id: 'user123' } }; |
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: '', |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | test('should test ephemeral agent loading logic', async () => { |
| | const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; |
| |
|
| | getCachedTools.mockResolvedValue({ |
| | tool1_mcp_server1: {}, |
| | tool2_mcp_server2: {}, |
| | another_tool: {}, |
| | }); |
| |
|
| | |
| | getMCPServerTools.mockImplementation(async (_userId, server) => { |
| | if (server === 'server1') { |
| | return { tool1_mcp_server1: {} }; |
| | } else if (server === 'server2') { |
| | return { tool2_mcp_server2: {} }; |
| | } |
| | return null; |
| | }); |
| |
|
| | const mockReq = { |
| | user: { id: 'user123' }, |
| | body: { |
| | promptPrefix: 'Test instructions', |
| | ephemeralAgent: { |
| | execute_code: true, |
| | web_search: true, |
| | mcp: ['server1', 'server2'], |
| | }, |
| | }, |
| | }; |
| |
|
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: EPHEMERAL_AGENT_ID, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4', temperature: 0.7 }, |
| | }); |
| |
|
| | if (result) { |
| | expect(result.id).toBe(EPHEMERAL_AGENT_ID); |
| | expect(result.instructions).toBe('Test instructions'); |
| | expect(result.provider).toBe('openai'); |
| | expect(result.model).toBe('gpt-4'); |
| | expect(result.model_parameters.temperature).toBe(0.7); |
| | expect(result.tools).toContain('execute_code'); |
| | expect(result.tools).toContain('web_search'); |
| | expect(result.tools).toContain('tool1_mcp_server1'); |
| | expect(result.tools).toContain('tool2_mcp_server2'); |
| | } else { |
| | expect(result).toBeNull(); |
| | } |
| | }); |
| |
|
| | test('should return null for non-existent agent', async () => { |
| | const mockReq = { user: { id: 'user123' } }; |
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: 'non_existent_agent', |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | test('should load agent when user is the author', async () => { |
| | const userId = new mongoose.Types.ObjectId(); |
| | const agentId = `agent_${uuidv4()}`; |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userId, |
| | description: 'Test description', |
| | tools: ['web_search'], |
| | }); |
| |
|
| | const mockReq = { user: { id: userId.toString() } }; |
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: agentId, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | expect(result).toBeDefined(); |
| | expect(result.id).toBe(agentId); |
| | expect(result.name).toBe('Test Agent'); |
| | expect(result.author.toString()).toBe(userId.toString()); |
| | expect(result.version).toBe(1); |
| | }); |
| |
|
| | test('should return agent even when user is not author (permissions checked at route level)', async () => { |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const userId = new mongoose.Types.ObjectId(); |
| | const agentId = `agent_${uuidv4()}`; |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: authorId, |
| | }); |
| |
|
| | const mockReq = { user: { id: userId.toString() } }; |
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: agentId, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | |
| | |
| | expect(result).toBeTruthy(); |
| | expect(result.id).toBe(agentId); |
| | expect(result.name).toBe('Test Agent'); |
| | }); |
| |
|
| | test('should handle ephemeral agent with no MCP servers', async () => { |
| | const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; |
| |
|
| | getCachedTools.mockResolvedValue({}); |
| |
|
| | const mockReq = { |
| | user: { id: 'user123' }, |
| | body: { |
| | promptPrefix: 'Simple instructions', |
| | ephemeralAgent: { |
| | execute_code: false, |
| | web_search: false, |
| | mcp: [], |
| | }, |
| | }, |
| | }; |
| |
|
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: EPHEMERAL_AGENT_ID, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-3.5-turbo' }, |
| | }); |
| |
|
| | if (result) { |
| | expect(result.tools).toEqual([]); |
| | expect(result.instructions).toBe('Simple instructions'); |
| | } else { |
| | expect(result).toBeFalsy(); |
| | } |
| | }); |
| |
|
| | test('should handle ephemeral agent with undefined ephemeralAgent in body', async () => { |
| | const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; |
| |
|
| | getCachedTools.mockResolvedValue({}); |
| |
|
| | const mockReq = { |
| | user: { id: 'user123' }, |
| | body: { |
| | promptPrefix: 'Basic instructions', |
| | }, |
| | }; |
| |
|
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: EPHEMERAL_AGENT_ID, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | if (result) { |
| | expect(result.tools).toEqual([]); |
| | } else { |
| | expect(result).toBeFalsy(); |
| | } |
| | }); |
| |
|
| | describe('Edge Cases', () => { |
| | test('should handle loadAgent with malformed req object', async () => { |
| | const result = await loadAgent({ |
| | req: null, |
| | agent_id: 'test', |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | test('should handle ephemeral agent with extremely large tool list', async () => { |
| | const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; |
| |
|
| | const largeToolList = Array.from({ length: 100 }, (_, i) => `tool_${i}_mcp_server1`); |
| | const availableTools = largeToolList.reduce((acc, tool) => { |
| | acc[tool] = {}; |
| | return acc; |
| | }, {}); |
| |
|
| | getCachedTools.mockResolvedValue(availableTools); |
| |
|
| | |
| | getMCPServerTools.mockImplementation(async (_userId, server) => { |
| | if (server === 'server1') { |
| | return availableTools; |
| | } |
| | return null; |
| | }); |
| |
|
| | const mockReq = { |
| | user: { id: 'user123' }, |
| | body: { |
| | promptPrefix: 'Test', |
| | ephemeralAgent: { |
| | execute_code: true, |
| | web_search: true, |
| | mcp: ['server1'], |
| | }, |
| | }, |
| | }; |
| |
|
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: EPHEMERAL_AGENT_ID, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | if (result) { |
| | expect(result.tools.length).toBeGreaterThan(100); |
| | } |
| | }); |
| |
|
| | test('should return agent from different project (permissions checked at route level)', async () => { |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const userId = new mongoose.Types.ObjectId(); |
| | const agentId = `agent_${uuidv4()}`; |
| | const projectId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Project Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: authorId, |
| | projectIds: [projectId], |
| | }); |
| |
|
| | const mockReq = { user: { id: userId.toString() } }; |
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: agentId, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | |
| | |
| | expect(result).toBeTruthy(); |
| | expect(result.id).toBe(agentId); |
| | expect(result.name).toBe('Project Agent'); |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Agent Edge Cases and Error Handling', () => { |
| | let mongoServer; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await mongoose.connect(mongoUri); |
| | }, 20000); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await Agent.deleteMany({}); |
| | }); |
| |
|
| | test('should handle agent creation with minimal required fields', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | const agent = await createAgent({ |
| | id: agentId, |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | expect(agent).toBeDefined(); |
| | expect(agent.id).toBe(agentId); |
| | expect(agent.versions).toHaveLength(1); |
| | expect(agent.versions[0].provider).toBe('test'); |
| | expect(agent.versions[0].model).toBe('test-model'); |
| | }); |
| |
|
| | test('should handle agent creation with all optional fields', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const projectId = new mongoose.Types.ObjectId(); |
| |
|
| | const agent = await createAgent({ |
| | id: agentId, |
| | name: 'Complex Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | description: 'Complex description', |
| | instructions: 'Complex instructions', |
| | tools: ['tool1', 'tool2'], |
| | actions: ['action1', 'action2'], |
| | model_parameters: { temperature: 0.8, max_tokens: 1000 }, |
| | projectIds: [projectId], |
| | avatar: 'https://example.com/avatar.png', |
| | isCollaborative: true, |
| | tool_resources: { |
| | file_search: { file_ids: ['file1', 'file2'] }, |
| | }, |
| | }); |
| |
|
| | expect(agent).toBeDefined(); |
| | expect(agent.name).toBe('Complex Agent'); |
| | expect(agent.description).toBe('Complex description'); |
| | expect(agent.instructions).toBe('Complex instructions'); |
| | expect(agent.tools).toEqual(['tool1', 'tool2']); |
| | expect(agent.actions).toEqual(['action1', 'action2']); |
| | expect(agent.model_parameters.temperature).toBe(0.8); |
| | expect(agent.model_parameters.max_tokens).toBe(1000); |
| | expect(agent.projectIds.map((id) => id.toString())).toContain(projectId.toString()); |
| | expect(agent.avatar).toBe('https://example.com/avatar.png'); |
| | expect(agent.isCollaborative).toBe(true); |
| | expect(agent.tool_resources.file_search.file_ids).toEqual(['file1', 'file2']); |
| | }); |
| |
|
| | test('should handle updateAgent with empty update object', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | const updatedAgent = await updateAgent({ id: agentId }, {}); |
| |
|
| | expect(updatedAgent).toBeDefined(); |
| | expect(updatedAgent.name).toBe('Test Agent'); |
| | expect(updatedAgent.versions).toHaveLength(1); |
| | }); |
| |
|
| | test('should handle concurrent updates to different agents', async () => { |
| | const agent1Id = `agent_${uuidv4()}`; |
| | const agent2Id = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agent1Id, |
| | name: 'Agent 1', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | await createAgent({ |
| | id: agent2Id, |
| | name: 'Agent 2', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | |
| | const [updated1, updated2] = await Promise.all([ |
| | updateAgent({ id: agent1Id }, { description: 'Updated Agent 1' }), |
| | updateAgent({ id: agent2Id }, { description: 'Updated Agent 2' }), |
| | ]); |
| |
|
| | expect(updated1.description).toBe('Updated Agent 1'); |
| | expect(updated2.description).toBe('Updated Agent 2'); |
| | expect(updated1.versions).toHaveLength(2); |
| | expect(updated2.versions).toHaveLength(2); |
| | }); |
| |
|
| | test('should handle agent deletion with non-existent ID', async () => { |
| | const nonExistentId = `agent_${uuidv4()}`; |
| | const result = await deleteAgent({ id: nonExistentId }); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | test('should handle getListAgents with no agents', async () => { |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const result = await getListAgents({ author: authorId.toString() }); |
| |
|
| | expect(result).toBeDefined(); |
| | expect(result.data).toEqual([]); |
| | expect(result.has_more).toBe(false); |
| | expect(result.first_id).toBeNull(); |
| | expect(result.last_id).toBeNull(); |
| | }); |
| |
|
| | test('should handle updateAgent with MongoDB operators mixed with direct updates', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | tools: ['tool1'], |
| | }); |
| |
|
| | |
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { |
| | name: 'Updated Name', |
| | $push: { tools: 'tool2' }, |
| | }, |
| | ); |
| |
|
| | expect(updatedAgent.name).toBe('Updated Name'); |
| | expect(updatedAgent.tools).toContain('tool1'); |
| | expect(updatedAgent.tools).toContain('tool2'); |
| | expect(updatedAgent.versions).toHaveLength(2); |
| | }); |
| |
|
| | test('should handle revertAgentVersion with invalid version index', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | |
| | await expect(revertAgentVersion({ id: agentId }, 5)).rejects.toThrow('Version 5 not found'); |
| | }); |
| |
|
| | test('should handle revertAgentVersion with non-existent agent', async () => { |
| | const nonExistentId = `agent_${uuidv4()}`; |
| |
|
| | await expect(revertAgentVersion({ id: nonExistentId }, 0)).rejects.toThrow('Agent not found'); |
| | }); |
| |
|
| | test('should handle addAgentResourceFile with non-existent agent', async () => { |
| | const nonExistentId = `agent_${uuidv4()}`; |
| | const mockReq = { user: { id: 'user123' } }; |
| |
|
| | await expect( |
| | addAgentResourceFile({ |
| | req: mockReq, |
| | agent_id: nonExistentId, |
| | tool_resource: 'file_search', |
| | file_id: 'file123', |
| | }), |
| | ).rejects.toThrow('Agent not found for adding resource file'); |
| | }); |
| |
|
| | test('should handle removeAgentResourceFiles with non-existent agent', async () => { |
| | const nonExistentId = `agent_${uuidv4()}`; |
| |
|
| | await expect( |
| | removeAgentResourceFiles({ |
| | agent_id: nonExistentId, |
| | files: [{ tool_resource: 'file_search', file_id: 'file123' }], |
| | }), |
| | ).rejects.toThrow('Agent not found for removing resource files'); |
| | }); |
| |
|
| | test('should handle updateAgent with complex nested updates', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | model_parameters: { temperature: 0.5 }, |
| | tools: ['tool1'], |
| | }); |
| |
|
| | |
| | const firstUpdate = await updateAgent( |
| | { id: agentId }, |
| | { |
| | $push: { tools: 'tool2' }, |
| | }, |
| | ); |
| |
|
| | expect(firstUpdate.tools).toContain('tool1'); |
| | expect(firstUpdate.tools).toContain('tool2'); |
| |
|
| | |
| | const secondUpdate = await updateAgent( |
| | { id: agentId }, |
| | { |
| | name: 'Updated Agent', |
| | model_parameters: { temperature: 0.8, max_tokens: 500 }, |
| | $addToSet: { tools: 'tool3' }, |
| | }, |
| | ); |
| |
|
| | expect(secondUpdate.name).toBe('Updated Agent'); |
| | expect(secondUpdate.model_parameters.temperature).toBe(0.8); |
| | expect(secondUpdate.model_parameters.max_tokens).toBe(500); |
| | expect(secondUpdate.tools).toContain('tool1'); |
| | expect(secondUpdate.tools).toContain('tool2'); |
| | expect(secondUpdate.tools).toContain('tool3'); |
| | expect(secondUpdate.versions).toHaveLength(3); |
| | }); |
| |
|
| | test('should preserve version order in versions array', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Version 1', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | await updateAgent({ id: agentId }, { name: 'Version 2' }); |
| | await updateAgent({ id: agentId }, { name: 'Version 3' }); |
| | const finalAgent = await updateAgent({ id: agentId }, { name: 'Version 4' }); |
| |
|
| | expect(finalAgent.versions).toHaveLength(4); |
| | expect(finalAgent.versions[0].name).toBe('Version 1'); |
| | expect(finalAgent.versions[1].name).toBe('Version 2'); |
| | expect(finalAgent.versions[2].name).toBe('Version 3'); |
| | expect(finalAgent.versions[3].name).toBe('Version 4'); |
| | expect(finalAgent.name).toBe('Version 4'); |
| | }); |
| |
|
| | test('should handle updateAgentProjects error scenarios', async () => { |
| | const nonExistentId = `agent_${uuidv4()}`; |
| | const userId = new mongoose.Types.ObjectId(); |
| | const projectId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | const result = await updateAgentProjects({ |
| | user: { id: userId.toString() }, |
| | agentId: nonExistentId, |
| | projectIds: [projectId.toString()], |
| | }); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | test('should handle revertAgentVersion properly', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Original Name', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | description: 'Original description', |
| | }); |
| |
|
| | await updateAgent( |
| | { id: agentId }, |
| | { name: 'Updated Name', description: 'Updated description' }, |
| | ); |
| |
|
| | const revertedAgent = await revertAgentVersion({ id: agentId }, 0); |
| |
|
| | expect(revertedAgent.name).toBe('Original Name'); |
| | expect(revertedAgent.description).toBe('Original description'); |
| | expect(revertedAgent.author.toString()).toBe(authorId.toString()); |
| | }); |
| |
|
| | test('should handle action-related updates with getActions error', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Agent with Actions', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | actions: ['test.com_action_invalid_id'], |
| | }); |
| |
|
| | |
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { description: 'Updated description' }, |
| | ); |
| |
|
| | expect(updatedAgent).toBeDefined(); |
| | expect(updatedAgent.description).toBe('Updated description'); |
| | expect(updatedAgent.versions).toHaveLength(2); |
| | }); |
| |
|
| | test('should handle updateAgent with combined MongoDB operators', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const projectId1 = new mongoose.Types.ObjectId(); |
| | const projectId2 = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | tools: ['tool1'], |
| | projectIds: [projectId1], |
| | }); |
| |
|
| | |
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { |
| | name: 'Updated Name', |
| | $push: { tools: 'tool2' }, |
| | $addToSet: { projectIds: projectId2 }, |
| | }, |
| | ); |
| |
|
| | const finalAgent = await updateAgent( |
| | { id: agentId }, |
| | { |
| | $pull: { projectIds: projectId1 }, |
| | }, |
| | ); |
| |
|
| | expect(updatedAgent).toBeDefined(); |
| | expect(updatedAgent.name).toBe('Updated Name'); |
| | expect(updatedAgent.tools).toContain('tool1'); |
| | expect(updatedAgent.tools).toContain('tool2'); |
| | expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); |
| |
|
| | expect(finalAgent).toBeDefined(); |
| | expect(finalAgent.projectIds.map((id) => id.toString())).not.toContain(projectId1.toString()); |
| | expect(finalAgent.versions).toHaveLength(3); |
| | }); |
| |
|
| | test('should handle updateAgent when agent does not exist', async () => { |
| | const nonExistentId = `agent_${uuidv4()}`; |
| |
|
| | const result = await updateAgent({ id: nonExistentId }, { name: 'New Name' }); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | test('should handle concurrent updates with database errors', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | |
| | const cleanup = mockFindOneAndUpdateError(2); |
| |
|
| | |
| | const promises = [ |
| | updateAgent({ id: agentId }, { name: 'Update 1' }), |
| | updateAgent({ id: agentId }, { name: 'Update 2' }), |
| | updateAgent({ id: agentId }, { name: 'Update 3' }), |
| | ]; |
| |
|
| | const results = await Promise.allSettled(promises); |
| |
|
| | cleanup(); |
| |
|
| | const succeeded = results.filter((r) => r.status === 'fulfilled').length; |
| | const failed = results.filter((r) => r.status === 'rejected').length; |
| |
|
| | expect(succeeded).toBe(2); |
| | expect(failed).toBe(1); |
| | }); |
| |
|
| | test('should handle removeAgentResourceFiles when agent is deleted during operation', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | tool_resources: { |
| | file_search: { |
| | file_ids: ['file1', 'file2', 'file3'], |
| | }, |
| | }, |
| | }); |
| |
|
| | |
| | const originalFindOneAndUpdate = Agent.findOneAndUpdate; |
| | Agent.findOneAndUpdate = jest.fn().mockImplementation(() => ({ |
| | lean: jest.fn().mockResolvedValue(null), |
| | })); |
| |
|
| | |
| | await expect( |
| | removeAgentResourceFiles({ |
| | agent_id: agentId, |
| | files: [ |
| | { tool_resource: 'file_search', file_id: 'file1' }, |
| | { tool_resource: 'file_search', file_id: 'file2' }, |
| | ], |
| | }), |
| | ).rejects.toThrow('Failed to update agent during file removal (pull step)'); |
| |
|
| | Agent.findOneAndUpdate = originalFindOneAndUpdate; |
| | }); |
| |
|
| | test('should handle loadEphemeralAgent with malformed MCP tool names', async () => { |
| | const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants; |
| |
|
| | getCachedTools.mockResolvedValue({ |
| | malformed_tool_name: {}, |
| | tool__server1: {}, |
| | tool_mcp_server1: {}, |
| | tool_mcp_server2: {}, |
| | }); |
| |
|
| | |
| | getMCPServerTools.mockImplementation(async (_userId, server) => { |
| | if (server === 'server1') { |
| | |
| | return { tool_mcp_server1: {} }; |
| | } else if (server === 'server2') { |
| | return { tool_mcp_server2: {} }; |
| | } |
| | return null; |
| | }); |
| |
|
| | const mockReq = { |
| | user: { id: 'user123' }, |
| | body: { |
| | promptPrefix: 'Test instructions', |
| | ephemeralAgent: { |
| | execute_code: false, |
| | web_search: false, |
| | mcp: ['server1'], |
| | }, |
| | }, |
| | }; |
| |
|
| | const result = await loadAgent({ |
| | req: mockReq, |
| | agent_id: EPHEMERAL_AGENT_ID, |
| | endpoint: 'openai', |
| | model_parameters: { model: 'gpt-4' }, |
| | }); |
| |
|
| | if (result) { |
| | expect(result.tools).toEqual(['tool_mcp_server1']); |
| | expect(result.tools).not.toContain('malformed_tool_name'); |
| | expect(result.tools).not.toContain('tool__server1'); |
| | expect(result.tools).not.toContain('tool_mcp_server2'); |
| | } |
| | }); |
| |
|
| | test('should handle addAgentResourceFile when array initialization fails', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | |
| | const originalUpdateOne = Agent.updateOne; |
| | let updateOneCalled = false; |
| | Agent.updateOne = jest.fn().mockImplementation((...args) => { |
| | if (!updateOneCalled) { |
| | updateOneCalled = true; |
| | return Promise.reject(new Error('Database error')); |
| | } |
| | return originalUpdateOne.apply(Agent, args); |
| | }); |
| |
|
| | try { |
| | const result = await addAgentResourceFile({ |
| | agent_id: agentId, |
| | tool_resource: 'new_tool', |
| | file_id: 'file123', |
| | }); |
| |
|
| | expect(result).toBeDefined(); |
| | expect(result.tools).toContain('new_tool'); |
| | } catch (error) { |
| | expect(error.message).toBe('Database error'); |
| | } |
| |
|
| | Agent.updateOne = originalUpdateOne; |
| | }); |
| | }); |
| |
|
| | describe('Agent IDs Field in Version Detection', () => { |
| | let mongoServer; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await mongoose.connect(mongoUri); |
| | }, 20000); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await Agent.deleteMany({}); |
| | }); |
| |
|
| | test('should now create new version when agent_ids field changes', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | const agent = await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | agent_ids: ['agent1', 'agent2'], |
| | }); |
| |
|
| | expect(agent).toBeDefined(); |
| | expect(agent.versions).toHaveLength(1); |
| |
|
| | const updated = await updateAgent( |
| | { id: agentId }, |
| | { agent_ids: ['agent1', 'agent2', 'agent3'] }, |
| | ); |
| |
|
| | |
| | expect(updated.versions).toHaveLength(2); |
| | expect(updated.agent_ids).toEqual(['agent1', 'agent2', 'agent3']); |
| | }); |
| |
|
| | test('should detect duplicate version if agent_ids is updated to same value', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | agent_ids: ['agent1', 'agent2'], |
| | }); |
| |
|
| | const updatedAgent = await updateAgent( |
| | { id: agentId }, |
| | { agent_ids: ['agent1', 'agent2', 'agent3'] }, |
| | ); |
| | expect(updatedAgent.versions).toHaveLength(2); |
| |
|
| | |
| | const duplicateUpdate = await updateAgent( |
| | { id: agentId }, |
| | { agent_ids: ['agent1', 'agent2', 'agent3'] }, |
| | ); |
| | expect(duplicateUpdate.versions).toHaveLength(2); |
| | }); |
| |
|
| | test('should handle agent_ids field alongside other fields', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | description: 'Initial description', |
| | agent_ids: ['agent1'], |
| | }); |
| |
|
| | const updated = await updateAgent( |
| | { id: agentId }, |
| | { |
| | agent_ids: ['agent1', 'agent2'], |
| | description: 'Updated description', |
| | }, |
| | ); |
| |
|
| | expect(updated.versions).toHaveLength(2); |
| | expect(updated.agent_ids).toEqual(['agent1', 'agent2']); |
| | expect(updated.description).toBe('Updated description'); |
| |
|
| | const updated2 = await updateAgent({ id: agentId }, { description: 'Another description' }); |
| |
|
| | expect(updated2.versions).toHaveLength(3); |
| | expect(updated2.agent_ids).toEqual(['agent1', 'agent2']); |
| | expect(updated2.description).toBe('Another description'); |
| | }); |
| |
|
| | test('should skip version creation when skipVersioning option is used', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| | const projectId1 = new mongoose.Types.ObjectId(); |
| | const projectId2 = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | projectIds: [projectId1], |
| | }); |
| |
|
| | |
| | const shared = await updateAgentProjects({ |
| | user: { id: authorId.toString() }, |
| | agentId: agentId, |
| | projectIds: [projectId2.toString()], |
| | }); |
| |
|
| | |
| | expect(shared.versions).toHaveLength(1); |
| | expect(shared.projectIds.map((id) => id.toString())).toContain(projectId1.toString()); |
| | expect(shared.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); |
| |
|
| | |
| | const unshared = await updateAgentProjects({ |
| | user: { id: authorId.toString() }, |
| | agentId: agentId, |
| | removeProjectIds: [projectId1.toString()], |
| | }); |
| |
|
| | |
| | expect(unshared.versions).toHaveLength(1); |
| | expect(unshared.projectIds.map((id) => id.toString())).not.toContain(projectId1.toString()); |
| | expect(unshared.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); |
| |
|
| | |
| | const regularUpdate = await updateAgent( |
| | { id: agentId }, |
| | { description: 'Updated description' }, |
| | ); |
| |
|
| | expect(regularUpdate.versions).toHaveLength(2); |
| | expect(regularUpdate.description).toBe('Updated description'); |
| |
|
| | |
| | const directUpdate = await updateAgent( |
| | { id: agentId }, |
| | { $addToSet: { projectIds: { $each: [projectId1] } } }, |
| | ); |
| |
|
| | expect(directUpdate.versions).toHaveLength(3); |
| | expect(directUpdate.projectIds.length).toBe(2); |
| | }); |
| |
|
| | test('should preserve agent_ids in version history', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | agent_ids: ['agent1'], |
| | }); |
| |
|
| | await updateAgent({ id: agentId }, { agent_ids: ['agent1', 'agent2'] }); |
| |
|
| | await updateAgent({ id: agentId }, { agent_ids: ['agent3'] }); |
| |
|
| | const finalAgent = await getAgent({ id: agentId }); |
| |
|
| | expect(finalAgent.versions).toHaveLength(3); |
| | expect(finalAgent.versions[0].agent_ids).toEqual(['agent1']); |
| | expect(finalAgent.versions[1].agent_ids).toEqual(['agent1', 'agent2']); |
| | expect(finalAgent.versions[2].agent_ids).toEqual(['agent3']); |
| | expect(finalAgent.agent_ids).toEqual(['agent3']); |
| | }); |
| |
|
| | test('should handle empty agent_ids arrays', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | agent_ids: ['agent1', 'agent2'], |
| | }); |
| |
|
| | const updated = await updateAgent({ id: agentId }, { agent_ids: [] }); |
| |
|
| | expect(updated.versions).toHaveLength(2); |
| | expect(updated.agent_ids).toEqual([]); |
| |
|
| | |
| | const duplicateUpdate = await updateAgent({ id: agentId }, { agent_ids: [] }); |
| | expect(duplicateUpdate.versions).toHaveLength(2); |
| | expect(duplicateUpdate.agent_ids).toEqual([]); |
| | }); |
| |
|
| | test('should handle agent without agent_ids field', async () => { |
| | const agentId = `agent_${uuidv4()}`; |
| | const authorId = new mongoose.Types.ObjectId(); |
| |
|
| | const agent = await createAgent({ |
| | id: agentId, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: authorId, |
| | }); |
| |
|
| | expect(agent.agent_ids).toEqual([]); |
| |
|
| | const updated = await updateAgent({ id: agentId }, { agent_ids: ['agent1'] }); |
| |
|
| | expect(updated.versions).toHaveLength(2); |
| | expect(updated.agent_ids).toEqual(['agent1']); |
| | }); |
| | }); |
| | }); |
| |
|
| | describe('Support Contact Field', () => { |
| | let mongoServer; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await mongoose.connect(mongoUri); |
| | }, 20000); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await Agent.deleteMany({}); |
| | }); |
| |
|
| | it('should not create subdocument with ObjectId for support_contact', async () => { |
| | const userId = new mongoose.Types.ObjectId(); |
| | const agentData = { |
| | id: 'agent_test_support', |
| | name: 'Test Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userId, |
| | support_contact: { |
| | name: 'Support Team', |
| | email: 'support@example.com', |
| | }, |
| | }; |
| |
|
| | |
| | const agent = await createAgent(agentData); |
| |
|
| | |
| | expect(agent.support_contact).toBeDefined(); |
| | expect(agent.support_contact.name).toBe('Support Team'); |
| | expect(agent.support_contact.email).toBe('support@example.com'); |
| |
|
| | |
| | expect(agent.support_contact._id).toBeUndefined(); |
| |
|
| | |
| | const dbAgent = await Agent.findOne({ id: agentData.id }); |
| | expect(dbAgent.support_contact).toBeDefined(); |
| | expect(dbAgent.support_contact.name).toBe('Support Team'); |
| | expect(dbAgent.support_contact.email).toBe('support@example.com'); |
| | expect(dbAgent.support_contact._id).toBeUndefined(); |
| | }); |
| |
|
| | it('should handle empty support_contact correctly', async () => { |
| | const userId = new mongoose.Types.ObjectId(); |
| | const agentData = { |
| | id: 'agent_test_empty_support', |
| | name: 'Test Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userId, |
| | support_contact: {}, |
| | }; |
| |
|
| | const agent = await createAgent(agentData); |
| |
|
| | |
| | expect(agent.support_contact).toEqual({}); |
| | expect(agent.support_contact._id).toBeUndefined(); |
| | }); |
| |
|
| | it('should handle missing support_contact correctly', async () => { |
| | const userId = new mongoose.Types.ObjectId(); |
| | const agentData = { |
| | id: 'agent_test_no_support', |
| | name: 'Test Agent', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userId, |
| | }; |
| |
|
| | const agent = await createAgent(agentData); |
| |
|
| | |
| | expect(agent.support_contact).toBeUndefined(); |
| | }); |
| |
|
| | describe('getListAgentsByAccess - Security Tests', () => { |
| | let userA, userB; |
| | let agentA1, agentA2, agentA3; |
| |
|
| | beforeEach(async () => { |
| | Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema); |
| | await Agent.deleteMany({}); |
| | await AclEntry.deleteMany({}); |
| |
|
| | |
| | userA = new mongoose.Types.ObjectId(); |
| | userB = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | agentA1 = await createAgent({ |
| | id: `agent_${uuidv4().slice(0, 12)}`, |
| | name: 'Agent A1', |
| | description: 'User A agent 1', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userA, |
| | }); |
| |
|
| | agentA2 = await createAgent({ |
| | id: `agent_${uuidv4().slice(0, 12)}`, |
| | name: 'Agent A2', |
| | description: 'User A agent 2', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userA, |
| | }); |
| |
|
| | agentA3 = await createAgent({ |
| | id: `agent_${uuidv4().slice(0, 12)}`, |
| | name: 'Agent A3', |
| | description: 'User A agent 3', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userA, |
| | }); |
| | }); |
| |
|
| | test('should return empty list when user has no accessible agents (empty accessibleIds)', async () => { |
| | |
| | const result = await getListAgentsByAccess({ |
| | accessibleIds: [], |
| | otherParams: {}, |
| | }); |
| |
|
| | expect(result.data).toHaveLength(0); |
| | expect(result.has_more).toBe(false); |
| | expect(result.first_id).toBeNull(); |
| | expect(result.last_id).toBeNull(); |
| | }); |
| |
|
| | test('should not return other users agents when accessibleIds is empty', async () => { |
| | |
| | const result = await getListAgentsByAccess({ |
| | accessibleIds: [], |
| | otherParams: { author: userB }, |
| | }); |
| |
|
| | expect(result.data).toHaveLength(0); |
| | expect(result.has_more).toBe(false); |
| | }); |
| |
|
| | test('should only return agents in accessibleIds list', async () => { |
| | |
| | const accessibleIds = [agentA1._id]; |
| |
|
| | const result = await getListAgentsByAccess({ |
| | accessibleIds, |
| | otherParams: {}, |
| | }); |
| |
|
| | expect(result.data).toHaveLength(1); |
| | expect(result.data[0].id).toBe(agentA1.id); |
| | expect(result.data[0].name).toBe('Agent A1'); |
| | }); |
| |
|
| | test('should return multiple accessible agents when provided', async () => { |
| | |
| | const accessibleIds = [agentA1._id, agentA3._id]; |
| |
|
| | const result = await getListAgentsByAccess({ |
| | accessibleIds, |
| | otherParams: {}, |
| | }); |
| |
|
| | expect(result.data).toHaveLength(2); |
| | const returnedIds = result.data.map((agent) => agent.id); |
| | expect(returnedIds).toContain(agentA1.id); |
| | expect(returnedIds).toContain(agentA3.id); |
| | expect(returnedIds).not.toContain(agentA2.id); |
| | }); |
| |
|
| | test('should respect other query parameters while enforcing accessibleIds', async () => { |
| | |
| | const accessibleIds = [agentA1._id, agentA2._id, agentA3._id]; |
| |
|
| | const result = await getListAgentsByAccess({ |
| | accessibleIds, |
| | otherParams: { name: 'Agent A2' }, |
| | }); |
| |
|
| | expect(result.data).toHaveLength(1); |
| | expect(result.data[0].id).toBe(agentA2.id); |
| | }); |
| |
|
| | test('should handle pagination correctly with accessibleIds filter', async () => { |
| | |
| | const moreAgents = []; |
| | for (let i = 4; i <= 10; i++) { |
| | const agent = await createAgent({ |
| | id: `agent_${uuidv4().slice(0, 12)}`, |
| | name: `Agent A${i}`, |
| | description: `User A agent ${i}`, |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userA, |
| | }); |
| | moreAgents.push(agent); |
| | } |
| |
|
| | |
| | const allAgentIds = [agentA1, agentA2, agentA3, ...moreAgents].map((a) => a._id); |
| |
|
| | |
| | const page1 = await getListAgentsByAccess({ |
| | accessibleIds: allAgentIds, |
| | otherParams: {}, |
| | limit: 5, |
| | }); |
| |
|
| | expect(page1.data).toHaveLength(5); |
| | expect(page1.has_more).toBe(true); |
| | expect(page1.after).toBeTruthy(); |
| |
|
| | |
| | const page2 = await getListAgentsByAccess({ |
| | accessibleIds: allAgentIds, |
| | otherParams: {}, |
| | limit: 5, |
| | after: page1.after, |
| | }); |
| |
|
| | expect(page2.data).toHaveLength(5); |
| | expect(page2.has_more).toBe(false); |
| |
|
| | |
| | const page1Ids = page1.data.map((a) => a.id); |
| | const page2Ids = page2.data.map((a) => a.id); |
| | const intersection = page1Ids.filter((id) => page2Ids.includes(id)); |
| | expect(intersection).toHaveLength(0); |
| | }); |
| |
|
| | test('should return empty list when accessibleIds contains non-existent IDs', async () => { |
| | |
| | const fakeIds = [new mongoose.Types.ObjectId(), new mongoose.Types.ObjectId()]; |
| |
|
| | const result = await getListAgentsByAccess({ |
| | accessibleIds: fakeIds, |
| | otherParams: {}, |
| | }); |
| |
|
| | expect(result.data).toHaveLength(0); |
| | expect(result.has_more).toBe(false); |
| | }); |
| |
|
| | test('should handle undefined accessibleIds as empty array', async () => { |
| | |
| | const result = await getListAgentsByAccess({ |
| | accessibleIds: undefined, |
| | otherParams: {}, |
| | }); |
| |
|
| | expect(result.data).toHaveLength(0); |
| | expect(result.has_more).toBe(false); |
| | }); |
| |
|
| | test('should combine accessibleIds with author filter correctly', async () => { |
| | |
| | const agentB1 = await createAgent({ |
| | id: `agent_${uuidv4().slice(0, 12)}`, |
| | name: 'Agent B1', |
| | description: 'User B agent 1', |
| | provider: 'openai', |
| | model: 'gpt-4', |
| | author: userB, |
| | }); |
| |
|
| | |
| | const accessibleIds = [agentA1._id, agentB1._id]; |
| |
|
| | |
| | const result = await getListAgentsByAccess({ |
| | accessibleIds, |
| | otherParams: { author: userB }, |
| | }); |
| |
|
| | expect(result.data).toHaveLength(1); |
| | expect(result.data[0].id).toBe(agentB1.id); |
| | expect(result.data[0].author).toBe(userB.toString()); |
| | }); |
| | }); |
| | }); |
| |
|
| | function createBasicAgent(overrides = {}) { |
| | const defaults = { |
| | id: `agent_${uuidv4()}`, |
| | name: 'Test Agent', |
| | provider: 'test', |
| | model: 'test-model', |
| | author: new mongoose.Types.ObjectId(), |
| | }; |
| | return createAgent({ ...defaults, ...overrides }); |
| | } |
| |
|
| | function createTestIds() { |
| | return { |
| | agentId: `agent_${uuidv4()}`, |
| | authorId: new mongoose.Types.ObjectId(), |
| | projectId: new mongoose.Types.ObjectId(), |
| | fileId: uuidv4(), |
| | }; |
| | } |
| |
|
| | function createFileOperations(agentId, fileIds, operation = 'add') { |
| | return fileIds.map((fileId) => |
| | operation === 'add' |
| | ? addAgentResourceFile({ agent_id: agentId, tool_resource: 'test_tool', file_id: fileId }) |
| | : removeAgentResourceFiles({ |
| | agent_id: agentId, |
| | files: [{ tool_resource: 'test_tool', file_id: fileId }], |
| | }), |
| | ); |
| | } |
| |
|
| | function mockFindOneAndUpdateError(errorOnCall = 1) { |
| | const original = Agent.findOneAndUpdate; |
| | let callCount = 0; |
| |
|
| | Agent.findOneAndUpdate = jest.fn().mockImplementation((...args) => { |
| | callCount++; |
| | if (callCount === errorOnCall) { |
| | throw new Error('Database connection lost'); |
| | } |
| | return original.apply(Agent, args); |
| | }); |
| |
|
| | return () => { |
| | Agent.findOneAndUpdate = original; |
| | }; |
| | } |
| |
|
| | function generateVersionTestCases() { |
| | const projectId1 = new mongoose.Types.ObjectId(); |
| | const projectId2 = new mongoose.Types.ObjectId(); |
| |
|
| | return [ |
| | { |
| | name: 'simple field update', |
| | initial: { |
| | name: 'Test Agent', |
| | description: 'Initial description', |
| | }, |
| | update: { name: 'Updated Name' }, |
| | duplicate: { name: 'Updated Name' }, |
| | }, |
| | { |
| | name: 'object field update', |
| | initial: { |
| | model_parameters: { temperature: 0.7 }, |
| | }, |
| | update: { model_parameters: { temperature: 0.8 } }, |
| | duplicate: { model_parameters: { temperature: 0.8 } }, |
| | }, |
| | { |
| | name: 'array field update', |
| | initial: { |
| | tools: ['tool1', 'tool2'], |
| | }, |
| | update: { tools: ['tool2', 'tool3'] }, |
| | duplicate: { tools: ['tool2', 'tool3'] }, |
| | }, |
| | { |
| | name: 'projectIds update', |
| | initial: { |
| | projectIds: [projectId1], |
| | }, |
| | update: { projectIds: [projectId1, projectId2] }, |
| | duplicate: { projectIds: [projectId2, projectId1] }, |
| | }, |
| | ]; |
| | } |
| |
|