mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-22 15:46:33 +01:00
* fix: use ACL ownership for prompt group cleanup on user deletion deleteUserPrompts previously called getAllPromptGroups with only an author filter, which defaults to searchShared=true and drops the author filter for shared/global project entries. This caused any user deleting their account to strip shared prompt group associations and ACL entries for other users. Replace the author-based query with ACL-based ownership lookup: - Find prompt groups where the user has OWNER permission (DELETE bit) - Only delete groups where the user is the sole owner - Preserve multi-owned groups and their ACL entries for other owners * fix: use ACL ownership for agent cleanup on user deletion deleteUserAgents used the deprecated author field to find and delete agents, then unconditionally removed all ACL entries for those agents. This could destroy ACL entries for agents shared with or co-owned by other users. Replace the author-based query with ACL-based ownership lookup: - Find agents where the user has OWNER permission (DELETE bit) - Only delete agents where the user is the sole owner - Preserve multi-owned agents and their ACL entries for other owners - Also clean up handoff edges referencing deleted agents * fix: add MCP server cleanup on user deletion User deletion had no cleanup for MCP servers, leaving solely-owned servers orphaned in the database with dangling ACL entries for other users. Add deleteUserMcpServers that follows the same ACL ownership pattern as prompt groups and agents: find servers with OWNER permission, check for sole ownership, and only delete those with no other owners. * style: fix prettier formatting in Prompt.spec.js * refactor: extract getSoleOwnedResourceIds to PermissionService The ACL sole-ownership detection algorithm was duplicated across deleteUserPrompts, deleteUserAgents, and deleteUserMcpServers. Centralizes the three-step pattern (find owned entries, find other owners, compute sole-owned set) into a single reusable utility. * refactor: use getSoleOwnedResourceIds in all deletion functions - Replace inline ACL queries with the centralized utility - Remove vestigial _req parameter from deleteUserPrompts - Use Promise.all for parallel project removal instead of sequential awaits - Disconnect live MCP sessions and invalidate tool cache before deleting sole-owned MCP server documents - Export deleteUserMcpServers for testability * test: improve deletion test coverage and quality - Move deleteUserPrompts call to beforeAll to eliminate execution-order dependency between tests - Standardize on test() instead of it() for consistency in Prompt.spec.js - Add assertion for deleting user's own ACL entry preservation on multi-owned agents - Add deleteUserMcpServers integration test suite with 6 tests covering sole-owner deletion, multi-owner preservation, session disconnect, cache invalidation, model-not-registered guard, and missing MCPManager - Add PermissionService mock to existing deleteUser.spec.js to fix import chain * fix: add legacy author-based fallback for unmigrated resources Resources created before the ACL system have author set but no AclEntry records. The sole-ownership detection returns empty for these, causing deleteUserPrompts, deleteUserAgents, and deleteUserMcpServers to silently skip them — permanently orphaning data on user deletion. Add a fallback that identifies author-owned resources with zero ACL entries (truly unmigrated) and includes them in the deletion set. This preserves the multi-owner safety of the ACL path while ensuring pre-ACL resources are still cleaned up regardless of migration status. * style: fix prettier formatting across all changed files * test: add resource type coverage guard for user deletion Ensures every ResourceType in the ACL system has a corresponding cleanup handler wired into deleteUserController. When a new ResourceType is added (e.g. WORKFLOW), this test fails immediately, preventing silent data orphaning on user account deletion. * style: fix import order in PermissionService destructure * test: add opt-out set and fix test lifecycle in coverage guard Add NO_USER_CLEANUP_NEEDED set for resource types that legitimately require no per-user deletion. Move fs.readFileSync into beforeAll so path errors surface as clean test failures instead of unhandled crashes.
3964 lines
126 KiB
JavaScript
3964 lines
126 KiB
JavaScript
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 {
|
|
ResourceType,
|
|
AccessRoleIds,
|
|
PrincipalType,
|
|
PermissionBits,
|
|
} = require('librechat-data-provider');
|
|
const {
|
|
getAgent,
|
|
loadAgent,
|
|
createAgent,
|
|
updateAgent,
|
|
deleteAgent,
|
|
deleteUserAgents,
|
|
revertAgentVersion,
|
|
updateAgentProjects,
|
|
addAgentResourceFile,
|
|
getListAgentsByAccess,
|
|
removeAgentResourceFiles,
|
|
generateActionMetadataHash,
|
|
} = require('./Agent');
|
|
const permissionService = require('~/server/services/PermissionService');
|
|
const { getCachedTools, getMCPServerTools } = require('~/server/services/Config');
|
|
const { AclEntry, User } = require('~/db/models');
|
|
|
|
/**
|
|
* @type {import('mongoose').Model<import('@librechat/data-schemas').IAgent>}
|
|
*/
|
|
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({});
|
|
await User.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: AccessRoleIds.AGENT_OWNER,
|
|
name: 'Owner',
|
|
description: 'Full control over agents',
|
|
resourceType: ResourceType.AGENT,
|
|
permBits: 15, // VIEW | EDIT | DELETE | SHARE
|
|
});
|
|
}, 20000);
|
|
|
|
afterAll(async () => {
|
|
await mongoose.disconnect();
|
|
await mongoServer.stop();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await Agent.deleteMany({});
|
|
await AclEntry.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: PrincipalType.USER,
|
|
principalId: authorId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: authorId,
|
|
});
|
|
|
|
// Verify ACL entry exists
|
|
const aclEntriesBefore = await AclEntry.find({
|
|
resourceType: 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: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
});
|
|
expect(aclEntriesAfter).toHaveLength(0);
|
|
});
|
|
|
|
test('should remove handoff edges referencing deleted agent from other agents', async () => {
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
const targetAgentId = `agent_${uuidv4()}`;
|
|
const sourceAgentId = `agent_${uuidv4()}`;
|
|
|
|
// Create target agent (handoff destination)
|
|
await createAgent({
|
|
id: targetAgentId,
|
|
name: 'Target Agent',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
// Create source agent with handoff edge to target
|
|
await createAgent({
|
|
id: sourceAgentId,
|
|
name: 'Source Agent',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
edges: [
|
|
{
|
|
from: sourceAgentId,
|
|
to: targetAgentId,
|
|
edgeType: 'handoff',
|
|
},
|
|
],
|
|
});
|
|
|
|
// Verify edge exists before deletion
|
|
const sourceAgentBefore = await getAgent({ id: sourceAgentId });
|
|
expect(sourceAgentBefore.edges).toHaveLength(1);
|
|
expect(sourceAgentBefore.edges[0].to).toBe(targetAgentId);
|
|
|
|
// Delete the target agent
|
|
await deleteAgent({ id: targetAgentId });
|
|
|
|
// Verify the edge is removed from source agent
|
|
const sourceAgentAfter = await getAgent({ id: sourceAgentId });
|
|
expect(sourceAgentAfter.edges).toHaveLength(0);
|
|
});
|
|
|
|
test('should remove agent from user favorites when agent is deleted', async () => {
|
|
const agentId = `agent_${uuidv4()}`;
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
const userId = new mongoose.Types.ObjectId();
|
|
|
|
// Create agent
|
|
await createAgent({
|
|
id: agentId,
|
|
name: 'Agent To Delete',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
// Create user with the agent in favorites
|
|
await User.create({
|
|
_id: userId,
|
|
name: 'Test User',
|
|
email: `test-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: agentId }, { model: 'gpt-4', endpoint: 'openAI' }],
|
|
});
|
|
|
|
// Verify user has agent in favorites
|
|
const userBefore = await User.findById(userId);
|
|
expect(userBefore.favorites).toHaveLength(2);
|
|
expect(userBefore.favorites.some((f) => f.agentId === agentId)).toBe(true);
|
|
|
|
// Delete the agent
|
|
await deleteAgent({ id: agentId });
|
|
|
|
// Verify agent is deleted
|
|
const agentAfterDelete = await getAgent({ id: agentId });
|
|
expect(agentAfterDelete).toBeNull();
|
|
|
|
// Verify agent is removed from user favorites
|
|
const userAfter = await User.findById(userId);
|
|
expect(userAfter.favorites).toHaveLength(1);
|
|
expect(userAfter.favorites.some((f) => f.agentId === agentId)).toBe(false);
|
|
expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true);
|
|
});
|
|
|
|
test('should remove agent from multiple users favorites when agent is deleted', async () => {
|
|
const agentId = `agent_${uuidv4()}`;
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
const user1Id = new mongoose.Types.ObjectId();
|
|
const user2Id = new mongoose.Types.ObjectId();
|
|
|
|
// Create agent
|
|
await createAgent({
|
|
id: agentId,
|
|
name: 'Agent To Delete',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
// Create two users with the agent in favorites
|
|
await User.create({
|
|
_id: user1Id,
|
|
name: 'Test User 1',
|
|
email: `test1-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: agentId }],
|
|
});
|
|
|
|
await User.create({
|
|
_id: user2Id,
|
|
name: 'Test User 2',
|
|
email: `test2-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: agentId }, { agentId: `agent_${uuidv4()}` }],
|
|
});
|
|
|
|
// Delete the agent
|
|
await deleteAgent({ id: agentId });
|
|
|
|
// Verify agent is removed from both users' favorites
|
|
const user1After = await User.findById(user1Id);
|
|
const user2After = await User.findById(user2Id);
|
|
|
|
expect(user1After.favorites).toHaveLength(0);
|
|
expect(user2After.favorites).toHaveLength(1);
|
|
expect(user2After.favorites.some((f) => f.agentId === agentId)).toBe(false);
|
|
});
|
|
|
|
test('should preserve other agents in database when one agent is deleted', async () => {
|
|
const agentToDeleteId = `agent_${uuidv4()}`;
|
|
const agentToKeep1Id = `agent_${uuidv4()}`;
|
|
const agentToKeep2Id = `agent_${uuidv4()}`;
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
|
|
// Create multiple agents
|
|
await createAgent({
|
|
id: agentToDeleteId,
|
|
name: 'Agent To Delete',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
await createAgent({
|
|
id: agentToKeep1Id,
|
|
name: 'Agent To Keep 1',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
await createAgent({
|
|
id: agentToKeep2Id,
|
|
name: 'Agent To Keep 2',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
// Verify all agents exist
|
|
expect(await getAgent({ id: agentToDeleteId })).not.toBeNull();
|
|
expect(await getAgent({ id: agentToKeep1Id })).not.toBeNull();
|
|
expect(await getAgent({ id: agentToKeep2Id })).not.toBeNull();
|
|
|
|
// Delete one agent
|
|
await deleteAgent({ id: agentToDeleteId });
|
|
|
|
// Verify only the deleted agent is removed, others remain intact
|
|
expect(await getAgent({ id: agentToDeleteId })).toBeNull();
|
|
const keptAgent1 = await getAgent({ id: agentToKeep1Id });
|
|
const keptAgent2 = await getAgent({ id: agentToKeep2Id });
|
|
expect(keptAgent1).not.toBeNull();
|
|
expect(keptAgent1.name).toBe('Agent To Keep 1');
|
|
expect(keptAgent2).not.toBeNull();
|
|
expect(keptAgent2.name).toBe('Agent To Keep 2');
|
|
});
|
|
|
|
test('should preserve other agents in user favorites when one agent is deleted', async () => {
|
|
const agentToDeleteId = `agent_${uuidv4()}`;
|
|
const agentToKeep1Id = `agent_${uuidv4()}`;
|
|
const agentToKeep2Id = `agent_${uuidv4()}`;
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
const userId = new mongoose.Types.ObjectId();
|
|
|
|
// Create multiple agents
|
|
await createAgent({
|
|
id: agentToDeleteId,
|
|
name: 'Agent To Delete',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
await createAgent({
|
|
id: agentToKeep1Id,
|
|
name: 'Agent To Keep 1',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
await createAgent({
|
|
id: agentToKeep2Id,
|
|
name: 'Agent To Keep 2',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
// Create user with all three agents in favorites
|
|
await User.create({
|
|
_id: userId,
|
|
name: 'Test User',
|
|
email: `test-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [
|
|
{ agentId: agentToDeleteId },
|
|
{ agentId: agentToKeep1Id },
|
|
{ agentId: agentToKeep2Id },
|
|
],
|
|
});
|
|
|
|
// Verify user has all three agents in favorites
|
|
const userBefore = await User.findById(userId);
|
|
expect(userBefore.favorites).toHaveLength(3);
|
|
|
|
// Delete one agent
|
|
await deleteAgent({ id: agentToDeleteId });
|
|
|
|
// Verify only the deleted agent is removed from favorites
|
|
const userAfter = await User.findById(userId);
|
|
expect(userAfter.favorites).toHaveLength(2);
|
|
expect(userAfter.favorites.some((f) => f.agentId === agentToDeleteId)).toBe(false);
|
|
expect(userAfter.favorites.some((f) => f.agentId === agentToKeep1Id)).toBe(true);
|
|
expect(userAfter.favorites.some((f) => f.agentId === agentToKeep2Id)).toBe(true);
|
|
});
|
|
|
|
test('should not affect users who do not have deleted agent in favorites', async () => {
|
|
const agentToDeleteId = `agent_${uuidv4()}`;
|
|
const otherAgentId = `agent_${uuidv4()}`;
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
const userWithDeletedAgentId = new mongoose.Types.ObjectId();
|
|
const userWithoutDeletedAgentId = new mongoose.Types.ObjectId();
|
|
|
|
// Create agents
|
|
await createAgent({
|
|
id: agentToDeleteId,
|
|
name: 'Agent To Delete',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
await createAgent({
|
|
id: otherAgentId,
|
|
name: 'Other Agent',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
// Create user with the agent to be deleted
|
|
await User.create({
|
|
_id: userWithDeletedAgentId,
|
|
name: 'User With Deleted Agent',
|
|
email: `user1-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: agentToDeleteId }, { model: 'gpt-4', endpoint: 'openAI' }],
|
|
});
|
|
|
|
// Create user without the agent to be deleted
|
|
await User.create({
|
|
_id: userWithoutDeletedAgentId,
|
|
name: 'User Without Deleted Agent',
|
|
email: `user2-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: otherAgentId }, { model: 'claude-3', endpoint: 'anthropic' }],
|
|
});
|
|
|
|
// Delete the agent
|
|
await deleteAgent({ id: agentToDeleteId });
|
|
|
|
// Verify user with deleted agent has it removed
|
|
const userWithDeleted = await User.findById(userWithDeletedAgentId);
|
|
expect(userWithDeleted.favorites).toHaveLength(1);
|
|
expect(userWithDeleted.favorites.some((f) => f.agentId === agentToDeleteId)).toBe(false);
|
|
expect(userWithDeleted.favorites.some((f) => f.model === 'gpt-4')).toBe(true);
|
|
|
|
// Verify user without deleted agent is completely unaffected
|
|
const userWithoutDeleted = await User.findById(userWithoutDeletedAgentId);
|
|
expect(userWithoutDeleted.favorites).toHaveLength(2);
|
|
expect(userWithoutDeleted.favorites.some((f) => f.agentId === otherAgentId)).toBe(true);
|
|
expect(userWithoutDeleted.favorites.some((f) => f.model === 'claude-3')).toBe(true);
|
|
});
|
|
|
|
test('should remove all user agents from favorites when deleteUserAgents is called', async () => {
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
const otherAuthorId = new mongoose.Types.ObjectId();
|
|
const userId = new mongoose.Types.ObjectId();
|
|
|
|
const agent1Id = `agent_${uuidv4()}`;
|
|
const agent2Id = `agent_${uuidv4()}`;
|
|
const otherAuthorAgentId = `agent_${uuidv4()}`;
|
|
|
|
const agent1 = await createAgent({
|
|
id: agent1Id,
|
|
name: 'Author Agent 1',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
const agent2 = await createAgent({
|
|
id: agent2Id,
|
|
name: 'Author Agent 2',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
await createAgent({
|
|
id: otherAuthorAgentId,
|
|
name: 'Other Author Agent',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: otherAuthorId,
|
|
});
|
|
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: authorId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent1._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: authorId,
|
|
});
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: authorId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent2._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: authorId,
|
|
});
|
|
|
|
await User.create({
|
|
_id: userId,
|
|
name: 'Test User',
|
|
email: `test-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [
|
|
{ agentId: agent1Id },
|
|
{ agentId: agent2Id },
|
|
{ agentId: otherAuthorAgentId },
|
|
{ model: 'gpt-4', endpoint: 'openAI' },
|
|
],
|
|
});
|
|
|
|
const userBefore = await User.findById(userId);
|
|
expect(userBefore.favorites).toHaveLength(4);
|
|
|
|
await deleteUserAgents(authorId.toString());
|
|
|
|
expect(await getAgent({ id: agent1Id })).toBeNull();
|
|
expect(await getAgent({ id: agent2Id })).toBeNull();
|
|
|
|
expect(await getAgent({ id: otherAuthorAgentId })).not.toBeNull();
|
|
|
|
const userAfter = await User.findById(userId);
|
|
expect(userAfter.favorites).toHaveLength(2);
|
|
expect(userAfter.favorites.some((f) => f.agentId === agent1Id)).toBe(false);
|
|
expect(userAfter.favorites.some((f) => f.agentId === agent2Id)).toBe(false);
|
|
expect(userAfter.favorites.some((f) => f.agentId === otherAuthorAgentId)).toBe(true);
|
|
expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true);
|
|
});
|
|
|
|
test('should handle deleteUserAgents when agents are in multiple users favorites', async () => {
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
const user1Id = new mongoose.Types.ObjectId();
|
|
const user2Id = new mongoose.Types.ObjectId();
|
|
const user3Id = new mongoose.Types.ObjectId();
|
|
|
|
const agent1Id = `agent_${uuidv4()}`;
|
|
const agent2Id = `agent_${uuidv4()}`;
|
|
const unrelatedAgentId = `agent_${uuidv4()}`;
|
|
|
|
const agent1 = await createAgent({
|
|
id: agent1Id,
|
|
name: 'Author Agent 1',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
const agent2 = await createAgent({
|
|
id: agent2Id,
|
|
name: 'Author Agent 2',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: authorId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent1._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: authorId,
|
|
});
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: authorId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent2._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: authorId,
|
|
});
|
|
|
|
await User.create({
|
|
_id: user1Id,
|
|
name: 'User 1',
|
|
email: `user1-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: agent1Id }, { agentId: agent2Id }],
|
|
});
|
|
|
|
await User.create({
|
|
_id: user2Id,
|
|
name: 'User 2',
|
|
email: `user2-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: agent1Id }, { model: 'claude-3', endpoint: 'anthropic' }],
|
|
});
|
|
|
|
await User.create({
|
|
_id: user3Id,
|
|
name: 'User 3',
|
|
email: `user3-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: unrelatedAgentId }, { model: 'gpt-4', endpoint: 'openAI' }],
|
|
});
|
|
|
|
await deleteUserAgents(authorId.toString());
|
|
|
|
const user1After = await User.findById(user1Id);
|
|
expect(user1After.favorites).toHaveLength(0);
|
|
|
|
const user2After = await User.findById(user2Id);
|
|
expect(user2After.favorites).toHaveLength(1);
|
|
expect(user2After.favorites.some((f) => f.agentId === agent1Id)).toBe(false);
|
|
expect(user2After.favorites.some((f) => f.model === 'claude-3')).toBe(true);
|
|
|
|
const user3After = await User.findById(user3Id);
|
|
expect(user3After.favorites).toHaveLength(2);
|
|
expect(user3After.favorites.some((f) => f.agentId === unrelatedAgentId)).toBe(true);
|
|
expect(user3After.favorites.some((f) => f.model === 'gpt-4')).toBe(true);
|
|
});
|
|
|
|
test('should handle deleteUserAgents when user has no agents', async () => {
|
|
const authorWithNoAgentsId = new mongoose.Types.ObjectId();
|
|
const otherAuthorId = new mongoose.Types.ObjectId();
|
|
const userId = new mongoose.Types.ObjectId();
|
|
|
|
const existingAgentId = `agent_${uuidv4()}`;
|
|
|
|
const existingAgent = await createAgent({
|
|
id: existingAgentId,
|
|
name: 'Existing Agent',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: otherAuthorId,
|
|
});
|
|
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: otherAuthorId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: existingAgent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: otherAuthorId,
|
|
});
|
|
|
|
await User.create({
|
|
_id: userId,
|
|
name: 'Test User',
|
|
email: `test-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ agentId: existingAgentId }, { model: 'gpt-4', endpoint: 'openAI' }],
|
|
});
|
|
|
|
await deleteUserAgents(authorWithNoAgentsId.toString());
|
|
|
|
expect(await getAgent({ id: existingAgentId })).not.toBeNull();
|
|
|
|
const userAfter = await User.findById(userId);
|
|
expect(userAfter.favorites).toHaveLength(2);
|
|
expect(userAfter.favorites.some((f) => f.agentId === existingAgentId)).toBe(true);
|
|
expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true);
|
|
});
|
|
|
|
test('should handle deleteUserAgents when agents are not in any favorites', async () => {
|
|
const authorId = new mongoose.Types.ObjectId();
|
|
const userId = new mongoose.Types.ObjectId();
|
|
|
|
const agent1Id = `agent_${uuidv4()}`;
|
|
const agent2Id = `agent_${uuidv4()}`;
|
|
|
|
const agent1 = await createAgent({
|
|
id: agent1Id,
|
|
name: 'Agent 1',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
const agent2 = await createAgent({
|
|
id: agent2Id,
|
|
name: 'Agent 2',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: authorId,
|
|
});
|
|
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: authorId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent1._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: authorId,
|
|
});
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: authorId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent2._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: authorId,
|
|
});
|
|
|
|
await User.create({
|
|
_id: userId,
|
|
name: 'Test User',
|
|
email: `test-${uuidv4()}@example.com`,
|
|
provider: 'local',
|
|
favorites: [{ model: 'gpt-4', endpoint: 'openAI' }],
|
|
});
|
|
|
|
expect(await getAgent({ id: agent1Id })).not.toBeNull();
|
|
expect(await getAgent({ id: agent2Id })).not.toBeNull();
|
|
|
|
await deleteUserAgents(authorId.toString());
|
|
|
|
expect(await getAgent({ id: agent1Id })).toBeNull();
|
|
expect(await getAgent({ id: agent2Id })).toBeNull();
|
|
|
|
const userAfter = await User.findById(userId);
|
|
expect(userAfter.favorites).toHaveLength(1);
|
|
expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true);
|
|
});
|
|
|
|
test('should preserve multi-owned agents when deleteUserAgents is called', async () => {
|
|
const deletingUserId = new mongoose.Types.ObjectId();
|
|
const otherOwnerId = new mongoose.Types.ObjectId();
|
|
|
|
const soleOwnedId = `agent_${uuidv4()}`;
|
|
const multiOwnedId = `agent_${uuidv4()}`;
|
|
|
|
const soleAgent = await createAgent({
|
|
id: soleOwnedId,
|
|
name: 'Sole Owned Agent',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: deletingUserId,
|
|
});
|
|
|
|
const multiAgent = await createAgent({
|
|
id: multiOwnedId,
|
|
name: 'Multi Owned Agent',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: deletingUserId,
|
|
});
|
|
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: deletingUserId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: soleAgent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: deletingUserId,
|
|
});
|
|
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: deletingUserId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: multiAgent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: deletingUserId,
|
|
});
|
|
await permissionService.grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: otherOwnerId,
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: multiAgent._id,
|
|
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
|
grantedBy: otherOwnerId,
|
|
});
|
|
|
|
await deleteUserAgents(deletingUserId.toString());
|
|
|
|
expect(await getAgent({ id: soleOwnedId })).toBeNull();
|
|
expect(await getAgent({ id: multiOwnedId })).not.toBeNull();
|
|
|
|
const soleAcl = await AclEntry.find({
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: soleAgent._id,
|
|
});
|
|
expect(soleAcl).toHaveLength(0);
|
|
|
|
const multiAcl = await AclEntry.find({
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: multiAgent._id,
|
|
principalId: otherOwnerId,
|
|
});
|
|
expect(multiAcl).toHaveLength(1);
|
|
expect(multiAcl[0].permBits & PermissionBits.DELETE).toBeTruthy();
|
|
|
|
const deletingUserMultiAcl = await AclEntry.find({
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: multiAgent._id,
|
|
principalId: deletingUserId,
|
|
});
|
|
expect(deletingUserMultiAcl).toHaveLength(1);
|
|
});
|
|
|
|
test('should delete legacy agents that have author but no ACL entries', async () => {
|
|
const legacyUserId = new mongoose.Types.ObjectId();
|
|
const legacyAgentId = `agent_${uuidv4()}`;
|
|
|
|
await createAgent({
|
|
id: legacyAgentId,
|
|
name: 'Legacy Agent (no ACL)',
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
author: legacyUserId,
|
|
});
|
|
|
|
await deleteUserAgents(legacyUserId.toString());
|
|
|
|
expect(await getAgent({ id: legacyAgentId })).toBeNull();
|
|
});
|
|
|
|
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 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 but return successfully
|
|
const duplicateUpdate = await updateAgent(
|
|
{ id: agentId },
|
|
{
|
|
support_contact: {
|
|
name: 'Updated Support',
|
|
email: 'updated@support.com',
|
|
},
|
|
},
|
|
);
|
|
|
|
// Should not create a new version
|
|
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();
|
|
|
|
// 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 - should return successfully without creating new version
|
|
const duplicateUpdate = await updateAgent(
|
|
{ id: agentId },
|
|
{
|
|
support_contact: {
|
|
name: 'New Name',
|
|
email: '',
|
|
},
|
|
},
|
|
);
|
|
|
|
// Should not create a new version since content is the same
|
|
expect(duplicateUpdate.versions).toHaveLength(2);
|
|
expect(duplicateUpdate.version).toBe(2);
|
|
expect(duplicateUpdate.support_contact.name).toBe('New Name');
|
|
expect(duplicateUpdate.support_contact.email).toBe('');
|
|
});
|
|
|
|
// 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: {},
|
|
});
|
|
|
|
// Mock getMCPServerTools to return tools for each server
|
|
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) {
|
|
// Ephemeral agent ID is encoded with endpoint and model
|
|
expect(result.id).toBe('openai__gpt-4');
|
|
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: 'agent_non_existent',
|
|
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: 'agent_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);
|
|
|
|
// Mock getMCPServerTools to return all tools for server1
|
|
getMCPServerTools.mockImplementation(async (_userId, server) => {
|
|
if (server === 'server1') {
|
|
return availableTools; // All 100 tools belong to server1
|
|
}
|
|
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' },
|
|
});
|
|
|
|
// 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 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
|
|
});
|
|
|
|
// Mock getMCPServerTools to return only tools matching the server
|
|
getMCPServerTools.mockImplementation(async (_userId, server) => {
|
|
if (server === 'server1') {
|
|
// Only return tool that correctly matches server1 format
|
|
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,
|
|
});
|
|
|
|
// 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();
|
|
});
|
|
|
|
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({});
|
|
|
|
// Create two users
|
|
userA = new mongoose.Types.ObjectId();
|
|
userB = new mongoose.Types.ObjectId();
|
|
|
|
// Create agents for user A
|
|
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 () => {
|
|
// User B has no agents and no shared agents
|
|
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 () => {
|
|
// User B trying to list agents with empty accessibleIds should not see User A's agents
|
|
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 () => {
|
|
// Give User B access to only one of User A's agents
|
|
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 () => {
|
|
// Give User B access to two of User A's agents
|
|
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 () => {
|
|
// Give access to all agents but filter by name
|
|
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 () => {
|
|
// Create more agents
|
|
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);
|
|
}
|
|
|
|
// Give access to all agents
|
|
const allAgentIds = [agentA1, agentA2, agentA3, ...moreAgents].map((a) => a._id);
|
|
|
|
// First page
|
|
const page1 = await getListAgentsByAccess({
|
|
accessibleIds: allAgentIds,
|
|
otherParams: {},
|
|
limit: 5,
|
|
});
|
|
|
|
expect(page1.data).toHaveLength(5);
|
|
expect(page1.has_more).toBe(true);
|
|
expect(page1.after).toBeTruthy();
|
|
|
|
// Second page
|
|
const page2 = await getListAgentsByAccess({
|
|
accessibleIds: allAgentIds,
|
|
otherParams: {},
|
|
limit: 5,
|
|
after: page1.after,
|
|
});
|
|
|
|
expect(page2.data).toHaveLength(5);
|
|
expect(page2.has_more).toBe(false);
|
|
|
|
// Verify no overlap between pages
|
|
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 () => {
|
|
// Try with non-existent agent IDs
|
|
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 () => {
|
|
// When accessibleIds is undefined, it should be treated as empty array
|
|
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 () => {
|
|
// Create an agent for User B
|
|
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,
|
|
});
|
|
|
|
// Give User B access to one of User A's agents
|
|
const accessibleIds = [agentA1._id, agentB1._id];
|
|
|
|
// Filter by author should further restrict the results
|
|
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] },
|
|
},
|
|
];
|
|
}
|