feat: Add agent role assignment and database CRUD tools for AI agent nodes (#12888)
This PR introduces a significant enhancement to the role-based permission system by extending it to support AI agents, enabling them to perform database operations based on assigned permissions. ## Key Changes ### 1. Database Schema Migration - **Table Rename**: `userWorkspaceRole` → `roleTargets` to better reflect its expanded purpose - **New Column**: Added `agentId` (UUID, nullable) to support AI agent role assignments - **Constraint Updates**: - Made `userWorkspaceId` nullable to accommodate agent-only role assignments - Added check constraint `CHK_role_targets_either_agent_or_user` ensuring either `agentId` OR `userWorkspaceId` is set (not both) ### 2. Entity & Service Layer Updates - **RoleTargetsEntity**: Updated with new `agentId` field and constraint validation - **AgentRoleService**: New service for managing agent role assignments with validation - **AgentService**: Enhanced to include role information when retrieving agents - **RoleResolver**: Added GraphQL mutations for `assignRoleToAgent` and `removeRoleFromAgent` ### 3. AI Agent CRUD Operations - **Permission-Based Tool Generation**: AI agents now receive database tools based on their assigned role permissions - **Dynamic Tool Creation**: The `AgentToolService` generates CRUD tools (`create_*`, `find_*`, `update_*`, `soft_delete_*`, `destroy_*`) for each object based on role permissions - **Granular Permissions**: Supports both global role permissions (`canReadAllObjectRecords`) and object-specific permissions (`canReadObjectRecords`) ### 4. Frontend Integration - **Role Assignment UI**: Added hooks and components for assigning/removing roles from agents ## Demo https://github.com/user-attachments/assets/41732267-742e-416c-b423-b687c2614c82 --------- Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Guillim <guillim@users.noreply.github.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com> Co-authored-by: Weiko <corentin@twenty.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: Baptiste Devessier <baptiste@devessier.fr> Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com> Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Co-authored-by: prastoin <paul@twenty.com> Co-authored-by: Vicky Wang <157669812+vickywxng@users.noreply.github.com> Co-authored-by: Vicky Wang <vw92@cornell.edu> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,21 @@
|
||||
export const AGENT_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
description
|
||||
prompt
|
||||
modelId
|
||||
responseFormat
|
||||
roleId
|
||||
createdAt
|
||||
updatedAt
|
||||
`;
|
||||
|
||||
export const AGENT_TOOL_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
description
|
||||
type
|
||||
config
|
||||
createdAt
|
||||
updatedAt
|
||||
`;
|
||||
@ -0,0 +1,251 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { AGENT_GQL_FIELDS } from 'test/integration/constants/agent-gql-fields.constants';
|
||||
import { createAgentOperation } from 'test/integration/graphql/utils/create-agent-operation-factory.util';
|
||||
import { deleteAgentOperation } from 'test/integration/graphql/utils/delete-agent-operation-factory.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateAgentOperation } from 'test/integration/graphql/utils/update-agent-operation-factory.util';
|
||||
|
||||
describe('agentResolver', () => {
|
||||
describe('createOneAgent', () => {
|
||||
it('should create an agent successfully', async () => {
|
||||
const operation = createAgentOperation({
|
||||
name: 'Test AI Agent Admin',
|
||||
description: 'A test AI agent created by admin',
|
||||
prompt: 'You are a helpful AI assistant for testing.',
|
||||
modelId: 'gpt-4o',
|
||||
responseFormat: { type: 'json_object' },
|
||||
});
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.createOneAgent).toBeDefined();
|
||||
expect(response.body.data.createOneAgent.id).toBeDefined();
|
||||
expect(response.body.data.createOneAgent.name).toBe(
|
||||
'Test AI Agent Admin',
|
||||
);
|
||||
expect(response.body.data.createOneAgent.description).toBe(
|
||||
'A test AI agent created by admin',
|
||||
);
|
||||
expect(response.body.data.createOneAgent.prompt).toBe(
|
||||
'You are a helpful AI assistant for testing.',
|
||||
);
|
||||
expect(response.body.data.createOneAgent.modelId).toBe('gpt-4o');
|
||||
expect(response.body.data.createOneAgent.responseFormat).toEqual({
|
||||
type: 'json_object',
|
||||
});
|
||||
await makeGraphqlAPIRequest(
|
||||
deleteAgentOperation(response.body.data.createOneAgent.id),
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate required fields and return error', async () => {
|
||||
const operation = createAgentOperation({
|
||||
name: undefined as any,
|
||||
description: 'Agent without required fields',
|
||||
prompt: undefined as any,
|
||||
modelId: undefined as any,
|
||||
});
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toContain(
|
||||
'Field "name" of required type "String!" was not provided',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOneAgent', () => {
|
||||
let testAgentId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const operation = createAgentOperation({
|
||||
name: 'Test Agent for Find',
|
||||
description: 'A test agent for find operations',
|
||||
prompt: 'You are a test agent for finding.',
|
||||
modelId: 'gpt-4o',
|
||||
});
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
testAgentId = response.body.data.createOneAgent.id;
|
||||
});
|
||||
afterAll(async () => {
|
||||
await makeGraphqlAPIRequest(deleteAgentOperation(testAgentId));
|
||||
});
|
||||
it('should find agent by ID successfully', async () => {
|
||||
const queryData = {
|
||||
query: gql`
|
||||
query FindOneAgent($input: AgentIdInput!) {
|
||||
findOneAgent(input: $input) {
|
||||
${AGENT_GQL_FIELDS}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { input: { id: testAgentId } },
|
||||
};
|
||||
const response = await makeGraphqlAPIRequest(queryData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.findOneAgent).toBeDefined();
|
||||
expect(response.body.data.findOneAgent.id).toBe(testAgentId);
|
||||
expect(response.body.data.findOneAgent.name).toBe('Test Agent for Find');
|
||||
});
|
||||
it('should return 404 error for non-existent agent', async () => {
|
||||
const nonExistentId = '00000000-0000-0000-0000-000000000000';
|
||||
const queryData = {
|
||||
query: gql`
|
||||
query FindOneAgent($input: AgentIdInput!) {
|
||||
findOneAgent(input: $input) {
|
||||
${AGENT_GQL_FIELDS}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { input: { id: nonExistentId } },
|
||||
};
|
||||
const response = await makeGraphqlAPIRequest(queryData);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toContain('not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findManyAgents', () => {
|
||||
const testAgentIds: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const operation = createAgentOperation({
|
||||
name: `Test Agent ${i + 1}`,
|
||||
description: `A test agent ${i + 1} for find many operations`,
|
||||
prompt: `You are test agent ${i + 1}.`,
|
||||
modelId: 'gpt-4o',
|
||||
});
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
testAgentIds.push(response.body.data.createOneAgent.id);
|
||||
}
|
||||
});
|
||||
afterAll(async () => {
|
||||
for (const agentId of testAgentIds) {
|
||||
await makeGraphqlAPIRequest(deleteAgentOperation(agentId));
|
||||
}
|
||||
});
|
||||
it('should find all agents successfully', async () => {
|
||||
const queryData = {
|
||||
query: gql`
|
||||
query FindManyAgents {
|
||||
findManyAgents {
|
||||
${AGENT_GQL_FIELDS}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
const response = await makeGraphqlAPIRequest(queryData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.findManyAgents).toBeDefined();
|
||||
expect(Array.isArray(response.body.data.findManyAgents)).toBe(true);
|
||||
expect(response.body.data.findManyAgents.length).toBeGreaterThanOrEqual(
|
||||
3,
|
||||
);
|
||||
const testAgentNames = response.body.data.findManyAgents
|
||||
.filter((agent: any) => testAgentIds.includes(agent.id))
|
||||
.map((agent: any) => agent.name);
|
||||
|
||||
expect(testAgentNames).toContain('Test Agent 1');
|
||||
expect(testAgentNames).toContain('Test Agent 2');
|
||||
expect(testAgentNames).toContain('Test Agent 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOneAgent', () => {
|
||||
let testAgentId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const operation = createAgentOperation({
|
||||
name: 'Original Test Agent',
|
||||
description: 'Original description',
|
||||
prompt: 'Original prompt',
|
||||
modelId: 'gpt-4o',
|
||||
});
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
testAgentId = response.body.data.createOneAgent.id;
|
||||
});
|
||||
afterAll(async () => {
|
||||
await makeGraphqlAPIRequest(deleteAgentOperation(testAgentId));
|
||||
});
|
||||
it('should update an agent successfully', async () => {
|
||||
const operation = updateAgentOperation({
|
||||
id: testAgentId,
|
||||
name: 'Updated Test Agent Admin',
|
||||
description: 'Updated description',
|
||||
prompt: 'Updated prompt for admin',
|
||||
modelId: 'gpt-4o-mini',
|
||||
});
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updateOneAgent).toBeDefined();
|
||||
expect(response.body.data.updateOneAgent.id).toBe(testAgentId);
|
||||
expect(response.body.data.updateOneAgent.name).toBe(
|
||||
'Updated Test Agent Admin',
|
||||
);
|
||||
expect(response.body.data.updateOneAgent.description).toBe(
|
||||
'Updated description',
|
||||
);
|
||||
expect(response.body.data.updateOneAgent.prompt).toBe(
|
||||
'Updated prompt for admin',
|
||||
);
|
||||
expect(response.body.data.updateOneAgent.modelId).toBe('gpt-4o-mini');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOneAgent', () => {
|
||||
let testAgentId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const operation = createAgentOperation({
|
||||
name: 'Agent to Delete',
|
||||
description: 'This agent will be deleted',
|
||||
prompt: 'You are an agent that will be deleted.',
|
||||
modelId: 'gpt-4o',
|
||||
});
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
testAgentId = response.body.data.createOneAgent.id;
|
||||
});
|
||||
it('should delete an agent successfully', async () => {
|
||||
const operation = deleteAgentOperation(testAgentId);
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.deleteOneAgent).toBeDefined();
|
||||
expect(response.body.data.deleteOneAgent.id).toBe(testAgentId);
|
||||
expect(response.body.data.deleteOneAgent.name).toBe('Agent to Delete');
|
||||
const findQueryData = {
|
||||
query: gql`
|
||||
query FindOneAgent($input: AgentIdInput!) {
|
||||
findOneAgent(input: $input) {
|
||||
${AGENT_GQL_FIELDS}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { input: { id: testAgentId } },
|
||||
};
|
||||
const findResponse = await makeGraphqlAPIRequest(findQueryData);
|
||||
|
||||
expect(findResponse.body.errors).toBeDefined();
|
||||
expect(findResponse.body.errors[0].message).toContain('not found');
|
||||
});
|
||||
it('should return 404 error for non-existent agent', async () => {
|
||||
const nonExistentId = '00000000-0000-0000-0000-000000000000';
|
||||
const operation = deleteAgentOperation(nonExistentId);
|
||||
const response = await makeGraphqlAPIRequest(operation);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toContain('not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { AGENT_GQL_FIELDS } from 'test/integration/constants/agent-gql-fields.constants';
|
||||
|
||||
export const createAgentOperation = ({
|
||||
name,
|
||||
description,
|
||||
prompt,
|
||||
modelId,
|
||||
responseFormat,
|
||||
}: {
|
||||
name: string;
|
||||
description?: string;
|
||||
prompt: string;
|
||||
modelId: string;
|
||||
responseFormat?: object;
|
||||
}) => ({
|
||||
query: gql`
|
||||
mutation CreateOneAgent($input: CreateAgentInput!) {
|
||||
createOneAgent(input: $input) {
|
||||
${AGENT_GQL_FIELDS}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
name,
|
||||
description,
|
||||
prompt,
|
||||
modelId,
|
||||
responseFormat,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { AGENT_GQL_FIELDS } from 'test/integration/constants/agent-gql-fields.constants';
|
||||
|
||||
export const deleteAgentOperation = (id: string) => ({
|
||||
query: gql`
|
||||
mutation DeleteOneAgent($input: AgentIdInput!) {
|
||||
deleteOneAgent(input: $input) {
|
||||
${AGENT_GQL_FIELDS}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { AGENT_GQL_FIELDS } from 'test/integration/constants/agent-gql-fields.constants';
|
||||
|
||||
export const updateAgentOperation = ({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
prompt,
|
||||
modelId,
|
||||
responseFormat,
|
||||
}: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
prompt?: string;
|
||||
modelId?: string;
|
||||
responseFormat?: object;
|
||||
}) => ({
|
||||
query: gql`
|
||||
mutation UpdateOneAgent($input: UpdateAgentInput!) {
|
||||
updateOneAgent(input: $input) {
|
||||
${AGENT_GQL_FIELDS}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id,
|
||||
...(name && { name }),
|
||||
...(description && { description }),
|
||||
...(prompt && { prompt }),
|
||||
...(modelId && { modelId }),
|
||||
...(responseFormat && { responseFormat }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,954 @@
|
||||
import {
|
||||
AgentToolTestContext,
|
||||
createAgentToolTestModule,
|
||||
createMockRepository,
|
||||
createTestRecord,
|
||||
createTestRecords,
|
||||
expectErrorResult,
|
||||
expectSuccessResult,
|
||||
setupBasicPermissions,
|
||||
setupRepositoryMock,
|
||||
} from './utils/agent-tool-test-utils';
|
||||
|
||||
describe('AgentToolService Integration', () => {
|
||||
let context: AgentToolTestContext;
|
||||
|
||||
beforeEach(async () => {
|
||||
context = await createAgentToolTestModule();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await context.module.close();
|
||||
});
|
||||
|
||||
describe('Tool Generation', () => {
|
||||
it('should generate complete tool set for agent with full permissions', async () => {
|
||||
const roleWithFullPermissions = {
|
||||
...context.testRole,
|
||||
canDestroyAllObjectRecords: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(roleWithFullPermissions);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
'getRolesPermissionsFromCache',
|
||||
)
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
version: '1.0',
|
||||
});
|
||||
jest
|
||||
.spyOn(context.objectMetadataService, 'findManyWithinWorkspace')
|
||||
.mockResolvedValue([context.testObjectMetadata]);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
|
||||
expect(tools).toBeDefined();
|
||||
expect(Object.keys(tools)).toHaveLength(8);
|
||||
expect(Object.keys(tools)).toContain('create_testObject');
|
||||
expect(Object.keys(tools)).toContain('update_testObject');
|
||||
expect(Object.keys(tools)).toContain('find_testObject');
|
||||
expect(Object.keys(tools)).toContain('find_one_testObject');
|
||||
expect(Object.keys(tools)).toContain('soft_delete_testObject');
|
||||
expect(Object.keys(tools)).toContain('soft_delete_many_testObject');
|
||||
expect(Object.keys(tools)).toContain('destroy_testObject');
|
||||
expect(Object.keys(tools)).toContain('destroy_many_testObject');
|
||||
});
|
||||
|
||||
it('should generate read-only tools for agent with read permissions only', async () => {
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(context.testRole);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
'getRolesPermissionsFromCache',
|
||||
)
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
version: '1.0',
|
||||
});
|
||||
jest
|
||||
.spyOn(context.objectMetadataService, 'findManyWithinWorkspace')
|
||||
.mockResolvedValue([context.testObjectMetadata]);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
|
||||
expect(tools).toBeDefined();
|
||||
expect(Object.keys(tools)).toHaveLength(2);
|
||||
expect(Object.keys(tools)).toContain('find_testObject');
|
||||
expect(Object.keys(tools)).toContain('find_one_testObject');
|
||||
expect(Object.keys(tools)).not.toContain('create_testObject');
|
||||
expect(Object.keys(tools)).not.toContain('update_testObject');
|
||||
});
|
||||
|
||||
it('should return empty tools for agent without role', async () => {
|
||||
const agentWithoutRole = { ...context.testAgent, roleId: null };
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(agentWithoutRole as any);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
|
||||
expect(tools).toEqual({});
|
||||
});
|
||||
|
||||
it('should return empty tools when role does not exist', async () => {
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest.spyOn(context.roleRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
|
||||
expect(tools).toEqual({});
|
||||
});
|
||||
|
||||
it('should filter out workflow-related objects', async () => {
|
||||
const workflowObject = {
|
||||
...context.testObjectMetadata,
|
||||
nameSingular: 'workflow',
|
||||
namePlural: 'workflows',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(context.testRole);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
'getRolesPermissionsFromCache',
|
||||
)
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[workflowObject.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
version: '1.0',
|
||||
});
|
||||
jest
|
||||
.spyOn(context.objectMetadataService, 'findManyWithinWorkspace')
|
||||
.mockResolvedValue([workflowObject]);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
|
||||
expect(tools).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create Record Operations', () => {
|
||||
it('should create a record successfully', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
const testRecord = createTestRecord('test-record-id', {
|
||||
name: 'Test Record',
|
||||
description: 'Test description',
|
||||
});
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.save.mockResolvedValue(testRecord);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const createTool = tools['create_testObject'];
|
||||
|
||||
expect(createTool).toBeDefined();
|
||||
|
||||
if (!createTool.execute) {
|
||||
throw new Error(
|
||||
'Create tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await createTool.execute(
|
||||
{ name: 'Test Record', description: 'Test description' },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Test Record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(result, 'Successfully created testObject');
|
||||
expect(result.record).toEqual(testRecord);
|
||||
expect(mockRepository.save).toHaveBeenCalledWith({
|
||||
name: 'Test Record',
|
||||
description: 'Test description',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle create record errors gracefully', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.save.mockRejectedValue(
|
||||
new Error('Database constraint violation'),
|
||||
);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const createTool = tools['create_testObject'];
|
||||
|
||||
expect(createTool).toBeDefined();
|
||||
|
||||
if (!createTool.execute) {
|
||||
throw new Error(
|
||||
'Create tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await createTool.execute(
|
||||
{ name: 'Test Record' },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Test Record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectErrorResult(
|
||||
result,
|
||||
'Database constraint violation',
|
||||
'Failed to create testObject',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Find Record Operations', () => {
|
||||
it('should find records with basic parameters', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
const testRecords = createTestRecords(3);
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.find.mockResolvedValue(testRecords);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const findTool = tools['find_testObject'];
|
||||
|
||||
expect(findTool).toBeDefined();
|
||||
|
||||
if (!findTool.execute) {
|
||||
throw new Error(
|
||||
'Find tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await findTool.execute(
|
||||
{ limit: 10, offset: 0 },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Find records',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(result, 'Found 3 testObject records');
|
||||
expect(result.records).toEqual(testRecords);
|
||||
expect(result.count).toBe(3);
|
||||
expect(mockRepository.find).toHaveBeenCalledWith({
|
||||
where: {},
|
||||
take: 10,
|
||||
skip: 0,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should find one record by ID', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
const testRecord = createTestRecord('test-record-id', {
|
||||
name: 'Test Record',
|
||||
});
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.findOne.mockResolvedValue(testRecord);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const findOneTool = tools['find_one_testObject'];
|
||||
|
||||
expect(findOneTool).toBeDefined();
|
||||
|
||||
if (!findOneTool.execute) {
|
||||
throw new Error(
|
||||
'Find one tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await findOneTool.execute(
|
||||
{ id: 'test-record-id' },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Find one record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(result, 'Found testObject record');
|
||||
expect(result.record).toEqual(testRecord);
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: 'test-record-id' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle find one record not found', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const findOneTool = tools['find_one_testObject'];
|
||||
|
||||
expect(findOneTool).toBeDefined();
|
||||
|
||||
if (!findOneTool.execute) {
|
||||
throw new Error(
|
||||
'Find one tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await findOneTool.execute(
|
||||
{ id: 'non-existent-id' },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Find one record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectErrorResult(
|
||||
result,
|
||||
'Record not found',
|
||||
'Failed to find testObject: Record with ID non-existent-id not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle find one record without ID', async () => {
|
||||
setupBasicPermissions(context);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const findOneTool = tools['find_one_testObject'];
|
||||
|
||||
expect(findOneTool).toBeDefined();
|
||||
|
||||
if (!findOneTool.execute) {
|
||||
throw new Error(
|
||||
'Find one tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await findOneTool.execute(
|
||||
{},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Find one record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectErrorResult(
|
||||
result,
|
||||
'Record ID is required',
|
||||
'Failed to find testObject: Record ID is required',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update Record Operations', () => {
|
||||
it('should update a record successfully', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
const existingRecord = createTestRecord('test-record-id', {
|
||||
name: 'Old Name',
|
||||
description: 'Old description',
|
||||
});
|
||||
const updatedRecord = createTestRecord('test-record-id', {
|
||||
name: 'New Name',
|
||||
description: 'New description',
|
||||
});
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
jest
|
||||
.spyOn(context.objectMetadataService, 'findOneWithinWorkspace')
|
||||
.mockResolvedValue(context.testObjectMetadata);
|
||||
mockRepository.findOne
|
||||
.mockResolvedValueOnce(existingRecord)
|
||||
.mockResolvedValueOnce(updatedRecord);
|
||||
mockRepository.update.mockResolvedValue({ affected: 1 } as any);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const updateTool = tools['update_testObject'];
|
||||
|
||||
expect(updateTool).toBeDefined();
|
||||
|
||||
if (!updateTool.execute) {
|
||||
throw new Error(
|
||||
'Update tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await updateTool.execute(
|
||||
{
|
||||
id: 'test-record-id',
|
||||
name: 'New Name',
|
||||
description: 'New description',
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Update record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(result, 'Successfully updated testObject');
|
||||
expect(result.record).toEqual(updatedRecord);
|
||||
expect(mockRepository.update).toHaveBeenCalledWith('test-record-id', {
|
||||
name: 'New Name',
|
||||
description: 'New description',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle update record not found', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const updateTool = tools['update_testObject'];
|
||||
|
||||
expect(updateTool).toBeDefined();
|
||||
|
||||
if (!updateTool.execute) {
|
||||
throw new Error(
|
||||
'Update tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await updateTool.execute(
|
||||
{
|
||||
id: 'non-existent-id',
|
||||
name: 'New Name',
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Update record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectErrorResult(
|
||||
result,
|
||||
'Record not found',
|
||||
'Failed to update testObject: Record with ID non-existent-id not found',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Soft Delete Operations', () => {
|
||||
it('should soft delete a single record', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
const existingRecord = createTestRecord('test-record-id', {
|
||||
name: 'Test Record',
|
||||
});
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.findOne.mockResolvedValue(existingRecord);
|
||||
mockRepository.softDelete.mockResolvedValue({ affected: 1 } as any);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const softDeleteTool = tools['soft_delete_testObject'];
|
||||
|
||||
expect(softDeleteTool).toBeDefined();
|
||||
|
||||
if (!softDeleteTool.execute) {
|
||||
throw new Error(
|
||||
'Soft delete tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await softDeleteTool.execute(
|
||||
{ id: 'test-record-id' },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Soft delete record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(result, 'Successfully soft deleted testObject');
|
||||
expect(mockRepository.softDelete).toHaveBeenCalledWith('test-record-id');
|
||||
});
|
||||
|
||||
it('should soft delete multiple records', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
const existingRecords = createTestRecords(3);
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.find.mockResolvedValue(existingRecords);
|
||||
mockRepository.softDelete.mockResolvedValue({ affected: 3 } as any);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const softDeleteManyTool = tools['soft_delete_many_testObject'];
|
||||
|
||||
expect(softDeleteManyTool).toBeDefined();
|
||||
|
||||
if (!softDeleteManyTool.execute) {
|
||||
throw new Error(
|
||||
'Soft delete many tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await softDeleteManyTool.execute(
|
||||
{
|
||||
filter: { id: { in: ['record-1', 'record-2', 'record-3'] } },
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Soft delete many records',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(
|
||||
result,
|
||||
'Successfully soft deleted 3 testObject records',
|
||||
);
|
||||
expect(mockRepository.softDelete).toHaveBeenCalledWith({
|
||||
id: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Destroy Operations', () => {
|
||||
it('should destroy a single record', async () => {
|
||||
const roleWithDestroyPermission = {
|
||||
...context.testRole,
|
||||
canDestroyAllObjectRecords: true,
|
||||
};
|
||||
const mockRepository = createMockRepository();
|
||||
const existingRecord = createTestRecord('test-record-id', {
|
||||
name: 'Test Record',
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(roleWithDestroyPermission);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
'getRolesPermissionsFromCache',
|
||||
)
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
version: '1.0',
|
||||
});
|
||||
jest
|
||||
.spyOn(context.objectMetadataService, 'findManyWithinWorkspace')
|
||||
.mockResolvedValue([context.testObjectMetadata]);
|
||||
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.findOne.mockResolvedValue(existingRecord);
|
||||
mockRepository.remove.mockResolvedValue(existingRecord);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const destroyTool = tools['destroy_testObject'];
|
||||
|
||||
expect(destroyTool).toBeDefined();
|
||||
|
||||
if (!destroyTool.execute) {
|
||||
throw new Error(
|
||||
'Destroy tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await destroyTool.execute(
|
||||
{ id: 'test-record-id' },
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Destroy record',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(result, 'Successfully destroyed testObject');
|
||||
expect(mockRepository.remove).toHaveBeenCalledWith(existingRecord);
|
||||
});
|
||||
|
||||
it('should destroy multiple records', async () => {
|
||||
const roleWithDestroyPermission = {
|
||||
...context.testRole,
|
||||
canDestroyAllObjectRecords: true,
|
||||
};
|
||||
const mockRepository = createMockRepository();
|
||||
const existingRecords = createTestRecords(3);
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(roleWithDestroyPermission);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
'getRolesPermissionsFromCache',
|
||||
)
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
version: '1.0',
|
||||
});
|
||||
jest
|
||||
.spyOn(context.objectMetadataService, 'findManyWithinWorkspace')
|
||||
.mockResolvedValue([context.testObjectMetadata]);
|
||||
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.find.mockResolvedValue(existingRecords);
|
||||
mockRepository.remove.mockResolvedValue(existingRecords);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const destroyManyTool = tools['destroy_many_testObject'];
|
||||
|
||||
expect(destroyManyTool).toBeDefined();
|
||||
|
||||
if (!destroyManyTool.execute) {
|
||||
throw new Error(
|
||||
'Destroy many tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await destroyManyTool.execute(
|
||||
{
|
||||
filter: { id: { in: ['record-1', 'record-2', 'record-3'] } },
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Destroy many records',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(
|
||||
result,
|
||||
'Successfully destroyed 3 testObject records',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty search criteria in find records', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
const testRecords = createTestRecords(2);
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.find.mockResolvedValue(testRecords);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const findTool = tools['find_testObject'];
|
||||
|
||||
expect(findTool).toBeDefined();
|
||||
|
||||
if (!findTool.execute) {
|
||||
throw new Error(
|
||||
'Find tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await findTool.execute(
|
||||
{},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Find records',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(result, 'Found 2 testObject records');
|
||||
expect(mockRepository.find).toHaveBeenCalledWith({
|
||||
where: {},
|
||||
take: 100,
|
||||
skip: 0,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null and undefined values in search criteria', async () => {
|
||||
const mockRepository = createMockRepository();
|
||||
const testRecords = createTestRecords(1);
|
||||
|
||||
setupBasicPermissions(context);
|
||||
setupRepositoryMock(context, mockRepository);
|
||||
mockRepository.find.mockResolvedValue(testRecords);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
const findTool = tools['find_testObject'];
|
||||
|
||||
expect(findTool).toBeDefined();
|
||||
|
||||
if (!findTool.execute) {
|
||||
throw new Error(
|
||||
'Find tool is missing or does not have an execute method',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await findTool.execute(
|
||||
{
|
||||
name: null,
|
||||
description: undefined,
|
||||
status: '',
|
||||
validField: 'valid value',
|
||||
},
|
||||
{
|
||||
toolCallId: 'test-tool-call-id',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Find records',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expectSuccessResult(result, 'Found 1 testObject records');
|
||||
expect(mockRepository.find).toHaveBeenCalledWith({
|
||||
where: { validField: 'valid value' },
|
||||
take: 100,
|
||||
skip: 0,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple object metadata with different permissions', async () => {
|
||||
const secondObjectMetadata = {
|
||||
...context.testObjectMetadata,
|
||||
id: 'second-object-id',
|
||||
nameSingular: 'secondObject',
|
||||
namePlural: 'secondObjects',
|
||||
labelSingular: 'Second Object',
|
||||
labelPlural: 'Second Objects',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(context.testRole);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
'getRolesPermissionsFromCache',
|
||||
)
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
},
|
||||
[secondObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
version: '1.0',
|
||||
});
|
||||
jest
|
||||
.spyOn(context.objectMetadataService, 'findManyWithinWorkspace')
|
||||
.mockResolvedValue([context.testObjectMetadata, secondObjectMetadata]);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
);
|
||||
|
||||
expect(tools).toBeDefined();
|
||||
expect(Object.keys(tools)).toHaveLength(8);
|
||||
expect(Object.keys(tools)).toContain('create_testObject');
|
||||
expect(Object.keys(tools)).toContain('update_testObject');
|
||||
expect(Object.keys(tools)).toContain('find_testObject');
|
||||
expect(Object.keys(tools)).toContain('find_one_testObject');
|
||||
expect(Object.keys(tools)).toContain('soft_delete_secondObject');
|
||||
expect(Object.keys(tools)).toContain('soft_delete_many_secondObject');
|
||||
expect(Object.keys(tools)).not.toContain('soft_delete_testObject');
|
||||
expect(Object.keys(tools)).not.toContain('create_secondObject');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,262 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
|
||||
export interface AgentToolTestContext {
|
||||
module: TestingModule;
|
||||
agentToolService: AgentToolService;
|
||||
agentService: AgentService;
|
||||
objectMetadataService: ObjectMetadataService;
|
||||
roleRepository: Repository<RoleEntity>;
|
||||
workspacePermissionsCacheService: WorkspacePermissionsCacheService;
|
||||
twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
testAgent: AgentEntity & { roleId: string | null };
|
||||
testRole: RoleEntity;
|
||||
testObjectMetadata: ObjectMetadataEntity;
|
||||
testWorkspaceId: string;
|
||||
testAgentId: string;
|
||||
testRoleId: string;
|
||||
}
|
||||
|
||||
export const createAgentToolTestModule =
|
||||
async (): Promise<AgentToolTestContext> => {
|
||||
const testWorkspaceId = 'test-workspace-id';
|
||||
const testAgentId = 'test-agent-id';
|
||||
const testRoleId = 'test-role-id';
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
AgentToolService,
|
||||
{
|
||||
provide: AgentService,
|
||||
useValue: {
|
||||
findOneAgent: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RoleEntity, 'core'),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ObjectMetadataService,
|
||||
useValue: {
|
||||
findManyWithinWorkspace: jest.fn(),
|
||||
findOneWithinWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TwentyORMGlobalManager,
|
||||
useValue: {
|
||||
getRepositoryForWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceEventEmitter,
|
||||
useValue: {
|
||||
emit: jest.fn(),
|
||||
emitDatabaseBatchEvent: jest.fn(),
|
||||
emitCustomBatchEvent: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspacePermissionsCacheService,
|
||||
useValue: {
|
||||
getRolesPermissionsFromCache: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const agentToolService = module.get<AgentToolService>(AgentToolService);
|
||||
const agentService = module.get<AgentService>(AgentService);
|
||||
const objectMetadataService = module.get<ObjectMetadataService>(
|
||||
ObjectMetadataService,
|
||||
);
|
||||
const roleRepository = module.get<Repository<RoleEntity>>(
|
||||
getRepositoryToken(RoleEntity, 'core'),
|
||||
);
|
||||
const workspacePermissionsCacheService =
|
||||
module.get<WorkspacePermissionsCacheService>(
|
||||
WorkspacePermissionsCacheService,
|
||||
);
|
||||
const twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
|
||||
TwentyORMGlobalManager,
|
||||
);
|
||||
|
||||
const testAgent: AgentEntity & { roleId: string | null } = {
|
||||
id: testAgentId,
|
||||
name: 'Test Agent',
|
||||
description: 'Test agent for integration tests',
|
||||
prompt: 'You are a test agent',
|
||||
modelId: 'gpt-4o',
|
||||
responseFormat: {},
|
||||
workspaceId: testWorkspaceId,
|
||||
workspace: {} as any,
|
||||
roleId: testRoleId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const testRole: RoleEntity = {
|
||||
id: testRoleId,
|
||||
label: 'Test Role',
|
||||
description: 'Test role for integration tests',
|
||||
canUpdateAllSettings: false,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isEditable: true,
|
||||
} as RoleEntity;
|
||||
|
||||
const testObjectMetadata = {
|
||||
id: 'test-object-id',
|
||||
standardId: null,
|
||||
dataSourceId: 'test-data-source-id',
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
labelSingular: 'Test Object',
|
||||
labelPlural: 'Test Objects',
|
||||
description: 'Test object for integration tests',
|
||||
icon: 'IconTest',
|
||||
targetTableName: 'test_objects',
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isCustom: false,
|
||||
isRemote: false,
|
||||
isAuditLogged: true,
|
||||
isSearchable: false,
|
||||
shortcut: '',
|
||||
isLabelSyncedWithName: false,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
fields: [],
|
||||
indexMetadatas: [],
|
||||
targetRelationFields: [],
|
||||
dataSource: {} as any,
|
||||
objectPermissions: [],
|
||||
};
|
||||
|
||||
return {
|
||||
module,
|
||||
agentToolService,
|
||||
agentService,
|
||||
objectMetadataService,
|
||||
roleRepository,
|
||||
workspacePermissionsCacheService,
|
||||
twentyORMGlobalManager,
|
||||
testAgent,
|
||||
testRole,
|
||||
testObjectMetadata,
|
||||
testWorkspaceId,
|
||||
testAgentId,
|
||||
testRoleId,
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockRepository = () => ({
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
});
|
||||
|
||||
export const setupBasicPermissions = (context: AgentToolTestContext) => {
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(context.testRole);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
'getRolesPermissionsFromCache',
|
||||
)
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
version: '1.0',
|
||||
});
|
||||
jest
|
||||
.spyOn(context.objectMetadataService, 'findManyWithinWorkspace')
|
||||
.mockResolvedValue([context.testObjectMetadata]);
|
||||
};
|
||||
|
||||
export const setupRepositoryMock = (
|
||||
context: AgentToolTestContext,
|
||||
mockRepository: any,
|
||||
) => {
|
||||
jest
|
||||
.spyOn(context.twentyORMGlobalManager, 'getRepositoryForWorkspace')
|
||||
.mockResolvedValue(mockRepository);
|
||||
};
|
||||
|
||||
export const createTestRecord = (
|
||||
id: string,
|
||||
data: Record<string, any> = {},
|
||||
) => ({
|
||||
id,
|
||||
name: `Test Record ${id}`,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...data,
|
||||
});
|
||||
|
||||
export const createTestRecords = (
|
||||
count: number,
|
||||
baseData: Record<string, any> = {},
|
||||
) => {
|
||||
return Array.from({ length: count }, (_, i) =>
|
||||
createTestRecord(`record-${i + 1}`, baseData),
|
||||
);
|
||||
};
|
||||
|
||||
export const expectSuccessResult = (result: any, expectedMessage?: string) => {
|
||||
expect(result.success).toBe(true);
|
||||
if (expectedMessage) {
|
||||
expect(result.message).toContain(expectedMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export const expectErrorResult = (
|
||||
result: any,
|
||||
expectedError?: string,
|
||||
expectedMessage?: string,
|
||||
) => {
|
||||
expect(result.success).toBe(false);
|
||||
if (expectedError) {
|
||||
expect(result.error).toBe(expectedError);
|
||||
}
|
||||
if (expectedMessage) {
|
||||
expect(result.message).toContain(expectedMessage);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user