fix: flush all native providers on shutdown to prevent data loss

Shutdown/close/flush now properly flushes all 4 components in parallel:
metadataIndex, graphIndex, HNSW dirty nodes, and storage counts. Previously
only counts were flushed, causing native provider data loss on restart.

Also:
- Wire roaring, msgpack, entityIdMapper provider consumption from plugins
- Fix allOf filter O(n²) intersection → O(n) Set-based
- Fix ne/exists negation filter to use Set-based exclusion
- Add setMsgpackImplementation() swap in SSTable for native msgpack
- Add setRoaringImplementation() swap for native CRoaring bitmaps
- Add getAllIntIds() to EntityIdMapper for bitmap operations
- Remove TypeAwareHNSWIndex from default index creation path
- Export memory detection utilities from internals
- Clean up 26 permanently-skipped dead tests
This commit is contained in:
David Snelling 2026-02-01 16:23:49 -08:00
parent b87426409d
commit 773c5171c3
24 changed files with 297 additions and 1268 deletions

View file

@ -459,29 +459,8 @@ describe('Brainy.add()', () => {
)
})
it.skip('should handle batch adds efficiently', async () => {
// NOTE: Flaky performance test - depends on system load
// Arrange
const count = 100
const params = Array.from({ length: count }, (_, i) =>
createAddParams({
data: `Batch entity ${i}`,
type: 'thing'
})
)
// Act
const start = performance.now()
const ids = await Promise.all(params.map(p => brain.add(p)))
const duration = performance.now() - start
// Assert
expect(ids).toHaveLength(count)
const opsPerSecond = (count / duration) * 1000
expect(opsPerSecond).toBeGreaterThan(100) // At least 100 ops/second
})
})
describe('caching behavior', () => {
it('should retrieve consistent entities', async () => {
// Arrange (v5.1.0: use valid UUID format)

View file

@ -36,83 +36,6 @@ describe('Brainy Batch Operations', () => {
}
})
// TODO: Investigate missing entities in batch operations - may be concurrency/timing issue
it.skip('should handle large batches efficiently', async () => {
const batchSize = 100
const entities = Array.from({ length: batchSize }, (_, i) => ({
data: `Entity ${i}`,
type: NounType.Thing,
metadata: { index: i, batch: true }
}))
const startTime = Date.now()
const result = await brain.addMany({ items: entities })
const duration = Date.now() - startTime
expect(result.successful).toHaveLength(batchSize)
expect(duration).toBeLessThan(10000) // 10 seconds for 100 embeddings is reasonable
// Verify a sample
const sampleEntity = await brain.get(result.successful[50])
expect(sampleEntity?.metadata?.index).toBe(50)
})
it.skip('should handle mixed entity types', async () => {
// NOTE: Test skipped - addMany order preservation is flaky (see d582069)
const entities = [
{ data: 'John Doe', type: NounType.Person, metadata: { role: 'developer' } },
{ data: 'TechCorp', type: NounType.Organization, metadata: { industry: 'tech' } },
{ data: 'San Francisco', type: NounType.Location, metadata: { country: 'USA' } },
{ data: 'Project Alpha', type: NounType.Project, metadata: { status: 'active' } }
]
const result = await brain.addMany({ items: entities })
expect(result.successful).toHaveLength(4)
// Verify different types were added correctly
const person = await brain.get(result.successful[0])
expect(person?.type).toBe(NounType.Person)
const org = await brain.get(result.successful[1])
expect(org?.type).toBe(NounType.Organization)
})
it.skip('should handle partial failures gracefully', async () => {
// NOTE: Test is flaky - fails intermittently with empty data validation
const entities = [
{ data: 'Valid Entity 1', type: NounType.Thing },
{ data: '', type: NounType.Thing }, // Invalid - empty data
{ data: 'Valid Entity 2', type: NounType.Thing }
]
try {
const result = await brain.addMany({ items: entities })
// Some implementations might skip invalid entries
expect(result.successful.length).toBeLessThanOrEqual(3)
} catch (error) {
// Or might throw an error
expect(error).toBeDefined()
}
})
it.skip('should maintain order of additions', async () => {
// NOTE: Test skipped - addMany order preservation is flaky (see d582069)
const entities = Array.from({ length: 10 }, (_, i) => ({
data: `Ordered Entity ${i}`,
type: NounType.Thing,
metadata: { order: i }
}))
const result = await brain.addMany({ items: entities })
// Verify order is maintained
for (let i = 0; i < result.successful.length; i++) {
const entity = await brain.get(result.successful[i])
expect(entity?.metadata?.order).toBe(i)
}
})
it('should generate embeddings for all entities', async () => {
const entities = [
{ data: 'Machine learning is fascinating', type: NounType.Concept },
@ -325,25 +248,6 @@ describe('Brainy Batch Operations', () => {
expect(sample).toBeNull()
})
// TODO: Investigate deleteMany not actually deleting entities - may be cache/consistency issue
it.skip('should ignore non-existent IDs', async () => {
const mixedIds = [
testIds[0],
'non-existent-1',
testIds[1],
'non-existent-2'
]
// Should not throw
await brain.deleteMany({ ids: mixedIds })
// Valid ones should be deleted
expect(await brain.get(testIds[0])).toBeNull()
expect(await brain.get(testIds[1])).toBeNull()
// Others should still exist
expect(await brain.get(testIds[2])).toBeDefined()
})
})
describe('relateMany - Batch Relationship Creation', () => {

View file

@ -1,359 +0,0 @@
import { describe, it, expect, beforeAll } from 'vitest'
import { Brainy } from '../../../src/brainy'
import { createAddParams } from '../../helpers/test-factory'
// v4.11.2: SKIPPED - brain.init() takes >60s causing timeout
// This is a pre-existing performance issue (also failed in v4.11.0)
// TODO: Investigate why Brainy initialization is so slow in this test context
describe.skip('Brainy.delete()', () => {
let brain: Brainy<any>
// v4.11.2: Use shared brain instance to prevent memory errors
// Creating new instance per test consumes too much memory (OOM errors)
beforeAll(async () => {
brain = new Brainy()
await brain.init()
}, 60000) // Increase timeout for initial setup
describe('success paths', () => {
it('should delete an existing entity', async () => {
// Arrange
const id = await brain.add(createAddParams({
data: 'Test entity',
type: 'thing',
metadata: { test: true }
}))
// Verify it exists
const before = await brain.get(id)
expect(before).not.toBeNull()
// Act
await brain.delete(id)
// Assert
const after = await brain.get(id)
expect(after).toBeNull()
})
it('should delete entity with relationships', async () => {
// TODO: Fix relationship cleanup - verbs are being found after deletion
// Possible causes: storage cache, metadata index, or graph index not updating
// Arrange - Create entities and relationships
const entity1 = await brain.add(createAddParams({ data: 'Entity 1' }))
const entity2 = await brain.add(createAddParams({ data: 'Entity 2' }))
const entity3 = await brain.add(createAddParams({ data: 'Entity 3' }))
await brain.relate({
from: entity1,
to: entity2,
type: 'relatedTo'
})
await brain.relate({
from: entity1,
to: entity3,
type: 'creates'
})
await brain.relate({
from: entity2,
to: entity1,
type: 'references'
})
// Act - Delete entity1
await brain.delete(entity1)
// Assert - Entity should be gone
const deleted = await brain.get(entity1)
expect(deleted).toBeNull()
// Entity2 and entity3 should still exist
const e2 = await brain.get(entity2)
const e3 = await brain.get(entity3)
expect(e2).not.toBeNull()
expect(e3).not.toBeNull()
// Relationships involving entity1 should be removed
const fromEntity1 = await brain.getRelations({ from: entity1 })
const toEntity1 = await brain.getRelations({ to: entity1 })
expect(fromEntity1).toHaveLength(0)
expect(toEntity1).toHaveLength(0)
// Other relationships should remain
const fromEntity2 = await brain.getRelations({ from: entity2 })
expect(fromEntity2.some(r => r.to === entity1)).toBe(false)
})
it.skip('should delete entity and clean up index', async () => {
// Arrange
const id = await brain.add(createAddParams({
data: 'Searchable entity',
metadata: { searchable: true }
}))
// Verify it's searchable
const beforeResults = await brain.find({
query: 'Searchable entity',
limit: 10
})
expect(beforeResults.some(r => r.entity.id === id)).toBe(true)
// Act
await brain.delete(id)
// Assert - Should not be in search results
const afterResults = await brain.find({
query: 'Searchable entity',
limit: 10
})
expect(afterResults.some(r => r.entity.id === id)).toBe(false)
})
it('should handle deleting multiple entities', async () => {
// Arrange
const ids = await Promise.all([
brain.add(createAddParams({ data: 'Entity 1' })),
brain.add(createAddParams({ data: 'Entity 2' })),
brain.add(createAddParams({ data: 'Entity 3' }))
])
// Act - Delete all
await Promise.all(ids.map(id => brain.delete(id)))
// Assert - All should be gone
const results = await Promise.all(ids.map(id => brain.get(id)))
expect(results.every(r => r === null)).toBe(true)
})
it('should handle deleting entity with bidirectional relationships', async () => {
// Arrange
const entity1 = await brain.add(createAddParams({ data: 'Entity 1' }))
const entity2 = await brain.add(createAddParams({ data: 'Entity 2' }))
await brain.relate({
from: entity1,
to: entity2,
type: 'friendOf',
bidirectional: true
})
// Verify both directions exist
const before1 = await brain.getRelations({ from: entity1 })
const before2 = await brain.getRelations({ from: entity2 })
expect(before1.some(r => r.to === entity2)).toBe(true)
expect(before2.some(r => r.to === entity1)).toBe(true)
// Act
await brain.delete(entity1)
// Assert - All relationships should be cleaned up
const after1 = await brain.getRelations({ from: entity1 })
const after2 = await brain.getRelations({ from: entity2 })
expect(after1).toHaveLength(0)
expect(after2.some(r => r.to === entity1)).toBe(false)
})
})
describe('error paths', () => {
it('should handle deleting non-existent entity', async () => {
// Act - Delete non-existent entity (should not throw)
const nonExistentId = 'fake-id-123'
// Should complete without error
await expect(brain.delete(nonExistentId)).resolves.not.toThrow()
})
it('should handle invalid ID format', async () => {
// Act & Assert
await expect(brain.delete('')).resolves.not.toThrow()
await expect(brain.delete(null as any)).resolves.not.toThrow()
await expect(brain.delete(undefined as any)).resolves.not.toThrow()
})
it('should handle double deletion', async () => {
// Arrange
const id = await brain.add(createAddParams({ data: 'Test' }))
// Act - Delete twice
await brain.delete(id)
await brain.delete(id) // Should not throw
// Assert
const result = await brain.get(id)
expect(result).toBeNull()
})
})
describe('edge cases', () => {
it('should handle deletion with circular relationships', async () => {
// TODO: Fix relationship cleanup - related to same issue as above
// Arrange - Create circular relationship
const entity1 = await brain.add(createAddParams({ data: 'Entity 1' }))
const entity2 = await brain.add(createAddParams({ data: 'Entity 2' }))
const entity3 = await brain.add(createAddParams({ data: 'Entity 3' }))
await brain.relate({ from: entity1, to: entity2, type: 'relatedTo' })
await brain.relate({ from: entity2, to: entity3, type: 'relatedTo' })
await brain.relate({ from: entity3, to: entity1, type: 'relatedTo' })
// Act
await brain.delete(entity2)
// Assert - Entity2 gone, others remain
expect(await brain.get(entity2)).toBeNull()
expect(await brain.get(entity1)).not.toBeNull()
expect(await brain.get(entity3)).not.toBeNull()
// Relationships involving entity2 should be gone
const relations1 = await brain.getRelations({ from: entity1 })
const relations3 = await brain.getRelations({ from: entity3 })
expect(relations1.some(r => r.to === entity2)).toBe(false)
expect(relations3.some(r => r.to === entity2)).toBe(false)
})
it('should handle concurrent deletions', async () => {
// Arrange
const ids = await Promise.all(
Array.from({ length: 10 }, (_, i) =>
brain.add(createAddParams({ data: `Entity ${i}` }))
)
)
// Act - Delete concurrently
await Promise.all(ids.map(id => brain.delete(id)))
// Assert - All should be deleted
const results = await Promise.all(ids.map(id => brain.get(id)))
expect(results.every(r => r === null)).toBe(true)
})
it('should maintain data integrity after deletion', async () => {
// Arrange
const keepId = await brain.add(createAddParams({
data: 'Keep this',
metadata: { important: true }
}))
const deleteIds = await Promise.all([
brain.add(createAddParams({ data: 'Delete 1' })),
brain.add(createAddParams({ data: 'Delete 2' })),
brain.add(createAddParams({ data: 'Delete 3' }))
])
// Create relationships
for (const deleteId of deleteIds) {
await brain.relate({
from: keepId,
to: deleteId,
type: 'relatedTo'
})
}
// Act - Delete some entities
await Promise.all(deleteIds.map(id => brain.delete(id)))
// Assert - Kept entity should be intact
const kept = await brain.get(keepId)
expect(kept).not.toBeNull()
expect(kept!.metadata.important).toBe(true)
// Relations to deleted entities should be gone
const relations = await brain.getRelations({ from: keepId })
expect(relations).toHaveLength(0)
})
})
describe('performance', () => {
it('should delete entities quickly', async () => {
// Arrange
const id = await brain.add(createAddParams({ data: 'Fast delete' }))
// Act & Assert
const start = Date.now()
await brain.delete(id)
const duration = Date.now() - start
expect(duration).toBeLessThan(50) // Should be very fast
})
it('should handle batch deletion efficiently', async () => {
// Arrange - Create many entities
const ids = await Promise.all(
Array.from({ length: 100 }, (_, i) =>
brain.add(createAddParams({ data: `Entity ${i}` }))
)
)
// Act
const start = Date.now()
await Promise.all(ids.map(id => brain.delete(id)))
const duration = Date.now() - start
// Assert
// Note: After fixing the relationship cleanup bug, deletes are slightly slower
// because we now properly fetch ALL relationships (not just first 100)
expect(duration).toBeLessThan(2000) // Should handle 100 deletes in under 2s
// Verify all deleted
const results = await Promise.all(ids.map(id => brain.get(id)))
expect(results.every(r => r === null)).toBe(true)
})
})
describe('consistency', () => {
it.skip('should properly invalidate cache after deletion', async () => {
// NOTE: Test skipped - flaky search timing issue (see commits 8476047, c64967d)
// Arrange
const id = await brain.add(createAddParams({
data: 'Cached entity',
metadata: { cached: true }
}))
// Do a search to potentially cache results
const before = await brain.find({ query: 'Cached entity' })
expect(before.some(r => r.entity.id === id)).toBe(true)
// Act
await brain.delete(id)
// Assert - Cache should be invalidated
const after = await brain.find({ query: 'Cached entity' })
expect(after.some(r => r.entity.id === id)).toBe(false)
})
it('should maintain consistency across operations', async () => {
// Arrange
const entity1 = await brain.add(createAddParams({ data: 'Entity 1' }))
const entity2 = await brain.add(createAddParams({ data: 'Entity 2' }))
await brain.relate({
from: entity1,
to: entity2,
type: 'relatedTo'
})
// Act - Update entity1, delete entity2
await brain.update({
id: entity1,
metadata: { updated: true },
merge: true
})
await brain.delete(entity2)
// Assert
const e1 = await brain.get(entity1)
expect(e1).not.toBeNull()
expect(e1!.metadata.updated).toBe(true)
const e2 = await brain.get(entity2)
expect(e2).toBeNull()
// Relations should be cleaned up
const relations = await brain.getRelations({ from: entity1 })
expect(relations.some(r => r.to === entity2)).toBe(false)
})
})
})

View file

@ -296,27 +296,8 @@ describe('Brainy.relate()', () => {
expect(relation!.metadata || {}).toMatchObject(specialMetadata)
})
it.skip('should handle concurrent relationship creation', async () => {
// NOTE: Test skipped - flaky due to race condition in duplicate detection (expected 10, got 9)
// Act - Create 10 relationships concurrently
const promises = Array.from({ length: 10 }, (_, i) =>
brain.relate({
from: entity1Id,
to: entity2Id,
type: 'relatedTo',
metadata: { index: i }
})
)
await Promise.all(promises)
// Assert
const relations = await brain.getRelations({ from: entity1Id })
const toEntity2 = relations.filter(r => r.to === entity2Id)
expect(toEntity2.length).toBe(10)
})
})
describe('performance', () => {
it('should create relationships quickly', async () => {
// Act & Assert