diff --git a/tests/integration/clear-persistence.test.ts b/tests/integration/clear-persistence.test.ts index a91b38f9..0ff59119 100644 --- a/tests/integration/clear-persistence.test.ts +++ b/tests/integration/clear-persistence.test.ts @@ -1,234 +1,119 @@ /** - * Integration tests for clear() bug fix (v5.10.4) + * Integration tests for clear() — fully clears storage, including across a restart. * - * Bug report: a consumer team reported that brain.clear() doesn't fully delete persistent storage. - * After calling clear() and creating a new Brainy instance, all data was restored from _cow/ directory. + * Rewritten for 8.0: the original v5.10.4 suite asserted copy-on-write internals + * (`_cow/` directory + a `cow-disabled` marker). COW was removed in 8.0 (replaced by + * generational MVCC), so those internals no longer exist. These tests verify the + * durable BEHAVIOR that the original bug report cared about: `clear()` empties the + * store, and a fresh instance opened over the same path sees no data. * - * Root cause: Setting cowEnabled = false on old instance doesn't affect new instances. - * Fix: Create persistent marker file that survives instance restarts. - * - * These tests verify: - * 1. clear() actually deletes all data - * 2. New instances don't restore data from _cow/ - * 3. Marker file persists across restarts - * 4. Works for all storage adapters + * NO MOCKS — real filesystem + memory storage. Every brain is closed in teardown so + * background flush / writer-lock heartbeat timers cannot bleed across tests. */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, afterEach } from 'vitest' import { Brainy } from '../../src/brainy.js' import * as fs from 'fs' -import * as path from 'path' -describe('Clear Persistence Bug Fix (v5.10.4)', () => { +describe('clear() fully clears storage', () => { const testStoragePath = './test-clear-persistence-' + Date.now() + const brains: Brainy[] = [] + + const open = async (rootDirectory = testStoragePath): Promise => { + const brain = new Brainy({ + requireSubtype: false, + storage: { type: 'filesystem', rootDirectory } + }) + await brain.init() + brains.push(brain) + return brain + } afterEach(async () => { - // Cleanup test storage - try { - if (fs.existsSync(testStoragePath)) { - await fs.promises.rm(testStoragePath, { recursive: true, force: true }) + for (const b of brains.splice(0)) { + try { + await b.close() + } catch { + /* already closed */ } - } catch (error) { - console.warn('Failed to cleanup test storage:', error) } - }) - - it('should fully clear persistent storage (the reported scenario)', async () => { - // Step 1: Create and populate instance - const brain1 = new Brainy({ requireSubtype: false, - storage: { - type: 'filesystem', - path: testStoragePath, - enableCompression: true - } - }) - await brain1.init() - - // Add data (using only entities, not VFS) - const entityId = await brain1.add({ data: 'Test Entity', type: 'concept' }) - - // Verify data exists (filter by type to exclude VFS directories) - const entityCount1 = (await brain1.find({ type: 'concept' })).length - expect(entityCount1).toBe(1) - - // Step 2: Clear all data - await brain1.clear() - - // Verify cleared in same instance (filter by type to exclude VFS directories) - const entityCount2 = (await brain1.find({ type: 'concept' })).length - expect(entityCount2).toBe(0) - - // Step 3: Create NEW instance (simulate server restart) - const brain2 = new Brainy({ requireSubtype: false, - storage: { - type: 'filesystem', - path: testStoragePath, - enableCompression: true - } - }) - await brain2.init() - - // CRITICAL: Verify data is NOT restored (THE BUG) - const entityCount3 = (await brain2.find({ type: 'concept' })).length - - expect(entityCount3).toBe(0) // Should be 0, not 1 - }) - - it('should create cow-disabled marker file', async () => { - const brain = new Brainy({ requireSubtype: false, - storage: { - type: 'filesystem', - path: testStoragePath - } - }) - await brain.init() - - // Add some data to ensure COW is initialized - await brain.add({ data: 'Test', type: 'concept' }) - - // Clear - await brain.clear() - - // Check for marker file - const markerPath = path.join(testStoragePath, '_system', 'cow-disabled') - const markerExists = fs.existsSync(markerPath) - expect(markerExists).toBe(true) - }) - - it('should delete _cow/ directory', async () => { - const brain = new Brainy({ requireSubtype: false, - storage: { - type: 'filesystem', - path: testStoragePath - } - }) - await brain.init() - - // Add data to create COW commits - await brain.add({ data:'Test', type: 'concept' }) - - // Verify _cow/ exists - const cowPath = path.join(testStoragePath, '_cow') - const cowExistsBefore = fs.existsSync(cowPath) - expect(cowExistsBefore).toBe(true) - - // Clear - await brain.clear() - - // Verify _cow/ is deleted - const cowExistsAfter = fs.existsSync(cowPath) - expect(cowExistsAfter).toBe(false) - }) - - it('should prevent COW reinitialization after clear()', async () => { - // Create and clear - const brain1 = new Brainy({ requireSubtype: false, - storage: { type: 'filesystem', path: testStoragePath } - }) - await brain1.init() - await brain1.add({ data:'Test', type: 'concept' }) - await brain1.clear() - - // Create new instance - const brain2 = new Brainy({ requireSubtype: false, - storage: { type: 'filesystem', path: testStoragePath } - }) - await brain2.init() - - // Add new data - should NOT recreate _cow/ - // (COW stays disabled because marker exists) - await brain2.add({ data:'New Data', type: 'concept' }) - - // Verify _cow/ still doesn't exist - const cowPath = path.join(testStoragePath, '_cow') - const cowExists = fs.existsSync(cowPath) - expect(cowExists).toBe(false) - }) - - it('should work across multiple clear() calls', async () => { - const storagePath = testStoragePath + '-multi' - - try { - // Iteration 1 - const brain1 = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: storagePath }}) - await brain1.init() - await brain1.add({ data:'Entity 1', type: 'concept' }) - expect((await brain1.find({ type: 'concept' })).length).toBe(1) - await brain1.clear() - - // Iteration 2 - const brain2 = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: storagePath }}) - await brain2.init() - expect((await brain2.find({ type: 'concept' })).length).toBe(0) - await brain2.add({ data:'Entity 2', type: 'concept' }) - expect((await brain2.find({ type: 'concept' })).length).toBe(1) - await brain2.clear() - - // Iteration 3 - const brain3 = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: storagePath }}) - await brain3.init() - expect((await brain3.find({ type: 'concept' })).length).toBe(0) - } finally { - // Cleanup - if (fs.existsSync(storagePath)) { - await fs.promises.rm(storagePath, { recursive: true, force: true }) + for (const p of [testStoragePath, testStoragePath + '-multi']) { + try { + if (fs.existsSync(p)) await fs.promises.rm(p, { recursive: true, force: true }) + } catch { + /* ignore cleanup errors */ } } }) - it('should clear both entities and relations', async () => { - const brain1 = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: testStoragePath }}) - await brain1.init() - - // Add graph data - const entity1Id = await brain1.add({ data:'Entity 1', type: 'person' }) - const entity2Id = await brain1.add({ data:'Entity 2', type: 'concept' }) - await brain1.relate({ from: entity1Id, to: entity2Id, type: 'relatedTo' }) - - // Verify data exists - expect((await brain1.find({ type: 'person' })).length).toBe(1) + it('fully clears persistent storage, including after a restart (the reported scenario)', async () => { + const brain1 = await open() + await brain1.add({ data: 'Test Entity', type: 'concept' }) expect((await brain1.find({ type: 'concept' })).length).toBe(1) + + await brain1.clear() + expect((await brain1.find({ type: 'concept' })).length).toBe(0) + await brain1.close() + + // New instance over the same path — data must NOT come back. + const brain2 = await open() + expect((await brain2.find({ type: 'concept' })).length).toBe(0) + }) + + it('clears both entities and relations across a restart', async () => { + const brain1 = await open() + const e1 = await brain1.add({ data: 'Entity 1', type: 'person' }) + const e2 = await brain1.add({ data: 'Entity 2', type: 'concept' }) + await brain1.relate({ from: e1, to: e2, type: 'relatedTo' }) + expect((await brain1.find({ type: 'person' })).length).toBe(1) expect((await brain1.related({})).length).toBe(1) - // Clear await brain1.clear() + await brain1.close() - // Create new instance - const brain2 = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: testStoragePath }}) - await brain2.init() - - // Verify everything is cleared + const brain2 = await open() expect((await brain2.find({ type: 'person' })).length).toBe(0) expect((await brain2.find({ type: 'concept' })).length).toBe(0) expect((await brain2.related({})).length).toBe(0) }) - it('should handle clear() on empty storage', async () => { - const brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: testStoragePath }}) - await brain.init() - - // Clear without adding any data - await expect(brain.clear()).resolves.not.toThrow() - - // Should create marker even if no data was present - const markerPath = path.join(testStoragePath, '_system', 'cow-disabled') - const markerExists = fs.existsSync(markerPath) - expect(markerExists).toBe(true) - }) -}) - -describe('Clear() works for MemoryStorage', () => { - it('should clear memory storage completely', async () => { - const brain1 = new Brainy({ requireSubtype: false, storage: { type: 'memory' }}) - await brain1.init() - - await brain1.add({ data:'Test', type: 'concept' }) + it('works across multiple clear() cycles and restarts', async () => { + const p = testStoragePath + '-multi' + const brain1 = await open(p) + await brain1.add({ data: 'Entity 1', type: 'concept' }) expect((await brain1.find({ type: 'concept' })).length).toBe(1) - await brain1.clear() - expect((await brain1.find({ type: 'concept' })).length).toBe(0) + await brain1.close() - // Note: MemoryStorage doesn't persist, so we can't test instance restart - // The marker methods are no-ops for MemoryStorage + const brain2 = await open(p) + expect((await brain2.find({ type: 'concept' })).length).toBe(0) + await brain2.add({ data: 'Entity 2', type: 'concept' }) + expect((await brain2.find({ type: 'concept' })).length).toBe(1) + await brain2.clear() + await brain2.close() + + const brain3 = await open(p) + expect((await brain3.find({ type: 'concept' })).length).toBe(0) + }) + + it('handles clear() on empty storage', async () => { + const brain = await open() + await expect(brain.clear()).resolves.not.toThrow() + expect((await brain.find({ type: 'concept' })).length).toBe(0) + }) +}) + +describe('clear() works for memory storage', () => { + it('clears memory storage completely', async () => { + const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } }) + await brain.init() + try { + await brain.add({ data: 'Test', type: 'concept' }) + expect((await brain.find({ type: 'concept' })).length).toBe(1) + await brain.clear() + expect((await brain.find({ type: 'concept' })).length).toBe(0) + } finally { + await brain.close() + } }) }) diff --git a/tests/integration/metadata-only-comprehensive.test.ts b/tests/integration/metadata-only-comprehensive.test.ts index 928a24ac..de366b71 100644 --- a/tests/integration/metadata-only-comprehensive.test.ts +++ b/tests/integration/metadata-only-comprehensive.test.ts @@ -12,7 +12,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Brainy } from '../../src/brainy.js' import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js' -import { NounType } from '../../src/types/graphTypes.js' +import { NounType, VerbType } from '../../src/types/graphTypes.js' import { mkdtempSync, rmSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' @@ -129,7 +129,7 @@ describe('Metadata-Only Comprehensive Integration (v5.11.1)', () => { type: NounType.Person }) - await brain.addRelationship(alice, 'knows', bob) + await brain.relate({ from: alice, to: bob, type: VerbType.Knows }) // getVerbsBySource uses graph index const relationships = await brain.getVerbsBySource(alice)