/** * @module tests/unit/brainy/reserved-field-policy * @description The 8.0 `reservedFieldPolicy` matrix — what happens when an * untyped (JavaScript) caller smuggles a Brainy-reserved field INSIDE the * `metadata` bag of a write call, past the compile-time guard. * * 8.0 is a clean break with no silent failures. The decided contract: * - `'throw'` (DEFAULT): a reserved key in the bag throws a clear Error naming * the offending key(s) and the correct write path. No remap, no data loss. * - `'warn'`: legacy remap PLUS a one-shot (per method+field, per process) * warning for EVERY reserved key found. * - `'remap'`: the pre-8.0 silent remap, no warning. * * The deep correctness of the remap itself (top-level precedence, system-managed * drops, transact()/with() mirrors, read-side splitting) lives in * tests/unit/brainy/update-reserved-metadata-remap.test.ts (which now runs under * `reservedFieldPolicy: 'remap'`). This file pins the POLICY SELECTION and the * throw/warn behaviors. * * Compile-time callers can't write these shapes at all (see * tests/unit/types/reserved-metadata-keys.test-d.ts); the `as object` widenings * below simulate untyped callers. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { Brainy } from '../../../src/index.js' import { NounType, VerbType } from '../../../src/types/graphTypes.js' import { createTestConfig } from '../../helpers/test-factory.js' import { prodLog } from '../../../src/utils/logger.js' describe('reservedFieldPolicy', () => { describe("default policy is 'throw'", () => { let brain: Brainy beforeEach(async () => { // No reservedFieldPolicy override → resolves to 'throw'. brain = new Brainy(createTestConfig()) await brain.init() }) afterEach(async () => { await brain.close() }) it('add() throws naming the offending key and the correct write path', async () => { await expect( brain.add({ type: NounType.Concept, subtype: 'general', data: 'x', metadata: { confidence: 0.8 } as object }) ).rejects.toThrow(/metadata\.confidence is a reserved field/) // The error names the right param and the reserved list for discoverability. await expect( brain.add({ type: NounType.Concept, subtype: 'general', data: 'x', metadata: { confidence: 0.8 } as object }) ).rejects.toThrow(/'confidence' param.*RESERVED_ENTITY_FIELDS/s) }) it('add() lists EVERY offending key when several are present', async () => { const err = await brain .add({ type: NounType.Person, data: 'multi', metadata: { confidence: 0.5, weight: 0.6, subtype: 'employee' } as object }) .catch((e) => e as Error) expect(err).toBeInstanceOf(Error) expect(err.message).toMatch(/confidence/) expect(err.message).toMatch(/weight/) expect(err.message).toMatch(/subtype/) }) it('update() throws on a reserved key in the patch', async () => { const id = await brain.add({ type: NounType.Concept, subtype: 'general', data: 'y' }) await expect( brain.update({ id, metadata: { confidence: 0.3 } as object }) ).rejects.toThrow(/metadata\.confidence is a reserved field/) }) it('relate() throws on a reserved key in the bag', async () => { const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' }) const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' }) await expect( brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'colleague', metadata: { confidence: 0.4 } as object }) ).rejects.toThrow(/metadata\.confidence is a reserved field.*RESERVED_RELATION_FIELDS/s) }) it('updateRelation() throws on a reserved key in the patch', async () => { const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' }) const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' }) const relId = await brain.relate({ from: a, to: b, type: VerbType.ReportsTo, subtype: 'direct' }) await expect( brain.updateRelation({ id: relId, metadata: { weight: 0.2 } as object }) ).rejects.toThrow(/metadata\.weight is a reserved field/) }) it('transact() add op throws on a reserved key in the bag', async () => { await expect( brain.transact([ { op: 'add', type: NounType.Concept, subtype: 'general', data: 'tx', metadata: { confidence: 0.7 } as object } ]) ).rejects.toThrow(/metadata\.confidence is a reserved field/) }) it('a custom (non-reserved) key in the bag does NOT throw', async () => { const id = await brain.add({ type: NounType.Concept, subtype: 'general', data: 'ok', metadata: { status: 'draft', rating: 4 } }) const entity = await brain.get(id) expect(entity?.metadata).toEqual({ status: 'draft', rating: 4 }) }) }) describe("'remap' policy remaps silently (no warning)", () => { let brain: Brainy let warnSpy: ReturnType beforeEach(async () => { warnSpy = vi.spyOn(prodLog, 'warn').mockImplementation(() => {}) brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' })) await brain.init() }) afterEach(async () => { await brain.close() warnSpy.mockRestore() }) it('lifts user-mutable reserved fields to top-level without warning', async () => { const id = await brain.add({ type: NounType.Person, data: 'remap lift', metadata: { confidence: 0.8, weight: 0.6, subtype: 'employee', dept: 'eng' } as object }) const entity = await brain.get(id) expect(entity?.confidence).toBe(0.8) expect(entity?.weight).toBe(0.6) expect(entity?.subtype).toBe('employee') expect(entity?.metadata).toEqual({ dept: 'eng' }) // 'remap' is silent about reserved fields (unrelated storage logs may fire, // so assert specifically that no reserved-field warning was emitted). const reservedWarned = warnSpy.mock.calls.some((c) => String(c[0]).includes('reserved field') ) expect(reservedWarned).toBe(false) }) it('preserves _originalId on natural-key ids through the remap path', async () => { // A speculative view applies the same normalization and maps a natural-key // id to a stable UUID, preserving the caller's original string. const base = await brain.now() const speculative = await base.with([ { op: 'add', id: 'remap-spec-entity', type: NounType.Concept, subtype: 'general', data: 'spec', metadata: { confidence: 0.65, custom: 'spec' } as object } ]) const entity = await speculative.get('remap-spec-entity') expect(entity?.confidence).toBe(0.65) expect(entity?.metadata).toEqual({ custom: 'spec', _originalId: 'remap-spec-entity' }) await speculative.release() await base.release() }) }) describe("'warn' policy remaps AND warns once per key", () => { let brain: Brainy let warnSpy: ReturnType beforeEach(async () => { warnSpy = vi.spyOn(prodLog, 'warn').mockImplementation(() => {}) brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'warn' })) await brain.init() }) afterEach(async () => { await brain.close() warnSpy.mockRestore() }) it('remaps the value (same as remap) and emits a warning naming the field', async () => { // Use a method+field combo unique to this test so the per-process one-shot // registry has not already consumed it. const id = await brain.add({ type: NounType.Person, data: 'warn lift', // weight is user-mutable → remapped; this is the only 'warn'-policy // add({ weight }) in the suite, so the one-shot warning fires here. metadata: { weight: 0.42, dept: 'eng' } as object }) const entity = await brain.get(id) // Value is honored (remap still happens under 'warn'). expect(entity?.weight).toBe(0.42) expect(entity?.metadata).toEqual({ dept: 'eng' }) // And a warning was emitted naming the reserved field. expect(warnSpy).toHaveBeenCalled() const warned = warnSpy.mock.calls.some((c) => String(c[0]).includes("'weight'") ) expect(warned).toBe(true) }) it('warns for system-managed keys too (closes the historical gap)', async () => { // Pre-8.0 only system-managed fields warned; 'warn' warns for every key. // 'createdBy' (system-managed on update) is unique to this test. const id = await brain.add({ type: NounType.Concept, subtype: 'general', data: 'sys' }) warnSpy.mockClear() await brain.update({ id, metadata: { createdBy: 'nope', keep: 'me' } as object }) const entity = await brain.get(id) // System-managed key dropped; custom field merged. expect((entity?.metadata as Record)?.createdBy).toBeUndefined() expect((entity?.metadata as Record)?.keep).toBe('me') // A warning was emitted for the dropped system-managed key. const warned = warnSpy.mock.calls.some((c) => String(c[0]).includes("'createdBy'") ) expect(warned).toBe(true) }) }) })