/** * Error Handling and Recovery Test Suite for Brainy v3.0 * * Comprehensive error scenario testing including: * - Invalid input validation * - Network failure recovery * - Resource exhaustion handling * - Concurrent error scenarios * - Graceful degradation * - Error message consistency */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Brainy } from '../../src/brainy' describe('Error Handling and Recovery', () => { let brainy: Brainy beforeEach(async () => { brainy = new Brainy({ requireSubtype: false, storage: { type: 'memory' } }) await brainy.init() }) afterEach(async () => { await brainy.close() }) describe('Input Validation Errors', () => { describe('ADD operation validation', () => { it('should reject null data', async () => { await expect( brainy.add({ data: null as any, type: 'document' }) ).rejects.toThrow() }) it('should reject undefined data', async () => { await expect( brainy.add({ data: undefined as any, type: 'document' }) ).rejects.toThrow() }) it('should reject empty string data', async () => { await expect( brainy.add({ data: '', type: 'document' }) ).rejects.toThrow(/data.*required|empty/i) }) it('should reject invalid type values', async () => { await expect( brainy.add({ data: 'Valid data', type: 'invalid-type' as any }) ).rejects.toThrow(/invalid.*type|noun.*type/i) }) it('should reject non-string data types', async () => { const invalidData = [ 42, true, [], { text: 'object' }, () => 'function' ] for (const data of invalidData) { await expect( brainy.add({ data: data as any, type: 'document' }) ).rejects.toThrow() } }) it('should handle extremely large metadata gracefully', async () => { const hugeMetadata: any = {} for (let i = 0; i < 10000; i++) { hugeMetadata[`field${i}`] = `value${i}`.repeat(100) } // Should either succeed or throw meaningful error try { const id = await brainy.add({ data: 'Test with huge metadata', type: 'document', metadata: hugeMetadata }) expect(id).toBeDefined() } catch (error: any) { expect(error.message).toMatch(/size|memory|large/i) } }) }) describe('GET operation validation', () => { it('should return null for non-existent IDs', async () => { const result = await brainy.get('non-existent-id-12345') expect(result).toBeNull() }) it('should handle invalid ID formats gracefully', async () => { const invalidIds = [ null, undefined, '', ' ', '../etc/passwd', '' ] for (const id of invalidIds) { const result = await brainy.get(id as any) expect(result).toBeNull() } }) }) describe('UPDATE operation validation', () => { it('should reject updates without ID', async () => { await expect( brainy.update({ id: undefined as any, metadata: { updated: true } }) ).rejects.toThrow(/id.*required/i) }) it('should reject updates to non-existent entities', async () => { await expect( brainy.update({ id: 'non-existent-id', metadata: { updated: true } }) ).rejects.toThrow(/not found|doesn't exist/i) }) it('should handle circular references in metadata', async () => { const id = await brainy.add({ data: 'Test entity', type: 'document' }) const metadata: any = { level: 1 } metadata.self = metadata // Circular reference // Should either handle or throw meaningful error try { await brainy.update({ id, metadata }) // If successful, verify it was handled const updated = await brainy.get(id) expect(updated).toBeDefined() } catch (error: any) { expect(error.message).toMatch(/circular|cyclic/i) } }) }) describe('DELETE operation validation', () => { it('should handle deletion of non-existent entities', async () => { // Should not throw, just return void await expect( brainy.remove('non-existent-id') ).resolves.toBeUndefined() }) it('should handle double deletion gracefully', async () => { const id = await brainy.add({ data: 'To be deleted', type: 'document' }) await brainy.remove(id) // Second delete should not throw await expect(brainy.remove(id)).resolves.toBeUndefined() }) }) describe('RELATE operation validation', () => { it('should reject relationships without source', async () => { await expect( brainy.relate({ from: undefined as any, to: 'some-id', type: 'relatedTo' }) ).rejects.toThrow(/from.*required/i) }) it('should reject relationships without target', async () => { await expect( brainy.relate({ from: 'some-id', to: undefined as any, type: 'relatedTo' }) ).rejects.toThrow(/to.*required/i) }) it('should reject invalid relationship types', async () => { const id1 = await brainy.add({ data: 'Entity 1', type: 'thing' }) const id2 = await brainy.add({ data: 'Entity 2', type: 'thing' }) await expect( brainy.relate({ from: id1, to: id2, type: 'invalid-verb' as any }) ).rejects.toThrow(/invalid.*type|verb.*type/i) }) it('should reject self-relationships by default', async () => { const id = await brainy.add({ data: 'Self entity', type: 'thing' }) // Some systems reject self-relationships try { await brainy.relate({ from: id, to: id, type: 'relatedTo' }) } catch (error: any) { expect(error.message).toMatch(/self|same.*entity/i) } }) }) describe('FIND operation validation', () => { it('should handle empty queries gracefully', async () => { const results = await brainy.find({}) expect(Array.isArray(results)).toBe(true) }) it('should handle invalid where clauses', async () => { const results = await brainy.find({ where: { 'metadata.field': { $invalidOp: 'value' } as any } }) // Should return empty array or handle gracefully expect(Array.isArray(results)).toBe(true) }) it('should handle negative limits', async () => { const results = await brainy.find({ limit: -10 }) expect(Array.isArray(results)).toBe(true) expect(results.length).toBeGreaterThanOrEqual(0) }) it('should handle huge limits reasonably', async () => { const results = await brainy.find({ limit: Number.MAX_SAFE_INTEGER }) expect(Array.isArray(results)).toBe(true) // Should cap at reasonable limit expect(results.length).toBeLessThanOrEqual(10000) }) }) }) describe('Batch Operation Error Handling', () => { it('should handle partial failures in addMany', async () => { const items = [ { data: 'Valid 1', type: 'document' as const }, { data: '', type: 'document' as const }, // Invalid { data: 'Valid 2', type: 'document' as const }, { data: null as any, type: 'document' as const }, // Invalid { data: 'Valid 3', type: 'document' as const } ] const result = await brainy.addMany({ items, continueOnError: true }) expect(result.successful.length).toBeGreaterThanOrEqual(3) expect(result.failed.length).toBeGreaterThanOrEqual(0) expect(result.total).toBe(5) }) it('should rollback batch on critical failure', async () => { const items = Array(100).fill(null).map((_, i) => ({ data: `Item ${i}`, type: 'document' as const })) // Inject a failure midway items[50] = { data: null as any, type: 'document' as const } const result = await brainy.addMany({ items, continueOnError: false }) // Should either complete all or rollback if (result.successful.length > 0) { expect(result.successful.length).toBe(100) } else { expect(result.failed.length).toBeGreaterThan(0) } }) }) describe('Concurrent Operation Errors', () => { it('should handle concurrent updates to same entity', async () => { const id = await brainy.add({ data: 'Concurrent test', type: 'document', metadata: { version: 1 } }) // Attempt concurrent updates const updates = Array(10).fill(null).map((_, i) => brainy.update({ id, metadata: { version: i + 2 } }) ) const results = await Promise.allSettled(updates) // At least one should succeed const succeeded = results.filter(r => r.status === 'fulfilled') expect(succeeded.length).toBeGreaterThanOrEqual(1) // Final state should be consistent const final = await brainy.get(id) expect(final?.metadata?.version).toBeGreaterThanOrEqual(2) }) it('should handle concurrent deletes gracefully', async () => { const id = await brainy.add({ data: 'To be deleted concurrently', type: 'document' }) // Attempt concurrent deletes const deletes = Array(5).fill(null).map(() => brainy.remove(id)) // Should not throw await expect(Promise.all(deletes)).resolves.toBeDefined() // Entity should be deleted const result = await brainy.get(id) expect(result).toBeNull() }) }) describe('Resource Exhaustion', () => { it('should handle memory pressure gracefully', async () => { const largeData = 'x'.repeat(1024 * 1024) // 1MB let successCount = 0 let errorCount = 0 // Try to add many large entities for (let i = 0; i < 100; i++) { try { await brainy.add({ data: largeData + ` Entity ${i}`, type: 'document', metadata: { index: i, size: largeData.length } }) successCount++ } catch (error) { errorCount++ // Should get meaningful error expect(error).toBeDefined() } } // Should handle at least some operations expect(successCount).toBeGreaterThan(0) }) it('should handle rapid-fire operations', async () => { const operations = 1000 const promises: Promise[] = [] for (let i = 0; i < operations; i++) { const op = i % 4 switch (op) { case 0: promises.push( brainy.add({ data: `Rapid ${i}`, type: 'document' }).catch(() => null) ) break case 1: promises.push(brainy.get(`rapid-${i}`).catch(() => null)) break case 2: promises.push( brainy.update({ id: `rapid-${i}`, metadata: { updated: true } }).catch(() => null) ) break case 3: promises.push(brainy.remove(`rapid-${i}`).catch(() => null)) break } } const results = await Promise.allSettled(promises) // Most should complete const completed = results.filter(r => r.status === 'fulfilled') expect(completed.length).toBeGreaterThan(operations * 0.5) }) }) describe('Error Message Quality', () => { it('should provide clear error messages for common mistakes', async () => { // Missing required field try { await brainy.add({} as any) } catch (error: any) { expect(error.message).toBeDefined() expect(error.message.length).toBeGreaterThan(10) // Should mention what's missing expect(error.message).toMatch(/data|required/i) } // Invalid type try { await brainy.add({ data: 'Valid data', type: 'not-a-valid-type' as any }) } catch (error: any) { expect(error.message).toMatch(/type|invalid|noun/i) } // Entity not found try { await brainy.update({ id: 'definitely-does-not-exist', metadata: { test: true } }) } catch (error: any) { expect(error.message).toMatch(/not found|doesn't exist|does not exist/i) } }) it('should not leak sensitive information in errors', async () => { try { await brainy.add({ data: 'Test', type: 'document', metadata: { password: 'secret123', apiKey: 'sk-1234567890' } }) await brainy.update({ id: 'non-existent', metadata: { password: 'should-not-appear' } }) } catch (error: any) { // Error message should not contain sensitive data expect(error.message).not.toMatch(/secret123|sk-1234567890|should-not-appear/i) } }) }) describe('Recovery Mechanisms', () => { it('should recover from transient failures', async () => { let attemptCount = 0 const maxAttempts = 3 async function retryableAdd(data: string): Promise { attemptCount++ if (attemptCount < maxAttempts) { throw new Error('Transient failure') } return brainy.add({ data, type: 'document' }) } // Should eventually succeed with retry let result: string | null = null for (let i = 0; i < maxAttempts; i++) { try { result = await retryableAdd('Test with retry') break } catch (error) { if (i === maxAttempts - 1) throw error } } expect(result).toBeDefined() expect(attemptCount).toBe(maxAttempts) }) it('should maintain consistency after errors', async () => { // Add some valid data const validIds: string[] = [] for (let i = 0; i < 5; i++) { const id = await brainy.add({ data: `Valid entity ${i}`, type: 'document' }) validIds.push(id) } // Cause some errors try { await brainy.add({ data: null as any, type: 'document' }) } catch {} try { await brainy.update({ id: 'non-existent', metadata: {} }) } catch {} try { await brainy.relate({ from: 'non-existent-1', to: 'non-existent-2', type: 'relatedTo' }) } catch {} // Valid data should still be intact for (const id of validIds) { const entity = await brainy.get(id) expect(entity).toBeDefined() expect(entity?.type).toBe('document') } // Should still be able to add new data const newId = await brainy.add({ data: 'Added after errors', type: 'document' }) expect(newId).toBeDefined() }) }) describe('Edge Cases', () => { it('should handle operations on just-deleted entities', async () => { const id = await brainy.add({ data: 'About to be deleted', type: 'document' }) await brainy.remove(id) // Operations on deleted entity should handle gracefully const getResult = await brainy.get(id) expect(getResult).toBeNull() await expect( brainy.update({ id, metadata: { attempt: 'update-after-delete' } }) ).rejects.toThrow(/not found/i) // Should not throw for delete await expect(brainy.remove(id)).resolves.toBeUndefined() }) it('should handle rapid create-delete cycles', async () => { const cycles = 50 for (let i = 0; i < cycles; i++) { const id = await brainy.add({ data: `Cycle ${i}`, type: 'document' }) // Immediately delete await brainy.remove(id) // Verify it's gone const result = await brainy.get(id) expect(result).toBeNull() } }) it('should handle maximum field lengths', async () => { const maxFieldLength = 1024 * 1024 // 1MB const longString = 'x'.repeat(maxFieldLength) try { const id = await brainy.add({ data: longString, type: 'document', metadata: { description: longString.substring(0, 1000) } }) // If successful, should be retrievable const entity = await brainy.get(id) expect(entity).toBeDefined() } catch (error: any) { // If it fails, should have meaningful error expect(error.message).toMatch(/size|large|limit/i) } }) }) })