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
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.
463 lines
No EOL
14 KiB
TypeScript
463 lines
No EOL
14 KiB
TypeScript
/**
|
|
* Unit tests for Brainy.get() method
|
|
* Tests all aspects of retrieving entities from the neural database
|
|
*/
|
|
|
|
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,
|
|
generateTestVector,
|
|
createTestConfig,
|
|
} from '../../helpers/test-factory'
|
|
import {
|
|
assertCompletesWithin,
|
|
} from '../../helpers/test-assertions'
|
|
|
|
describe('Brainy.get()', () => {
|
|
let brain: Brainy
|
|
|
|
beforeEach(async () => {
|
|
brain = new Brainy(createTestConfig())
|
|
await brain.init()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await brain.close()
|
|
})
|
|
|
|
describe('success paths', () => {
|
|
it('should retrieve an existing entity by ID', async () => {
|
|
// Arrange
|
|
const params = createAddParams({
|
|
data: 'Test entity for retrieval',
|
|
type: 'document',
|
|
metadata: { title: 'Test Doc', version: 1 }
|
|
})
|
|
const id = await brain.add(params)
|
|
|
|
// Act
|
|
const entity = await brain.get(id)
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.id).toBe(id)
|
|
expect(entity!.type).toBe('document')
|
|
expect(entity!.metadata).toBeDefined()
|
|
})
|
|
|
|
it('should retrieve entity with correct vector', async () => {
|
|
// Arrange
|
|
const vector = generateTestVector()
|
|
const params = createAddParams({
|
|
vector,
|
|
type: 'thing',
|
|
metadata: { vectorized: true }
|
|
})
|
|
const id = await brain.add(params)
|
|
|
|
// Act - v5.11.1: Need includeVectors to get vector data
|
|
const entity = await brain.get(id, { includeVectors: true })
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.vector).toEqual(vector)
|
|
})
|
|
|
|
it('should retrieve entity with metadata intact', async () => {
|
|
// Arrange
|
|
const metadata = {
|
|
name: 'Test Item',
|
|
count: 42,
|
|
tags: ['test', 'unit'],
|
|
nested: { level: 1, data: 'value' }
|
|
}
|
|
const params = createAddParams({
|
|
data: 'Metadata test',
|
|
type: 'thing',
|
|
metadata
|
|
})
|
|
const id = await brain.add(params)
|
|
|
|
// Act
|
|
const entity = await brain.get(id)
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
// Check specific fields since metadata might be modified
|
|
expect(entity!.metadata.name).toBe(metadata.name)
|
|
expect(entity!.metadata.count).toBe(metadata.count)
|
|
expect(entity!.metadata.tags).toEqual(metadata.tags)
|
|
})
|
|
|
|
it('should retrieve entity with timestamps', async () => {
|
|
// Arrange
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Timestamp test',
|
|
type: 'thing'
|
|
}))
|
|
|
|
// Act
|
|
const entity = await brain.get(id)
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.createdAt).toBeDefined()
|
|
expect(entity!.createdAt).toBeTypeOf('number')
|
|
expect(entity!.createdAt).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should retrieve entity with service namespace', async () => {
|
|
// Arrange
|
|
const service = 'tenant-456'
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Service test',
|
|
type: 'thing',
|
|
service
|
|
}))
|
|
|
|
// Act
|
|
const entity = await brain.get(id)
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.service).toBe(service)
|
|
})
|
|
|
|
it('should retrieve multiple different entities correctly', async () => {
|
|
// Arrange
|
|
const ids: string[] = []
|
|
for (let i = 0; i < 5; i++) {
|
|
const id = await brain.add(createAddParams({
|
|
data: `Entity ${i}`,
|
|
type: 'thing',
|
|
metadata: { index: i }
|
|
}))
|
|
ids.push(id)
|
|
}
|
|
|
|
// Act & Assert
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const entity = await brain.get(ids[i])
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.id).toBe(ids[i])
|
|
expect(entity!.metadata.index).toBe(i)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('error paths', () => {
|
|
it('should return null for non-existent ID', async () => {
|
|
// v5.1.0: Use valid UUID format
|
|
const fakeId = '00000000-0000-0000-0000-999999999999'
|
|
|
|
// Act
|
|
const entity = await brain.get(fakeId)
|
|
|
|
// Assert
|
|
expect(entity).toBeNull()
|
|
})
|
|
|
|
it('should handle invalid ID formats gracefully', async () => {
|
|
// 8.0: any non-empty string is a valid custom id (sharding hashes it), so
|
|
// a lookup that misses resolves to null — there is no "invalid format".
|
|
const missingIds = [
|
|
'not-a-uuid', // valid custom id, simply absent
|
|
'12345' // valid custom id, simply absent
|
|
]
|
|
for (const id of missingIds) {
|
|
await expect(brain.get(id)).resolves.toBeNull()
|
|
}
|
|
|
|
// Structurally-invalid input (empty / null / undefined) still throws.
|
|
await expect(brain.get('')).rejects.toThrow()
|
|
await expect(brain.get(null as any)).rejects.toThrow()
|
|
await expect(brain.get(undefined as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle deleted entities', async () => {
|
|
// Arrange
|
|
const id = await brain.add(createAddParams({
|
|
data: 'To be deleted',
|
|
type: 'thing'
|
|
}))
|
|
await brain.remove(id)
|
|
|
|
// Act
|
|
const entity = await brain.get(id)
|
|
|
|
// Assert
|
|
expect(entity).toBeNull()
|
|
})
|
|
|
|
it('should handle concurrent get requests for same ID', async () => {
|
|
// Arrange
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Concurrent test',
|
|
type: 'thing',
|
|
metadata: { concurrent: true }
|
|
}))
|
|
|
|
// Act - Make 10 concurrent requests
|
|
const promises = Array(10).fill(null).map(() => brain.get(id))
|
|
const results = await Promise.all(promises)
|
|
|
|
// Assert - All should return the same entity
|
|
expect(results).toHaveLength(10)
|
|
results.forEach(entity => {
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.id).toBe(id)
|
|
expect(entity!.metadata.concurrent).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle very long IDs', async () => {
|
|
// 8.0: a long string is a valid custom id; a miss resolves to null.
|
|
const longId = 'x'.repeat(1000)
|
|
await expect(brain.get(longId)).resolves.toBeNull()
|
|
})
|
|
|
|
it('should handle special characters in IDs', async () => {
|
|
// v5.1.0: Use valid UUID format
|
|
const specialId = '00000000-0000-0000-0000-000000000004'
|
|
const params = createAddParams({
|
|
id: specialId,
|
|
data: 'Special ID test',
|
|
type: 'thing'
|
|
})
|
|
await brain.add(params)
|
|
|
|
// Act
|
|
const entity = await brain.get(specialId)
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.id).toBe(specialId)
|
|
})
|
|
|
|
it('should handle unicode characters in IDs', async () => {
|
|
// Arrange
|
|
const unicodeId = '00000000-0000-0000-0000-000000000003'
|
|
const params = createAddParams({
|
|
id: unicodeId,
|
|
data: 'Unicode ID test',
|
|
type: 'thing'
|
|
})
|
|
await brain.add(params)
|
|
|
|
// Act
|
|
const entity = await brain.get(unicodeId)
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.id).toBe(unicodeId)
|
|
})
|
|
|
|
it('should handle immediately getting after add', async () => {
|
|
// Arrange & Act
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Immediate get test',
|
|
type: 'thing'
|
|
}))
|
|
const entity = await brain.get(id)
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.id).toBe(id)
|
|
})
|
|
|
|
// THE INDEXABLE-ARRAY BOUND, from get()'s side. This case used to park a
|
|
// 1000-element array in the metadata bag and assert it came back. That
|
|
// shape is refused at the write door now — an array field mints one
|
|
// posting per element, so an unbounded array is an unbounded write — so
|
|
// the case pins BOTH halves of the law that replaced it: a large SCALAR
|
|
// payload still round-trips whole, and an array over the bound refuses by
|
|
// name. Every length derives from MAX_INDEXED_ARRAY_LENGTH so the pin
|
|
// follows the constant wherever it moves.
|
|
it('should get an entity with a large scalar metadata payload', async () => {
|
|
// Arrange — large in every dimension EXCEPT array length: a long string,
|
|
// many fields, deep nesting, 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: 100 }, (_, i) => [`key${i}`, `value${i}`])
|
|
),
|
|
longString: 'x'.repeat(10_000),
|
|
deepNesting: Array(10).fill(null).reduce(
|
|
(acc) => ({ nested: acc }),
|
|
{ value: 'deep' }
|
|
)
|
|
}
|
|
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Large metadata',
|
|
type: 'thing',
|
|
metadata: largeMetadata
|
|
}))
|
|
|
|
// Act
|
|
const entity = await brain.get(id)
|
|
|
|
// Assert — the payload comes back whole, first element to last
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.metadata.atTheBound).toHaveLength(MAX_INDEXED_ARRAY_LENGTH)
|
|
expect(entity!.metadata.atTheBound[0]).toBe('item0')
|
|
expect(entity!.metadata.atTheBound[MAX_INDEXED_ARRAY_LENGTH - 1])
|
|
.toBe(`item${MAX_INDEXED_ARRAY_LENGTH - 1}`)
|
|
expect(Object.keys(entity!.metadata.bigObject)).toHaveLength(100)
|
|
expect(entity!.metadata.longString).toHaveLength(10_000)
|
|
|
|
// ...including the deep nest, walked to the bottom.
|
|
let cursor: any = entity!.metadata.deepNesting
|
|
for (let depth = 0; depth < 10; depth++) cursor = cursor.nested
|
|
expect(cursor.value).toBe('deep')
|
|
})
|
|
|
|
it('should refuse a metadata array over the indexing bound, by name', async () => {
|
|
// Arrange
|
|
const overTheBound = MAX_INDEXED_ARRAY_LENGTH + 1
|
|
|
|
// Act
|
|
const err = await brain
|
|
.add(createAddParams({
|
|
data: 'Large metadata',
|
|
type: 'thing',
|
|
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))
|
|
})
|
|
})
|
|
|
|
describe('performance', () => {
|
|
it('should retrieve entities quickly', async () => {
|
|
// Arrange
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Performance test',
|
|
type: 'thing'
|
|
}))
|
|
|
|
// Act & Assert
|
|
await assertCompletesWithin(
|
|
() => brain.get(id),
|
|
50, // Should complete within 50ms
|
|
'Get operation'
|
|
)
|
|
})
|
|
|
|
it('should handle batch gets efficiently', async () => {
|
|
// Arrange - Add 100 entities
|
|
const ids: string[] = []
|
|
for (let i = 0; i < 100; i++) {
|
|
const id = await brain.add(createAddParams({
|
|
data: `Batch entity ${i}`,
|
|
type: 'thing'
|
|
}))
|
|
ids.push(id)
|
|
}
|
|
|
|
// Act - Get all entities
|
|
const start = performance.now()
|
|
const entities = await Promise.all(ids.map(id => brain.get(id)))
|
|
const duration = performance.now() - start
|
|
|
|
// Assert
|
|
expect(entities).toHaveLength(100)
|
|
entities.forEach(entity => {
|
|
expect(entity).not.toBeNull()
|
|
})
|
|
|
|
const opsPerSecond = (100 / duration) * 1000
|
|
expect(opsPerSecond).toBeGreaterThan(500) // At least 500 gets/second
|
|
})
|
|
|
|
it('should retrieve entities quickly on repeated access', async () => {
|
|
// Arrange
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Cache test',
|
|
type: 'thing',
|
|
metadata: { cached: true }
|
|
}))
|
|
|
|
// Act - First get (may hit storage)
|
|
const entity1 = await brain.get(id)
|
|
|
|
// Act - Second get (may benefit from caching)
|
|
const start = performance.now()
|
|
const entity2 = await brain.get(id)
|
|
const duration = performance.now() - start
|
|
|
|
// Assert - Entities should be equivalent (not necessarily same instance)
|
|
expect(entity1).not.toBeNull()
|
|
expect(entity2).not.toBeNull()
|
|
expect(entity1!.id).toBe(entity2!.id)
|
|
expect(entity1!.type).toBe(entity2!.type)
|
|
expect(entity1!.metadata.cached).toBe(true)
|
|
expect(entity2!.metadata.cached).toBe(true)
|
|
expect(duration).toBeLessThan(10) // Should be fast
|
|
})
|
|
})
|
|
|
|
describe('consistency', () => {
|
|
it('should return consistent data across multiple gets', async () => {
|
|
// Arrange
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Consistency test',
|
|
type: 'thing',
|
|
metadata: { version: 1, stable: true }
|
|
}))
|
|
|
|
// Act - Get the same entity 5 times
|
|
const entities: any[] = []
|
|
for (let i = 0; i < 5; i++) {
|
|
const entity = await brain.get(id)
|
|
entities.push(entity)
|
|
}
|
|
|
|
// Assert - All should have identical content (not necessarily same instance)
|
|
const first = entities[0]
|
|
expect(first).not.toBeNull()
|
|
entities.forEach(entity => {
|
|
expect(entity).not.toBeNull()
|
|
expect(entity.id).toBe(first.id)
|
|
expect(entity.type).toBe(first.type)
|
|
expect(entity.metadata.version).toBe(1)
|
|
expect(entity.metadata.stable).toBe(true)
|
|
expect(entity.createdAt).toBe(first.createdAt)
|
|
})
|
|
})
|
|
|
|
it('should reflect updates immediately', async () => {
|
|
// Arrange
|
|
const id = await brain.add(createAddParams({
|
|
data: 'Update test',
|
|
type: 'thing',
|
|
metadata: { version: 1 }
|
|
}))
|
|
|
|
// Act - Update and immediately get
|
|
await brain.update({
|
|
id,
|
|
metadata: { version: 2 },
|
|
merge: false
|
|
})
|
|
const entity = await brain.get(id)
|
|
|
|
// Assert
|
|
expect(entity).not.toBeNull()
|
|
expect(entity!.metadata.version).toBe(2)
|
|
})
|
|
})
|
|
}) |