fix: ensure Contains relationships are maintained when updating files in VFS

- Add Contains relationship verification when updating existing files
- Create missing relationships to prevent orphaned files
- Fix resolvePath to return entity IDs instead of path strings
- Improve error handling in ensureDirectory method
- Add comprehensive tests for Contains relationship integrity

Resolves critical bug where vfs.readdir() returned empty arrays
This commit is contained in:
David Snelling 2025-09-26 15:12:04 -07:00
parent 493fc48603
commit a20418bca9
3 changed files with 202 additions and 4 deletions

21
apps/studio/package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "@brainy/studio",
"version": "1.0.0",
"description": "Brainy Studio - Interactive VFS and Neural Graph Explorer",
"type": "module",
"scripts": {
"start": "npx tsx server.ts",
"dev": "npx tsx --watch server.ts"
},
"dependencies": {
"@soulcraft/brainy": "file:../../",
"express": "^4.18.2",
"cors": "^2.8.5"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/cors": "^2.8.17",
"@types/node": "^20.0.0",
"tsx": "^4.19.2"
}
}

View file

@ -316,6 +316,22 @@ export class VirtualFileSystem implements IVirtualFileSystem {
data: entityData,
metadata
})
// Ensure Contains relationship exists (fix for missing relationships)
const existingRelations = await this.brain.getRelations({
from: parentId,
to: existingId,
type: VerbType.Contains
})
// Create relationship if it doesn't exist
if (existingRelations.length === 0) {
await this.brain.relate({
from: parentId,
to: existingId,
type: VerbType.Contains
})
}
} else {
// Create new file entity
// For embedding: use text content, for storage: use raw data
@ -907,9 +923,13 @@ export class VirtualFileSystem implements IVirtualFileSystem {
}
return entityId
} catch (err) {
// Directory doesn't exist, create it recursively
await this.mkdir(path, { recursive: true })
return await this.pathResolver.resolve(path)
// Only create directory if it doesn't exist (ENOENT error)
if (err instanceof VFSError && err.code === VFSErrorCode.ENOENT) {
await this.mkdir(path, { recursive: true })
return await this.pathResolver.resolve(path)
}
// Re-throw other errors (like ENOTDIR)
throw err
}
}
@ -2418,6 +2438,14 @@ export class VirtualFileSystem implements IVirtualFileSystem {
}
// Normalize path
return path.replace(/\/+/g, '/').replace(/\/$/, '') || '/'
const normalizedPath = path.replace(/\/+/g, '/').replace(/\/$/, '') || '/'
// Special case for root
if (normalizedPath === '/') {
return this.rootEntityId!
}
// Resolve the path to an entity ID
return await this.pathResolver.resolve(normalizedPath)
}
}

View file

@ -192,4 +192,153 @@ describe('VFS Graph Relationships', () => {
const references = related.filter(r => r.direction === 'from')
expect(references.length).toBeGreaterThanOrEqual(2)
})
it('should always create Contains relationship when writing files', async () => {
// This test verifies the fix for the critical bug where writeFile()
// was not creating Contains relationships for updated files
// Test 1: New file should have Contains relationship
await vfs.writeFile('/test-new.txt', 'Hello World')
const rootId = await vfs.resolvePath('/')
const fileEntityId = await vfs.resolvePath('/test-new.txt')
// Check that Contains relationship exists
const relations = await brain.getRelations({
from: rootId,
to: fileEntityId,
type: VerbType.Contains
})
expect(relations).toHaveLength(1)
expect(relations[0].type).toBe(VerbType.Contains)
// Verify readdir returns the file
const files = await vfs.readdir('/')
expect(files).toContain('test-new.txt')
// Test 2: Updated file should maintain Contains relationship
await vfs.writeFile('/test-new.txt', 'Updated content')
// Relationship should still exist after update
const relationsAfterUpdate = await brain.getRelations({
from: rootId,
to: fileEntityId,
type: VerbType.Contains
})
expect(relationsAfterUpdate).toHaveLength(1)
// readdir should still work
const filesAfterUpdate = await vfs.readdir('/')
expect(filesAfterUpdate).toContain('test-new.txt')
// Test 3: Multiple updates should not create duplicate relationships
await vfs.writeFile('/test-new.txt', 'Another update')
await vfs.writeFile('/test-new.txt', 'Yet another update')
const finalRelations = await brain.getRelations({
from: rootId,
to: fileEntityId,
type: VerbType.Contains
})
// Should still have exactly one Contains relationship
expect(finalRelations).toHaveLength(1)
})
it('should repair missing Contains relationships on file update', async () => {
// This test simulates a scenario where a file exists but its Contains
// relationship is missing (could happen due to corruption or bugs)
// Create a file normally first
await vfs.writeFile('/orphan-test.txt', 'Initial content')
const rootId = await vfs.resolvePath('/')
const fileEntityId = await vfs.resolvePath('/orphan-test.txt')
// Manually delete the Contains relationship to simulate the bug
const initialRelations = await brain.getRelations({
from: rootId,
to: fileEntityId,
type: VerbType.Contains
})
// Delete the relationship (simulating the corruption/bug)
for (const rel of initialRelations) {
await brain.unrelate(rel.id)
}
// Verify the relationship is gone
const brokenRelations = await brain.getRelations({
from: rootId,
to: fileEntityId,
type: VerbType.Contains
})
expect(brokenRelations).toHaveLength(0)
// readdir should fail to find the file (the bug symptom)
const brokenList = await vfs.readdir('/')
expect(brokenList).not.toContain('orphan-test.txt')
// Now update the file - this should repair the missing relationship
await vfs.writeFile('/orphan-test.txt', 'Fixed content')
// Verify the relationship is restored
const fixedRelations = await brain.getRelations({
from: rootId,
to: fileEntityId,
type: VerbType.Contains
})
expect(fixedRelations).toHaveLength(1)
expect(fixedRelations[0].type).toBe(VerbType.Contains)
// readdir should now work again
const fixedList = await vfs.readdir('/')
expect(fixedList).toContain('orphan-test.txt')
})
it('should create Contains relationships for files in nested directories', async () => {
// Test that the fix works for nested directory structures
await vfs.mkdir('/level1')
await vfs.mkdir('/level1/level2')
await vfs.mkdir('/level1/level2/level3')
// Write a file deep in the structure
await vfs.writeFile('/level1/level2/level3/deep.txt', 'Deep content')
// Get entity IDs
const level3Id = await vfs.resolvePath('/level1/level2/level3')
const fileEntityId = await vfs.resolvePath('/level1/level2/level3/deep.txt')
// Verify Contains relationship exists
const relations = await brain.getRelations({
from: level3Id,
to: fileEntityId,
type: VerbType.Contains
})
expect(relations).toHaveLength(1)
// Verify readdir works at the deep level
const files = await vfs.readdir('/level1/level2/level3')
expect(files).toContain('deep.txt')
// Update the file and verify relationship persists
await vfs.writeFile('/level1/level2/level3/deep.txt', 'Updated deep content')
const relationsAfterUpdate = await brain.getRelations({
from: level3Id,
to: fileEntityId,
type: VerbType.Contains
})
expect(relationsAfterUpdate).toHaveLength(1)
// readdir should still work
const filesAfterUpdate = await vfs.readdir('/level1/level2/level3')
expect(filesAfterUpdate).toContain('deep.txt')
})
})