diff --git a/tests/api/batch-operations.test.ts b/tests/api/batch-operations.test.ts deleted file mode 100644 index be6673fc..00000000 --- a/tests/api/batch-operations.test.ts +++ /dev/null @@ -1,660 +0,0 @@ -/** - * Batch Operations Test Suite for Brainy v3.0 - * - * Comprehensive testing of batch operations including: - * - addMany: Bulk entity creation - * - removeMany: Bulk deletion - * - Performance validation at scale - * - Memory efficiency testing - * - Error recovery scenarios - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { Brainy } from '../../src/brainy' -import { performance } from 'perf_hooks' - -interface BatchMetrics { - operation: string - totalItems: number - successCount: number - failureCount: number - duration: number - throughput: number - memoryUsed: number - errors: string[] -} - -class BatchMetricsCollector { - private metrics: BatchMetrics[] = [] - - recordBatch( - operation: string, - totalItems: number, - results: any[], - duration: number, - memoryUsed: number - ): BatchMetrics { - const successCount = results.filter(r => - r?.status === 'fulfilled' || r === true || (r && !r.error) - ).length - const failureCount = totalItems - successCount - const errors = results - .filter(r => r?.status === 'rejected' || r?.error) - .map(r => r?.reason?.message || r?.error || 'Unknown error') - .slice(0, 5) // Limit to first 5 errors - - const metric: BatchMetrics = { - operation, - totalItems, - successCount, - failureCount, - duration, - throughput: (totalItems / duration) * 1000, - memoryUsed, - errors - } - - this.metrics.push(metric) - return metric - } - - getMetrics(): BatchMetrics[] { - return this.metrics - } - - generateReport(): void { - console.log('\n=== Batch Operations Performance Report ===\n') - console.table(this.metrics.map(m => ({ - Operation: m.operation, - 'Total Items': m.totalItems, - 'Success': `${m.successCount}/${m.totalItems} (${((m.successCount/m.totalItems)*100).toFixed(1)}%)`, - 'Duration (ms)': m.duration.toFixed(2), - 'Throughput (items/s)': m.throughput.toFixed(0), - 'Memory (MB)': (m.memoryUsed / 1024 / 1024).toFixed(2), - 'Errors': m.errors.length > 0 ? m.errors[0] : 'None' - }))) - - // Summary statistics - const totalOps = this.metrics.reduce((sum, m) => sum + m.totalItems, 0) - const totalSuccess = this.metrics.reduce((sum, m) => sum + m.successCount, 0) - const avgThroughput = this.metrics.reduce((sum, m) => sum + m.throughput, 0) / this.metrics.length - - console.log('\nSummary:') - console.log(`Total Operations: ${totalOps}`) - console.log(`Success Rate: ${((totalSuccess/totalOps)*100).toFixed(2)}%`) - console.log(`Average Throughput: ${avgThroughput.toFixed(0)} items/sec`) - } -} - -describe('Batch Operations - Public API Testing', () => { - let brainy: Brainy - let metricsCollector: BatchMetricsCollector - - beforeEach(async () => { - brainy = new Brainy({ requireSubtype: false, - storage: { type: 'memory' } - }) - await brainy.init() - metricsCollector = new BatchMetricsCollector() - }) - - afterEach(async () => { - await brainy.close() - if (global.gc) global.gc() - }) - - describe('addMany - Bulk Entity Creation', () => { - it('should add 100 entities efficiently', async () => { - const entities = Array(100).fill(null).map((_, i) => ({ - data: `Batch entity ${i}: Test content for bulk operations`, - type: 'document' as const, - metadata: { - batchIndex: i, - batchId: 'batch-100', - timestamp: Date.now() - } - })) - - const startMemory = process.memoryUsage().heapUsed - const startTime = performance.now() - - const result = await brainy.addMany({ items: entities }) - - const duration = performance.now() - startTime - const memoryUsed = process.memoryUsage().heapUsed - startMemory - - const metric = metricsCollector.recordBatch( - 'addMany-100', - entities.length, - result.successful, - duration, - memoryUsed - ) - - expect(result.successful).toHaveLength(100) - expect(result.failed).toHaveLength(0) - expect(metric.throughput).toBeGreaterThan(100) // > 100 items/sec - }) - - it('should handle 1,000 entities with proper batching', async () => { - const entities = Array(1000).fill(null).map((_, i) => ({ - data: `Entity ${i}: ${' '.repeat(100)}`, // ~100 bytes each - type: 'document' as const, - metadata: { - index: i, - category: `cat-${i % 10}`, - tags: [`tag-${i % 5}`, `tag-${i % 7}`] - } - })) - - const startMemory = process.memoryUsage().heapUsed - const startTime = performance.now() - - const result = await brainy.addMany({ - items: entities, - chunkSize: 100, // Process in batches - parallel: true - }) - - const duration = performance.now() - startTime - const memoryUsed = process.memoryUsage().heapUsed - startMemory - - metricsCollector.recordBatch( - 'addMany-1k', - entities.length, - result.successful, - duration, - memoryUsed - ) - - expect(result.successful.length).toBe(1000) - expect(result.failed.length).toBe(0) - expect(duration).toBeLessThan(10000) // < 10 seconds - - // Verify data integrity - const sampleIds = result.successful.slice(0, 10) - for (const id of sampleIds) { - const entity = await brainy.get(id) - expect(entity).toBeDefined() - } - }) - - it('should handle 10,000 entities with memory efficiency', async () => { - const chunkSize = 1000 - const totalEntities = 10000 - let allSuccessful: string[] = [] - let allFailed: any[] = [] - - const startMemory = process.memoryUsage().heapUsed - const startTime = performance.now() - - // Process in chunks to avoid memory issues - for (let chunk = 0; chunk < totalEntities; chunk += chunkSize) { - const entities = Array(Math.min(chunkSize, totalEntities - chunk)) - .fill(null) - .map((_, i) => ({ - data: `Chunk entity ${chunk + i}`, - type: 'document' as const, - metadata: { globalIndex: chunk + i } - })) - - const result = await brainy.addMany({ items: entities }) - allSuccessful.push(...result.successful) - allFailed.push(...result.failed) - - // Check memory usage - const currentMemory = process.memoryUsage().heapUsed - const memoryGrowth = currentMemory - startMemory - expect(memoryGrowth).toBeLessThan(500 * 1024 * 1024) // < 500MB total - } - - const duration = performance.now() - startTime - const finalMemory = process.memoryUsage().heapUsed - startMemory - - metricsCollector.recordBatch( - 'addMany-10k', - totalEntities, - allSuccessful, - duration, - finalMemory - ) - - expect(allSuccessful.length).toBe(10000) - expect(allFailed.length).toBe(0) - }) - - it('should handle partial failures gracefully', async () => { - const entities = [ - { data: 'Valid entity 1', type: 'document' as const }, - { data: '', type: 'document' as const }, // Invalid: empty data - { data: 'Valid entity 2', type: 'document' as const }, - { data: null as any, type: 'document' as const }, // Invalid: null data - { data: 'Valid entity 3', type: 'document' as const }, - ] - - const result = await brainy.addMany({ - items: entities, - continueOnError: true - }) - - // Should process valid entities even if some fail - expect(result.successful.length).toBeGreaterThanOrEqual(3) - expect(result.failed.length).toBeGreaterThanOrEqual(0) // May or may not validate - - // Verify valid entities were added - for (const id of result.successful) { - const entity = await brainy.get(id) - expect(entity).toBeDefined() - } - }) - - it('should support concurrent batch operations', async () => { - const batches = Array(5).fill(null).map((_, batchIdx) => - Array(100).fill(null).map((_, i) => ({ - data: `Batch ${batchIdx} Entity ${i}`, - type: 'document' as const, - metadata: { batchId: batchIdx, index: i } - })) - ) - - const startTime = performance.now() - - // Execute batches concurrently - const results = await Promise.all( - batches.map(entities => brainy.addMany({ items: entities })) - ) - - const duration = performance.now() - startTime - - expect(results).toHaveLength(5) - const totalSuccess = results.reduce((sum, r) => sum + r.successful.length, 0) - expect(totalSuccess).toBe(500) - expect(duration).toBeLessThan(5000) // Concurrent should be faster - }) - }) - - let testIds: string[] = [] - - beforeEach(async () => { - // Create test entities - const entities = Array(100).fill(null).map((_, i) => ({ - data: `Original content ${i}`, - type: 'document' as const, - metadata: { version: 1, index: i } - })) - - const result = await brainy.addMany({ items: entities }) - testIds = result.successful - }) - - it('should update multiple entities by IDs', async () => { - const updates = testIds.slice(0, 50).map(id => ({ - id, - updates: { - metadata: { - version: 2, - updatedAt: Date.now() - } - } - })) - - const startTime = performance.now() - - const results = await Promise.all( - updates.map(u => brainy.update({ id: u.id, ...u.updates })) - ) - - const duration = performance.now() - startTime - - metricsCollector.recordBatch( - updates.length, - results, - duration, - 0 - ) - - // Verify updates - const sample = await brainy.get(testIds[0]) - expect(sample?.metadata?.version).toBe(2) - }) - - it('should update entities matching criteria', async () => { - // Update all entities with index < 20 - const targetIds = testIds.slice(0, 20) - - const startTime = performance.now() - - const results = await Promise.all( - targetIds.map(id => - brainy.update({ - id, - metadata: { - status: 'updated', - processedAt: Date.now() - } - }) - ) - ) - - const duration = performance.now() - startTime - - metricsCollector.recordBatch( - targetIds.length, - results, - duration, - 0 - ) - - // Verify updates - for (const id of targetIds.slice(0, 5)) { - const entity = await brainy.get(id) - expect(entity?.metadata?.status).toBe('updated') - } - }) - }) - - describe('removeMany - Bulk Deletion', () => { - let testIds: string[] = [] - - beforeEach(async () => { - // Create test entities - const entities = Array(200).fill(null).map((_, i) => ({ - data: `Delete test ${i}`, - type: 'document' as const, - metadata: { deleteGroup: i % 4 } - })) - - const result = await brainy.addMany({ items: entities }) - testIds = result.successful - }) - - it('should delete multiple entities by IDs', async () => { - const toDelete = testIds.slice(0, 100) - - const startMemory = process.memoryUsage().heapUsed - const startTime = performance.now() - - // Use removeMany if available, otherwise individual deletes - let results: any[] - if ('removeMany' in brainy && typeof brainy.removeMany === 'function') { - const result = await brainy.removeMany({ ids: toDelete }) - results = result.successful - } else { - results = await Promise.all( - toDelete.map(id => brainy.remove(id)) - ) - } - - const duration = performance.now() - startTime - const memoryUsed = process.memoryUsage().heapUsed - startMemory - - metricsCollector.recordBatch( - 'removeMany-100', - toDelete.length, - results, - duration, - memoryUsed - ) - - // Verify deletion - for (const id of toDelete.slice(0, 10)) { - const entity = await brainy.get(id) - expect(entity).toBeNull() - } - - // Remaining entities should still exist - const remaining = testIds.slice(100, 110) - for (const id of remaining) { - const entity = await brainy.get(id) - expect(entity).toBeDefined() - } - }) - - it('should handle large-scale deletion efficiently', async () => { - // Create a large dataset - const largeDataset = Array(1000).fill(null).map((_, i) => ({ - data: `Large scale delete test ${i}`, - type: 'document' as const - })) - - const addResult = await brainy.addMany({ items: largeDataset }) - const idsToDelete = addResult.successful - - const startTime = performance.now() - - // Delete in batches - const batchSize = 100 - for (let i = 0; i < idsToDelete.length; i += batchSize) { - const batch = idsToDelete.slice(i, i + batchSize) - await Promise.all(batch.map(id => brainy.remove(id))) - } - - const duration = performance.now() - startTime - - metricsCollector.recordBatch( - 'removeMany-1k', - idsToDelete.length, - idsToDelete.map(() => true), - duration, - 0 - ) - - expect(duration).toBeLessThan(10000) // < 10 seconds - }) - }) - - let nodeIds: string[] = [] - - beforeEach(async () => { - // Create nodes for relationships - const nodes = Array(50).fill(null).map((_, i) => ({ - data: `Node ${i}`, - type: 'thing' as const, - metadata: { nodeIndex: i } - })) - - const result = await brainy.addMany({ items: nodes }) - nodeIds = result.successful - }) - - it('should create multiple relationships efficiently', async () => { - const relationships: Array<{from: string, to: string, type: string}> = [] - - // Create a connected graph - for (let i = 0; i < nodeIds.length - 1; i++) { - relationships.push({ - from: nodeIds[i], - to: nodeIds[i + 1], - type: 'connected_to' - }) - } - - const startTime = performance.now() - - const results = await Promise.all( - relationships.map(r => - brainy.relate({ from: r.from, to: r.to, type: r.type as any }) - ) - ) - - const duration = performance.now() - startTime - - metricsCollector.recordBatch( - relationships.length, - results, - duration, - 0 - ) - - expect(results.every(r => typeof r === 'string')).toBe(true) // relate returns ID - expect(duration).toBeLessThan(5000) - }) - - it('should create complex graph structures', async () => { - // Create a fully connected subgraph (first 10 nodes) - const subgraph = nodeIds.slice(0, 10) - const relationships: Array<{from: string, to: string}> = [] - - for (let i = 0; i < subgraph.length; i++) { - for (let j = i + 1; j < subgraph.length; j++) { - relationships.push({ - from: subgraph[i], - to: subgraph[j] - }) - } - } - - const startTime = performance.now() - - const results = await Promise.all( - relationships.map(r => - brainy.relate({ from: r.from, to: r.to, type: 'relatedTo' }) - ) - ) - - const duration = performance.now() - startTime - - metricsCollector.recordBatch( - relationships.length, - results, - duration, - 0 - ) - - expect(relationships.length).toBe(45) // 10 choose 2 - expect(results.every(r => typeof r === 'string')).toBe(true) // relate returns ID - }) - }) - - describe('Mixed Batch Operations', () => { - it('should handle mixed operation types concurrently', async () => { - // Prepare different operation types - const addOps = Array(100).fill(null).map((_, i) => ({ - data: `Mixed add ${i}`, - type: 'document' as const - })) - - // Add initial entities for update/delete - const setupResult = await brainy.addMany({ - items: Array(100).fill(null).map((_, i) => ({ - data: `Setup ${i}`, - type: 'document' as const - })) - }) - - const updateIds = setupResult.successful.slice(0, 50) - const deleteIds = setupResult.successful.slice(50, 100) - - const startTime = performance.now() - - // Execute all operations concurrently - const [addResults, updateResults, deleteResults] = await Promise.all([ - brainy.addMany({ items: addOps }), - Promise.all(updateIds.map(id => - brainy.update({ id, metadata: { updated: true } }) - )), - Promise.all(deleteIds.map(id => brainy.remove(id))) - ]) - - const duration = performance.now() - startTime - - console.log(`Mixed operations completed in ${duration.toFixed(2)}ms`) - - expect(addResults.successful.length).toBe(100) - expect(updateResults.length).toBe(50) - // Delete returns void, so just check length - expect(deleteResults).toHaveLength(50) - }) - }) - - describe('Error Recovery', () => { - it('should recover from transient failures', async () => { - let attemptCount = 0 - const entities = Array(10).fill(null).map((_, i) => ({ - data: `Retry test ${i}`, - type: 'document' as const - })) - - // Simulate transient failures - const originalAdd = brainy.add.bind(brainy) - brainy.add = async function(params: any) { - attemptCount++ - if (attemptCount % 3 === 0) { - throw new Error('Transient failure') - } - return originalAdd(params) - } - - const results: any[] = [] - for (const entity of entities) { - let retries = 0 - while (retries < 3) { - try { - const id = await brainy.add(entity) - results.push({ status: 'fulfilled', value: id }) - break - } catch (error) { - retries++ - if (retries === 3) { - results.push({ status: 'rejected', reason: error }) - } - } - } - } - - const successful = results.filter(r => r.status === 'fulfilled') - expect(successful.length).toBeGreaterThanOrEqual(6) // Most should succeed with retry - }) - - it('should handle memory pressure gracefully', async () => { - const largeEntities = Array(100).fill(null).map((_, i) => ({ - data: 'x'.repeat(100000), // 100KB each = 10MB total - type: 'document' as const, - metadata: { index: i } - })) - - const startMemory = process.memoryUsage().heapUsed - - // Process with memory monitoring - const batchSize = 10 - const results: any[] = [] - - for (let i = 0; i < largeEntities.length; i += batchSize) { - const batch = largeEntities.slice(i, i + batchSize) - - // Check memory before processing - const currentMemory = process.memoryUsage().heapUsed - const memoryUsed = currentMemory - startMemory - - if (memoryUsed > 100 * 1024 * 1024) { // If > 100MB - if (global.gc) global.gc() // Force GC if available - } - - const batchResult = await brainy.addMany({ items: batch }) - results.push(...batchResult.successful) - } - - expect(results.length).toBe(100) - - const finalMemory = process.memoryUsage().heapUsed - startMemory - expect(finalMemory).toBeLessThan(200 * 1024 * 1024) // Should stay under 200MB - }) - }) - - describe('Performance Report', () => { - it('should generate comprehensive batch operations report', async () => { - metricsCollector.generateReport() - - const metrics = metricsCollector.getMetrics() - expect(metrics.length).toBeGreaterThan(0) - - // Validate performance targets - for (const metric of metrics) { - expect(metric.successCount).toBeGreaterThan(0) - if (metric.operation.includes('100')) { - expect(metric.throughput).toBeGreaterThan(50) // At least 50 items/sec - } - } - }) - }) -}) \ No newline at end of file diff --git a/tests/api/crud-operations-enhanced.test.ts b/tests/api/crud-operations-enhanced.test.ts deleted file mode 100644 index b6070d15..00000000 --- a/tests/api/crud-operations-enhanced.test.ts +++ /dev/null @@ -1,764 +0,0 @@ -/** - * Enhanced CRUD Operations Test Suite for Brainy v3.0 - * - * Comprehensive validation of all public API CRUD operations with: - * - Exhaustive edge case testing - * - Performance benchmarking - * - Memory leak detection - * - Concurrent operation validation - * - Large-scale data handling - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { Brainy } from '../../src/brainy' -import { performance } from 'perf_hooks' - -interface PerformanceMetrics { - operation: string - itemCount: number - duration: number - memoryUsed: number - itemsPerSecond: number - percentiles?: { - p50: number - p95: number - p99: number - } -} - -class TestMetricsCollector { - private metrics: PerformanceMetrics[] = [] - private initialMemory: number = 0 - private latencies: Map = new Map() - - startOperation(operation: string, itemCount: number = 1): number { - this.initialMemory = process.memoryUsage().heapUsed - return performance.now() - } - - endOperation(operation: string, itemCount: number, startTime: number): PerformanceMetrics { - const duration = performance.now() - startTime - const memoryUsed = process.memoryUsage().heapUsed - this.initialMemory - const itemsPerSecond = (itemCount / duration) * 1000 - - // Track latencies for percentile calculations - if (!this.latencies.has(operation)) { - this.latencies.set(operation, []) - } - this.latencies.get(operation)!.push(duration / itemCount) - - const metric: PerformanceMetrics = { - operation, - itemCount, - duration, - memoryUsed, - itemsPerSecond - } - - this.metrics.push(metric) - return metric - } - - calculatePercentiles(operation: string): { p50: number; p95: number; p99: number } | undefined { - const latencies = this.latencies.get(operation) - if (!latencies || latencies.length === 0) return undefined - - const sorted = [...latencies].sort((a, b) => a - b) - return { - p50: sorted[Math.floor(sorted.length * 0.5)], - p95: sorted[Math.floor(sorted.length * 0.95)], - p99: sorted[Math.floor(sorted.length * 0.99)] - } - } - - getMetrics(): PerformanceMetrics[] { - return this.metrics - } - - async reportMetrics(): Promise { - console.log('\n=== Performance Metrics Report ===\n') - - // Add percentiles to metrics - for (const metric of this.metrics) { - metric.percentiles = this.calculatePercentiles(metric.operation) - } - - console.table(this.metrics.map(m => ({ - Operation: m.operation, - 'Items': m.itemCount, - 'Duration (ms)': m.duration.toFixed(2), - 'Items/sec': m.itemsPerSecond.toFixed(0), - 'Memory (MB)': (m.memoryUsed / 1024 / 1024).toFixed(2), - 'P50 (ms)': m.percentiles?.p50.toFixed(2) || 'N/A', - 'P95 (ms)': m.percentiles?.p95.toFixed(2) || 'N/A', - 'P99 (ms)': m.percentiles?.p99.toFixed(2) || 'N/A' - }))) - } -} - -describe('Enhanced CRUD Operations - Public API Validation', () => { - let brainy: Brainy - let metricsCollector: TestMetricsCollector - - beforeEach(async () => { - brainy = new Brainy({ requireSubtype: false, - storage: { type: 'memory' } - }) - await brainy.init() - metricsCollector = new TestMetricsCollector() - }) - - afterEach(async () => { - await brainy.close() - // Force garbage collection if available - if (global.gc) { - global.gc() - } - }) - - describe('ADD Operations - Comprehensive Testing', () => { - describe('Basic Add Functionality', () => { - it('should add entity with all supported data types', async () => { - const testData = { - text: 'This is a comprehensive test document about AI and machine learning', - metadata: { - title: 'Test Document', - author: 'Test Suite', - version: '1.0.0', - tags: ['test', 'validation', 'comprehensive'], - stats: { - words: 10, - characters: 68, - readingTime: 2 - }, - nullField: null, - booleanField: true, - numberField: 42.5, - dateField: new Date().toISOString() - } - } - - const startTime = metricsCollector.startOperation('add-single') - const id = await brainy.add({ - data: testData.text, - type: 'document' as const, - metadata: testData.metadata - }) - const metric = metricsCollector.endOperation('add-single', 1, startTime) - - expect(id).toBeDefined() - expect(typeof id).toBe('string') - expect(id.length).toBeGreaterThan(0) - expect(metric.duration).toBeLessThan(50) // 50ms SLA for single add - - // Verify stored data - const retrieved = await brainy.get(id) - expect(retrieved).toBeDefined() - expect(retrieved?.metadata?.title).toBe('Test Document') - expect(retrieved?.type).toBe('document') - expect(retrieved?.metadata?.title).toBe('Test Document') - }) - - it('should handle Unicode and special characters correctly', async () => { - const specialCases = [ - { data: '😀🎉🚀 Emoji test document', desc: 'Emoji support' }, - { data: '中文测试文档 Chinese text', desc: 'Chinese characters' }, - { data: 'العربية اختبار Arabic text', desc: 'Arabic script' }, - { data: 'Русский текст тестирование', desc: 'Cyrillic script' }, - { data: '日本語のテストドキュメント', desc: 'Japanese characters' }, - { data: 'Line\nBreak\rReturn\tTab test', desc: 'Control characters' }, - { data: '', desc: 'HTML injection attempt' }, - { data: '../../etc/passwd traversal', desc: 'Path traversal attempt' }, - { data: 'NULL\x00byte test', desc: 'Null byte injection' }, - { data: '𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁', desc: 'Mathematical alphanumeric symbols' } - ] - - for (const testCase of specialCases) { - const id = await brainy.add({ - data: testCase.data, - type: 'document' as const, - metadata: { description: testCase.desc } - }) - - const retrieved = await brainy.get(id) - expect(retrieved?.data).toBe(testCase.data) - expect(retrieved?.metadata?.description).toBe(testCase.desc) - } - }) - - it('should auto-generate unique IDs when not provided', async () => { - const ids = new Set() - const count = 100 - - for (let i = 0; i < count; i++) { - const id = await brainy.add({ - data: `Auto-generated ID test ${i}`, - type: 'test' - }) - ids.add(id) - } - - expect(ids.size).toBe(count) // All IDs must be unique - - // Verify ID format - for (const id of ids) { - expect(id).toMatch(/^[a-zA-Z0-9_-]+$/) // Valid ID characters - expect(id.length).toBeGreaterThanOrEqual(8) // Reasonable length - expect(id.length).toBeLessThanOrEqual(64) // Not too long - } - }) - }) - - describe('Bulk Add Operations', () => { - it('should efficiently handle 1,000 entities', async () => { - const entities = Array(1000).fill(null).map((_, i) => ({ - data: `Test document ${i}: This is content for testing bulk operations at scale`, - type: 'document', - metadata: { - index: i, - category: `category-${i % 10}`, - score: Math.random() * 100, - tags: [`tag-${i % 5}`, `tag-${i % 7}`] - } - })) - - const startTime = metricsCollector.startOperation('add-bulk-1k', 1000) - const ids: string[] = [] - - // Add in batches for better performance - const batchSize = 100 - for (let i = 0; i < entities.length; i += batchSize) { - const batch = entities.slice(i, i + batchSize) - const batchIds = await Promise.all( - batch.map(e => brainy.add(e)) - ) - ids.push(...batchIds) - } - - const metric = metricsCollector.endOperation('add-bulk-1k', 1000, startTime) - - expect(ids).toHaveLength(1000) - expect(new Set(ids).size).toBe(1000) // All unique - expect(metric.itemsPerSecond).toBeGreaterThan(100) // At least 100 items/sec - expect(metric.memoryUsed).toBeLessThan(100 * 1024 * 1024) // Less than 100MB - - // Verify data integrity with random sampling - for (let i = 0; i < 10; i++) { - const randomIdx = Math.floor(Math.random() * 1000) - const entity = await brainy.get(ids[randomIdx]) - expect(entity?.metadata?.index).toBe(randomIdx) - } - }) - - it('should handle 10,000 entities with memory efficiency', async () => { - const startMemory = process.memoryUsage().heapUsed - const count = 10000 - const dataSize = 500 // bytes per entity - - const startTime = metricsCollector.startOperation('add-bulk-10k', count) - const ids: string[] = [] - - // Stream-like processing - const batchSize = 500 - for (let i = 0; i < count; i += batchSize) { - const batch = Array(Math.min(batchSize, count - i)).fill(null).map((_, j) => ({ - data: 'x'.repeat(dataSize) + ` Entity ${i + j}`, - type: 'bulk-test', - metadata: { index: i + j } - })) - - const batchIds = await Promise.all( - batch.map(e => brainy.add(e)) - ) - ids.push(...batchIds) - - // Check memory periodically - if (i % 2000 === 0 && i > 0) { - const currentMemory = process.memoryUsage().heapUsed - const memoryGrowth = currentMemory - startMemory - expect(memoryGrowth).toBeLessThan(200 * 1024 * 1024) // Less than 200MB - } - } - - const metric = metricsCollector.endOperation('add-bulk-10k', count, startTime) - - expect(ids).toHaveLength(count) - expect(metric.duration).toBeLessThan(60000) // Complete within 60 seconds - expect(metric.itemsPerSecond).toBeGreaterThan(150) // At least 150 items/sec - }) - - it('should detect and prevent memory leaks', async () => { - const iterations = 5 - const memoryDeltas: number[] = [] - - for (let iter = 0; iter < iterations; iter++) { - if (global.gc) global.gc() // Force GC if available - - const beforeMemory = process.memoryUsage().heapUsed - - // Add and delete 500 entities - const ids: string[] = [] - for (let i = 0; i < 500; i++) { - const id = await brainy.add({ - data: 'x'.repeat(1000), // 1KB per entity - type: 'leak-test', - metadata: { iteration: iter, index: i } - }) - ids.push(id) - } - - // Delete all entities - for (const id of ids) { - await brainy.remove(id) - } - - if (global.gc) global.gc() - const afterMemory = process.memoryUsage().heapUsed - memoryDeltas.push(afterMemory - beforeMemory) - } - - // Memory should stabilize, not continuously grow - const avgFirstTwo = (memoryDeltas[0] + memoryDeltas[1]) / 2 - const avgLastTwo = (memoryDeltas[3] + memoryDeltas[4]) / 2 - const growth = avgLastTwo - avgFirstTwo - - expect(growth).toBeLessThan(10 * 1024 * 1024) // Less than 10MB growth - }) - }) - - describe('Concurrent Operations', () => { - it('should handle 100 concurrent adds without race conditions', async () => { - const concurrentCount = 100 - const operations = Array(concurrentCount).fill(null).map((_, i) => ({ - data: `Concurrent test document ${i}`, - type: 'concurrent-test', - metadata: { - index: i, - timestamp: Date.now(), - random: Math.random() - } - })) - - const startTime = performance.now() - const ids = await Promise.all( - operations.map(op => brainy.add(op)) - ) - const duration = performance.now() - startTime - - expect(ids).toHaveLength(concurrentCount) - expect(new Set(ids).size).toBe(concurrentCount) // All unique - expect(duration).toBeLessThan(2000) // Complete within 2 seconds - - // Verify data integrity - for (let i = 0; i < 10; i++) { - const randomIdx = Math.floor(Math.random() * concurrentCount) - const entity = await brainy.get(ids[randomIdx]) - expect(entity?.metadata?.index).toBe(randomIdx) - } - }) - - it('should handle write conflicts gracefully', async () => { - // Test optimistic concurrency control - const id = await brainy.add({ - data: 'Original content', - type: 'conflict-test', - metadata: { version: 1 } - }) - - // Simulate concurrent updates - const updates = Array(10).fill(null).map((_, i) => - brainy.update(id, { - metadata: { version: i + 2, updatedBy: `process-${i}` } - }) - ) - - 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) - }) - }) - - describe('Input Validation', () => { - it('should reject invalid input types', async () => { - const invalidCases = [ - { data: null, desc: 'null data' }, - { data: undefined, desc: 'undefined data' }, - { data: '', desc: 'empty string' }, - { data: 42, desc: 'number instead of string' }, - { data: true, desc: 'boolean instead of string' }, - { data: [], desc: 'array instead of string' }, - { data: {}, desc: 'object instead of string' } - ] - - for (const testCase of invalidCases) { - await expect( - brainy.add({ - data: testCase.data as any, - type: 'test' - }) - ).rejects.toThrow() - } - }) - - it('should handle extremely large entities', async () => { - const largeData = 'x'.repeat(5 * 1024 * 1024) // 5MB - - const startTime = performance.now() - const id = await brainy.add({ - data: largeData, - type: 'large-entity', - metadata: { size: largeData.length } - }) - const duration = performance.now() - startTime - - expect(id).toBeDefined() - expect(duration).toBeLessThan(5000) // Handle within 5 seconds - - const retrieved = await brainy.get(id) - expect(retrieved?.data.length).toBe(largeData.length) - }) - - it('should sanitize and validate metadata fields', async () => { - const metadata = { - 'normal-field': 'value1', - '$special.field': 'value2', - '__proto__': 'attempt-pollution', - 'constructor': 'attempt-override', - '../../../etc/passwd': 'path-traversal', - '': 'xss-attempt' - } - - const id = await brainy.add({ - data: 'Test document with suspicious metadata', - type: 'security-test', - metadata - }) - - const retrieved = await brainy.get(id) - expect(retrieved).toBeDefined() - - // Verify prototype pollution prevention - expect(Object.prototype).not.toHaveProperty('attempt-pollution') - expect(Object.constructor).not.toBe('attempt-override') - }) - - it('should handle circular references gracefully', async () => { - const metadata: any = { - name: 'Circular test', - level: 1 - } - metadata.self = metadata // Create circular reference - - // Should either serialize correctly or throw meaningful error - try { - const id = await brainy.add({ - data: 'Document with circular metadata', - type: 'circular-test', - metadata - }) - - const retrieved = await brainy.get(id) - expect(retrieved).toBeDefined() - // Circular reference should be handled (removed or converted) - expect(retrieved?.metadata?.self).not.toBe(retrieved?.metadata) - } catch (error: any) { - expect(error.message).toMatch(/circular|cyclic/i) - } - }) - }) - - describe('Performance SLAs', () => { - it('should meet latency SLAs for single operations', async () => { - const operations = 100 - const latencies: number[] = [] - - for (let i = 0; i < operations; i++) { - const start = performance.now() - await brainy.add({ - data: `Performance test document ${i}`, - type: 'perf-test', - metadata: { index: i } - }) - latencies.push(performance.now() - start) - } - - // Calculate percentiles - latencies.sort((a, b) => a - b) - const p50 = latencies[Math.floor(latencies.length * 0.5)] - const p95 = latencies[Math.floor(latencies.length * 0.95)] - const p99 = latencies[Math.floor(latencies.length * 0.99)] - - console.log(`Add Latencies - P50: ${p50.toFixed(2)}ms, P95: ${p95.toFixed(2)}ms, P99: ${p99.toFixed(2)}ms`) - - expect(p50).toBeLessThan(10) // P50 < 10ms - expect(p95).toBeLessThan(50) // P95 < 50ms - expect(p99).toBeLessThan(100) // P99 < 100ms - }) - - it('should maintain throughput under sustained load', async () => { - const duration = 5000 // 5 seconds - const startTime = performance.now() - let operationCount = 0 - - while (performance.now() - startTime < duration) { - await brainy.add({ - data: `Sustained load test ${operationCount}`, - type: 'load-test', - metadata: { timestamp: Date.now() } - }) - operationCount++ - } - - const actualDuration = performance.now() - startTime - const throughput = (operationCount / actualDuration) * 1000 - - console.log(`Sustained Load - Ops: ${operationCount}, Throughput: ${throughput.toFixed(2)} ops/sec`) - - expect(throughput).toBeGreaterThan(50) // At least 50 ops/sec sustained - }) - }) - }) - - describe('GET Operations - Comprehensive Testing', () => { - let testIds: string[] = [] - - beforeEach(async () => { - // Seed test data - testIds = [] - for (let i = 0; i < 10; i++) { - const id = await brainy.add({ - data: `Test document ${i}`, - type: 'test', - metadata: { index: i, category: `cat-${i % 3}` } - }) - testIds.push(id) - } - }) - - it('should retrieve existing entities', async () => { - const startTime = metricsCollector.startOperation('get-single') - const entity = await brainy.get(testIds[0]) - metricsCollector.endOperation('get-single', 1, startTime) - - expect(entity).toBeDefined() - expect(entity?.data).toBe('Test document 0') - expect(entity?.metadata?.index).toBe(0) - }) - - it('should return null for non-existent IDs', async () => { - const result = await brainy.get('non-existent-id-12345') - expect(result).toBeNull() - }) - - it('should handle batch retrieval efficiently', async () => { - const startTime = metricsCollector.startOperation('get-batch', testIds.length) - const entities = await Promise.all( - testIds.map(id => brainy.get(id)) - ) - const metric = metricsCollector.endOperation('get-batch', testIds.length, startTime) - - expect(entities.every(e => e !== null)).toBe(true) - expect(metric.itemsPerSecond).toBeGreaterThan(100) - }) - }) - - describe('UPDATE Operations - Comprehensive Testing', () => { - let testId: string - - beforeEach(async () => { - testId = await brainy.add({ - data: 'Original content', - type: 'update-test', - metadata: { version: 1, status: 'draft' } - }) - }) - - it('should update entity metadata', async () => { - const startTime = metricsCollector.startOperation('update-single') - await brainy.update(testId, { - metadata: { version: 2, status: 'published' } - }) - metricsCollector.endOperation('update-single', 1, startTime) - - const updated = await brainy.get(testId) - expect(updated?.metadata?.version).toBe(2) - expect(updated?.metadata?.status).toBe('published') - }) - - it('should handle concurrent updates', async () => { - const updates = Array(10).fill(null).map((_, i) => - brainy.update(testId, { - metadata: { lastUpdate: i } - }) - ) - - const results = await Promise.allSettled(updates) - const succeeded = results.filter(r => r.status === 'fulfilled') - expect(succeeded.length).toBeGreaterThanOrEqual(1) - }) - }) - - describe('DELETE Operations - Comprehensive Testing', () => { - it('should delete existing entities', async () => { - const id = await brainy.add({ - data: 'To be deleted', - type: 'delete-test' - }) - - const startTime = metricsCollector.startOperation('delete-single') - const result = await brainy.remove(id) - metricsCollector.endOperation('delete-single', 1, startTime) - - expect(result).toBe(true) - - const retrieved = await brainy.get(id) - expect(retrieved).toBeNull() - }) - - it('should handle bulk deletions', async () => { - const ids: string[] = [] - for (let i = 0; i < 100; i++) { - const id = await brainy.add({ - data: `Bulk delete test ${i}`, - type: 'bulk-delete' - }) - ids.push(id) - } - - const startTime = metricsCollector.startOperation('delete-bulk', ids.length) - const results = await Promise.all( - ids.map(id => brainy.remove(id)) - ) - metricsCollector.endOperation('delete-bulk', ids.length, startTime) - - expect(results.every(r => r === true)).toBe(true) - }) - }) - - describe('RELATE Operations - Comprehensive Testing', () => { - it('should create relationships between entities', async () => { - const id1 = await brainy.add({ - data: 'Entity 1', - type: 'node' - }) - const id2 = await brainy.add({ - data: 'Entity 2', - type: 'node' - }) - - const startTime = metricsCollector.startOperation('relate-single') - const result = await brainy.relate(id1, id2, 'connects_to') - metricsCollector.endOperation('relate-single', 1, startTime) - - expect(result).toBe(true) - }) - - it('should handle complex graph structures', async () => { - const nodeIds: string[] = [] - - // Create nodes - for (let i = 0; i < 20; i++) { - const id = await brainy.add({ - data: `Node ${i}`, - type: 'graph-node', - metadata: { index: i } - }) - nodeIds.push(id) - } - - // Create relationships (each node connects to next 3) - const startTime = metricsCollector.startOperation('relate-graph', 60) - for (let i = 0; i < nodeIds.length; i++) { - for (let j = 1; j <= 3; j++) { - const targetIdx = (i + j) % nodeIds.length - await brainy.relate(nodeIds[i], nodeIds[targetIdx], 'links_to') - } - } - metricsCollector.endOperation('relate-graph', 60, startTime) - - // Graph should be created successfully - expect(nodeIds).toHaveLength(20) - }) - }) - - describe('FIND Operations - Comprehensive Testing', () => { - beforeEach(async () => { - // Create diverse test dataset - for (let i = 0; i < 50; i++) { - await brainy.add({ - data: `Search test document ${i}: Lorem ipsum dolor sit amet`, - type: 'searchable', - metadata: { - index: i, - category: `cat-${i % 5}`, - score: Math.random() * 100, - active: i % 2 === 0 - } - }) - } - }) - - it('should find entities by metadata criteria', async () => { - const startTime = metricsCollector.startOperation('find-simple') - const results = await brainy.find({ - where: { - 'metadata.category': 'cat-0' - } - }) - metricsCollector.endOperation('find-simple', 1, startTime) - - expect(results.length).toBe(10) // 50 / 5 = 10 - }) - - it('should support pagination', async () => { - const page1 = await brainy.find({ - limit: 10, - offset: 0 - }) - - const page2 = await brainy.find({ - limit: 10, - offset: 10 - }) - - expect(page1).toHaveLength(10) - expect(page2).toHaveLength(10) - expect(page1[0].id).not.toBe(page2[0].id) - }) - - it('should handle complex queries', async () => { - const results = await brainy.find({ - where: { - 'metadata.active': true, - 'metadata.score': { $gte: 50 } - }, - limit: 100 - }) - - expect(results.every(r => r.metadata?.active === true)).toBe(true) - expect(results.every(r => (r.metadata?.score ?? 0) >= 50)).toBe(true) - }) - }) - - describe('Performance Summary', () => { - it('should generate comprehensive performance report', async () => { - await metricsCollector.reportMetrics() - - const metrics = metricsCollector.getMetrics() - expect(metrics.length).toBeGreaterThan(0) - - // Validate all operations met SLAs - for (const metric of metrics) { - if (metric.operation.includes('single')) { - expect(metric.duration).toBeLessThan(100) // Single ops < 100ms - } - } - }) - }) -}) \ No newline at end of file diff --git a/tests/brainy-3.test.ts b/tests/brainy-3.test.ts deleted file mode 100644 index 8db458ce..00000000 --- a/tests/brainy-3.test.ts +++ /dev/null @@ -1,431 +0,0 @@ -/** - * Brainy 3.0 API Tests - * Comprehensive tests for the new beautiful, consistent API - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { Brainy, NounType, VerbType } from '../src/brainy.js' - -describe('Brainy 3.0 API', () => { - let brain: Brainy - - beforeEach(async () => { - brain = new Brainy({ requireSubtype: false, - storage: { type: 'memory' } - }) - await brain.init() - }) - - afterEach(async () => { - await brain.close() - }) - - describe('Entity Operations', () => { - it('should add entities with beautiful API', async () => { - const id = await brain.add({ - data: 'Test document about machine learning', - type: NounType.Document, - metadata: { - title: 'ML Guide', - author: 'Test Author' - }, - service: 'test-service' - }) - - expect(id).toBeTypeOf('string') - expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) - }) - - it('should retrieve entities', async () => { - const id = await brain.add({ - data: 'Sample data', - type: NounType.Document, - metadata: { title: 'Sample Title' } - }) - - // v5.11.1: Need includeVectors to check vectors - const entity = await brain.get(id, { includeVectors: true }) - - expect(entity).toBeDefined() - expect(entity!.id).toBe(id) - expect(entity!.type).toBe(NounType.Document) - expect(entity!.metadata.title).toBe('Sample Title') - expect(entity!.vector).toBeDefined() - expect(entity!.vector.length).toBeGreaterThan(0) - }) - - it('should update entities', async () => { - const id = await brain.add({ - data: 'Original data', - type: NounType.Document, - metadata: { title: 'Original Title' } - }) - - await brain.update({ - id, - metadata: { title: 'Updated Title', version: 2 } - }) - - const entity = await brain.get(id) - expect(entity!.metadata.title).toBe('Updated Title') - expect(entity!.metadata.version).toBe(2) - }) - - it('should delete entities', async () => { - const id = await brain.add({ - data: 'To be deleted', - type: NounType.Document - }) - - await brain.remove(id) - - const entity = await brain.get(id) - expect(entity).toBeNull() - }) - }) - - describe('Relationship Operations', () => { - it('should create relationships', async () => { - const doc1 = await brain.add({ - data: 'First document', - type: NounType.Document - }) - - const doc2 = await brain.add({ - data: 'Second document', - type: NounType.Document - }) - - const relationId = await brain.relate({ - from: doc1, - to: doc2, - type: VerbType.References, - weight: 0.8, - metadata: { context: 'test' } - }) - - expect(relationId).toBeTypeOf('string') - }) - - it('should get relationships', async () => { - const doc1 = await brain.add({ - data: 'Document one', - type: NounType.Document - }) - - const doc2 = await brain.add({ - data: 'Document two', - type: NounType.Document - }) - - await brain.relate({ - from: doc1, - to: doc2, - type: VerbType.References - }) - - const relations = await brain.related({ from: doc1 }) - - expect(relations).toHaveLength(1) - expect(relations[0].from).toBe(doc1) - expect(relations[0].to).toBe(doc2) - expect(relations[0].type).toBe(VerbType.References) - }) - - it('should create bidirectional relationships', async () => { - const person = await brain.add({ - data: 'John Smith', - type: NounType.Person - }) - - const org = await brain.add({ - data: 'Tech Corp', - type: NounType.Organization - }) - - await brain.relate({ - from: person, - to: org, - type: VerbType.WorksWith, - bidirectional: true - }) - - const fromPerson = await brain.related({ from: person }) - const toPerson = await brain.related({ to: person }) - - expect(fromPerson).toHaveLength(1) - expect(toPerson).toHaveLength(1) - }) - - it('should delete relationships', async () => { - const doc1 = await brain.add({ - data: 'Doc 1', - type: NounType.Document - }) - - const doc2 = await brain.add({ - data: 'Doc 2', - type: NounType.Document - }) - - const relationId = await brain.relate({ - from: doc1, - to: doc2, - type: VerbType.References - }) - - await brain.unrelate(relationId) - - const relations = await brain.related({ from: doc1 }) - expect(relations).toHaveLength(0) - }) - }) - - describe('Search Operations', () => { - beforeEach(async () => { - // Add test data - await brain.add({ - data: 'Machine learning is a subset of artificial intelligence', - type: NounType.Document, - metadata: { category: 'AI', importance: 5 } - }) - - await brain.add({ - data: 'Neural networks are used in deep learning', - type: NounType.Document, - metadata: { category: 'AI', importance: 4 } - }) - - await brain.add({ - data: 'JavaScript is a programming language', - type: NounType.Document, - metadata: { category: 'Programming', importance: 3 } - }) - }) - - it('should find entities by text query', async () => { - const results = await brain.find({ - query: 'machine learning artificial intelligence', - limit: 2 - }) - - expect(results).toHaveLength(2) - expect(results[0].score).toBeGreaterThan(0) - expect(results[0].entity.type).toBe(NounType.Document) - }) - - it('should find similar entities', async () => { - const aiDocId = await brain.add({ - data: 'Deep learning algorithms', - type: NounType.Document - }) - - const results = await brain.similar({ - to: aiDocId, - limit: 2 - }) - - expect(results.length).toBeGreaterThan(0) - expect(results[0].entity.id).not.toBe(aiDocId) // Shouldn't include self - }) - - it('should filter by metadata', async () => { - const results = await brain.find({ - query: 'learning', - where: { category: 'AI' }, - limit: 5 - }) - - results.forEach(result => { - expect(result.entity.metadata.category).toBe('AI') - }) - }) - - it('should filter by entity type', async () => { - await brain.add({ - data: 'John Doe', - type: NounType.Person - }) - - const results = await brain.find({ - query: 'learning', - type: NounType.Document - }) - - results.forEach(result => { - expect(result.entity.type).toBe(NounType.Document) - }) - }) - - it('should support pagination', async () => { - const page1 = await brain.find({ - query: 'learning', - limit: 1, - offset: 0 - }) - - const page2 = await brain.find({ - query: 'learning', - limit: 1, - offset: 1 - }) - - expect(page1).toHaveLength(1) - expect(page2).toHaveLength(1) - expect(page1[0].entity.id).not.toBe(page2[0].entity.id) - }) - }) - - describe('Batch Operations', () => { - it('should add multiple entities', async () => { - const result = await brain.addMany({ - items: [ - { - data: 'Document 1', - type: NounType.Document, - metadata: { index: 1 } - }, - { - data: 'Document 2', - type: NounType.Document, - metadata: { index: 2 } - }, - { - data: 'Document 3', - type: NounType.Document, - metadata: { index: 3 } - } - ] - }) - - expect(result.successful).toHaveLength(3) - expect(result.failed).toHaveLength(0) - expect(result.total).toBe(3) - expect(result.duration).toBeGreaterThan(0) - }) - - it('should handle batch errors gracefully', async () => { - const result = await brain.addMany({ - items: [ - { - data: 'Valid document', - type: NounType.Document - }, - { - data: null, // Invalid data to trigger error - type: NounType.Document - } - ], - continueOnError: true - }) - - expect(result.successful).toHaveLength(1) - expect(result.failed).toHaveLength(1) - }) - - it('should delete multiple entities', async () => { - // Add test entities - const ids = await Promise.all([ - brain.add({ data: 'Delete me 1', type: NounType.Document, metadata: { delete: true } }), - brain.add({ data: 'Delete me 2', type: NounType.Document, metadata: { delete: true } }), - brain.add({ data: 'Keep me', type: NounType.Document, metadata: { delete: false } }) - ]) - - const result = await brain.removeMany({ - where: { delete: true } - }) - - expect(result.successful).toHaveLength(2) - expect(result.failed).toHaveLength(0) - - // Verify entities were deleted - const remaining = await brain.get(ids[2]) - expect(remaining).toBeDefined() - }) - }) - - describe('Type Safety', () => { - it('should enforce NounType enum', async () => { - const id = await brain.add({ - data: 'Test', - type: NounType.Document // Must use enum - }) - - const entity = await brain.get(id) - expect(entity!.type).toBe(NounType.Document) - }) - - it('should enforce VerbType enum', async () => { - const doc1 = await brain.add({ data: 'Doc 1', type: NounType.Document }) - const doc2 = await brain.add({ data: 'Doc 2', type: NounType.Document }) - - await brain.relate({ - from: doc1, - to: doc2, - type: VerbType.References // Must use enum - }) - - const relations = await brain.related({ from: doc1 }) - expect(relations[0].type).toBe(VerbType.References) - }) - }) - - describe('Error Handling', () => { - it('should handle missing entities gracefully', async () => { - const nonExistentId = '00000000-0000-0000-0000-000000000000' - const entity = await brain.get(nonExistentId) - expect(entity).toBeNull() - }) - - it('should require initialization before operations', async () => { - const uninitializedBrain = new Brainy({ requireSubtype: false }) - - await expect(uninitializedBrain.add({ - data: 'Test', - type: NounType.Document - })).rejects.toThrow('not initialized') - }) - - it('should validate required parameters', async () => { - // @ts-expect-error - Testing validation - await expect(brain.add({})).rejects.toThrow() - }) - }) - - describe('Configuration', () => { - it('should support custom configuration', async () => { - const customBrain = new Brainy({ requireSubtype: false, - storage: { type: 'memory' }, - model: { type: 'fast' }, - cache: true, - warmup: false - }) - - await customBrain.init() - - const id = await customBrain.add({ - data: 'Test with custom config', - type: NounType.Document - }) - - expect(id).toBeTypeOf('string') - await customBrain.close() - }) - }) -}) - -describe('Brainy 3.0 Neural API', () => { - let brain: Brainy - - beforeEach(async () => { - brain = new Brainy({ requireSubtype: false, - storage: { type: 'memory' }, - }) - await brain.init() - }) - - afterEach(async () => { - await brain.close() - }) - -}) \ No newline at end of file diff --git a/tests/comprehensive/core-api.test.ts b/tests/comprehensive/core-api.test.ts deleted file mode 100644 index 3970efee..00000000 --- a/tests/comprehensive/core-api.test.ts +++ /dev/null @@ -1,394 +0,0 @@ -/** - * Brainy v3.0 Core API Test Suite - * Testing all core CRUD operations and basic functionality - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { Brainy } from '../../src/brainy.js' -import { NounType, VerbType } from '../../src/types/graphTypes.js' - -describe('Brainy v3.0 Core API Tests', () => { - let brain: Brainy - - beforeEach(async () => { - brain = new Brainy({ requireSubtype: false, - storage: { type: 'memory' } - }) - await brain.init() - }) - - afterEach(async () => { - // Note: Brainy doesn't have shutdown() in v3, just let GC handle it - brain = null as any - }) - - describe('1. Add Operations', () => { - it('should add a simple text item', async () => { - const id = await brain.add({ - data: 'Hello world', - type: NounType.Document - }) - - expect(id).toBeDefined() - expect(typeof id).toBe('string') - expect(id.length).toBeGreaterThan(0) - }) - - it('should add item with custom ID', async () => { - const customId = 'custom-123' - const id = await brain.add({ - id: customId, - data: 'Custom ID test', - type: NounType.Document - }) - - expect(id).toBe(customId) - }) - - it('should add item with metadata', async () => { - const metadata = { - title: 'Test Doc', - category: 'testing', - score: 95.5, - tags: ['test', 'validation'] - } - - const id = await brain.add({ - data: 'Document with metadata', - metadata, - type: NounType.Document - }) - - const retrieved = await brain.get(id) - expect(retrieved?.metadata).toEqual(metadata) - }) - - it('should add item with pre-computed vector', async () => { - const vector = new Array(384).fill(0).map(() => Math.random()) - const id = await brain.add({ - data: 'Pre-vectorized content', - vector, - type: NounType.Thing - }) - - const retrieved = await brain.get(id) - expect(retrieved).toBeDefined() - expect(retrieved?.vector).toHaveLength(384) - }) - - it('should handle concurrent adds', async () => { - const promises = Array(10).fill(0).map((_, i) => - brain.add({ - data: `Concurrent item ${i}`, - type: NounType.Document - }) - ) - - const ids = await Promise.all(promises) - expect(ids).toHaveLength(10) - expect(new Set(ids).size).toBe(10) // All unique - }) - - it('should validate noun types', async () => { - // Valid type should work - const id = await brain.add({ - data: 'Valid type', - type: NounType.Person - }) - expect(id).toBeDefined() - - // Invalid type should throw - await expect(brain.add({ - data: 'Invalid type test', - type: 'InvalidType' as any - })).rejects.toThrow() - }) - }) - - describe('2. Get Operations', () => { - let testId: string - - beforeEach(async () => { - testId = await brain.add({ - data: 'Test document for retrieval', - metadata: { test: true }, - type: NounType.Document - }) - }) - - it('should retrieve existing item by ID', async () => { - const item = await brain.get(testId) - - expect(item).toBeDefined() - expect(item?.id).toBe(testId) - expect(item?.metadata?.test).toBe(true) - expect(item?.type).toBe(NounType.Document) - }) - - it('should return null for non-existent ID', async () => { - const item = await brain.get('non-existent-id') - expect(item).toBeNull() - }) - - it('should retrieve with vector included', async () => { - const item = await brain.get(testId) - expect(item?.vector).toBeDefined() - expect(item?.vector?.length).toBeGreaterThanOrEqual(384) // Default dimension - }) - }) - - describe('3. Update Operations', () => { - let testId: string - - beforeEach(async () => { - testId = await brain.add({ - data: 'Original content', - metadata: { version: 1 }, - type: NounType.Document - }) - }) - - it('should update existing item data', async () => { - const success = await brain.update({ - id: testId, - data: 'Updated content' - }) - - expect(success).toBe(true) - - const updated = await brain.get(testId) - expect(updated).toBeDefined() - // Vector should be recalculated after content update - }) - - it('should update only metadata', async () => { - const success = await brain.update({ - id: testId, - metadata: { version: 2, updated: true } - }) - - expect(success).toBe(true) - - const updated = await brain.get(testId) - expect(updated?.metadata).toEqual({ version: 2, updated: true }) - }) - - it('should update both data and metadata', async () => { - const success = await brain.update({ - id: testId, - data: 'New content', - metadata: { version: 3 } - }) - - expect(success).toBe(true) - - const updated = await brain.get(testId) - expect(updated?.metadata?.version).toBe(3) - }) - - it('should return false for non-existent ID', async () => { - const success = await brain.update({ - id: 'non-existent', - data: 'Will fail' - }) - - expect(success).toBe(false) - }) - }) - - describe('4. Delete Operations', () => { - it('should delete existing item', async () => { - const id = await brain.add({ - data: 'To be deleted', - type: NounType.Document - }) - - const success = await brain.remove(id) - expect(success).toBe(true) - - const item = await brain.get(id) - expect(item).toBeNull() - }) - - it('should return false for non-existent ID', async () => { - const success = await brain.remove('non-existent') - expect(success).toBe(false) - }) - - it('should handle concurrent deletes', async () => { - const ids = await Promise.all( - Array(5).fill(0).map(() => - brain.add({ data: 'Concurrent delete test', type: NounType.Document }) - ) - ) - - await Promise.all(ids.map(id => brain.remove(id))) - // delete returns void, not boolean - - // Verify all deleted - const items = await Promise.all(ids.map(id => brain.get(id))) - expect(items.every(item => item === null)).toBe(true) - }) - }) - - describe('5. Relationship Operations', () => { - let entityA: string - let entityB: string - - beforeEach(async () => { - entityA = await brain.add({ data: 'Entity A', type: NounType.Person }) - entityB = await brain.add({ data: 'Entity B', type: NounType.Organization }) - }) - - it('should create relationships between entities', async () => { - const verbId = await brain.relate({ - from: entityA, - to: entityB, - type: VerbType.MemberOf, - metadata: { since: '2023' } - }) - - expect(verbId).toBeDefined() - expect(typeof verbId).toBe('string') - }) - - it('should retrieve relationships', async () => { - await brain.relate({ - from: entityA, - to: entityB, - type: VerbType.Creates - }) - - const relations = await brain.related({ from: entityA }) - - expect(relations.length).toBeGreaterThanOrEqual(1) - expect(relations[0].from).toBe(entityA) - }) - - it('should delete relationships', async () => { - const verbId = await brain.relate({ - from: entityA, - to: entityB, - type: VerbType.Owns - }) - - const success = await brain.unrelate(verbId) - expect(success).toBe(true) - - const relations = await brain.related({ from: entityA }) - const found = relations.find(r => r.id === verbId) - expect(found).toBeUndefined() - }) - }) - - describe('6. Batch Operations', () => { - it('should add multiple items in batch', async () => { - const items = Array(10).fill(0).map((_, i) => ({ - data: `Batch item ${i}`, - metadata: { index: i }, - type: NounType.Document - })) - - const result = await brain.addMany({ items }) - - expect(result.successful).toHaveLength(10) - expect(result.failed).toHaveLength(0) - expect(result.total).toBe(10) - }) - - it('should delete multiple items in batch', async () => { - // First add some items - const ids = await Promise.all( - Array(5).fill(0).map((_, i) => - brain.add({ data: `Delete batch ${i}`, type: NounType.Document }) - ) - ) - - // Then delete them in batch - const result = await brain.removeMany({ ids }) - - expect(result.successful).toHaveLength(5) - expect(result.failed).toHaveLength(0) - }) - - it('should handle partial batch failures', async () => { - const items = [ - { data: 'Valid item', type: NounType.Document }, - { data: 'Invalid item', type: 'InvalidType' as any }, - { data: 'Another valid', type: NounType.Document } - ] - - const result = await brain.addMany({ - items, - continueOnError: true - }) - - expect(result.successful.length).toBeGreaterThanOrEqual(2) - expect(result.failed.length).toBeGreaterThanOrEqual(1) - }) - }) - - describe('7. Import/Export Operations', () => { - it('should export all data', async () => { - // Add some test data - const id1 = await brain.add({ data: 'Export test 1', type: NounType.Document }) - const id2 = await brain.add({ data: 'Export test 2', type: NounType.Document }) - await brain.relate({ from: id1, to: id2, type: VerbType.References }) - - const exported = await brain.export() - - expect(exported).toBeDefined() - expect(exported.entities).toBeDefined() - expect(exported.relationships).toBeDefined() - expect(exported.metadata).toBeDefined() - }) - - it('should import data', async () => { - // Create export data - const exportData = { - entities: [ - { id: 'imp-1', data: 'Imported 1', type: NounType.Document, metadata: {} }, - { id: 'imp-2', data: 'Imported 2', type: NounType.Document, metadata: {} } - ], - relationships: [], - metadata: { version: '3.0.0' } - } - - await brain.import(exportData) - - // Verify imported data - const item1 = await brain.get('imp-1') - const item2 = await brain.get('imp-2') - - expect(item1).toBeDefined() - expect(item2).toBeDefined() - }) - }) - - describe('8. Statistics and Insights', () => { - beforeEach(async () => { - // Add some test data - await brain.add({ data: 'Doc 1', type: NounType.Document }) - await brain.add({ data: 'Person 1', type: NounType.Person }) - await brain.add({ data: 'Org 1', type: NounType.Organization }) - }) - - it('should provide insights', async () => { - const insights = await brain.insights() - - expect(insights).toBeDefined() - expect(insights.entities).toBeGreaterThanOrEqual(3) - expect(insights.types).toBeDefined() - expect(Object.keys(insights.types).length).toBeGreaterThan(0) - }) - - it('should suggest relevant queries', async () => { - const suggestions = await brain.suggest({ limit: 3 }) - - expect(suggestions).toBeDefined() - expect(suggestions.queries).toBeDefined() - expect(Array.isArray(suggestions.queries)).toBe(true) - expect(suggestions.queries.length).toBeLessThanOrEqual(3) - }) - }) -}) \ No newline at end of file diff --git a/tests/comprehensive/find-triple-intelligence.test.ts b/tests/comprehensive/find-triple-intelligence.test.ts deleted file mode 100644 index e385fd25..00000000 --- a/tests/comprehensive/find-triple-intelligence.test.ts +++ /dev/null @@ -1,367 +0,0 @@ -/** - * Brainy v3.0 Find & Triple Intelligence Test Suite - * Testing vector search, metadata filtering, and fusion strategies - */ - -import { describe, it, expect, beforeAll, afterAll } from 'vitest' -import { Brainy } from '../../src/brainy.js' -import { NounType, VerbType } from '../../src/types/graphTypes.js' - -describe('Find and Triple Intelligence', () => { - let brain: Brainy - let testData: any[] - - beforeAll(async () => { - brain = new Brainy({ requireSubtype: false, - storage: { type: 'memory' } - }) - await brain.init() - - // Add diverse test data - testData = [ - // Technology items - { data: 'JavaScript is a programming language for web development', metadata: { category: 'tech', language: 'javascript', difficulty: 'medium' }, type: NounType.Document }, - { data: 'TypeScript adds static typing to JavaScript', metadata: { category: 'tech', language: 'typescript', difficulty: 'advanced' }, type: NounType.Document }, - { data: 'Python is great for data science and AI', metadata: { category: 'tech', language: 'python', difficulty: 'easy' }, type: NounType.Document }, - { data: 'React is a JavaScript library for building UIs', metadata: { category: 'tech', framework: 'react', language: 'javascript' }, type: NounType.Document }, - - // Science items - { data: 'Quantum computing uses quantum mechanics principles', metadata: { category: 'science', field: 'physics', complexity: 'high' }, type: NounType.Concept }, - { data: 'Machine learning enables computers to learn from data', metadata: { category: 'science', field: 'ai', complexity: 'medium' }, type: NounType.Concept }, - { data: 'Neural networks are inspired by biological brains', metadata: { category: 'science', field: 'ai', complexity: 'high' }, type: NounType.Concept }, - - // Business items - { data: 'Market analysis helps understand consumer behavior', metadata: { category: 'business', domain: 'marketing', importance: 'high' }, type: NounType.Process }, - { data: 'Project management ensures successful delivery', metadata: { category: 'business', domain: 'management', importance: 'critical' }, type: NounType.Process }, - { data: 'Financial planning is essential for growth', metadata: { category: 'business', domain: 'finance', importance: 'critical' }, type: NounType.Process } - ] - - // Add all test data - for (const item of testData) { - await brain.add(item) - } - }) - - afterAll(() => { - brain = null as any - }) - - describe('Vector Search', () => { - it('should find similar items by text query', async () => { - const results = await brain.find({ - query: 'JavaScript programming', - limit: 3 - }) - - expect(results).toHaveLength(3) - expect(results[0].score).toBeGreaterThan(0) - expect(results[0].score).toBeLessThanOrEqual(1) - - // Should find JavaScript-related items first - const topResult = results[0].entity - expect(topResult.metadata?.language).toBeDefined() - }) - - it('should respect limit parameter', async () => { - const results = await brain.find({ - query: 'technology', - limit: 5 - }) - - expect(results.length).toBeLessThanOrEqual(5) - }) - - it('should find by vector directly', async () => { - // Get vector from an existing item - const jsItem = await brain.find({ query: 'JavaScript', limit: 1 }) - const vector = jsItem[0].entity.vector - - const results = await brain.find({ - vector, - limit: 3 - }) - - expect(results).toHaveLength(3) - // First result should be very similar (same or nearly same vector) - expect(results[0].score).toBeGreaterThan(0.9) - }) - - it('should return all items when no query provided', async () => { - const results = await brain.find({}) - - expect(results.length).toBeGreaterThanOrEqual(testData.length) - }) - }) - - describe('Metadata Filtering', () => { - it('should filter by single metadata field', async () => { - const results = await brain.find({ - where: { category: 'tech' } - }) - - expect(results.length).toBeGreaterThan(0) - results.forEach(r => { - expect(r.entity.metadata?.category).toBe('tech') - }) - }) - - it('should filter by multiple metadata fields', async () => { - const results = await brain.find({ - where: { - category: 'tech', - language: 'javascript' - } - }) - - results.forEach(r => { - expect(r.entity.metadata?.category).toBe('tech') - expect(r.entity.metadata?.language).toBe('javascript') - }) - }) - - it('should support $gte operator', async () => { - const results = await brain.find({ - where: { - importance: { $gte: 'high' } - } - }) - - results.forEach(r => { - const importance = r.entity.metadata?.importance - if (importance) { - expect(['high', 'critical']).toContain(importance) - } - }) - }) - - it('should support $contains operator for arrays', async () => { - // Add item with array metadata - await brain.add({ - data: 'Test with tags', - metadata: { tags: ['test', 'validation', 'qa'] }, - type: NounType.Document - }) - - const results = await brain.find({ - where: { - tags: { $contains: 'test' } - } - }) - - const found = results.find(r => - Array.isArray(r.entity.metadata?.tags) && - r.entity.metadata.tags.includes('test') - ) - expect(found).toBeDefined() - }) - - it('should handle complex nested filters', async () => { - const results = await brain.find({ - where: { - $or: [ - { category: 'tech', language: 'javascript' }, - { category: 'science', field: 'ai' } - ] - } - }) - - results.forEach(r => { - const meta = r.entity.metadata - const matchesTech = meta?.category === 'tech' && meta?.language === 'javascript' - const matchesScience = meta?.category === 'science' && meta?.field === 'ai' - expect(matchesTech || matchesScience).toBe(true) - }) - }) - }) - - describe('Type Filtering', () => { - it('should filter by single noun type', async () => { - const results = await brain.find({ - type: NounType.Document - }) - - results.forEach(r => { - expect(r.entity.type).toBe(NounType.Document) - }) - }) - - it('should filter by multiple noun types', async () => { - const results = await brain.find({ - type: [NounType.Document, NounType.Concept] - }) - - results.forEach(r => { - expect([NounType.Document, NounType.Concept]).toContain(r.entity.type) - }) - }) - }) - - describe('Combined Search (Triple Intelligence)', () => { - it('should combine vector search with metadata filtering', async () => { - const results = await brain.find({ - query: 'programming', - where: { category: 'tech' }, - limit: 5 - }) - - expect(results.length).toBeGreaterThan(0) - results.forEach(r => { - expect(r.entity.metadata?.category).toBe('tech') - }) - }) - - it('should combine vector, metadata, and type filtering', async () => { - const results = await brain.find({ - query: 'artificial intelligence', - where: { category: 'science' }, - type: NounType.Concept, - limit: 5 - }) - - results.forEach(r => { - expect(r.entity.metadata?.category).toBe('science') - expect(r.entity.type).toBe(NounType.Concept) - }) - }) - - it('should support fusion strategies', async () => { - const results = await brain.find({ - query: 'JavaScript', - where: { category: 'tech' }, - fusion: { - strategy: 'adaptive', - weights: { vector: 0.7, field: 0.3 } - } - }) - - expect(results).toBeDefined() - expect(results.length).toBeGreaterThan(0) - }) - - it('should handle fusion with graph intelligence', async () => { - // Create some relationships - const items = await brain.find({ limit: 3 }) - if (items.length >= 2) { - await brain.relate({ - from: items[0].id, - to: items[1].id, - type: VerbType.References - }) - } - - const results = await brain.find({ - query: 'technology', - connected: { to: items[0]?.id }, - fusion: { - strategy: 'adaptive', - weights: { vector: 0.5, graph: 0.3, field: 0.2 } - } - }) - - expect(results).toBeDefined() - }) - }) - - describe('Performance', () => { - it('should handle large result sets efficiently', async () => { - const start = Date.now() - const results = await brain.find({ limit: 100 }) - const elapsed = Date.now() - start - - expect(elapsed).toBeLessThan(1000) // Should complete in under 1 second - expect(results.length).toBeLessThanOrEqual(100) - }) - - it('should cache repeated searches', async () => { - const query = { query: 'caching test', limit: 5 } - - // First search - const results1 = await brain.find(query) - - // Second search (should hit cache) - const results2 = await brain.find(query) - - expect(results1).toEqual(results2) - // Note: Cache might not always be faster in tests due to overhead - // But results should be identical - }) - }) - - describe('Edge Cases', () => { - it('should handle empty query gracefully', async () => { - const results = await brain.find({ query: '' }) - expect(results).toBeDefined() - expect(Array.isArray(results)).toBe(true) - }) - - it('should handle non-matching filters', async () => { - const results = await brain.find({ - where: { nonExistentField: 'value' } - }) - - expect(results).toBeDefined() - expect(Array.isArray(results)).toBe(true) - }) - - it('should handle very long queries', async () => { - const longQuery = 'test '.repeat(1000) // 5000 characters - const results = await brain.find({ query: longQuery, limit: 1 }) - - expect(results).toBeDefined() - expect(Array.isArray(results)).toBe(true) - }) - - it('should handle special characters in queries', async () => { - const specialQuery = '!@#$%^&*()_+-=[]{}|;\':",./<>?' - const results = await brain.find({ query: specialQuery, limit: 1 }) - - expect(results).toBeDefined() - expect(Array.isArray(results)).toBe(true) - }) - - it('should handle unicode in queries', async () => { - const unicodeQuery = '你好世界 🌍 مرحبا بالعالم' - const results = await brain.find({ query: unicodeQuery, limit: 1 }) - - expect(results).toBeDefined() - expect(Array.isArray(results)).toBe(true) - }) - }) - - describe('Similarity Search', () => { - it('should find similar items to a given ID', async () => { - const items = await brain.find({ limit: 1 }) - if (items.length > 0) { - const results = await brain.similar({ - to: items[0].id, - limit: 3 - }) - - expect(results).toHaveLength(3) - // First result might be the item itself - results.forEach(r => { - expect(r.score).toBeGreaterThanOrEqual(0) - expect(r.score).toBeLessThanOrEqual(1) - }) - } - }) - - it('should exclude the source item from similar results', async () => { - const items = await brain.find({ limit: 1 }) - if (items.length > 0) { - const results = await brain.similar({ - to: items[0].id, - limit: 5 - }) - - // Check if source is excluded (implementation dependent) - const selfMatch = results.find(r => r.id === items[0].id) - if (selfMatch) { - // If included, should be first with score ~1 - expect(results[0].id).toBe(items[0].id) - expect(results[0].score).toBeGreaterThan(0.99) - } - } - }) - }) -}) \ No newline at end of file diff --git a/tests/comprehensive/public-api-complete.test.ts b/tests/comprehensive/public-api-complete.unit.test.ts similarity index 100% rename from tests/comprehensive/public-api-complete.test.ts rename to tests/comprehensive/public-api-complete.unit.test.ts diff --git a/tests/integrations/core.test.ts b/tests/integrations/core.unit.test.ts similarity index 100% rename from tests/integrations/core.test.ts rename to tests/integrations/core.unit.test.ts diff --git a/tests/integrations/odata.test.ts b/tests/integrations/odata.unit.test.ts similarity index 100% rename from tests/integrations/odata.test.ts rename to tests/integrations/odata.unit.test.ts diff --git a/tests/regression/metadata-index-cleanup.test.ts b/tests/regression/metadata-index-cleanup.unit.test.ts similarity index 100% rename from tests/regression/metadata-index-cleanup.test.ts rename to tests/regression/metadata-index-cleanup.unit.test.ts diff --git a/tests/regression/v5.7.0-deadlock.test.ts b/tests/regression/v5.7.0-deadlock.unit.test.ts similarity index 100% rename from tests/regression/v5.7.0-deadlock.test.ts rename to tests/regression/v5.7.0-deadlock.unit.test.ts diff --git a/tests/streaming-pipeline.test.ts b/tests/streaming-pipeline.test.ts deleted file mode 100644 index 24e51521..00000000 --- a/tests/streaming-pipeline.test.ts +++ /dev/null @@ -1,330 +0,0 @@ -/** - * Streaming Pipeline Tests - * Tests for the new streaming data pipeline system - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { Pipeline, createPipeline } from '../src/streaming/pipeline.js' -import { Brainy } from '../src/brainy.js' -import { NounType } from '../src/types/graphTypes.js' - -describe('Streaming Pipeline', () => { - let brain: Brainy - - beforeEach(async () => { - brain = new Brainy({ requireSubtype: false, - storage: { type: 'memory' }, - warmup: false - }) - await brain.init() - }) - - afterEach(async () => { - await brain.close() - }) - - describe('Core Operations', () => { - it('should process data through pipeline', async () => { - const results: number[] = [] - - await new Pipeline() - .source(async function* () { - for (let i = 1; i <= 5; i++) { - yield i - } - }) - .map(x => x * 2) - .filter(x => x > 4) - .sink(x => { results.push(x) }) - .run() - - expect(results).toEqual([6, 8, 10]) - }) - - it('should handle async transformations', async () => { - const results: string[] = [] - - await new Pipeline() - .source(async function* () { - yield 'hello' - yield 'world' - }) - .map(async (text) => { - await new Promise(resolve => setTimeout(resolve, 10)) - return text.toUpperCase() - }) - .sink(x => { results.push(x) }) - .run() - - expect(results).toEqual(['HELLO', 'WORLD']) - }) - - it('should batch items', async () => { - const batches: number[][] = [] - - await new Pipeline() - .source(async function* () { - for (let i = 1; i <= 10; i++) { - yield i - } - }) - .batch(3) - .sink(batch => { batches.push(batch) }) - .run() - - expect(batches).toEqual([ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [10] - ]) - }) - - it('should collect all results', async () => { - const pipeline = new Pipeline() - .source(async function* () { - for (let i = 1; i <= 5; i++) { - yield i - } - }) - .map(x => x * x) - - const results = await pipeline.collect() - expect(results).toEqual([1, 4, 9, 16, 25]) - }) - }) - - describe('Window Operations', () => { - it('should support tumbling windows', async () => { - const windows: number[][] = [] - - await new Pipeline() - .source(async function* () { - for (let i = 1; i <= 10; i++) { - yield i - } - }) - .window(3, 'tumbling') - .sink(window => { windows.push(window) }) - .run() - - expect(windows).toEqual([ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], - [10] - ]) - }) - - it('should support sliding windows', async () => { - const windows: number[][] = [] - - await new Pipeline() - .source(async function* () { - for (let i = 1; i <= 5; i++) { - yield i - } - }) - .window(3, 'sliding') - .sink(window => { windows.push([...window]) }) - .run() - - expect(windows).toEqual([ - [1, 2, 3], - [2, 3, 4], - [3, 4, 5] - ]) - }) - }) - - describe('Reduce Operations', () => { - it('should reduce values', async () => { - let result = 0 - - await new Pipeline() - .source(async function* () { - for (let i = 1; i <= 5; i++) { - yield i - } - }) - .reduce((acc, val) => acc + val, 0) - .sink(sum => { result = sum }) - .run() - - expect(result).toBe(15) - }) - - it('should work with complex reducers', async () => { - interface Stats { - count: number - sum: number - max: number - } - - let stats: Stats = { count: 0, sum: 0, max: 0 } - - await new Pipeline() - .source(async function* () { - for (let i = 1; i <= 10; i++) { - yield i - } - }) - .reduce((acc, val) => ({ - count: acc.count + 1, - sum: acc.sum + val, - max: Math.max(acc.max, val) - }), { count: 0, sum: 0, max: 0 }) - .sink(s => { stats = s }) - .run() - - expect(stats).toEqual({ - count: 10, - sum: 55, - max: 10 - }) - }) - }) - - describe('Brainy Integration', () => { - it('should sink data to Brainy', async () => { - const pipeline = new Pipeline(brain) - .source(async function* () { - yield { content: 'Document 1' } - yield { content: 'Document 2' } - yield { content: 'Document 3' } - }) - .toBrainy({ - type: NounType.Document, - metadata: { source: 'pipeline' }, - batchSize: 2 - }) - - await pipeline.run() - - // Verify data was added - const results = await brain.find({ - where: { 'metadata.source': 'pipeline' } - }) - - expect(results.length).toBe(3) - }) - - it('should process and transform before storing', async () => { - const pipeline = new Pipeline(brain) - .source(async function* () { - yield 'hello world' - yield 'goodbye world' - }) - .map(text => ({ - content: text, - processed: text.toUpperCase() - })) - .toBrainy({ - type: NounType.Document, - metadata: { pipeline: true } - }) - - await pipeline.run() - - const results = await brain.find({ - where: { 'metadata.pipeline': true } - }) - - expect(results.length).toBe(2) - }) - }) - - describe('Error Handling', () => { - it('should handle errors with handler', async () => { - const errors: Error[] = [] - - await new Pipeline() - .source(async function* () { - yield 1 - yield 2 - yield 3 - }) - .map(x => { - if (x === 2) throw new Error('Test error') - return x - }) - .sink(() => {}) - .run({ - errorHandler: (error) => errors.push(error) - }) - - expect(errors.length).toBe(1) - expect(errors[0].message).toBe('Test error') - }) - - it('should stop on abort signal', async () => { - let count = 0 - - const pipeline = new Pipeline() - .source(async function* () { - for (let i = 1; i <= 100; i++) { - yield i - } - }) - .sink(() => { count++ }) - - // Start pipeline - const runPromise = pipeline.run() - - // Stop after a short delay - setTimeout(() => pipeline.stop(), 50) - - await runPromise - - // Should have processed some but not all items - expect(count).toBeGreaterThan(0) - expect(count).toBeLessThan(100) - }) - }) - - describe('Performance Features', () => { - it('should throttle sink operations', async () => { - const times: number[] = [] - const startTime = Date.now() - - await new Pipeline() - .source(async function* () { - for (let i = 1; i <= 3; i++) { - yield i - } - }) - .throttledSink( - () => { times.push(Date.now() - startTime) }, - 10 // 10 ops/sec max - ) - .run() - - // Check that operations were throttled - expect(times.length).toBe(3) - expect(times[2] - times[0]).toBeGreaterThanOrEqual(200) // At least 200ms for 3 items at 10/sec - }) - - it('should run monitoring', async () => { - const logs: string[] = [] - const originalLog = console.log - console.log = (msg: string) => logs.push(msg) - - try { - await new Pipeline() - .source(async function* () { - for (let i = 1; i <= 10; i++) { - yield i - } - }) - .sink(() => {}) - .run({ monitoring: true }) - - // Should have logged completion metrics - expect(logs.some(log => log.includes('Pipeline completed'))).toBe(true) - expect(logs.some(log => log.includes('Throughput'))).toBe(true) - } finally { - console.log = originalLog - } - }) - }) -}) \ No newline at end of file diff --git a/tests/transaction/Transaction.test.ts b/tests/transaction/Transaction.unit.test.ts similarity index 100% rename from tests/transaction/Transaction.test.ts rename to tests/transaction/Transaction.unit.test.ts diff --git a/tests/transaction/TransactionManager.test.ts b/tests/transaction/TransactionManager.unit.test.ts similarity index 100% rename from tests/transaction/TransactionManager.test.ts rename to tests/transaction/TransactionManager.unit.test.ts diff --git a/tests/transaction/integration/typeaware-transactions.test.ts b/tests/transaction/integration/typeaware-transactions.test.ts deleted file mode 100644 index 51a659c6..00000000 --- a/tests/transaction/integration/typeaware-transactions.test.ts +++ /dev/null @@ -1,368 +0,0 @@ -/** - * TypeAware + Transactions Integration Tests - * - * Verifies that transactions work correctly with type-aware storage: - * - Type-specific routing - * - Type cache updates - * - Type changes during updates - * - Per-type performance optimization - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { Brainy } from '../../../src/brainy.js' -import { NounType, VerbType } from '../../../src/types/graphTypes.js' -import { tmpdir } from 'os' -import { join } from 'path' -import { mkdirSync, rmSync } from 'fs' - -describe('Transactions + TypeAware Storage Integration', () => { - let brain: Brainy - let testDir: string - - beforeEach(async () => { - testDir = join(tmpdir(), `brainy-typeaware-test-${Date.now()}`) - mkdirSync(testDir, { recursive: true }) - - brain = new Brainy({ requireSubtype: false, - storage: { - type: 'filesystem', - path: testDir - } - }) - - await brain.init() - }) - - afterEach(async () => { - if (brain) { - await brain.shutdown() - } - if (testDir) { - rmSync(testDir, { recursive: true, force: true }) - } - }) - - describe('Type-Specific Routing', () => { - it('should route entities to type-specific storage atomically', async () => { - // Add entities of different types - const personId = await brain.add({ - data: { name: 'John Doe', role: 'Developer' }, - type: NounType.Person - }) - - const orgId = await brain.add({ - data: { name: 'Acme Corp', industry: 'Tech' }, - type: NounType.Organization - }) - - const placeId = await brain.add({ - data: { name: 'San Francisco', country: 'USA' }, - type: NounType.Place - }) - - // Verify all entities stored with correct types - const person = await brain.get(personId) - const org = await brain.get(orgId) - const place = await brain.get(placeId) - - expect(person?.type).toBe(NounType.Person) - expect(org?.type).toBe(NounType.Organization) - expect(place?.type).toBe(NounType.Place) - }) - - it('should handle type-specific queries atomically', async () => { - // Add multiple entities of same type - await brain.add({ - data: { name: 'Alice', role: 'Engineer' }, - type: NounType.Person - }) - - await brain.add({ - data: { name: 'Bob', role: 'Designer' }, - type: NounType.Person - }) - - await brain.add({ - data: { name: 'Acme', industry: 'Tech' }, - type: NounType.Organization - }) - - // Query by type - const results = await brain.find({ - filter: { type: NounType.Person } - }) - - expect(results).toHaveLength(2) - expect(results.every(r => r.type === NounType.Person)).toBe(true) - }) - }) - - describe('Type Changes During Updates', () => { - it('should handle type changes atomically in update operations', async () => { - // Add entity as Person - const id = await brain.add({ - data: { name: 'John Smith', category: 'individual' }, - type: NounType.Person - }) - - // Verify initial type - let entity = await brain.get(id) - expect(entity?.type).toBe(NounType.Person) - - // Update to Organization (type change) - await brain.update({ - id, - type: NounType.Organization, - data: { name: 'Smith Corp', category: 'business' } - }) - - // Verify type changed - entity = await brain.get(id) - expect(entity?.type).toBe(NounType.Organization) - expect(entity?.data.category).toBe('business') - }) - - it('should rollback type changes on failed update', async () => { - // Add entity as Person - const id = await brain.add({ - data: { name: 'Jane Doe' }, - type: NounType.Person - }) - - // Attempt to update with invalid data (will fail) - let failed = false - try { - await brain.update({ - id, - type: NounType.Organization, - data: null as any // Invalid - }) - } catch (e) { - failed = true - } - - expect(failed).toBe(true) - - // Type should remain Person (rollback) - const entity = await brain.get(id) - expect(entity?.type).toBe(NounType.Person) - expect(entity?.data.name).toBe('Jane Doe') - }) - }) - - describe('Type Cache Consistency', () => { - it('should maintain type cache consistency during transactions', async () => { - // Add multiple entities - const ids: string[] = [] - const types = [ - NounType.Person, - NounType.Organization, - NounType.Place, - NounType.Event, - NounType.Concept - ] - - for (let i = 0; i < types.length; i++) { - const id = await brain.add({ - data: { name: `Entity ${i}`, index: i }, - type: types[i] - }) - ids.push(id) - } - - // Verify all types cached correctly - for (let i = 0; i < ids.length; i++) { - const entity = await brain.get(ids[i]) - expect(entity?.type).toBe(types[i]) - } - }) - - it('should update type cache on type changes', async () => { - const id = await brain.add({ - data: { name: 'Initial' }, - type: NounType.Thing - }) - - // Sequence of type changes - const typeSequence = [ - NounType.Concept, - NounType.Event, - NounType.Organization, - NounType.Person - ] - - for (const newType of typeSequence) { - await brain.update({ - id, - type: newType, - data: { name: `As ${newType}` } - }) - - const entity = await brain.get(id) - expect(entity?.type).toBe(newType) - } - }) - }) - - describe('Per-Type Performance', () => { - it('should handle large numbers of same-type entities efficiently', async () => { - const startTime = Date.now() - - // Add 50 entities of same type (type-aware storage optimization) - const ids: string[] = [] - for (let i = 0; i < 50; i++) { - const id = await brain.add({ - data: { name: `Person ${i}`, index: i }, - type: NounType.Person - }) - ids.push(id) - } - - const addTime = Date.now() - startTime - - // Verify all entities stored - const retrieveStart = Date.now() - for (const id of ids) { - const entity = await brain.get(id) - expect(entity).toBeTruthy() - } - const retrieveTime = Date.now() - retrieveStart - - // Type-aware storage should be reasonably fast - expect(addTime).toBeLessThan(5000) // 5 seconds for 50 adds - expect(retrieveTime).toBeLessThan(2000) // 2 seconds for 50 retrieves - }) - }) - - describe('Mixed Type Operations', () => { - it('should handle operations across multiple types atomically', async () => { - // Create entities of different types - const personId = await brain.add({ - data: { name: 'Alice' }, - type: NounType.Person - }) - - const orgId = await brain.add({ - data: { name: 'TechCorp' }, - type: NounType.Organization - }) - - const eventId = await brain.add({ - data: { name: 'Conference 2024' }, - type: NounType.Event - }) - - // Create relationships across types (atomic) - await brain.relate({ - from: personId, - to: orgId, - type: VerbType.WorksFor - }) - - await brain.relate({ - from: personId, - to: eventId, - type: VerbType.Attends - }) - - await brain.relate({ - from: orgId, - to: eventId, - type: VerbType.Sponsors - }) - - // Verify relationships exist - const personRelations = await brain.related({ from: personId }) - const orgRelations = await brain.related({ from: orgId }) - - expect(personRelations).toHaveLength(2) - expect(orgRelations).toHaveLength(1) - }) - - it('should handle delete with cascade across types', async () => { - // Create multi-type graph - const personId = await brain.add({ - data: { name: 'Bob' }, - type: NounType.Person - }) - - const projectId = await brain.add({ - data: { name: 'Project X' }, - type: NounType.Thing - }) - - const taskId = await brain.add({ - data: { name: 'Task 1' }, - type: NounType.Thing - }) - - // Create relationships - await brain.relate({ - from: personId, - to: projectId, - type: VerbType.WorksOn - }) - - await brain.relate({ - from: projectId, - to: taskId, - type: VerbType.Contains - }) - - // Delete person (should cascade delete relationships) - await brain.remove(personId) - - // Verify person deleted - const person = await brain.get(personId) - expect(person).toBeNull() - - // Verify project and task still exist (different types) - const project = await brain.get(projectId) - const task = await brain.get(taskId) - expect(project).toBeTruthy() - expect(task).toBeTruthy() - - // Verify relationships from person are deleted - const relations = await brain.related({ from: personId }) - expect(relations).toHaveLength(0) - }) - }) - - describe('Type Validation', () => { - it('should validate types during atomic operations', async () => { - // Valid type - should succeed - const id = await brain.add({ - data: { name: 'Test' }, - type: NounType.Person - }) - - expect(id).toBeTruthy() - - // Verify type stored correctly - const entity = await brain.get(id) - expect(entity?.type).toBe(NounType.Person) - }) - - it('should handle type-specific metadata atomically', async () => { - // Add with type-specific metadata - const personId = await brain.add({ - data: { name: 'Charlie', age: 30, occupation: 'Engineer' }, - type: NounType.Person, - metadata: { verified: true, source: 'HR' } - }) - - const orgId = await brain.add({ - data: { name: 'StartupCo', employees: 50, founded: 2020 }, - type: NounType.Organization, - metadata: { verified: false, source: 'Registration' } - }) - - // Verify metadata with type context - const person = await brain.get(personId) - const org = await brain.get(orgId) - - expect(person?.metadata?.verified).toBe(true) - expect(org?.metadata?.verified).toBe(false) - }) - }) -}) diff --git a/tests/type-utils.test.ts b/tests/type-utils.unit.test.ts similarity index 100% rename from tests/type-utils.test.ts rename to tests/type-utils.unit.test.ts diff --git a/tests/unit/test-suite-coverage-guard.test.ts b/tests/unit/test-suite-coverage-guard.test.ts new file mode 100644 index 00000000..93db4421 --- /dev/null +++ b/tests/unit/test-suite-coverage-guard.test.ts @@ -0,0 +1,69 @@ +/** + * @module tests/unit/test-suite-coverage-guard + * @description Prevents a test file from silently falling outside EVERY vitest + * config (so it never runs and gives false coverage confidence — the exact drift + * that left ~27 test files un-run before 8.0). Every `*.test.ts` must either match + * a gate config (`tests/unit/**`, `tests/integration/**`, `*.unit.test.ts`, + * `*.integration.test.ts`) or be explicitly listed in MANUAL_ONLY below. + */ +import { describe, it, expect } from 'vitest' +import { readdirSync } from 'node:fs' +import { join, relative, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../..') +const testsDir = join(repoRoot, 'tests') + +function allTestFiles(dir: string, out: string[] = []): string[] { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name) + if (e.isDirectory()) allTestFiles(p, out) + else if (e.isFile() && p.endsWith('.test.ts')) out.push(relative(repoRoot, p).split('\\').join('/')) + } + return out +} + +/** + * Test files INTENTIONALLY excluded from the unit/integration gate: benchmarks, + * scale/perf measurements, package-size checks, and real-model-load checks. They + * are run manually (slow / need real resources), not in CI. Every entry is a + * conscious decision — a NEW orphan not listed here fails the guard below. + */ +const MANUAL_ONLY = new Set([ + 'tests/api/performance-benchmarks.test.ts', + 'tests/critical-neural-validation.test.ts', + 'tests/critical-performance-benchmark.test.ts', + 'tests/model-loading.test.ts', + 'tests/package-size-breakdown.test.ts', + 'tests/package-size-limit.test.ts', + 'tests/performance/graph-scale-performance.test.ts', + 'tests/performance/triple-intelligence-scale.test.ts', + 'tests/performance/typeAware.bench.test.ts' +]) + +function inGate(rel: string): boolean { + return ( + rel.startsWith('tests/unit/') || + rel.startsWith('tests/integration/') || + rel.endsWith('.unit.test.ts') || + rel.endsWith('.integration.test.ts') + ) +} + +describe('test-suite coverage guard', () => { + it('every *.test.ts runs in a gate config or is explicitly allowlisted as manual', () => { + const orphans = allTestFiles(testsDir).filter((f) => !inGate(f) && !MANUAL_ONLY.has(f)) + expect( + orphans, + 'These test files match NO vitest config and are not in MANUAL_ONLY — rename to ' + + '*.unit.test.ts / *.integration.test.ts (or move under tests/unit|integration), or add to ' + + `MANUAL_ONLY if they are benchmarks:\n${orphans.join('\n')}` + ).toEqual([]) + }) + + it('the manual allowlist has no stale entries (every listed file still exists)', () => { + const all = new Set(allTestFiles(testsDir)) + const stale = [...MANUAL_ONLY].filter((f) => !all.has(f)) + expect(stale, `MANUAL_ONLY lists files that no longer exist — remove them:\n${stale.join('\n')}`).toEqual([]) + }) +}) diff --git a/tests/vfs/tree-operations.test.ts b/tests/vfs/tree-operations.unit.test.ts similarity index 100% rename from tests/vfs/tree-operations.test.ts rename to tests/vfs/tree-operations.unit.test.ts diff --git a/tests/vfs/vfs-bulkwrite-race.test.ts b/tests/vfs/vfs-bulkwrite-race.unit.test.ts similarity index 100% rename from tests/vfs/vfs-bulkwrite-race.test.ts rename to tests/vfs/vfs-bulkwrite-race.unit.test.ts diff --git a/tests/vfs/vfs-relationships.test.ts b/tests/vfs/vfs-relationships.test.ts deleted file mode 100644 index c466cfcb..00000000 --- a/tests/vfs/vfs-relationships.test.ts +++ /dev/null @@ -1,344 +0,0 @@ -/** - * Test VFS Graph Relationships - * - * Verifies that VFS properly uses Brainy's graph relationships - * instead of metadata-based path queries - */ - -import { describe, it, expect, beforeEach } from 'vitest' -import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js' -import { Brainy } from '../../src/brainy.js' -import { VerbType } from '../../src/types/graphTypes.js' - -describe('VFS Graph Relationships', () => { - let vfs: VirtualFileSystem - let brain: Brainy - - beforeEach(async () => { - brain = new Brainy({ requireSubtype: false }) - await brain.init() - vfs = new VirtualFileSystem(brain) - await vfs.init() - }) - - it('should use proper graph relationships for directory structure', async () => { - // Create a directory structure - await vfs.mkdir('/projects') - await vfs.mkdir('/projects/brainy') - await vfs.writeFile('/projects/brainy/README.md', 'Test content') - await vfs.writeFile('/projects/brainy/package.json', '{}') - - // Get the entity IDs - const projectsId = await vfs.resolvePath('/projects') - const brainyId = await vfs.resolvePath('/projects/brainy') - const readmeId = await vfs.resolvePath('/projects/brainy/README.md') - - // Verify relationships are created properly - const projectRelations = await brain.related({ - from: projectsId, - type: VerbType.Contains - }) - - expect(projectRelations).toHaveLength(1) - expect(projectRelations[0].to).toBe(brainyId) - - // Verify brainy directory contains its files - const brainyRelations = await brain.related({ - from: brainyId, - type: VerbType.Contains - }) - - expect(brainyRelations).toHaveLength(2) - const childIds = brainyRelations.map(r => r.to) - expect(childIds).toContain(readmeId) - }) - - it('should traverse directory tree using relationships', async () => { - // Create nested structure - await vfs.mkdir('/a') - await vfs.mkdir('/a/b') - await vfs.mkdir('/a/b/c') - await vfs.writeFile('/a/b/c/file.txt', 'deep file') - - // Read directory using relationships - const contents = await vfs.readdir('/a/b/c') - expect(contents).toContain('file.txt') - - // Verify path resolution uses graph traversal - const fileId = await vfs.resolvePath('/a/b/c/file.txt') - const fileEntity = await brain.get(fileId) - expect(fileEntity?.metadata?.name).toBe('file.txt') - }) - - it('should properly handle custom relationships between files', async () => { - // Create two files - await vfs.writeFile('/doc1.md', 'Document 1') - await vfs.writeFile('/doc2.md', 'Document 2') - - // Add a custom relationship - await vfs.addRelationship('/doc1.md', '/doc2.md', VerbType.References) - - // Get relationships using proper graph API - const relationships = await vfs.getRelationships('/doc1.md') - - // Should find the reference relationship - const refRelation = relationships.find(r => r.type === VerbType.References) - expect(refRelation).toBeDefined() - - // Verify it's using actual graph relationships, not metadata - const doc1Id = await vfs.resolvePath('/doc1.md') - const doc2Id = await vfs.resolvePath('/doc2.md') - - const directRelations = await brain.related({ - from: doc1Id, - type: VerbType.References - }) - - expect(directRelations).toHaveLength(1) - expect(directRelations[0].to).toBe(doc2Id) - }) - - it('should not fall back to metadata path queries', async () => { - // Create a file - await vfs.writeFile('/test.txt', 'test') - - // Get the entity - const entity = await vfs.getEntity('/test.txt') - - // Verify the entity has proper metadata - expect(entity.metadata.path).toBe('/test.txt') - expect(entity.metadata.name).toBe('test.txt') - - // But the parent relationship should be through graph, not metadata - const rootId = await vfs.resolvePath('/') - const testId = entity.id - - // Check that root contains test.txt via relationships - const rootRelations = await brain.related({ - from: rootId, - type: VerbType.Contains - }) - - expect(rootRelations.some(r => r.to === testId)).toBe(true) - }) - - it('should efficiently query children using relationships', async () => { - // Create many files in a directory - await vfs.mkdir('/many') - for (let i = 0; i < 10; i++) { - await vfs.writeFile(`/many/file${i}.txt`, `content ${i}`) - } - - // Get directory contents - const files = await vfs.readdir('/many') - expect(files).toHaveLength(10) - - // Verify it's using relationships, not metadata queries - const manyId = await vfs.resolvePath('/many') - const relations = await brain.related({ - from: manyId, - type: VerbType.Contains - }) - - expect(relations).toHaveLength(10) - }) - - it('should handle moving files by updating relationships', async () => { - // Create source structure - await vfs.mkdir('/source') - await vfs.writeFile('/source/file.txt', 'content') - await vfs.mkdir('/dest') - - // Move file - await vfs.move('/source/file.txt', '/dest/file.txt') - - // Verify relationships are updated - const sourceId = await vfs.resolvePath('/source') - const destId = await vfs.resolvePath('/dest') - const fileId = await vfs.resolvePath('/dest/file.txt') - - // Source should not contain file anymore - const sourceRelations = await brain.related({ - from: sourceId, - type: VerbType.Contains - }) - expect(sourceRelations).toHaveLength(0) - - // Dest should contain file - const destRelations = await brain.related({ - from: destId, - type: VerbType.Contains - }) - expect(destRelations).toHaveLength(1) - expect(destRelations[0].to).toBe(fileId) - }) - - it('should support complex graph queries', async () => { - // Create interconnected structure - await vfs.mkdir('/docs') - await vfs.writeFile('/docs/main.md', 'Main doc') - await vfs.writeFile('/docs/related1.md', 'Related 1') - await vfs.writeFile('/docs/related2.md', 'Related 2') - - // Add cross-references - await vfs.addRelationship('/docs/main.md', '/docs/related1.md', VerbType.References) - await vfs.addRelationship('/docs/main.md', '/docs/related2.md', VerbType.References) - await vfs.addRelationship('/docs/related1.md', '/docs/related2.md', VerbType.References) - - // Get all related documents - const related = await vfs.getRelated('/docs/main.md') - - // Should find parent (Contains) and references - const references = related.filter(r => r.direction === 'from') - expect(references.length).toBeGreaterThanOrEqual(2) - }) - - it('should always create Contains relationship when writing files', async () => { - // This test verifies the fix for the critical bug where writeFile() - // was not creating Contains relationships for updated files - - // Test 1: New file should have Contains relationship - await vfs.writeFile('/test-new.txt', 'Hello World') - - const rootId = await vfs.resolvePath('/') - const fileEntityId = await vfs.resolvePath('/test-new.txt') - - // Check that Contains relationship exists - const relations = await brain.related({ - from: rootId, - to: fileEntityId, - type: VerbType.Contains - }) - - expect(relations).toHaveLength(1) - expect(relations[0].type).toBe(VerbType.Contains) - - // Verify readdir returns the file - const files = await vfs.readdir('/') - expect(files).toContain('test-new.txt') - - // Test 2: Updated file should maintain Contains relationship - await vfs.writeFile('/test-new.txt', 'Updated content') - - // Relationship should still exist after update - const relationsAfterUpdate = await brain.related({ - from: rootId, - to: fileEntityId, - type: VerbType.Contains - }) - - expect(relationsAfterUpdate).toHaveLength(1) - - // readdir should still work - const filesAfterUpdate = await vfs.readdir('/') - expect(filesAfterUpdate).toContain('test-new.txt') - - // Test 3: Multiple updates should not create duplicate relationships - await vfs.writeFile('/test-new.txt', 'Another update') - await vfs.writeFile('/test-new.txt', 'Yet another update') - - const finalRelations = await brain.related({ - from: rootId, - to: fileEntityId, - type: VerbType.Contains - }) - - // Should still have exactly one Contains relationship - expect(finalRelations).toHaveLength(1) - }) - - it('should repair missing Contains relationships on file update', async () => { - // This test simulates a scenario where a file exists but its Contains - // relationship is missing (could happen due to corruption or bugs) - - // Create a file normally first - await vfs.writeFile('/orphan-test.txt', 'Initial content') - - const rootId = await vfs.resolvePath('/') - const fileEntityId = await vfs.resolvePath('/orphan-test.txt') - - // Manually delete the Contains relationship to simulate the bug - const initialRelations = await brain.related({ - from: rootId, - to: fileEntityId, - type: VerbType.Contains - }) - - // Delete the relationship (simulating the corruption/bug) - for (const rel of initialRelations) { - await brain.unrelate(rel.id) - } - - // Verify the relationship is gone - const brokenRelations = await brain.related({ - from: rootId, - to: fileEntityId, - type: VerbType.Contains - }) - expect(brokenRelations).toHaveLength(0) - - // readdir should fail to find the file (the bug symptom) - const brokenList = await vfs.readdir('/') - expect(brokenList).not.toContain('orphan-test.txt') - - // Now update the file - this should repair the missing relationship - await vfs.writeFile('/orphan-test.txt', 'Fixed content') - - // Verify the relationship is restored - const fixedRelations = await brain.related({ - from: rootId, - to: fileEntityId, - type: VerbType.Contains - }) - - expect(fixedRelations).toHaveLength(1) - expect(fixedRelations[0].type).toBe(VerbType.Contains) - - // readdir should now work again - const fixedList = await vfs.readdir('/') - expect(fixedList).toContain('orphan-test.txt') - }) - - it('should create Contains relationships for files in nested directories', async () => { - // Test that the fix works for nested directory structures - - await vfs.mkdir('/level1') - await vfs.mkdir('/level1/level2') - await vfs.mkdir('/level1/level2/level3') - - // Write a file deep in the structure - await vfs.writeFile('/level1/level2/level3/deep.txt', 'Deep content') - - // Get entity IDs - const level3Id = await vfs.resolvePath('/level1/level2/level3') - const fileEntityId = await vfs.resolvePath('/level1/level2/level3/deep.txt') - - // Verify Contains relationship exists - const relations = await brain.related({ - from: level3Id, - to: fileEntityId, - type: VerbType.Contains - }) - - expect(relations).toHaveLength(1) - - // Verify readdir works at the deep level - const files = await vfs.readdir('/level1/level2/level3') - expect(files).toContain('deep.txt') - - // Update the file and verify relationship persists - await vfs.writeFile('/level1/level2/level3/deep.txt', 'Updated deep content') - - const relationsAfterUpdate = await brain.related({ - from: level3Id, - to: fileEntityId, - type: VerbType.Contains - }) - - expect(relationsAfterUpdate).toHaveLength(1) - - // readdir should still work - const filesAfterUpdate = await vfs.readdir('/level1/level2/level3') - expect(filesAfterUpdate).toContain('deep.txt') - }) -}) \ No newline at end of file