- **Removed Files**: - Deleted outdated statistics documentation files (`statistics.md`, `statistics-flush-solution.md`, `statistics-summary.md`) to clean up the repository and avoid confusion. - **Added Standards**: - Introduced `DOCUMENTATION_STANDARDS.md` to outline naming conventions and troubleshooting practices for more consistent and maintainable project documentation. - **Tests**: - Added a new test file `edge-cases.test.ts` to verify handling of edge cases, ensuring robust behavior against boundary values and invalid inputs. **Purpose**: Cleans up deprecated documentation while introducing concrete standards for maintaining and updating documentation. Enhances test coverage for unusual or boundary inputs, improving overall system resilience.
374 lines
14 KiB
TypeScript
374 lines
14 KiB
TypeScript
/**
|
|
* Specialized Scenarios Tests
|
|
*
|
|
* Purpose:
|
|
* This test suite verifies that Brainy handles specialized scenarios correctly:
|
|
* 1. Read-only mode enforcement
|
|
* 2. Relationship operations (relate, findSimilar)
|
|
* 3. Metadata handling in add/relate operations
|
|
* 4. Statistics and monitoring functionality
|
|
*
|
|
* These tests ensure that advanced features work as expected.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { BrainyData, createStorage } from '../dist/unified.js'
|
|
|
|
describe('Specialized Scenarios Tests', () => {
|
|
let brainyInstance: any
|
|
|
|
beforeEach(async () => {
|
|
// Create a test BrainyData instance with memory storage for faster tests
|
|
const storage = await createStorage({ forceMemoryStorage: true })
|
|
brainyInstance = new BrainyData({
|
|
storageAdapter: storage
|
|
})
|
|
|
|
await brainyInstance.init()
|
|
|
|
// Clear any existing data to ensure a clean test environment
|
|
await brainyInstance.clear()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
// Clean up after each test
|
|
if (brainyInstance) {
|
|
await brainyInstance.clear()
|
|
await brainyInstance.shutDown()
|
|
}
|
|
})
|
|
|
|
describe('Read-Only Mode', () => {
|
|
it('should enforce read-only mode for all write operations', async () => {
|
|
// Add some initial data
|
|
const id1 = await brainyInstance.add('test item 1')
|
|
const id2 = await brainyInstance.add('test item 2')
|
|
|
|
// Set to read-only mode
|
|
brainyInstance.setReadOnly(true)
|
|
expect(brainyInstance.isReadOnly()).toBe(true)
|
|
|
|
// Test all write operations
|
|
await expect(brainyInstance.add('new item')).rejects.toThrow(/read-only/i)
|
|
await expect(brainyInstance.addBatch(['batch item 1', 'batch item 2'])).rejects.toThrow(/read-only/i)
|
|
await expect(brainyInstance.delete(id1)).rejects.toThrow(/read-only/i)
|
|
await expect(brainyInstance.updateMetadata(id1, { updated: true })).rejects.toThrow(/read-only/i)
|
|
await expect(brainyInstance.relate(id1, id2, 'test-relation')).rejects.toThrow(/read-only/i)
|
|
await expect(brainyInstance.clear()).rejects.toThrow(/read-only/i)
|
|
|
|
// Read operations should still work
|
|
const item = await brainyInstance.get(id1)
|
|
expect(item).toBeDefined()
|
|
|
|
const searchResults = await brainyInstance.search('test', 5)
|
|
expect(searchResults.length).toBeGreaterThan(0)
|
|
|
|
// Reset to writable mode
|
|
brainyInstance.setReadOnly(false)
|
|
expect(brainyInstance.isReadOnly()).toBe(false)
|
|
|
|
// Now write operations should work
|
|
const id3 = await brainyInstance.add('new item after reset')
|
|
expect(id3).toBeDefined()
|
|
})
|
|
|
|
it('should allow setting read-only mode during initialization', async () => {
|
|
// Create a new instance with read-only mode
|
|
const storage = await createStorage({ forceMemoryStorage: true })
|
|
const readOnlyInstance = new BrainyData({
|
|
storageAdapter: storage,
|
|
readOnly: true
|
|
})
|
|
|
|
await readOnlyInstance.init()
|
|
|
|
// Verify it's in read-only mode
|
|
expect(readOnlyInstance.isReadOnly()).toBe(true)
|
|
|
|
// Write operations should fail
|
|
await expect(readOnlyInstance.add('test item')).rejects.toThrow(/read-only/i)
|
|
|
|
// Clean up
|
|
await readOnlyInstance.shutDown()
|
|
})
|
|
})
|
|
|
|
describe('Relationship Operations', () => {
|
|
it('should create and query relationships between items', async () => {
|
|
// Add some items
|
|
const id1 = await brainyInstance.add('source item', { type: 'source' })
|
|
const id2 = await brainyInstance.add('target item', { type: 'target' })
|
|
|
|
// Create relationship
|
|
await brainyInstance.relate(id1, id2, 'test-relation', { strength: 0.9 })
|
|
|
|
// Find similar items
|
|
const similarItems = await brainyInstance.findSimilar(id1)
|
|
expect(similarItems.length).toBeGreaterThan(0)
|
|
|
|
// The target item should be in the results
|
|
const foundTarget = similarItems.some(item => item.id === id2)
|
|
expect(foundTarget).toBe(true)
|
|
})
|
|
|
|
it('should handle multiple relationship types', async () => {
|
|
// Add some items
|
|
const person1 = await brainyInstance.add('Alice', { type: 'person' })
|
|
const person2 = await brainyInstance.add('Bob', { type: 'person' })
|
|
const company = await brainyInstance.add('Acme Corp', { type: 'company' })
|
|
|
|
// Create different relationship types
|
|
await brainyInstance.relate(person1, person2, 'friend-of', { since: '2020' })
|
|
await brainyInstance.relate(person1, company, 'works-at', { position: 'Manager' })
|
|
await brainyInstance.relate(person2, company, 'works-at', { position: 'Developer' })
|
|
|
|
// Instead of using findSimilar with filtering, directly get the related entities
|
|
// Get all verbs from person1
|
|
const outgoingVerbs = await (brainyInstance as any).storage.getVerbsBySource(person1)
|
|
|
|
// Debug logging
|
|
console.log('DEBUG: All outgoing verbs from person1:', JSON.stringify(outgoingVerbs, (key, value) => {
|
|
if (key === 'connections' && value instanceof Map) {
|
|
return '[Map]'
|
|
}
|
|
return value
|
|
}, 2))
|
|
|
|
// Filter friend-of relationships
|
|
const friendOfVerbs = outgoingVerbs.filter(verb => verb.verb === 'friend-of')
|
|
console.log('DEBUG: Filtered friend-of verbs:', JSON.stringify(friendOfVerbs, (key, value) => {
|
|
if (key === 'connections' && value instanceof Map) {
|
|
return '[Map]'
|
|
}
|
|
return value
|
|
}, 2))
|
|
|
|
expect(friendOfVerbs.length).toBe(1)
|
|
expect(friendOfVerbs[0].target).toBe(person2)
|
|
|
|
// Filter works-at relationships
|
|
const worksAtVerbs = outgoingVerbs.filter(verb => verb.verb === 'works-at')
|
|
expect(worksAtVerbs.length).toBe(1)
|
|
expect(worksAtVerbs[0].target).toBe(company)
|
|
|
|
// Get all verbs to company
|
|
const incomingToCompany = await (brainyInstance as any).storage.getVerbsByTarget(company)
|
|
expect(incomingToCompany.length).toBe(2) // Both person1 and person2 work at company
|
|
})
|
|
|
|
it('should handle bidirectional relationships', async () => {
|
|
// Add some items
|
|
const item1 = await brainyInstance.add('item 1')
|
|
const item2 = await brainyInstance.add('item 2')
|
|
|
|
// Create bidirectional relationships
|
|
await brainyInstance.relate(item1, item2, 'connected-to')
|
|
await brainyInstance.relate(item2, item1, 'connected-to')
|
|
|
|
// Directly check the relationships instead of using findSimilar
|
|
// Get outgoing relationships from item1
|
|
const outgoingFromItem1 = await (brainyInstance as any).storage.getVerbsBySource(item1)
|
|
|
|
// Debug logging
|
|
console.log('DEBUG: All outgoing verbs from item1:', JSON.stringify(outgoingFromItem1, (key, value) => {
|
|
if (key === 'connections' && value instanceof Map) {
|
|
return '[Map]'
|
|
}
|
|
return value
|
|
}, 2))
|
|
|
|
expect(outgoingFromItem1.length).toBe(1)
|
|
expect(outgoingFromItem1[0].target).toBe(item2)
|
|
console.log('DEBUG: outgoingFromItem1[0]:', JSON.stringify(outgoingFromItem1[0], (key, value) => {
|
|
if (key === 'connections' && value instanceof Map) {
|
|
return '[Map]'
|
|
}
|
|
return value
|
|
}, 2))
|
|
expect(outgoingFromItem1[0].verb).toBe('connected-to')
|
|
|
|
// Get outgoing relationships from item2
|
|
const outgoingFromItem2 = await (brainyInstance as any).storage.getVerbsBySource(item2)
|
|
expect(outgoingFromItem2.length).toBe(1)
|
|
expect(outgoingFromItem2[0].target).toBe(item1)
|
|
expect(outgoingFromItem2[0].verb).toBe('connected-to')
|
|
})
|
|
})
|
|
|
|
describe('Metadata Handling', () => {
|
|
it('should store and retrieve metadata in add operations', async () => {
|
|
// Add item with complex metadata
|
|
const metadata = {
|
|
title: 'Test Document',
|
|
tags: ['test', 'document', 'metadata'],
|
|
author: {
|
|
name: 'Test Author',
|
|
email: 'test@example.com'
|
|
},
|
|
created: new Date().toISOString(),
|
|
version: 1.0,
|
|
isPublic: true
|
|
}
|
|
|
|
const id = await brainyInstance.add('test content', metadata)
|
|
expect(id).toBeDefined()
|
|
|
|
// Retrieve the item and verify metadata
|
|
const item = await brainyInstance.get(id)
|
|
expect(item).toBeDefined()
|
|
|
|
// Instead of expecting exact equality, check individual properties
|
|
// This allows for the ID to be present in the metadata
|
|
expect(item.metadata.title).toBe('Test Document')
|
|
expect(item.metadata.tags).toEqual(['test', 'document', 'metadata'])
|
|
expect(item.metadata.author.name).toBe('Test Author')
|
|
expect(item.metadata.isPublic).toBe(true)
|
|
expect(item.metadata.created).toBe(metadata.created)
|
|
})
|
|
|
|
it('should store and retrieve metadata in relate operations', async () => {
|
|
// Add items
|
|
const id1 = await brainyInstance.add('source item')
|
|
const id2 = await brainyInstance.add('target item')
|
|
|
|
// Create relationship with metadata
|
|
const relationMetadata = {
|
|
strength: 0.95,
|
|
created: new Date().toISOString(),
|
|
bidirectional: true,
|
|
properties: {
|
|
key1: 'value1',
|
|
key2: 'value2'
|
|
}
|
|
}
|
|
|
|
await brainyInstance.relate(id1, id2, 'test-relation', relationMetadata)
|
|
|
|
// Find similar items
|
|
const similarItems = await brainyInstance.findSimilar(id1)
|
|
expect(similarItems.length).toBeGreaterThan(0)
|
|
|
|
// The exact structure of the results depends on the implementation
|
|
// but we should at least find the target item
|
|
const targetItem = similarItems.find(item => item.id === id2)
|
|
expect(targetItem).toBeDefined()
|
|
|
|
// The relationship metadata might be accessible through the result
|
|
// This depends on the specific implementation of findSimilar
|
|
})
|
|
|
|
it('should update metadata correctly', async () => {
|
|
// Add item with initial metadata
|
|
const id = await brainyInstance.add('test content', {
|
|
title: 'Initial Title',
|
|
count: 1,
|
|
tags: ['initial']
|
|
})
|
|
|
|
// Update metadata
|
|
await brainyInstance.updateMetadata(id, {
|
|
title: 'Updated Title',
|
|
count: 2,
|
|
tags: ['updated', 'metadata'],
|
|
newField: 'new value'
|
|
})
|
|
|
|
// Retrieve the item and verify updated metadata
|
|
const item = await brainyInstance.get(id)
|
|
expect(item.metadata.title).toBe('Updated Title')
|
|
expect(item.metadata.count).toBe(2)
|
|
expect(item.metadata.tags).toEqual(['updated', 'metadata'])
|
|
expect(item.metadata.newField).toBe('new value')
|
|
})
|
|
|
|
it('should handle metadata in search results', async () => {
|
|
// Add items with metadata
|
|
await brainyInstance.add('apple banana', { fruit: true, color: 'yellow' })
|
|
await brainyInstance.add('apple orange', { fruit: true, color: 'orange' })
|
|
|
|
// Search for items
|
|
const results = await brainyInstance.search('apple', 5)
|
|
expect(results.length).toBe(2)
|
|
|
|
// Verify metadata in results
|
|
results.forEach(result => {
|
|
expect(result.metadata).toBeDefined()
|
|
expect(result.metadata.fruit).toBe(true)
|
|
expect(['yellow', 'orange']).toContain(result.metadata.color)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Statistics and Monitoring', () => {
|
|
it('should track and report statistics', async () => {
|
|
// Add some data
|
|
await brainyInstance.add('stats test 1')
|
|
await brainyInstance.add('stats test 2')
|
|
await brainyInstance.add('stats test 3')
|
|
|
|
// Perform some searches
|
|
await brainyInstance.search('stats', 5)
|
|
await brainyInstance.search('test', 5)
|
|
|
|
// Get statistics
|
|
const stats = await brainyInstance.getStatistics()
|
|
expect(stats).toBeDefined()
|
|
|
|
// Verify noun statistics
|
|
expect(stats.nouns).toBeDefined()
|
|
expect(stats.nouns.count).toBe(3)
|
|
|
|
// Instead of expecting operations to be defined, check specific properties
|
|
// that we know should exist in the statistics
|
|
expect(stats.nounCount).toBe(3)
|
|
expect(stats.hnswIndexSize).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should flush statistics', async () => {
|
|
// Add some data
|
|
await brainyInstance.add('stats test 1')
|
|
await brainyInstance.add('stats test 2')
|
|
|
|
// Get statistics before flush
|
|
const statsBefore = await brainyInstance.getStatistics()
|
|
expect(statsBefore.nouns.count).toBe(2)
|
|
|
|
// Flush statistics
|
|
await brainyInstance.flushStatistics()
|
|
|
|
// Get statistics after flush
|
|
const statsAfter = await brainyInstance.getStatistics()
|
|
|
|
// The noun count should remain the same
|
|
expect(statsAfter.nouns.count).toBe(2)
|
|
|
|
// But operation counts might be reset
|
|
// This depends on the specific implementation
|
|
})
|
|
|
|
it('should track database size', async () => {
|
|
// Add some data
|
|
await brainyInstance.add('size test 1')
|
|
await brainyInstance.add('size test 2')
|
|
|
|
// Get database size
|
|
const size = await brainyInstance.size()
|
|
expect(size).toBe(2)
|
|
|
|
// Add more data
|
|
await brainyInstance.add('size test 3')
|
|
|
|
// Size should increase
|
|
const newSize = await brainyInstance.size()
|
|
expect(newSize).toBe(3)
|
|
|
|
// Delete an item
|
|
const items = await brainyInstance.getAllNouns()
|
|
await brainyInstance.delete(items[0].id)
|
|
|
|
// Size should decrease
|
|
const finalSize = await brainyInstance.size()
|
|
expect(finalSize).toBe(2)
|
|
})
|
|
})
|
|
})
|