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(), })); const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); const { agentSchema } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { getAgent, loadAgent, createAgent, updateAgent, deleteAgent, getListAgents, updateAgentProjects, addAgentResourceFile, removeAgentResourceFiles, generateActionMetadataHash, revertAgentVersion, } = require('./Agent'); const { getCachedTools } = require('~/server/services/Config'); const permissionService = require('~/server/services/PermissionService'); const { AclEntry } = require('~/db/models'); /** * @type {import('mongoose').Model} */ 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); // Should not duplicate 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'; // First add await addAgentResourceFile({ agent_id: agent.id, tool_resource: toolResource, file_id: fileId1, }); // Second add (should not duplicate) 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()); // Concurrent additions 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()); // Add all files first await Promise.all( fileIds.map((fileId) => addAgentResourceFile({ agent_id: agent.id, tool_resource: 'test_tool', file_id: fileId, }), ), ); // Concurrently remove all files 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 }); // Check if the array is empty or the tool resource itself is removed 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); // Initialize models const dbModels = require('~/db/models'); AccessRole = dbModels.AccessRole; // Create necessary access roles for agents await AccessRole.create({ accessRoleId: 'agent_owner', name: 'Owner', description: 'Full control over agents', resourceType: 'agent', permBits: 15, // VIEW | EDIT | DELETE | SHARE }); }, 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(); // Create agent const agent = await createAgent({ id: agentId, name: 'Agent With Permissions', provider: 'test', model: 'test-model', author: authorId, }); // Grant permissions (simulating sharing) await permissionService.grantPermission({ principalType: 'user', principalId: authorId, resourceType: 'agent', resourceId: agent._id, accessRoleId: 'agent_owner', grantedBy: authorId, }); // Verify ACL entry exists const aclEntriesBefore = await AclEntry.find({ resourceType: 'agent', resourceId: agent._id, }); expect(aclEntriesBefore).toHaveLength(1); // Delete the agent await deleteAgent({ id: agentId }); // Verify agent is deleted const agentAfterDelete = await getAgent({ id: agentId }); expect(agentAfterDelete).toBeNull(); // Verify ACL entries are removed const aclEntriesAfter = await AclEntry.find({ 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); // No new version created // Update with duplicate data should succeed but not create a new version const duplicateUpdate = await updateAgent({ id: testAgentId }, testCase.duplicate); expect(duplicateUpdate.versions).toHaveLength(2); // No new version created 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', }); // User 1 makes an update await updateAgent( { id: agentId }, { name: 'Updated by User 1', description: 'First update' }, { updatingUserId: user1.toString() }, ); // Original author makes an update await updateAgent( { id: agentId }, { description: 'Updated by original author' }, { updatingUserId: originalAuthor.toString() }, ); // User 2 makes an update await updateAgent( { id: agentId }, { name: 'Updated by User 2', model: 'new-model' }, { updatingUserId: user2.toString() }, ); // User 3 makes an update 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()); // Check that each version has the correct updatedBy expect(finalAgent.versions[0].updatedBy).toBeUndefined(); // Initial creation has no updatedBy 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()); // Verify the final state 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'; // Create agent with actions 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'], }); // First update with forceVersion should create a version 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); // Second update with same data but forceVersion should still create a version 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); // Update without forceVersion and no changes should not create a version 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); // No new version created }); 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], }); // Update with same array but different null/undefined arrangement 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: {} }, ], }); // Try to update with reordered but equivalent tool_kwargs const updatedAgent = await updateAgent( { id: agentId }, { tool_kwargs: [ { tool: 'tool2', config: {} }, { tool: 'tool1', config: { setting: 'value' } }, {}, ], }, ); // Should create new version as order matters for arrays 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], }); // Update with same values but different types const updatedAgent = await updateAgent( { id: agentId }, { mixed_array: ['1', 'string', { key: 'value' }, 'true', null] }, ); // Should create new version as types differ 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, }); // First create a version with changes await updateAgent({ id: agentId }, { description: 'Updated' }); // Then try to create duplicate of the original version await updateAgent( { id: agentId }, { model_parameters: deepObject, description: undefined, }, ); // Since we're updating back to the same model_parameters but with a different description, // it should create a new version 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 }, }); // Update with a real field change first const firstUpdate = await updateAgent({ id: agentId }, { description: 'New description' }); expect(firstUpdate.versions).toHaveLength(2); // Update with model parameters change 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(); // Create agent with initial support_contact 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', }, }); // Update support_contact name only 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'); // Update support_contact email only 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'); // Try to update with same support_contact - should be detected as duplicate await expect( updateAgent( { id: agentId }, { support_contact: { name: 'Updated Support', email: 'updated@support.com', }, }, ), ).rejects.toThrow('Duplicate version'); }); test('should handle support_contact from empty to populated', async () => { const agentId = `agent_${uuidv4()}`; const authorId = new mongoose.Types.ObjectId(); // Create agent without support_contact const agent = await createAgent({ id: agentId, name: 'Agent without Support', provider: 'test', model: 'test-model', author: authorId, }); // Verify support_contact is undefined since it wasn't provided expect(agent.support_contact).toBeUndefined(); // Update to add support_contact 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(); // Create agent with support_contact await createAgent({ id: agentId, name: 'Edge Case Agent', provider: 'test', model: 'test-model', author: authorId, support_contact: { name: 'Support', email: 'support@test.com', }, }); // Update to empty support_contact const emptyUpdate = await updateAgent( { id: agentId }, { support_contact: {}, }, ); expect(emptyUpdate.versions).toHaveLength(2); expect(emptyUpdate.support_contact).toEqual({}); // Update back to populated support_contact const repopulated = await updateAgent( { id: agentId }, { support_contact: { name: 'Support', email: 'support@test.com', }, }, ); expect(repopulated.versions).toHaveLength(3); // Verify all versions have correct support_contact 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(); // Create agent await createAgent({ id: agentId, name: 'Version History Test', provider: 'test', model: 'test-model', author: authorId, support_contact: { name: 'Initial Contact', email: 'initial@test.com', }, }); // Multiple updates with different support_contact values 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 }); // Verify version history 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', }); // Current state should match last version 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(); // Create agent with full support_contact await createAgent({ id: agentId, name: 'Partial Update Test', provider: 'test', model: 'test-model', author: authorId, support_contact: { name: 'Original Name', email: 'original@email.com', }, }); // MongoDB's findOneAndUpdate will replace the entire support_contact object // So we need to verify that partial updates still work correctly const updated = await updateAgent( { id: agentId }, { support_contact: { name: 'New Name', email: '', // Empty email }, }, ); expect(updated.versions).toHaveLength(2); expect(updated.support_contact.name).toBe('New Name'); expect(updated.support_contact.email).toBe(''); // Verify isDuplicateVersion works with partial changes await expect( updateAgent( { id: agentId }, { support_contact: { name: 'New Name', email: '', }, }, ), ).rejects.toThrow('Duplicate version'); }); // 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('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); // SHA-256 produces 64 character hex string }); 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' } }, // missing action with id '999' ]; 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: {}, }); 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' }, }); // With the new permission system, loadAgent returns the agent regardless of permissions // Permission checks are handled at the route level via middleware 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); 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' }, }); // With the new permission system, loadAgent returns the agent regardless of permissions // Permission checks are handled at the route level via middleware 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); // No new version should be created }); 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, }); // Concurrent updates to different agents 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'], }); // Test with $push and direct field update 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, }); // Try to revert to non-existent version 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'], }); // First update with $push operation const firstUpdate = await updateAgent( { id: agentId }, { $push: { tools: 'tool2' }, }, ); expect(firstUpdate.tools).toContain('tool1'); expect(firstUpdate.tools).toContain('tool2'); // Second update with direct field update and $addToSet 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(); // Test with non-existent agent 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(); // Create agent with actions that might cause getActions to fail await createAgent({ id: agentId, name: 'Agent with Actions', provider: 'test', model: 'test-model', author: authorId, actions: ['test.com_action_invalid_id'], }); // Update should still work even if getActions fails 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], }); // Use multiple operators in single update - but avoid conflicting operations on same field 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, }); // Mock findOneAndUpdate to simulate database error const cleanup = mockFindOneAndUpdateError(2); // Concurrent updates where one fails 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'], }, }, }); // Mock findOneAndUpdate to return null (simulating deletion) const originalFindOneAndUpdate = Agent.findOneAndUpdate; Agent.findOneAndUpdate = jest.fn().mockImplementation(() => ({ lean: jest.fn().mockResolvedValue(null), })); // Try to remove files from deleted agent 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: {}, // No mcp delimiter tool__server1: {}, // Wrong delimiter tool_mcp_server1: {}, // Correct format tool_mcp_server2: {}, // Different server }); 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, }); // Mock the updateOne operation to fail but let updateAgent succeed 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'] }, ); // Since agent_ids is no longer excluded, this should create a new version 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); // Update with same agent_ids should succeed but not create a new version const duplicateUpdate = await updateAgent( { id: agentId }, { agent_ids: ['agent1', 'agent2', 'agent3'] }, ); expect(duplicateUpdate.versions).toHaveLength(2); // No new version created }); 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(); // Create agent with initial projectIds await createAgent({ id: agentId, name: 'Test Agent', provider: 'test', model: 'test-model', author: authorId, projectIds: [projectId1], }); // Share agent using updateAgentProjects (which uses skipVersioning) const shared = await updateAgentProjects({ user: { id: authorId.toString() }, // Use the same author ID agentId: agentId, projectIds: [projectId2.toString()], }); // Should NOT create a new version due to skipVersioning 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()); // Unshare agent using updateAgentProjects const unshared = await updateAgentProjects({ user: { id: authorId.toString() }, agentId: agentId, removeProjectIds: [projectId1.toString()], }); // Still should NOT create a new version 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()); // Regular update without skipVersioning should create a version const regularUpdate = await updateAgent( { id: agentId }, { description: 'Updated description' }, ); expect(regularUpdate.versions).toHaveLength(2); expect(regularUpdate.description).toBe('Updated description'); // Direct updateAgent with MongoDB operators should still create versions 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([]); // Update with same empty agent_ids should succeed but not create a new version const duplicateUpdate = await updateAgent({ id: agentId }, { agent_ids: [] }); expect(duplicateUpdate.versions).toHaveLength(2); // No new version created 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', }, }; // Create agent const agent = await createAgent(agentData); // Verify support_contact is stored correctly expect(agent.support_contact).toBeDefined(); expect(agent.support_contact.name).toBe('Support Team'); expect(agent.support_contact.email).toBe('support@example.com'); // Verify no _id field is created in support_contact expect(agent.support_contact._id).toBeUndefined(); // Fetch from database to double-check 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); // Verify empty support_contact is stored as empty object 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); // Verify support_contact is undefined when not provided expect(agent.support_contact).toBeUndefined(); }); }); 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] }, }, ]; }