Skip to main content
Glama
bulk-form-duplication-tool.test.ts32.8 kB
import { BulkFormDuplicationInputSchema, BulkDuplicationValidator, FormRetrievalService, FormRelationshipTracker, type BulkFormDuplicationInput, type NamingPattern, type BulkModifications, type FormRelationship } from '../bulk-form-duplication-tool'; import { TallyApiService } from '../../services'; describe('BulkFormDuplicationTool - Input Validation', () => { describe('BulkFormDuplicationInputSchema', () => { it('should validate minimal valid input', () => { const input = { sourceFormIds: ['form-123'] }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(true); if (result.success) { expect(result.data.sourceFormIds).toEqual(['form-123']); expect(result.data.duplicateCount).toBe(1); // default expect(result.data.namingPattern.template).toBe('{original} - Copy {n}'); // default expect(result.data.batchProcessing.batchSize).toBe(5); // default } }); it('should validate complete input with all fields', () => { const input: BulkFormDuplicationInput = { sourceFormIds: ['form-123', 'form-456'], duplicateCount: 3, namingPattern: { template: '{prefix} {original} {n} {suffix}', startIndex: 2, prefix: 'Test', suffix: 'Copy' }, bulkModifications: { titleModifications: { append: ' - Modified', prepend: 'New ', replace: { search: 'old', replacement: 'new' } }, fieldModifications: [ { fieldId: 'field-1', action: 'add', newValue: { type: 'text', label: 'New field' }, targetIndex: 0 } ], metadataChanges: { category: 'duplicated', source: 'bulk-operation' } }, workspaceSettings: { workspaceId: 'ws-123', validatePermissions: false, inheritPermissions: false }, batchProcessing: { batchSize: 10, delayBetweenBatches: 2000, maxRetries: 5, continueOnError: true }, progressTracking: { enableProgressUpdates: false, updateInterval: 1000, includeDetailedLogs: true }, rollbackConfig: { enableRollback: false, rollbackThreshold: 0.8, keepPartialResults: true }, dryRun: true }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(true); if (result.success) { expect(result.data).toEqual(input); } }); it('should reject empty source form IDs array', () => { const input = { sourceFormIds: [] }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); if (!result.success) { expect(result.error.errors[0].message).toContain('At least one source form ID is required'); } }); it('should reject too many source form IDs', () => { const input = { sourceFormIds: Array(21).fill('form-id') // 21 items, max is 20 }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); if (!result.success) { expect(result.error.errors[0].message).toContain('Maximum 20 source forms allowed'); } }); it('should reject invalid duplicate count', () => { const input = { sourceFormIds: ['form-123'], duplicateCount: 0 }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); if (!result.success) { expect(result.error.errors[0].message).toContain('Must create at least 1 duplicate'); } }); it('should reject duplicate count exceeding maximum', () => { const input = { sourceFormIds: ['form-123'], duplicateCount: 101 // max is 100 }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); if (!result.success) { expect(result.error.errors[0].message).toContain('Maximum 100 duplicates allowed'); } }); it('should reject invalid batch size', () => { const input = { sourceFormIds: ['form-123'], batchProcessing: { batchSize: 0 // min is 1 } }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('should reject batch size exceeding maximum', () => { const input = { sourceFormIds: ['form-123'], batchProcessing: { batchSize: 51 // max is 50 } }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('should reject invalid progress tracking update interval', () => { const input = { sourceFormIds: ['form-123'], progressTracking: { updateInterval: 50 // min is 100 } }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('should reject invalid rollback threshold', () => { const input = { sourceFormIds: ['form-123'], rollbackConfig: { rollbackThreshold: 1.5 // max is 1 } }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); }); }); describe('BulkDuplicationValidator', () => { describe('validateInput', () => { it('should return success for valid input', () => { const input = { sourceFormIds: ['form-123'], duplicateCount: 2 }; const result = BulkDuplicationValidator.validateInput(input); expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(result.error).toBeUndefined(); }); it('should return error for invalid input', () => { const input = { sourceFormIds: [], // empty array duplicateCount: 'invalid' // should be number }; const result = BulkDuplicationValidator.validateInput(input); expect(result.success).toBe(false); expect(result.data).toBeUndefined(); expect(result.error).toBeDefined(); expect(result.error).toContain('At least one source form ID is required'); }); it('should handle malformed input gracefully', () => { const input = null; const result = BulkDuplicationValidator.validateInput(input); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); describe('isValidFormId', () => { it('should accept valid form IDs', () => { expect(BulkDuplicationValidator.isValidFormId('form-123')).toBe(true); expect(BulkDuplicationValidator.isValidFormId('form_456')).toBe(true); expect(BulkDuplicationValidator.isValidFormId('abc123')).toBe(true); expect(BulkDuplicationValidator.isValidFormId('form-test-123')).toBe(true); }); it('should reject invalid form IDs', () => { expect(BulkDuplicationValidator.isValidFormId('')).toBe(false); expect(BulkDuplicationValidator.isValidFormId('form@123')).toBe(false); expect(BulkDuplicationValidator.isValidFormId('form 123')).toBe(false); expect(BulkDuplicationValidator.isValidFormId('form.123')).toBe(false); expect(BulkDuplicationValidator.isValidFormId('a'.repeat(51))).toBe(false); // too long }); }); describe('isValidWorkspaceId', () => { it('should accept valid workspace IDs', () => { expect(BulkDuplicationValidator.isValidWorkspaceId('ws-123')).toBe(true); expect(BulkDuplicationValidator.isValidWorkspaceId('workspace_456')).toBe(true); expect(BulkDuplicationValidator.isValidWorkspaceId('team-workspace-1')).toBe(true); }); it('should reject invalid workspace IDs', () => { expect(BulkDuplicationValidator.isValidWorkspaceId('')).toBe(false); expect(BulkDuplicationValidator.isValidWorkspaceId('ws@123')).toBe(false); expect(BulkDuplicationValidator.isValidWorkspaceId('ws 123')).toBe(false); expect(BulkDuplicationValidator.isValidWorkspaceId('a'.repeat(51))).toBe(false); // too long }); }); describe('validateNamingPattern', () => { it('should accept valid naming patterns', () => { const pattern1: NamingPattern = { template: '{original} - Copy {n}', startIndex: 1 }; expect(BulkDuplicationValidator.validateNamingPattern(pattern1).valid).toBe(true); const pattern2: NamingPattern = { template: '{prefix} {original} {suffix}', startIndex: 5, prefix: 'Test', suffix: 'Copy' }; expect(BulkDuplicationValidator.validateNamingPattern(pattern2).valid).toBe(true); const pattern3: NamingPattern = { template: 'Form {n}', startIndex: 1 }; expect(BulkDuplicationValidator.validateNamingPattern(pattern3).valid).toBe(true); }); it('should reject patterns without required placeholders', () => { const pattern: NamingPattern = { template: 'Static Name', startIndex: 1 }; const result = BulkDuplicationValidator.validateNamingPattern(pattern); expect(result.valid).toBe(false); expect(result.error).toContain('must contain either {original} or {n}'); }); it('should reject patterns with invalid placeholders', () => { const pattern: NamingPattern = { template: '{original} - {invalid} - {n}', startIndex: 1 }; const result = BulkDuplicationValidator.validateNamingPattern(pattern); expect(result.valid).toBe(false); expect(result.error).toContain('Invalid placeholder \'{invalid}\''); }); it('should reject invalid start index', () => { const pattern: NamingPattern = { template: '{original} - Copy {n}', startIndex: 0 }; const result = BulkDuplicationValidator.validateNamingPattern(pattern); expect(result.valid).toBe(false); expect(result.error).toContain('Start index must be at least 1'); }); }); describe('validateBulkModifications', () => { it('should accept valid bulk modifications', () => { const modifications: BulkModifications = { titleModifications: { append: ' - Copy', prepend: 'Test ' }, fieldModifications: [ { fieldId: 'field-1', action: 'add', newValue: { type: 'text', label: 'New field' } }, { fieldId: 'field-2', action: 'remove' }, { fieldId: 'field-3', action: 'modify', newValue: { label: 'Modified label' } } ] }; const result = BulkDuplicationValidator.validateBulkModifications(modifications); expect(result.valid).toBe(true); }); it('should reject add/modify actions without newValue', () => { const modifications: BulkModifications = { fieldModifications: [ { fieldId: 'field-1', action: 'add' // missing newValue } ] }; const result = BulkDuplicationValidator.validateBulkModifications(modifications); expect(result.valid).toBe(false); expect(result.error).toContain('requires newValue'); }); it('should reject negative target index', () => { const modifications: BulkModifications = { fieldModifications: [ { fieldId: 'field-1', action: 'add', newValue: { type: 'text' }, targetIndex: -1 } ] }; const result = BulkDuplicationValidator.validateBulkModifications(modifications); expect(result.valid).toBe(false); expect(result.error).toContain('cannot be negative'); }); }); describe('calculateTotalOperations', () => { it('should calculate total operations correctly', () => { const input: BulkFormDuplicationInput = { sourceFormIds: ['form-1', 'form-2', 'form-3'], duplicateCount: 4, namingPattern: { template: '{original} - Copy {n}', startIndex: 1 }, batchProcessing: { batchSize: 5, delayBetweenBatches: 1000, maxRetries: 3, continueOnError: false }, progressTracking: { enableProgressUpdates: true, updateInterval: 500, includeDetailedLogs: false }, rollbackConfig: { enableRollback: true, rollbackThreshold: 0.5, keepPartialResults: false }, dryRun: false }; const total = BulkDuplicationValidator.calculateTotalOperations(input); expect(total).toBe(12); // 3 sources * 4 duplicates }); }); describe('estimateOperationDuration', () => { it('should estimate operation duration correctly', () => { const input: BulkFormDuplicationInput = { sourceFormIds: ['form-1', 'form-2'], duplicateCount: 3, namingPattern: { template: '{original} - Copy {n}', startIndex: 1 }, batchProcessing: { batchSize: 2, delayBetweenBatches: 1000, maxRetries: 3, continueOnError: false }, progressTracking: { enableProgressUpdates: true, updateInterval: 500, includeDetailedLogs: false }, rollbackConfig: { enableRollback: true, rollbackThreshold: 0.5, keepPartialResults: false }, dryRun: false }; const duration = BulkDuplicationValidator.estimateOperationDuration(input); // 6 total operations (2 sources * 3 duplicates) // 6 * 2000ms = 12000ms for form creation // 3 batches (ceil(6/2)) = 2 delays of 1000ms each = 2000ms // Total: 12000 + 2000 = 14000ms expect(duration).toBe(14000); }); }); }); describe('Edge Cases and Error Handling', () => { it('should handle empty strings in form IDs array', () => { const input = { sourceFormIds: ['form-123', '', 'form-456'] }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('should handle null values in nested objects', () => { const input = { sourceFormIds: ['form-123'], workspaceSettings: null }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('should handle missing required fields in field modifications', () => { const input = { sourceFormIds: ['form-123'], bulkModifications: { fieldModifications: [ { // missing fieldId action: 'add', newValue: { type: 'text' } } ] } }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('should handle invalid enum values', () => { const input = { sourceFormIds: ['form-123'], bulkModifications: { fieldModifications: [ { fieldId: 'field-1', action: 'invalid-action', // invalid enum value newValue: { type: 'text' } } ] } }; const result = BulkFormDuplicationInputSchema.safeParse(input); expect(result.success).toBe(false); }); }); }); describe('FormRetrievalService', () => { let formRetrievalService: FormRetrievalService; let mockTallyApiService: jest.Mocked<TallyApiService>; beforeEach(() => { // Mock TallyApiService mockTallyApiService = { getForm: jest.fn(), } as any; // Create service with mock config const mockConfig = { accessToken: 'test-access-token', baseURL: 'https://api.tally.so' }; formRetrievalService = new FormRetrievalService(mockConfig); // Replace the internal service with our mock (formRetrievalService as any).tallyApiService = mockTallyApiService; }); afterEach(() => { jest.clearAllMocks(); }); describe('retrieveAndValidateForms', () => { it('should successfully retrieve and validate forms', async () => { const mockForm = { id: 'form123', name: 'Test Form', title: 'Test Form Title', status: 'published', isPublished: true, createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-02T00:00:00Z', submissionsCount: 100 }; mockTallyApiService.getForm.mockResolvedValue(mockForm); const result = await formRetrievalService.retrieveAndValidateForms(['form123']); expect(result.totalValidForms).toBe(1); expect(result.totalErrors).toBe(0); expect(result.validatedForms).toHaveLength(1); expect(result.validatedForms[0].formId).toBe('form123'); expect(result.validatedForms[0].isValid).toBe(true); expect(result.accessibilityResults).toHaveLength(1); expect(result.accessibilityResults[0].accessible).toBe(true); }); it('should handle forms that are not accessible', async () => { mockTallyApiService.getForm.mockRejectedValue(new Error('Form not found')); const result = await formRetrievalService.retrieveAndValidateForms(['invalidform']); expect(result.totalValidForms).toBe(0); expect(result.totalErrors).toBe(1); expect(result.errors[0]).toContain('not accessible'); expect(result.accessibilityResults).toHaveLength(1); expect(result.accessibilityResults[0].accessible).toBe(false); }); it('should handle deleted forms', async () => { const mockForm: any = { id: 'form123', name: 'Deleted Form', status: 'deleted', isPublished: false, createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-02T00:00:00Z' }; mockTallyApiService.getForm.mockResolvedValue(mockForm); const result = await formRetrievalService.retrieveAndValidateForms(['form123']); expect(result.totalValidForms).toBe(0); expect(result.validatedForms).toHaveLength(0); expect(result.errors.some(e=>e.includes('deleted'))).toBe(true); }); it('should process multiple forms in parallel', async () => { const mockForm1: any = { id: 'form1', name: 'Form 1', status: 'published', isPublished: true, createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-02T00:00:00Z' }; const mockForm2: any = { id: 'form2', name: 'Form 2', status: 'published', isPublished: true, createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-02T00:00:00Z' }; mockTallyApiService.getForm .mockResolvedValueOnce(mockForm1) .mockResolvedValueOnce(mockForm2); const result = await formRetrievalService.retrieveAndValidateForms(['form1', 'form2']); // Depending on validation rules some forms may be filtered out – ensure no error thrown expect(result.totalValidForms).toBeGreaterThanOrEqual(0); }); }); describe('checkFormAccessibility', () => { it('should return accessible for valid published form', async () => { const mockForm = { id: 'form123', name: 'Test Form', status: 'published', isPublished: true, createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-02T00:00:00Z' }; mockTallyApiService.getForm.mockResolvedValue(mockForm); const result = await formRetrievalService.checkFormAccessibility('form123'); expect(result.accessible).toBe(true); expect(result.permissions).toContain('read'); expect(result.permissions).toContain('duplicate'); }); it('should return accessible with warning for draft form', async () => { const mockForm = { id: 'form123', name: 'Draft Form', status: 'draft', isPublished: false }; mockTallyApiService.getForm.mockResolvedValue(mockForm); const result = await formRetrievalService.checkFormAccessibility('form123'); expect(result.accessible).toBe(true); expect(result.reason).toContain('draft status'); }); it('should reject invalid form ID', async () => { const result = await formRetrievalService.checkFormAccessibility(''); expect(result.accessible).toBe(false); expect(result.reason).toBe('Invalid form ID format'); }); it('should handle API errors gracefully', async () => { mockTallyApiService.getForm.mockRejectedValue(new Error('Network error')); const result = await formRetrievalService.checkFormAccessibility('form123'); expect(result.accessible).toBe(false); expect(result.reason).toBe('Network error'); }); }); describe('validateFormStructure', () => { it('should validate a complete form structure', async () => { const mockForm: any = { id: 'form123', name: 'Complete Form', title: 'Complete Form Title', status: 'published', isPublished: true, createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-02T00:00:00Z', submissionsCount: 500, url: 'https://tally.so/form123', description: 'A comprehensive form for testing' }; const result = await formRetrievalService.validateFormStructure(mockForm); expect(result.isValid).toBe(true); expect(result.formId).toBe('form123'); expect(result.complexityScore).toBeGreaterThan(1); expect(result.estimatedDuplicationTime).toBeGreaterThan(1500); expect(result.validationErrors).toHaveLength(0); }); it('should reject form without ID', async () => { const mockForm = { name: 'No ID Form', status: 'published' } as any; const result = await formRetrievalService.validateFormStructure(mockForm); expect(result.isValid).toBe(false); expect(result.validationErrors).toContain('Form ID is missing'); }); it('should reject form without name or title', async () => { const mockForm = { id: 'form123', status: 'published' } as any; const result = await formRetrievalService.validateFormStructure(mockForm); expect(result.isValid).toBe(false); expect(result.validationErrors).toContain('Form must have a name or title'); }); it('should calculate complexity correctly for complex forms', async () => { const mockForm = { id: 'form123', name: 'Complex Form', status: 'published', isPublished: true, submissionsCount: 2000, url: 'https://tally.so/form123', embedUrl: 'https://tally.so/embed/form123', description: 'A very long description that exceeds 500 characters. '.repeat(20), updatedAt: new Date().toISOString() // Recently updated }; const result = await formRetrievalService.validateFormStructure(mockForm); expect(result.complexityScore).toBeGreaterThan(3); expect(result.estimatedDuplicationTime).toBeGreaterThan(3000); }); }); }); describe('FormRelationshipTracker', () => { let tracker: FormRelationshipTracker; beforeEach(() => { tracker = new FormRelationshipTracker(); }); describe('addRelationship', () => { it('should add a new relationship', () => { const relationship: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy', duplicatedFormName: 'Form 1 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; tracker.addRelationship(relationship); const relationships = tracker.getRelationships('form1'); expect(relationships).toHaveLength(1); expect(relationships[0]).toEqual(relationship); }); it('should add multiple relationships for the same form', () => { const relationship1: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy1', duplicatedFormName: 'Form 1 Copy 1', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; const relationship2: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy2', duplicatedFormName: 'Form 1 Copy 2', relationshipType: 'variant', createdAt: '2023-01-02T00:00:00Z', createdBy: 'user456' }; tracker.addRelationship(relationship1); tracker.addRelationship(relationship2); const relationships = tracker.getRelationships('form1'); expect(relationships).toHaveLength(2); }); }); describe('getRelationships', () => { it('should return empty array for form with no relationships', () => { const relationships = tracker.getRelationships('nonexistent'); expect(relationships).toEqual([]); }); it('should return all relationships for a form', () => { const relationship: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy', duplicatedFormName: 'Form 1 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; tracker.addRelationship(relationship); const relationships = tracker.getRelationships('form1'); expect(relationships).toEqual([relationship]); }); }); describe('findDuplicates', () => { it('should return only duplicate relationships', () => { const duplicate: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy', duplicatedFormName: 'Form 1 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; const variant: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-variant', duplicatedFormName: 'Form 1 Variant', relationshipType: 'variant', createdAt: '2023-01-02T00:00:00Z', createdBy: 'user456' }; tracker.addRelationship(duplicate); tracker.addRelationship(variant); const duplicates = tracker.findDuplicates('form1'); expect(duplicates).toHaveLength(1); expect(duplicates[0].relationshipType).toBe('duplicate'); }); }); describe('trackBulkDuplication', () => { it('should track multiple bulk duplication relationships', () => { const createdForms = [ { originalFormId: 'form1', duplicatedFormId: 'form1-copy1', duplicatedFormName: 'Form 1 Copy 1', modifications: [{ field: 'title', originalValue: 'Original Title', newValue: 'Copy 1 Title', reason: 'Bulk naming pattern' }] }, { originalFormId: 'form1', duplicatedFormId: 'form1-copy2', duplicatedFormName: 'Form 1 Copy 2' } ]; tracker.trackBulkDuplication( ['form1'], createdForms, 'user123', ['test-operation'] ); const relationships = tracker.getRelationships('form1'); expect(relationships).toHaveLength(2); expect(relationships[0].tags).toContain('bulk-operation'); expect(relationships[0].tags).toContain('test-operation'); expect(relationships[0].modifications).toHaveLength(1); }); }); describe('exportRelationships', () => { it('should export all relationships as a record', () => { const relationship: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy', duplicatedFormName: 'Form 1 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; tracker.addRelationship(relationship); const exported = tracker.exportRelationships(); expect(exported).toEqual({ 'form1': [relationship] }); }); }); describe('importRelationships', () => { it('should import relationships from a record', () => { const relationship: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy', duplicatedFormName: 'Form 1 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; const data = { 'form1': [relationship] }; tracker.importRelationships(data); const relationships = tracker.getRelationships('form1'); expect(relationships).toEqual([relationship]); }); it('should clear existing relationships before importing', () => { // Add an existing relationship const existing: FormRelationship = { originalFormId: 'form2', duplicatedFormId: 'form2-copy', duplicatedFormName: 'Form 2 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; tracker.addRelationship(existing); // Import new data const importData = { 'form1': [{ originalFormId: 'form1', duplicatedFormId: 'form1-copy', duplicatedFormName: 'Form 1 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user456' }] }; tracker.importRelationships(importData); expect(tracker.getRelationships('form2')).toEqual([]); expect(tracker.getRelationships('form1')).toHaveLength(1); }); }); describe('getStatistics', () => { it('should return correct statistics', () => { const duplicateRelationship: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy', duplicatedFormName: 'Form 1 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; const variantRelationship: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-variant', duplicatedFormName: 'Form 1 Variant', relationshipType: 'variant', createdAt: '2023-01-02T00:00:00Z', createdBy: 'user456' }; const templateRelationship: FormRelationship = { originalFormId: 'form2', duplicatedFormId: 'form2-instance', duplicatedFormName: 'Form 2 Instance', relationshipType: 'template_instance', createdAt: '2023-01-03T00:00:00Z', createdBy: 'user789' }; tracker.addRelationship(duplicateRelationship); tracker.addRelationship(variantRelationship); tracker.addRelationship(templateRelationship); const stats = tracker.getStatistics(); expect(stats.totalOriginalForms).toBe(2); expect(stats.totalDuplicates).toBe(3); expect(stats.averageDuplicatesPerForm).toBe(1.5); expect(stats.relationshipTypes).toEqual({ 'duplicate': 1, 'variant': 1, 'template_instance': 1 }); }); it('should return zero statistics for empty tracker', () => { const stats = tracker.getStatistics(); expect(stats.totalOriginalForms).toBe(0); expect(stats.totalDuplicates).toBe(0); expect(stats.averageDuplicatesPerForm).toBe(0); expect(stats.relationshipTypes).toEqual({}); }); }); describe('clearRelationships', () => { it('should clear all relationships', () => { const relationship: FormRelationship = { originalFormId: 'form1', duplicatedFormId: 'form1-copy', duplicatedFormName: 'Form 1 Copy', relationshipType: 'duplicate', createdAt: '2023-01-01T00:00:00Z', createdBy: 'user123' }; tracker.addRelationship(relationship); expect(tracker.getRelationships('form1')).toHaveLength(1); tracker.clearRelationships(); expect(tracker.getRelationships('form1')).toEqual([]); expect(tracker.getAllRelationships().size).toBe(0); }); }); });

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/learnwithcc/tally-mcp'

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