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:
parent
493fc48603
commit
a20418bca9
3 changed files with 202 additions and 4 deletions
21
apps/studio/package.json
Normal file
21
apps/studio/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -316,6 +316,22 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
||||||
data: entityData,
|
data: entityData,
|
||||||
metadata
|
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 {
|
} else {
|
||||||
// Create new file entity
|
// Create new file entity
|
||||||
// For embedding: use text content, for storage: use raw data
|
// For embedding: use text content, for storage: use raw data
|
||||||
|
|
@ -907,9 +923,13 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
||||||
}
|
}
|
||||||
return entityId
|
return entityId
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Directory doesn't exist, create it recursively
|
// Only create directory if it doesn't exist (ENOENT error)
|
||||||
await this.mkdir(path, { recursive: true })
|
if (err instanceof VFSError && err.code === VFSErrorCode.ENOENT) {
|
||||||
return await this.pathResolver.resolve(path)
|
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
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -192,4 +192,153 @@ describe('VFS Graph Relationships', () => {
|
||||||
const references = related.filter(r => r.direction === 'from')
|
const references = related.filter(r => r.direction === 'from')
|
||||||
expect(references.length).toBeGreaterThanOrEqual(2)
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue