Skip to main content
Glama
test-debugging.ts•31.8 kB
/** * Test Data Debugging and Validation Utilities * * Comprehensive utilities for E2E test debugging, data validation, * and error analysis to achieve 100% test success rate. * * Created for Issue #480 Phase 3: E2E Test Coverage & Validation */ import type { AttioTask, AttioRecord } from '../../src/types/attio.js'; import { TestEnvironment } from './mock-factories/test-environment.js'; /** * Validation result interface */ export interface ValidationResult { valid: boolean; errors: string[]; warnings: string[]; fieldCoverage: number; missingRequiredFields: string[]; } /** * Test data comparison result */ export interface ComparisonResult { match: boolean; differences: { field: string; expected: any; actual: any; reason: string; }[]; score: number; // 0-1 compatibility score } /** * Mock factory validation report */ export interface FactoryValidationReport { taskFactory: ValidationResult; companyFactory: ValidationResult; personFactory: ValidationResult; listFactory: ValidationResult; overallScore: number; criticalIssues: string[]; } /** * Test environment diagnostic report */ export interface DiagnosticReport { environmentDetection: { isTest: boolean; testType: string; useMocks: boolean; isCI: boolean; }; mockFactoryStatus: FactoryValidationReport; apiMockStatus: { configured: boolean; responding: boolean; errorRate: number; }; testDataCleanup: { tracking: boolean; leakageDetected: boolean; orphanedRecords: number; }; recommendations: string[]; } /** * Test Data Inspector - Validates data structures and identifies issues */ export class TestDataInspector { /** * Validates task structure against E2E test expectations */ static validateTaskStructure(task: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; const requiredFields = ['id', 'content', 'status', 'created_at']; const missingRequiredFields: string[] = []; // Check basic structure if (!task || typeof task !== 'object') { errors.push('Task must be a valid object'); return { valid: false, errors, warnings, fieldCoverage: 0, missingRequiredFields, }; } // Issue #480 specific validations if (!task.content && !task.title) { errors.push( 'Task must have either content or title field (Issue #480 compatibility)' ); } if (task.id && !task.id.task_id) { errors.push( 'Task ID must include task_id field (Issue #480 compatibility)' ); } if (task.id && !task.id.record_id) { errors.push('Task ID must include record_id field'); } // Check required fields requiredFields.forEach((field) => { if (!task[field]) { missingRequiredFields.push(field); } }); // Check status validity if ( task.status && !['pending', 'completed', 'in_progress', 'cancelled'].includes( task.status ) ) { warnings.push(`Unusual task status: ${task.status}`); } // Check date format if (task.created_at && isNaN(new Date(task.created_at).getTime())) { errors.push('created_at must be valid ISO date string'); } if ( task.due_date && task.due_date !== null && isNaN(new Date(task.due_date).getTime()) ) { errors.push('due_date must be valid ISO date string or null'); } // Check assignee structure if (task.assignee && (!task.assignee.id || !task.assignee.type)) { errors.push('Assignee must have id and type fields'); } // Check linked records if (task.linked_records && Array.isArray(task.linked_records)) { task.linked_records.forEach((record: any, index: number) => { if (!record.id || !record.object_slug) { errors.push( `Linked record ${index} missing required id or object_slug` ); } }); } const fieldCoverage = this.calculateFieldCoverage(task, [ 'id', 'content', 'title', 'status', 'created_at', 'updated_at', 'due_date', 'assignee', 'linked_records', ]); return { valid: errors.length === 0, errors, warnings, fieldCoverage, missingRequiredFields, }; } /** * Validates company structure */ static validateCompanyStructure(company: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; const requiredFields = ['id', 'values']; const missingRequiredFields: string[] = []; if (!company || typeof company !== 'object') { errors.push('Company must be a valid object'); return { valid: false, errors, warnings, fieldCoverage: 0, missingRequiredFields, }; } // Check ID structure if (!company.id) { missingRequiredFields.push('id'); } else if (!company.id.record_id || !company.id.workspace_id) { errors.push('Company ID must include record_id and workspace_id'); } // Check values structure if (!company.values) { missingRequiredFields.push('values'); } else if (typeof company.values !== 'object') { errors.push('Company values must be an object'); } const fieldCoverage = this.calculateFieldCoverage(company, [ 'id', 'values', 'created_at', 'web_url', ]); return { valid: errors.length === 0, errors, warnings, fieldCoverage, missingRequiredFields, }; } /** * Validates person structure */ static validatePersonStructure(person: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; const requiredFields = ['id', 'values']; const missingRequiredFields: string[] = []; if (!person || typeof person !== 'object') { errors.push('Person must be a valid object'); return { valid: false, errors, warnings, fieldCoverage: 0, missingRequiredFields, }; } // Check ID structure - similar to company but for person if (!person.id) { missingRequiredFields.push('id'); } else if (!person.id.record_id || !person.id.workspace_id) { errors.push('Person ID must include record_id and workspace_id'); } // Check values structure if (!person.values) { missingRequiredFields.push('values'); } const fieldCoverage = this.calculateFieldCoverage(person, [ 'id', 'values', 'created_at', 'web_url', ]); return { valid: errors.length === 0, errors, warnings, fieldCoverage, missingRequiredFields, }; } /** * Compares actual data with expected test structure */ static compareWithExpectedStructure( actual: any, expected: any ): ComparisonResult { const differences: ComparisonResult['differences'] = []; let matches = 0; let total = 0; const compareFields = (actualObj: any, expectedObj: any, path = '') => { const expectedKeys = Object.keys(expectedObj); for (const key of expectedKeys) { total++; const currentPath = path ? `${path}.${key}` : key; if (!(key in actualObj)) { differences.push({ field: currentPath, expected: expectedObj[key], actual: undefined, reason: 'Field missing from actual object', }); } else if ( typeof expectedObj[key] === 'object' && expectedObj[key] !== null ) { if (typeof actualObj[key] !== 'object' || actualObj[key] === null) { differences.push({ field: currentPath, expected: expectedObj[key], actual: actualObj[key], reason: 'Type mismatch - expected object', }); } else { // Recursive comparison compareFields(actualObj[key], expectedObj[key], currentPath); } } else if (actualObj[key] !== expectedObj[key]) { differences.push({ field: currentPath, expected: expectedObj[key], actual: actualObj[key], reason: 'Value mismatch', }); } else { matches++; } } }; compareFields(actual, expected); const score = total > 0 ? matches / total : 1; return { match: differences.length === 0, differences, score, }; } /** * Validates error message format against expected patterns */ static validateErrorMessage( errorMessage: string, expectedPattern: RegExp ): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!errorMessage) { errors.push('Error message is empty or undefined'); return { valid: false, errors, warnings, fieldCoverage: 0, missingRequiredFields: [], }; } if (!expectedPattern.test(errorMessage)) { errors.push( `Error message "${errorMessage}" does not match expected pattern ${expectedPattern}` ); } return { valid: errors.length === 0, errors, warnings, fieldCoverage: 1, missingRequiredFields: [], }; } /** * Calculates field coverage percentage */ private static calculateFieldCoverage( obj: any, expectedFields: string[] ): number { if (!obj || expectedFields.length === 0) return 0; const presentFields = expectedFields.filter((field) => { // Handle nested field paths like 'id.record_id' if (field.includes('.')) { const parts = field.split('.'); let current = obj; for (const part of parts) { if ( current && current[part] !== undefined && current[part] !== null ) { current = current[part]; } else { return false; } } return true; } return obj[field] !== undefined && obj[field] !== null; }); return presentFields.length / expectedFields.length; } } /** * Mock Data Validator - Validates all mock factories */ export class MockDataValidator { /** * Validates all mock factories and returns comprehensive report * Enhanced with CI environment robustness and timeout protection */ static async validateAllFactories(): Promise<FactoryValidationReport> { // Default factory result for fallback scenarios const defaultFactoryResult: ValidationResult = { valid: false, errors: ['Factory validation failed - using fallback result'], warnings: ['CI environment might have module loading issues'], fieldCoverage: 0, missingRequiredFields: [], }; // Timeout wrapper for async operations (CI protection) const withTimeout = <T>( promise: Promise<T>, timeoutMs: number = 10000 ): Promise<T> => { return Promise.race([ promise, new Promise<T>((_, reject) => setTimeout( () => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs ) ), ]); }; // Enhanced validation with error recovery const safeValidate = async ( factoryName: string, validator: () => Promise<ValidationResult> ): Promise<ValidationResult> => { try { const result = await withTimeout(validator(), 10000); // Additional safety check - ensure result has required structure if (!result || typeof result !== 'object') { console.warn( `[CI Warning] ${factoryName} returned invalid result, using fallback` ); return { ...defaultFactoryResult, errors: [`${factoryName} returned invalid result type`], }; } // Ensure all required fields exist if (!Array.isArray(result.errors)) result.errors = []; if (!Array.isArray(result.warnings)) result.warnings = []; if (!Array.isArray(result.missingRequiredFields)) result.missingRequiredFields = []; if (typeof result.fieldCoverage !== 'number') result.fieldCoverage = 0; if (typeof result.valid !== 'boolean') result.valid = false; return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.warn( `[CI Warning] ${factoryName} validation failed:`, errorMessage ); return { ...defaultFactoryResult, errors: [`${factoryName} validation failed: ${errorMessage}`], }; } }; // Run validations with enhanced error handling const taskFactory = await safeValidate('TaskFactory', () => this.validateTaskFactory() ); const companyFactory = await safeValidate('CompanyFactory', () => this.validateCompanyFactory() ); const personFactory = await safeValidate('PersonFactory', () => this.validatePersonFactory() ); const listFactory = await safeValidate('ListFactory', () => this.validateListFactory() ); // Calculate overall score with safety checks const validCoverages = [ taskFactory, companyFactory, personFactory, listFactory, ] .map((f) => (typeof f.fieldCoverage === 'number' ? f.fieldCoverage : 0)) .filter((coverage) => !isNaN(coverage)); const overallScore = validCoverages.length > 0 ? validCoverages.reduce((sum, coverage) => sum + coverage, 0) / validCoverages.length : 0; const criticalIssues: string[] = []; // Collect critical issues with enhanced safety [taskFactory, companyFactory, personFactory, listFactory].forEach( (result, index) => { const factoryNames = [ 'TaskFactory', 'CompanyFactory', 'PersonFactory', 'ListFactory', ]; const factoryName = factoryNames[index]; // Multiple layers of safety checks for CI environments if (!result) { criticalIssues.push( `${factoryName}: Result object is null/undefined` ); return; } if (!result.errors) { criticalIssues.push(`${factoryName}: Missing errors array`); return; } if (!Array.isArray(result.errors)) { criticalIssues.push(`${factoryName}: Errors is not an array`); return; } try { result.errors.forEach((error) => { if ( typeof error === 'string' && (error.includes('Issue #480') || error.includes('required')) ) { criticalIssues.push(`${factoryName}: ${error}`); } }); } catch (iterationError) { criticalIssues.push( `${factoryName}: Error during iteration: ${iterationError}` ); } } ); return { taskFactory, companyFactory, personFactory, listFactory, overallScore, criticalIssues, }; } /** * Validates task mock factory */ static async validateTaskFactory(): Promise<ValidationResult> { try { // Import factory dynamically to avoid circular dependencies const TaskMockFactoryModule = await import( './mock-factories/TaskMockFactory.js' ); const TaskMockFactory = TaskMockFactoryModule.TaskMockFactory || TaskMockFactoryModule.default; if (!TaskMockFactory) { return { valid: false, errors: ['TaskMockFactory not found in module'], warnings: [], fieldCoverage: 0, missingRequiredFields: [], }; } // Test basic creation const basicTask = TaskMockFactory.create(); const basicValidation = TestDataInspector.validateTaskStructure(basicTask); // Test with overrides const customTask = TaskMockFactory.create({ content: 'Test content', status: 'pending', assignee_id: 'test-assignee', }); const customValidation = TestDataInspector.validateTaskStructure(customTask); // Test multiple creation const multipleTasks = TaskMockFactory.createMultiple(3); const multipleValid = multipleTasks.every( (task) => TestDataInspector.validateTaskStructure(task).valid ); const combinedErrors = [ ...basicValidation.errors, ...customValidation.errors, ...(multipleValid ? [] : ['Multiple task creation validation failed']), ]; return { valid: combinedErrors.length === 0, errors: combinedErrors, warnings: [...basicValidation.warnings, ...customValidation.warnings], fieldCoverage: Math.max( basicValidation.fieldCoverage, customValidation.fieldCoverage ), missingRequiredFields: basicValidation.missingRequiredFields, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error && error.stack ? ` Stack: ${error.stack.split('\n')[0]}` : ''; // Enhanced error reporting for CI environments console.warn(`[CI Debug] TaskMockFactory validation failed:`, { error: errorMessage, type: typeof error, isError: error instanceof Error, nodeVersion: process.version, platform: process.platform, }); return { valid: false, errors: [ `TaskMockFactory validation failed: ${errorMessage}${errorStack}`, ], warnings: [ 'Error occurred during factory validation - check CI environment', ], fieldCoverage: 0, missingRequiredFields: [], }; } } /** * Validates company mock factory */ static async validateCompanyFactory(): Promise<ValidationResult> { try { const CompanyMockFactoryModule = await import( './mock-factories/CompanyMockFactory.js' ); const CompanyMockFactory = CompanyMockFactoryModule.CompanyMockFactory || CompanyMockFactoryModule.default; if (!CompanyMockFactory) { return { valid: false, errors: ['CompanyMockFactory not found in module'], warnings: [], fieldCoverage: 0, missingRequiredFields: [], }; } const company = CompanyMockFactory.create(); return TestDataInspector.validateCompanyStructure(company); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error && error.stack ? ` Stack: ${error.stack.split('\n')[0]}` : ''; // Enhanced error reporting for CI environments console.warn(`[CI Debug] CompanyMockFactory validation failed:`, { error: errorMessage, type: typeof error, isError: error instanceof Error, nodeVersion: process.version, platform: process.platform, }); return { valid: false, errors: [ `CompanyMockFactory validation failed: ${errorMessage}${errorStack}`, ], warnings: [ 'Error occurred during factory validation - check CI environment', ], fieldCoverage: 0, missingRequiredFields: [], }; } } /** * Validates person mock factory */ static async validatePersonFactory(): Promise<ValidationResult> { try { const PersonMockFactoryModule = await import( './mock-factories/PersonMockFactory.js' ); const PersonMockFactory = PersonMockFactoryModule.PersonMockFactory || PersonMockFactoryModule.default; if (!PersonMockFactory) { return { valid: false, errors: ['PersonMockFactory not found in module'], warnings: [], fieldCoverage: 0, missingRequiredFields: [], }; } const person = PersonMockFactory.create(); return TestDataInspector.validatePersonStructure(person); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error && error.stack ? ` Stack: ${error.stack.split('\n')[0]}` : ''; // Enhanced error reporting for CI environments console.warn(`[CI Debug] PersonMockFactory validation failed:`, { error: errorMessage, type: typeof error, isError: error instanceof Error, nodeVersion: process.version, platform: process.platform, }); return { valid: false, errors: [ `PersonMockFactory validation failed: ${errorMessage}${errorStack}`, ], warnings: [ 'Error occurred during factory validation - check CI environment', ], fieldCoverage: 0, missingRequiredFields: [], }; } } /** * Validates list mock factory */ static async validateListFactory(): Promise<ValidationResult> { try { const ListMockFactoryModule = await import( './mock-factories/ListMockFactory.js' ); const ListMockFactory = ListMockFactoryModule.ListMockFactory || ListMockFactoryModule.default; if (!ListMockFactory) { return { valid: false, errors: ['ListMockFactory not found in module'], warnings: [], fieldCoverage: 0, missingRequiredFields: [], }; } const list = ListMockFactory.create(); // Basic structure validation for lists const errors: string[] = []; const warnings: string[] = []; const missingRequiredFields: string[] = []; if (!list || typeof list !== 'object') { errors.push('List must be a valid object'); return { valid: false, errors, warnings, fieldCoverage: 0, missingRequiredFields: ['id', 'title', 'name'], }; } // Check list_id structure (AttioList uses list_id) if (!list.id || !list.id.list_id) { errors.push('List must have valid ID structure with list_id'); missingRequiredFields.push('id.list_id'); } if (!list.title && !list.name) { errors.push('List must have title or name'); missingRequiredFields.push('title/name'); } // Calculate field coverage for list const expectedFields = [ 'id', 'title', 'name', 'description', 'object_slug', 'created_at', ]; const fieldCoverage = this.calculateFieldCoverage(list, expectedFields); return { valid: errors.length === 0, errors, warnings, fieldCoverage, missingRequiredFields, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error && error.stack ? ` Stack: ${error.stack.split('\n')[0]}` : ''; // Enhanced error reporting for CI environments console.warn(`[CI Debug] ListMockFactory validation failed:`, { error: errorMessage, type: typeof error, isError: error instanceof Error, nodeVersion: process.version, platform: process.platform, }); return { valid: false, errors: [ `ListMockFactory validation failed: ${errorMessage}${errorStack}`, ], warnings: [ 'Error occurred during factory validation - check CI environment', ], fieldCoverage: 0, missingRequiredFields: [], }; } } /** * Local field coverage calculator to avoid cross-class private access */ private static calculateFieldCoverage( obj: any, expectedFields: string[] ): number { if (!obj || expectedFields.length === 0) return 0; const present = expectedFields.filter((field) => { if (field.includes('.')) { const parts = field.split('.'); let current: any = obj; for (const part of parts) { if ( current && current[part] !== undefined && current[part] !== null ) { current = current[part]; } else { return false; } } return true; } return obj[field] !== undefined && obj[field] !== null; }); return present.length / expectedFields.length; } } /** * Test Environment Diagnostics - Comprehensive system health check */ export class TestEnvironmentDiagnostics { /** * Runs full diagnostic suite */ static async runFullDiagnostic(): Promise<DiagnosticReport> { return { environmentDetection: this.checkEnvironmentDetection(), mockFactoryStatus: await MockDataValidator.validateAllFactories(), apiMockStatus: this.checkApiMockingStatus(), testDataCleanup: this.validateTestDataCleanup(), recommendations: await this.generateRecommendations(), }; } /** * Checks environment detection accuracy */ private static checkEnvironmentDetection() { return { isTest: TestEnvironment.isTest(), testType: TestEnvironment.getContext(), useMocks: TestEnvironment.useMocks(), isCI: TestEnvironment.isCI(), }; } /** * Checks API mocking status */ private static checkApiMockingStatus() { return { configured: TestEnvironment.useMocks(), responding: true, // Could implement ping test errorRate: 0, // Could track from metrics }; } /** * Validates test data cleanup tracking */ private static validateTestDataCleanup() { // This would integrate with cleanup tracking system return { tracking: true, leakageDetected: false, orphanedRecords: 0, }; } /** * Generates actionable recommendations */ private static async generateRecommendations(): Promise<string[]> { const recommendations: string[] = []; const report = await MockDataValidator.validateAllFactories(); if (report.overallScore < 0.9) { recommendations.push( 'Mock factory validation score below 90% - review and fix validation errors' ); } if (report.criticalIssues.length > 0) { recommendations.push( 'Critical issues detected in mock factories - address immediately' ); } if (!TestEnvironment.isTest()) { recommendations.push( 'Environment detection may be incorrect - verify test environment setup' ); } return recommendations; } } /** * E2E Test Failure Analyzer - Analyzes common failure patterns */ export class E2ETestFailureAnalyzer { /** * Error message patterns that commonly fail in E2E tests */ private static readonly ERROR_PATTERNS = { notFound: /not found|invalid|does not exist/i, taskCreation: /task.*creat|creat.*task/i, highPriority: /high priority|priority.*high/i, recordLinking: /link.*record|record.*link/i, concurrentOperations: /concurrent|parallel/i, }; /** * Analyzes error message and suggests fixes */ static analyzeFailure( errorMessage: string, testContext: string ): { category: string; suggestions: string[]; mockDataFix?: string; codeChanges?: string[]; } { const suggestions: string[] = []; let category = 'unknown'; let mockDataFix: string | undefined; const codeChanges: string[] = []; // Analyze error patterns if (this.ERROR_PATTERNS.notFound.test(errorMessage)) { category = 'error-message-format'; suggestions.push('Error message format does not match expected pattern'); suggestions.push( 'Update mock error responses to include "not found", "invalid", or "does not exist"' ); mockDataFix = 'Standardize error message formats across mock factories'; } if (this.ERROR_PATTERNS.taskCreation.test(errorMessage)) { category = 'task-creation'; suggestions.push('Task creation failure detected'); suggestions.push('Check TaskMockFactory for Issue #480 compatibility'); mockDataFix = 'Ensure task mock includes both content and title fields'; } if (this.ERROR_PATTERNS.highPriority.test(errorMessage)) { category = 'high-priority-task'; suggestions.push('High priority task handling needs improvement'); codeChanges.push('Review TaskMockFactory.createHighPriority() method'); } if (errorMessage.includes('Response has error flag')) { category = 'response-error-flag'; suggestions.push('Mock response incorrectly flagged as error'); suggestions.push('Check mock data generation for error conditions'); mockDataFix = 'Ensure successful operations return success flag'; } return { category, suggestions, mockDataFix, codeChanges, }; } /** * Generates comprehensive failure report */ static generateFailureReport( failures: Array<{ testName: string; errorMessage: string; suite: string; }> ): { summary: { totalFailures: number; categorizedFailures: Record<string, number>; criticalIssues: string[]; }; detailedAnalysis: Array<{ test: string; analysis: ReturnType<typeof E2ETestFailureAnalyzer.analyzeFailure>; }>; actionPlan: string[]; } { const categorizedFailures: Record<string, number> = {}; const criticalIssues: string[] = []; const detailedAnalysis = failures.map((failure) => { const analysis = this.analyzeFailure(failure.errorMessage, failure.suite); categorizedFailures[analysis.category] = (categorizedFailures[analysis.category] || 0) + 1; if ( analysis.category === 'task-creation' || analysis.category === 'error-message-format' ) { criticalIssues.push(`${failure.suite}: ${failure.testName}`); } return { test: `${failure.suite} > ${failure.testName}`, analysis, }; }); const actionPlan = this.generateActionPlan( categorizedFailures, criticalIssues ); return { summary: { totalFailures: failures.length, categorizedFailures, criticalIssues, }, detailedAnalysis, actionPlan, }; } /** * Generates prioritized action plan */ private static generateActionPlan( categorizedFailures: Record<string, number>, criticalIssues: string[] ): string[] { const plan: string[] = []; // Priority 1: Critical issues if (criticalIssues.length > 0) { plan.push( 'PRIORITY 1: Address critical task creation and error message format issues' ); criticalIssues.forEach((issue) => plan.push(` - Fix: ${issue}`)); } // Priority 2: Most common failure categories const sortedCategories = Object.entries(categorizedFailures) .sort(([, a], [, b]) => b - a) .slice(0, 3); sortedCategories.forEach(([category, count]) => { if (count > 1) { plan.push(`PRIORITY 2: Fix ${category} issues (${count} failures)`); } }); // Priority 3: Implement comprehensive validation plan.push('PRIORITY 3: Implement comprehensive mock data validation'); plan.push('PRIORITY 4: Create regression prevention tests'); return plan; } }

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