docs(8.0): correct public docs to the real 8.0 API + honest perf claims
The GA-readiness audit found the public docs had drifted from the shipped surface and presented uncited performance numbers as measured fact. - quick-start: `FindResult`→`Result`, `VerbType.BuiltOn`→`DependsOn` (the canonical getting-started example now compiles). - noun-verb-taxonomy: rewrote every sample off removed/fictional APIs (`augment`/`connectModel`/`getVerbs`/two-arg `add`/`like`/`$gte`) onto the real single-object `add`/`find`/`relate`/`related`; replaced the stale 31-noun/40-verb catalogs with accurate, complete tables (42 nouns, 127 verbs). - triple-intelligence: `like:`→`query:`, dollar-operators→bare operators, and several other fictional keys swept to the real `FindParams`. - FIND_SYSTEM / PERFORMANCE / index-architecture / BATCHING: replaced fabricated, mutually-inconsistent latency tables and uncited speedup multipliers with Big-O characterizations, qualitative mechanism descriptions, and the one genuinely-measured benchmark (graph O(1) neighbor lookup), per the evidence-based-claims rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3d116190f3
commit
40d2cd5419
9 changed files with 1209 additions and 2283 deletions
|
|
@ -1,638 +0,0 @@
|
|||
/**
|
||||
* Error Handling and Recovery Test Suite for Brainy v3.0
|
||||
*
|
||||
* Comprehensive error scenario testing including:
|
||||
* - Invalid input validation
|
||||
* - Network failure recovery
|
||||
* - Resource exhaustion handling
|
||||
* - Concurrent error scenarios
|
||||
* - Graceful degradation
|
||||
* - Error message consistency
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { Brainy } from '../../src/brainy'
|
||||
|
||||
describe('Error Handling and Recovery', () => {
|
||||
let brainy: Brainy
|
||||
|
||||
beforeEach(async () => {
|
||||
brainy = new Brainy({ requireSubtype: false,
|
||||
storage: { type: 'memory' }
|
||||
})
|
||||
await brainy.init()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await brainy.close()
|
||||
})
|
||||
|
||||
describe('Input Validation Errors', () => {
|
||||
describe('ADD operation validation', () => {
|
||||
it('should reject null data', async () => {
|
||||
await expect(
|
||||
brainy.add({
|
||||
data: null as any,
|
||||
type: 'document'
|
||||
})
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should reject undefined data', async () => {
|
||||
await expect(
|
||||
brainy.add({
|
||||
data: undefined as any,
|
||||
type: 'document'
|
||||
})
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should reject empty string data', async () => {
|
||||
await expect(
|
||||
brainy.add({
|
||||
data: '',
|
||||
type: 'document'
|
||||
})
|
||||
).rejects.toThrow(/data.*required|empty/i)
|
||||
})
|
||||
|
||||
it('should reject invalid type values', async () => {
|
||||
await expect(
|
||||
brainy.add({
|
||||
data: 'Valid data',
|
||||
type: 'invalid-type' as any
|
||||
})
|
||||
).rejects.toThrow(/invalid.*type|noun.*type/i)
|
||||
})
|
||||
|
||||
it('should reject non-string data types', async () => {
|
||||
const invalidData = [
|
||||
42,
|
||||
true,
|
||||
[],
|
||||
{ text: 'object' },
|
||||
() => 'function'
|
||||
]
|
||||
|
||||
for (const data of invalidData) {
|
||||
await expect(
|
||||
brainy.add({
|
||||
data: data as any,
|
||||
type: 'document'
|
||||
})
|
||||
).rejects.toThrow()
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle extremely large metadata gracefully', async () => {
|
||||
const hugeMetadata: any = {}
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
hugeMetadata[`field${i}`] = `value${i}`.repeat(100)
|
||||
}
|
||||
|
||||
// Should either succeed or throw meaningful error
|
||||
try {
|
||||
const id = await brainy.add({
|
||||
data: 'Test with huge metadata',
|
||||
type: 'document',
|
||||
metadata: hugeMetadata
|
||||
})
|
||||
expect(id).toBeDefined()
|
||||
} catch (error: any) {
|
||||
expect(error.message).toMatch(/size|memory|large/i)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET operation validation', () => {
|
||||
it('should return null for non-existent IDs', async () => {
|
||||
const result = await brainy.get('non-existent-id-12345')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle invalid ID formats gracefully', async () => {
|
||||
const invalidIds = [
|
||||
null,
|
||||
undefined,
|
||||
'',
|
||||
' ',
|
||||
'../etc/passwd',
|
||||
'<script>alert(1)</script>'
|
||||
]
|
||||
|
||||
for (const id of invalidIds) {
|
||||
const result = await brainy.get(id as any)
|
||||
expect(result).toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('UPDATE operation validation', () => {
|
||||
it('should reject updates without ID', async () => {
|
||||
await expect(
|
||||
brainy.update({
|
||||
id: undefined as any,
|
||||
metadata: { updated: true }
|
||||
})
|
||||
).rejects.toThrow(/id.*required/i)
|
||||
})
|
||||
|
||||
it('should reject updates to non-existent entities', async () => {
|
||||
await expect(
|
||||
brainy.update({
|
||||
id: 'non-existent-id',
|
||||
metadata: { updated: true }
|
||||
})
|
||||
).rejects.toThrow(/not found|doesn't exist/i)
|
||||
})
|
||||
|
||||
it('should handle circular references in metadata', async () => {
|
||||
const id = await brainy.add({
|
||||
data: 'Test entity',
|
||||
type: 'document'
|
||||
})
|
||||
|
||||
const metadata: any = { level: 1 }
|
||||
metadata.self = metadata // Circular reference
|
||||
|
||||
// Should either handle or throw meaningful error
|
||||
try {
|
||||
await brainy.update({
|
||||
id,
|
||||
metadata
|
||||
})
|
||||
// If successful, verify it was handled
|
||||
const updated = await brainy.get(id)
|
||||
expect(updated).toBeDefined()
|
||||
} catch (error: any) {
|
||||
expect(error.message).toMatch(/circular|cyclic/i)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE operation validation', () => {
|
||||
it('should handle deletion of non-existent entities', async () => {
|
||||
// Should not throw, just return void
|
||||
await expect(
|
||||
brainy.remove('non-existent-id')
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle double deletion gracefully', async () => {
|
||||
const id = await brainy.add({
|
||||
data: 'To be deleted',
|
||||
type: 'document'
|
||||
})
|
||||
|
||||
await brainy.remove(id)
|
||||
// Second delete should not throw
|
||||
await expect(brainy.remove(id)).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('RELATE operation validation', () => {
|
||||
it('should reject relationships without source', async () => {
|
||||
await expect(
|
||||
brainy.relate({
|
||||
from: undefined as any,
|
||||
to: 'some-id',
|
||||
type: 'relatedTo'
|
||||
})
|
||||
).rejects.toThrow(/from.*required/i)
|
||||
})
|
||||
|
||||
it('should reject relationships without target', async () => {
|
||||
await expect(
|
||||
brainy.relate({
|
||||
from: 'some-id',
|
||||
to: undefined as any,
|
||||
type: 'relatedTo'
|
||||
})
|
||||
).rejects.toThrow(/to.*required/i)
|
||||
})
|
||||
|
||||
it('should reject invalid relationship types', async () => {
|
||||
const id1 = await brainy.add({
|
||||
data: 'Entity 1',
|
||||
type: 'thing'
|
||||
})
|
||||
const id2 = await brainy.add({
|
||||
data: 'Entity 2',
|
||||
type: 'thing'
|
||||
})
|
||||
|
||||
await expect(
|
||||
brainy.relate({
|
||||
from: id1,
|
||||
to: id2,
|
||||
type: 'invalid-verb' as any
|
||||
})
|
||||
).rejects.toThrow(/invalid.*type|verb.*type/i)
|
||||
})
|
||||
|
||||
it('should reject self-relationships by default', async () => {
|
||||
const id = await brainy.add({
|
||||
data: 'Self entity',
|
||||
type: 'thing'
|
||||
})
|
||||
|
||||
// Some systems reject self-relationships
|
||||
try {
|
||||
await brainy.relate({
|
||||
from: id,
|
||||
to: id,
|
||||
type: 'relatedTo'
|
||||
})
|
||||
} catch (error: any) {
|
||||
expect(error.message).toMatch(/self|same.*entity/i)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('FIND operation validation', () => {
|
||||
it('should handle empty queries gracefully', async () => {
|
||||
const results = await brainy.find({})
|
||||
expect(Array.isArray(results)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle invalid where clauses', async () => {
|
||||
const results = await brainy.find({
|
||||
where: {
|
||||
'metadata.field': { $invalidOp: 'value' } as any
|
||||
}
|
||||
})
|
||||
// Should return empty array or handle gracefully
|
||||
expect(Array.isArray(results)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle negative limits', async () => {
|
||||
const results = await brainy.find({
|
||||
limit: -10
|
||||
})
|
||||
expect(Array.isArray(results)).toBe(true)
|
||||
expect(results.length).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it('should handle huge limits reasonably', async () => {
|
||||
const results = await brainy.find({
|
||||
limit: Number.MAX_SAFE_INTEGER
|
||||
})
|
||||
expect(Array.isArray(results)).toBe(true)
|
||||
// Should cap at reasonable limit
|
||||
expect(results.length).toBeLessThanOrEqual(10000)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Batch Operation Error Handling', () => {
|
||||
it('should handle partial failures in addMany', async () => {
|
||||
const items = [
|
||||
{ data: 'Valid 1', type: 'document' as const },
|
||||
{ data: '', type: 'document' as const }, // Invalid
|
||||
{ data: 'Valid 2', type: 'document' as const },
|
||||
{ data: null as any, type: 'document' as const }, // Invalid
|
||||
{ data: 'Valid 3', type: 'document' as const }
|
||||
]
|
||||
|
||||
const result = await brainy.addMany({
|
||||
items,
|
||||
continueOnError: true
|
||||
})
|
||||
|
||||
expect(result.successful.length).toBeGreaterThanOrEqual(3)
|
||||
expect(result.failed.length).toBeGreaterThanOrEqual(0)
|
||||
expect(result.total).toBe(5)
|
||||
})
|
||||
|
||||
it('should rollback batch on critical failure', async () => {
|
||||
const items = Array(100).fill(null).map((_, i) => ({
|
||||
data: `Item ${i}`,
|
||||
type: 'document' as const
|
||||
}))
|
||||
|
||||
// Inject a failure midway
|
||||
items[50] = {
|
||||
data: null as any,
|
||||
type: 'document' as const
|
||||
}
|
||||
|
||||
const result = await brainy.addMany({
|
||||
items,
|
||||
continueOnError: false
|
||||
})
|
||||
|
||||
// Should either complete all or rollback
|
||||
if (result.successful.length > 0) {
|
||||
expect(result.successful.length).toBe(100)
|
||||
} else {
|
||||
expect(result.failed.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Concurrent Operation Errors', () => {
|
||||
it('should handle concurrent updates to same entity', async () => {
|
||||
const id = await brainy.add({
|
||||
data: 'Concurrent test',
|
||||
type: 'document',
|
||||
metadata: { version: 1 }
|
||||
})
|
||||
|
||||
// Attempt concurrent updates
|
||||
const updates = Array(10).fill(null).map((_, i) =>
|
||||
brainy.update({
|
||||
id,
|
||||
metadata: { version: i + 2 }
|
||||
})
|
||||
)
|
||||
|
||||
const results = await Promise.allSettled(updates)
|
||||
|
||||
// At least one should succeed
|
||||
const succeeded = results.filter(r => r.status === 'fulfilled')
|
||||
expect(succeeded.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// Final state should be consistent
|
||||
const final = await brainy.get(id)
|
||||
expect(final?.metadata?.version).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('should handle concurrent deletes gracefully', async () => {
|
||||
const id = await brainy.add({
|
||||
data: 'To be deleted concurrently',
|
||||
type: 'document'
|
||||
})
|
||||
|
||||
// Attempt concurrent deletes
|
||||
const deletes = Array(5).fill(null).map(() => brainy.remove(id))
|
||||
|
||||
// Should not throw
|
||||
await expect(Promise.all(deletes)).resolves.toBeDefined()
|
||||
|
||||
// Entity should be deleted
|
||||
const result = await brainy.get(id)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Resource Exhaustion', () => {
|
||||
it('should handle memory pressure gracefully', async () => {
|
||||
const largeData = 'x'.repeat(1024 * 1024) // 1MB
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
// Try to add many large entities
|
||||
for (let i = 0; i < 100; i++) {
|
||||
try {
|
||||
await brainy.add({
|
||||
data: largeData + ` Entity ${i}`,
|
||||
type: 'document',
|
||||
metadata: { index: i, size: largeData.length }
|
||||
})
|
||||
successCount++
|
||||
} catch (error) {
|
||||
errorCount++
|
||||
// Should get meaningful error
|
||||
expect(error).toBeDefined()
|
||||
}
|
||||
}
|
||||
|
||||
// Should handle at least some operations
|
||||
expect(successCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle rapid-fire operations', async () => {
|
||||
const operations = 1000
|
||||
const promises: Promise<any>[] = []
|
||||
|
||||
for (let i = 0; i < operations; i++) {
|
||||
const op = i % 4
|
||||
switch (op) {
|
||||
case 0:
|
||||
promises.push(
|
||||
brainy.add({
|
||||
data: `Rapid ${i}`,
|
||||
type: 'document'
|
||||
}).catch(() => null)
|
||||
)
|
||||
break
|
||||
case 1:
|
||||
promises.push(brainy.get(`rapid-${i}`).catch(() => null))
|
||||
break
|
||||
case 2:
|
||||
promises.push(
|
||||
brainy.update({
|
||||
id: `rapid-${i}`,
|
||||
metadata: { updated: true }
|
||||
}).catch(() => null)
|
||||
)
|
||||
break
|
||||
case 3:
|
||||
promises.push(brainy.remove(`rapid-${i}`).catch(() => null))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises)
|
||||
|
||||
// Most should complete
|
||||
const completed = results.filter(r => r.status === 'fulfilled')
|
||||
expect(completed.length).toBeGreaterThan(operations * 0.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Message Quality', () => {
|
||||
it('should provide clear error messages for common mistakes', async () => {
|
||||
// Missing required field
|
||||
try {
|
||||
await brainy.add({} as any)
|
||||
} catch (error: any) {
|
||||
expect(error.message).toBeDefined()
|
||||
expect(error.message.length).toBeGreaterThan(10)
|
||||
// Should mention what's missing
|
||||
expect(error.message).toMatch(/data|required/i)
|
||||
}
|
||||
|
||||
// Invalid type
|
||||
try {
|
||||
await brainy.add({
|
||||
data: 'Valid data',
|
||||
type: 'not-a-valid-type' as any
|
||||
})
|
||||
} catch (error: any) {
|
||||
expect(error.message).toMatch(/type|invalid|noun/i)
|
||||
}
|
||||
|
||||
// Entity not found
|
||||
try {
|
||||
await brainy.update({
|
||||
id: 'definitely-does-not-exist',
|
||||
metadata: { test: true }
|
||||
})
|
||||
} catch (error: any) {
|
||||
expect(error.message).toMatch(/not found|doesn't exist|does not exist/i)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not leak sensitive information in errors', async () => {
|
||||
try {
|
||||
await brainy.add({
|
||||
data: 'Test',
|
||||
type: 'document',
|
||||
metadata: {
|
||||
password: 'secret123',
|
||||
apiKey: 'sk-1234567890'
|
||||
}
|
||||
})
|
||||
|
||||
await brainy.update({
|
||||
id: 'non-existent',
|
||||
metadata: { password: 'should-not-appear' }
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Error message should not contain sensitive data
|
||||
expect(error.message).not.toMatch(/secret123|sk-1234567890|should-not-appear/i)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Recovery Mechanisms', () => {
|
||||
it('should recover from transient failures', async () => {
|
||||
let attemptCount = 0
|
||||
const maxAttempts = 3
|
||||
|
||||
async function retryableAdd(data: string): Promise<string> {
|
||||
attemptCount++
|
||||
if (attemptCount < maxAttempts) {
|
||||
throw new Error('Transient failure')
|
||||
}
|
||||
return brainy.add({
|
||||
data,
|
||||
type: 'document'
|
||||
})
|
||||
}
|
||||
|
||||
// Should eventually succeed with retry
|
||||
let result: string | null = null
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
result = await retryableAdd('Test with retry')
|
||||
break
|
||||
} catch (error) {
|
||||
if (i === maxAttempts - 1) throw error
|
||||
}
|
||||
}
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(attemptCount).toBe(maxAttempts)
|
||||
})
|
||||
|
||||
it('should maintain consistency after errors', async () => {
|
||||
// Add some valid data
|
||||
const validIds: string[] = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const id = await brainy.add({
|
||||
data: `Valid entity ${i}`,
|
||||
type: 'document'
|
||||
})
|
||||
validIds.push(id)
|
||||
}
|
||||
|
||||
// Cause some errors
|
||||
try {
|
||||
await brainy.add({ data: null as any, type: 'document' })
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
await brainy.update({ id: 'non-existent', metadata: {} })
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
await brainy.relate({
|
||||
from: 'non-existent-1',
|
||||
to: 'non-existent-2',
|
||||
type: 'relatedTo'
|
||||
})
|
||||
} catch {}
|
||||
|
||||
// Valid data should still be intact
|
||||
for (const id of validIds) {
|
||||
const entity = await brainy.get(id)
|
||||
expect(entity).toBeDefined()
|
||||
expect(entity?.type).toBe('document')
|
||||
}
|
||||
|
||||
// Should still be able to add new data
|
||||
const newId = await brainy.add({
|
||||
data: 'Added after errors',
|
||||
type: 'document'
|
||||
})
|
||||
expect(newId).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle operations on just-deleted entities', async () => {
|
||||
const id = await brainy.add({
|
||||
data: 'About to be deleted',
|
||||
type: 'document'
|
||||
})
|
||||
|
||||
await brainy.remove(id)
|
||||
|
||||
// Operations on deleted entity should handle gracefully
|
||||
const getResult = await brainy.get(id)
|
||||
expect(getResult).toBeNull()
|
||||
|
||||
await expect(
|
||||
brainy.update({
|
||||
id,
|
||||
metadata: { attempt: 'update-after-delete' }
|
||||
})
|
||||
).rejects.toThrow(/not found/i)
|
||||
|
||||
// Should not throw for delete
|
||||
await expect(brainy.remove(id)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle rapid create-delete cycles', async () => {
|
||||
const cycles = 50
|
||||
|
||||
for (let i = 0; i < cycles; i++) {
|
||||
const id = await brainy.add({
|
||||
data: `Cycle ${i}`,
|
||||
type: 'document'
|
||||
})
|
||||
|
||||
// Immediately delete
|
||||
await brainy.remove(id)
|
||||
|
||||
// Verify it's gone
|
||||
const result = await brainy.get(id)
|
||||
expect(result).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle maximum field lengths', async () => {
|
||||
const maxFieldLength = 1024 * 1024 // 1MB
|
||||
const longString = 'x'.repeat(maxFieldLength)
|
||||
|
||||
try {
|
||||
const id = await brainy.add({
|
||||
data: longString,
|
||||
type: 'document',
|
||||
metadata: {
|
||||
description: longString.substring(0, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
// If successful, should be retrievable
|
||||
const entity = await brainy.get(id)
|
||||
expect(entity).toBeDefined()
|
||||
} catch (error: any) {
|
||||
// If it fails, should have meaningful error
|
||||
expect(error.message).toMatch(/size|large|limit/i)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue