/** * @module tests/integration/transact-edge-delete-bigint-aliasing * @description Regression for a fleet-adoption blocker: ANY edge delete * inside `transact()` — a direct unrelate or a noun-remove's cascade — * aborted with the metadata seam's BigInt JSON-guard error on a strict * (native) metadata provider. * * The aliasing chain: `planTxUnrelate`/the remove-cascade pass the SAME verb * object to the graph-retraction op and the metadata-retraction op. The * metadata leg's JSON-safe wrap ran at PLAN time, when the verb was still * clean — so it returned the same reference. At EXECUTE time the graph op * runs first and `resolveVerbEndpointInts` mirrors BigInt * `sourceInt`/`targetInt` onto the shared object (deliberately deferred for * same-batch forward refs — see transact-forward-ref-graph.test.ts); the * metadata op then crossed the seam with the polluted object. Direct * `unrelate()` resolves ints at BUILD time, before its sanitize, which is why * only the transact() shapes ever hit it. * * Fix under pin: the JSON-safe view is taken AT THE CROSSING — inside the * metadata-index operations' execute/rollback — so no plan-vs-execute * ordering can bypass it. The JS baseline index tolerates BigInts (it would * mask the bug), so these pins SPY on the seam and assert what actually * crossed, exactly as a strict native provider would judge it. */ 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' import { AddToMetadataIndexOperation, RemoveFromMetadataIndexOperation } from '../../src/transaction/operations/index.js' let seq = 0 const freshId = (): string => `00000000-0000-4000-8000-${(++seq).toString(16).padStart(12, '0')}` /** Top-level BigInt-valued keys of a candidate seam crossing (the guard's law). */ const bigintKeys = (metadata: unknown): string[] => { if (metadata === null || typeof metadata !== 'object') return [] return Object.entries(metadata as Record) .filter(([, v]) => typeof v === 'bigint') .map(([k]) => k) } describe('transact() edge deletes never carry BigInt across the metadata seam', () => { let dir: string let brain: any let crossings: Array<{ door: string; id: string; keys: string[] }> beforeEach(async () => { process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true' dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-tx-bigint-')) brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir }, dimensions: 384, silent: true }) await brain.init() // Spy on the seam the way a strict native provider judges it: record the // BigInt-valued top-level keys of every metadata argument that crosses. // The JS baseline index tolerates BigInts, so without this the baseline // run would green a shape the native pair aborts on. crossings = [] const index = brain.metadataIndex for (const door of ['addToIndex', 'removeFromIndex'] as const) { const real = index[door].bind(index) index[door] = (id: string, metadata: unknown, ...rest: unknown[]) => { crossings.push({ door, id, keys: bigintKeys(metadata) }) return real(id, metadata, ...rest) } } }) afterEach(async () => { await brain.close() fs.rmSync(dir, { recursive: true, force: true }) }) it('CASE 1 (the fleet repro): relate, then transact([{op: unrelate}])', 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 }) const verbId = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo }) crossings.length = 0 await brain.transact([{ op: 'unrelate', id: verbId }]) const polluted = crossings.filter((c) => c.keys.length > 0) expect(polluted).toEqual([]) expect(await brain.storage.getVerb(verbId)).toBeFalsy() }) it('CASE 2 (the cascade shape): transact([{op: remove}]) cascading edge deletes', 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 }) const c = await brain.add({ id: freshId(), data: 'c', type: NounType.Thing }) const ab = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo }) const ca = await brain.relate({ from: c, to: a, type: VerbType.RelatedTo }) crossings.length = 0 await brain.transact([{ op: 'remove', id: a }]) const polluted = crossings.filter((c2) => c2.keys.length > 0) expect(polluted).toEqual([]) expect(await brain.get(a)).toBeFalsy() expect(await brain.storage.getVerb(ab)).toBeFalsy() expect(await brain.storage.getVerb(ca)).toBeFalsy() }) it('CASE 3 (one batch, both legs): adds + relate + unrelate of a pre-existing edge', 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 }) const old = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo }) const x = freshId() crossings.length = 0 await brain.transact([ { op: 'add', id: x, data: 'x', type: NounType.Thing }, { op: 'relate', from: a, to: x, type: VerbType.RelatedTo }, { op: 'unrelate', id: old } ]) const polluted = crossings.filter((c) => c.keys.length > 0) expect(polluted).toEqual([]) expect(await brain.storage.getVerb(old)).toBeFalsy() const edges = await brain.related({ from: a }) expect(edges.length).toBe(1) expect(edges[0].id).not.toBe(old) }) }) describe('the metadata-index operations sanitize at the crossing, not at construction', () => { /** A strict seam: refuses BigInts exactly as the native provider does. */ const strictIndex = () => { const seen: Array<{ door: string; keys: string[] }> = [] const judge = (door: string, metadata: unknown) => { const keys = bigintKeys(metadata) seen.push({ door, keys }) if (keys.length > 0) { throw new Error( `${door}: the metadata object violates the provider seam's JSON ` + `contract — BigInt at ${keys.join(', ')}.` ) } } return { seen, addToIndex: async (_id: string, metadata: unknown) => judge('addToIndex', metadata), removeFromIndex: async (_id: string, metadata: unknown) => judge('removeFromIndex', metadata) } } it('RemoveFromMetadataIndexOperation: entity mutated AFTER construction still crosses clean', async () => { const index = strictIndex() const verb: Record = { id: 'v1', sourceId: 'a', targetId: 'b' } const op = new RemoveFromMetadataIndexOperation(index as any, 'v1', verb, () => 7n) // The graph leg's execute-time endpoint resolution, simulated: the shared // object is polluted between plan and execute. verb.sourceInt = 800_000n verb.targetInt = 800_001n const rollback = await op.execute() await rollback() expect(index.seen.map((s) => s.keys)).toEqual([[], []]) }) it('AddToMetadataIndexOperation: same law on the add leg and its rollback', async () => { const index = strictIndex() const verb: Record = { id: 'v2', sourceId: 'a', targetId: 'b' } const op = new AddToMetadataIndexOperation(index as any, 'v2', verb, () => 7n) verb.sourceInt = 800_000n verb.targetInt = 800_001n const rollback = await op.execute() await rollback() expect(index.seen.map((s) => s.keys)).toEqual([[], []]) }) })