test: pin the read-your-writes contract under the single writer

A returned write must be immediately readable by id, metadata filter, vector
search, and graph traversal — no await, delay, or retry between the write
returning and the read. This holds by construction because every projection
(canonical + HNSW + metadata index + graph index) commits inside the write's
transaction; the generation counter is the {seq} a caller can pin. The test
locks that guarantee so index maintenance can never quietly move off the commit
path (which would turn a returned write into a not-yet-queryable one).

First brainy chapter of the write/index-spine hardening program.
This commit is contained in:
David Snelling 2026-07-12 18:24:31 -07:00
parent ffd81ea206
commit eb9c4eb963

View file

@ -0,0 +1,133 @@
/**
* @module tests/integration/read-your-writes-contract
* @description The read-your-writes contract brainy's spine guarantee that a
* write which has RETURNED is immediately readable, under the single writer, by
* BOTH id and every index (metadata filter, vector search, graph traversal),
* with NO await, delay, or retry between the write returning and the read.
*
* Why it holds by construction on the canonical + JS-index path: every mutation
* runs its canonical writes AND all derived-index projections (HNSW, metadata
* index, graph index) as operations inside ONE transaction that commits before
* the method returns there is no post-return "eventual" indexing window. get()
* additionally rides the storage write-through cache (read-after-write within
* the process). The generation counter is the {seq} a caller can pin: a write
* returns generation N and every subsequent live read observes N.
*
* This pins the guarantee so no future change can quietly move index maintenance
* off the commit path (which would turn a returned write into a not-yet-queryable
* one the "a 200 is still not durability/queryability" failure the spine
* program exists to make impossible). The native accelerator must honor the same
* in-commit contract; a background consolidation/optimization must never gate
* queryability of an acknowledged write.
*/
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 } from '../../src/brainy.js'
import { NounType, VerbType } from '../../src/types/graphTypes.js'
let seq = 0
const freshId = (): string =>
`00000000-0000-4000-8000-${(++seq).toString(16).padStart(12, '0')}`
describe('read-your-writes contract (single-writer, no await between write and read)', () => {
let dir: string
let brain: any
beforeEach(async () => {
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-ryw-'))
brain = new Brainy({
requireSubtype: false,
storage: { type: 'filesystem', path: dir },
dimensions: 384,
silent: true
})
await brain.init()
})
afterEach(async () => {
await brain.close()
fs.rmSync(dir, { recursive: true, force: true })
})
it('add(): the entity is immediately readable by id, metadata filter, AND vector search', async () => {
const id = freshId()
const marker = `ryw-${id}`
await brain.add({ id, data: 'read your writes probe alpha', type: NounType.Concept, metadata: { marker } })
// id read (write-through cache / canonical)
expect(await brain.get(id)).not.toBeNull()
// metadata-filter read (metadata index committed in-transaction)
const byMeta = await brain.find({ where: { marker }, limit: 10 })
expect(byMeta.map((r: any) => r.id)).toContain(id)
// vector read (HNSW committed in-transaction)
const byVector = await brain.find({ query: 'read your writes probe alpha', limit: 10 })
expect(byVector.map((r: any) => r.id)).toContain(id)
// type-filter read
const byType = await brain.find({ type: NounType.Concept, limit: 100 })
expect(byType.map((r: any) => r.id)).toContain(id)
})
it('update(): the new metadata is immediately queryable and the old value is immediately gone', async () => {
const id = freshId()
await brain.add({ id, data: 'updatable', type: NounType.Thing, metadata: { phase: 'before' } })
await brain.update({ id, metadata: { phase: 'after' } })
const after = await brain.find({ where: { phase: 'after' }, limit: 10 })
expect(after.map((r: any) => r.id)).toContain(id)
const before = await brain.find({ where: { phase: 'before' }, limit: 10 })
expect(before.map((r: any) => r.id)).not.toContain(id)
})
it('relate(): the edge is immediately traversable', async () => {
const a = await brain.add({ id: freshId(), data: 'A', type: NounType.Thing })
const b = await brain.add({ id: freshId(), data: 'B', type: NounType.Thing })
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
expect((await brain.related(a)).map((r: any) => r.to)).toContain(b)
})
it('remove(): the entity is immediately gone from id, metadata, and vector reads', async () => {
const id = freshId()
const marker = `gone-${id}`
await brain.add({ id, data: 'to be removed promptly', type: NounType.Thing, metadata: { marker } })
expect(await brain.get(id)).not.toBeNull()
await brain.remove(id)
expect(await brain.get(id)).toBeNull()
expect((await brain.find({ where: { marker }, limit: 10 })).map((r: any) => r.id)).not.toContain(id)
expect((await brain.find({ query: 'to be removed promptly', limit: 10 })).map((r: any) => r.id)).not.toContain(id)
})
it('transact(): every item of a committed batch is immediately readable by id, index, and traversal', async () => {
const a = freshId()
const b = freshId()
const marker = `batch-${a}`
const db = await brain.transact([
{ op: 'add', id: a, data: 'batch node A', type: NounType.Thing, metadata: { marker } },
{ op: 'add', id: b, data: 'batch node B', type: NounType.Thing, metadata: { marker } },
{ op: 'relate', from: a, to: b, type: VerbType.Contains }
])
expect(db.generation).toBeGreaterThan(0)
expect(await brain.get(a)).not.toBeNull()
expect(await brain.get(b)).not.toBeNull()
const byMeta = (await brain.find({ where: { marker }, limit: 10 })).map((r: any) => r.id)
expect(byMeta).toContain(a)
expect(byMeta).toContain(b)
expect((await brain.related(a)).map((r: any) => r.to)).toContain(b)
})
it('the write returns a generation ({seq}) and a live read observes it — the awaitable-read primitive', async () => {
const id = freshId()
const before = brain.generationStore.generation()
await brain.add({ id, data: 'generation stamped', type: NounType.Thing })
const after = brain.generationStore.generation()
// The write advanced the generation…
expect(after).toBeGreaterThan(before)
// …and a read pinned to that generation observes the write (asOf ≥ N sees it).
const pinned = await brain.asOf(after)
expect(await pinned.get(id)).not.toBeNull()
})
})