2025-09-11 16:23:32 -07:00
/ * *
* 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 ( ) = > {
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.
OPT-OUT REMAINS FULLY SUPPORTED
The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:
- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
contract entirely. Recommended only for migration windows or test
fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
optional vocabulary. Composes with the brain-wide flag.
Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.
TEST SWEEP
Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
- `new Brainy({` → `new Brainy({ requireSubtype: false,`
- `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
- `new Brainy()` → `new Brainy({ requireSubtype: false })`
tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.
The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.
CHANGES
src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
Comment refreshed to document the three opt-out paths.
tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
opt-out preserves the test author's original intent.
tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.
NO-OP for consumers who were already passing subtype on every write.
For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
no other regressions from the flip)
2026-06-09 14:58:25 -07:00
brainy = new Brainy ( { requireSubtype : false ,
2025-09-11 16:23:32 -07:00
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' ,
'<script>alert(1)</script>'
]
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 (
2026-06-11 14:51:00 -07:00
brainy . remove ( 'non-existent-id' )
2025-09-11 16:23:32 -07:00
) . resolves . toBeUndefined ( )
} )
it ( 'should handle double deletion gracefully' , async ( ) = > {
const id = await brainy . add ( {
data : 'To be deleted' ,
type : 'document'
} )
2026-06-11 14:51:00 -07:00
await brainy . remove ( id )
2025-09-11 16:23:32 -07:00
// Second delete should not throw
2026-06-11 14:51:00 -07:00
await expect ( brainy . remove ( id ) ) . resolves . toBeUndefined ( )
2025-09-11 16:23:32 -07:00
} )
} )
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
2026-06-11 14:51:00 -07:00
const deletes = Array ( 5 ) . fill ( null ) . map ( ( ) = > brainy . remove ( id ) )
2025-09-11 16:23:32 -07:00
// 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 < any > [ ] = [ ]
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 :
2026-06-11 14:51:00 -07:00
promises . push ( brainy . remove ( ` rapid- ${ i } ` ) . catch ( ( ) = > null ) )
2025-09-11 16:23:32 -07:00
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 < string > {
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'
} )
2026-06-11 14:51:00 -07:00
await brainy . remove ( id )
2025-09-11 16:23:32 -07:00
// 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
2026-06-11 14:51:00 -07:00
await expect ( brainy . remove ( id ) ) . resolves . toBeUndefined ( )
2025-09-11 16:23:32 -07:00
} )
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
2026-06-11 14:51:00 -07:00
await brainy . remove ( id )
2025-09-11 16:23:32 -07:00
// 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 )
}
} )
} )
} )