BREAKING CHANGE: New unified API for vector, graph, and document search
Major Changes:
- NEW: brain.add() replaces brain.addNoun()
- NEW: brain.find() replaces brain.search()
- NEW: brain.relate() replaces brain.addVerb()
- NEW: brain.update() replaces brain.updateNoun()
- NEW: brain.delete() replaces brain.deleteNoun()
Features:
- Triple Intelligence™ engine (vector + graph + document)
- 31 NounTypes × 40 VerbTypes for universal knowledge modeling
- Zero-config parameter validation
- Enhanced augmentation system (cache, display, metrics)
- <10ms search performance with HNSW indexing
- Full TypeScript type safety
Infrastructure:
- Comprehensive test suites for find() and neural APIs
- Fixed neural API internal calls (getNoun → get)
- Updated README with accurate 3.0 examples
- ESLint v9 configuration
- Structured logging framework
🧠 Generated with Brainy 3.0
Co-Authored-By: Claude <noreply@anthropic.com>
1483 lines
48 KiB
TypeScript
1483 lines
48 KiB
TypeScript
/**
|
|
* Comprehensive Edge Case Testing for find() Method and GraphAdjacencyIndex
|
|
*
|
|
* This test suite covers all critical edge cases, error scenarios, and boundary conditions
|
|
* for the Brainy find() method and GraphAdjacencyIndex to ensure bulletproof reliability.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'
|
|
import { Brainy } from '../../src/brainy.js'
|
|
import { GraphAdjacencyIndex } from '../../src/graph/graphAdjacencyIndex.js'
|
|
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
|
import { MemoryStorage } from '../../src/storage/adapters/memoryStorage.js'
|
|
import { GraphVerb } from '../../src/coreTypes.js'
|
|
|
|
// Mock storage adapter for controlled testing
|
|
class MockStorageAdapter extends MemoryStorage {
|
|
private shouldFail = false
|
|
private failOperation: string | null = null
|
|
|
|
setFailure(operation: string) {
|
|
this.shouldFail = true
|
|
this.failOperation = operation
|
|
}
|
|
|
|
clearFailure() {
|
|
this.shouldFail = false
|
|
this.failOperation = null
|
|
}
|
|
|
|
async getVerbs(params?: any) {
|
|
if (this.shouldFail && this.failOperation === 'getVerbs') {
|
|
throw new Error('Storage adapter failure: getVerbs')
|
|
}
|
|
return super.getVerbs(params)
|
|
}
|
|
|
|
async saveVerb(verb: GraphVerb) {
|
|
if (this.shouldFail && this.failOperation === 'saveVerb') {
|
|
throw new Error('Storage adapter failure: saveVerb')
|
|
}
|
|
return super.saveVerb(verb)
|
|
}
|
|
}
|
|
|
|
describe('Find() Method and GraphAdjacencyIndex Edge Cases', () => {
|
|
let brain: Brainy
|
|
let graphIndex: GraphAdjacencyIndex
|
|
let mockStorage: MockStorageAdapter
|
|
|
|
beforeAll(async () => {
|
|
// Setup Brainy instance
|
|
brain = new Brainy({
|
|
storage: { type: 'memory' }
|
|
})
|
|
await brain.init()
|
|
|
|
// Setup GraphAdjacencyIndex with mock storage
|
|
mockStorage = new MockStorageAdapter()
|
|
await mockStorage.init()
|
|
graphIndex = new GraphAdjacencyIndex(mockStorage)
|
|
})
|
|
|
|
afterAll(async () => {
|
|
if (graphIndex) {
|
|
await graphIndex.close()
|
|
}
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
// Clear mock failures
|
|
mockStorage.clearFailure()
|
|
|
|
// Reset graph index
|
|
await graphIndex.rebuild()
|
|
})
|
|
|
|
afterEach(() => {
|
|
// Clear all mocks
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// ============= ERROR HANDLING SCENARIOS =============
|
|
|
|
describe('Error Handling Scenarios', () => {
|
|
describe('Invalid Node IDs and Relationship Types', () => {
|
|
it('should handle null node ID in getNeighbors', async () => {
|
|
await expect(graphIndex.getNeighbors(null as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle undefined node ID in getNeighbors', async () => {
|
|
await expect(graphIndex.getNeighbors(undefined as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle empty string node ID', async () => {
|
|
const result = await graphIndex.getNeighbors('')
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('should handle extremely long node IDs', async () => {
|
|
const longId = 'a'.repeat(10000)
|
|
const result = await graphIndex.getNeighbors(longId)
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('should handle node IDs with special characters', async () => {
|
|
const specialId = 'node@#$%^&*()_+{}|:<>?[]\\;\'",./'
|
|
const result = await graphIndex.getNeighbors(specialId)
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('should handle Unicode node IDs', async () => {
|
|
const unicodeId = '节点🚀测试ñáéíóú'
|
|
const result = await graphIndex.getNeighbors(unicodeId)
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('should handle invalid relationship types in addVerb', async () => {
|
|
const invalidVerb: GraphVerb = {
|
|
id: 'test-verb',
|
|
sourceId: 'source',
|
|
targetId: 'target',
|
|
type: 'INVALID_TYPE' as any,
|
|
metadata: {}
|
|
}
|
|
|
|
await expect(graphIndex.addVerb(invalidVerb)).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('Null/Undefined Parameters in All Methods', () => {
|
|
it('should handle null parameters in find()', async () => {
|
|
await expect(brain.find(null as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle undefined parameters in find()', async () => {
|
|
await expect(brain.find(undefined as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle null query object', async () => {
|
|
await expect(brain.find({ query: null } as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle undefined vector', async () => {
|
|
await expect(brain.find({ vector: undefined } as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle null metadata filters', async () => {
|
|
await expect(brain.find({ where: null } as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle null graph constraints', async () => {
|
|
await expect(brain.find({ connected: null } as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle null direction in getNeighbors', async () => {
|
|
const result = await graphIndex.getNeighbors('test-node', null as any)
|
|
expect(Array.isArray(result)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Storage Adapter Failures and Network Issues', () => {
|
|
it('should handle storage failure during rebuild', async () => {
|
|
mockStorage.setFailure('getVerbs')
|
|
|
|
await expect(graphIndex.rebuild()).rejects.toThrow('Storage adapter failure')
|
|
expect(graphIndex.isHealthy()).toBe(false)
|
|
})
|
|
|
|
it('should handle storage failure during addVerb', async () => {
|
|
mockStorage.setFailure('saveVerb')
|
|
|
|
const verb: GraphVerb = {
|
|
id: 'test-verb',
|
|
sourceId: 'source',
|
|
targetId: 'target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
}
|
|
|
|
await expect(graphIndex.addVerb(verb)).rejects.toThrow('Storage adapter failure')
|
|
})
|
|
|
|
it('should handle network timeout during large queries', async () => {
|
|
// Mock a timeout scenario
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
setTimeout(() => reject(new Error('Network timeout')), 100)
|
|
})
|
|
|
|
vi.spyOn(mockStorage, 'getVerbs').mockImplementation(() => timeoutPromise as any)
|
|
|
|
await expect(graphIndex.rebuild()).rejects.toThrow('Network timeout')
|
|
})
|
|
|
|
it('should handle connection reset during find()', async () => {
|
|
// Mock connection failure
|
|
const originalGet = brain['get'].bind(brain)
|
|
vi.spyOn(brain as any, 'get').mockRejectedValue(new Error('Connection reset'))
|
|
|
|
await expect(brain.find({ id: 'nonexistent' })).rejects.toThrow('Connection reset')
|
|
|
|
// Restore original method
|
|
vi.restoreAllMocks()
|
|
})
|
|
})
|
|
|
|
describe('Memory Pressure and Out-of-Memory Conditions', () => {
|
|
it('should handle memory pressure during large index rebuild', async () => {
|
|
// Create a large number of verbs to simulate memory pressure
|
|
const verbs: GraphVerb[] = []
|
|
for (let i = 0; i < 10000; i++) {
|
|
verbs.push({
|
|
id: `verb-${i}`,
|
|
sourceId: `source-${i}`,
|
|
targetId: `target-${i}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: { largeData: 'x'.repeat(1000) }
|
|
})
|
|
}
|
|
|
|
// Add verbs one by one to simulate memory pressure
|
|
for (const verb of verbs.slice(0, 100)) {
|
|
await graphIndex.addVerb(verb)
|
|
}
|
|
|
|
const stats = graphIndex.getStats()
|
|
expect(stats.totalRelationships).toBeGreaterThan(0)
|
|
expect(stats.memoryUsage).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should handle out-of-memory during concurrent operations', async () => {
|
|
const operations = Array(100).fill(null).map((_, i) =>
|
|
graphIndex.addVerb({
|
|
id: `concurrent-verb-${i}`,
|
|
sourceId: `source-${i % 10}`,
|
|
targetId: `target-${i % 10}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
)
|
|
|
|
// Should handle concurrent operations without OOM
|
|
await expect(Promise.all(operations)).resolves.not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('Concurrent Modification Conflicts', () => {
|
|
it('should handle concurrent addVerb operations', async () => {
|
|
const concurrentAdds = Array(50).fill(null).map((_, i) =>
|
|
graphIndex.addVerb({
|
|
id: `concurrent-add-${i}`,
|
|
sourceId: `source-${i % 10}`,
|
|
targetId: `target-${i % 10}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
)
|
|
|
|
await expect(Promise.all(concurrentAdds)).resolves.not.toThrow()
|
|
|
|
// Verify all relationships were added
|
|
const neighbors = await graphIndex.getNeighbors('source-0')
|
|
expect(neighbors.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should handle concurrent removeVerb operations', async () => {
|
|
// First add some verbs
|
|
const verbs: GraphVerb[] = []
|
|
for (let i = 0; i < 20; i++) {
|
|
const verb = {
|
|
id: `remove-test-${i}`,
|
|
sourceId: 'remove-source',
|
|
targetId: `remove-target-${i}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
}
|
|
verbs.push(verb)
|
|
await graphIndex.addVerb(verb)
|
|
}
|
|
|
|
// Concurrent removal
|
|
const concurrentRemoves = verbs.map(verb =>
|
|
graphIndex.removeVerb(verb.id)
|
|
)
|
|
|
|
await expect(Promise.all(concurrentRemoves)).resolves.not.toThrow()
|
|
|
|
// Verify all were removed
|
|
const neighbors = await graphIndex.getNeighbors('remove-source')
|
|
expect(neighbors).toEqual([])
|
|
})
|
|
|
|
it('should handle mixed concurrent add/remove operations', async () => {
|
|
const operations: Promise<void>[] = []
|
|
|
|
for (let i = 0; i < 30; i++) {
|
|
if (i % 2 === 0) {
|
|
operations.push(graphIndex.addVerb({
|
|
id: `mixed-${i}`,
|
|
sourceId: 'mixed-source',
|
|
targetId: `mixed-target-${i}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
}))
|
|
} else {
|
|
operations.push(graphIndex.removeVerb(`mixed-${i - 1}`))
|
|
}
|
|
}
|
|
|
|
await expect(Promise.allSettled(operations)).resolves.not.toThrow()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============= BOUNDARY CONDITIONS =============
|
|
|
|
describe('Boundary Conditions', () => {
|
|
describe('Empty Graphs and Zero Relationships', () => {
|
|
it('should handle empty graph with no relationships', async () => {
|
|
const result = await graphIndex.getNeighbors('nonexistent')
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('should handle find() on empty database', async () => {
|
|
const results = await brain.find({ query: 'anything' })
|
|
expect(results).toEqual([])
|
|
})
|
|
|
|
it('should handle zero relationships in graph search', async () => {
|
|
const results = await brain.find({
|
|
connected: { to: 'nonexistent' }
|
|
})
|
|
expect(results).toEqual([])
|
|
})
|
|
|
|
it('should handle empty result sets from all search types', async () => {
|
|
const vectorResults = await brain.find({ query: 'nonexistent-query' })
|
|
const metadataResults = await brain.find({ where: { nonexistent: 'field' } })
|
|
const graphResults = await brain.find({ connected: { from: 'nonexistent' } })
|
|
|
|
expect(vectorResults).toEqual([])
|
|
expect(metadataResults).toEqual([])
|
|
expect(graphResults).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('Maximum Relationship Limits Per Node', () => {
|
|
it('should handle maximum relationships per node', async () => {
|
|
const maxRelationships = 1000
|
|
const sourceId = 'max-relations-source'
|
|
|
|
// Add maximum relationships
|
|
for (let i = 0; i < maxRelationships; i++) {
|
|
await graphIndex.addVerb({
|
|
id: `max-rel-${i}`,
|
|
sourceId,
|
|
targetId: `target-${i}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
}
|
|
|
|
const neighbors = await graphIndex.getNeighbors(sourceId)
|
|
expect(neighbors).toHaveLength(maxRelationships)
|
|
})
|
|
|
|
it('should handle bidirectional maximum relationships', async () => {
|
|
const nodeA = 'bidirectional-a'
|
|
const nodeB = 'bidirectional-b'
|
|
const maxRelationships = 500
|
|
|
|
// Create bidirectional relationships
|
|
for (let i = 0; i < maxRelationships; i++) {
|
|
await graphIndex.addVerb({
|
|
id: `bi-rel-a-${i}`,
|
|
sourceId: nodeA,
|
|
targetId: `${nodeB}-${i}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
await graphIndex.addVerb({
|
|
id: `bi-rel-b-${i}`,
|
|
sourceId: `${nodeB}-${i}`,
|
|
targetId: nodeA,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
}
|
|
|
|
const neighborsA = await graphIndex.getNeighbors(nodeA)
|
|
const neighborsB = await graphIndex.getNeighbors(`${nodeB}-0`)
|
|
|
|
expect(neighborsA).toHaveLength(maxRelationships)
|
|
expect(neighborsB).toHaveLength(1) // Only connected back to A
|
|
})
|
|
})
|
|
|
|
describe('Extremely Large Node IDs and Relationship Types', () => {
|
|
it('should handle extremely large node IDs', async () => {
|
|
const largeId = 'node-' + 'x'.repeat(10000)
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'large-id-verb',
|
|
sourceId: largeId,
|
|
targetId: 'target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors(largeId)
|
|
expect(neighbors).toContain('target')
|
|
})
|
|
|
|
it('should handle extremely large relationship type names', async () => {
|
|
const largeType = 'RELATIONSHIP_' + 'TYPE_'.repeat(1000)
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'large-type-verb',
|
|
sourceId: 'source',
|
|
targetId: 'target',
|
|
type: largeType as VerbType,
|
|
metadata: {}
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors('source')
|
|
expect(neighbors).toContain('target')
|
|
})
|
|
|
|
it('should handle maximum length metadata values', async () => {
|
|
const maxMetadata = 'x'.repeat(100000) // 100KB metadata
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'max-metadata-verb',
|
|
sourceId: 'source',
|
|
targetId: 'target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: { largeField: maxMetadata }
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors('source')
|
|
expect(neighbors).toContain('target')
|
|
})
|
|
})
|
|
|
|
describe('Unicode and Special Character Handling', () => {
|
|
it('should handle Unicode characters in node IDs', async () => {
|
|
const unicodeNodes = [
|
|
'节点1', '🚀', 'ñáéíóú', 'тест', 'مرحبا', 'こんにちは',
|
|
'🌟⭐', 'αβγδε', '中文', 'русский'
|
|
]
|
|
|
|
for (const nodeId of unicodeNodes) {
|
|
await graphIndex.addVerb({
|
|
id: `unicode-${nodeId}`,
|
|
sourceId: nodeId,
|
|
targetId: 'target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
}
|
|
|
|
for (const nodeId of unicodeNodes) {
|
|
const neighbors = await graphIndex.getNeighbors(nodeId)
|
|
expect(neighbors).toContain('target')
|
|
}
|
|
})
|
|
|
|
it('should handle special characters in relationship types', async () => {
|
|
const specialTypes = [
|
|
'TYPE@#$%', 'TYPE-with-dashes', 'TYPE.with.dots',
|
|
'TYPE_underscore', 'TYPE spaces', 'TYPE+plus'
|
|
]
|
|
|
|
for (const type of specialTypes) {
|
|
await graphIndex.addVerb({
|
|
id: `special-${type}`,
|
|
sourceId: 'source',
|
|
targetId: `target-${type}`,
|
|
type: type as VerbType,
|
|
metadata: {}
|
|
})
|
|
}
|
|
|
|
const neighbors = await graphIndex.getNeighbors('source')
|
|
expect(neighbors).toHaveLength(specialTypes.length)
|
|
})
|
|
|
|
it('should handle emoji in metadata', async () => {
|
|
await graphIndex.addVerb({
|
|
id: 'emoji-verb',
|
|
sourceId: 'emoji-source',
|
|
targetId: 'emoji-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {
|
|
emoji: '🚀⭐🌟',
|
|
description: 'Test with 🎉 emojis 🎊'
|
|
}
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors('emoji-source')
|
|
expect(neighbors).toContain('emoji-target')
|
|
})
|
|
})
|
|
|
|
describe('Very Deep Graph Traversals', () => {
|
|
it('should handle deep graph traversal (100+ levels)', async () => {
|
|
const depth = 150
|
|
let currentNode = 'root'
|
|
|
|
// Create a deep chain
|
|
for (let i = 0; i < depth; i++) {
|
|
const nextNode = `node-${i}`
|
|
await graphIndex.addVerb({
|
|
id: `chain-${i}`,
|
|
sourceId: currentNode,
|
|
targetId: nextNode,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
currentNode = nextNode
|
|
}
|
|
|
|
// Verify the chain exists
|
|
let node = 'root'
|
|
for (let i = 0; i < Math.min(depth, 10); i++) {
|
|
const neighbors = await graphIndex.getNeighbors(node, 'out')
|
|
expect(neighbors).toHaveLength(1)
|
|
node = neighbors[0]
|
|
}
|
|
})
|
|
|
|
it('should handle circular references in deep traversal', async () => {
|
|
// Create a circular graph
|
|
const nodes = ['A', 'B', 'C', 'D', 'E']
|
|
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const current = nodes[i]
|
|
const next = nodes[(i + 1) % nodes.length]
|
|
|
|
await graphIndex.addVerb({
|
|
id: `circle-${i}`,
|
|
sourceId: current,
|
|
targetId: next,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
}
|
|
|
|
// Each node should have one outgoing neighbor
|
|
for (const node of nodes) {
|
|
const neighbors = await graphIndex.getNeighbors(node, 'out')
|
|
expect(neighbors).toHaveLength(1)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============= COMPLEX QUERY PATTERNS =============
|
|
|
|
describe('Complex Query Patterns', () => {
|
|
describe('Circular Relationship Detection', () => {
|
|
it('should detect and handle self-referencing relationships', async () => {
|
|
await graphIndex.addVerb({
|
|
id: 'self-ref',
|
|
sourceId: 'self',
|
|
targetId: 'self',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors('self')
|
|
expect(neighbors).toContain('self')
|
|
})
|
|
|
|
it('should handle multiple self-references', async () => {
|
|
for (let i = 0; i < 5; i++) {
|
|
await graphIndex.addVerb({
|
|
id: `self-ref-${i}`,
|
|
sourceId: 'multi-self',
|
|
targetId: 'multi-self',
|
|
type: VerbType.RelatesTo,
|
|
metadata: { index: i }
|
|
})
|
|
}
|
|
|
|
const neighbors = await graphIndex.getNeighbors('multi-self')
|
|
expect(neighbors).toEqual(['multi-self'])
|
|
})
|
|
|
|
it('should handle circular relationships between multiple nodes', async () => {
|
|
const nodes = ['X', 'Y', 'Z']
|
|
|
|
// Create circular relationships
|
|
await graphIndex.addVerb({ id: 'x-to-y', sourceId: 'X', targetId: 'Y', type: VerbType.RelatesTo, metadata: {} })
|
|
await graphIndex.addVerb({ id: 'y-to-z', sourceId: 'Y', targetId: 'Z', type: VerbType.RelatesTo, metadata: {} })
|
|
await graphIndex.addVerb({ id: 'z-to-x', sourceId: 'Z', targetId: 'X', type: VerbType.RelatesTo, metadata: {} })
|
|
|
|
for (const node of nodes) {
|
|
const neighbors = await graphIndex.getNeighbors(node, 'out')
|
|
expect(neighbors).toHaveLength(1)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Self-Referencing Relationships', () => {
|
|
it('should handle self-referencing with different relationship types', async () => {
|
|
const types = [VerbType.RelatesTo, VerbType.Contains, VerbType.BelongsTo]
|
|
|
|
for (const type of types) {
|
|
await graphIndex.addVerb({
|
|
id: `self-${type}`,
|
|
sourceId: 'self-multi-type',
|
|
targetId: 'self-multi-type',
|
|
type,
|
|
metadata: {}
|
|
})
|
|
}
|
|
|
|
const neighbors = await graphIndex.getNeighbors('self-multi-type')
|
|
expect(neighbors).toEqual(['self-multi-type'])
|
|
})
|
|
|
|
it('should handle self-reference with complex metadata', async () => {
|
|
await graphIndex.addVerb({
|
|
id: 'self-complex',
|
|
sourceId: 'self-complex-node',
|
|
targetId: 'self-complex-node',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {
|
|
nested: { data: { value: 42 } },
|
|
array: [1, 2, 3],
|
|
string: 'complex metadata'
|
|
}
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors('self-complex-node')
|
|
expect(neighbors).toEqual(['self-complex-node'])
|
|
})
|
|
})
|
|
|
|
describe('Multiple Relationship Types Between Same Nodes', () => {
|
|
it('should handle multiple relationship types between same nodes', async () => {
|
|
const source = 'multi-type-source'
|
|
const target = 'multi-type-target'
|
|
const types = [VerbType.RelatesTo, VerbType.Contains, VerbType.BelongsTo, VerbType.ConnectsTo]
|
|
|
|
for (const type of types) {
|
|
await graphIndex.addVerb({
|
|
id: `multi-${type}`,
|
|
sourceId: source,
|
|
targetId: target,
|
|
type,
|
|
metadata: { relationshipType: type }
|
|
})
|
|
}
|
|
|
|
const outgoing = await graphIndex.getNeighbors(source, 'out')
|
|
const incoming = await graphIndex.getNeighbors(target, 'in')
|
|
|
|
expect(outgoing).toContain(target)
|
|
expect(incoming).toContain(source)
|
|
})
|
|
|
|
it('should handle conflicting metadata in multiple relationships', async () => {
|
|
const source = 'conflict-source'
|
|
const target = 'conflict-target'
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'conflict-1',
|
|
sourceId: source,
|
|
targetId: target,
|
|
type: VerbType.RelatesTo,
|
|
metadata: { weight: 1, priority: 'high' }
|
|
})
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'conflict-2',
|
|
sourceId: source,
|
|
targetId: target,
|
|
type: VerbType.RelatesTo,
|
|
metadata: { weight: 2, priority: 'low' }
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors(source, 'out')
|
|
expect(neighbors).toContain(target)
|
|
})
|
|
})
|
|
|
|
describe('Bidirectional Relationship Conflicts', () => {
|
|
it('should handle bidirectional relationships correctly', async () => {
|
|
const nodeA = 'bidir-a'
|
|
const nodeB = 'bidir-b'
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'a-to-b',
|
|
sourceId: nodeA,
|
|
targetId: nodeB,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'b-to-a',
|
|
sourceId: nodeB,
|
|
targetId: nodeA,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
const neighborsA = await graphIndex.getNeighbors(nodeA, 'both')
|
|
const neighborsB = await graphIndex.getNeighbors(nodeB, 'both')
|
|
|
|
expect(neighborsA).toContain(nodeB)
|
|
expect(neighborsB).toContain(nodeA)
|
|
})
|
|
|
|
it('should handle conflicting bidirectional metadata', async () => {
|
|
const nodeA = 'conflict-bidir-a'
|
|
const nodeB = 'conflict-bidir-b'
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'a-to-b-conflict',
|
|
sourceId: nodeA,
|
|
targetId: nodeB,
|
|
type: VerbType.RelatesTo,
|
|
metadata: { direction: 'a-to-b', value: 1 }
|
|
})
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'b-to-a-conflict',
|
|
sourceId: nodeB,
|
|
targetId: nodeA,
|
|
type: VerbType.RelatesTo,
|
|
metadata: { direction: 'b-to-a', value: 2 }
|
|
})
|
|
|
|
const neighborsABoth = await graphIndex.getNeighbors(nodeA, 'both')
|
|
const neighborsBBoth = await graphIndex.getNeighbors(nodeB, 'both')
|
|
|
|
expect(neighborsABoth).toContain(nodeB)
|
|
expect(neighborsBBoth).toContain(nodeA)
|
|
})
|
|
})
|
|
|
|
describe('Complex Metadata Filtering with Nested Objects', () => {
|
|
beforeEach(async () => {
|
|
// Add test data with complex metadata
|
|
await brain.add({
|
|
data: 'Complex entity 1',
|
|
metadata: {
|
|
nested: { level1: { level2: 'value1' } },
|
|
array: ['item1', 'item2'],
|
|
number: 42,
|
|
boolean: true
|
|
},
|
|
type: NounType.Document
|
|
})
|
|
|
|
await brain.add({
|
|
data: 'Complex entity 2',
|
|
metadata: {
|
|
nested: { level1: { level2: 'value2' } },
|
|
array: ['item2', 'item3'],
|
|
number: 24,
|
|
boolean: false
|
|
},
|
|
type: NounType.Document
|
|
})
|
|
})
|
|
|
|
it('should handle nested object filtering', async () => {
|
|
const results = await brain.find({
|
|
where: { 'nested.level1.level2': 'value1' }
|
|
})
|
|
|
|
expect(results).toHaveLength(1)
|
|
expect(results[0].entity.metadata?.nested?.level1?.level2).toBe('value1')
|
|
})
|
|
|
|
it('should handle array contains filtering', async () => {
|
|
const results = await brain.find({
|
|
where: { array: { $contains: 'item2' } }
|
|
})
|
|
|
|
expect(results).toHaveLength(2)
|
|
})
|
|
|
|
it('should handle complex nested queries', async () => {
|
|
const results = await brain.find({
|
|
where: {
|
|
'nested.level1.level2': 'value1',
|
|
number: { $gte: 40 },
|
|
boolean: true
|
|
}
|
|
})
|
|
|
|
expect(results).toHaveLength(1)
|
|
})
|
|
|
|
it('should handle invalid nested paths gracefully', async () => {
|
|
const results = await brain.find({
|
|
where: { 'nonexistent.path': 'value' }
|
|
})
|
|
|
|
expect(results).toEqual([])
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============= DATA INTEGRITY EDGE CASES =============
|
|
|
|
describe('Data Integrity Edge Cases', () => {
|
|
describe('Corrupted Relationship Data', () => {
|
|
it('should handle relationships with missing source nodes', async () => {
|
|
// Add a relationship where source doesn't exist in main storage
|
|
await mockStorage.saveVerb({
|
|
id: 'orphan-verb',
|
|
sourceId: 'nonexistent-source',
|
|
targetId: 'existing-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
await graphIndex.rebuild()
|
|
|
|
const neighbors = await graphIndex.getNeighbors('nonexistent-source')
|
|
expect(neighbors).toEqual(['existing-target'])
|
|
})
|
|
|
|
it('should handle relationships with missing target nodes', async () => {
|
|
await mockStorage.saveVerb({
|
|
id: 'orphan-target-verb',
|
|
sourceId: 'existing-source',
|
|
targetId: 'nonexistent-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
await graphIndex.rebuild()
|
|
|
|
const neighbors = await graphIndex.getNeighbors('existing-source')
|
|
expect(neighbors).toEqual(['nonexistent-target'])
|
|
})
|
|
|
|
it('should handle malformed relationship data', async () => {
|
|
const malformedVerb = {
|
|
id: 'malformed',
|
|
sourceId: null, // Invalid
|
|
targetId: 'target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
} as any
|
|
|
|
await expect(graphIndex.addVerb(malformedVerb)).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('Inconsistent Relationship States', () => {
|
|
it('should handle duplicate relationship IDs', async () => {
|
|
const verb1: GraphVerb = {
|
|
id: 'duplicate-id',
|
|
sourceId: 'source1',
|
|
targetId: 'target1',
|
|
type: VerbType.RelatesTo,
|
|
metadata: { version: 1 }
|
|
}
|
|
|
|
const verb2: GraphVerb = {
|
|
id: 'duplicate-id', // Same ID
|
|
sourceId: 'source2',
|
|
targetId: 'target2',
|
|
type: VerbType.RelatesTo,
|
|
metadata: { version: 2 }
|
|
}
|
|
|
|
await graphIndex.addVerb(verb1)
|
|
await graphIndex.addVerb(verb2) // Should overwrite
|
|
|
|
const neighbors1 = await graphIndex.getNeighbors('source1')
|
|
const neighbors2 = await graphIndex.getNeighbors('source2')
|
|
|
|
// Only the second relationship should exist
|
|
expect(neighbors1).toEqual([])
|
|
expect(neighbors2).toEqual(['target2'])
|
|
})
|
|
|
|
it('should handle inconsistent bidirectional relationships', async () => {
|
|
// Add A->B but not B->A
|
|
await graphIndex.addVerb({
|
|
id: 'inconsistent-1',
|
|
sourceId: 'inconsistent-a',
|
|
targetId: 'inconsistent-b',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
const neighborsA = await graphIndex.getNeighbors('inconsistent-a', 'out')
|
|
const neighborsB = await graphIndex.getNeighbors('inconsistent-b', 'in')
|
|
|
|
expect(neighborsA).toContain('inconsistent-b')
|
|
expect(neighborsB).toEqual([]) // No incoming relationship
|
|
})
|
|
})
|
|
|
|
describe('Orphaned Relationships', () => {
|
|
it('should handle relationships where nodes are deleted', async () => {
|
|
// Add relationship
|
|
await graphIndex.addVerb({
|
|
id: 'orphan-test',
|
|
sourceId: 'orphan-source',
|
|
targetId: 'orphan-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
// Simulate node deletion by clearing the index
|
|
await graphIndex.rebuild()
|
|
|
|
const neighbors = await graphIndex.getNeighbors('orphan-source')
|
|
expect(neighbors).toEqual([])
|
|
})
|
|
|
|
it('should handle partial relationship cleanup', async () => {
|
|
await graphIndex.addVerb({
|
|
id: 'partial-1',
|
|
sourceId: 'partial-source',
|
|
targetId: 'partial-target-1',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'partial-2',
|
|
sourceId: 'partial-source',
|
|
targetId: 'partial-target-2',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
// Remove one relationship
|
|
await graphIndex.removeVerb('partial-1')
|
|
|
|
const neighbors = await graphIndex.getNeighbors('partial-source')
|
|
expect(neighbors).toEqual(['partial-target-2'])
|
|
expect(neighbors).not.toContain('partial-target-1')
|
|
})
|
|
})
|
|
|
|
describe('Duplicate Relationship Handling', () => {
|
|
it('should handle exact duplicate relationships', async () => {
|
|
const verb: GraphVerb = {
|
|
id: 'duplicate-1',
|
|
sourceId: 'dup-source',
|
|
targetId: 'dup-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: { duplicate: true }
|
|
}
|
|
|
|
await graphIndex.addVerb(verb)
|
|
await graphIndex.addVerb({ ...verb, id: 'duplicate-2' }) // Same content, different ID
|
|
|
|
const neighbors = await graphIndex.getNeighbors('dup-source')
|
|
expect(neighbors).toEqual(['dup-target'])
|
|
})
|
|
|
|
it('should handle duplicate relationships with different metadata', async () => {
|
|
await graphIndex.addVerb({
|
|
id: 'dup-meta-1',
|
|
sourceId: 'dup-meta-source',
|
|
targetId: 'dup-meta-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: { version: 1 }
|
|
})
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'dup-meta-2',
|
|
sourceId: 'dup-meta-source',
|
|
targetId: 'dup-meta-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: { version: 2 }
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors('dup-meta-source')
|
|
expect(neighbors).toEqual(['dup-meta-target'])
|
|
})
|
|
})
|
|
|
|
describe('Relationship Type Conflicts', () => {
|
|
it('should handle conflicting relationship types for same nodes', async () => {
|
|
await graphIndex.addVerb({
|
|
id: 'conflict-type-1',
|
|
sourceId: 'type-conflict-source',
|
|
targetId: 'type-conflict-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
await graphIndex.addVerb({
|
|
id: 'conflict-type-2',
|
|
sourceId: 'type-conflict-source',
|
|
targetId: 'type-conflict-target',
|
|
type: VerbType.Contains,
|
|
metadata: {}
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors('type-conflict-source')
|
|
expect(neighbors).toEqual(['type-conflict-target'])
|
|
})
|
|
|
|
it('should handle invalid relationship type transitions', async () => {
|
|
// This should work fine - GraphAdjacencyIndex doesn't validate relationship semantics
|
|
await graphIndex.addVerb({
|
|
id: 'type-transition',
|
|
sourceId: 'transition-source',
|
|
targetId: 'transition-target',
|
|
type: 'INVALID_TRANSITION' as VerbType,
|
|
metadata: {}
|
|
})
|
|
|
|
const neighbors = await graphIndex.getNeighbors('transition-source')
|
|
expect(neighbors).toEqual(['transition-target'])
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============= PERFORMANCE EDGE CASES =============
|
|
|
|
describe('Performance Edge Cases', () => {
|
|
describe('Memory Leaks Under Sustained Load', () => {
|
|
it('should not leak memory during sustained add/remove operations', async () => {
|
|
const initialMemory = graphIndex.getStats().memoryUsage
|
|
const operations = 1000
|
|
|
|
// Perform many add/remove operations
|
|
for (let i = 0; i < operations; i++) {
|
|
const verb: GraphVerb = {
|
|
id: `leak-test-${i}`,
|
|
sourceId: `leak-source-${i % 10}`,
|
|
targetId: `leak-target-${i % 10}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
}
|
|
|
|
await graphIndex.addVerb(verb)
|
|
|
|
if (i % 100 === 0) {
|
|
await graphIndex.removeVerb(`leak-test-${i - 50}`)
|
|
}
|
|
}
|
|
|
|
const finalMemory = graphIndex.getStats().memoryUsage
|
|
|
|
// Memory should not grow excessively
|
|
expect(finalMemory).toBeLessThan(initialMemory * 2)
|
|
})
|
|
|
|
it('should handle memory pressure during large rebuilds', async () => {
|
|
const largeVerbCount = 5000
|
|
const verbs: GraphVerb[] = []
|
|
|
|
// Create many verbs
|
|
for (let i = 0; i < largeVerbCount; i++) {
|
|
verbs.push({
|
|
id: `large-rebuild-${i}`,
|
|
sourceId: `large-source-${i % 100}`,
|
|
targetId: `large-target-${i % 100}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: { index: i }
|
|
})
|
|
}
|
|
|
|
// Add to storage
|
|
for (const verb of verbs) {
|
|
await mockStorage.saveVerb(verb)
|
|
}
|
|
|
|
// Measure rebuild time and memory
|
|
const startTime = Date.now()
|
|
const startMemory = graphIndex.getStats().memoryUsage
|
|
|
|
await graphIndex.rebuild()
|
|
|
|
const endTime = Date.now()
|
|
const endMemory = graphIndex.getStats().memoryUsage
|
|
|
|
expect(endTime - startTime).toBeLessThan(10000) // Should complete within 10 seconds
|
|
expect(graphIndex.getStats().totalRelationships).toBe(largeVerbCount)
|
|
})
|
|
})
|
|
|
|
describe('CPU Spikes During Complex Traversals', () => {
|
|
it('should handle CPU-intensive traversals without blocking', async () => {
|
|
// Create a dense graph
|
|
const nodeCount = 100
|
|
const verbs: GraphVerb[] = []
|
|
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
for (let j = 0; j < 10; j++) {
|
|
verbs.push({
|
|
id: `dense-${i}-${j}`,
|
|
sourceId: `dense-node-${i}`,
|
|
targetId: `dense-node-${(i + j) % nodeCount}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Add all verbs
|
|
for (const verb of verbs) {
|
|
await graphIndex.addVerb(verb)
|
|
}
|
|
|
|
// Perform traversals
|
|
const traversalPromises = Array(10).fill(null).map((_, i) =>
|
|
graphIndex.getNeighbors(`dense-node-${i}`, 'both')
|
|
)
|
|
|
|
const startTime = Date.now()
|
|
const results = await Promise.all(traversalPromises)
|
|
const endTime = Date.now()
|
|
|
|
expect(endTime - startTime).toBeLessThan(1000) // Should complete within 1 second
|
|
results.forEach(result => expect(result.length).toBeGreaterThan(0))
|
|
})
|
|
|
|
it('should handle recursive relationship patterns', async () => {
|
|
// Create a pattern that could cause recursive traversal issues
|
|
const patternSize = 50
|
|
|
|
for (let i = 0; i < patternSize; i++) {
|
|
await graphIndex.addVerb({
|
|
id: `recursive-${i}`,
|
|
sourceId: `recursive-${i}`,
|
|
targetId: `recursive-${(i + 1) % patternSize}`,
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
}
|
|
|
|
// This should not cause infinite loops or excessive CPU usage
|
|
const startTime = Date.now()
|
|
const neighbors = await graphIndex.getNeighbors('recursive-0', 'both')
|
|
const endTime = Date.now()
|
|
|
|
expect(endTime - startTime).toBeLessThan(100) // Should be very fast
|
|
expect(neighbors.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('Network Timeouts During Large Queries', () => {
|
|
it('should handle timeout during large dataset queries', async () => {
|
|
// Mock a slow storage operation
|
|
const slowGetVerbs = vi.fn().mockImplementation(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 200)) // 200ms delay
|
|
return { items: [], hasMore: false, nextCursor: null }
|
|
})
|
|
|
|
vi.spyOn(mockStorage, 'getVerbs').mockImplementation(slowGetVerbs)
|
|
|
|
const startTime = Date.now()
|
|
await expect(graphIndex.rebuild()).rejects.toThrow()
|
|
const endTime = Date.now()
|
|
|
|
expect(endTime - startTime).toBeGreaterThan(150) // Should take at least the delay time
|
|
})
|
|
|
|
it('should handle partial failures during large operations', async () => {
|
|
let callCount = 0
|
|
const failingGetVerbs = vi.fn().mockImplementation(async () => {
|
|
callCount++
|
|
if (callCount === 3) {
|
|
throw new Error('Simulated network failure')
|
|
}
|
|
return { items: [], hasMore: false, nextCursor: null }
|
|
})
|
|
|
|
vi.spyOn(mockStorage, 'getVerbs').mockImplementation(failingGetVerbs)
|
|
|
|
await expect(graphIndex.rebuild()).rejects.toThrow('Simulated network failure')
|
|
expect(callCount).toBe(3)
|
|
})
|
|
})
|
|
|
|
describe('Database Connection Failures', () => {
|
|
it('should handle connection failures during read operations', async () => {
|
|
mockStorage.setFailure('getVerbs')
|
|
|
|
await expect(graphIndex.rebuild()).rejects.toThrow('Storage adapter failure')
|
|
expect(graphIndex.isHealthy()).toBe(false)
|
|
})
|
|
|
|
it('should handle connection failures during write operations', async () => {
|
|
mockStorage.setFailure('saveVerb')
|
|
|
|
const verb: GraphVerb = {
|
|
id: 'connection-fail-verb',
|
|
sourceId: 'source',
|
|
targetId: 'target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
}
|
|
|
|
await expect(graphIndex.addVerb(verb)).rejects.toThrow('Storage adapter failure')
|
|
})
|
|
|
|
it('should recover from temporary connection failures', async () => {
|
|
// First fail
|
|
mockStorage.setFailure('saveVerb')
|
|
const verb: GraphVerb = {
|
|
id: 'recovery-test',
|
|
sourceId: 'recovery-source',
|
|
targetId: 'recovery-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
}
|
|
|
|
await expect(graphIndex.addVerb(verb)).rejects.toThrow()
|
|
|
|
// Then succeed
|
|
mockStorage.clearFailure()
|
|
await expect(graphIndex.addVerb(verb)).resolves.not.toThrow()
|
|
|
|
const neighbors = await graphIndex.getNeighbors('recovery-source')
|
|
expect(neighbors).toContain('recovery-target')
|
|
})
|
|
})
|
|
|
|
describe('Cache Invalidation Scenarios', () => {
|
|
it('should handle cache invalidation during concurrent operations', async () => {
|
|
// Add some initial data
|
|
await graphIndex.addVerb({
|
|
id: 'cache-test',
|
|
sourceId: 'cache-source',
|
|
targetId: 'cache-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
// Simulate cache invalidation by clearing and rebuilding
|
|
await graphIndex.rebuild()
|
|
|
|
const neighbors = await graphIndex.getNeighbors('cache-source')
|
|
expect(neighbors).toContain('cache-target')
|
|
})
|
|
|
|
it('should handle cache corruption scenarios', async () => {
|
|
// Add data
|
|
await graphIndex.addVerb({
|
|
id: 'corruption-test',
|
|
sourceId: 'corruption-source',
|
|
targetId: 'corruption-target',
|
|
type: VerbType.RelatesTo,
|
|
metadata: {}
|
|
})
|
|
|
|
// Simulate cache corruption by manually modifying internal state
|
|
const sourceIndex = (graphIndex as any).sourceIndex
|
|
sourceIndex.set('corruption-source', new Set(['corrupted-neighbor']))
|
|
|
|
// Rebuild should fix corruption
|
|
await graphIndex.rebuild()
|
|
|
|
const neighbors = await graphIndex.getNeighbors('corruption-source')
|
|
expect(neighbors).toContain('corruption-target')
|
|
expect(neighbors).not.toContain('corrupted-neighbor')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============= INTEGRATION EDGE CASES =============
|
|
|
|
describe('Integration Edge Cases', () => {
|
|
describe('Vector Search Failures in Unified Queries', () => {
|
|
it('should handle vector search failures gracefully', async () => {
|
|
// Mock vector search failure
|
|
const originalVectorSearch = (brain as any).vectorSearch
|
|
;(brain as any).vectorSearch = vi.fn().mockRejectedValue(new Error('Vector search failed'))
|
|
|
|
const results = await brain.find({
|
|
query: 'test query',
|
|
mode: 'hybrid'
|
|
})
|
|
|
|
// Should still return results from other search types
|
|
expect(Array.isArray(results)).toBe(true)
|
|
|
|
// Restore original method
|
|
;(brain as any).vectorSearch = originalVectorSearch
|
|
})
|
|
|
|
it('should fallback when vector search returns empty results', async () => {
|
|
const originalVectorSearch = (brain as any).vectorSearch
|
|
;(brain as any).vectorSearch = vi.fn().mockResolvedValue([])
|
|
|
|
const results = await brain.find({
|
|
query: 'test query',
|
|
mode: 'hybrid'
|
|
})
|
|
|
|
expect(Array.isArray(results)).toBe(true)
|
|
|
|
;(brain as any).vectorSearch = originalVectorSearch
|
|
})
|
|
})
|
|
|
|
describe('Graph Traversal Failures in Combined Searches', () => {
|
|
it('should handle graph traversal failures in unified queries', async () => {
|
|
// Mock graph search failure
|
|
const originalGraphSearch = (brain as any).graphSearch
|
|
;(brain as any).graphSearch = vi.fn().mockRejectedValue(new Error('Graph traversal failed'))
|
|
|
|
const results = await brain.find({
|
|
connected: { to: 'test-node' },
|
|
mode: 'hybrid'
|
|
})
|
|
|
|
expect(Array.isArray(results)).toBe(true)
|
|
|
|
;(brain as any).graphSearch = originalGraphSearch
|
|
})
|
|
|
|
it('should continue with other search types when graph search fails', async () => {
|
|
const originalGraphSearch = (brain as any).graphSearch
|
|
;(brain as any).graphSearch = vi.fn().mockRejectedValue(new Error('Graph traversal failed'))
|
|
|
|
// Add some test data for other search types
|
|
await brain.add({
|
|
data: 'Test document for fallback',
|
|
metadata: { category: 'test' },
|
|
type: NounType.Document
|
|
})
|
|
|
|
const results = await brain.find({
|
|
connected: { to: 'test-node' },
|
|
where: { category: 'test' },
|
|
mode: 'hybrid'
|
|
})
|
|
|
|
expect(results.length).toBeGreaterThan(0)
|
|
|
|
;(brain as any).graphSearch = originalGraphSearch
|
|
})
|
|
})
|
|
|
|
describe('Metadata Filtering Failures in Complex Queries', () => {
|
|
it('should handle metadata filtering failures gracefully', async () => {
|
|
const originalMetadataSearch = (brain as any).metadataSearch
|
|
;(brain as any).metadataSearch = vi.fn().mockRejectedValue(new Error('Metadata search failed'))
|
|
|
|
const results = await brain.find({
|
|
where: { category: 'test' },
|
|
mode: 'hybrid'
|
|
})
|
|
|
|
expect(Array.isArray(results)).toBe(true)
|
|
|
|
;(brain as any).metadataSearch = originalMetadataSearch
|
|
})
|
|
|
|
it('should handle invalid metadata filter syntax', async () => {
|
|
const results = await brain.find({
|
|
where: { $invalid: 'operator' } as any
|
|
})
|
|
|
|
expect(Array.isArray(results)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Fusion Ranking Edge Cases with Extreme Scores', () => {
|
|
it('should handle extreme similarity scores in fusion', async () => {
|
|
// Mock search results with extreme scores
|
|
const mockResults = [
|
|
[{ id: 'high-score', score: 0.99, entity: { id: 'high-score', vector: [1, 2, 3], metadata: {} } }],
|
|
[{ id: 'low-score', score: 0.01, entity: { id: 'low-score', vector: [4, 5, 6], metadata: {} } }],
|
|
[]
|
|
]
|
|
|
|
const originalVectorSearch = (brain as any).vectorSearch
|
|
const originalMetadataSearch = (brain as any).metadataSearch
|
|
const originalGraphSearch = (brain as any).graphSearch
|
|
|
|
;(brain as any).vectorSearch = vi.fn().mockResolvedValue(mockResults[0])
|
|
;(brain as any).metadataSearch = vi.fn().mockResolvedValue(mockResults[1])
|
|
;(brain as any).graphSearch = vi.fn().mockResolvedValue(mockResults[2])
|
|
|
|
const results = await brain.find({
|
|
query: 'test',
|
|
mode: 'hybrid'
|
|
})
|
|
|
|
expect(results.length).toBeGreaterThan(0)
|
|
expect(results[0].score).toBeGreaterThan(0)
|
|
expect(results[0].score).toBeLessThanOrEqual(1)
|
|
|
|
// Restore methods
|
|
;(brain as any).vectorSearch = originalVectorSearch
|
|
;(brain as any).metadataSearch = originalMetadataSearch
|
|
;(brain as any).graphSearch = originalGraphSearch
|
|
})
|
|
|
|
it('should handle NaN and infinite scores', async () => {
|
|
const mockResults = [
|
|
[{ id: 'nan-score', score: NaN, entity: { id: 'nan-score', vector: [1, 2, 3], metadata: {} } }],
|
|
[{ id: 'inf-score', score: Infinity, entity: { id: 'inf-score', vector: [4, 5, 6], metadata: {} } }],
|
|
[]
|
|
]
|
|
|
|
const originalVectorSearch = (brain as any).vectorSearch
|
|
const originalMetadataSearch = (brain as any).metadataSearch
|
|
const originalGraphSearch = (brain as any).graphSearch
|
|
|
|
;(brain as any).vectorSearch = vi.fn().mockResolvedValue(mockResults[0])
|
|
;(brain as any).metadataSearch = vi.fn().mockResolvedValue(mockResults[1])
|
|
;(brain as any).graphSearch = vi.fn().mockResolvedValue(mockResults[2])
|
|
|
|
const results = await brain.find({
|
|
query: 'test',
|
|
mode: 'hybrid'
|
|
})
|
|
|
|
expect(Array.isArray(results)).toBe(true)
|
|
|
|
;(brain as any).vectorSearch = originalVectorSearch
|
|
;(brain as any).metadataSearch = originalMetadataSearch
|
|
;(brain as any).graphSearch = originalGraphSearch
|
|
})
|
|
})
|
|
|
|
describe('Query Optimization Failures', () => {
|
|
it('should handle query optimization failures', async () => {
|
|
// Mock a failure in the reciprocal rank fusion
|
|
const originalFusion = (brain as any).reciprocalRankFusion
|
|
;(brain as any).reciprocalRankFusion = vi.fn().mockRejectedValue(new Error('Fusion failed'))
|
|
|
|
const results = await brain.find({
|
|
query: 'test query'
|
|
})
|
|
|
|
expect(Array.isArray(results)).toBe(true)
|
|
|
|
;(brain as any).reciprocalRankFusion = originalFusion
|
|
})
|
|
|
|
it('should handle empty search results from all search types', async () => {
|
|
const originalVectorSearch = (brain as any).vectorSearch
|
|
const originalMetadataSearch = (brain as any).metadataSearch
|
|
const originalGraphSearch = (brain as any).graphSearch
|
|
|
|
;(brain as any).vectorSearch = vi.fn().mockResolvedValue([])
|
|
;(brain as any).metadataSearch = vi.fn().mockResolvedValue([])
|
|
;(brain as any).graphSearch = vi.fn().mockResolvedValue([])
|
|
|
|
const results = await brain.find({
|
|
query: 'nonexistent',
|
|
where: { nonexistent: 'field' },
|
|
connected: { to: 'nonexistent' }
|
|
})
|
|
|
|
expect(results).toEqual([])
|
|
|
|
;(brain as any).vectorSearch = originalVectorSearch
|
|
;(brain as any).metadataSearch = originalMetadataSearch
|
|
;(brain as any).graphSearch = originalGraphSearch
|
|
})
|
|
|
|
it('should handle malformed query parameters', async () => {
|
|
const malformedQueries = [
|
|
{ query: null },
|
|
{ vector: 'not-an-array' },
|
|
{ limit: 'not-a-number' },
|
|
{ offset: -1 },
|
|
{ connected: { depth: -5 } }
|
|
]
|
|
|
|
for (const query of malformedQueries) {
|
|
const results = await brain.find(query as any)
|
|
expect(Array.isArray(results)).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
})
|