This repository has been archived on 2026-09-03. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
open-brainy/tests/unit/brainy/relate.test.ts
David Snelling d7444ae804
Some checks are pending
CI / Node 24 (push) Waiting to run
CI / Node 22 (push) Waiting to run
CI / Integration + conformance (Node 22) (push) Waiting to run
CI / Bun (latest) (push) Waiting to run
test(metadata): the three large-metadata cases pin the bound, not a magic length
get(), relate() and update() each carried a "very large metadata" case that
parked an array of 1000 (or 100) elements in the metadata bag and asserted it
came back. The indexable-array bound refuses that shape at the write door now
— an array field mints one posting per element, so an unbounded array is an
unbounded write — and the three cases were failing on the refusal they should
have been pinning.

Each is rewritten to the law that replaced it, in two halves:

  - a large SCALAR payload still round-trips whole through the door: a
    10,000-character string, 100 sibling fields, a ten-deep nest walked to the
    bottom, and an array sitting exactly ON the bound, checked first element
    to last;
  - an array one element OVER the bound refuses with MetadataArrayTooLargeError
    carrying the field, the length and the bound, on the error object AND in
    the message. update()'s refusal additionally proves the row is unchanged,
    and relate()'s that no relation was written — refused means not written,
    not written-then-skipped.

Every length is derived from the imported MAX_INDEXED_ARRAY_LENGTH; none is
typed as a number. That is what made the old cases fragile: 100 read as "over
the bound" and 1000 as "large", and both meanings changed under them when the
constant moved. These follow the constant instead.
2026-09-02 15:41:21 -07:00

490 lines
No EOL
15 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 { MetadataArrayTooLargeError, MAX_INDEXED_ARRAY_LENGTH } from '../../../src/errors/brainyError'
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
})
// THE INDEXABLE-ARRAY BOUND, from relate()'s side. This case used to pass a
// 100-element array through relate() and assert it came back — a length
// hardcoded either side of a bound it never named, so it read green or red
// purely by where the constant happened to sit. Both halves of the law are
// pinned here instead, and every length derives from
// MAX_INDEXED_ARRAY_LENGTH so the pin follows the constant.
it('should handle a large scalar metadata payload on a relation', async () => {
// Arrange — large in every dimension EXCEPT array length: a long string,
// many fields, and an array sitting exactly ON the bound.
const largeMetadata = {
atTheBound: Array.from({ length: MAX_INDEXED_ARRAY_LENGTH }, (_, i) => `item${i}`),
bigObject: Object.fromEntries(
Array.from({ length: 50 }, (_, i) => [`key${i}`, `value${i}`])
),
longString: 'x'.repeat(10_000)
}
// Act
await brain.relate({
from: entity1Id,
to: entity2Id,
type: 'relatedTo',
metadata: largeMetadata
})
// Assert — the payload comes back whole, first element to last
const relations = await brain.related({ from: entity1Id })
const relation = relations.find(r => r.to === entity2Id)
expect(relation).toBeDefined()
expect(relation!.metadata?.atTheBound).toHaveLength(MAX_INDEXED_ARRAY_LENGTH)
expect(relation!.metadata?.atTheBound[0]).toBe('item0')
expect(relation!.metadata?.atTheBound[MAX_INDEXED_ARRAY_LENGTH - 1])
.toBe(`item${MAX_INDEXED_ARRAY_LENGTH - 1}`)
expect(Object.keys(relation!.metadata?.bigObject)).toHaveLength(50)
expect(relation!.metadata?.longString).toHaveLength(10_000)
})
it('should refuse a relation metadata array over the indexing bound, by name', async () => {
// Arrange
const overTheBound = MAX_INDEXED_ARRAY_LENGTH + 1
// Act
const err = await brain
.relate({
from: entity1Id,
to: entity3Id,
type: 'relatedTo',
metadata: { bigArray: new Array(overTheBound).fill('item') }
})
.catch((e: any) => e)
// Assert — the field, the length and the bound, on the error and in the
// message, so a handler can report or repair without parsing prose.
expect(err).toBeInstanceOf(MetadataArrayTooLargeError)
expect(err.field).toBe('bigArray')
expect(err.length).toBe(overTheBound)
expect(err.limit).toBe(MAX_INDEXED_ARRAY_LENGTH)
expect(err.message).toContain('bigArray')
expect(err.message).toContain(String(overTheBound))
expect(err.message).toContain(String(MAX_INDEXED_ARRAY_LENGTH))
// Refused means not written: no relation of this shape exists.
const relations = await brain.related({ from: entity1Id })
expect(relations.some(r => r.to === entity3Id && r.metadata?.bigArray)).toBe(false)
})
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)
})
})
})