84 lines
4.3 KiB
TypeScript
84 lines
4.3 KiB
TypeScript
|
|
/**
|
||
|
|
* @module tests/integration/read-gate-scope-and-no-reembed
|
||
|
|
* @description Two cures from the pair's first production adoption:
|
||
|
|
* (1) THE READ GATE IS PER-FAMILY — a not-serving VECTOR leg refuses vector
|
||
|
|
* search only; a pure metadata find({ where }) and graph traversal keep
|
||
|
|
* serving. The brain-global gate refused a deployment's badge reads for a
|
||
|
|
* vector-leg verdict that had nothing to do with them.
|
||
|
|
* (2) NO RE-EMBED ON UNCHANGED DATA — an update() carrying the row's current
|
||
|
|
* data lands no vector, defers no embed, rewrites nothing. A host
|
||
|
|
* heartbeat re-writing an unchanged row fed a live index-row loop.
|
||
|
|
*/
|
||
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||
|
|
import * as fs from 'node:fs'
|
||
|
|
import * as os from 'node:os'
|
||
|
|
import * as path from 'node:path'
|
||
|
|
import { Brainy, VectorIndexNotReadyError } from '../../src/index.js'
|
||
|
|
|
||
|
|
describe('read gate scope + no re-embed on unchanged data', () => {
|
||
|
|
let dir: string
|
||
|
|
let brain: any
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
||
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-gate-scope-'))
|
||
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir }, silent: true, dimensions: 384 })
|
||
|
|
await brain.init()
|
||
|
|
})
|
||
|
|
afterEach(async () => {
|
||
|
|
await brain.close?.().catch(() => {})
|
||
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
||
|
|
})
|
||
|
|
|
||
|
|
it('a not-serving VECTOR leg refuses vector search only — metadata and graph reads keep serving', async () => {
|
||
|
|
const a = await brain.add({ data: 'employee alpha', type: 'person', metadata: { status: 'active' } })
|
||
|
|
const b = await brain.add({ data: 'employee beta', type: 'person', metadata: { status: 'active' } })
|
||
|
|
await brain.relate({ from: a, to: b, type: 'relatedTo' })
|
||
|
|
await brain.flush()
|
||
|
|
|
||
|
|
// The vector provider says it is NOT serving (a rebuild-class failure).
|
||
|
|
brain.index.healthReport = () => ({
|
||
|
|
provider: 'vector', healthy: false, serving: false, generation: 7, unledgered: [],
|
||
|
|
invariants: [{ name: 'node-coverage', holds: false, heal: 'rebuild', detail: 'posted 0 < canonical 2', source: 'ledger' }],
|
||
|
|
checkedAt: 1, durationMs: 1
|
||
|
|
})
|
||
|
|
try {
|
||
|
|
const byStatus = await brain.find({ where: { status: 'active' } })
|
||
|
|
expect(byStatus.map((r: any) => r.id).sort(), 'metadata find serves').toEqual([a, b].sort())
|
||
|
|
const rel = await brain.related(a)
|
||
|
|
expect(rel.length, 'graph traversal serves').toBe(1)
|
||
|
|
await expect(brain.find({ query: 'employee' }), 'vector search refuses typed').rejects.toBeInstanceOf(VectorIndexNotReadyError)
|
||
|
|
} finally {
|
||
|
|
delete brain.index.healthReport
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('update() with the row\'s current data re-embeds nothing; a real change re-embeds', async () => {
|
||
|
|
const id = await brain.add({ data: 'invoice 1042 pending', type: 'document', metadata: { n: 1 } })
|
||
|
|
await brain.flush()
|
||
|
|
const before = (await brain.get(id, { includeVectors: true })).vector
|
||
|
|
const ledgerBefore = await brain.storage.getCanonicalCounts()
|
||
|
|
const logBefore = (await brain.transactionLog({ limit: 50 })).length
|
||
|
|
|
||
|
|
// The heartbeat shape: same data, re-written, deferred.
|
||
|
|
for (let i = 0; i < 3; i++) {
|
||
|
|
await brain.update({ id, data: 'invoice 1042 pending', metadata: { n: 1, tick: i }, deferEmbedding: true })
|
||
|
|
}
|
||
|
|
await brain.flush()
|
||
|
|
const after = (await brain.get(id, { includeVectors: true })).vector
|
||
|
|
const ledgerAfter = await brain.storage.getCanonicalCounts()
|
||
|
|
const log = await brain.transactionLog({ limit: 50 })
|
||
|
|
expect(after, 'vector untouched by unchanged-data writes').toEqual(before)
|
||
|
|
expect(ledgerAfter.vectors.all, 'vectored ledger untouched').toBe(ledgerBefore.vectors.all)
|
||
|
|
expect(log.filter((e: any) => e.origin === 'system:embed-landing').length, 'no landing commit for unchanged data').toBe(0)
|
||
|
|
expect(log.length - logBefore, 'the metadata writes themselves still commit').toBe(3)
|
||
|
|
|
||
|
|
// A REAL change re-embeds (deferred → the worker lands it).
|
||
|
|
await brain.update({ id, data: 'invoice 1042 PAID', deferEmbedding: true })
|
||
|
|
await brain.flush()
|
||
|
|
const changed = (await brain.get(id, { includeVectors: true })).vector
|
||
|
|
expect(changed, 'a real data change re-embeds').not.toEqual(before)
|
||
|
|
expect((await brain.storage.getCanonicalCounts()).vectors.all, 'a re-embed of a vectored row never double-counts').toBe(ledgerBefore.vectors.all)
|
||
|
|
})
|
||
|
|
})
|