From 79ae349b60f6754fea5da3f3e738924f2953797a Mon Sep 17 00:00:00 2001 From: David Snelling Date: Fri, 16 Jan 2026 17:04:09 -0800 Subject: [PATCH] fix: clear() now properly resets VFS and COW state Bug: After brain.clear(), VFS operations failed with "Source entity 00000000-0000-0000-0000-000000000000 not found" Root causes fixed: - VFS instance remained in memory pointing to deleted root entity - FileSystemStorage.clear() set blobStorage=undefined but didn't reinit - Write-through cache returned stale entity data after clear() Changes: - Re-initialize COW (BlobStorage) after storage.clear() in brainy.ts - Reset and reinitialize VFS following checkout() pattern - Add clearWriteCache() to BaseStorage, call in Memory/FileSystem adapters - Add 7 integration tests for VFS clear functionality Co-Authored-By: Claude Opus 4.5 --- src/brainy.ts | 28 +++ src/storage/adapters/fileSystemStorage.ts | 5 + src/storage/adapters/memoryStorage.ts | 6 + src/storage/baseStorage.ts | 10 + tests/integration/clear-vfs.test.ts | 228 ++++++++++++++++++++++ 5 files changed, 277 insertions(+) create mode 100644 tests/integration/clear-vfs.test.ts diff --git a/src/brainy.ts b/src/brainy.ts index b1f90dae..3c978102 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -2638,6 +2638,34 @@ export class Brainy implements BrainyInterface { this._neural = undefined this._nlp = undefined this._tripleIntelligence = undefined + + // v7.3.1: Re-initialize COW (BlobStorage) after storage.clear() + // storage.clear() sets blobStorage=undefined for FileSystem/cloud adapters + // VFS depends on blobStorage being available (unified blob storage for all files) + // Must be done BEFORE VFS reinitialization + if (typeof (this.storage as any).initializeCOW === 'function') { + await (this.storage as any).initializeCOW({ + branch: (this.config.storage as any)?.branch || 'main', + enableCompression: true + }) + } + + // v7.3.1: Reset VFS state - root entity was deleted by storage.clear() + // Bug: VFS instance remained in memory pointing to deleted root entity + // Following checkout() pattern exactly (see lines 2907-2914) + if (this._vfs) { + // Clear PathResolver caches (including UnifiedCache VFS entries) + if ((this._vfs as any).pathResolver?.invalidateAllCaches) { + (this._vfs as any).pathResolver.invalidateAllCaches() + } + // Recreate and reinitialize VFS so it's ready for use + this._vfs = new VirtualFileSystem(this) + await this._vfs.init() + // _vfsInitialized remains true since we just initialized + } else { + // VFS was never used, reset flag for clean state + this._vfsInitialized = false + } }) } diff --git a/src/storage/adapters/fileSystemStorage.ts b/src/storage/adapters/fileSystemStorage.ts index 52a8d8a9..a1bc9c46 100644 --- a/src/storage/adapters/fileSystemStorage.ts +++ b/src/storage/adapters/fileSystemStorage.ts @@ -1039,6 +1039,11 @@ export class FileSystemStorage extends BaseStorage { // These in-memory counters must be reset to 0 after clearing all data ;(this as any).totalNounCount = 0 ;(this as any).totalVerbCount = 0 + + // v7.3.1: Clear write-through cache (inherited from BaseStorage) + // Without this, readWithInheritance() would return stale cached data + // after clear(), causing "ghost" entities to appear + this.clearWriteCache() } /** diff --git a/src/storage/adapters/memoryStorage.ts b/src/storage/adapters/memoryStorage.ts index f2018db2..c0ca0daa 100644 --- a/src/storage/adapters/memoryStorage.ts +++ b/src/storage/adapters/memoryStorage.ts @@ -160,6 +160,7 @@ export class MemoryStorage extends BaseStorage { /** * Clear all data from storage * v5.4.0: Clears objectStore (type-first paths) + * v7.3.1: Also clears writeCache to prevent stale data after clear */ public async clear(): Promise { this.objectStore.clear() @@ -172,6 +173,11 @@ export class MemoryStorage extends BaseStorage { // Clear the statistics cache this.statisticsCache = null this.statisticsModified = false + + // v7.3.1: Clear write-through cache (inherited from BaseStorage) + // Without this, readWithInheritance() would return stale cached data + // after clear(), causing "ghost" entities to appear + this.clearWriteCache() } /** diff --git a/src/storage/baseStorage.ts b/src/storage/baseStorage.ts index a7f7d370..7245ddd2 100644 --- a/src/storage/baseStorage.ts +++ b/src/storage/baseStorage.ts @@ -155,6 +155,16 @@ export abstract class BaseStorage extends BaseStorageAdapter { // Memory footprint: Bounded by batch size (typically <1000 items during imports) private writeCache = new Map() + /** + * v7.3.1: Clear the write-through cache + * MUST be called by all storage adapter clear() implementations to ensure + * read-after-write consistency cache doesn't return stale data after clear. + * @protected - Available to subclasses for clear() implementation + */ + protected clearWriteCache(): void { + this.writeCache.clear() + } + // COW (Copy-on-Write) support - v5.0.0 public refManager?: RefManager public blobStorage?: BlobStorage diff --git a/tests/integration/clear-vfs.test.ts b/tests/integration/clear-vfs.test.ts new file mode 100644 index 00000000..7109d7bd --- /dev/null +++ b/tests/integration/clear-vfs.test.ts @@ -0,0 +1,228 @@ +/** + * Integration tests for clear() VFS reinitialization fix (v7.3.1) + * + * Bug report: Workshop team reported that brain.clear() breaks VFS operations. + * After calling clear(), VFS operations fail with: + * "Error: Source entity 00000000-0000-0000-0000-000000000000 not found" + * + * Root cause: clear() deleted the VFS root entity but didn't reset/reinitialize + * the VFS instance. The VFS remained in memory pointing to the deleted root. + * + * Fix: Reset VFS state in clear() following the checkout() pattern. + * + * These tests verify: + * 1. VFS operations work after clear() without instance recreation + * 2. clear() works when VFS was never used + * 3. VFS root entity is properly reinitialized + * 4. Old VFS data is actually deleted + */ + +import { describe, it, expect } from 'vitest' +import { Brainy } from '../../src/brainy.js' +import * as fs from 'fs' + +describe('VFS operations after clear() (v7.3.1 fix)', () => { + // Use unique paths per test to avoid filesystem conflicts + const getTestPath = () => `./test-clear-vfs-${Date.now()}-${Math.random().toString(36).slice(2)}` + + // Helper to clean up brain and storage after each test + async function cleanup(brain: Brainy | null, storagePath?: string) { + try { + if (brain) { + await brain.close() + } + } catch (e) { + // Ignore close errors + } + try { + if (storagePath && fs.existsSync(storagePath)) { + await fs.promises.rm(storagePath, { recursive: true, force: true }) + } + } catch (e) { + // Ignore cleanup errors + } + } + + it('should allow VFS operations after brain.clear() without recreation (memory)', async () => { + const brain = new Brainy({ storage: { type: 'memory' } }) + try { + await brain.init() + + // Use VFS before clear + await brain.vfs.writeFile('/test.txt', Buffer.from('hello')) + const contentBefore = await brain.vfs.readFile('/test.txt') + expect(contentBefore.toString()).toBe('hello') + + // Clear all data + await brain.clear() + + // VFS should work immediately without needing instance recreation + await brain.vfs.writeFile('/after-clear.txt', Buffer.from('world')) + const contentAfter = await brain.vfs.readFile('/after-clear.txt') + expect(contentAfter.toString()).toBe('world') + + // Original file should not exist + await expect(brain.vfs.readFile('/test.txt')).rejects.toThrow() + } finally { + await cleanup(brain) + } + }) + + it('should allow VFS operations after brain.clear() without recreation (filesystem)', async () => { + const testPath = getTestPath() + const brain = new Brainy({ + storage: { type: 'filesystem', path: testPath } + }) + try { + await brain.init() + + // Use VFS before clear + await brain.vfs.writeFile('/test.txt', Buffer.from('hello')) + const contentBefore = await brain.vfs.readFile('/test.txt') + expect(contentBefore.toString()).toBe('hello') + + // Clear all data + await brain.clear() + + // VFS should work immediately without needing instance recreation + await brain.vfs.writeFile('/after-clear.txt', Buffer.from('world')) + const contentAfter = await brain.vfs.readFile('/after-clear.txt') + expect(contentAfter.toString()).toBe('world') + + // Original file should not exist + await expect(brain.vfs.readFile('/test.txt')).rejects.toThrow() + } finally { + await cleanup(brain, testPath) + } + }) + + it('should handle clear() when VFS was never used', async () => { + const brain = new Brainy({ storage: { type: 'memory' } }) + try { + await brain.init() + + // Add regular entities, don't use VFS + await brain.add({ data: 'test', type: 'concept' }) + expect((await brain.find({ type: 'concept' })).length).toBe(1) + + // Clear should not throw even if VFS was never accessed + await expect(brain.clear()).resolves.not.toThrow() + + // Entities should be cleared + expect((await brain.find({ type: 'concept' })).length).toBe(0) + + // VFS should work after clear even though it wasn't used before + await brain.vfs.writeFile('/new.txt', Buffer.from('content')) + const content = await brain.vfs.readFile('/new.txt') + expect(content.toString()).toBe('content') + } finally { + await cleanup(brain) + } + }) + + it('should reinitialize VFS root entity after clear', async () => { + const brain = new Brainy({ storage: { type: 'memory' } }) + try { + await brain.init() + + // Create directory structure + await brain.vfs.mkdir('/projects', { recursive: true }) + await brain.vfs.writeFile('/projects/index.js', Buffer.from('console.log("hi")')) + + // Verify VFS root exists + const rootBefore = await brain.vfs.stat('/') + expect(rootBefore.isDirectory()).toBe(true) + + // Verify directory structure exists + const dirBefore = await brain.vfs.stat('/projects') + expect(dirBefore.isDirectory()).toBe(true) + + // Clear + await brain.clear() + + // VFS root should be recreated + const rootAfter = await brain.vfs.stat('/') + expect(rootAfter.isDirectory()).toBe(true) + + // Old directory should not exist + await expect(brain.vfs.stat('/projects')).rejects.toThrow() + } finally { + await cleanup(brain) + } + }) + + it('should work across multiple clear() cycles', async () => { + const brain = new Brainy({ storage: { type: 'memory' } }) + try { + await brain.init() + + // Cycle 1 + await brain.vfs.writeFile('/cycle1.txt', Buffer.from('cycle1')) + expect((await brain.vfs.readFile('/cycle1.txt')).toString()).toBe('cycle1') + await brain.clear() + + // Cycle 2 + await brain.vfs.writeFile('/cycle2.txt', Buffer.from('cycle2')) + expect((await brain.vfs.readFile('/cycle2.txt')).toString()).toBe('cycle2') + await expect(brain.vfs.readFile('/cycle1.txt')).rejects.toThrow() + await brain.clear() + + // Cycle 3 + await brain.vfs.writeFile('/cycle3.txt', Buffer.from('cycle3')) + expect((await brain.vfs.readFile('/cycle3.txt')).toString()).toBe('cycle3') + await expect(brain.vfs.readFile('/cycle1.txt')).rejects.toThrow() + await expect(brain.vfs.readFile('/cycle2.txt')).rejects.toThrow() + } finally { + await cleanup(brain) + } + }) + + it('should preserve VFS functionality with nested directories after clear', async () => { + const brain = new Brainy({ storage: { type: 'memory' } }) + try { + await brain.init() + + // Create complex structure before clear + await brain.vfs.mkdir('/a/b/c', { recursive: true }) + await brain.vfs.writeFile('/a/b/c/deep.txt', Buffer.from('deep content')) + + await brain.clear() + + // Create new complex structure after clear + await brain.vfs.mkdir('/x/y/z', { recursive: true }) + await brain.vfs.writeFile('/x/y/z/new-deep.txt', Buffer.from('new deep content')) + const content = await brain.vfs.readFile('/x/y/z/new-deep.txt') + expect(content.toString()).toBe('new deep content') + + // Old structure should not exist + await expect(brain.vfs.stat('/a')).rejects.toThrow() + } finally { + await cleanup(brain) + } + }) + + it('should work with concurrent VFS and regular entity operations after clear', async () => { + const brain = new Brainy({ storage: { type: 'memory' } }) + try { + await brain.init() + + // Mix VFS and regular entities before clear + await brain.vfs.writeFile('/config.json', Buffer.from('{"version": 1}')) + await brain.add({ data: 'user data', type: 'person' }) + + await brain.clear() + + // Both should work after clear + await brain.vfs.writeFile('/config.json', Buffer.from('{"version": 2}')) + await brain.add({ data: 'new user data', type: 'person' }) + + const vfsContent = await brain.vfs.readFile('/config.json') + expect(vfsContent.toString()).toBe('{"version": 2}') + + const entities = await brain.find({ type: 'person' }) + expect(entities.length).toBe(1) + } finally { + await cleanup(brain) + } + }) +})