feat(engine): the wiring wave — stamps ride every flush, provider generations, waitForIndexed, adopt-backfill, match-all serves
- Watermark stamping fans out at flush: all three projections stamped
with the committed generation before their flushes persist.
- waitForIndexed(path?, {generation, timeoutMs}) — the one honest read
barrier for write-then-recall consumers; typed timeout error carries
the pending count and names the gauge; getIndexStatus() gains
per-projection gauges. awaitPendingEmbeds() unchanged underneath.
- adoptLogAuthority() self-backfills curable divergences (pre-log
records, witness drift) by identity re-commit before flipping — a
fresh brain flips clean; log-ahead divergences still refuse loudly.
- The verification oracle gains VERB legs (all four divergence classes;
unwired = honest verbsChecked: 0, never a scope claim).
- find({where: {}}) match-all serves (was silent-empty, warm AND cold;
same fix in count/streaming/subgraph seeding); removeMany({where:{}})
refuses typed — a match-all bulk delete must be explicit.
- Aggregation native envelope stamped via noteSourceGeneration before
serializeState; the native-blob restore gates through the same
adoption verdict as caller-side state (the unconditional adopt dies).
- LC8 pinned: a wholesale directory move opens and serves identically
across all three intelligences, with history traveling.
Gates: unit 2031/2031 (156 files) · integration 812 (91 files) ·
conformance 27/27.
This commit is contained in:
parent
b35d87a7ab
commit
b53e6e8987
10 changed files with 1234 additions and 30 deletions
108
tests/integration/brain-relocation.test.ts
Normal file
108
tests/integration/brain-relocation.test.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* @module tests/integration/brain-relocation
|
||||
* @description LC8 — RELOCATABLE BRAIN DIRECTORY. A brain's directory moved
|
||||
* wholesale to a new path (rename/copy — backup-restore, disk migration,
|
||||
* container re-mount) must open and serve IDENTICALLY: no absolute paths may
|
||||
* hide in any persisted artifact. Pinned across every intelligence: point
|
||||
* reads, metadata find, semantic find, graph traversal, aggregation — plus
|
||||
* continued writes with monotonic generations and time-travel reads over
|
||||
* pre-move history.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { mkdtempSync, rmSync, renameSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Brainy } from '../../src/index.js'
|
||||
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
||||
|
||||
const dirs: string[] = []
|
||||
const brains: Brainy[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||||
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const AGG = {
|
||||
name: 'by_kind',
|
||||
source: { type: NounType.Document },
|
||||
groupBy: ['kind'] as string[],
|
||||
metrics: { count: { op: 'count' as const } }
|
||||
}
|
||||
|
||||
describe('LC8 — a moved brain directory opens and serves identically', () => {
|
||||
it('rename the directory: all three intelligences serve, writes continue, history travels', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'brainy-reloc-'))
|
||||
dirs.push(home)
|
||||
const oldPath = join(home, 'brain-old')
|
||||
const newPath = join(home, 'brain-new')
|
||||
|
||||
// Season a brain: rows, a relation, an aggregate, then flush + close.
|
||||
let brain = new Brainy({ storage: { type: 'filesystem', path: oldPath }, requireSubtype: false })
|
||||
await brain.init()
|
||||
brains.push(brain)
|
||||
brain.defineAggregate(AGG)
|
||||
const alpha = await brain.add({
|
||||
data: 'alpha document about mountain geology',
|
||||
type: NounType.Document,
|
||||
metadata: { kind: 'report', n: 1 }
|
||||
})
|
||||
const beta = await brain.add({
|
||||
data: 'beta document about coastal erosion',
|
||||
type: NounType.Document,
|
||||
metadata: { kind: 'report', n: 2 }
|
||||
})
|
||||
await brain.relate({ from: alpha, to: beta, verb: VerbType.RelatedTo })
|
||||
await brain.queryAggregate(AGG.name) // settle backfill
|
||||
const preMoveGen = brain.generation()
|
||||
await brain.flush()
|
||||
await brain.close()
|
||||
brains.pop()
|
||||
|
||||
// The move: wholesale directory rename.
|
||||
renameSync(oldPath, newPath)
|
||||
|
||||
// Reopen at the NEW path — everything serves.
|
||||
brain = new Brainy({ storage: { type: 'filesystem', path: newPath }, requireSubtype: false })
|
||||
await brain.init()
|
||||
brains.push(brain)
|
||||
brain.defineAggregate(AGG)
|
||||
|
||||
// Point read + metadata find.
|
||||
expect((await brain.get(alpha))!.data).toContain('mountain geology')
|
||||
const found = await brain.find({ where: { kind: 'report' }, limit: 10 })
|
||||
expect(found.map((r) => r.id).sort()).toEqual([alpha, beta].sort())
|
||||
|
||||
// Semantic find.
|
||||
const sem = await brain.find({ query: 'alpha document about mountain geology', limit: 3 })
|
||||
expect(sem.map((r) => r.id)).toContain(alpha)
|
||||
|
||||
// Graph traversal.
|
||||
const related = await brain.related(alpha)
|
||||
expect(related.map((r) => r.to)).toContain(beta)
|
||||
|
||||
// Aggregation.
|
||||
const agg = (await brain.queryAggregate(AGG.name)) as Array<{
|
||||
groupKey: Record<string, unknown>
|
||||
metrics: Record<string, unknown>
|
||||
}>
|
||||
const reportRow = agg.find((g) => g.groupKey['kind'] === 'report')
|
||||
expect(Number(reportRow?.metrics.count)).toBe(2)
|
||||
|
||||
// Writes continue with monotonic generations.
|
||||
const gamma = await brain.add({
|
||||
data: 'gamma addendum after the move',
|
||||
type: NounType.Document,
|
||||
metadata: { kind: 'report', n: 3 }
|
||||
})
|
||||
expect(brain.generation()).toBeGreaterThan(preMoveGen)
|
||||
expect((await brain.get(gamma))!.data).toContain('addendum')
|
||||
|
||||
// Time travel across the move boundary: the pre-move pin sees exactly
|
||||
// the pre-move world (no gamma), served from relocated history.
|
||||
const dbPast = await brain.asOf(preMoveGen)
|
||||
expect(await dbPast.get(gamma)).toBeNull()
|
||||
expect((await dbPast.get(alpha))!.data).toContain('mountain geology')
|
||||
await dbPast.release()
|
||||
}, 120000)
|
||||
})
|
||||
184
tests/integration/find-matchall-cold.test.ts
Normal file
184
tests/integration/find-matchall-cold.test.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
* @module tests/integration/find-matchall-cold
|
||||
* @description THE MATCH-ALL SILENT-EMPTY PIN: `find({ where: {} })` is a
|
||||
* match-all query — zero predicates constrain nothing — yet it used to route
|
||||
* through the index-filter branch, where `getIdsForFilter({})` answers `[]`
|
||||
* by contract. Result: 0 rows while storage held rows (worst on a freshly
|
||||
* reopened brain, where it masqueraded as data loss), the forbidden answer
|
||||
* class — a silent empty instead of served-or-refused. These tests pin the
|
||||
* law: an empty `where` routes exactly like an absent `where`, serving from
|
||||
* truth-complete sources (a storage page bounded to the offset+limit window,
|
||||
* or the column store's top-K sort under orderBy) — warm AND cold, on the
|
||||
* live brain, the Db pin path, pagination.count, streaming.entities, and the
|
||||
* semantic path (`{ query, where: {} }` must not short-circuit to `[]`).
|
||||
* The one deliberate refusal: `removeMany({ where: {} })` throws — a
|
||||
* match-all BULK DELETE must be asked for explicitly, never inherited.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Brainy } from '../../src/index.js'
|
||||
import { NounType } from '../../src/types/graphTypes.js'
|
||||
|
||||
const dirs: string[] = []
|
||||
const brains: Brainy[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||||
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function open(dir: string): Promise<Brainy> {
|
||||
const b = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false })
|
||||
await b.init()
|
||||
brains.push(b)
|
||||
return b
|
||||
}
|
||||
|
||||
/** Seed three plain documents with a sortable numeric field. */
|
||||
async function seed(brain: Brainy): Promise<string[]> {
|
||||
const ids: string[] = []
|
||||
ids.push(await brain.add({ data: 'alpha row', type: NounType.Document, metadata: { n: 1 } }))
|
||||
ids.push(await brain.add({ data: 'beta row', type: NounType.Document, metadata: { n: 2 } }))
|
||||
ids.push(await brain.add({ data: 'gamma row', type: NounType.Document, metadata: { n: 3 } }))
|
||||
await brain.flush()
|
||||
return ids
|
||||
}
|
||||
|
||||
describe('find({ where: {} }) — match-all serves, warm and cold', () => {
|
||||
it('the repro: a freshly reopened filesystem brain serves match-all (not a silent 0)', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-matchall-cold-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
await seed(brain)
|
||||
await brain.close()
|
||||
brains.pop()
|
||||
|
||||
const reopened = await open(dir)
|
||||
const rows = await reopened.find({ where: {}, limit: 10 })
|
||||
expect(rows.length, 'match-all serves every stored row on the cold brain').toBe(3)
|
||||
|
||||
// The predicate paths that always worked cold stay working — same brain.
|
||||
expect((await reopened.find({ where: { n: 1 }, limit: 10 })).length).toBe(1)
|
||||
expect((await reopened.find({ where: { 'system.type': 'document' }, limit: 10 })).length).toBe(3)
|
||||
}, 120000)
|
||||
|
||||
it('match-all + orderBy on a metadata field serves sorted after reopen', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-matchall-order-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
await seed(brain)
|
||||
await brain.close()
|
||||
brains.pop()
|
||||
|
||||
const reopened = await open(dir)
|
||||
const rows = await reopened.find({ where: {}, orderBy: 'n', order: 'desc', limit: 10 })
|
||||
expect(rows.length, 'sorted match-all serves every stored row cold').toBe(3)
|
||||
expect(
|
||||
rows.map((r) => (r.metadata as { n: number }).n),
|
||||
'orderBy is honored on the cold match-all page'
|
||||
).toEqual([3, 2, 1])
|
||||
}, 120000)
|
||||
|
||||
it('warm brain unchanged: match-all, sorted match-all, and predicates all serve in-session', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-matchall-warm-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
await seed(brain)
|
||||
|
||||
expect((await brain.find({ where: {}, limit: 10 })).length).toBe(3)
|
||||
const sorted = await brain.find({ where: {}, orderBy: 'n', order: 'asc', limit: 2 })
|
||||
expect(sorted.map((r) => (r.metadata as { n: number }).n)).toEqual([1, 2])
|
||||
expect((await brain.find({ where: { n: 2 }, limit: 10 })).length).toBe(1)
|
||||
// Pagination window respected: match-all never over-serves the page.
|
||||
expect((await brain.find({ where: {}, limit: 2, offset: 2 })).length).toBe(1)
|
||||
}, 120000)
|
||||
|
||||
it('the semantic path: find({ query, where: {} }) must not short-circuit to []', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-matchall-query-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
await seed(brain)
|
||||
await brain.close()
|
||||
brains.pop()
|
||||
|
||||
const reopened = await open(dir)
|
||||
// Before the fix, the pre-resolved empty filter matched nothing and the
|
||||
// vector search was skipped entirely — a silent [] for every such query.
|
||||
const rows = await reopened.find({ query: 'alpha row', where: {}, limit: 10 })
|
||||
expect(rows.length, 'an unconstraining where must not empty a semantic query').toBeGreaterThan(0)
|
||||
}, 120000)
|
||||
|
||||
it('the Db pin path: asOf(g).find({ where: {} }) serves at the pinned generation after reopen', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-matchall-asof-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
await brain.add({ data: 'first', type: NounType.Document, metadata: { n: 1 } })
|
||||
await brain.add({ data: 'second', type: NounType.Document, metadata: { n: 2 } })
|
||||
await brain.flush()
|
||||
const gTwo = brain.generation()
|
||||
await brain.add({ data: 'third', type: NounType.Document, metadata: { n: 3 } })
|
||||
await brain.flush()
|
||||
await brain.close()
|
||||
brains.pop()
|
||||
|
||||
const reopened = await open(dir)
|
||||
// Current-generation pin (delegates to the live find fast path).
|
||||
const now = reopened.now()
|
||||
expect((await now.find({ where: {}, limit: 10 })).length).toBe(3)
|
||||
|
||||
// Historical pin: the record-overlay path must serve match-all too.
|
||||
const past = await reopened.asOf(gTwo)
|
||||
try {
|
||||
const rows = await past.find({ where: {}, limit: 10 })
|
||||
expect(rows.length, 'match-all at the pinned generation sees exactly the rows of that generation').toBe(2)
|
||||
} finally {
|
||||
await past.release()
|
||||
}
|
||||
}, 120000)
|
||||
|
||||
it('pagination.count({ where: {} }) counts every row instead of a silent 0', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-matchall-count-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
await seed(brain)
|
||||
await brain.close()
|
||||
brains.pop()
|
||||
|
||||
const reopened = await open(dir)
|
||||
// The law: an empty where counts exactly like an absent where (the
|
||||
// unfiltered total — which by long-standing count semantics includes
|
||||
// system entities such as the VFS root, hence >= the 3 user rows).
|
||||
const emptyWhere = await reopened.pagination.count({ where: {} })
|
||||
expect(emptyWhere).toBe(await reopened.pagination.count({}))
|
||||
expect(emptyWhere).toBeGreaterThanOrEqual(3)
|
||||
}, 120000)
|
||||
|
||||
it('streaming.entities({ where: {} }) streams every row instead of nothing', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-matchall-stream-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
await seed(brain)
|
||||
await brain.close()
|
||||
brains.pop()
|
||||
|
||||
const reopened = await open(dir)
|
||||
const streamed: string[] = []
|
||||
for await (const entity of reopened.streaming.entities({ where: {} })) {
|
||||
streamed.push(entity.id)
|
||||
}
|
||||
expect(streamed.length, 'an unconstraining where streams the full store').toBeGreaterThanOrEqual(3)
|
||||
}, 120000)
|
||||
|
||||
it('removeMany({ where: {} }) refuses loudly — match-all bulk delete is never implicit', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-matchall-remove-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
await seed(brain)
|
||||
|
||||
await expect(brain.removeMany({ where: {} })).rejects.toThrow(/matches EVERYTHING/)
|
||||
// Nothing was deleted by the refused call.
|
||||
expect((await brain.find({ where: {}, limit: 10 })).length).toBe(3)
|
||||
}, 120000)
|
||||
})
|
||||
83
tests/integration/log-authority-adopt.test.ts
Normal file
83
tests/integration/log-authority-adopt.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* @module tests/integration/log-authority-adopt
|
||||
* @description THE SANCTIONED FLIP, END TO END: adoptLogAuthority() cures
|
||||
* its own curable divergences by baseline backfill — a FRESH brain (whose
|
||||
* generation-0 VFS root never entered the log) flips WITHOUT any manual
|
||||
* white-box backfill. Before this, no fresh brain could ever flip: the
|
||||
* oracle reported the bootstrap row as pre-log-record and the flip refused.
|
||||
* Log-AHEAD divergences stay incurable and refuse loudly (witness wins).
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Brainy } from '../../src/index.js'
|
||||
import { NounType } from '../../src/types/graphTypes.js'
|
||||
|
||||
const dirs: string[] = []
|
||||
const brains: Brainy[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||||
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function open(dir: string): Promise<Brainy> {
|
||||
const b = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false })
|
||||
await b.init()
|
||||
brains.push(b)
|
||||
return b
|
||||
}
|
||||
|
||||
describe('adoptLogAuthority — the sanctioned flip with self-backfill', () => {
|
||||
it('a fresh brain flips directly: the backfill cures the generation-0 baseline', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-adopt-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
const idA = await brain.add({ data: 'first row', type: NounType.Document, metadata: { n: 1 } })
|
||||
await brain.add({ data: 'second row', type: NounType.Document, metadata: { n: 2 } })
|
||||
await brain.flush()
|
||||
|
||||
const report = await brain.adoptLogAuthority()
|
||||
expect(report.verdict, 'the flip receipt is a green oracle').toBe('green')
|
||||
expect(brain.logAuthority().authority).toBe('log')
|
||||
|
||||
// The switch survives reopen; the brain keeps serving identically.
|
||||
await brain.close()
|
||||
brains.pop()
|
||||
const reopened = await open(dir)
|
||||
expect(reopened.logAuthority().authority).toBe('log')
|
||||
expect(await reopened.get(idA), 'records serve at reopen').toBeTruthy()
|
||||
const rows = await reopened.find({ where: {}, limit: 10 })
|
||||
expect(rows.length, 'match-all serves on the reopened flipped brain').toBeGreaterThanOrEqual(2)
|
||||
// And a fresh oracle run on the flipped brain stays green.
|
||||
expect((await reopened.verifyLogAuthority()).verdict).toBe('green')
|
||||
}, 120000)
|
||||
|
||||
it('witness drift (out-of-generation canonical rewrite) is cured by the backfill, then flips', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-adopt-drift-'))
|
||||
dirs.push(dir)
|
||||
const brain = await open(dir)
|
||||
const id = await brain.add({ data: 'drifter', type: NounType.Document, metadata: { v: 1 } })
|
||||
await brain.flush()
|
||||
|
||||
// Simulate maintenance rewriting canonical OUTSIDE a generation (the
|
||||
// witness-drift class): mutate the stored record directly.
|
||||
const storage = (brain as unknown as {
|
||||
storage: {
|
||||
readNounRaw(id: string): Promise<{ metadata: unknown; vector: unknown }>
|
||||
writeNounRaw(id: string, r: { metadata: unknown; vector: unknown }): Promise<void>
|
||||
}
|
||||
}).storage
|
||||
const raw = await storage.readNounRaw(id)
|
||||
await storage.writeNounRaw(id, {
|
||||
metadata: { ...(raw.metadata as Record<string, unknown>), drifted: true },
|
||||
vector: raw.vector
|
||||
})
|
||||
expect((await brain.verifyLogAuthority()).verdict, 'drift detected').toBe('red')
|
||||
|
||||
const report = await brain.adoptLogAuthority()
|
||||
expect(report.verdict).toBe('green')
|
||||
expect(brain.logAuthority().authority).toBe('log')
|
||||
}, 120000)
|
||||
})
|
||||
219
tests/integration/wait-for-indexed.test.ts
Normal file
219
tests/integration/wait-for-indexed.test.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
/**
|
||||
* @module tests/integration/wait-for-indexed
|
||||
* @description THE READ BARRIER — `brain.waitForIndexed(path?, opts?)`. A
|
||||
* consumer that writes and then semantically recalls gets ONE honest barrier
|
||||
* instead of guessing. The contract pinned here:
|
||||
*
|
||||
* 1. SEMANTIC LEG: a deferred add followed by `waitForIndexed('semantic')`
|
||||
* resolves only after the vector landed — the row is vector-searchable
|
||||
* the moment the barrier returns.
|
||||
* 2. TYPED TIMEOUT: `timeoutMs` expiry REJECTS with
|
||||
* WaitForIndexedTimeoutError carrying the leg + the pending count and
|
||||
* naming the gauge — never a silent partial wait.
|
||||
* 3. NO-ARG: every projection at the head; today that means the deferred
|
||||
* embed backlog is drained.
|
||||
* 4. SYNCHRONOUS LEGS: metadata/graph/aggregation resolve immediately by
|
||||
* design today (they update inside the write path) — even while the
|
||||
* semantic backlog is wedged.
|
||||
* 5. GAUGES: getIndexStatus().projections carries the per-leg numbers, and
|
||||
* the top-level pendingEmbeds compat field agrees with the semantic one.
|
||||
* 6. GENERATION REFINEMENT: an empty backlog satisfies any generation
|
||||
* immediately; a non-empty one falls back to the full drain.
|
||||
*/
|
||||
import { describe, it, expect, afterEach, vi } from 'vitest'
|
||||
import { Brainy, WaitForIndexedTimeoutError } from '../../src/index.js'
|
||||
import { NounType } from '../../src/types/graphTypes.js'
|
||||
|
||||
const brains: Brainy[] = []
|
||||
|
||||
async function memBrain(): Promise<Brainy> {
|
||||
const b = new Brainy({ storage: { type: 'memory' }, requireSubtype: false })
|
||||
await b.init()
|
||||
brains.push(b)
|
||||
return b
|
||||
}
|
||||
|
||||
/**
|
||||
* Abandon a poisoned in-flight embed run (its embed promise never resolves —
|
||||
* production is covered by the worker's 60s hang guard; the test takes the
|
||||
* white-box shortcut for speed), then drain so teardown never wedges.
|
||||
*/
|
||||
async function unwedge(brain: Brainy): Promise<void> {
|
||||
;(brain as unknown as { _embedWorkerFlight: Promise<void> | null })._embedWorkerFlight = null
|
||||
await brain.awaitPendingEmbeds()
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks()
|
||||
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||||
})
|
||||
|
||||
describe('waitForIndexed — the read barrier', () => {
|
||||
it("SEMANTIC LEG: deferred add → waitForIndexed('semantic') resolves and the row is vector-searchable after", async () => {
|
||||
const brain = await memBrain()
|
||||
const embedSpy = vi.spyOn(brain, 'embed')
|
||||
|
||||
const id = await brain.add({
|
||||
data: 'the quarterly revenue report for the northern region',
|
||||
type: NounType.Document,
|
||||
deferEmbedding: true,
|
||||
metadata: { kind: 'report' }
|
||||
})
|
||||
expect(embedSpy, 'no embed on the ack path').not.toHaveBeenCalled()
|
||||
expect(brain.pendingEmbedCount()).toBeGreaterThanOrEqual(1)
|
||||
|
||||
await brain.waitForIndexed('semantic')
|
||||
|
||||
// The barrier's meaning: backlog drained, vector real, row searchable.
|
||||
expect(brain.pendingEmbedCount(), 'barrier means drained').toBe(0)
|
||||
const after = await brain.get(id, { includeVectors: true })
|
||||
expect((after!.vector as number[]).length, 'real vector after the barrier').toBeGreaterThan(0)
|
||||
const hits = await brain.find({
|
||||
query: 'the quarterly revenue report for the northern region',
|
||||
searchMode: 'semantic',
|
||||
limit: 5
|
||||
})
|
||||
expect(hits.map((r) => r.id), 'vector-searchable after the barrier').toContain(id)
|
||||
})
|
||||
|
||||
it('TYPED TIMEOUT: a hung embedder + timeoutMs rejects with the typed error naming the pending count and the gauge', async () => {
|
||||
const brain = await memBrain()
|
||||
const hang = vi
|
||||
.spyOn(brain, 'embed')
|
||||
.mockImplementation(() => new Promise<number[]>(() => {}))
|
||||
|
||||
await brain.add({
|
||||
data: 'never lands while the embedder hangs',
|
||||
type: NounType.Document,
|
||||
deferEmbedding: true,
|
||||
metadata: {}
|
||||
})
|
||||
expect(brain.pendingEmbedCount()).toBe(1)
|
||||
|
||||
let caught: unknown
|
||||
try {
|
||||
await brain.waitForIndexed('semantic', { timeoutMs: 200 })
|
||||
} catch (e) {
|
||||
caught = e
|
||||
}
|
||||
|
||||
expect(caught, 'expiry REJECTS — never a silent partial wait').toBeInstanceOf(
|
||||
WaitForIndexedTimeoutError
|
||||
)
|
||||
const err = caught as WaitForIndexedTimeoutError
|
||||
expect(err.path).toBe('semantic')
|
||||
expect(err.timeoutMs).toBe(200)
|
||||
expect(err.pendingEmbeds).toBeGreaterThanOrEqual(1)
|
||||
// The message names what was still pending and the gauge to check.
|
||||
expect(err.message).toContain(`${err.pendingEmbeds} deferred embed`)
|
||||
expect(err.message).toContain('getIndexStatus().projections.semantic.pendingEmbeds')
|
||||
|
||||
hang.mockRestore()
|
||||
await unwedge(brain)
|
||||
expect(brain.pendingEmbedCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('NO-ARG: waitForIndexed() waits on the pending-embed drain (every projection at the head)', async () => {
|
||||
const brain = await memBrain()
|
||||
await brain.add({
|
||||
data: 'a deferred capture that the bare barrier must cover',
|
||||
type: NounType.Document,
|
||||
deferEmbedding: true,
|
||||
metadata: {}
|
||||
})
|
||||
expect(brain.pendingEmbedCount()).toBeGreaterThanOrEqual(1)
|
||||
|
||||
await brain.waitForIndexed()
|
||||
|
||||
expect(
|
||||
brain.pendingEmbedCount(),
|
||||
'the bare barrier drained the only asynchronous projection'
|
||||
).toBe(0)
|
||||
})
|
||||
|
||||
it('SYNCHRONOUS LEGS: metadata/graph/aggregation resolve immediately — even while the semantic backlog is wedged', async () => {
|
||||
const brain = await memBrain()
|
||||
|
||||
// Quiet brain first: all three legs resolve on a brain with no backlog.
|
||||
await brain.add({ data: 'quiet row', type: NounType.Document, metadata: { q: 1 } })
|
||||
await brain.awaitPendingEmbeds()
|
||||
await brain.waitForIndexed('metadata')
|
||||
await brain.waitForIndexed('graph')
|
||||
await brain.waitForIndexed('aggregation')
|
||||
|
||||
// The stronger pin: these projections update inside the write path today,
|
||||
// so their leg resolves immediately BY DESIGN — independent of a wedged
|
||||
// semantic backlog. (If any of them incorrectly delegated to the embed
|
||||
// drain, this test would hang.)
|
||||
const hang = vi
|
||||
.spyOn(brain, 'embed')
|
||||
.mockImplementation(() => new Promise<number[]>(() => {}))
|
||||
await brain.add({
|
||||
data: 'wedged deferred row',
|
||||
type: NounType.Document,
|
||||
deferEmbedding: true,
|
||||
metadata: {}
|
||||
})
|
||||
expect(brain.pendingEmbedCount()).toBe(1)
|
||||
|
||||
await brain.waitForIndexed('metadata')
|
||||
await brain.waitForIndexed('graph')
|
||||
await brain.waitForIndexed('aggregation')
|
||||
|
||||
hang.mockRestore()
|
||||
await unwedge(brain)
|
||||
})
|
||||
|
||||
it('GAUGES: getIndexStatus().projections carries the per-leg shape, and the compat field agrees', async () => {
|
||||
const brain = await memBrain()
|
||||
await brain.add({ data: 'gauge row', type: NounType.Document, metadata: { g: 1 } })
|
||||
await brain.awaitPendingEmbeds()
|
||||
|
||||
const status = await brain.getIndexStatus()
|
||||
expect(status.projections).toEqual({
|
||||
semantic: { pendingEmbeds: 0 },
|
||||
metadata: { synchronous: true },
|
||||
graph: { synchronous: true },
|
||||
aggregation: { pendingBackfills: 0, pendingCatchUps: 0 }
|
||||
})
|
||||
// Compat: the existing top-level gauge stays and agrees.
|
||||
expect(status.pendingEmbeds).toBe(0)
|
||||
|
||||
// The semantic gauge is honest while a backlog exists.
|
||||
const hang = vi
|
||||
.spyOn(brain, 'embed')
|
||||
.mockImplementation(() => new Promise<number[]>(() => {}))
|
||||
await brain.add({
|
||||
data: 'backlogged row',
|
||||
type: NounType.Document,
|
||||
deferEmbedding: true,
|
||||
metadata: {}
|
||||
})
|
||||
const busy = await brain.getIndexStatus()
|
||||
expect(busy.projections.semantic.pendingEmbeds).toBeGreaterThanOrEqual(1)
|
||||
expect(busy.pendingEmbeds).toBe(busy.projections.semantic.pendingEmbeds)
|
||||
|
||||
hang.mockRestore()
|
||||
await unwedge(brain)
|
||||
})
|
||||
|
||||
it('GENERATION REFINEMENT: an empty backlog satisfies any generation immediately; a non-empty one falls back to the full drain', async () => {
|
||||
const brain = await memBrain()
|
||||
await brain.add({ data: 'generation row', type: NounType.Document, metadata: {} })
|
||||
await brain.awaitPendingEmbeds()
|
||||
|
||||
// Empty backlog: the semantic watermark is at the head — >= any committed G.
|
||||
await brain.waitForIndexed('semantic', { generation: 1 })
|
||||
|
||||
// Non-empty backlog: the conservative full drain (a superset of the
|
||||
// requested wait, never a partial one).
|
||||
await brain.add({
|
||||
data: 'second generation row',
|
||||
type: NounType.Document,
|
||||
deferEmbedding: true,
|
||||
metadata: {}
|
||||
})
|
||||
await brain.waitForIndexed('semantic', { generation: 1 })
|
||||
expect(brain.pendingEmbedCount(), 'the fallback is the full drain').toBe(0)
|
||||
})
|
||||
})
|
||||
Reference in a new issue