/** * @module tests/integration/migration-7x-to-8x * @description Proof suite for the 7.x → 8.0 on-disk layout migration. 7.x stored * every object branch-scoped under `branches//`; 8.0 stores the * identical entity structure at the ROOT (`entities////…`) plus * the generational `_system/manifest.json` + `_generations/`. GenerationStore.open * is TOLERANT of a missing manifest (opens at generation 0), so a naive 8.0 open of * a 7.x directory reports zero entities and SILENTLY LOSES ALL DATA. The migration * collapses the HEAD branch's entities to the root before storage is set up. * * Tests: * 1. DATA-LOSS LOCK — a built-in FileSystemStorage opened on the reshaped 7.x dir * reports zero nouns at the root layout (the mode the migration fixes; durable * because storage never migrates). * 2. ROUND TRIP — opening a brain with autoMigrate (default) restores * find/related/counts byte-equal to the reference captured before reshaping. * 3. IDEMPOTENCY — a second open is a no-op (marker short-circuits; branch drained). * 4. GUARD — autoMigrate:false on a legacy layout throws explicit guidance. * 5. NO-OP — a native flat 8.0 brain opens untouched (no marker churn, no data move). */ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest' import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { Brainy } from '../../src/brainy.js' import { FileSystemStorage } from '../../src/storage/adapters/fileSystemStorage.js' import { NounType, VerbType } from '../../src/types/graphTypes.js' interface Reference { nounCount: number verbCount: number docIds: string[] activeIds: string[] relatedFrom0: string[] } const docId = (i: number): string => `00000000-0000-4000-8000-00000000000${i}` async function buildReferenceBrain(dir: string): Promise { const brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', rootDirectory: dir }, dimensions: 384, silent: true }) await brain.init() for (let i = 0; i < 5; i++) { await brain.add({ id: docId(i), data: `Document number ${i} about distributed systems`, type: NounType.Document, metadata: { title: `Doc ${i}`, status: i % 2 === 0 ? 'active' : 'archived' } }) } await brain.relate({ from: docId(0), to: docId(1), type: VerbType.RelatedTo }) await brain.relate({ from: docId(0), to: docId(2), type: VerbType.RelatedTo }) await brain.flush() const ref = await captureReference(brain) await brain.close() return ref } async function captureReference(brain: Brainy): Promise { const docs = await brain.find({ type: NounType.Document, limit: 100 }) const active = await brain.find({ type: NounType.Document, where: { status: 'active' }, limit: 100 }) const related = await brain.related(docId(0)) return { nounCount: await brain.getNounCount(), verbCount: await brain.getVerbCount(), docIds: docs.map((r: any) => r.id).sort(), activeIds: active.map((r: any) => r.id).sort(), relatedFrom0: related.map((r: any) => r.to).sort() } } /** * Reshape a flat 8.0 directory into the 7.x branch layout: move `entities/` under * `branches/main/entities/`, and delete the 8.0-only derived + MVCC state (8.0 * rebuilds all of it from the canonical entities). The result is what a 7.x store * looks like to 8.0: canonical entities present, but invisible at the root. */ function reshapeToLegacy(dir: string): void { const branchEntities = path.join(dir, 'branches', 'main', 'entities') fs.mkdirSync(path.dirname(branchEntities), { recursive: true }) fs.renameSync(path.join(dir, 'entities'), branchEntities) for (const derived of ['_system', '_column_index', '_blobs', '_generations', 'indexes']) { fs.rmSync(path.join(dir, derived), { recursive: true, force: true }) } } const makeTempDir = (): string => fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-migrate-')) async function openBrain(dir: string, extra: Record = {}): Promise { return new Brainy({ requireSubtype: false, storage: { type: 'filesystem', rootDirectory: dir }, dimensions: 384, silent: true, ...extra }) } describe('7.x → 8.0 layout migration', () => { const dirs: string[] = [] const brains: Brainy[] = [] let reference: Reference let legacyTemplate: string beforeAll(async () => { process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true' legacyTemplate = makeTempDir() reference = await buildReferenceBrain(legacyTemplate) reshapeToLegacy(legacyTemplate) }) afterAll(() => fs.rmSync(legacyTemplate, { recursive: true, force: true })) afterEach(async () => { for (const b of brains.splice(0)) { try { await b.close() } catch { /* already closed */ } } for (const d of dirs.splice(0)) fs.rmSync(d, { recursive: true, force: true }) }) /** A fresh copy of the reshaped legacy directory for one test to mutate. */ function freshLegacyDir(): string { const dir = makeTempDir() dirs.push(dir) fs.cpSync(legacyTemplate, dir, { recursive: true }) return dir } it('DATA-LOSS LOCK: a built-in FileSystemStorage on a 7.x layout reports zero nouns at the root', async () => { const dir = freshLegacyDir() const storage = new FileSystemStorage(dir) await storage.init() // The canonical entities exist on disk (under branches/main/) but are invisible // at the 8.0 root layout — exactly the silent data loss the migration prevents. expect(await storage.getNounCount()).toBe(0) }) it('ROUND TRIP: opening with autoMigrate (default) restores all three intelligences byte-equal', async () => { const dir = freshLegacyDir() const brain = await openBrain(dir) brains.push(brain) await brain.init() const after = await captureReference(brain) expect(after).toEqual(reference) // The branch layout is drained and the marker stamped. expect(fs.existsSync(path.join(dir, 'branches', 'main', 'entities'))).toBe(false) expect(fs.existsSync(path.join(dir, 'entities'))).toBe(true) }) it('IDEMPOTENCY: a second open is a no-op and still reads the migrated data', async () => { const dir = freshLegacyDir() const first = await openBrain(dir) brains.push(first) await first.init() await first.close() brains.splice(brains.indexOf(first), 1) // Mtime of the marker must not change on the second open (no re-migration). const markerPath = path.join(dir, '_system', 'migration-layout.json') const markerExists = fs.existsSync(markerPath) || fs.existsSync(markerPath + '.gz') const second = await openBrain(dir) brains.push(second) await second.init() expect(markerExists).toBe(true) expect(await captureReference(second)).toEqual(reference) }) it('GUARD: autoMigrate:false on a 7.x layout throws explicit guidance instead of losing data', async () => { const dir = freshLegacyDir() const brain = await openBrain(dir, { autoMigrate: false }) brains.push(brain) await expect(brain.init()).rejects.toThrow(/7\.x branch layout/) }) it('NO-OP: a native flat 8.0 brain opens untouched', async () => { const dir = makeTempDir() dirs.push(dir) const ref = await buildReferenceBrain(dir) // builds + closes a normal flat brain const reopened = await openBrain(dir) brains.push(reopened) await reopened.init() // migration phase is a strict no-op here expect(await captureReference(reopened)).toEqual(ref) expect(fs.existsSync(path.join(dir, 'branches'))).toBe(false) }) })