8.0 RC cleanup toward "one place per thing, zero-config, no deprecation":
- Remove the `brain.neural()` clustering namespace (ImprovedNeuralAPI + the dead
legacy NeuralAPI + the neural CLI + neural-only types). Similarity is `find({vector})`
/ `similar({to})`; attribute grouping is the aggregation `GROUP BY` engine. The separate
entity-extraction / smart-import feature (NeuralImport, NeuralEntityExtractor, SmartExtractor,
NaturalLanguageProcessor, `brain.extract()`/`brain.nlp()`) is kept.
- Remove `Db.search()`; `find()` is the one query verb (accepts a bare string or FindParams).
Fix the bundled MCP client, which called a non-existent `brain.search(query, limit)` →
now `find({ query, limit })`.
- Storage config: collapse to one canonical top-level `path` key. The pre-8.0 aliases
(`rootDirectory`, `options.*`, `fileSystemStorage.*`) are removed and now THROW with the
exact rename instead of silently defaulting to `./brainy-data` on upgrade. A single resolver
feeds createStorage, the 7.x→8.0 migration probe, and the plugin-factory handoff, so a native
storage provider resolves the identical root (no split-brain).
- Fix `similar({ threshold })`: the min-similarity filter was silently dropped; it is now
applied as a post-filter on `result.score` (the documented way to bound semantic results).
- Fix `vfs.rename()` on a directory: child path updates spread the entity vector into `update()`
and failed dimension validation; they are metadata-only updates now.
- Fix `vfs.move()`: copy+delete orphaned the content-addressed content blob (the destination
shared the source hash, then unlink removed it). `move()` now delegates to `rename()` — an
in-place path change that preserves the blob and the entity id, for files and directories.
- Fix streaming import: the bulk fast path never flushed mid-import nor signalled queryability.
Entity writes are now chunked by a progressive flush interval (100 → 1000 → 5000); each chunk
flushes and emits `progress.queryable`, so imported data is queryable during the import.
- Sweep all docs, comments, and JSDoc for the removed/changed APIs.
Integration suite: 49 files / 588 passed / 0 failed. Unit: 80 files / 1456 passed, no type errors.
737 lines
23 KiB
TypeScript
737 lines
23 KiB
TypeScript
/**
|
|
* Comprehensive Public API Test Suite
|
|
*
|
|
* Validates all public API methods exposed by Brainy,
|
|
* ensuring complete coverage of documented functionality.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'
|
|
import { Brainy } from '../../src/brainy'
|
|
import { NounType, VerbType } from '../../src/types/graphTypes'
|
|
import type { Entity, Result } from '../../src/types/brainy.types'
|
|
import * as fs from 'fs/promises'
|
|
import * as path from 'path'
|
|
import { tmpdir } from 'os'
|
|
import { randomUUID } from 'crypto'
|
|
|
|
describe('Brainy Public API - Complete Coverage', () => {
|
|
let brain: Brainy
|
|
let testDir: string
|
|
|
|
beforeAll(async () => {
|
|
testDir = path.join(tmpdir(), `brainy-test-${Date.now()}`)
|
|
await fs.mkdir(testDir, { recursive: true })
|
|
})
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await fs.rm(testDir, { recursive: true, force: true })
|
|
} catch (error) {
|
|
console.warn('Failed to cleanup test directory:', error)
|
|
}
|
|
})
|
|
|
|
describe('Core CRUD Operations', () => {
|
|
beforeEach(async () => {
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await brain.close()
|
|
})
|
|
|
|
describe('brain.add()', () => {
|
|
it('should handle all noun types', async () => {
|
|
const nounTypes = Object.values(NounType)
|
|
|
|
for (const nounType of nounTypes) {
|
|
const id = await brain.add({
|
|
data: `Test ${nounType}`,
|
|
type: nounType,
|
|
metadata: { nounType }
|
|
})
|
|
|
|
expect(id).toBeDefined()
|
|
expect(typeof id).toBe('string')
|
|
|
|
const entity = await brain.get(id)
|
|
expect(entity).toBeDefined()
|
|
expect(entity?.type).toBe(nounType)
|
|
}
|
|
}, 120000)
|
|
|
|
it('should validate required fields', async () => {
|
|
await expect(brain.add({
|
|
type: NounType.Document
|
|
} as any)).rejects.toThrow()
|
|
|
|
await expect(brain.add({
|
|
data: 'test'
|
|
} as any)).rejects.toThrow()
|
|
})
|
|
|
|
it('should handle very large data', async () => {
|
|
const largeText = 'x'.repeat(1_000_000)
|
|
const id = await brain.add({
|
|
data: largeText,
|
|
type: NounType.Document
|
|
})
|
|
|
|
const entity = await brain.get(id)
|
|
expect(entity?.data).toBe(largeText)
|
|
})
|
|
})
|
|
|
|
describe('brain.addMany()', () => {
|
|
it('should handle batch operations', async () => {
|
|
const items = Array.from({ length: 10 }, (_, i) => ({
|
|
data: `Batch item ${i}`,
|
|
type: NounType.Document,
|
|
metadata: { index: i }
|
|
}))
|
|
|
|
const result = await brain.addMany({ items })
|
|
expect(result.successful).toHaveLength(10)
|
|
}, 120000)
|
|
|
|
it('should handle partial failures', async () => {
|
|
const items = [
|
|
{ data: 'Valid 1', type: NounType.Document },
|
|
{ data: null as any, type: NounType.Document },
|
|
{ data: 'Valid 2', type: NounType.Document }
|
|
]
|
|
|
|
try {
|
|
const result = await brain.addMany({ items })
|
|
expect(result.successful.length).toBeGreaterThan(0)
|
|
} catch (error) {
|
|
expect(error).toBeDefined()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('brain.update()', () => {
|
|
it('should handle concurrent updates', async () => {
|
|
const id = await brain.add({
|
|
data: 'Initial',
|
|
type: NounType.Document,
|
|
metadata: { version: 1 }
|
|
})
|
|
|
|
const updates = Array.from({ length: 10 }, (_, i) =>
|
|
brain.update({
|
|
id,
|
|
metadata: { version: i + 2, updatedBy: `thread-${i}` }
|
|
})
|
|
)
|
|
|
|
await Promise.all(updates)
|
|
|
|
const final = await brain.get(id)
|
|
expect(final).toBeDefined()
|
|
expect(final?.metadata?.version).toBeGreaterThan(1)
|
|
})
|
|
|
|
it('should reject update of a non-existent entity', async () => {
|
|
const fakeId = randomUUID()
|
|
await expect(
|
|
brain.update({ id: fakeId, metadata: { test: true } })
|
|
).rejects.toThrow(/not found/)
|
|
})
|
|
|
|
it('should preserve unmodified fields', async () => {
|
|
const id = await brain.add({
|
|
data: 'Test',
|
|
type: NounType.Document,
|
|
metadata: {
|
|
field1: 'value1',
|
|
field2: 'value2',
|
|
nested: { a: 1, b: 2 }
|
|
}
|
|
})
|
|
|
|
await brain.update({
|
|
id,
|
|
metadata: { field1: 'updated' }
|
|
})
|
|
|
|
const updated = await brain.get(id)
|
|
expect(updated?.metadata?.field1).toBe('updated')
|
|
expect(updated?.metadata?.field2).toBe('value2')
|
|
expect(updated?.metadata?.nested).toEqual({ a: 1, b: 2 })
|
|
})
|
|
})
|
|
|
|
describe('brain.updateMany()', () => {
|
|
it('should batch update multiple entities', async () => {
|
|
const result = await brain.addMany({
|
|
items: [
|
|
{ data: 'Item 1', type: NounType.Document },
|
|
{ data: 'Item 2', type: NounType.Document },
|
|
{ data: 'Item 3', type: NounType.Document }
|
|
]
|
|
})
|
|
|
|
const ids = result.successful
|
|
const updates = ids.map(id => ({
|
|
id,
|
|
metadata: { updated: true }
|
|
}))
|
|
|
|
const updateResult = await brain.updateMany({ items: updates })
|
|
expect(updateResult).toBeDefined()
|
|
|
|
for (const id of ids) {
|
|
const entity = await brain.get(id)
|
|
expect(entity?.metadata?.updated).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('brain.remove()', () => {
|
|
it('should handle cascade deletion of relationships', async () => {
|
|
const id1 = await brain.add({ data: 'Entity 1', type: NounType.Person })
|
|
const id2 = await brain.add({ data: 'Entity 2', type: NounType.Organization })
|
|
|
|
await brain.relate({ from: id1, to: id2, type: VerbType.WorksWith })
|
|
|
|
await brain.remove(id1)
|
|
|
|
const relations = await brain.related(id2)
|
|
expect(relations.filter(r => r.from === id1 || r.to === id1)).toHaveLength(0)
|
|
})
|
|
|
|
it('should treat deletion of a non-existent entity as a no-op', async () => {
|
|
const fakeId = randomUUID()
|
|
// delete() is idempotent: unknown IDs resolve without throwing
|
|
await expect(brain.remove(fakeId)).resolves.toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('brain.removeMany()', () => {
|
|
it('should efficiently delete batches', async () => {
|
|
const result = await brain.addMany({
|
|
items: Array.from({ length: 5 }, (_, i) => ({
|
|
data: `Item ${i}`,
|
|
type: NounType.Document
|
|
}))
|
|
})
|
|
|
|
const ids = result.successful
|
|
await brain.removeMany({ ids })
|
|
|
|
for (const id of ids) {
|
|
const entity = await brain.get(id)
|
|
expect(entity).toBeNull()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Relationship Operations', () => {
|
|
beforeEach(async () => {
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await brain.close()
|
|
})
|
|
|
|
describe('brain.relate()', () => {
|
|
it('should create relationships with multiple verb types', async () => {
|
|
const id1 = await brain.add({ data: 'Source', type: NounType.Person })
|
|
const id2 = await brain.add({ data: 'Target', type: NounType.Organization })
|
|
|
|
// Test a representative subset of verb types (not all 127)
|
|
const testVerbs = [
|
|
VerbType.RelatedTo, VerbType.Creates, VerbType.References,
|
|
VerbType.WorksWith, VerbType.DependsOn, VerbType.Contains,
|
|
VerbType.Requires, VerbType.FriendOf
|
|
]
|
|
|
|
for (const verbType of testVerbs) {
|
|
const relationId = await brain.relate({
|
|
from: id1,
|
|
to: id2,
|
|
type: verbType,
|
|
metadata: { verbType }
|
|
})
|
|
|
|
expect(relationId).toBeDefined()
|
|
expect(typeof relationId).toBe('string')
|
|
}
|
|
})
|
|
|
|
it('should handle bidirectional relationships', async () => {
|
|
const person1 = await brain.add({ data: 'Alice', type: NounType.Person })
|
|
const person2 = await brain.add({ data: 'Bob', type: NounType.Person })
|
|
|
|
await brain.relate({
|
|
from: person1,
|
|
to: person2,
|
|
type: VerbType.FriendOf,
|
|
bidirectional: true
|
|
})
|
|
|
|
const relations1 = await brain.related(person1)
|
|
const relations2 = await brain.related(person2)
|
|
|
|
expect(relations1.some(r => r.to === person2)).toBe(true)
|
|
expect(relations2.some(r => r.to === person1)).toBe(true)
|
|
})
|
|
|
|
it('should allow duplicate relationships', async () => {
|
|
const id1 = await brain.add({ data: 'A', type: NounType.Document })
|
|
const id2 = await brain.add({ data: 'B', type: NounType.Document })
|
|
|
|
await brain.relate({ from: id1, to: id2, type: VerbType.References })
|
|
await brain.relate({ from: id1, to: id2, type: VerbType.References })
|
|
|
|
const relations = await brain.related(id1)
|
|
const referenceRelations = relations.filter(
|
|
r => r.type === VerbType.References && r.to === id2
|
|
)
|
|
|
|
expect(referenceRelations.length).toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
it('should handle relationship metadata and weights', async () => {
|
|
const id1 = await brain.add({ data: 'Source', type: NounType.Document })
|
|
const id2 = await brain.add({ data: 'Target', type: NounType.Document })
|
|
|
|
const relationId = await brain.relate({
|
|
from: id1,
|
|
to: id2,
|
|
type: VerbType.References,
|
|
weight: 0.8,
|
|
metadata: {
|
|
context: 'academic',
|
|
verified: true
|
|
}
|
|
})
|
|
|
|
const relations = await brain.related(id1)
|
|
const relation = relations.find(r => r.id === relationId)
|
|
|
|
expect(relation).toBeDefined()
|
|
expect(relation?.metadata?.context).toBe('academic')
|
|
})
|
|
})
|
|
|
|
describe('brain.relateMany()', () => {
|
|
it('should create multiple relationships efficiently', async () => {
|
|
const result = await brain.addMany({
|
|
items: Array.from({ length: 5 }, (_, i) => ({
|
|
data: `Entity ${i}`,
|
|
type: NounType.Document
|
|
}))
|
|
})
|
|
|
|
const entities = result.successful
|
|
|
|
const items = []
|
|
for (let i = 0; i < entities.length - 1; i++) {
|
|
items.push({
|
|
from: entities[i],
|
|
to: entities[i + 1],
|
|
type: VerbType.Precedes
|
|
})
|
|
}
|
|
|
|
const relationIds = await brain.relateMany({ items })
|
|
expect(relationIds).toHaveLength(items.length)
|
|
})
|
|
})
|
|
|
|
describe('brain.related()', () => {
|
|
it('should retrieve outgoing relationships', async () => {
|
|
const center = await brain.add({ data: 'Center', type: NounType.Person })
|
|
const related1 = await brain.add({ data: 'Related1', type: NounType.Organization })
|
|
const related2 = await brain.add({ data: 'Related2', type: NounType.Document })
|
|
|
|
await brain.relate({ from: center, to: related1, type: VerbType.WorksWith })
|
|
await brain.relate({ from: center, to: related2, type: VerbType.Creates })
|
|
|
|
const outgoing = await brain.related({ from: center })
|
|
|
|
expect(outgoing).toHaveLength(2)
|
|
expect(outgoing.some(r => r.type === VerbType.WorksWith)).toBe(true)
|
|
expect(outgoing.some(r => r.type === VerbType.Creates)).toBe(true)
|
|
})
|
|
|
|
it('should retrieve incoming relationships', async () => {
|
|
const center = await brain.add({ data: 'Center', type: NounType.Person })
|
|
const source = await brain.add({ data: 'Source', type: NounType.Task })
|
|
|
|
await brain.relate({ from: source, to: center, type: VerbType.DependsOn })
|
|
|
|
const incoming = await brain.related({ to: center })
|
|
|
|
expect(incoming).toHaveLength(1)
|
|
expect(incoming[0].type).toBe(VerbType.DependsOn)
|
|
})
|
|
|
|
it('should filter by relationship direction', async () => {
|
|
const center = await brain.add({ data: 'Center', type: NounType.Document })
|
|
const source = await brain.add({ data: 'Source', type: NounType.Person })
|
|
const target = await brain.add({ data: 'Target', type: NounType.Task })
|
|
|
|
await brain.relate({ from: source, to: center, type: VerbType.Creates })
|
|
await brain.relate({ from: center, to: target, type: VerbType.Requires })
|
|
|
|
const outgoing = await brain.related({ from: center })
|
|
const incoming = await brain.related({ to: center })
|
|
|
|
expect(outgoing).toHaveLength(1)
|
|
expect(outgoing[0].to).toBe(target)
|
|
|
|
expect(incoming).toHaveLength(1)
|
|
expect(incoming[0].from).toBe(source)
|
|
})
|
|
|
|
it('should filter by verb type', async () => {
|
|
const doc = await brain.add({ data: 'Document', type: NounType.Document })
|
|
const ref1 = await brain.add({ data: 'Reference1', type: NounType.Document })
|
|
const ref2 = await brain.add({ data: 'Reference2', type: NounType.Document })
|
|
const author = await brain.add({ data: 'Author', type: NounType.Person })
|
|
|
|
await brain.relate({ from: doc, to: ref1, type: VerbType.References })
|
|
await brain.relate({ from: doc, to: ref2, type: VerbType.References })
|
|
await brain.relate({ from: author, to: doc, type: VerbType.Creates })
|
|
|
|
const references = await brain.related({
|
|
from: doc,
|
|
type: VerbType.References
|
|
})
|
|
|
|
expect(references).toHaveLength(2)
|
|
expect(references.every(r => r.type === VerbType.References)).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Search Operations', () => {
|
|
beforeEach(async () => {
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
|
|
await brain.addMany({
|
|
items: [
|
|
{ data: 'The quick brown fox', type: NounType.Document, metadata: { category: 'animals' } },
|
|
{ data: 'jumps over the lazy dog', type: NounType.Document, metadata: { category: 'animals' } },
|
|
{ data: 'Machine learning algorithms', type: NounType.Document, metadata: { category: 'tech' } },
|
|
{ data: 'Deep neural networks', type: NounType.Document, metadata: { category: 'tech' } },
|
|
{ data: 'Natural language processing', type: NounType.Document, metadata: { category: 'tech' } }
|
|
]
|
|
})
|
|
}, 120000)
|
|
|
|
afterEach(async () => {
|
|
await brain.close()
|
|
})
|
|
|
|
describe('brain.find()', () => {
|
|
it('should support metadata search', async () => {
|
|
const results = await brain.find({
|
|
where: { category: 'tech' },
|
|
limit: 5
|
|
})
|
|
|
|
expect(results).toBeDefined()
|
|
expect(Array.isArray(results)).toBe(true)
|
|
expect(results.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should handle metadata filters', async () => {
|
|
const results = await brain.find({
|
|
where: { category: 'tech' },
|
|
limit: 10
|
|
})
|
|
|
|
expect(results.length).toBeGreaterThan(0)
|
|
expect(results.every(r => r.entity.metadata?.category === 'tech')).toBe(true)
|
|
})
|
|
|
|
it('should support graph-connected searches', async () => {
|
|
const doc1 = await brain.add({ data: 'Primary document', type: NounType.Document })
|
|
const doc2 = await brain.add({ data: 'Related document', type: NounType.Document })
|
|
await brain.relate({ from: doc1, to: doc2, type: VerbType.References })
|
|
|
|
const results = await brain.find({
|
|
query: 'document',
|
|
connected: { from: doc1 },
|
|
limit: 5
|
|
})
|
|
|
|
expect(results.some(r => r.entity.id === doc2)).toBe(true)
|
|
})
|
|
|
|
it('should support pagination', async () => {
|
|
const page1 = await brain.find({
|
|
where: { category: 'tech' },
|
|
limit: 2,
|
|
offset: 0
|
|
})
|
|
|
|
const page2 = await brain.find({
|
|
where: { category: 'tech' },
|
|
limit: 2,
|
|
offset: 2
|
|
})
|
|
|
|
if (page1.length > 0 && page2.length > 0) {
|
|
expect(page1[0]?.entity.id).not.toBe(page2[0]?.entity.id)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('brain.similar()', () => {
|
|
it('should find similar entities', async () => {
|
|
const reference = await brain.add({
|
|
data: 'Artificial intelligence and machine learning',
|
|
type: NounType.Document
|
|
})
|
|
|
|
const similar = await brain.similar({
|
|
to: reference,
|
|
limit: 5
|
|
})
|
|
|
|
expect(similar).toBeDefined()
|
|
expect(similar.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should return results sorted by score', async () => {
|
|
const reference = await brain.add({
|
|
data: 'Deep learning and neural networks research',
|
|
type: NounType.Document
|
|
})
|
|
|
|
const similar = await brain.similar({
|
|
to: reference,
|
|
limit: 10
|
|
})
|
|
|
|
// Results should be sorted by score (descending)
|
|
if (similar.length > 1) {
|
|
for (let i = 1; i < similar.length; i++) {
|
|
expect(similar[i - 1].score).toBeGreaterThanOrEqual(similar[i].score - 0.001)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Statistics', () => {
|
|
beforeEach(async () => {
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await brain.close()
|
|
})
|
|
|
|
describe('brain.getStats()', () => {
|
|
it('should return statistics', async () => {
|
|
await brain.addMany({
|
|
items: [
|
|
{ data: 'Item 1', type: NounType.Document },
|
|
{ data: 'Item 2', type: NounType.Person },
|
|
{ data: 'Item 3', type: NounType.Task }
|
|
]
|
|
})
|
|
|
|
const stats = await brain.getStats()
|
|
|
|
expect(stats).toBeDefined()
|
|
// Stats returns { entities: { total, byType }, relationships, density }
|
|
expect(stats.entities).toBeDefined()
|
|
expect(stats.entities.total).toBeGreaterThanOrEqual(3)
|
|
expect(stats.entities.byType).toBeDefined()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('FileSystem Storage', () => {
|
|
it('should handle basic CRUD with filesystem', async () => {
|
|
const fsTestDir = path.join(tmpdir(), `brainy-fs-crud-${Date.now()}`)
|
|
await fs.mkdir(fsTestDir, { recursive: true })
|
|
|
|
const fsBrain = new Brainy({ requireSubtype: false,
|
|
storage: {
|
|
type: 'filesystem',
|
|
path: fsTestDir
|
|
}
|
|
})
|
|
|
|
await fsBrain.init()
|
|
|
|
const id = await fsBrain.add({
|
|
data: 'Filesystem test',
|
|
type: NounType.Document
|
|
})
|
|
|
|
const retrieved = await fsBrain.get(id)
|
|
expect(retrieved?.data).toBe('Filesystem test')
|
|
|
|
await fsBrain.update({ id, metadata: { updated: true } })
|
|
|
|
const updated = await fsBrain.get(id)
|
|
expect(updated?.metadata?.updated).toBe(true)
|
|
|
|
await fsBrain.remove(id)
|
|
const deleted = await fsBrain.get(id)
|
|
expect(deleted).toBeNull()
|
|
|
|
await fsBrain.close()
|
|
await fs.rm(fsTestDir, { recursive: true, force: true }).catch(() => {})
|
|
})
|
|
|
|
it('should handle concurrent filesystem operations', async () => {
|
|
const fsTestDir = path.join(tmpdir(), `brainy-fs-concurrent-${Date.now()}`)
|
|
await fs.mkdir(fsTestDir, { recursive: true })
|
|
|
|
const fsBrain = new Brainy({ requireSubtype: false,
|
|
storage: {
|
|
type: 'filesystem',
|
|
path: fsTestDir
|
|
}
|
|
})
|
|
|
|
await fsBrain.init()
|
|
|
|
const operations = Array.from({ length: 5 }, async (_, i) => {
|
|
const id = await fsBrain.add({
|
|
data: `Concurrent ${i}`,
|
|
type: NounType.Document
|
|
})
|
|
return id
|
|
})
|
|
|
|
const ids = await Promise.all(operations)
|
|
expect(ids).toHaveLength(5)
|
|
expect(new Set(ids).size).toBe(5)
|
|
|
|
await fsBrain.close()
|
|
await fs.rm(fsTestDir, { recursive: true, force: true }).catch(() => {})
|
|
})
|
|
})
|
|
|
|
describe('Error Recovery and Resilience', () => {
|
|
beforeEach(async () => {
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await brain.close()
|
|
})
|
|
|
|
it('should handle and recover from transient errors', async () => {
|
|
const originalAdd = brain.add.bind(brain)
|
|
let failCount = 0
|
|
brain.add = vi.fn(async (...args) => {
|
|
if (failCount++ < 2) {
|
|
throw new Error('Storage temporarily unavailable')
|
|
}
|
|
return originalAdd(...args)
|
|
})
|
|
|
|
let succeeded = false
|
|
for (let i = 0; i < 3; i++) {
|
|
try {
|
|
await brain.add({
|
|
data: 'Test',
|
|
type: NounType.Document
|
|
})
|
|
succeeded = true
|
|
break
|
|
} catch (error) {
|
|
// Expected for first attempts
|
|
}
|
|
}
|
|
|
|
expect(succeeded).toBe(true)
|
|
})
|
|
|
|
it('should handle malformed input gracefully', async () => {
|
|
const malformedInputs = [
|
|
null,
|
|
undefined,
|
|
{},
|
|
{ data: null },
|
|
{ type: 'invalid' },
|
|
]
|
|
|
|
for (const input of malformedInputs) {
|
|
try {
|
|
await brain.add(input as any)
|
|
} catch (error) {
|
|
expect(error).toBeDefined()
|
|
expect((error as Error).message).toBeDefined()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Performance', () => {
|
|
it('should handle moderate-scale operations', async () => {
|
|
const perfBrain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await perfBrain.init()
|
|
|
|
// Add 50 items in batches
|
|
const items = Array.from({ length: 50 }, (_, i) => ({
|
|
data: `Performance item ${i}`,
|
|
type: NounType.Document,
|
|
metadata: { batch: Math.floor(i / 10), index: i }
|
|
}))
|
|
|
|
const result = await perfBrain.addMany({ items })
|
|
expect(result.successful.length).toBe(50)
|
|
|
|
// Search should work
|
|
const results = await perfBrain.find({
|
|
query: 'Performance item',
|
|
limit: 20
|
|
})
|
|
expect(results.length).toBeGreaterThan(0)
|
|
|
|
await perfBrain.close()
|
|
}, 180000)
|
|
})
|
|
|
|
describe('Clear Operations', () => {
|
|
beforeEach(async () => {
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
|
|
await brain.addMany({
|
|
items: [
|
|
{ data: 'Doc 1', type: NounType.Document, metadata: { category: 'A' } },
|
|
{ data: 'Doc 2', type: NounType.Document, metadata: { category: 'B' } },
|
|
{ data: 'Person 1', type: NounType.Person, metadata: { category: 'A' } }
|
|
]
|
|
})
|
|
}, 120000)
|
|
|
|
afterEach(async () => {
|
|
await brain.close()
|
|
})
|
|
|
|
it('should clear data and allow re-use', async () => {
|
|
const beforeClear = await brain.find({ where: { category: 'A' } })
|
|
expect(beforeClear.length).toBeGreaterThan(0)
|
|
|
|
await brain.clear()
|
|
|
|
// After clear, add should still work
|
|
const id = await brain.add({ data: 'After clear', type: NounType.Document })
|
|
const entity = await brain.get(id)
|
|
expect(entity).toBeDefined()
|
|
expect(entity?.data).toBe('After clear')
|
|
}, 120000)
|
|
})
|
|
})
|