/** * @module tests/integration/transact-forward-ref-graph * @description Regression for a consumer-reported native/JS parity bug: * `transact([{op:'add', id:X}, {op:'relate', to:X}])` — the platform's first * atomic add+relate consumer — threw in `EntityIdMapper.getOrAssign` at PLAN * time on the native id mapper (an entity added in the same batch does not * exist when the planner runs, so a strict mapper rightly refuses to assign; * the permissive JS mapper masked the bug — and silently leaked an int * assignment whenever the batch was later rejected at precommit). * * Fix: graph-index operations resolve endpoint ints at EXECUTE time (a lazy * thunk, mirroring the existing lazy `generationFn`), after the batch's add * operations have applied. These tests pin every forward-ref shape on the JS * path, including the JS-observable deferral proof: a REJECTED batch leaves * the id mapper untouched (pre-fix, plan-time resolution assigned ints for * entities that never came to exist). The native side is locked in by the * accelerator's own gate running this same shape. */ 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('transact() forward references into the graph index (native/JS parity)', () => { let dir: string let brain: any beforeEach(async () => { process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true' dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-fwd-ref-')) 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('CASE 1 (the exact repro): add + relate-to-that-add in ONE transact', async () => { const agent = await brain.add({ id: freshId(), data: 'agent probe', type: NounType.Person }) const thread = freshId() const db = await brain.transact([ { op: 'add', id: thread, data: 'thread PROBE-ONE', type: NounType.Thing }, { op: 'relate', from: agent, to: thread, type: VerbType.RelatedTo } ]) // The batch committed as one generation and the edge traverses. expect(db.generation).toBeGreaterThan(0) const related = await brain.related(agent) expect(related.map((r: any) => r.to)).toContain(thread) }) it('both endpoints created in-batch: add A + add B + relate A→B', async () => { const a = freshId() const b = freshId() await brain.transact([ { op: 'add', id: a, data: 'node A', type: NounType.Thing }, { op: 'add', id: b, data: 'node B', type: NounType.Thing }, { op: 'relate', from: a, to: b, type: VerbType.Contains } ]) expect((await brain.related(a)).map((r: any) => r.to)).toContain(b) }) it('bidirectional relate to an in-batch add: both edges traverse', async () => { const hub = await brain.add({ id: freshId(), data: 'hub', type: NounType.Thing }) const spoke = freshId() await brain.transact([ { op: 'add', id: spoke, data: 'spoke', type: NounType.Thing }, { op: 'relate', from: hub, to: spoke, type: VerbType.RelatedTo, bidirectional: true } ]) expect((await brain.related(hub)).map((r: any) => r.to)).toContain(spoke) expect((await brain.related(spoke)).map((r: any) => r.to)).toContain(hub) }) it('add + relate + remove in ONE transact: the cascade covers the in-batch verb', async () => { const keeper = await brain.add({ id: freshId(), data: 'keeper', type: NounType.Thing }) const doomed = freshId() await brain.transact([ { op: 'add', id: doomed, data: 'doomed', type: NounType.Thing }, { op: 'relate', from: keeper, to: doomed, type: VerbType.Contains }, { op: 'remove', id: doomed } ]) expect(await brain.get(doomed)).toBeNull() expect((await brain.related(keeper)).length).toBe(0) }) it('CASE 2 control: the same two ops split across two transacts still work', async () => { const agent = await brain.add({ id: freshId(), data: 'agent two', type: NounType.Person }) const thread = freshId() await brain.transact([{ op: 'add', id: thread, data: 'thread two', type: NounType.Thing }]) await brain.transact([{ op: 'relate', from: agent, to: thread, type: VerbType.RelatedTo }]) expect((await brain.related(agent)).map((r: any) => r.to)).toContain(thread) }) it('a REJECTED forward-ref batch leaves the id mapper untouched (the deferral proof)', async () => { const existing = await brain.add({ id: freshId(), data: 'cas anchor', type: NounType.Thing }) const never = freshId() // Reject the batch via a stale per-op CAS — planning completes, the // commit precondition throws, nothing applies. await expect( brain.transact([ { op: 'add', id: never, data: 'never exists', type: NounType.Thing }, { op: 'relate', from: existing, to: never, type: VerbType.RelatedTo }, { op: 'update', id: existing, metadata: { poke: 1 }, ifRev: 999 } ]) ).rejects.toMatchObject({ name: 'RevisionConflictError' }) // Nothing applied… expect(await brain.get(never)).toBeNull() // …and the id mapper was never asked to assign for the phantom entity — // pre-fix, plan-time resolution leaked an int here on every rejected batch. const idMapper = brain['metadataIndex'].getIdMapper() expect(idMapper.getInt(never)).toBeUndefined() // The same shape then succeeds cleanly on retry. await brain.transact([ { op: 'add', id: never, data: 'exists now', type: NounType.Thing }, { op: 'relate', from: existing, to: never, type: VerbType.RelatedTo } ]) expect((await brain.related(existing)).map((r: any) => r.to)).toContain(never) }) })