/** * @module tests/integration/id-normalization * @description Correctness gate for the 8.0 transparent entity-id normalization * layer (#18). A caller may use a natural string key (e.g. `'user-1'`) anywhere * an id is accepted; it is mapped to a STABLE UUID (`v5('user-1')`) on creation * AND on every lookup, so it round-trips transparently while the engine only * ever sees a UUID. The caller's original string is preserved under * {@link ORIGINAL_ID_KEY} in the stored entity metadata. A real UUID passes * through untouched (no `_originalId`). * * The layer is ALL-OR-NOTHING: every creation AND lookup path must normalize * identically, or round-trips break. These tests prove the contract holds * end-to-end across `add` / `get` / `update` / `remove` / `relate` / `related` * / `find({ connected })` / `transact` / `addMany` / `relateMany`, plus * determinism (same key → same UUID, upsert not duplicate), UUID passthrough, * and the v7 defaults for no-id `add()` and `newId()`. * * All entities carry explicit 384-dim vectors so no test invokes the embedder. */ import { describe, it, expect } from 'vitest' import { Brainy } from '../../src/brainy.js' import { NounType, VerbType } from '../../src/types/graphTypes.js' import { v5, v7, isUUID } from '../../src/universal/uuid.js' import { ORIGINAL_ID_KEY } from '../../src/utils/idNormalization.js' /** Deterministic 384-dim vector so no test ever invokes the embedder. */ function vec(seed: number): number[] { return Array.from({ length: 384 }, (_, i) => ((seed * 31 + i * 7) % 100) / 100) } /** Fresh in-memory brain (no embedder, no subtype enforcement). */ async function makeBrain(): Promise { const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } }) await brain.init() return brain } describe('id normalization — transparent string-key round-trips', () => { it('1. add() returns v5(key); get(key) and get(returnedId) both resolve; _originalId preserved', async () => { const brain = await makeBrain() const returnedId = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person }) // The returned id is the stable v5 mapping, a real UUID. expect(returnedId).toBe(v5('user-1')) expect(isUUID(returnedId)).toBe(true) // get() by the natural key AND by the canonical id both resolve. const byKey = await brain.get('user-1') const byId = await brain.get(returnedId) expect(byKey).not.toBeNull() expect(byId).not.toBeNull() expect(byKey!.id).toBe(returnedId) expect(byId!.id).toBe(returnedId) // The caller's original string is surfaced under ORIGINAL_ID_KEY. expect(byKey!.metadata[ORIGINAL_ID_KEY]).toBe('user-1') }) it('2. relate() by string keys; related(key) and related({from:key}) return the edge to v5(toKey)', async () => { const brain = await makeBrain() await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person }) await brain.add({ id: 'doc-1', vector: vec(2), type: NounType.Document }) await brain.relate({ from: 'user-1', to: 'doc-1', type: VerbType.Creates }) const expectedTo = v5('doc-1') const viaString = await brain.related('user-1') expect(viaString.length).toBe(1) expect(viaString[0].from).toBe(v5('user-1')) expect(viaString[0].to).toBe(expectedTo) const viaFrom = await brain.related({ from: 'user-1' }) expect(viaFrom.length).toBe(1) expect(viaFrom[0].to).toBe(expectedTo) // And the inbound anchor resolves too. const viaTo = await brain.related({ to: 'doc-1' }) expect(viaTo.length).toBe(1) expect(viaTo[0].from).toBe(v5('user-1')) }) it('3. update() by string key reflects on get(key)', async () => { const brain = await makeBrain() await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { role: 'admin' } }) await brain.update({ id: 'user-1', metadata: { role: 'owner' } }) const e = await brain.get('user-1') expect(e).not.toBeNull() expect((e!.metadata as any).role).toBe('owner') // Original id still surfaced after update. expect(e!.metadata[ORIGINAL_ID_KEY]).toBe('user-1') }) it('4. remove() by string key deletes; get(key) is null', async () => { const brain = await makeBrain() await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person }) expect(await brain.get('user-1')).not.toBeNull() await brain.remove('user-1') expect(await brain.get('user-1')).toBeNull() // The canonical id is gone too. expect(await brain.get(v5('user-1'))).toBeNull() }) it('5. find({ connected: { from: key } }) resolves the anchor key', async () => { const brain = await makeBrain() await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person }) await brain.add({ id: 'doc-1', vector: vec(2), type: NounType.Document }) await brain.relate({ from: 'user-1', to: 'doc-1', type: VerbType.Creates }) const results = await brain.find({ connected: { from: 'user-1' } }) const ids = results.map((r) => r.id) expect(ids).toContain(v5('doc-1')) }) it('6. transact() add+relate by string keys round-trips with consistent canonical ids', async () => { const brain = await makeBrain() // Seed user-1 so the relate op has a target to point at. await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person }) const db = await brain.transact([ { op: 'add', id: 'u-2', vector: vec(3), type: NounType.Person }, { op: 'relate', from: 'u-2', to: 'user-1', type: VerbType.RelatedTo } ]) // Receipt ids resolve to the canonical mappings. expect(db.receipt.ids[0]).toBe(v5('u-2')) // get('u-2') works and surfaces the original id. const u2 = await brain.get('u-2') expect(u2).not.toBeNull() expect(u2!.id).toBe(v5('u-2')) expect(u2!.metadata[ORIGINAL_ID_KEY]).toBe('u-2') // The relation links the right canonical ids. const rels = await brain.related('u-2') expect(rels.length).toBe(1) expect(rels[0].from).toBe(v5('u-2')) expect(rels[0].to).toBe(v5('user-1')) }) it('7. addMany() + relateMany() with string ids round-trip', async () => { const brain = await makeBrain() const added = await brain.addMany({ items: [ { id: 'a-1', vector: vec(10), type: NounType.Person }, { id: 'b-1', vector: vec(11), type: NounType.Document } ] }) expect(added.successful).toContain(v5('a-1')) expect(added.successful).toContain(v5('b-1')) const relIds = await brain.relateMany({ items: [{ from: 'a-1', to: 'b-1', type: VerbType.Creates }] }) expect(relIds.length).toBe(1) const a1 = await brain.get('a-1') expect(a1).not.toBeNull() expect(a1!.metadata[ORIGINAL_ID_KEY]).toBe('a-1') const rels = await brain.related('a-1') expect(rels.length).toBe(1) expect(rels[0].to).toBe(v5('b-1')) }) it('8. determinism: same key maps to same UUID — two adds upsert ONE entity, not two', async () => { const brain = await makeBrain() const id1 = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { n: 1 } }) const id2 = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { n: 2 } }) // Stable mapping across calls. expect(id1).toBe(id2) expect(id1).toBe(v5('user-1')) // Only ONE entity exists (the second add overwrote the first). const count = await brain.getNounCount() expect(count).toBe(1) const e = await brain.get('user-1') expect((e!.metadata as any).n).toBe(2) }) it('9. valid-UUID passthrough: a real UUID is kept verbatim with NO _originalId', async () => { const brain = await makeBrain() const realUuid = v7() const returnedId = await brain.add({ id: realUuid, vector: vec(5), type: NounType.Thing }) expect(returnedId).toBe(realUuid) const e = await brain.get(realUuid) expect(e).not.toBeNull() expect(e!.id).toBe(realUuid) expect(e!.metadata[ORIGINAL_ID_KEY]).toBeUndefined() }) it('10. no-id add() mints a v7; newId() mints a v7', async () => { const brain = await makeBrain() const autoId = await brain.add({ vector: vec(6), type: NounType.Thing }) expect(isUUID(autoId)).toBe(true) // v7 carries version nibble '7' at the canonical position. expect(autoId[14]).toBe('7') const minted = brain.newId() expect(isUUID(minted)).toBe(true) expect(minted[14]).toBe('7') // The auto-id entity round-trips by its returned canonical id. const e = await brain.get(autoId) expect(e).not.toBeNull() expect(e!.metadata[ORIGINAL_ID_KEY]).toBeUndefined() }) })