test(8.0): re-home orphaned test files into the gate + guard against recurrence
27 test files matched no vitest config, so they never ran in CI and gave false coverage confidence (the drift the audit flagged). - Re-homed 10 functional suites into the gate (renamed to *.unit.test.ts / *.integration.test.ts): Transaction + TransactionManager, type-utils, integrations/core + odata, comprehensive/public-api-complete, regression (metadata-index-cleanup + v5.7.0-deadlock), vfs/tree-operations + vfs-bulkwrite-race. ~192 previously-dark tests now run and pass. - Deleted 8 bit-rotted/redundant files that fail against 8.0 and never ran: one had a literal syntax error; one used filesystem writer-locks that polluted parallel runs; the rest assert old "Brainy 3.0/v3.0" APIs already covered by the live suites (api/batch-operations + crud-operations-enhanced, brainy-3, comprehensive/core-api + find-triple-intelligence, streaming-pipeline, vfs/vfs-relationships, transaction/integration/typeaware-transactions). - Added a coverage guard (tests/unit/test-suite-coverage-guard.test.ts) that FAILS CI if any *.test.ts ever again falls outside every config, with an explicit, conscious MANUAL_ONLY allowlist for the 9 genuine benchmark / perf / model-load files that are run by hand. Gate green: unit 1712, integration 607. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5f974abc8a
commit
3f9f140c8c
19 changed files with 69 additions and 3658 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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<string, number[]> = 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<void> {
|
||||
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: '<script>alert("XSS")</script>', 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<string>()
|
||||
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',
|
||||
'<script>alert("xss")</script>': '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
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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<number>()
|
||||
.source(async function* () {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
yield i
|
||||
}
|
||||
})
|
||||
.reduce<Stats>((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
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
69
tests/unit/test-suite-coverage-guard.test.ts
Normal file
69
tests/unit/test-suite-coverage-guard.test.ts
Normal file
|
|
@ -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<string>([
|
||||
'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([])
|
||||
})
|
||||
})
|
||||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue