/** * @module tests/unit/db/generation-chain * @description Locks in the Model-B scalability fixes in `generationStore.ts`: * the per-id history chains that make `resolveAt` O(log) instead of an * O(database-age) scan of the global `committedGens`, the bounded `deltaCache` * (evicted deltas are transparently re-read from storage), and chain * maintenance across commit + compaction. These assert the OBSERVABLE behavior * — `asOf()` correctness — across the code paths those fixes touch, including the * edge cases the existing temporal suites don't reach (eviction past the cap, * and chain rebuild after compaction). */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Brainy } from '../../../src/index.js' import { NounType } from '../../../src/types/graphTypes.js' import { createTestConfig } from '../../helpers/test-factory.js' const X = '11111111-1111-4111-8111-111111111111' const Y = '22222222-2222-4222-8222-222222222222' describe('generation history chains (Model-B scalability)', () => { let brain: Brainy beforeEach(async () => { brain = new Brainy(createTestConfig()) await brain.init() }) afterEach(async () => { await brain.close() }) /** Seed X (its own committed generation), then return g0 = that generation. */ async function seedX(): Promise { const db = await brain.transact([ { op: 'add', id: X, type: NounType.Document, subtype: 'note', data: 'x', metadata: { v: 0 } } ]) await db.release() return brain.generation() } async function bumpX(v: number): Promise { const db = await brain.transact([{ op: 'update', id: X, metadata: { v } }]) await db.release() return brain.generation() } const vAt = async (gen: number): Promise => { const db = await brain.asOf(gen) const e = (await db.get(X)) as any await db.release() return e?.metadata?.v } it('resolveAt returns the value as-of each pinned generation (chain binary search)', async () => { const g0 = await seedX() const g1 = await bumpX(1) const g2 = await bumpX(2) expect(await vAt(g0)).toBe(0) // before-image of the g1 update = the seed value expect(await vAt(g1)).toBe(1) // before-image of the g2 update expect(((await brain.get(X)) as any)?.metadata?.v).toBe(2) // live head expect(g2).toBeGreaterThan(g1) expect(g1).toBeGreaterThan(g0) }) it('stays correct when an UNCHANGED entity is read at an old pin (no chain → O(1) current)', async () => { const g0 = await seedX() // Churn a DIFFERENT entity many times; X is never touched again. const db = await brain.transact([{ op: 'add', id: Y, type: NounType.Document, subtype: 'note', data: 'y', metadata: { v: 0 } }]) await db.release() for (let i = 1; i <= 30; i++) { const d = await brain.transact([{ op: 'update', id: Y, metadata: { v: i } }]) await d.release() } // X has no chain entry after g0 → resolveAt returns 'current' without scanning the 31 Y-generations. expect(await vAt(g0)).toBe(0) expect(((await brain.get(X)) as any)?.metadata?.v).toBe(0) }) it('survives deltaCache eviction (cap lowered; evicted deltas re-read from storage)', async () => { ;(brain as any).generationStore.deltaCacheMax = 4 // force eviction well before the gen count const g0 = await seedX() for (let v = 1; v <= 12; v++) await bumpX(v) // 12 gens > cap 4 → evictions // asOf still resolves correctly (ensureChains re-read every evicted delta to build the chain)… expect(await vAt(g0)).toBe(0) // …and a range op (since scans committedGens via getDelta, re-reading evicted deltas) still works. const now = await brain.now() const changed = await now.since(g0) await now.release() expect(changed.nouns).toContain(X) }) it('rebuilds chains after compaction and keeps asOf correct above the horizon', async () => { const g0 = await seedX() const g1 = await bumpX(1) const g2 = await bumpX(2) await bumpX(3) await bumpX(4) // Reclaim everything except the 2 most recent committed generations. const res = await brain.compactHistory({ retainGenerations: 2 }) expect(res.removedGenerations).toBeGreaterThan(0) // A pin ABOVE the new horizon still resolves (chains were rebuilt from the survivors)… const live = ((await brain.get(X)) as any)?.metadata?.v expect(live).toBe(4) // …and the reclaimed-depth pins are below the horizon now. expect(res.horizon).toBeGreaterThanOrEqual(g0) expect(g2).toBeGreaterThan(g1) }) })