Skip to main content
Glama
core-workflows.e2e.test.ts•32 kB
/** * Core Workflows E2E Test Suite * * Consolidates the highest business value E2E tests for essential user workflows: * - Tasks Management Core Operations (Score: 44) * - Notes CRUD Operations (Score: 39) * * This consolidated suite covers: * - Test data setup and management (companies, people) * - Task lifecycle: creation, updates, deletion * - Notes management: creation, retrieval, formatting * - Cross-resource operations and relationships * * Total coverage: 15 essential user workflow scenarios * Combined business value score: 83/100 * * Part of Issue #526 Sprint 4 - E2E Test Consolidation */ import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, } from 'vitest'; import { E2ETestBase } from '../setup.js'; import { E2EAssertions } from '../utils/assertions.js'; import { CompanyFactory, PersonFactory, TaskFactory, } from '../fixtures/index.js'; import type { TestDataObject, McpToolResponse } from '../types/index.js'; // Define TaskRecord locally to avoid import issues interface TaskRecord { id: { task_id: string; record_id?: string; object_id?: string; }; type?: string; content?: string; title?: string; content_plaintext?: string; status?: string; due_date?: string; assignee_id?: string; assignee?: { id: string; referenced_actor_id?: string; }; assignees?: Array<{ referenced_actor_type: string; referenced_actor_id: string; }>; values?: { content?: Array<{ value: string }>; title?: Array<{ value: string }>; status?: Array<{ value: string }>; [key: string]: unknown; }; attributes?: Record<string, unknown>; created_at?: string; updated_at?: string; } // Define NoteRecord locally interface NoteRecord { id: { note_id?: string; record_id?: string; }; title?: string; content?: string; format?: string; created_at?: string; updated_at?: string; [key: string]: unknown; } // Define AttioRecord locally interface AttioRecord { id: { record_id: string; object_id?: string; }; values?: { name?: Array<{ value: string }>; [key: string]: unknown; }; [key: string]: unknown; } // Import enhanced tool callers with logging and migration import { callTasksTool, callNotesTool, validateTestEnvironment, getToolMigrationStats, } from '../utils/enhanced-tool-caller.js'; import { startTestSuite, endTestSuite } from '../utils/logger.js'; // Import notes-specific setup utilities import { testCompanies, testPeople, createdNotes, createSharedSetup, createTestCompany, createTestPerson, callNotesTool as notesToolCaller, E2EAssertions as NotesAssertions, noteFixtures, } from './notes-management/shared-setup.js'; /** * Helper function to safely cast tool responses to McpToolResponse */ function asToolResponse(response: unknown): McpToolResponse { return response as McpToolResponse; } /** * Helper function to safely extract task data from MCP response */ function extractTaskData(response: McpToolResponse): TaskRecord { const data = E2EAssertions.expectMcpData(response); if (!data) { throw new Error('No data returned from MCP tool response'); } return data as unknown as TaskRecord; } /** * Core Workflows E2E Test Suite - Consolidated Tasks + Notes */ describe.skipIf( !process.env.ATTIO_API_KEY || process.env.SKIP_E2E_TESTS === 'true' )('Core Workflows E2E Tests - Tasks & Notes', () => { // Task management test data const taskTestCompanies: TestDataObject[] = []; const taskTestPeople: TestDataObject[] = []; let createdTasks: TaskRecord[] = []; // Notes management setup const notesSetup = createSharedSetup(); beforeAll(async () => { // Start comprehensive logging for this consolidated test suite startTestSuite('core-workflows'); // Validate test environment and tool migration setup const envValidation = await validateTestEnvironment(); if (!envValidation.valid) { console.warn('āš ļø Test environment warnings:', envValidation.warnings); } console.error('šŸ“Š Tool migration stats:', getToolMigrationStats()); await E2ETestBase.setup({ requiresRealApi: true, // šŸ”’ Real API only for E2E tests cleanupAfterTests: true, timeout: 120000, }); // Initialize notes setup await notesSetup.beforeAll(); console.error( 'šŸš€ Starting Core Workflows E2E Tests - Consolidated Tasks & Notes' ); }, 60000); afterAll(async () => { // Cleanup notes await notesSetup.afterAll(); // End comprehensive logging for this test suite endTestSuite(); console.error( 'āœ… Core Workflows E2E Tests completed with enhanced logging' ); }, 60000); beforeEach(() => { vi.clearAllMocks(); notesSetup.beforeEach(); }); describe('Shared Test Data Setup', () => { it('should create test companies for task and note testing', async () => { // Create for tasks const companyData = CompanyFactory.create(); const response = asToolResponse( await callTasksTool('create-record', { resource_type: 'companies', record_data: companyData as any, }) ); E2EAssertions.expectMcpSuccess(response); const company = E2EAssertions.expectMcpData(response)!; E2EAssertions.expectCompanyRecord(company); taskTestCompanies.push(company); console.error( 'šŸ¢ Created test company for core workflows:', (company as any)?.id?.record_id ); // Also populate the shared testCompanies array for notes tests testCompanies.push(company); // Also create for notes (fallback) await createTestCompany(); }, 45000); it('should create test people for task assignment and note management', async () => { // Create for tasks const personData = PersonFactory.create(); const response = asToolResponse( await callTasksTool('create-record', { resource_type: 'people', record_data: personData as any, }) ); E2EAssertions.expectMcpSuccess(response); const person = E2EAssertions.expectMcpData(response)!; E2EAssertions.expectPersonRecord(person); taskTestPeople.push(person); console.error( 'šŸ‘¤ Created test person for core workflows:', (person as any)?.id?.record_id ); // Also populate the shared testPeople array for notes tests testPeople.push(person); // Also create for notes (fallback) await createTestPerson(); }, 45000); }); describe('Tasks Management - Core Operations', () => { describe('Task Creation and Basic Operations', () => { beforeAll(async () => { const { TestDataSeeder } = await import('../utils/test-data-seeder.js'); await TestDataSeeder.ensureCompany('task-linking', taskTestCompanies); }, 30000); it('should create a basic task', async () => { const taskData = TaskFactory.create(); const response = asToolResponse( await callTasksTool('create-record', { resource_type: 'tasks', record_data: { content: taskData.content, format: 'plaintext', deadline_at: taskData.due_date, }, }) ); E2EAssertions.expectMcpSuccess(response); const createdTask = extractTaskData(response); E2EAssertions.expectTaskRecord(createdTask); E2EAssertions.expectResourceId(createdTask, 'tasks'); // Access content from the correct field in the record structure const taskContent = createdTask.values?.content?.[0]?.value || createdTask.content || createdTask.title; expect(taskContent).toContain('Test Task'); createdTasks.push(createdTask); console.error('šŸ“‹ Created basic task:', createdTask.id.task_id); }, 30000); it('should create task with assignee', async () => { console.log('šŸ” ASSIGNEE TEST DEBUG', { taskTestPeopleLength: taskTestPeople.length, hasFirstPerson: !!taskTestPeople[0], firstPersonId: taskTestPeople[0]?.id?.record_id, }); if (taskTestPeople.length === 0) { console.error('ā­ļø Skipping assignee test - no test people available'); return; } const taskData = TaskFactory.create(); const assignee = taskTestPeople[0]; console.log('šŸŽÆ ASSIGNEE DEBUG', { assigneeId: assignee?.id?.record_id, assigneeKeys: Object.keys(assignee || {}), }); const response = asToolResponse( await callTasksTool('create-record', { resource_type: 'tasks', record_data: { content: taskData.content, format: 'plaintext', assignees: [ { referenced_actor_type: 'workspace-member', referenced_actor_id: assignee.id.record_id, }, ], deadline_at: taskData.due_date, }, }) ); E2EAssertions.expectMcpSuccess(response); const createdTask = extractTaskData(response); console.log('šŸ“‹ TASK RESPONSE DEBUG', { hasAssignee: !!createdTask.assignee, assigneeKeys: createdTask.assignee ? Object.keys(createdTask.assignee) : null, taskKeys: Object.keys(createdTask || {}), }); E2EAssertions.expectTaskRecord(createdTask); // Accept multiple valid response shapes from Attio API const assigneeIdCandidate = // Preferred: top-level assignee object with id (createdTask as any)?.assignee?.id || // Some responses may include referenced actor id shape (createdTask as any)?.assignee?.referenced_actor_id || // Or top-level assignees array (request echo or API variant) (createdTask as any)?.assignees?.[0]?.referenced_actor_id || // Or values.assignee attribute array (createdTask as any)?.values?.assignee?.[0]?.value; expect(assigneeIdCandidate).toBeDefined(); createdTasks.push(createdTask); console.error('šŸ‘„ Created task with assignee:', createdTask.id.task_id); }, 30000); it('should create task linked to company record', async () => { if (taskTestCompanies.length === 0) { console.error( 'ā­ļø Skipping record link test - no test companies available' ); return; } const taskData = TaskFactory.create(); const company = taskTestCompanies[0]; const response = asToolResponse( await callTasksTool('create-record', { resource_type: 'tasks', record_data: { content: `Follow up with ${company.values.name?.[0]?.value || 'company'}`, format: 'plaintext', recordId: company.id.record_id, targetObject: 'companies', deadline_at: taskData.due_date, }, }) ); E2EAssertions.expectMcpSuccess(response); const createdTask = extractTaskData(response); E2EAssertions.expectTaskRecord(createdTask); createdTasks.push(createdTask); console.error( 'šŸ”— Created task linked to company record:', createdTask.id.task_id ); }, 30000); it('should create high priority task', async () => { const taskData = TaskFactory.createHighPriority(); const response = asToolResponse( await callTasksTool('create-record', { resource_type: 'tasks', record_data: { content: taskData.content, format: 'plaintext', deadline_at: taskData.due_date, }, }) ); E2EAssertions.expectMcpSuccess(response); const createdTask = extractTaskData(response); E2EAssertions.expectTaskRecord(createdTask); // Check for content in various possible locations const taskContent = createdTask.content || createdTask.title || createdTask.values?.content?.[0]?.value || createdTask.values?.title?.[0]?.value || createdTask.content_plaintext; expect(taskContent).toContain('Test Task'); createdTasks.push(createdTask); console.error('⚔ Created high priority task:', createdTask.id.task_id); }, 30000); }); describe('Task Updates and Modifications', () => { it('should update task status', async () => { if (createdTasks.length === 0) { console.error( 'ā­ļø Skipping status update test - no created tasks available' ); return; } const task = createdTasks[0]; const taskId = task.id.task_id; const response = asToolResponse( await callTasksTool('update-record', { resource_type: 'tasks', record_id: taskId, record_data: { is_completed: true, }, }) ); E2EAssertions.expectMcpSuccess(response); const updatedTask = extractTaskData(response); E2EAssertions.expectResourceId(updatedTask, 'tasks'); expect(updatedTask.id.task_id).toBe(taskId); console.error('āœ… Updated task status:', taskId); }, 30000); it('should update task assignee', async () => { if (createdTasks.length === 0 || taskTestPeople.length === 0) { console.error( 'ā­ļø Skipping assignee update test - insufficient test data' ); return; } const task = createdTasks[0]; const taskId = task.id.task_id; const newAssignee = taskTestPeople[0]; const response = asToolResponse( await callTasksTool('update-record', { resource_type: 'tasks', record_id: taskId, record_data: { assignees: [ { referenced_actor_type: 'workspace-member', referenced_actor_id: newAssignee.id.record_id, }, ], }, }) ); E2EAssertions.expectMcpSuccess(response); const updatedTask = extractTaskData(response); E2EAssertions.expectResourceId(updatedTask, 'tasks'); expect(updatedTask.id.task_id).toBe(taskId); console.error('šŸ‘¤ Updated task assignee:', taskId); }, 30000); it('should update multiple task fields simultaneously', async () => { if (createdTasks.length === 0) { console.error( 'ā­ļø Skipping multi-field update test - no created tasks available' ); return; } const task = createdTasks[0]; const taskId = task.id.task_id; const newDueDate = new Date(); newDueDate.setDate(newDueDate.getDate() + 7); const response = asToolResponse( await callTasksTool('update-record', { resource_type: 'tasks', record_id: taskId, record_data: { is_completed: false, deadline_at: newDueDate.toISOString(), }, }) ); E2EAssertions.expectMcpSuccess(response); const updatedTask = extractTaskData(response); E2EAssertions.expectResourceId(updatedTask, 'tasks'); expect(updatedTask.id.task_id).toBe(taskId); console.error('šŸ”„ Updated multiple task fields:', taskId); }, 30000); }); describe('Task Deletion and Cleanup', () => { beforeAll(async () => { const { TestDataSeeder } = await import('../utils/test-data-seeder.js'); await TestDataSeeder.ensureTask('deletion-test', createdTasks as any); }, 30000); it('should delete individual tasks', async () => { if (createdTasks.length === 0) { console.error( 'ā­ļø Skipping deletion test - no created tasks available' ); return; } // Delete the last created task to avoid affecting other tests const taskToDelete = createdTasks[createdTasks.length - 1]; const taskId = taskToDelete.id.task_id; const response = asToolResponse( await callTasksTool('delete-record', { resource_type: 'tasks', record_id: taskId, }) ); E2EAssertions.expectMcpSuccess(response); // Remove from our tracking createdTasks = createdTasks.filter((t) => t.id.task_id !== taskId); console.error('šŸ—‘ļø Deleted task:', taskId); }, 30000); it('should handle deletion of non-existent task gracefully', async () => { const response = asToolResponse( await callTasksTool('delete-record', { resource_type: 'tasks', record_id: 'already-deleted-task-12345', }) ); E2EAssertions.expectMcpError( response, /not found|invalid|does not exist|missing required parameter|bad request|400|delete.*failed|failed.*400/i ); console.error('āœ… Handled non-existent task deletion gracefully'); }, 15000); }); }); describe('Notes Management - CRUD Operations', () => { describe('Company Notes Management', () => { // Ensure at least one company exists when running this block in isolation beforeAll(async () => { try { if (testCompanies.length === 0) { const companyData = CompanyFactory.create(); const resp = asToolResponse( await callNotesTool('create-record', { resource_type: 'companies', record_data: companyData as any, }) ); if (!resp.isError) { const created = E2EAssertions.expectMcpData(resp) as any; if (created?.id?.record_id) testCompanies.push(created); } } } catch {} }); it('should create a company note with basic content', async () => { if (testCompanies.length === 0) { console.error( 'ā­ļø Skipping company note test - no test companies available' ); return; } const testCompany = testCompanies[0] as unknown as AttioRecord; if (!testCompany?.id?.record_id) { console.error('ā­ļø Skipping company note test - invalid company data'); return; } const noteData = noteFixtures.companies.meeting( testCompany.id.record_id ); const response = (await notesToolCaller('create-note', { resource_type: 'companies', record_id: testCompany.id.record_id, title: noteData.title, content: noteData.content, format: 'markdown', })) as McpToolResponse; NotesAssertions.expectMcpSuccess(response); const createdNote = NotesAssertions.expectMcpData( response ) as unknown as NoteRecord; NotesAssertions.expectValidNoteStructure(createdNote); expect(createdNote.title).toBe(noteData.title); expect(createdNote.content).toBe(noteData.content); createdNotes.push(createdNote); console.error('šŸ“ Created company note:', createdNote.title); }, 30000); it('should retrieve company notes', async () => { if (testCompanies.length === 0) { console.error( 'ā­ļø Skipping get company notes test - no test companies available' ); return; } const testCompany = testCompanies[0] as unknown as AttioRecord; if (!testCompany?.id?.record_id) { console.error( 'ā­ļø Skipping get company notes test - invalid company data' ); return; } const response = (await notesToolCaller('list-notes', { resource_type: 'companies', parent_object: 'companies', record_id: testCompany.id.record_id, limit: 10, offset: 0, })) as McpToolResponse; NotesAssertions.expectMcpSuccess(response); const notes = NotesAssertions.expectMcpData(response); // Notes might be an array or a response object with data array let noteArray: any[] = []; if (Array.isArray(notes)) { noteArray = notes; } else if (notes && Array.isArray(notes.data)) { noteArray = notes.data; } expect(noteArray).toBeDefined(); if (noteArray.length > 0) { NotesAssertions.expectValidNoteCollection(noteArray); console.error('šŸ“‹ Retrieved company notes:', noteArray.length); } else { console.error( 'šŸ“‹ No company notes found (expected for new test data)' ); } }, 30000); it('should create company note with markdown content', async () => { if (testCompanies.length === 0) { console.error( 'ā­ļø Skipping markdown note test - no test companies available' ); return; } const testCompany = testCompanies[0] as unknown as AttioRecord; if (!testCompany?.id?.record_id) { console.error( 'ā­ļø Skipping markdown note test - invalid company data' ); return; } const noteData = noteFixtures.markdown.meetingAgenda( testCompany.id.record_id ); const response = (await notesToolCaller('create-note', { resource_type: 'companies', record_id: testCompany.id.record_id, title: noteData.title, content: noteData.content, format: 'markdown', })) as McpToolResponse; NotesAssertions.expectMcpSuccess(response); const createdNote = NotesAssertions.expectMcpData( response ) as unknown as NoteRecord; NotesAssertions.expectValidNoteStructure(createdNote); expect(createdNote.content).toContain('# E2E Client Meeting Agenda'); expect(createdNote.content).toContain('## Attendees'); createdNotes.push(createdNote); console.error('šŸ“‹ Created markdown company note:', createdNote.title); }, 30000); it('should handle company note creation with URI format', async () => { if (testCompanies.length === 0) { console.error( 'ā­ļø Skipping URI format test - no test companies available' ); return; } const testCompany = testCompanies[0] as unknown as AttioRecord; if (!testCompany?.id?.record_id) { console.error('ā­ļø Skipping URI format test - invalid company data'); return; } const noteData = noteFixtures.companies.followUp( testCompany.id.record_id ); const uri = `attio://companies/${testCompany.id.record_id}`; const response = (await notesToolCaller('create-note', { uri: uri, title: noteData.title, content: noteData.content, format: 'markdown', })) as McpToolResponse; NotesAssertions.expectMcpSuccess(response); const createdNote = NotesAssertions.expectMcpData( response ) as unknown as NoteRecord; NotesAssertions.expectValidNoteStructure(createdNote); createdNotes.push(createdNote); console.error('šŸ”— Created company note via URI:', createdNote.title); }, 30000); }); describe('Person Notes Management', () => { it('should create a person note with basic content', async () => { if (testPeople.length === 0) { console.error( 'ā­ļø Skipping person note test - no test people available' ); return; } const testPerson = testPeople[0] as unknown as AttioRecord; if (!testPerson?.id?.record_id) { console.error('ā­ļø Skipping person note test - invalid person data'); return; } const noteData = noteFixtures.people.introduction( testPerson.id.record_id ); const response = (await notesToolCaller('create-note', { resource_type: 'people', record_id: testPerson.id.record_id, title: noteData.title, content: noteData.content, format: 'markdown', })) as McpToolResponse; NotesAssertions.expectMcpSuccess(response); const createdNote = NotesAssertions.expectMcpData( response ) as unknown as NoteRecord; NotesAssertions.expectValidNoteStructure(createdNote); expect(createdNote.title).toBe(noteData.title); expect(createdNote.content).toBe(noteData.content); createdNotes.push(createdNote); console.error('šŸ‘¤ Created person note:', createdNote.title); }, 30000); it('should retrieve person notes', async () => { if (testPeople.length === 0) { console.error( 'ā­ļø Skipping get person notes test - no test people available' ); return; } const testPerson = testPeople[0] as unknown as AttioRecord; if (!testPerson?.id?.record_id) { console.error( 'ā­ļø Skipping get person notes test - invalid person data' ); return; } const response = (await notesToolCaller('list-notes', { resource_type: 'people', parent_object: 'people', record_id: testPerson.id.record_id, })) as McpToolResponse; NotesAssertions.expectMcpSuccess(response); const notes = NotesAssertions.expectMcpData(response); // Notes might be an array or a response object let noteArray: any[] = []; if (Array.isArray(notes)) { noteArray = notes; } else if (notes && Array.isArray(notes.data)) { noteArray = notes.data; } expect(noteArray).toBeDefined(); if (noteArray.length > 0) { NotesAssertions.expectValidNoteCollection(noteArray); console.error('šŸ‘„ Retrieved person notes:', noteArray.length); } else { console.error( 'šŸ‘„ No person notes found (expected for new test data)' ); } }, 30000); it('should create person note with technical content', async () => { if (testPeople.length === 0) { console.error( 'ā­ļø Skipping technical note test - no test people available' ); return; } const testPerson = testPeople[0] as unknown as AttioRecord; if (!testPerson?.id?.record_id) { console.error( 'ā­ļø Skipping technical note test - invalid person data' ); return; } const noteData = noteFixtures.people.technical(testPerson.id.record_id); const response = (await notesToolCaller('create-note', { resource_type: 'people', record_id: testPerson.id.record_id, title: noteData.title, content: noteData.content, format: 'markdown', })) as McpToolResponse; NotesAssertions.expectMcpSuccess(response); const createdNote = NotesAssertions.expectMcpData( response ) as unknown as NoteRecord; NotesAssertions.expectValidNoteStructure(createdNote); NotesAssertions.expectTestNote(createdNote); createdNotes.push(createdNote); console.error('šŸ”§ Created technical person note:', createdNote.title); }, 30000); it('should create person note with markdown formatting', async () => { if (testPeople.length === 0) { console.error( 'ā­ļø Skipping markdown person note test - no test people available' ); return; } const testPerson = testPeople[0] as unknown as AttioRecord; if (!testPerson?.id?.record_id) { console.error( 'ā­ļø Skipping markdown person note test - invalid person data' ); return; } const noteData = noteFixtures.markdown.technicalSpecs( testPerson.id.record_id, 'people' ); const response = (await notesToolCaller('create-note', { resource_type: 'people', record_id: testPerson.id.record_id, title: noteData.title, content: noteData.content, format: 'markdown', })) as McpToolResponse; NotesAssertions.expectMcpSuccess(response); const createdNote = NotesAssertions.expectMcpData( response ) as unknown as NoteRecord; NotesAssertions.expectValidNoteStructure(createdNote); expect(createdNote.content).toContain( '# E2E Technical Integration Specifications' ); createdNotes.push(createdNote); console.error('šŸ“‹ Created markdown person note:', createdNote.title); }, 30000); }); }); describe('Cross-Resource Integration Workflows', () => { it('should create task and note for the same company record', async () => { if (taskTestCompanies.length === 0 || testCompanies.length === 0) { console.error( 'ā­ļø Skipping cross-resource test - insufficient test data' ); return; } // Use the first company for both operations const company = taskTestCompanies[0]; const companyId = company.id.record_id; // Create a task for the company const taskData = TaskFactory.create(); const taskResponse = asToolResponse( await callTasksTool('create-record', { resource_type: 'tasks', record_data: { content: `Follow up on integration for ${company.values.name?.[0]?.value || 'company'}`, format: 'plaintext', recordId: companyId, targetObject: 'companies', deadline_at: taskData.due_date, }, }) ); E2EAssertions.expectMcpSuccess(taskResponse); const createdTask = extractTaskData(taskResponse); createdTasks.push(createdTask); // Create a note for the same company const noteData = noteFixtures.companies.meeting(companyId); const noteResponse = (await notesToolCaller('create-note', { resource_type: 'companies', record_id: companyId, title: noteData.title, content: noteData.content, format: 'markdown', })) as McpToolResponse; NotesAssertions.expectMcpSuccess(noteResponse); const createdNote = NotesAssertions.expectMcpData( noteResponse ) as unknown as NoteRecord; createdNotes.push(createdNote); console.error('šŸ”„ Created task and note for same company:', companyId); }, 45000); it('should demonstrate task-note workflow integration', async () => { if (createdTasks.length === 0 || createdNotes.length === 0) { console.error( 'ā­ļø Skipping workflow integration test - insufficient created data' ); return; } // Update task status and add a corresponding note const task = createdTasks[0]; const taskId = task.id.task_id; // Update task to completed const taskUpdateResponse = asToolResponse( await callTasksTool('update-record', { resource_type: 'tasks', record_id: taskId, record_data: { status: 'completed', }, }) ); E2EAssertions.expectMcpSuccess(taskUpdateResponse); console.error('āœ… Demonstrated integrated task-note workflow completion'); }, 30000); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server