/** * Generation threading through the graph-index transaction operations. * * The graph time-travel contract (AddVerb/RemoveVerb gain a `generation`) is * load-bearing for native providers: `db.asOf(g)` graph hops resolve historical * endpoints from a per-generation edge chain the provider stamps at write time. * Brainy's JS index ignores the value, so a behavioural test against it proves * nothing — these tests use a spy provider to assert the operation layer: * 1. passes the resolved generation to addVerb/removeVerb, * 2. resolves the generation at EXECUTE time, not construction (the batch * generation is only assigned once the commit begins executing), and * 3. reuses one generation across an operation's forward + rollback halves * (so an add and its undo reference the same watermark in the chain). */ import { describe, it, expect } from 'vitest' import { AddToGraphIndexOperation, RemoveFromGraphIndexOperation } from '../../../src/transaction/operations/IndexOperations.js' import type { GraphIndexProvider } from '../../../src/plugin.js' import type { GraphVerb } from '../../../src/coreTypes.js' import { VerbType } from '../../../src/types/graphTypes.js' /** A graph verb with just the fields the operations touch. */ function makeVerb(id: string): GraphVerb { return { id, sourceId: 'src-uuid', targetId: 'tgt-uuid', type: VerbType.RelatedTo, sourceInt: 10n, targetInt: 20n } as unknown as GraphVerb } /** Records every (method, generation) the operations push to the provider. */ function makeSpyProvider(): { provider: GraphIndexProvider calls: Array<{ method: 'addVerb' | 'removeVerb'; generation: bigint; verbId: string }> } { const calls: Array<{ method: 'addVerb' | 'removeVerb'; generation: bigint; verbId: string }> = [] const provider = { async addVerb(verb: GraphVerb, _s: bigint, _t: bigint, generation: bigint): Promise { calls.push({ method: 'addVerb', generation, verbId: verb.id }) return 99n // interned verb int }, async removeVerb(verbId: string, generation: bigint): Promise { calls.push({ method: 'removeVerb', generation, verbId }) } } as unknown as GraphIndexProvider return { provider, calls } } describe('Graph index operations — generation threading', () => { it('AddToGraphIndexOperation stamps addVerb (and its rollback removeVerb) at the resolved generation', async () => { const { provider, calls } = makeSpyProvider() const verb = makeVerb('verb-1') const op = new AddToGraphIndexOperation(provider, verb, 10n, 20n, () => 7n) const rollback = await op.execute() expect(calls).toEqual([{ method: 'addVerb', generation: 7n, verbId: 'verb-1' }]) await rollback() expect(calls[1]).toEqual({ method: 'removeVerb', generation: 7n, verbId: 'verb-1' }) }) it('RemoveFromGraphIndexOperation stamps removeVerb (and its rollback addVerb) at the resolved generation', async () => { const { provider, calls } = makeSpyProvider() const verb = makeVerb('verb-2') const op = new RemoveFromGraphIndexOperation(provider, verb, 10n, 20n, () => 12n) const rollback = await op.execute() expect(calls).toEqual([{ method: 'removeVerb', generation: 12n, verbId: 'verb-2' }]) await rollback() expect(calls[1]).toEqual({ method: 'addVerb', generation: 12n, verbId: 'verb-2' }) }) it('resolves the generation at EXECUTE time, not at construction', async () => { // The generation store assigns the batch generation only once the commit // begins executing — after the operations are built. The thunk must be read // then, so a value that changes between construction and execute is observed // at its execute-time value. const { provider, calls } = makeSpyProvider() let current = 1n const op = new AddToGraphIndexOperation(provider, makeVerb('verb-3'), 10n, 20n, () => current) current = 42n // store assigns the batch generation after the op is built await op.execute() expect(calls[0].generation).toBe(42n) }) it('still surfaces the provider verb int through onVerbInt after the signature change', async () => { const { provider } = makeSpyProvider() let seen: bigint | undefined const op = new AddToGraphIndexOperation( provider, makeVerb('verb-4'), 10n, 20n, () => 5n, (verbInt) => { seen = verbInt } ) await op.execute() expect(seen).toBe(99n) }) })