brainy/tests/unit/brainy/relate.test.ts

448 lines
No EOL
13 KiB
TypeScript

/**
* Unit tests for Brainy.relate() method
* Tests all aspects of creating relationships between entities
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/brainy'
import {
createAddParams,
createTestConfig,
} from '../../helpers/test-factory'
import {
assertCompletesWithin,
} from '../../helpers/test-assertions'
describe('Brainy.relate()', () => {
let brain: Brainy
let entity1Id: string
let entity2Id: string
let entity3Id: string
beforeEach(async () => {
brain = new Brainy(createTestConfig())
await brain.init()
// Create test entities for relationships
entity1Id = await brain.add(createAddParams({
data: 'Entity 1 - Person Alice',
type: 'person',
metadata: { name: 'Alice', role: 'developer' }
}))
entity2Id = await brain.add(createAddParams({
data: 'Entity 2 - Person Bob',
type: 'person',
metadata: { name: 'Bob', role: 'manager' }
}))
entity3Id = await brain.add(createAddParams({
data: 'Entity 3 - Project X',
type: 'project',
metadata: { name: 'Project X', status: 'active' }
}))
})
afterEach(async () => {
await brain.close()
})
describe('success paths', () => {
it('should create a relationship between two entities', async () => {
// Act
const relationId = await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'worksWith'
})
// Assert
expect(relationId).toBeDefined()
expect(typeof relationId).toBe('string')
// Verify relationship exists
const relations = await brain.related({ from: entity1Id })
expect(relations.length).toBeGreaterThan(0)
expect(relations.some(r => r.to === entity2Id)).toBe(true)
})
it('should create relationship with weight', async () => {
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'likes',
weight: 0.8
})
// Assert
const relations = await brain.related({ from: entity1Id })
const relation = relations.find(r => r.to === entity2Id)
expect(relation).toBeDefined()
expect(relation!.weight).toBe(0.8)
})
it('should create relationship with metadata', async () => {
// Arrange
const metadata = {
since: '2024-01-01',
strength: 'strong',
notes: 'Worked on multiple projects'
}
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'worksWith',
metadata
})
// Assert
const relations = await brain.related({ from: entity1Id })
const relation = relations.find(r => r.to === entity2Id)
expect(relation).toBeDefined()
expect(relation!.metadata || {}).toMatchObject(metadata)
})
it('should create bidirectional relationship when specified', async () => {
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'friendOf',
bidirectional: true
})
// Assert - Check both directions
const forwardRelations = await brain.related({ from: entity1Id })
const reverseRelations = await brain.related({ from: entity2Id })
expect(forwardRelations.some(r => r.to === entity2Id)).toBe(true)
expect(reverseRelations.some(r => r.to === entity1Id)).toBe(true)
})
it('should create multiple relationships from same entity', async () => {
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'worksWith'
})
await brain.relate({
from: entity1Id,
to: entity3Id,
type: 'creates'
})
// Assert
const relations = await brain.related({ from: entity1Id })
expect(relations.length).toBe(2)
expect(relations.some(r => r.to === entity2Id)).toBe(true)
expect(relations.some(r => r.to === entity3Id)).toBe(true)
})
it('should create different relationship types between same entities', async () => {
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'worksWith'
})
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'reportsTo'
})
// Assert
const relations = await brain.related({ from: entity1Id })
const toEntity2 = relations.filter(r => r.to === entity2Id)
expect(toEntity2.length).toBe(2)
expect(toEntity2.some(r => r.type === 'worksWith')).toBe(true)
expect(toEntity2.some(r => r.type === 'reportsTo')).toBe(true)
})
it('should handle self-relationships', async () => {
// Act
await brain.relate({
from: entity1Id,
to: entity1Id,
type: 'relatedTo',
metadata: { type: 'self-reference' }
})
// Assert
const relations = await brain.related({ from: entity1Id })
const selfRelation = relations.find(r => r.to === entity1Id)
expect(selfRelation).toBeDefined()
expect(selfRelation!.metadata?.type).toBe('self-reference')
})
})
describe('error paths', () => {
it('should handle relating non-existent entities', async () => {
// Arrange
const fakeId = 'non-existent-123'
// Act & Assert - Should handle gracefully or throw
await expect(brain.relate({
from: fakeId,
to: entity2Id,
type: 'relatedTo'
})).rejects.toThrow()
})
it('should handle invalid relationship type', async () => {
// Act & Assert - Invalid type should throw validation error
await expect(brain.relate({
from: entity1Id,
to: entity2Id,
type: 'invalidType' as any
})).rejects.toThrow('invalid VerbType')
})
it('should handle missing required parameters', async () => {
// Act & Assert
await expect(brain.relate({
from: '',
to: entity2Id,
type: 'relatedTo'
} as any)).rejects.toThrow()
await expect(brain.relate({
from: entity1Id,
to: '',
type: 'relatedTo'
} as any)).rejects.toThrow()
})
})
describe('edge cases', () => {
it('should handle duplicate relationships', async () => {
// Act - Create same relationship twice
const id1 = await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'worksWith',
weight: 0.5
})
const id2 = await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'worksWith',
weight: 0.8
})
// Assert - Should prevent duplicates (v3.43.2 bug fix)
// Second call should return existing relationship ID instead of creating duplicate
expect(id1).toBe(id2)
const relations = await brain.related({ from: entity1Id })
const matches = relations.filter(r =>
r.to === entity2Id && r.type === 'worksWith'
)
expect(matches.length).toBe(1) // Only one relationship should exist
})
it('should handle very long metadata', async () => {
// Arrange
const largeMetadata = {
bigArray: new Array(100).fill('item'),
bigObject: Object.fromEntries(
Array.from({ length: 50 }, (_, i) => [`key${i}`, `value${i}`])
),
longString: 'x'.repeat(1000)
}
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'relatedTo',
metadata: largeMetadata
})
// Assert
const relations = await brain.related({ from: entity1Id })
const relation = relations.find(r => r.to === entity2Id)
expect(relation).toBeDefined()
expect(relation!.metadata?.bigArray).toHaveLength(100)
})
it('should handle special characters in metadata', async () => {
// Arrange
const specialMetadata = {
emoji: '🚀🎉💻',
unicode: '你好世界',
special: '!@#$%^&*()',
newlines: 'line1\nline2\nline3'
}
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'relatedTo',
metadata: specialMetadata
})
// Assert
const relations = await brain.related({ from: entity1Id })
const relation = relations.find(r => r.to === entity2Id)
expect(relation!.metadata || {}).toMatchObject(specialMetadata)
})
})
describe('performance', () => {
it('should create relationships quickly', async () => {
// Act & Assert
await assertCompletesWithin(
() => brain.relate({
from: entity1Id,
to: entity2Id,
type: 'relatedTo'
}),
50, // Should complete within 50ms
'Relate operation'
)
})
it('should handle batch relationship creation efficiently', async () => {
// Arrange - Create more entities
const entityIds: string[] = []
for (let i = 0; i < 20; i++) {
const id = await brain.add(createAddParams({
data: `Entity ${i}`,
type: 'thing'
}))
entityIds.push(id)
}
// Act - Create relationships between all pairs
const start = performance.now()
const relates: Promise<string>[] = []
for (let i = 0; i < entityIds.length - 1; i++) {
for (let j = i + 1; j < entityIds.length; j++) {
relates.push(brain.relate({
from: entityIds[i],
to: entityIds[j],
type: 'relatedTo'
}))
}
}
await Promise.all(relates)
const duration = performance.now() - start
// Assert
const relationCount = (entityIds.length * (entityIds.length - 1)) / 2
const opsPerSecond = (relationCount / duration) * 1000
expect(opsPerSecond).toBeGreaterThan(100) // At least 100 relations/second
})
})
describe('consistency', () => {
it('should maintain relationship consistency', async () => {
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'worksWith',
metadata: { department: 'Engineering' }
})
// Assert - Multiple queries should return same data
const relations1 = await brain.related({ from: entity1Id })
const relations2 = await brain.related({ from: entity1Id })
expect(relations1.length).toBe(relations2.length)
const rel1 = relations1.find(r => r.to === entity2Id)
const rel2 = relations2.find(r => r.to === entity2Id)
expect(rel1).toBeDefined()
expect(rel2).toBeDefined()
expect(rel1!.type).toBe(rel2!.type)
expect(rel1!.metadata?.department).toBe('Engineering')
expect(rel2!.metadata?.department).toBe('Engineering')
})
it('should preserve relationships after entity updates', async () => {
// Arrange
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'worksWith'
})
// Act - Update an entity
await brain.update({
id: entity1Id,
metadata: { updated: true },
merge: true
})
// Assert - Relationship should still exist
const relations = await brain.related({ from: entity1Id })
expect(relations.some(r => r.to === entity2Id)).toBe(true)
})
})
describe('graph traversal', () => {
it('should support basic graph traversal', async () => {
// Arrange - Create a chain: entity1 -> entity2 -> entity3
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'precedes'
})
await brain.relate({
from: entity2Id,
to: entity3Id,
type: 'precedes'
})
// Act - Get relationships step by step
const step1 = await brain.related({ from: entity1Id })
const entity2Relations = await brain.related({ from: entity2Id })
// Assert
expect(step1.some(r => r.to === entity2Id)).toBe(true)
expect(entity2Relations.some(r => r.to === entity3Id)).toBe(true)
})
it('should handle circular relationships', async () => {
// Arrange - Create a cycle: entity1 -> entity2 -> entity3 -> entity1
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'relatedTo'
})
await brain.relate({
from: entity2Id,
to: entity3Id,
type: 'relatedTo'
})
await brain.relate({
from: entity3Id,
to: entity1Id,
type: 'relatedTo'
})
// Assert - All relationships should exist
const rel1 = await brain.related({ from: entity1Id })
const rel2 = await brain.related({ from: entity2Id })
const rel3 = await brain.related({ from: entity3Id })
expect(rel1.some(r => r.to === entity2Id)).toBe(true)
expect(rel2.some(r => r.to === entity3Id)).toBe(true)
expect(rel3.some(r => r.to === entity1Id)).toBe(true)
})
})
})