refactor: remove augmentation system and semantic type matching
Remove the entire augmentation pipeline infrastructure (52 files, ~15,000 lines) and the semantic type matching system. These were unused middleware layers adding complexity without value. What was removed: - src/augmentations/ directory (all augmentation implementations) - src/augmentationManager.ts (pipeline orchestrator) - src/types/augmentations.ts, src/types/pipelineTypes.ts - src/shared/default-augmentations.ts - Semantic type suggestion (BrainyTypes.suggestNoun/suggestVerb) - src/utils/typeMatching/ (embedding-based type matcher) What was preserved by relocating: - Import handlers (CSV, PDF, Excel) -> src/importers/handlers/ - NeuralImportAugmentation -> src/cortex/neuralImportAugmentation.ts - Type matching utilities -> heuristic inference in consumers What was simplified: - brainy.ts: operations call storage directly (no execute() wrapper) - IntegrationBase: standalone class (no BaseAugmentation parent) - BrainyTypes: validation-only (nouns, verbs, isValid*, get*) - Pipeline: direct execution (no augmentation interception) - index.ts: removed TypeSuggestion, suggestType exports - package.json: removed stale types/augmentations export Build passes, 1176 tests pass, 0 failures.
This commit is contained in:
parent
ac7a1f772c
commit
d1db3510be
97 changed files with 349 additions and 19705 deletions
|
|
@ -1,535 +0,0 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { Brainy } from '../../../src/brainy'
|
||||
import { createAddParams } from '../../helpers/test-factory'
|
||||
import { NounType, VerbType } from '../../../src/types/graphTypes'
|
||||
|
||||
/**
|
||||
* Comprehensive test suite for Brainy's built-in augmentation system
|
||||
* Tests the actual augmentation functionality that's available in production
|
||||
*/
|
||||
|
||||
describe('Brainy Built-in Augmentations', () => {
|
||||
let brain: Brainy<any>
|
||||
|
||||
beforeEach(async () => {
|
||||
brain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
augmentations: {
|
||||
cache: { enabled: true, maxSize: 1000 },
|
||||
display: { enabled: true },
|
||||
metrics: { enabled: true }
|
||||
}
|
||||
})
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
describe('1. Augmentation Registry', () => {
|
||||
it('should list all registered augmentations', async () => {
|
||||
const augmentations = brain.augmentations.list()
|
||||
expect(Array.isArray(augmentations)).toBe(true)
|
||||
expect(augmentations.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should have default augmentations', async () => {
|
||||
const augmentations = brain.augmentations.list()
|
||||
expect(augmentations).toContain('cache')
|
||||
expect(augmentations).toContain('display')
|
||||
expect(augmentations).toContain('metrics')
|
||||
})
|
||||
|
||||
it('should get augmentation by name', async () => {
|
||||
const cacheAug = brain.augmentations.get('cache')
|
||||
expect(cacheAug).toBeDefined()
|
||||
expect(cacheAug?.name).toBe('cache')
|
||||
|
||||
const displayAug = brain.augmentations.get('display')
|
||||
expect(displayAug).toBeDefined()
|
||||
expect(displayAug?.name).toBe('display')
|
||||
|
||||
const metricsAug = brain.augmentations.get('metrics')
|
||||
expect(metricsAug).toBeDefined()
|
||||
expect(metricsAug?.name).toBe('metrics')
|
||||
})
|
||||
|
||||
it('should check if augmentation exists', async () => {
|
||||
expect(brain.augmentations.has('cache')).toBe(true)
|
||||
expect(brain.augmentations.has('display')).toBe(true)
|
||||
expect(brain.augmentations.has('metrics')).toBe(true)
|
||||
expect(brain.augmentations.has('non-existent')).toBe(false)
|
||||
})
|
||||
|
||||
it('should return undefined for non-existent augmentation', async () => {
|
||||
const nonExistent = brain.augmentations.get('does-not-exist')
|
||||
expect(nonExistent).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('2. Cache Augmentation', () => {
|
||||
it('should cache get operations', async () => {
|
||||
const id = await brain.add(createAddParams({
|
||||
data: 'cached entity',
|
||||
metadata: { cached: true }
|
||||
}))
|
||||
|
||||
// First get - should load from storage
|
||||
const start1 = Date.now()
|
||||
const entity1 = await brain.get(id)
|
||||
const duration1 = Date.now() - start1
|
||||
|
||||
// Second get - should be cached (faster)
|
||||
const start2 = Date.now()
|
||||
const entity2 = await brain.get(id)
|
||||
const duration2 = Date.now() - start2
|
||||
|
||||
// Compare key properties instead of full object equality
|
||||
expect(entity1?.id).toBe(entity2?.id)
|
||||
expect(entity1?.data).toBe(entity2?.data)
|
||||
expect(entity1?.data).toBe('cached entity')
|
||||
expect(entity1?.metadata?.cached).toBe(true)
|
||||
|
||||
// Cache should make second call faster (though this might not be reliable)
|
||||
// expect(duration2).toBeLessThanOrEqual(duration1)
|
||||
})
|
||||
|
||||
it('should handle cache misses gracefully', async () => {
|
||||
// Try to get non-existent entity (v5.1.0: use valid UUID format)
|
||||
const nonExistent = await brain.get('00000000-0000-0000-0000-000000000099')
|
||||
expect(nonExistent).toBeNull()
|
||||
})
|
||||
|
||||
it('should work with find operations', async () => {
|
||||
await brain.add(createAddParams({ data: 'findable content 1' }))
|
||||
await brain.add(createAddParams({ data: 'findable content 2' }))
|
||||
|
||||
// First search
|
||||
const results1 = await brain.find({ query: 'findable' })
|
||||
|
||||
// Second search (may be cached)
|
||||
const results2 = await brain.find({ query: 'findable' })
|
||||
|
||||
expect(results1.length).toBe(results2.length)
|
||||
expect(results1.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('3. Display Augmentation', () => {
|
||||
it('should provide display functionality for entities', async () => {
|
||||
const id = await brain.add(createAddParams({
|
||||
data: 'Entity with display capabilities',
|
||||
type: NounType.Document,
|
||||
metadata: {
|
||||
title: 'Test Document',
|
||||
author: 'Test Author',
|
||||
category: 'test'
|
||||
}
|
||||
}))
|
||||
|
||||
const entity = await brain.get(id)
|
||||
expect(entity).toBeDefined()
|
||||
|
||||
// Check if getDisplay method is available
|
||||
if (entity && typeof entity.getDisplay === 'function') {
|
||||
const display = entity.getDisplay()
|
||||
expect(display).toBeDefined()
|
||||
expect(typeof display).toBe('object')
|
||||
}
|
||||
})
|
||||
|
||||
it('should work with different entity types', async () => {
|
||||
const personId = await brain.add(createAddParams({
|
||||
data: 'John Doe',
|
||||
type: NounType.Person,
|
||||
metadata: { role: 'developer', age: 30 }
|
||||
}))
|
||||
|
||||
const locationId = await brain.add(createAddParams({
|
||||
data: 'San Francisco',
|
||||
type: NounType.Location,
|
||||
metadata: { country: 'USA', population: 900000 }
|
||||
}))
|
||||
|
||||
const person = await brain.get(personId)
|
||||
const location = await brain.get(locationId)
|
||||
|
||||
expect(person).toBeDefined()
|
||||
expect(location).toBeDefined()
|
||||
|
||||
// Display should work for all types
|
||||
if (person && typeof person.getDisplay === 'function') {
|
||||
const personDisplay = person.getDisplay()
|
||||
expect(personDisplay).toBeDefined()
|
||||
}
|
||||
|
||||
if (location && typeof location.getDisplay === 'function') {
|
||||
const locationDisplay = location.getDisplay()
|
||||
expect(locationDisplay).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle entities without metadata', async () => {
|
||||
const id = await brain.add(createAddParams({
|
||||
data: 'Simple entity without metadata'
|
||||
}))
|
||||
|
||||
const entity = await brain.get(id)
|
||||
expect(entity).toBeDefined()
|
||||
|
||||
if (entity && typeof entity.getDisplay === 'function') {
|
||||
const display = entity.getDisplay()
|
||||
expect(display).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should provide schema information', async () => {
|
||||
const id = await brain.add(createAddParams({
|
||||
data: 'Entity with schema',
|
||||
metadata: {
|
||||
stringField: 'text',
|
||||
numberField: 42,
|
||||
booleanField: true,
|
||||
arrayField: [1, 2, 3]
|
||||
}
|
||||
}))
|
||||
|
||||
const entity = await brain.get(id)
|
||||
expect(entity).toBeDefined()
|
||||
|
||||
if (entity && typeof entity.getSchema === 'function') {
|
||||
const schema = entity.getSchema()
|
||||
expect(schema).toBeDefined()
|
||||
expect(typeof schema).toBe('object')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('4. Metrics Augmentation', () => {
|
||||
it('should track operations without interfering', async () => {
|
||||
// Perform various operations
|
||||
const id1 = await brain.add(createAddParams({ data: 'metrics test 1' }))
|
||||
const id2 = await brain.add(createAddParams({ data: 'metrics test 2' }))
|
||||
const id3 = await brain.add(createAddParams({ data: 'metrics test 3' }))
|
||||
|
||||
// Read operations
|
||||
await brain.get(id1)
|
||||
await brain.get(id2)
|
||||
await brain.find({ query: 'metrics' })
|
||||
|
||||
// Write operations
|
||||
await brain.update({ id: id1, data: 'updated metrics test 1' })
|
||||
await brain.delete(id3)
|
||||
|
||||
// Relationship operations
|
||||
await brain.relate({
|
||||
from: id1,
|
||||
to: id2,
|
||||
type: VerbType.RelatedTo
|
||||
})
|
||||
|
||||
// All operations should complete successfully
|
||||
const entity1 = await brain.get(id1)
|
||||
const entity2 = await brain.get(id2)
|
||||
const entity3 = await brain.get(id3)
|
||||
|
||||
expect(entity1).toBeDefined()
|
||||
expect(entity1?.data).toBe('updated metrics test 1')
|
||||
expect(entity2).toBeDefined()
|
||||
expect(entity3).toBeNull() // Deleted
|
||||
|
||||
// Check relationships (may be empty if relationship storage isn't implemented)
|
||||
const relations = await brain.getRelations(id1)
|
||||
expect(Array.isArray(relations)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle high-frequency operations', async () => {
|
||||
// Create many entities rapidly
|
||||
const promises = Array.from({ length: 50 }, (_, i) =>
|
||||
brain.add(createAddParams({
|
||||
data: `High frequency test ${i}`,
|
||||
metadata: { index: i }
|
||||
}))
|
||||
)
|
||||
|
||||
const ids = await Promise.all(promises)
|
||||
expect(ids.length).toBe(50)
|
||||
|
||||
// Perform many reads
|
||||
const readPromises = ids.map(id => brain.get(id))
|
||||
const entities = await Promise.all(readPromises)
|
||||
|
||||
expect(entities.length).toBe(50)
|
||||
expect(entities.every(e => e !== null)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle error scenarios gracefully', async () => {
|
||||
// v5.1.0: Invalid UUID formats throw errors
|
||||
await expect(brain.get('invalid-id')).rejects.toThrow('Invalid UUID format')
|
||||
await expect(brain.delete('invalid-id')).rejects.toThrow('Invalid UUID format')
|
||||
|
||||
// Try invalid find parameters
|
||||
await expect(brain.find({
|
||||
query: 'test',
|
||||
limit: -1
|
||||
} as any)).rejects.toThrow()
|
||||
|
||||
await expect(brain.find({
|
||||
query: 'test',
|
||||
offset: -1
|
||||
} as any)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('5. Augmentation Integration', () => {
|
||||
it('should apply all augmentations to add operations', async () => {
|
||||
const id = await brain.add(createAddParams({
|
||||
data: 'Full integration test',
|
||||
type: NounType.Document,
|
||||
metadata: {
|
||||
title: 'Integration Test',
|
||||
priority: 'high',
|
||||
tags: ['test', 'integration']
|
||||
}
|
||||
}))
|
||||
|
||||
expect(id).toBeDefined()
|
||||
expect(typeof id).toBe('string')
|
||||
|
||||
// Verify entity was created correctly
|
||||
const entity = await brain.get(id)
|
||||
expect(entity).toBeDefined()
|
||||
expect(entity?.data).toBe('Full integration test')
|
||||
expect(entity?.type).toBe(NounType.Document)
|
||||
expect(entity?.metadata?.title).toBe('Integration Test')
|
||||
})
|
||||
|
||||
it('should apply all augmentations to find operations', async () => {
|
||||
// Add test data
|
||||
const id1 = await brain.add(createAddParams({
|
||||
data: 'Integration search test 1',
|
||||
metadata: { category: 'integration' }
|
||||
}))
|
||||
|
||||
const id2 = await brain.add(createAddParams({
|
||||
data: 'Integration search test 2',
|
||||
metadata: { category: 'integration' }
|
||||
}))
|
||||
|
||||
await brain.add(createAddParams({
|
||||
data: 'Different content',
|
||||
metadata: { category: 'other' }
|
||||
}))
|
||||
|
||||
// Test text search
|
||||
const textResults = await brain.find({ query: 'Integration search' })
|
||||
expect(textResults.length).toBeGreaterThanOrEqual(2)
|
||||
|
||||
// Test metadata filtering
|
||||
const categoryResults = await brain.find({
|
||||
query: '',
|
||||
where: { category: 'integration' }
|
||||
})
|
||||
expect(categoryResults.length).toBe(2)
|
||||
|
||||
// Verify entities have display capabilities
|
||||
const firstResult = textResults[0]
|
||||
if (firstResult?.entity && typeof firstResult.entity.getDisplay === 'function') {
|
||||
const display = firstResult.entity.getDisplay()
|
||||
expect(display).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should apply augmentations to update operations', async () => {
|
||||
const id = await brain.add(createAddParams({
|
||||
data: 'Original data',
|
||||
metadata: { version: 1 }
|
||||
}))
|
||||
|
||||
// Update should work with all augmentations
|
||||
await brain.update({
|
||||
id,
|
||||
data: 'Updated data',
|
||||
metadata: { version: 2, updated: true }
|
||||
})
|
||||
|
||||
const updatedEntity = await brain.get(id)
|
||||
expect(updatedEntity?.data).toBe('Updated data')
|
||||
expect(updatedEntity?.metadata?.version).toBe(2)
|
||||
expect(updatedEntity?.metadata?.updated).toBe(true)
|
||||
})
|
||||
|
||||
it('should apply augmentations to relationship operations', async () => {
|
||||
const id1 = await brain.add(createAddParams({ data: 'Entity 1' }))
|
||||
const id2 = await brain.add(createAddParams({ data: 'Entity 2' }))
|
||||
|
||||
// Create relationship
|
||||
await brain.relate({
|
||||
from: id1,
|
||||
to: id2,
|
||||
type: VerbType.RelatedTo,
|
||||
metadata: { strength: 0.8 }
|
||||
})
|
||||
|
||||
// Verify relationship exists (may be empty depending on storage implementation)
|
||||
const relations = await brain.getRelations(id1)
|
||||
expect(Array.isArray(relations)).toBe(true)
|
||||
|
||||
// If relationships are stored, verify the properties
|
||||
if (relations.length > 0) {
|
||||
const relation = relations.find(r => r.to === id2)
|
||||
if (relation) {
|
||||
expect(relation.type).toBe(VerbType.RelatedTo)
|
||||
expect(relation.metadata?.strength).toBe(0.8)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('6. Performance with Augmentations', () => {
|
||||
it('should perform well with many entities', async () => {
|
||||
const start = Date.now()
|
||||
|
||||
// Create 100 entities
|
||||
const createPromises = Array.from({ length: 100 }, (_, i) =>
|
||||
brain.add(createAddParams({
|
||||
data: `Performance test entity ${i}`,
|
||||
metadata: { index: i, batch: 'performance' }
|
||||
}))
|
||||
)
|
||||
|
||||
const ids = await Promise.all(createPromises)
|
||||
const createDuration = Date.now() - start
|
||||
|
||||
expect(ids.length).toBe(100)
|
||||
expect(createDuration).toBeLessThan(5000) // Should complete in under 5 seconds
|
||||
|
||||
// Search should be fast
|
||||
const searchStart = Date.now()
|
||||
const searchResults = await brain.find({
|
||||
query: 'Performance test',
|
||||
limit: 50
|
||||
})
|
||||
const searchDuration = Date.now() - searchStart
|
||||
|
||||
expect(searchResults.length).toBeGreaterThan(0)
|
||||
expect(searchResults.length).toBeLessThanOrEqual(50) // May return fewer due to relevance
|
||||
expect(searchDuration).toBeLessThan(1000) // Should complete in under 1 second
|
||||
})
|
||||
|
||||
it('should handle concurrent operations efficiently', async () => {
|
||||
const start = Date.now()
|
||||
|
||||
// Perform 50 concurrent operations
|
||||
const operations = Array.from({ length: 50 }, (_, i) => {
|
||||
if (i % 4 === 0) {
|
||||
return brain.add(createAddParams({ data: `Concurrent add ${i}` }))
|
||||
} else if (i % 4 === 1) {
|
||||
return brain.find({ query: 'concurrent', limit: 5 })
|
||||
} else if (i % 4 === 2) {
|
||||
return brain.add(createAddParams({ data: `Another add ${i}` }))
|
||||
} else {
|
||||
return brain.find({ query: 'test', limit: 3 })
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(operations)
|
||||
const duration = Date.now() - start
|
||||
|
||||
expect(results.length).toBe(50)
|
||||
expect(duration).toBeLessThan(3000) // Should handle concurrency well
|
||||
|
||||
// Verify some results
|
||||
const addResults = results.filter(r => typeof r === 'string')
|
||||
const findResults = results.filter(r => Array.isArray(r))
|
||||
|
||||
expect(addResults.length).toBeGreaterThan(0)
|
||||
expect(findResults.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('7. Error Handling with Augmentations', () => {
|
||||
it('should handle errors gracefully without breaking augmentations', async () => {
|
||||
// v5.1.0: Invalid UUID formats throw errors (these should not crash the system)
|
||||
await expect(brain.get('')).rejects.toThrow('UUID is required')
|
||||
await expect(brain.update({
|
||||
id: 'non-existent',
|
||||
data: 'new data'
|
||||
})).rejects.toThrow('Invalid UUID format')
|
||||
|
||||
// Normal operations should still work
|
||||
const id = await brain.add(createAddParams({ data: 'After error test' }))
|
||||
const entity = await brain.get(id)
|
||||
expect(entity?.data).toBe('After error test')
|
||||
})
|
||||
|
||||
it('should handle edge case data gracefully', async () => {
|
||||
// Add entity with minimal but valid data
|
||||
const id = await brain.add(createAddParams({
|
||||
data: 'minimal', // Valid minimal data
|
||||
metadata: { test: true }
|
||||
}))
|
||||
|
||||
expect(id).toBeDefined()
|
||||
|
||||
const entity = await brain.get(id)
|
||||
expect(entity).toBeDefined()
|
||||
expect(entity?.data).toBe('minimal')
|
||||
})
|
||||
})
|
||||
|
||||
describe('8. Configuration and Customization', () => {
|
||||
it('should respect augmentation configuration', async () => {
|
||||
// Test that augmentations can be configured
|
||||
const configuredBrain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
augmentations: {
|
||||
cache: {
|
||||
enabled: true,
|
||||
maxSize: 50,
|
||||
ttl: 60000
|
||||
},
|
||||
display: {
|
||||
enabled: true,
|
||||
lazyComputation: true
|
||||
},
|
||||
metrics: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await configuredBrain.init()
|
||||
|
||||
// Should work with custom configuration
|
||||
const id = await configuredBrain.add(createAddParams({
|
||||
data: 'Configured test'
|
||||
}))
|
||||
|
||||
const entity = await configuredBrain.get(id)
|
||||
expect(entity?.data).toBe('Configured test')
|
||||
|
||||
await configuredBrain.close()
|
||||
})
|
||||
|
||||
it('should work with disabled augmentations', async () => {
|
||||
const minimalBrain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
augmentations: {
|
||||
cache: { enabled: false },
|
||||
display: { enabled: false },
|
||||
metrics: { enabled: false }
|
||||
}
|
||||
})
|
||||
|
||||
await minimalBrain.init()
|
||||
|
||||
// Should still work with augmentations disabled
|
||||
const id = await minimalBrain.add(createAddParams({
|
||||
data: 'Minimal test'
|
||||
}))
|
||||
|
||||
const entity = await minimalBrain.get(id)
|
||||
expect(entity?.data).toBe('Minimal test')
|
||||
|
||||
await minimalBrain.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,437 +0,0 @@
|
|||
/**
|
||||
* FormatHandlerRegistry Tests (v5.2.0)
|
||||
*
|
||||
* Tests for MIME-based format handler registration and routing
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { FormatHandlerRegistry } from '../../../src/augmentations/intelligentImport/FormatHandlerRegistry.js'
|
||||
import type { FormatHandler, ProcessedData } from '../../../src/augmentations/intelligentImport/types.js'
|
||||
|
||||
describe('FormatHandlerRegistry (v5.2.0)', () => {
|
||||
let registry: FormatHandlerRegistry
|
||||
|
||||
// Mock handlers for testing
|
||||
const createMockHandler = (format: string): FormatHandler => ({
|
||||
format,
|
||||
async process(): Promise<ProcessedData> {
|
||||
return {
|
||||
format,
|
||||
data: [],
|
||||
metadata: {
|
||||
rowCount: 0,
|
||||
fields: [],
|
||||
processingTime: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
canHandle(): boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new FormatHandlerRegistry()
|
||||
})
|
||||
|
||||
describe('Handler Registration', () => {
|
||||
it('should register handler with MIME types and extensions', () => {
|
||||
const csvHandler = createMockHandler('csv')
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv', 'application/csv'],
|
||||
extensions: ['.csv', '.tsv'],
|
||||
loader: async () => csvHandler
|
||||
})
|
||||
|
||||
expect(registry.hasHandler('csv')).toBe(true)
|
||||
expect(registry.getRegisteredHandlers()).toContain('csv')
|
||||
})
|
||||
|
||||
it('should register multiple handlers', () => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'excel',
|
||||
mimeTypes: ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
||||
extensions: ['.xlsx'],
|
||||
loader: async () => createMockHandler('excel')
|
||||
})
|
||||
|
||||
const handlers = registry.getRegisteredHandlers()
|
||||
expect(handlers).toContain('csv')
|
||||
expect(handlers).toContain('excel')
|
||||
expect(handlers).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should normalize extensions (remove leading dots)', () => {
|
||||
registry.registerHandler({
|
||||
name: 'test',
|
||||
mimeTypes: [],
|
||||
extensions: ['.txt', 'md'], // Mixed with/without dots
|
||||
loader: async () => createMockHandler('test')
|
||||
})
|
||||
|
||||
expect(registry.hasHandler('test')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('MIME Type Routing', () => {
|
||||
beforeEach(() => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv', 'application/csv'],
|
||||
extensions: ['.csv', '.tsv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'excel',
|
||||
mimeTypes: [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel'
|
||||
],
|
||||
extensions: ['.xlsx', '.xls'],
|
||||
loader: async () => createMockHandler('excel')
|
||||
})
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'pdf',
|
||||
mimeTypes: ['application/pdf'],
|
||||
extensions: ['.pdf'],
|
||||
loader: async () => createMockHandler('pdf')
|
||||
})
|
||||
})
|
||||
|
||||
it('should get handler by MIME type', async () => {
|
||||
const handler = await registry.getHandlerByMimeType('text/csv')
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('csv')
|
||||
})
|
||||
|
||||
it('should get handler by Excel MIME type', async () => {
|
||||
const handler = await registry.getHandlerByMimeType(
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('excel')
|
||||
})
|
||||
|
||||
it('should get handler by legacy Excel MIME type', async () => {
|
||||
const handler = await registry.getHandlerByMimeType('application/vnd.ms-excel')
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('excel')
|
||||
})
|
||||
|
||||
it('should return null for unknown MIME type', async () => {
|
||||
const handler = await registry.getHandlerByMimeType('application/unknown')
|
||||
expect(handler).toBeNull()
|
||||
})
|
||||
|
||||
it('should list handlers for a MIME type', () => {
|
||||
const handlers = registry.getHandlersForMimeType('text/csv')
|
||||
expect(handlers).toContain('csv')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Extension Routing', () => {
|
||||
beforeEach(() => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv', '.tsv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'excel',
|
||||
mimeTypes: ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
||||
extensions: ['.xlsx', '.xls'],
|
||||
loader: async () => createMockHandler('excel')
|
||||
})
|
||||
})
|
||||
|
||||
it('should get handler by extension (with dot)', async () => {
|
||||
const handler = await registry.getHandlerByExtension('.csv')
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('csv')
|
||||
})
|
||||
|
||||
it('should get handler by extension (without dot)', async () => {
|
||||
const handler = await registry.getHandlerByExtension('csv')
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('csv')
|
||||
})
|
||||
|
||||
it('should handle case-insensitive extensions', async () => {
|
||||
const handler = await registry.getHandlerByExtension('.XLSX')
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('excel')
|
||||
})
|
||||
|
||||
it('should return null for unknown extension', async () => {
|
||||
const handler = await registry.getHandlerByExtension('.xyz')
|
||||
expect(handler).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Filename-Based Handler Selection', () => {
|
||||
beforeEach(() => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'excel',
|
||||
mimeTypes: ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
||||
extensions: ['.xlsx'],
|
||||
loader: async () => createMockHandler('excel')
|
||||
})
|
||||
})
|
||||
|
||||
it('should get handler by filename (MIME detection)', async () => {
|
||||
const handler = await registry.getHandler('data.xlsx')
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('excel')
|
||||
})
|
||||
|
||||
it('should get handler by filename (extension fallback)', async () => {
|
||||
const handler = await registry.getHandler('report.csv')
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('csv')
|
||||
})
|
||||
|
||||
it('should handle full paths', async () => {
|
||||
const handler = await registry.getHandler('/path/to/file.xlsx')
|
||||
expect(handler).toBeDefined()
|
||||
expect(handler?.format).toBe('excel')
|
||||
})
|
||||
|
||||
it('should return null for unsupported filename', async () => {
|
||||
const handler = await registry.getHandler('document.txt')
|
||||
expect(handler).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Lazy Loading', () => {
|
||||
it('should lazy-load handlers on first access', async () => {
|
||||
let loadCount = 0
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => {
|
||||
loadCount++
|
||||
return createMockHandler('csv')
|
||||
}
|
||||
})
|
||||
|
||||
expect(loadCount).toBe(0)
|
||||
|
||||
const handler1 = await registry.getHandlerByName('csv')
|
||||
expect(loadCount).toBe(1)
|
||||
expect(handler1?.format).toBe('csv')
|
||||
|
||||
// Second access should use cached instance
|
||||
const handler2 = await registry.getHandlerByName('csv')
|
||||
expect(loadCount).toBe(1) // Still 1, not 2
|
||||
expect(handler2).toBe(handler1) // Same instance
|
||||
})
|
||||
|
||||
it('should cache handler instances', async () => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
const handler1 = await registry.getHandlerByExtension('.csv')
|
||||
const handler2 = await registry.getHandlerByMimeType('text/csv')
|
||||
|
||||
expect(handler2).toBe(handler1) // Same cached instance
|
||||
})
|
||||
|
||||
it('should handle loader errors gracefully', async () => {
|
||||
registry.registerHandler({
|
||||
name: 'failing',
|
||||
mimeTypes: ['application/x-failing'],
|
||||
extensions: ['.fail'],
|
||||
loader: async () => {
|
||||
throw new Error('Handler failed to load')
|
||||
}
|
||||
})
|
||||
|
||||
const handler = await registry.getHandlerByExtension('.fail')
|
||||
expect(handler).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interface Compatibility', () => {
|
||||
it('should implement HandlerRegistry interface', () => {
|
||||
// Check required properties exist
|
||||
expect(registry.handlers).toBeDefined()
|
||||
expect(registry.loaded).toBeDefined()
|
||||
expect(typeof registry.register).toBe('function')
|
||||
expect(typeof registry.getHandler).toBe('function')
|
||||
})
|
||||
|
||||
it('should support register() method for backward compatibility', async () => {
|
||||
const csvHandler = createMockHandler('csv')
|
||||
|
||||
registry.register(['.csv', '.tsv'], async () => csvHandler)
|
||||
|
||||
const handler = await registry.getHandler('.csv')
|
||||
expect(handler).toBe(csvHandler)
|
||||
})
|
||||
|
||||
it('should populate handlers Map for backward compatibility', () => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
expect(registry.handlers.has('csv')).toBe(true)
|
||||
expect(typeof registry.handlers.get('csv')).toBe('function')
|
||||
})
|
||||
|
||||
it('should populate loaded Map after loading', async () => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
expect(registry.loaded.has('csv')).toBe(false)
|
||||
|
||||
await registry.getHandlerByName('csv')
|
||||
|
||||
expect(registry.loaded.has('csv')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multiple Handlers Per MIME Type', () => {
|
||||
it('should support multiple handlers for same MIME type', () => {
|
||||
registry.registerHandler({
|
||||
name: 'csv-basic',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv-basic')
|
||||
})
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'csv-advanced',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv-advanced')
|
||||
})
|
||||
|
||||
const handlers = registry.getHandlersForMimeType('text/csv')
|
||||
expect(handlers).toHaveLength(2)
|
||||
expect(handlers).toContain('csv-basic')
|
||||
expect(handlers).toContain('csv-advanced')
|
||||
})
|
||||
|
||||
it('should return first matching handler', async () => {
|
||||
registry.registerHandler({
|
||||
name: 'csv-basic',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv-basic')
|
||||
})
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'csv-advanced',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv-advanced')
|
||||
})
|
||||
|
||||
// Should return first registered handler
|
||||
const handler = await registry.getHandlerByMimeType('text/csv')
|
||||
expect(handler?.format).toBe('csv-basic')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Registry Management', () => {
|
||||
it('should clear all handlers', () => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
expect(registry.hasHandler('csv')).toBe(true)
|
||||
|
||||
registry.clear()
|
||||
|
||||
expect(registry.hasHandler('csv')).toBe(false)
|
||||
expect(registry.getRegisteredHandlers()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should check if handler is registered', () => {
|
||||
expect(registry.hasHandler('csv')).toBe(false)
|
||||
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
expect(registry.hasHandler('csv')).toBe(true)
|
||||
expect(registry.hasHandler('excel')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle files without extensions', async () => {
|
||||
registry.registerHandler({
|
||||
name: 'csv',
|
||||
mimeTypes: ['text/csv'],
|
||||
extensions: ['.csv'],
|
||||
loader: async () => createMockHandler('csv')
|
||||
})
|
||||
|
||||
const handler = await registry.getHandler('data')
|
||||
expect(handler).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle empty extension list', () => {
|
||||
registry.registerHandler({
|
||||
name: 'special',
|
||||
mimeTypes: ['application/x-special'],
|
||||
extensions: [],
|
||||
loader: async () => createMockHandler('special')
|
||||
})
|
||||
|
||||
expect(registry.hasHandler('special')).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle empty MIME type list', async () => {
|
||||
registry.registerHandler({
|
||||
name: 'extension-only',
|
||||
mimeTypes: [],
|
||||
extensions: ['.xyz'],
|
||||
loader: async () => createMockHandler('extension-only')
|
||||
})
|
||||
|
||||
const handler = await registry.getHandlerByExtension('.xyz')
|
||||
expect(handler?.format).toBe('extension-only')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,311 +0,0 @@
|
|||
/**
|
||||
* ImageHandler Tests (v5.8.0 - Pure JavaScript)
|
||||
*
|
||||
* Tests for image processing with EXIF extraction and metadata extraction
|
||||
* Using probe-image-size + exifr (no native dependencies)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { ImageHandler } from '../../../src/augmentations/intelligentImport/handlers/imageHandler.js'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
describe('ImageHandler (v5.8.0 - Pure JS)', () => {
|
||||
let handler: ImageHandler
|
||||
|
||||
// Simple test image generators (minimal PNG/JPEG headers)
|
||||
const createMinimalJPEG = (width: number = 100, height: number = 100): Buffer => {
|
||||
// Minimal JPEG: SOI + SOF0 + EOI
|
||||
// This is a valid but minimal JPEG structure
|
||||
const soi = Buffer.from([0xFF, 0xD8]) // Start of Image
|
||||
const sof0 = Buffer.from([
|
||||
0xFF, 0xC0, // SOF0 marker
|
||||
0x00, 0x11, // Length: 17 bytes
|
||||
0x08, // Precision: 8 bits
|
||||
(height >> 8) & 0xFF, height & 0xFF, // Height
|
||||
(width >> 8) & 0xFF, width & 0xFF, // Width
|
||||
0x03, // Components: 3 (YCbCr)
|
||||
0x01, 0x22, 0x00, // Y component
|
||||
0x02, 0x11, 0x01, // Cb component
|
||||
0x03, 0x11, 0x01 // Cr component
|
||||
])
|
||||
const eoi = Buffer.from([0xFF, 0xD9]) // End of Image
|
||||
|
||||
return Buffer.concat([soi, sof0, eoi])
|
||||
}
|
||||
|
||||
const createMinimalPNG = (width: number = 100, height: number = 100): Buffer => {
|
||||
// PNG signature
|
||||
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
|
||||
|
||||
// IHDR chunk
|
||||
const ihdr = Buffer.alloc(25)
|
||||
ihdr.writeUInt32BE(13, 0) // Length
|
||||
ihdr.write('IHDR', 4)
|
||||
ihdr.writeUInt32BE(width, 8)
|
||||
ihdr.writeUInt32BE(height, 12)
|
||||
ihdr.writeUInt8(8, 16) // Bit depth
|
||||
ihdr.writeUInt8(2, 17) // Color type (RGB)
|
||||
ihdr.writeUInt8(0, 18) // Compression
|
||||
ihdr.writeUInt8(0, 19) // Filter
|
||||
ihdr.writeUInt8(0, 20) // Interlace
|
||||
ihdr.writeUInt32BE(0, 21) // CRC (simplified)
|
||||
|
||||
// IEND chunk
|
||||
const iend = Buffer.from([0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130])
|
||||
|
||||
return Buffer.concat([signature, ihdr, iend])
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new ImageHandler()
|
||||
})
|
||||
|
||||
describe('Handler Detection', () => {
|
||||
it('should identify as image format handler', () => {
|
||||
expect(handler.format).toBe('image')
|
||||
})
|
||||
|
||||
it('should handle JPEG files by filename', () => {
|
||||
expect(handler.canHandle({ filename: 'photo.jpg' })).toBe(true)
|
||||
expect(handler.canHandle({ filename: 'photo.jpeg' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle PNG files by filename', () => {
|
||||
expect(handler.canHandle({ filename: 'logo.png' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle WebP files by filename', () => {
|
||||
expect(handler.canHandle({ filename: 'image.webp' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle other image formats', () => {
|
||||
expect(handler.canHandle({ filename: 'photo.gif' })).toBe(true)
|
||||
expect(handler.canHandle({ filename: 'photo.tiff' })).toBe(true)
|
||||
expect(handler.canHandle({ filename: 'photo.bmp' })).toBe(true)
|
||||
expect(handler.canHandle({ filename: 'icon.svg' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle images by extension', () => {
|
||||
expect(handler.canHandle({ ext: '.jpg' })).toBe(true)
|
||||
expect(handler.canHandle({ ext: 'png' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject non-image files', () => {
|
||||
expect(handler.canHandle({ filename: 'document.pdf' })).toBe(false)
|
||||
expect(handler.canHandle({ filename: 'data.csv' })).toBe(false)
|
||||
expect(handler.canHandle({ filename: 'text.txt' })).toBe(false)
|
||||
})
|
||||
|
||||
it('should detect JPEG by magic bytes', () => {
|
||||
const jpegBuffer = createMinimalJPEG(100, 100)
|
||||
expect(handler.canHandle(jpegBuffer)).toBe(true)
|
||||
})
|
||||
|
||||
it('should detect PNG by magic bytes', () => {
|
||||
const pngBuffer = createMinimalPNG(100, 100)
|
||||
expect(handler.canHandle(pngBuffer)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Image Metadata Extraction', () => {
|
||||
it('should extract JPEG metadata', async () => {
|
||||
const imageBuffer = createMinimalJPEG(800, 600)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
extractEXIF: false
|
||||
})
|
||||
|
||||
expect(result.format).toBe('image')
|
||||
expect(result.data).toHaveLength(1)
|
||||
|
||||
const imageData = result.data[0]
|
||||
expect(imageData.type).toBe('media')
|
||||
expect(imageData.metadata).toBeDefined()
|
||||
expect(imageData.metadata.subtype).toBe('image')
|
||||
expect(imageData.metadata.width).toBe(800)
|
||||
expect(imageData.metadata.height).toBe(600)
|
||||
expect(imageData.metadata.format).toBe('jpg') // probe-image-size uses 'jpg' not 'jpeg'
|
||||
})
|
||||
|
||||
it('should extract PNG metadata', async () => {
|
||||
const imageBuffer = createMinimalPNG(400, 300)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
extractEXIF: false
|
||||
})
|
||||
|
||||
const imageData = result.data[0]
|
||||
expect(imageData.metadata.width).toBe(400)
|
||||
expect(imageData.metadata.height).toBe(300)
|
||||
expect(imageData.metadata.format).toBe('png')
|
||||
})
|
||||
|
||||
it('should include image size in bytes', async () => {
|
||||
const imageBuffer = createMinimalJPEG(200, 200)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
extractEXIF: false
|
||||
})
|
||||
|
||||
const imageData = result.data[0]
|
||||
expect(imageData.metadata.size).toBe(imageBuffer.length)
|
||||
expect(imageData.metadata.size).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should include MIME type when available', async () => {
|
||||
const imageBuffer = createMinimalJPEG(100, 100)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
extractEXIF: false
|
||||
})
|
||||
|
||||
const imageData = result.data[0]
|
||||
expect(imageData.metadata.mimeType).toBeDefined()
|
||||
expect(imageData.metadata.mimeType).toContain('image/')
|
||||
})
|
||||
|
||||
it('should include processing time in metadata', async () => {
|
||||
const imageBuffer = createMinimalJPEG(800, 600)
|
||||
|
||||
const result = await handler.process(imageBuffer)
|
||||
|
||||
expect(result.metadata.processingTime).toBeGreaterThanOrEqual(0)
|
||||
expect(result.metadata.processingTime).toBeLessThan(5000)
|
||||
})
|
||||
|
||||
it('should include image metadata in result metadata', async () => {
|
||||
const imageBuffer = createMinimalJPEG(800, 600)
|
||||
|
||||
const result = await handler.process(imageBuffer)
|
||||
|
||||
expect(result.metadata.imageMetadata).toBeDefined()
|
||||
expect(result.metadata.imageMetadata.width).toBe(800)
|
||||
expect(result.metadata.imageMetadata.height).toBe(600)
|
||||
})
|
||||
})
|
||||
|
||||
describe('EXIF Data Extraction', () => {
|
||||
it('should handle images without EXIF data', async () => {
|
||||
const imageBuffer = createMinimalJPEG(800, 600)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
extractEXIF: true
|
||||
})
|
||||
|
||||
const imageData = result.data[0]
|
||||
// Should not crash, EXIF will be undefined
|
||||
expect(imageData.metadata.exif).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should extract EXIF by default', async () => {
|
||||
const imageBuffer = createMinimalJPEG(800, 600)
|
||||
|
||||
// Default: EXIF extraction enabled
|
||||
const result = await handler.process(imageBuffer)
|
||||
|
||||
// Will be undefined for test images, but should not crash
|
||||
expect(result.metadata.exifData).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow disabling EXIF extraction', async () => {
|
||||
const imageBuffer = createMinimalJPEG(800, 600)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
extractEXIF: false
|
||||
})
|
||||
|
||||
const imageData = result.data[0]
|
||||
expect(imageData.metadata.exif).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle invalid image data gracefully', async () => {
|
||||
const invalidBuffer = Buffer.from('not an image', 'utf-8')
|
||||
|
||||
// Should still process but with fallback values
|
||||
const result = await handler.process(invalidBuffer, { extractEXIF: false })
|
||||
|
||||
// Fallback detection should provide basic info
|
||||
expect(result.data[0].metadata.format).toBeDefined()
|
||||
expect(result.data[0].metadata.size).toBe(invalidBuffer.length)
|
||||
})
|
||||
|
||||
it('should handle empty buffer', async () => {
|
||||
const emptyBuffer = Buffer.alloc(0)
|
||||
|
||||
// Should not crash, but will have unknown format
|
||||
const result = await handler.process(emptyBuffer, { extractEXIF: false })
|
||||
expect(result.data[0].metadata.format).toBe('unknown')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Format Support', () => {
|
||||
it('should process JPEG images', async () => {
|
||||
const imageBuffer = createMinimalJPEG(200, 200)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
extractEXIF: false
|
||||
})
|
||||
|
||||
expect(result.data[0].metadata.format).toBe('jpg') // probe-image-size uses 'jpg' not 'jpeg'
|
||||
})
|
||||
|
||||
it('should process PNG images', async () => {
|
||||
const imageBuffer = createMinimalPNG(200, 200)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
extractEXIF: false
|
||||
})
|
||||
|
||||
expect(result.data[0].metadata.format).toBe('png')
|
||||
})
|
||||
|
||||
it('should handle various image dimensions', async () => {
|
||||
const sizes = [
|
||||
[100, 100],
|
||||
[1920, 1080],
|
||||
[400, 300],
|
||||
[1000, 500]
|
||||
]
|
||||
|
||||
for (const [width, height] of sizes) {
|
||||
const imageBuffer = createMinimalJPEG(width, height)
|
||||
const result = await handler.process(imageBuffer, { extractEXIF: false })
|
||||
|
||||
expect(result.data[0].metadata.width).toBe(width)
|
||||
expect(result.data[0].metadata.height).toBe(height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration with BaseFormatHandler', () => {
|
||||
it('should inherit MIME type detection from BaseFormatHandler', () => {
|
||||
// getMimeType is protected, but we can verify behavior
|
||||
expect(handler.canHandle({ filename: 'test.jpg' })).toBe(true)
|
||||
expect(handler.canHandle({ filename: 'test.png' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should provide structured ProcessedData', async () => {
|
||||
const imageBuffer = createMinimalJPEG(200, 200)
|
||||
|
||||
const result = await handler.process(imageBuffer, {
|
||||
filename: 'test-image.jpg',
|
||||
extractEXIF: false
|
||||
})
|
||||
|
||||
// Verify ProcessedData structure
|
||||
expect(result.format).toBe('image')
|
||||
expect(result.data).toBeInstanceOf(Array)
|
||||
expect(result.metadata).toBeDefined()
|
||||
expect(result.filename).toBe('test-image.jpg')
|
||||
|
||||
// Verify data structure
|
||||
const entity = result.data[0]
|
||||
expect(entity.name).toBe('test-image')
|
||||
expect(entity.type).toBe('media')
|
||||
expect(entity.metadata).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -15,13 +15,7 @@ describe('Brainy 3.0 Core (Unit Tests)', () => {
|
|||
beforeEach(async () => {
|
||||
// Create instance with real embeddings for production-ready tests
|
||||
brain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
augmentations: {
|
||||
cache: false,
|
||||
metrics: false,
|
||||
display: false,
|
||||
monitoring: false
|
||||
}
|
||||
storage: { type: 'memory' }
|
||||
})
|
||||
|
||||
await brain.init()
|
||||
|
|
@ -196,7 +190,7 @@ describe('Brainy 3.0 Core (Unit Tests)', () => {
|
|||
})
|
||||
|
||||
describe('Statistics and Metadata', () => {
|
||||
it('should track statistics through augmentations', async () => {
|
||||
it('should track statistics', async () => {
|
||||
await brain.add({
|
||||
data: { name: 'Test1' },
|
||||
type: NounType.Concept
|
||||
|
|
|
|||
|
|
@ -482,24 +482,6 @@ describe('Brainy.add()', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('augmentations', () => {
|
||||
it('should apply augmentations during add', async () => {
|
||||
// This would test augmentation pipeline if configured
|
||||
// For now, just verify the entity is added correctly
|
||||
const params = createAddParams({
|
||||
data: 'Augmentation test',
|
||||
type: 'thing',
|
||||
metadata: { augment: true }
|
||||
})
|
||||
|
||||
const id = await brain.add(params)
|
||||
const entity = await brain.get(id)
|
||||
|
||||
expect(entity).not.toBeNull()
|
||||
expect(entity!.metadata.augment).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('caching behavior', () => {
|
||||
it('should retrieve consistent entities', async () => {
|
||||
// Arrange (v5.1.0: use valid UUID format)
|
||||
|
|
|
|||
|
|
@ -18,13 +18,7 @@ describe('add() Performance Regression Tests', () => {
|
|||
beforeEach(async () => {
|
||||
brain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
silent: true,
|
||||
augmentations: {
|
||||
cache: false,
|
||||
metrics: false,
|
||||
display: false,
|
||||
monitoring: false
|
||||
}
|
||||
silent: true
|
||||
})
|
||||
await brain.init()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -214,7 +214,6 @@ describe('Brainy plugin integration', () => {
|
|||
const brain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
silent: true,
|
||||
augmentations: { cache: false, metrics: false, display: false, monitoring: false }
|
||||
})
|
||||
brain.use(mockPlugin)
|
||||
await brain.init()
|
||||
|
|
@ -244,7 +243,6 @@ describe('Brainy plugin integration', () => {
|
|||
const brain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
silent: true,
|
||||
augmentations: { cache: false, metrics: false, display: false, monitoring: false }
|
||||
})
|
||||
brain.use(mockPlugin)
|
||||
await brain.init()
|
||||
|
|
@ -263,7 +261,6 @@ describe('Brainy plugin integration', () => {
|
|||
const brain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
silent: true,
|
||||
augmentations: { cache: false, metrics: false, display: false, monitoring: false }
|
||||
})
|
||||
brain.use(mockPlugin)
|
||||
await brain.init()
|
||||
|
|
@ -293,7 +290,6 @@ describe('Brainy plugin integration', () => {
|
|||
const brain = new Brainy({
|
||||
storage: { type: 'memory' },
|
||||
silent: true,
|
||||
augmentations: { cache: false, metrics: false, display: false, monitoring: false }
|
||||
})
|
||||
brain.use(mockPlugin)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,219 +0,0 @@
|
|||
/**
|
||||
* Tests for Intelligent Type Matching with embeddings
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { IntelligentTypeMatcher } from '../../src/augmentations/typeMatching/intelligentTypeMatcher.js'
|
||||
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
||||
|
||||
describe('Intelligent Type Matching', () => {
|
||||
let matcher: IntelligentTypeMatcher
|
||||
|
||||
beforeAll(async () => {
|
||||
matcher = new IntelligentTypeMatcher()
|
||||
await matcher.init()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await matcher.dispose()
|
||||
})
|
||||
|
||||
describe('Noun Type Detection', () => {
|
||||
it('should detect Person type from user data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
age: 30
|
||||
})
|
||||
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.confidence).toBeGreaterThan(0)
|
||||
expect(result.alternatives).toBeDefined()
|
||||
expect(result.alternatives.length).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it('should detect Organization type from company data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
companyName: 'Acme Corp',
|
||||
employees: 500,
|
||||
industry: 'Technology'
|
||||
})
|
||||
|
||||
// With real type embeddings (v3.33.0+), type detection uses actual semantic similarity
|
||||
// Results depend on the embedding quality and field patterns
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.confidence).toBeGreaterThan(0.1)
|
||||
})
|
||||
|
||||
it('should detect Location type from geographic data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
city: 'San Francisco'
|
||||
})
|
||||
|
||||
// With real type embeddings (v3.33.0+), geographic data detection is semantic
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.confidence).toBeGreaterThan(0.1)
|
||||
})
|
||||
|
||||
it('should detect Document type from text content', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
title: 'Research Paper',
|
||||
content: 'Abstract: This paper discusses...',
|
||||
author: 'Dr. Smith',
|
||||
pages: 20
|
||||
})
|
||||
|
||||
// With real type embeddings (v3.33.0+), could match various types based on semantic similarity
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.confidence).toBeGreaterThan(0.0)
|
||||
})
|
||||
|
||||
it('should detect Product type from commercial data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
price: 99.99,
|
||||
sku: 'PROD-123',
|
||||
inventory: 50,
|
||||
productId: 'abc-123'
|
||||
})
|
||||
|
||||
// With real type embeddings (v3.33.0+), commercial data uses semantic matching
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.confidence).toBeGreaterThan(0.1)
|
||||
})
|
||||
|
||||
it('should detect Event type from temporal data', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
startTime: '2024-01-01',
|
||||
endTime: '2024-01-02',
|
||||
attendees: 100,
|
||||
eventType: 'conference'
|
||||
})
|
||||
|
||||
// With real type embeddings (v3.33.0+), temporal data uses semantic matching
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.confidence).toBeGreaterThan(0.1)
|
||||
})
|
||||
|
||||
it('should handle ambiguous data with alternatives', async () => {
|
||||
const result = await matcher.matchNounType({
|
||||
value: 100,
|
||||
type: 'unknown',
|
||||
data: 'mixed'
|
||||
})
|
||||
|
||||
expect(result.type).toBeDefined()
|
||||
expect(result.alternatives).toBeDefined()
|
||||
expect(result.alternatives.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Verb Type Detection', () => {
|
||||
it('should detect MemberOf relationship', async () => {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'user1', type: 'person' },
|
||||
{ id: 'org1', type: 'organization' },
|
||||
'memberOf'
|
||||
)
|
||||
|
||||
// With mocked embeddings, type detection is non-deterministic
|
||||
expect(result.type).toBeDefined()
|
||||
expect(Object.values(VerbType)).toContain(result.type)
|
||||
expect(result.confidence).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should detect CreatedBy relationship', async () => {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'doc1', type: 'document' },
|
||||
{ id: 'author1', type: 'person' },
|
||||
'created by'
|
||||
)
|
||||
|
||||
// With mocked embeddings, type detection is non-deterministic
|
||||
expect(result.type).toBeDefined()
|
||||
expect(Object.values(VerbType)).toContain(result.type)
|
||||
expect(result.confidence).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should detect Contains relationship', async () => {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'folder1', type: 'collection' },
|
||||
{ id: 'file1', type: 'file' },
|
||||
'contains'
|
||||
)
|
||||
|
||||
// With mocked embeddings, type detection is non-deterministic
|
||||
expect(result.type).toBeDefined()
|
||||
expect(Object.values(VerbType)).toContain(result.type)
|
||||
expect(result.confidence).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle unknown relationships with defaults', async () => {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'a' },
|
||||
{ id: 'b' },
|
||||
'somehow connected'
|
||||
)
|
||||
|
||||
expect(result.type).toBeDefined()
|
||||
expect(Object.values(VerbType)).toContain(result.type)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Type Coverage', () => {
|
||||
it('should have embeddings for all 42 noun types', async () => {
|
||||
const nounTypes = Object.values(NounType)
|
||||
expect(nounTypes.length).toBe(42)
|
||||
|
||||
// Test that each type can be matched
|
||||
for (const nounType of nounTypes) {
|
||||
const result = await matcher.matchNounType({
|
||||
type: nounType,
|
||||
test: 'coverage check'
|
||||
})
|
||||
expect(result.type).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should have embeddings for all 127 verb types', async () => {
|
||||
const verbTypes = Object.values(VerbType)
|
||||
expect(verbTypes.length).toBe(127)
|
||||
|
||||
// Test that each type can be matched
|
||||
for (const verbType of verbTypes) {
|
||||
const result = await matcher.matchVerbType(
|
||||
{ id: 'source' },
|
||||
{ id: 'target' },
|
||||
verbType
|
||||
)
|
||||
expect(result.type).toBeDefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cache Performance', () => {
|
||||
it('should cache repeated type matches', async () => {
|
||||
const testData = { name: 'Cache Test', value: 123 }
|
||||
|
||||
const result1 = await matcher.matchNounType(testData)
|
||||
const result2 = await matcher.matchNounType(testData)
|
||||
|
||||
expect(result1.type).toBe(result2.type)
|
||||
expect(result1.confidence).toBe(result2.confidence)
|
||||
// Cache should return consistent results
|
||||
expect(result2.type).toBeDefined()
|
||||
})
|
||||
|
||||
it('should clear cache when requested', async () => {
|
||||
const testData = { name: 'Clear Cache Test' }
|
||||
|
||||
await matcher.matchNounType(testData) // Populate cache
|
||||
matcher.clearCache()
|
||||
|
||||
// Should still work after cache clear
|
||||
const result = await matcher.matchNounType(testData)
|
||||
expect(result.type).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue