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:
parent
ffd81ea206
commit
eb9c4eb963
1 changed files with 133 additions and 0 deletions
133
tests/integration/read-your-writes-contract.test.ts
Normal file
133
tests/integration/read-your-writes-contract.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue