/** * @module bigint-contract.test * @description 8.0 u64 provider contract — BigInt boundary of the JS * GraphAdjacencyIndex and the coordinator that drives it. * * Covers: * - bigint round-trip through the JS graph index: `addVerb` returns a stable * verb int, `getVerbIdsBySource`/`getVerbIdsByTarget` return that int, and * `verbIntsToIds` maps it back to the verb-id string (null for unknown ints) * - `getNeighbors` speaks entity ints both ways via the shared idMapper * - unmapped entity ints / UUIDs produce empty reads, never errors * - a missing idMapper fails loudly (wiring bug, not a silent fallback) * - coordinator-level behavior: relate → getRelations/neighbors → unrelate * works end-to-end over the BigInt boundary (public API unchanged) */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Brainy } from '../../../src/brainy.js' import { GraphAdjacencyIndex } from '../../../src/graph/graphAdjacencyIndex.js' import { EntityIdMapper } from '../../../src/utils/entityIdMapper.js' import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js' import { NounType, VerbType } from '../../../src/types/graphTypes.js' import type { GraphVerb } from '../../../src/coreTypes.js' // Verb ids are UUID-shaped by contract in 8.0 (storage enforces the shape on // every save; cortex keys verb-int interning on the raw UUID bytes). const VERB_UUID_1 = '11111111-1111-4111-8111-111111111111' const VERB_UUID_2 = '22222222-2222-4222-8222-222222222222' const VERB_UUID_3 = '33333333-3333-4333-8333-333333333333' /** Build a minimal GraphVerb for direct index-level tests. */ function makeVerb(id: string, sourceId: string, targetId: string): GraphVerb { return { id, sourceId, targetId, vector: [], type: VerbType.RelatedTo, verb: VerbType.RelatedTo } } describe('GraphAdjacencyIndex — BigInt boundary (JS implementation)', () => { let storage: MemoryStorage let idMapper: EntityIdMapper let index: GraphAdjacencyIndex beforeEach(async () => { storage = new MemoryStorage() await storage.init() idMapper = new EntityIdMapper({ storage, storageKey: 'test:graph:idMapper' }) await idMapper.init() index = new GraphAdjacencyIndex(storage, {}, idMapper) }) afterEach(async () => { await index.close() }) it('addVerb returns a stable verb int; reads return it; verbIntsToIds maps back', async () => { const sourceInt = BigInt(idMapper.getOrAssign('uuid-source')) const targetInt = BigInt(idMapper.getOrAssign('uuid-target')) const verb = makeVerb(VERB_UUID_1, 'uuid-source', 'uuid-target') verb.sourceInt = sourceInt verb.targetInt = targetInt const verbInt = await index.addVerb(verb, sourceInt, targetInt) expect(typeof verbInt).toBe('bigint') // Re-adding the same verb id returns the SAME interned int (stable). const verbIntAgain = await index.addVerb(verb, sourceInt, targetInt) expect(verbIntAgain).toBe(verbInt) // Both directed verb-int reads surface the int from addVerb. const bySource = await index.getVerbIdsBySource(sourceInt) expect(bySource).toContain(verbInt) const byTarget = await index.getVerbIdsByTarget(targetInt) expect(byTarget).toContain(verbInt) // Batch reverse resolution is order-preserving with null for unknowns. const resolved = await index.verbIntsToIds([verbInt, 999_999n]) expect(resolved).toEqual([VERB_UUID_1, null]) }) it('getNeighbors speaks entity ints in both directions', async () => { const aInt = BigInt(idMapper.getOrAssign('uuid-a')) const bInt = BigInt(idMapper.getOrAssign('uuid-b')) const verb = makeVerb(VERB_UUID_2, 'uuid-a', 'uuid-b') await index.addVerb(verb, aInt, bInt) const out = await index.getNeighbors(aInt, { direction: 'out' }) expect(out).toEqual([bInt]) const incoming = await index.getNeighbors(bInt, { direction: 'in' }) expect(incoming).toEqual([aInt]) const both = await index.getNeighbors(aInt) expect(both).toContain(bInt) }) it('reads for unmapped entity ints return empty without error', async () => { // 424242 was never assigned by the mapper — no UUID behind it. const ghost = 424_242n expect(await index.getNeighbors(ghost)).toEqual([]) expect(await index.getVerbIdsBySource(ghost)).toEqual([]) expect(await index.getVerbIdsByTarget(ghost)).toEqual([]) }) it('verb ints stay resolvable after removeVerb (interning is append-only)', async () => { const sInt = BigInt(idMapper.getOrAssign('uuid-s')) const tInt = BigInt(idMapper.getOrAssign('uuid-t')) const verb = makeVerb(VERB_UUID_3, 'uuid-s', 'uuid-t') const verbInt = await index.addVerb(verb, sInt, tInt) // Persist the verb (vector + metadata — getVerb requires both) so // removeVerb can resolve its type for count updates. await storage.saveVerb({ id: verb.id, vector: [], connections: new Map>(), verb: VerbType.RelatedTo, sourceId: verb.sourceId, targetId: verb.targetId }) await storage.saveVerbMetadata(verb.id, { verb: VerbType.RelatedTo, createdAt: Date.now() }) await index.removeVerb(verb.id) // Removed verb no longer appears in reads… expect(await index.getVerbIdsBySource(sInt)).toEqual([]) // …but its int still reverse-resolves (append-only interning). expect(await index.verbIntsToIds([verbInt])).toEqual([VERB_UUID_3]) }) it('fails loudly when the entityIdMapper is not wired', async () => { const bare = new GraphAdjacencyIndex(storage) await expect(bare.getNeighbors(1n)).rejects.toThrow(/entityIdMapper not wired/) await bare.close() }) }) describe('Coordinator — relate/related end-to-end over the BigInt boundary', () => { let brain: Brainy beforeEach(async () => { brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } }) await brain.init() }) afterEach(async () => { await brain.close() }) it('relate → getRelations → neighbors round-trips (public API unchanged)', async () => { const personId = await brain.add({ data: { name: 'Ada' }, type: NounType.Person }) const projectId = await brain.add({ data: { name: 'Analytical Engine' }, type: NounType.Thing }) const relId = await brain.relate({ from: personId, to: projectId, type: VerbType.WorksWith }) // getRelations resolves verb ints back to verb-id strings internally. const relations = await brain.getRelations({ from: personId }) expect(relations).toHaveLength(1) expect(relations[0].id).toBe(relId) expect(relations[0].to).toBe(projectId) // neighbors() routes through getNeighbors(bigint) + int → UUID mapping. const out = await brain.neighbors(personId, { direction: 'outgoing' }) expect(out).toContain(projectId) const incoming = await brain.neighbors(projectId, { direction: 'incoming' }) expect(incoming).toContain(personId) }) it('relate feeds the verb-int warm cache; duplicate detection still works', async () => { const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing }) const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing }) const first = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo }) // The AddToGraphIndexOperation callback recorded the verb int → id pair. const warmCache: Map = (brain as any).verbIntWarmCache expect([...warmCache.values()]).toContain(first) // Duplicate check runs through getVerbIdsBySource(bigint) + resolution. const duplicate = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo }) expect(duplicate).toBe(first) }) it('unrelate removes the edge from BigInt-boundary reads', async () => { const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing }) const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing }) const relId = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo }) await brain.unrelate(relId) const relations = await brain.getRelations({ from: a }) expect(relations).toHaveLength(0) }) it('neighbors() of an unknown UUID returns empty without error', async () => { const result = await brain.neighbors('00000000-0000-4000-8000-000000000000') expect(result).toEqual([]) }) it('relate() rejects a caller-supplied verb id with a teaching error (8.0 contract)', async () => { const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing }) const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing }) // RelateParams has no `id` field — untyped callers passing one used to be // silently ignored. 8.0 teaches: verb ids are brainy-generated UUIDs. await expect( brain.relate({ from: a, to: b, type: VerbType.RelatedTo, id: 'my-custom-id' } as any) ).rejects.toThrow(/Verb ids are UUIDs by contract in 8\.0/) }) })