/** * @module tests/unit/brainy/migration-gate-family-scoped * @description The coordinated migration LOCK is FAMILY-SCOPED: a read waits only * on the index families it actually consults. A one-time native migration of ONE * family (its `isMigrating()` held) must NOT block a read served entirely from * canonical storage (get / VFS content + dir) or from a HEALTHY family — only a * read that needs the migrating family blocks. Regression for the whole-brain * gate that hung getStats / readdir / readFile behind an unrelated family's * migration until the wait timed out. */ import { describe, it, expect, beforeEach } from 'vitest' import { Brainy } from '../../../src/brainy.js' import { MigrationInProgressError } from '../../../src/errors/brainyError.js' const seed = async () => { const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false, silent: true, // Short so a genuine block resolves fast; a real block would otherwise wait // the 30 s default — this test asserts the wait does NOT happen for scoped // reads, and DOES (bounded) for the one that needs the migrating family. migrationWaitTimeoutMs: 300 }) await brain.init() for (let i = 0; i < 5; i++) { await brain.add({ data: `doc ${i}`, type: 'document', metadata: { i } }) } await brain.vfs.writeFile('/notes/hello.txt', 'canonical bytes — no index needed') await brain.flush() return brain } /** Force a provider to report an in-flight (stuck) migration. */ const jam = (provider: unknown) => { ;(provider as { isMigrating: () => boolean }).isMigrating = () => true } describe('migration LOCK is family-scoped', () => { beforeEach(() => { process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true' }) it('a stuck VECTOR migration does not block canonical or graph/metadata reads', async () => { const brain = await seed() const childId = ( (await brain.vfs.readdir('/notes', { withFileTypes: true })) as Array<{ entityId: string }> )[0].entityId jam((brain as any).index) // vector provider migrating; graph + metadata healthy // None of these consult the vector family → they serve immediately. await expect(brain.getStats()).resolves.toBeDefined() await expect(brain.vfs.readdir('/notes')).resolves.toHaveLength(1) // graph traversal await expect(brain.vfs.readFile('/notes/hello.txt')).resolves.toBeDefined() // canonical blob await expect(brain.get(childId)).resolves.not.toBeNull() // canonical by id await expect(brain.find({ where: { i: 1 } })).resolves.toBeDefined() // metadata filter }) it('a stuck VECTOR migration STILL blocks a read that needs the vector family', async () => { const brain = await seed() jam((brain as any).index) // A semantic query consults the vector index — it must wait, and (bounded by // migrationWaitTimeoutMs) surface the retryable MigrationInProgressError // rather than serve a half-built result. await expect(brain.find({ query: 'doc' })).rejects.toBeInstanceOf(MigrationInProgressError) }) it('a stuck GRAPH migration blocks traversal but not vector/canonical reads', async () => { const brain = await seed() const childId = ( (await brain.vfs.readdir('/notes', { withFileTypes: true })) as Array<{ entityId: string }> )[0].entityId jam((brain as any).graphIndex) // graph provider migrating; vector + metadata healthy // Canonical + vector + metadata are unaffected. await expect(brain.get(childId)).resolves.not.toBeNull() await expect(brain.find({ query: 'doc' })).resolves.toBeDefined() await expect(brain.find({ where: { i: 1 } })).resolves.toBeDefined() // A graph traversal needs the migrating family → it blocks (bounded). await expect(brain.related({ from: childId })).rejects.toBeInstanceOf(MigrationInProgressError) }) it('with no migration in flight, every read serves (the fast path is a no-op)', async () => { const brain = await seed() await expect(brain.getStats()).resolves.toBeDefined() await expect(brain.find({ query: 'doc' })).resolves.toBeDefined() await expect(brain.vfs.readdir('/notes')).resolves.toHaveLength(1) }) })