95 lines
4.1 KiB
TypeScript
95 lines
4.1 KiB
TypeScript
|
|
/**
|
||
|
|
* @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)
|
||
|
|
})
|
||
|
|
})
|