2025-09-11 16:23:32 -07:00
/ * *
* Batch Operations Test Suite for Brainy v3 . 0
*
* Comprehensive testing of batch operations including :
* - addMany : Bulk entity creation
* - deleteMany : Bulk deletion
* - Performance validation at scale
* - Memory efficiency testing
* - Error recovery scenarios
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest'
import { Brainy } from '../../src/brainy'
import { performance } from 'perf_hooks'
interface BatchMetrics {
operation : string
totalItems : number
successCount : number
failureCount : number
duration : number
throughput : number
memoryUsed : number
errors : string [ ]
}
class BatchMetricsCollector {
private metrics : BatchMetrics [ ] = [ ]
recordBatch (
operation : string ,
totalItems : number ,
results : any [ ] ,
duration : number ,
memoryUsed : number
) : BatchMetrics {
const successCount = results . filter ( r = >
r ? . status === 'fulfilled' || r === true || ( r && ! r . error )
) . length
const failureCount = totalItems - successCount
const errors = results
. filter ( r = > r ? . status === 'rejected' || r ? . error )
. map ( r = > r ? . reason ? . message || r ? . error || 'Unknown error' )
. slice ( 0 , 5 ) // Limit to first 5 errors
const metric : BatchMetrics = {
operation ,
totalItems ,
successCount ,
failureCount ,
duration ,
throughput : ( totalItems / duration ) * 1000 ,
memoryUsed ,
errors
}
this . metrics . push ( metric )
return metric
}
getMetrics ( ) : BatchMetrics [ ] {
return this . metrics
}
generateReport ( ) : void {
console . log ( '\n=== Batch Operations Performance Report ===\n' )
console . table ( this . metrics . map ( m = > ( {
Operation : m.operation ,
'Total Items' : m . totalItems ,
'Success' : ` ${ m . successCount } / ${ m . totalItems } ( ${ ( ( m . successCount / m . totalItems ) * 100 ) . toFixed ( 1 ) } %) ` ,
'Duration (ms)' : m . duration . toFixed ( 2 ) ,
'Throughput (items/s)' : m . throughput . toFixed ( 0 ) ,
'Memory (MB)' : ( m . memoryUsed / 1024 / 1024 ) . toFixed ( 2 ) ,
'Errors' : m . errors . length > 0 ? m . errors [ 0 ] : 'None'
} ) ) )
// Summary statistics
const totalOps = this . metrics . reduce ( ( sum , m ) = > sum + m . totalItems , 0 )
const totalSuccess = this . metrics . reduce ( ( sum , m ) = > sum + m . successCount , 0 )
const avgThroughput = this . metrics . reduce ( ( sum , m ) = > sum + m . throughput , 0 ) / this . metrics . length
console . log ( '\nSummary:' )
console . log ( ` Total Operations: ${ totalOps } ` )
console . log ( ` Success Rate: ${ ( ( totalSuccess / totalOps ) * 100 ) . toFixed ( 2 ) } % ` )
console . log ( ` Average Throughput: ${ avgThroughput . toFixed ( 0 ) } items/sec ` )
}
}
describe ( 'Batch Operations - Public API Testing' , ( ) = > {
let brainy : Brainy
let metricsCollector : BatchMetricsCollector
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 ( )
metricsCollector = new BatchMetricsCollector ( )
} )
afterEach ( async ( ) = > {
await brainy . close ( )
if ( global . gc ) global . gc ( )
} )
describe ( 'addMany - Bulk Entity Creation' , ( ) = > {
it ( 'should add 100 entities efficiently' , async ( ) = > {
const entities = Array ( 100 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Batch entity ${ i } : Test content for bulk operations ` ,
type : 'document' as const ,
metadata : {
batchIndex : i ,
batchId : 'batch-100' ,
timestamp : Date.now ( )
}
} ) )
const startMemory = process . memoryUsage ( ) . heapUsed
const startTime = performance . now ( )
const result = await brainy . addMany ( { items : entities } )
const duration = performance . now ( ) - startTime
const memoryUsed = process . memoryUsage ( ) . heapUsed - startMemory
const metric = metricsCollector . recordBatch (
'addMany-100' ,
entities . length ,
result . successful ,
duration ,
memoryUsed
)
expect ( result . successful ) . toHaveLength ( 100 )
expect ( result . failed ) . toHaveLength ( 0 )
expect ( metric . throughput ) . toBeGreaterThan ( 100 ) // > 100 items/sec
} )
it ( 'should handle 1,000 entities with proper batching' , async ( ) = > {
const entities = Array ( 1000 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Entity ${ i } : ${ ' ' . repeat ( 100 ) } ` , // ~100 bytes each
type : 'document' as const ,
metadata : {
index : i ,
category : ` cat- ${ i % 10 } ` ,
tags : [ ` tag- ${ i % 5 } ` , ` tag- ${ i % 7 } ` ]
}
} ) )
const startMemory = process . memoryUsage ( ) . heapUsed
const startTime = performance . now ( )
const result = await brainy . addMany ( {
items : entities ,
chunkSize : 100 , // Process in batches
parallel : true
} )
const duration = performance . now ( ) - startTime
const memoryUsed = process . memoryUsage ( ) . heapUsed - startMemory
metricsCollector . recordBatch (
'addMany-1k' ,
entities . length ,
result . successful ,
duration ,
memoryUsed
)
expect ( result . successful . length ) . toBe ( 1000 )
expect ( result . failed . length ) . toBe ( 0 )
expect ( duration ) . toBeLessThan ( 10000 ) // < 10 seconds
// Verify data integrity
const sampleIds = result . successful . slice ( 0 , 10 )
for ( const id of sampleIds ) {
const entity = await brainy . get ( id )
expect ( entity ) . toBeDefined ( )
}
} )
it ( 'should handle 10,000 entities with memory efficiency' , async ( ) = > {
const chunkSize = 1000
const totalEntities = 10000
let allSuccessful : string [ ] = [ ]
let allFailed : any [ ] = [ ]
const startMemory = process . memoryUsage ( ) . heapUsed
const startTime = performance . now ( )
// Process in chunks to avoid memory issues
for ( let chunk = 0 ; chunk < totalEntities ; chunk += chunkSize ) {
const entities = Array ( Math . min ( chunkSize , totalEntities - chunk ) )
. fill ( null )
. map ( ( _ , i ) = > ( {
data : ` Chunk entity ${ chunk + i } ` ,
type : 'document' as const ,
metadata : { globalIndex : chunk + i }
} ) )
const result = await brainy . addMany ( { items : entities } )
allSuccessful . push ( . . . result . successful )
allFailed . push ( . . . result . failed )
// Check memory usage
const currentMemory = process . memoryUsage ( ) . heapUsed
const memoryGrowth = currentMemory - startMemory
expect ( memoryGrowth ) . toBeLessThan ( 500 * 1024 * 1024 ) // < 500MB total
}
const duration = performance . now ( ) - startTime
const finalMemory = process . memoryUsage ( ) . heapUsed - startMemory
metricsCollector . recordBatch (
'addMany-10k' ,
totalEntities ,
allSuccessful ,
duration ,
finalMemory
)
expect ( allSuccessful . length ) . toBe ( 10000 )
expect ( allFailed . length ) . toBe ( 0 )
} )
it ( 'should handle partial failures gracefully' , async ( ) = > {
const entities = [
{ data : 'Valid entity 1' , type : 'document' as const } ,
{ data : '' , type : 'document' as const } , // Invalid: empty data
{ data : 'Valid entity 2' , type : 'document' as const } ,
{ data : null as any , type : 'document' as const } , // Invalid: null data
{ data : 'Valid entity 3' , type : 'document' as const } ,
]
const result = await brainy . addMany ( {
items : entities ,
continueOnError : true
} )
// Should process valid entities even if some fail
expect ( result . successful . length ) . toBeGreaterThanOrEqual ( 3 )
expect ( result . failed . length ) . toBeGreaterThanOrEqual ( 0 ) // May or may not validate
// Verify valid entities were added
for ( const id of result . successful ) {
const entity = await brainy . get ( id )
expect ( entity ) . toBeDefined ( )
}
} )
it ( 'should support concurrent batch operations' , async ( ) = > {
const batches = Array ( 5 ) . fill ( null ) . map ( ( _ , batchIdx ) = >
Array ( 100 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Batch ${ batchIdx } Entity ${ i } ` ,
type : 'document' as const ,
metadata : { batchId : batchIdx , index : i }
} ) )
)
const startTime = performance . now ( )
// Execute batches concurrently
const results = await Promise . all (
batches . map ( entities = > brainy . addMany ( { items : entities } ) )
)
const duration = performance . now ( ) - startTime
expect ( results ) . toHaveLength ( 5 )
const totalSuccess = results . reduce ( ( sum , r ) = > sum + r . successful . length , 0 )
expect ( totalSuccess ) . toBe ( 500 )
expect ( duration ) . toBeLessThan ( 5000 ) // Concurrent should be faster
} )
} )
let testIds : string [ ] = [ ]
beforeEach ( async ( ) = > {
// Create test entities
const entities = Array ( 100 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Original content ${ i } ` ,
type : 'document' as const ,
metadata : { version : 1 , index : i }
} ) )
const result = await brainy . addMany ( { items : entities } )
testIds = result . successful
} )
it ( 'should update multiple entities by IDs' , async ( ) = > {
const updates = testIds . slice ( 0 , 50 ) . map ( id = > ( {
id ,
updates : {
metadata : {
version : 2 ,
updatedAt : Date.now ( )
}
}
} ) )
const startTime = performance . now ( )
const results = await Promise . all (
updates . map ( u = > brainy . update ( { id : u.id , . . . u . updates } ) )
)
const duration = performance . now ( ) - startTime
metricsCollector . recordBatch (
updates . length ,
results ,
duration ,
0
)
// Verify updates
const sample = await brainy . get ( testIds [ 0 ] )
expect ( sample ? . metadata ? . version ) . toBe ( 2 )
} )
it ( 'should update entities matching criteria' , async ( ) = > {
// Update all entities with index < 20
const targetIds = testIds . slice ( 0 , 20 )
const startTime = performance . now ( )
const results = await Promise . all (
targetIds . map ( id = >
brainy . update ( {
id ,
metadata : {
status : 'updated' ,
processedAt : Date.now ( )
}
} )
)
)
const duration = performance . now ( ) - startTime
metricsCollector . recordBatch (
targetIds . length ,
results ,
duration ,
0
)
// Verify updates
for ( const id of targetIds . slice ( 0 , 5 ) ) {
const entity = await brainy . get ( id )
expect ( entity ? . metadata ? . status ) . toBe ( 'updated' )
}
} )
} )
describe ( 'deleteMany - Bulk Deletion' , ( ) = > {
let testIds : string [ ] = [ ]
beforeEach ( async ( ) = > {
// Create test entities
const entities = Array ( 200 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Delete test ${ i } ` ,
type : 'document' as const ,
metadata : { deleteGroup : i % 4 }
} ) )
const result = await brainy . addMany ( { items : entities } )
testIds = result . successful
} )
it ( 'should delete multiple entities by IDs' , async ( ) = > {
const toDelete = testIds . slice ( 0 , 100 )
const startMemory = process . memoryUsage ( ) . heapUsed
const startTime = performance . now ( )
// Use deleteMany if available, otherwise individual deletes
let results : any [ ]
if ( 'deleteMany' in brainy && typeof brainy . deleteMany === 'function' ) {
const result = await brainy . deleteMany ( { ids : toDelete } )
results = result . successful
} else {
results = await Promise . all (
toDelete . map ( id = > brainy . delete ( id ) )
)
}
const duration = performance . now ( ) - startTime
const memoryUsed = process . memoryUsage ( ) . heapUsed - startMemory
metricsCollector . recordBatch (
'deleteMany-100' ,
toDelete . length ,
results ,
duration ,
memoryUsed
)
// Verify deletion
for ( const id of toDelete . slice ( 0 , 10 ) ) {
const entity = await brainy . get ( id )
expect ( entity ) . toBeNull ( )
}
// Remaining entities should still exist
const remaining = testIds . slice ( 100 , 110 )
for ( const id of remaining ) {
const entity = await brainy . get ( id )
expect ( entity ) . toBeDefined ( )
}
} )
it ( 'should handle large-scale deletion efficiently' , async ( ) = > {
// Create a large dataset
const largeDataset = Array ( 1000 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Large scale delete test ${ i } ` ,
type : 'document' as const
} ) )
const addResult = await brainy . addMany ( { items : largeDataset } )
const idsToDelete = addResult . successful
const startTime = performance . now ( )
// Delete in batches
const batchSize = 100
for ( let i = 0 ; i < idsToDelete . length ; i += batchSize ) {
const batch = idsToDelete . slice ( i , i + batchSize )
await Promise . all ( batch . map ( id = > brainy . delete ( id ) ) )
}
const duration = performance . now ( ) - startTime
metricsCollector . recordBatch (
'deleteMany-1k' ,
idsToDelete . length ,
idsToDelete . map ( ( ) = > true ) ,
duration ,
0
)
expect ( duration ) . toBeLessThan ( 10000 ) // < 10 seconds
} )
} )
let nodeIds : string [ ] = [ ]
beforeEach ( async ( ) = > {
// Create nodes for relationships
const nodes = Array ( 50 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Node ${ i } ` ,
type : 'thing' as const ,
metadata : { nodeIndex : i }
} ) )
const result = await brainy . addMany ( { items : nodes } )
nodeIds = result . successful
} )
it ( 'should create multiple relationships efficiently' , async ( ) = > {
const relationships : Array < { from : string , to : string , type : string } > = [ ]
// Create a connected graph
for ( let i = 0 ; i < nodeIds . length - 1 ; i ++ ) {
relationships . push ( {
from : nodeIds [ i ] ,
to : nodeIds [ i + 1 ] ,
type : 'connected_to'
} )
}
const startTime = performance . now ( )
const results = await Promise . all (
relationships . map ( r = >
brainy . relate ( { from : r . from , to : r.to , type : r . type as any } )
)
)
const duration = performance . now ( ) - startTime
metricsCollector . recordBatch (
relationships . length ,
results ,
duration ,
0
)
expect ( results . every ( r = > typeof r === 'string' ) ) . toBe ( true ) // relate returns ID
expect ( duration ) . toBeLessThan ( 5000 )
} )
it ( 'should create complex graph structures' , async ( ) = > {
// Create a fully connected subgraph (first 10 nodes)
const subgraph = nodeIds . slice ( 0 , 10 )
const relationships : Array < { from : string , to : string } > = [ ]
for ( let i = 0 ; i < subgraph . length ; i ++ ) {
for ( let j = i + 1 ; j < subgraph . length ; j ++ ) {
relationships . push ( {
from : subgraph [ i ] ,
to : subgraph [ j ]
} )
}
}
const startTime = performance . now ( )
const results = await Promise . all (
relationships . map ( r = >
brainy . relate ( { from : r . from , to : r.to , type : 'relatedTo' } )
)
)
const duration = performance . now ( ) - startTime
metricsCollector . recordBatch (
relationships . length ,
results ,
duration ,
0
)
expect ( relationships . length ) . toBe ( 45 ) // 10 choose 2
expect ( results . every ( r = > typeof r === 'string' ) ) . toBe ( true ) // relate returns ID
} )
} )
describe ( 'Mixed Batch Operations' , ( ) = > {
it ( 'should handle mixed operation types concurrently' , async ( ) = > {
// Prepare different operation types
const addOps = Array ( 100 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Mixed add ${ i } ` ,
type : 'document' as const
} ) )
// Add initial entities for update/delete
const setupResult = await brainy . addMany ( {
items : Array ( 100 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Setup ${ i } ` ,
type : 'document' as const
} ) )
} )
const updateIds = setupResult . successful . slice ( 0 , 50 )
const deleteIds = setupResult . successful . slice ( 50 , 100 )
const startTime = performance . now ( )
// Execute all operations concurrently
const [ addResults , updateResults , deleteResults ] = await Promise . all ( [
brainy . addMany ( { items : addOps } ) ,
Promise . all ( updateIds . map ( id = >
brainy . update ( { id , metadata : { updated : true } } )
) ) ,
Promise . all ( deleteIds . map ( id = > brainy . delete ( id ) ) )
] )
const duration = performance . now ( ) - startTime
console . log ( ` Mixed operations completed in ${ duration . toFixed ( 2 ) } ms ` )
expect ( addResults . successful . length ) . toBe ( 100 )
expect ( updateResults . length ) . toBe ( 50 )
// Delete returns void, so just check length
expect ( deleteResults ) . toHaveLength ( 50 )
} )
} )
describe ( 'Error Recovery' , ( ) = > {
it ( 'should recover from transient failures' , async ( ) = > {
let attemptCount = 0
const entities = Array ( 10 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : ` Retry test ${ i } ` ,
type : 'document' as const
} ) )
// Simulate transient failures
const originalAdd = brainy . add . bind ( brainy )
brainy . add = async function ( params : any ) {
attemptCount ++
if ( attemptCount % 3 === 0 ) {
throw new Error ( 'Transient failure' )
}
return originalAdd ( params )
}
const results : any [ ] = [ ]
for ( const entity of entities ) {
let retries = 0
while ( retries < 3 ) {
try {
const id = await brainy . add ( entity )
results . push ( { status : 'fulfilled' , value : id } )
break
} catch ( error ) {
retries ++
if ( retries === 3 ) {
results . push ( { status : 'rejected' , reason : error } )
}
}
}
}
const successful = results . filter ( r = > r . status === 'fulfilled' )
expect ( successful . length ) . toBeGreaterThanOrEqual ( 6 ) // Most should succeed with retry
} )
it ( 'should handle memory pressure gracefully' , async ( ) = > {
const largeEntities = Array ( 100 ) . fill ( null ) . map ( ( _ , i ) = > ( {
data : 'x' . repeat ( 100000 ) , // 100KB each = 10MB total
type : 'document' as const ,
metadata : { index : i }
} ) )
const startMemory = process . memoryUsage ( ) . heapUsed
// Process with memory monitoring
const batchSize = 10
const results : any [ ] = [ ]
for ( let i = 0 ; i < largeEntities . length ; i += batchSize ) {
const batch = largeEntities . slice ( i , i + batchSize )
// Check memory before processing
const currentMemory = process . memoryUsage ( ) . heapUsed
const memoryUsed = currentMemory - startMemory
if ( memoryUsed > 100 * 1024 * 1024 ) { // If > 100MB
if ( global . gc ) global . gc ( ) // Force GC if available
}
const batchResult = await brainy . addMany ( { items : batch } )
results . push ( . . . batchResult . successful )
}
expect ( results . length ) . toBe ( 100 )
const finalMemory = process . memoryUsage ( ) . heapUsed - startMemory
expect ( finalMemory ) . toBeLessThan ( 200 * 1024 * 1024 ) // Should stay under 200MB
} )
} )
describe ( 'Performance Report' , ( ) = > {
it ( 'should generate comprehensive batch operations report' , async ( ) = > {
metricsCollector . generateReport ( )
const metrics = metricsCollector . getMetrics ( )
expect ( metrics . length ) . toBeGreaterThan ( 0 )
// Validate performance targets
for ( const metric of metrics ) {
expect ( metric . successCount ) . toBeGreaterThan ( 0 )
if ( metric . operation . includes ( '100' ) ) {
expect ( metric . throughput ) . toBeGreaterThan ( 50 ) // At least 50 items/sec
}
}
} )
} )
} )