From a20418bca9dd17fd255e4ffa9444aef35a6a77fd Mon Sep 17 00:00:00 2001 From: David Snelling Date: Fri, 26 Sep 2025 15:12:04 -0700 Subject: [PATCH] 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 --- apps/studio/package.json | 21 ++++ src/vfs/VirtualFileSystem.ts | 36 ++++++- tests/vfs/vfs-relationships.test.ts | 149 ++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 apps/studio/package.json diff --git a/apps/studio/package.json b/apps/studio/package.json new file mode 100644 index 00000000..450c7b5f --- /dev/null +++ b/apps/studio/package.json @@ -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" + } +} \ No newline at end of file diff --git a/src/vfs/VirtualFileSystem.ts b/src/vfs/VirtualFileSystem.ts index 86f4f3a2..7c12c2b4 100644 --- a/src/vfs/VirtualFileSystem.ts +++ b/src/vfs/VirtualFileSystem.ts @@ -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) } } \ No newline at end of file diff --git a/tests/vfs/vfs-relationships.test.ts b/tests/vfs/vfs-relationships.test.ts index 5b5bde77..350c006f 100644 --- a/tests/vfs/vfs-relationships.test.ts +++ b/tests/vfs/vfs-relationships.test.ts @@ -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') + }) }) \ No newline at end of file