brainy/tests/unit/utils/entity-id-mapper-u32-ceiling.test.ts
David Snelling a3d6fdb8b3 feat(8.0): GraphAccelerationProvider contract — the native graph-engine seam
Defines the optional native graph-acceleration provider (cor 3.0) that
brainy feature-detects and routes brain.graph.* / related({node}) /
find({connected}) to, falling back to pure-TS adjacency when absent. This is
the published seam cor wires its native build against (mirrors the existing
VersionedIndexProvider pattern: defined in plugin.ts, exported from index.ts,
implemented externally, feature-detected at runtime).

Contract (locked cross-team in the design thread):
- GraphAccelerationProvider: traverse, edgesForNode, graphCursorOpen/Next/Close,
  pageRank, connectedComponents, shortestPath, neighborhoodSample, topByDegree.
  Every read op is generation-aware (optional trailing generation?: bigint) so
  db.asOf(g).graph.* resolves historically; omitted = now.
- Subgraph: columnar wire format (BigInt64Array node/edge columns, Uint16Array
  edgeTypes, optional Uint8Array nodeDepth, truncated flag) — parallel typed
  arrays, never array-of-objects; brainy maps u64<->UUID lazily for rendered rows.
- OpaqueIdSet: a find() universe forwarded opaquely into traverse/search for the
  zero-crossing query->expand fusion (brainy never inspects it; the provider
  version-tags + validates the envelope).
- VectorIndexProvider.search gains allowedIds?: OpaqueIdSet | ReadonlySet<string>
  (predicate pushdown — recovers filtered recall lost to post-filtering).
- EntityIdMapperProvider gains entityIntsToUuids(BigInt64Array) — the bigint batch
  reverse resolver for Subgraph.nodes; implemented on the JS mapper (TS fallback).

All new public types exported from index.ts. Test: entityIntsToUuids round-trip
+ order-preservation + empty-int sentinel.
2026-06-21 08:12:22 -07:00

126 lines
4.7 KiB
TypeScript

/**
* @description Brainy 8.0 IdSpace contract: the JS fallback
* `EntityIdMapper` caps at `u32::MAX` to match the metadata index's
* Roaring32 bitmap width. When `nextId` would exceed the ceiling,
* `getOrAssign` throws `EntityIdSpaceExceeded` with a message pointing
* at the cortex 3.0 binary mapper's `idSpace: 'u64'` mode as the
* migration path.
*
* This is the lockstep counterpart to cortex 3.0's Piece 10 / Step 15
* (TS wrapper + napi BigInt siblings + Roaring32-or-Treemap
* `PostingList` enum). Without this guard, a JS-only brainy install
* past 4.29 B entities would silently truncate entity ids into the
* low-32-bit range, zeroing query results and corrupting any
* persisted int-keyed structure that consumed the mapper.
*/
import { describe, it, expect } from 'vitest'
import {
EntityIdMapper,
EntityIdSpaceExceeded,
U32_ENTITY_ID_MAX,
} from '../../../src/utils/entityIdMapper.js'
/**
* Construct a fresh in-memory mapper without going through brainy's
* full storage adapter — we only exercise the in-RAM ceiling guard
* here, so a minimal `getMetadata`-returning shim is enough.
*/
function makeMapper(): EntityIdMapper {
const storage = {
getMetadata: async () => null,
setMetadata: async () => {},
getNouns: async () => ({ items: [], totalCount: 0 }),
} as any
return new EntityIdMapper({ storage })
}
describe('EntityIdMapper U32 IdSpace ceiling (Brainy 8.0)', () => {
it('U32_ENTITY_ID_MAX equals 0xFFFF_FFFF (u32 ceiling)', () => {
expect(U32_ENTITY_ID_MAX).toBe(0xffff_ffff)
})
it('EntityIdSpaceExceeded is an Error subclass with attempted + ceiling', () => {
const err = new EntityIdSpaceExceeded(0xffff_ffff + 1)
expect(err).toBeInstanceOf(Error)
expect(err.name).toBe('EntityIdSpaceExceeded')
expect(err.ceiling).toBe(0xffff_ffff)
expect(err.attempted).toBe(0x1_0000_0000)
expect(err.message).toMatch(/u32::MAX/)
expect(err.message).toMatch(/@soulcraft\/cor/)
expect(err.message).toMatch(/idSpace: 'u64'/)
})
it('normal allocations under the ceiling succeed', async () => {
const m = makeMapper()
await m.init()
const a = m.getOrAssign('uuid-a')
const b = m.getOrAssign('uuid-b')
expect(a).toBe(1)
expect(b).toBe(2)
expect(a).not.toBe(b)
})
it('throws EntityIdSpaceExceeded when nextId would exceed u32::MAX', async () => {
const m = makeMapper()
await m.init()
// Reach into the mapper to bump `nextId` past the ceiling without
// actually allocating 4.29 B entities at test time. This mirrors
// the runtime invariant the guard protects.
;(m as any).nextId = U32_ENTITY_ID_MAX + 1
expect(() => m.getOrAssign('uuid-overflow')).toThrow(EntityIdSpaceExceeded)
})
it('the last representable u32 int IS allocatable (boundary case)', async () => {
const m = makeMapper()
await m.init()
;(m as any).nextId = U32_ENTITY_ID_MAX
// `nextId === U32_ENTITY_ID_MAX` is still within range — the
// guard checks `> U32_ENTITY_ID_MAX`, so this last allocation
// succeeds.
const id = m.getOrAssign('uuid-at-ceiling')
expect(id).toBe(U32_ENTITY_ID_MAX)
// The NEXT allocation overflows.
expect(() => m.getOrAssign('uuid-after-ceiling')).toThrow(
EntityIdSpaceExceeded,
)
})
it('existing uuid lookup never overflows even when nextId is past ceiling', async () => {
const m = makeMapper()
await m.init()
const assigned = m.getOrAssign('uuid-existing')
;(m as any).nextId = U32_ENTITY_ID_MAX + 1
// Looking up an already-assigned UUID does NOT allocate; the
// guard is on the assign path only.
expect(m.getOrAssign('uuid-existing')).toBe(assigned)
})
})
describe('EntityIdMapper.entityIntsToUuids (bigint batch resolver — graph engine)', () => {
it('reverse-resolves a BigInt64Array of assigned ints to UUIDs, in order', async () => {
const m = makeMapper()
await m.init()
const a = m.getOrAssign('uuid-a')
const b = m.getOrAssign('uuid-b')
const c = m.getOrAssign('uuid-c')
// Out-of-assignment order, to prove it's positional (not sorted).
const ints = BigInt64Array.from([BigInt(c), BigInt(a), BigInt(b)])
expect(m.entityIntsToUuids(ints)).toEqual(['uuid-c', 'uuid-a', 'uuid-b'])
})
it('yields "" for a never-assigned int (order-preserving, no drop)', async () => {
const m = makeMapper()
await m.init()
const a = m.getOrAssign('uuid-a')
const ints = BigInt64Array.from([BigInt(a), 999999n])
// Position is preserved — the unknown int does not collapse the array.
expect(m.entityIntsToUuids(ints)).toEqual(['uuid-a', ''])
})
it('returns an empty array for empty input', async () => {
const m = makeMapper()
await m.init()
expect(m.entityIntsToUuids(new BigInt64Array(0))).toEqual([])
})
})