feat: add neural extraction APIs with NounType taxonomy
Add brain.extract() and brain.extractConcepts() methods that use NeuralEntityExtractor with embeddings and sophisticated NounType taxonomy (30+ entity types) for semantic entity and concept extraction. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
27cc699555
commit
dd50d89ad6
41 changed files with 3807 additions and 7391 deletions
|
|
@ -1,691 +0,0 @@
|
|||
/**
|
||||
* Knowledge Layer Tests
|
||||
*
|
||||
* Comprehensive tests for all Knowledge Layer components integrated with VFS:
|
||||
* - EventRecorder
|
||||
* - SemanticVersioning
|
||||
* - PersistentEntitySystem
|
||||
* - ConceptSystem
|
||||
* - GitBridge
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { Brainy } from '../../src/brainy.js'
|
||||
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
|
||||
import { EventRecorder } from '../../src/vfs/EventRecorder.js'
|
||||
import { SemanticVersioning } from '../../src/vfs/SemanticVersioning.js'
|
||||
import { PersistentEntitySystem } from '../../src/vfs/PersistentEntitySystem.js'
|
||||
import { ConceptSystem } from '../../src/vfs/ConceptSystem.js'
|
||||
import { GitBridge } from '../../src/vfs/GitBridge.js'
|
||||
import { tmpdir } from 'os'
|
||||
import { promises as fs } from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
describe('Knowledge Layer', () => {
|
||||
let brain: Brainy
|
||||
let vfs: VirtualFileSystem
|
||||
let tempDir: string
|
||||
|
||||
beforeEach(async () => {
|
||||
// Use in-memory storage for tests
|
||||
brain = new Brainy({
|
||||
storage: { type: 'memory' }
|
||||
})
|
||||
await brain.init()
|
||||
|
||||
vfs = new VirtualFileSystem(brain)
|
||||
await vfs.init()
|
||||
|
||||
// Create temporary directory for Git tests
|
||||
tempDir = await fs.mkdtemp(path.join(tmpdir(), 'brainy-knowledge-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up temporary directory
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
describe('VFS Knowledge Integration', () => {
|
||||
it('should have VFS integrated with Brainy', async () => {
|
||||
// VFS should be properly integrated with Brainy
|
||||
expect(vfs).toBeDefined()
|
||||
expect(brain.vfs()).toBeDefined()
|
||||
|
||||
// Test basic VFS operations
|
||||
const exists = await vfs.exists('/')
|
||||
expect(exists).toBe(true)
|
||||
})
|
||||
|
||||
it('should support knowledge layer operations through VFS', async () => {
|
||||
// Test that VFS supports file operations that the knowledge layer depends on
|
||||
await vfs.writeFile('/test.txt', 'test content')
|
||||
const content = await vfs.readFile('/test.txt')
|
||||
expect(content.toString()).toBe('test content')
|
||||
|
||||
// Test that file exists
|
||||
const exists = await vfs.exists('/test.txt')
|
||||
expect(exists).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('EventRecorder', () => {
|
||||
let eventRecorder: EventRecorder
|
||||
|
||||
beforeEach(() => {
|
||||
eventRecorder = new EventRecorder(brain)
|
||||
})
|
||||
|
||||
it('should record file events', async () => {
|
||||
const eventId = await eventRecorder.recordEvent({
|
||||
type: 'write',
|
||||
path: '/test.txt',
|
||||
content: Buffer.from('hello world'),
|
||||
size: 11,
|
||||
author: 'test-user'
|
||||
})
|
||||
|
||||
expect(eventId).toBeDefined()
|
||||
})
|
||||
|
||||
it('should retrieve file history', async () => {
|
||||
// Record multiple events
|
||||
await eventRecorder.recordEvent({
|
||||
type: 'create',
|
||||
path: '/test.txt',
|
||||
content: Buffer.from('initial'),
|
||||
size: 7
|
||||
})
|
||||
|
||||
await eventRecorder.recordEvent({
|
||||
type: 'write',
|
||||
path: '/test.txt',
|
||||
content: Buffer.from('updated'),
|
||||
size: 7
|
||||
})
|
||||
|
||||
const history = await eventRecorder.getHistory('/test.txt')
|
||||
|
||||
expect(history).toHaveLength(2)
|
||||
expect(history[0].type).toBe('write') // Newest first
|
||||
expect(history[1].type).toBe('create')
|
||||
})
|
||||
|
||||
it('should reconstruct file at specific time', async () => {
|
||||
const timestamp1 = Date.now()
|
||||
|
||||
await eventRecorder.recordEvent({
|
||||
type: 'create',
|
||||
path: '/test.txt',
|
||||
content: Buffer.from('version 1'),
|
||||
size: 9
|
||||
})
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
const timestamp2 = Date.now()
|
||||
|
||||
await eventRecorder.recordEvent({
|
||||
type: 'write',
|
||||
path: '/test.txt',
|
||||
content: Buffer.from('version 2'),
|
||||
size: 9
|
||||
})
|
||||
|
||||
// Reconstruct at timestamp1 (should get version 1)
|
||||
const content1 = await eventRecorder.reconstructFileAtTime('/test.txt', timestamp1)
|
||||
expect(content1?.toString()).toBe('version 1')
|
||||
|
||||
// Reconstruct at timestamp2 (should get version 2)
|
||||
const content2 = await eventRecorder.reconstructFileAtTime('/test.txt', timestamp2)
|
||||
expect(content2?.toString()).toBe('version 2')
|
||||
})
|
||||
|
||||
it('should calculate statistics', async () => {
|
||||
await eventRecorder.recordEvent({
|
||||
type: 'write',
|
||||
path: '/test.txt',
|
||||
size: 100,
|
||||
author: 'user1'
|
||||
})
|
||||
|
||||
await eventRecorder.recordEvent({
|
||||
type: 'append',
|
||||
path: '/test.txt',
|
||||
size: 50,
|
||||
author: 'user2'
|
||||
})
|
||||
|
||||
const stats = await eventRecorder.getStatistics('/test.txt')
|
||||
|
||||
expect(stats.totalEvents).toBe(2)
|
||||
expect(stats.totalWrites).toBe(2)
|
||||
expect(stats.totalBytes).toBe(150)
|
||||
expect(stats.authors).toContain('user1')
|
||||
expect(stats.authors).toContain('user2')
|
||||
})
|
||||
|
||||
it('should find temporal coupling', async () => {
|
||||
const now = Date.now()
|
||||
|
||||
// Record related events within time window
|
||||
await eventRecorder.recordEvent({
|
||||
type: 'write',
|
||||
path: '/file1.txt'
|
||||
})
|
||||
|
||||
await eventRecorder.recordEvent({
|
||||
type: 'write',
|
||||
path: '/file2.txt'
|
||||
})
|
||||
|
||||
const coupling = await eventRecorder.findTemporalCoupling('/file1.txt', 60000)
|
||||
|
||||
expect(coupling.has('/file2.txt')).toBe(true)
|
||||
expect(coupling.get('/file2.txt')).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SemanticVersioning', () => {
|
||||
let semanticVersioning: SemanticVersioning
|
||||
|
||||
beforeEach(() => {
|
||||
semanticVersioning = new SemanticVersioning(brain, {
|
||||
threshold: 0.3,
|
||||
maxVersions: 5
|
||||
})
|
||||
})
|
||||
|
||||
it('should detect when versioning is needed', async () => {
|
||||
const oldContent = Buffer.from('hello world')
|
||||
const newContent = Buffer.from('goodbye world')
|
||||
|
||||
const shouldVersion = await semanticVersioning.shouldVersion(oldContent, newContent)
|
||||
|
||||
// Should version due to content change (no embeddings available)
|
||||
expect(shouldVersion).toBe(true)
|
||||
})
|
||||
|
||||
it('should not version identical content', async () => {
|
||||
const content = Buffer.from('hello world')
|
||||
|
||||
const shouldVersion = await semanticVersioning.shouldVersion(content, content)
|
||||
|
||||
expect(shouldVersion).toBe(false)
|
||||
})
|
||||
|
||||
it('should create versions', async () => {
|
||||
const content = Buffer.from('version 1 content')
|
||||
|
||||
const versionId = await semanticVersioning.createVersion(
|
||||
'/test.txt',
|
||||
content,
|
||||
{ author: 'test-user', message: 'Initial version' }
|
||||
)
|
||||
|
||||
expect(versionId).toBeDefined()
|
||||
|
||||
// Check version exists
|
||||
const versions = await semanticVersioning.getVersions('/test.txt')
|
||||
expect(versions).toHaveLength(1)
|
||||
expect(versions[0].version).toBe(1)
|
||||
expect(versions[0].author).toBe('test-user')
|
||||
})
|
||||
|
||||
it('should retrieve version content', async () => {
|
||||
const content = Buffer.from('test content')
|
||||
|
||||
const versionId = await semanticVersioning.createVersion('/test.txt', content)
|
||||
|
||||
const retrievedContent = await semanticVersioning.getVersion('/test.txt', versionId)
|
||||
|
||||
expect(retrievedContent).toEqual(content)
|
||||
})
|
||||
|
||||
it('should provide version history', async () => {
|
||||
// Create multiple versions
|
||||
await semanticVersioning.createVersion('/test.txt', Buffer.from('v1'))
|
||||
await semanticVersioning.createVersion('/test.txt', Buffer.from('v2'))
|
||||
await semanticVersioning.createVersion('/test.txt', Buffer.from('v3'))
|
||||
|
||||
const history = await semanticVersioning.getVersionHistory('/test.txt')
|
||||
|
||||
expect(history).toHaveLength(3)
|
||||
expect(history[0].version.version).toBe(3) // Newest first
|
||||
expect(history[2].version.version).toBe(1) // Oldest last
|
||||
})
|
||||
|
||||
it('should prune old versions', async () => {
|
||||
// Create more versions than maxVersions
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
await semanticVersioning.createVersion('/test.txt', Buffer.from(`version ${i}`))
|
||||
}
|
||||
|
||||
const versions = await semanticVersioning.getVersions('/test.txt')
|
||||
|
||||
// Should have pruned to maxVersions (5)
|
||||
expect(versions.length).toBeLessThanOrEqual(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PersistentEntitySystem', () => {
|
||||
let entitySystem: PersistentEntitySystem
|
||||
|
||||
beforeEach(() => {
|
||||
entitySystem = new PersistentEntitySystem(brain, {
|
||||
autoExtract: false,
|
||||
evolutionTracking: true
|
||||
})
|
||||
})
|
||||
|
||||
it('should create persistent entities', async () => {
|
||||
const entityId = await entitySystem.createEntity({
|
||||
name: 'John Doe',
|
||||
type: 'character',
|
||||
aliases: ['Johnny', 'JD'],
|
||||
attributes: { age: 30, role: 'protagonist' }
|
||||
})
|
||||
|
||||
expect(entityId).toBeDefined()
|
||||
})
|
||||
|
||||
it('should find entities by name', async () => {
|
||||
await entitySystem.createEntity({
|
||||
name: 'Alice Smith',
|
||||
type: 'character',
|
||||
aliases: ['Alice'],
|
||||
attributes: {}
|
||||
})
|
||||
|
||||
const entities = await entitySystem.findEntity({ name: 'Alice Smith' })
|
||||
|
||||
expect(entities).toHaveLength(1)
|
||||
expect(entities[0].name).toBe('Alice Smith')
|
||||
expect(entities[0].type).toBe('character')
|
||||
})
|
||||
|
||||
it('should record entity appearances', async () => {
|
||||
const entityId = await entitySystem.createEntity({
|
||||
name: 'API Gateway',
|
||||
type: 'service',
|
||||
aliases: [],
|
||||
attributes: {}
|
||||
})
|
||||
|
||||
const appearanceId = await entitySystem.recordAppearance(
|
||||
entityId,
|
||||
'/docs/architecture.md',
|
||||
'The API Gateway handles all incoming requests...',
|
||||
{ confidence: 0.9 }
|
||||
)
|
||||
|
||||
expect(appearanceId).toBeDefined()
|
||||
|
||||
// Check appearances
|
||||
const appearances = await entitySystem.findAppearances(entityId)
|
||||
expect(appearances).toHaveLength(1)
|
||||
expect(appearances[0].filePath).toBe('/docs/architecture.md')
|
||||
expect(appearances[0].confidence).toBe(0.9)
|
||||
})
|
||||
|
||||
it('should track entity evolution', async () => {
|
||||
const entityId = await entitySystem.createEntity({
|
||||
name: 'UserService',
|
||||
type: 'service',
|
||||
aliases: [],
|
||||
attributes: { version: '1.0' }
|
||||
})
|
||||
|
||||
// Evolve the entity
|
||||
await entitySystem.evolveEntity(
|
||||
entityId,
|
||||
{ attributes: { version: '2.0', newFeature: true } },
|
||||
'/src/user-service.ts',
|
||||
'Added new feature'
|
||||
)
|
||||
|
||||
const { entity, timeline } = await entitySystem.getEvolution(entityId)
|
||||
|
||||
expect(entity.version).toBeGreaterThan(1)
|
||||
expect(entity.attributes.version).toBe('2.0')
|
||||
expect(entity.attributes.newFeature).toBe(true)
|
||||
})
|
||||
|
||||
it('should extract entities from content', async () => {
|
||||
entitySystem = new PersistentEntitySystem(brain, { autoExtract: true })
|
||||
|
||||
const content = Buffer.from(`
|
||||
class UserService {
|
||||
function authenticate(user) {
|
||||
// Authentication logic
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const entityIds = await entitySystem.extractEntities('/src/user.ts', content)
|
||||
|
||||
expect(entityIds.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should update references when files move', async () => {
|
||||
const entityId = await entitySystem.createEntity({
|
||||
name: 'TestEntity',
|
||||
type: 'test',
|
||||
aliases: [],
|
||||
attributes: {}
|
||||
})
|
||||
|
||||
await entitySystem.recordAppearance(
|
||||
entityId,
|
||||
'/old/path.txt',
|
||||
'test context'
|
||||
)
|
||||
|
||||
// Move file
|
||||
await entitySystem.updateReferences('/old/path.txt', '/new/path.txt')
|
||||
|
||||
// Check that appearances were updated
|
||||
const appearances = await entitySystem.findAppearances(entityId)
|
||||
expect(appearances[0].filePath).toBe('/new/path.txt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ConceptSystem', () => {
|
||||
let conceptSystem: ConceptSystem
|
||||
|
||||
beforeEach(() => {
|
||||
conceptSystem = new ConceptSystem(brain, {
|
||||
autoLink: false,
|
||||
similarityThreshold: 0.7
|
||||
})
|
||||
})
|
||||
|
||||
it('should create universal concepts', async () => {
|
||||
const conceptId = await conceptSystem.createConcept({
|
||||
name: 'Authentication',
|
||||
domain: 'technical',
|
||||
category: 'pattern',
|
||||
keywords: ['auth', 'security', 'login'],
|
||||
strength: 0.8,
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
expect(conceptId).toBeDefined()
|
||||
})
|
||||
|
||||
it('should find concepts by criteria', async () => {
|
||||
await conceptSystem.createConcept({
|
||||
name: 'User Experience',
|
||||
domain: 'business',
|
||||
category: 'concept',
|
||||
keywords: ['UX', 'usability'],
|
||||
strength: 0.9,
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
const concepts = await conceptSystem.findConcepts({ domain: 'business' })
|
||||
|
||||
expect(concepts).toHaveLength(1)
|
||||
expect(concepts[0].name).toBe('User Experience')
|
||||
expect(concepts[0].domain).toBe('business')
|
||||
})
|
||||
|
||||
it('should link concepts together', async () => {
|
||||
const concept1Id = await conceptSystem.createConcept({
|
||||
name: 'Authentication',
|
||||
domain: 'technical',
|
||||
category: 'pattern',
|
||||
keywords: [],
|
||||
strength: 0.8,
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
const concept2Id = await conceptSystem.createConcept({
|
||||
name: 'Security',
|
||||
domain: 'technical',
|
||||
category: 'concept',
|
||||
keywords: [],
|
||||
strength: 0.9,
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
const linkId = await conceptSystem.linkConcept(
|
||||
concept1Id,
|
||||
concept2Id,
|
||||
'related',
|
||||
{ strength: 0.8, bidirectional: true }
|
||||
)
|
||||
|
||||
expect(linkId).toBeDefined()
|
||||
})
|
||||
|
||||
it('should record concept manifestations', async () => {
|
||||
const conceptId = await conceptSystem.createConcept({
|
||||
name: 'Dependency Injection',
|
||||
domain: 'technical',
|
||||
category: 'pattern',
|
||||
keywords: [],
|
||||
strength: 0.8,
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
const manifestationId = await conceptSystem.recordManifestation(
|
||||
conceptId,
|
||||
'/src/di-container.ts',
|
||||
'class DIContainer implements dependency injection pattern...',
|
||||
'implementation',
|
||||
{ confidence: 0.9 }
|
||||
)
|
||||
|
||||
expect(manifestationId).toBeDefined()
|
||||
|
||||
// Check manifestations
|
||||
const manifestations = await conceptSystem.findAppearances(conceptId)
|
||||
expect(manifestations).toHaveLength(1)
|
||||
expect(manifestations[0].form).toBe('implementation')
|
||||
})
|
||||
|
||||
it('should extract concepts from content', async () => {
|
||||
conceptSystem = new ConceptSystem(brain, { autoLink: true })
|
||||
|
||||
const content = Buffer.from(`
|
||||
Authentication is a critical security concept.
|
||||
The authentication service validates user credentials.
|
||||
`)
|
||||
|
||||
const conceptIds = await conceptSystem.extractAndLinkConcepts('/docs/auth.md', content)
|
||||
|
||||
expect(conceptIds.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should generate concept graph', async () => {
|
||||
// Create test concepts
|
||||
const concept1Id = await conceptSystem.createConcept({
|
||||
name: 'API Design',
|
||||
domain: 'technical',
|
||||
category: 'concept',
|
||||
keywords: [],
|
||||
strength: 0.8,
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
const concept2Id = await conceptSystem.createConcept({
|
||||
name: 'REST',
|
||||
domain: 'technical',
|
||||
category: 'pattern',
|
||||
keywords: [],
|
||||
strength: 0.7,
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
await conceptSystem.linkConcept(concept1Id, concept2Id, 'uses')
|
||||
|
||||
const graph = await conceptSystem.getConceptGraph()
|
||||
|
||||
expect(graph.concepts).toHaveLength(2)
|
||||
expect(graph.links).toHaveLength(1)
|
||||
expect(graph.links[0].relationship).toBe('uses')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GitBridge', () => {
|
||||
let gitBridge: GitBridge
|
||||
let gitRepoPath: string
|
||||
|
||||
beforeEach(async () => {
|
||||
gitBridge = new GitBridge(vfs, brain)
|
||||
gitRepoPath = path.join(tempDir, 'git-repo')
|
||||
await fs.mkdir(gitRepoPath, { recursive: true })
|
||||
})
|
||||
|
||||
it('should export VFS to Git repository', async () => {
|
||||
// Create test files in VFS
|
||||
await vfs.mkdir('/project')
|
||||
await vfs.writeFile('/project/README.md', '# Test Project')
|
||||
await vfs.writeFile('/project/src/main.js', 'console.log("Hello World")')
|
||||
|
||||
const gitRepo = await gitBridge.exportToGit(
|
||||
'/project',
|
||||
gitRepoPath,
|
||||
{
|
||||
preserveMetadata: true,
|
||||
commitMessage: 'Initial export from VFS'
|
||||
}
|
||||
)
|
||||
|
||||
expect(gitRepo.files.length).toBeGreaterThan(0)
|
||||
expect(gitRepo.commits).toHaveLength(1)
|
||||
expect(gitRepo.commits[0].message).toBe('Initial export from VFS')
|
||||
|
||||
// Check files were created
|
||||
const readmeExists = await fs.access(path.join(gitRepoPath, 'README.md')).then(() => true).catch(() => false)
|
||||
const mainExists = await fs.access(path.join(gitRepoPath, 'src/main.js')).then(() => true).catch(() => false)
|
||||
|
||||
expect(readmeExists).toBe(true)
|
||||
expect(mainExists).toBe(true)
|
||||
})
|
||||
|
||||
it('should import Git repository to VFS', async () => {
|
||||
// Create test Git repository structure
|
||||
await fs.mkdir(path.join(gitRepoPath, 'src'), { recursive: true })
|
||||
await fs.writeFile(path.join(gitRepoPath, 'README.md'), '# Imported Project')
|
||||
await fs.writeFile(path.join(gitRepoPath, 'src/app.js'), 'const app = "hello"')
|
||||
|
||||
const stats = await gitBridge.importFromGit(
|
||||
gitRepoPath,
|
||||
'/imported'
|
||||
)
|
||||
|
||||
expect(stats.filesImported).toBe(2)
|
||||
|
||||
// Check files were imported
|
||||
const readmeExists = await vfs.exists('/imported/README.md')
|
||||
const appExists = await vfs.exists('/imported/src/app.js')
|
||||
|
||||
expect(readmeExists).toBe(true)
|
||||
expect(appExists).toBe(true)
|
||||
|
||||
// Check file content
|
||||
const readmeContent = await vfs.readFile('/imported/README.md')
|
||||
expect(readmeContent.toString()).toBe('# Imported Project')
|
||||
})
|
||||
|
||||
it('should preserve metadata during export/import cycle', async () => {
|
||||
// Create file with metadata
|
||||
await vfs.writeFile('/test.txt', 'test content', {
|
||||
metadata: { author: 'test-user', tags: ['important'] }
|
||||
})
|
||||
|
||||
// Export to Git
|
||||
await gitBridge.exportToGit('/', gitRepoPath, { preserveMetadata: true })
|
||||
|
||||
// Clear VFS
|
||||
await vfs.unlink('/test.txt')
|
||||
|
||||
// Import back from Git
|
||||
await gitBridge.importFromGit(gitRepoPath, '/restored', { extractMetadata: true })
|
||||
|
||||
// Check file was restored
|
||||
const exists = await vfs.exists('/restored/test.txt')
|
||||
expect(exists).toBe(true)
|
||||
|
||||
const content = await vfs.readFile('/restored/test.txt')
|
||||
expect(content.toString()).toBe('test content')
|
||||
})
|
||||
|
||||
it('should handle nested directory structures', async () => {
|
||||
// Create nested structure
|
||||
await vfs.mkdir('/deep/nested/structure', { recursive: true })
|
||||
await vfs.writeFile('/deep/nested/structure/file.txt', 'deep file')
|
||||
await vfs.writeFile('/deep/other.txt', 'other file')
|
||||
|
||||
// Export
|
||||
await gitBridge.exportToGit('/deep', gitRepoPath)
|
||||
|
||||
// Import to new location
|
||||
const stats = await gitBridge.importFromGit(gitRepoPath, '/imported')
|
||||
|
||||
expect(stats.filesImported).toBe(2)
|
||||
|
||||
const deepExists = await vfs.exists('/imported/nested/structure/file.txt')
|
||||
const otherExists = await vfs.exists('/imported/other.txt')
|
||||
|
||||
expect(deepExists).toBe(true)
|
||||
expect(otherExists).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
it('should work with all knowledge layer components together', async () => {
|
||||
// Test that individual knowledge components can work with VFS
|
||||
const eventRecorder = new EventRecorder(brain)
|
||||
const semanticVersioning = new SemanticVersioning(brain)
|
||||
const entitySystem = new PersistentEntitySystem(brain)
|
||||
const conceptSystem = new ConceptSystem(brain)
|
||||
|
||||
// Write a file through VFS
|
||||
const content = `
|
||||
# Authentication Service
|
||||
|
||||
The AuthService class handles user authentication.
|
||||
It implements the security pattern for validating credentials.
|
||||
|
||||
class AuthService {
|
||||
authenticate(user, password) {
|
||||
// Validation logic
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
await vfs.writeFile('/services/auth.md', content)
|
||||
|
||||
// Test that the file exists in VFS
|
||||
const exists = await vfs.exists('/services/auth.md')
|
||||
expect(exists).toBe(true)
|
||||
|
||||
// Test that file content is correct
|
||||
const readContent = await vfs.readFile('/services/auth.md')
|
||||
expect(readContent.toString()).toContain('AuthService')
|
||||
})
|
||||
|
||||
it('should maintain VFS consistency with file operations', async () => {
|
||||
// Test basic VFS operations that knowledge layer depends on
|
||||
await vfs.mkdir('/controllers', { recursive: true })
|
||||
await vfs.writeFile('/controllers/user.ts', 'class UserController {}')
|
||||
|
||||
// Test file rename
|
||||
await vfs.rename('/controllers/user.ts', '/controllers/user-controller.ts')
|
||||
|
||||
// Verify file still exists at new location
|
||||
const exists = await vfs.exists('/controllers/user-controller.ts')
|
||||
expect(exists).toBe(true)
|
||||
|
||||
// Verify old location doesn't exist
|
||||
const oldExists = await vfs.exists('/controllers/user.ts')
|
||||
expect(oldExists).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
424
tests/vfs/semantic/parser.unit.test.ts
Normal file
424
tests/vfs/semantic/parser.unit.test.ts
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
/**
|
||||
* Tests for SemanticPathParser
|
||||
* REAL TESTS - No mocks, pure logic testing
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'vitest'
|
||||
import {
|
||||
SemanticPathParser,
|
||||
ParsedSemanticPath,
|
||||
RelationshipValue,
|
||||
SimilarityValue
|
||||
} from '../../../src/vfs/semantic/SemanticPathParser.js'
|
||||
|
||||
describe('SemanticPathParser', () => {
|
||||
const parser = new SemanticPathParser()
|
||||
|
||||
describe('Traditional Paths', () => {
|
||||
test('parses simple traditional path', () => {
|
||||
const result = parser.parse('/src/auth.ts')
|
||||
|
||||
expect(result.dimension).toBe('traditional')
|
||||
expect(result.value).toBe('/src/auth.ts')
|
||||
expect(result.subpath).toBeUndefined()
|
||||
})
|
||||
|
||||
test('parses nested traditional path', () => {
|
||||
const result = parser.parse('/src/lib/utils/helper.ts')
|
||||
|
||||
expect(result.dimension).toBe('traditional')
|
||||
expect(result.value).toBe('/src/lib/utils/helper.ts')
|
||||
})
|
||||
|
||||
test('parses root path', () => {
|
||||
const result = parser.parse('/')
|
||||
|
||||
expect(result.dimension).toBe('traditional')
|
||||
expect(result.value).toBe('/')
|
||||
})
|
||||
|
||||
test('normalizes paths with trailing slash', () => {
|
||||
const result = parser.parse('/src/auth.ts/')
|
||||
|
||||
expect(result.dimension).toBe('traditional')
|
||||
expect(result.value).toBe('/src/auth.ts')
|
||||
})
|
||||
|
||||
test('normalizes paths with multiple slashes', () => {
|
||||
const result = parser.parse('//src///auth.ts')
|
||||
|
||||
expect(result.dimension).toBe('traditional')
|
||||
expect(result.value).toBe('/src/auth.ts')
|
||||
})
|
||||
|
||||
test('adds leading slash if missing', () => {
|
||||
const result = parser.parse('src/auth.ts')
|
||||
|
||||
expect(result.dimension).toBe('traditional')
|
||||
expect(result.value).toBe('/src/auth.ts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Concept Paths', () => {
|
||||
test('parses concept path without subpath', () => {
|
||||
const result = parser.parse('/by-concept/authentication')
|
||||
|
||||
expect(result.dimension).toBe('concept')
|
||||
expect(result.value).toBe('authentication')
|
||||
expect(result.subpath).toBeUndefined()
|
||||
})
|
||||
|
||||
test('parses concept path with subpath', () => {
|
||||
const result = parser.parse('/by-concept/authentication/login.ts')
|
||||
|
||||
expect(result.dimension).toBe('concept')
|
||||
expect(result.value).toBe('authentication')
|
||||
expect(result.subpath).toBe('login.ts')
|
||||
})
|
||||
|
||||
test('parses concept path with nested subpath', () => {
|
||||
const result = parser.parse('/by-concept/security/src/auth/login.ts')
|
||||
|
||||
expect(result.dimension).toBe('concept')
|
||||
expect(result.value).toBe('security')
|
||||
expect(result.subpath).toBe('src/auth/login.ts')
|
||||
})
|
||||
|
||||
test('parses concept with dashes', () => {
|
||||
const result = parser.parse('/by-concept/dependency-injection/service.ts')
|
||||
|
||||
expect(result.dimension).toBe('concept')
|
||||
expect(result.value).toBe('dependency-injection')
|
||||
expect(result.subpath).toBe('service.ts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Author Paths', () => {
|
||||
test('parses author path without subpath', () => {
|
||||
const result = parser.parse('/by-author/alice')
|
||||
|
||||
expect(result.dimension).toBe('author')
|
||||
expect(result.value).toBe('alice')
|
||||
expect(result.subpath).toBeUndefined()
|
||||
})
|
||||
|
||||
test('parses author path with subpath', () => {
|
||||
const result = parser.parse('/by-author/alice/code.ts')
|
||||
|
||||
expect(result.dimension).toBe('author')
|
||||
expect(result.value).toBe('alice')
|
||||
expect(result.subpath).toBe('code.ts')
|
||||
})
|
||||
|
||||
test('parses author path with nested subpath', () => {
|
||||
const result = parser.parse('/by-author/bob/projects/2024/file.ts')
|
||||
|
||||
expect(result.dimension).toBe('author')
|
||||
expect(result.value).toBe('bob')
|
||||
expect(result.subpath).toBe('projects/2024/file.ts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Time Paths', () => {
|
||||
test('parses time path without subpath', () => {
|
||||
const result = parser.parse('/as-of/2024-03-15')
|
||||
|
||||
expect(result.dimension).toBe('time')
|
||||
expect(result.value).toBeInstanceOf(Date)
|
||||
expect((result.value as Date).getFullYear()).toBe(2024)
|
||||
expect((result.value as Date).getMonth()).toBe(2) // March is index 2
|
||||
expect((result.value as Date).getDate()).toBe(15)
|
||||
expect(result.subpath).toBeUndefined()
|
||||
})
|
||||
|
||||
test('parses time path with subpath', () => {
|
||||
const result = parser.parse('/as-of/2024-03-15/src/auth.ts')
|
||||
|
||||
expect(result.dimension).toBe('time')
|
||||
expect(result.value).toBeInstanceOf(Date)
|
||||
expect(result.subpath).toBe('src/auth.ts')
|
||||
})
|
||||
|
||||
test('treats invalid date format as traditional path', () => {
|
||||
// Doesn't match the time pattern, so becomes traditional
|
||||
const result = parser.parse('/as-of/2024-03/file.ts')
|
||||
expect(result.dimension).toBe('traditional')
|
||||
})
|
||||
|
||||
test('treats invalid date components as traditional path', () => {
|
||||
// Doesn't match the time pattern, so becomes traditional
|
||||
const result = parser.parse('/as-of/invalid-date/file.ts')
|
||||
expect(result.dimension).toBe('traditional')
|
||||
})
|
||||
|
||||
test('throws on invalid year', () => {
|
||||
expect(() => parser.parse('/as-of/1800-03-15/file.ts')).toThrow('Invalid year')
|
||||
})
|
||||
|
||||
test('throws on invalid month', () => {
|
||||
expect(() => parser.parse('/as-of/2024-13-15/file.ts')).toThrow('Invalid month')
|
||||
})
|
||||
|
||||
test('throws on invalid day', () => {
|
||||
expect(() => parser.parse('/as-of/2024-03-32/file.ts')).toThrow('Invalid day')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Relationship Paths', () => {
|
||||
test('parses relationship path without depth', () => {
|
||||
const result = parser.parse('/related-to/src/auth.ts')
|
||||
|
||||
expect(result.dimension).toBe('relationship')
|
||||
const value = result.value as RelationshipValue
|
||||
expect(value.targetPath).toBe('src/auth.ts')
|
||||
expect(value.depth).toBeUndefined()
|
||||
expect(value.relationshipTypes).toBeUndefined()
|
||||
})
|
||||
|
||||
test('parses relationship path with depth', () => {
|
||||
const result = parser.parse('/related-to/src/auth.ts/depth-2')
|
||||
|
||||
expect(result.dimension).toBe('relationship')
|
||||
const value = result.value as RelationshipValue
|
||||
expect(value.targetPath).toBe('src/auth.ts')
|
||||
expect(value.depth).toBe(2)
|
||||
})
|
||||
|
||||
test('parses relationship path with types', () => {
|
||||
const result = parser.parse('/related-to/src/auth.ts/types-uses,imports')
|
||||
|
||||
expect(result.dimension).toBe('relationship')
|
||||
const value = result.value as RelationshipValue
|
||||
expect(value.targetPath).toBe('src/auth.ts')
|
||||
expect(value.relationshipTypes).toEqual(['uses', 'imports'])
|
||||
})
|
||||
|
||||
test('parses relationship path with depth, types, and subpath', () => {
|
||||
const result = parser.parse('/related-to/src/auth.ts/depth-3/types-uses,imports/helper.ts')
|
||||
|
||||
expect(result.dimension).toBe('relationship')
|
||||
const value = result.value as RelationshipValue
|
||||
expect(value.targetPath).toBe('src/auth.ts')
|
||||
expect(value.depth).toBe(3)
|
||||
expect(value.relationshipTypes).toEqual(['uses', 'imports'])
|
||||
expect(result.subpath).toBe('helper.ts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Similarity Paths', () => {
|
||||
test('parses similarity path without threshold', () => {
|
||||
const result = parser.parse('/similar-to/src/auth.ts')
|
||||
|
||||
expect(result.dimension).toBe('similar')
|
||||
const value = result.value as SimilarityValue
|
||||
expect(value.targetPath).toBe('src/auth.ts')
|
||||
expect(value.threshold).toBeUndefined()
|
||||
})
|
||||
|
||||
test('parses similarity path with threshold', () => {
|
||||
const result = parser.parse('/similar-to/src/auth.ts/threshold-0.85')
|
||||
|
||||
expect(result.dimension).toBe('similar')
|
||||
const value = result.value as SimilarityValue
|
||||
expect(value.targetPath).toBe('src/auth.ts')
|
||||
expect(value.threshold).toBe(0.85)
|
||||
})
|
||||
|
||||
test('parses similarity path with threshold and subpath', () => {
|
||||
const result = parser.parse('/similar-to/src/auth.ts/threshold-0.7/login.ts')
|
||||
|
||||
expect(result.dimension).toBe('similar')
|
||||
const value = result.value as SimilarityValue
|
||||
expect(value.targetPath).toBe('src/auth.ts')
|
||||
expect(value.threshold).toBe(0.7)
|
||||
expect(result.subpath).toBe('login.ts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tag Paths', () => {
|
||||
test('parses tag path without subpath', () => {
|
||||
const result = parser.parse('/by-tag/security')
|
||||
|
||||
expect(result.dimension).toBe('tag')
|
||||
expect(result.value).toBe('security')
|
||||
expect(result.subpath).toBeUndefined()
|
||||
})
|
||||
|
||||
test('parses tag path with subpath', () => {
|
||||
const result = parser.parse('/by-tag/security/auth.ts')
|
||||
|
||||
expect(result.dimension).toBe('tag')
|
||||
expect(result.value).toBe('security')
|
||||
expect(result.subpath).toBe('auth.ts')
|
||||
})
|
||||
|
||||
test('parses tag with dashes', () => {
|
||||
const result = parser.parse('/by-tag/code-review/file.ts')
|
||||
|
||||
expect(result.dimension).toBe('tag')
|
||||
expect(result.value).toBe('code-review')
|
||||
expect(result.subpath).toBe('file.ts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isSemanticPath', () => {
|
||||
test('returns false for traditional paths', () => {
|
||||
expect(parser.isSemanticPath('/src/auth.ts')).toBe(false)
|
||||
expect(parser.isSemanticPath('/projects/main/file.ts')).toBe(false)
|
||||
expect(parser.isSemanticPath('/')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true for concept paths', () => {
|
||||
expect(parser.isSemanticPath('/by-concept/auth')).toBe(true)
|
||||
expect(parser.isSemanticPath('/by-concept/auth/file.ts')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for author paths', () => {
|
||||
expect(parser.isSemanticPath('/by-author/alice')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for time paths', () => {
|
||||
expect(parser.isSemanticPath('/as-of/2024-03-15')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for relationship paths', () => {
|
||||
expect(parser.isSemanticPath('/related-to/src/auth.ts')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for similarity paths', () => {
|
||||
expect(parser.isSemanticPath('/similar-to/src/auth.ts')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for tag paths', () => {
|
||||
expect(parser.isSemanticPath('/by-tag/security')).toBe(true)
|
||||
})
|
||||
|
||||
test('handles invalid input gracefully', () => {
|
||||
expect(parser.isSemanticPath('')).toBe(false)
|
||||
expect(parser.isSemanticPath(null as any)).toBe(false)
|
||||
expect(parser.isSemanticPath(undefined as any)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDimension', () => {
|
||||
test('returns correct dimension for each path type', () => {
|
||||
expect(parser.getDimension('/src/auth.ts')).toBe('traditional')
|
||||
expect(parser.getDimension('/by-concept/auth')).toBe('concept')
|
||||
expect(parser.getDimension('/by-author/alice')).toBe('author')
|
||||
expect(parser.getDimension('/as-of/2024-03-15')).toBe('time')
|
||||
expect(parser.getDimension('/related-to/src/file.ts')).toBe('relationship')
|
||||
expect(parser.getDimension('/similar-to/src/file.ts')).toBe('similar')
|
||||
expect(parser.getDimension('/by-tag/security')).toBe('tag')
|
||||
})
|
||||
})
|
||||
|
||||
describe('validate', () => {
|
||||
test('validates correct parsed paths', () => {
|
||||
const valid: ParsedSemanticPath = {
|
||||
dimension: 'concept',
|
||||
value: 'authentication'
|
||||
}
|
||||
expect(parser.validate(valid)).toBe(true)
|
||||
})
|
||||
|
||||
test('validates time paths with Date objects', () => {
|
||||
const valid: ParsedSemanticPath = {
|
||||
dimension: 'time',
|
||||
value: new Date('2024-03-15')
|
||||
}
|
||||
expect(parser.validate(valid)).toBe(true)
|
||||
})
|
||||
|
||||
test('validates relationship paths', () => {
|
||||
const valid: ParsedSemanticPath = {
|
||||
dimension: 'relationship',
|
||||
value: { targetPath: '/src/auth.ts', depth: 2 }
|
||||
}
|
||||
expect(parser.validate(valid)).toBe(true)
|
||||
})
|
||||
|
||||
test('validates similarity paths', () => {
|
||||
const valid: ParsedSemanticPath = {
|
||||
dimension: 'similar',
|
||||
value: { targetPath: '/src/auth.ts', threshold: 0.8 }
|
||||
}
|
||||
expect(parser.validate(valid)).toBe(true)
|
||||
})
|
||||
|
||||
test('rejects invalid structures', () => {
|
||||
expect(parser.validate(null as any)).toBe(false)
|
||||
expect(parser.validate(undefined as any)).toBe(false)
|
||||
expect(parser.validate({} as any)).toBe(false)
|
||||
})
|
||||
|
||||
test('rejects missing dimension', () => {
|
||||
expect(parser.validate({ value: 'test' } as any)).toBe(false)
|
||||
})
|
||||
|
||||
test('rejects missing value', () => {
|
||||
expect(parser.validate({ dimension: 'concept' } as any)).toBe(false)
|
||||
})
|
||||
|
||||
test('rejects invalid time values', () => {
|
||||
const invalid: ParsedSemanticPath = {
|
||||
dimension: 'time',
|
||||
value: 'not-a-date' as any
|
||||
}
|
||||
expect(parser.validate(invalid)).toBe(false)
|
||||
})
|
||||
|
||||
test('rejects invalid relationship values', () => {
|
||||
const invalid: ParsedSemanticPath = {
|
||||
dimension: 'relationship',
|
||||
value: { targetPath: '' } as any
|
||||
}
|
||||
expect(parser.validate(invalid)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('throws on null path', () => {
|
||||
expect(() => parser.parse(null as any)).toThrow('Path must be a non-empty string')
|
||||
})
|
||||
|
||||
test('throws on undefined path', () => {
|
||||
expect(() => parser.parse(undefined as any)).toThrow('Path must be a non-empty string')
|
||||
})
|
||||
|
||||
test('throws on empty string', () => {
|
||||
expect(() => parser.parse('')).toThrow('Path must be a non-empty string')
|
||||
})
|
||||
|
||||
test('throws on non-string path', () => {
|
||||
expect(() => parser.parse(123 as any)).toThrow('Path must be a non-empty string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('handles paths with special characters in concepts', () => {
|
||||
const result = parser.parse('/by-concept/oauth-2.0/auth.ts')
|
||||
expect(result.dimension).toBe('concept')
|
||||
expect(result.value).toBe('oauth-2.0')
|
||||
})
|
||||
|
||||
test('handles paths with underscores', () => {
|
||||
const result = parser.parse('/by-concept/user_authentication/login.ts')
|
||||
expect(result.dimension).toBe('concept')
|
||||
expect(result.value).toBe('user_authentication')
|
||||
})
|
||||
|
||||
test('handles deeply nested subpaths', () => {
|
||||
const result = parser.parse('/by-concept/auth/src/lib/utils/helpers/validators/email.ts')
|
||||
expect(result.dimension).toBe('concept')
|
||||
expect(result.value).toBe('auth')
|
||||
expect(result.subpath).toBe('src/lib/utils/helpers/validators/email.ts')
|
||||
})
|
||||
|
||||
test('handles paths with dots', () => {
|
||||
const result = parser.parse('/by-author/alice/v2.0.0/file.ts')
|
||||
expect(result.dimension).toBe('author')
|
||||
expect(result.value).toBe('alice')
|
||||
expect(result.subpath).toBe('v2.0.0/file.ts')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -232,48 +232,6 @@ describe('VirtualFileSystem - Comprehensive Test Suite', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Knowledge Layer', () => {
|
||||
it('should enable and use Knowledge Layer features', async () => {
|
||||
// Enable Knowledge Layer
|
||||
await vfs.enableKnowledgeLayer()
|
||||
|
||||
// Write a file to trigger Knowledge Layer processing
|
||||
await vfs.writeFile('/knowledge.txt', 'This is a test of the knowledge system')
|
||||
|
||||
// Wait for background processing
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// Test Knowledge Layer methods (added dynamically)
|
||||
if ('getHistory' in vfs) {
|
||||
const history = await (vfs as any).getHistory('/knowledge.txt')
|
||||
expect(history).toBeDefined()
|
||||
}
|
||||
|
||||
if ('getVersions' in vfs) {
|
||||
const versions = await (vfs as any).getVersions('/knowledge.txt')
|
||||
expect(versions).toBeDefined()
|
||||
}
|
||||
|
||||
if ('createEntity' in vfs) {
|
||||
const entity = await (vfs as any).createEntity({
|
||||
name: 'TestEntity',
|
||||
type: 'character',
|
||||
description: 'A test character'
|
||||
})
|
||||
expect(entity).toBeDefined()
|
||||
}
|
||||
|
||||
if ('createConcept' in vfs) {
|
||||
const concept = await (vfs as any).createConcept({
|
||||
name: 'TestConcept',
|
||||
type: 'idea',
|
||||
domain: 'testing'
|
||||
})
|
||||
expect(concept).toBeDefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Import/Export', () => {
|
||||
it('should import files from local filesystem', async () => {
|
||||
// Create a temp file
|
||||
|
|
@ -380,20 +338,4 @@ describe('GitBridge Integration', () => {
|
|||
await brain?.close()
|
||||
})
|
||||
|
||||
it('should export relationships and history (FIXED - now queries real data)', async () => {
|
||||
// Enable Knowledge Layer for history
|
||||
await vfs.enableKnowledgeLayer()
|
||||
|
||||
// Create files with relationships
|
||||
await vfs.writeFile('/src/index.js', 'export default {}')
|
||||
await vfs.writeFile('/src/utils.js', 'export function util() {}')
|
||||
await vfs.addRelationship('/src/index.js', '/src/utils.js', VerbType.Uses)
|
||||
|
||||
// GitBridge export would now include real relationships and events
|
||||
const gitBridge = (vfs as any).gitBridge
|
||||
if (gitBridge) {
|
||||
// The export methods now query actual VFS data instead of returning empty arrays
|
||||
expect(gitBridge).toBeDefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue