feat(namespace): NO SPECIAL NAMES + storage fidelity — the ruled completion of the field-addressing law
The write side of the law, ruled 2026-08-03: data is either in main space where developers can use anything, or it is in system.*. - The reserved-name write door DIES: add/update/relate/updateRelation metadata bags accept EVERY name (confidence, type, id, data, level, content, ...) as ordinary user fields — indexed, filterable, sortable, aggregatable, identical to any other field. The remap/enforce/warn machinery, the reservedFieldPolicy config (now a typed init refusal), and the compile-time metadata key bans are all removed. The one write refusal left: keys spelled 'system.*' (namespace forgery), now enforced on all four write doors. - STORED RECORDS GO NESTED (v2): engine fields top-level, the user bag nested verbatim under 'metadata', sealed by a format stamp — by-name storage discrimination is unsound once colliders are admitted. Legacy flat records stay readable forever through the shape-aware splitters (sound for them: the old door refused colliders). Time travel rides the same split (generation store snapshots whole records). - Name-based index exclusions DIE: user frame indexes every name; the excludeFields/indexedFields knobs and their silent-[] holes are gone; bulk-payload protection is value-shape only, uniform across names. - Consumer-sweep findings fixed in the same wave: per-type counts read the frozen 'system.type' column (addToIndex sort, affinity tracking, cold-count rehydration, VFS type bitmaps — legacy 'noun' fallback for pre-rebuild reads); resolveHiddenIds addresses 'system.visibility' (bare 'visibility' was a silent no-op under the law — VFS/system entities leaked into default reads). - Fidelity fallout fixed in the owning layers: readEntityFieldAddress reads the bag first (colliders were absent-shadowed by its own guard) and never serves system addresses from the bag; blob history refs read the bag shape-aware; migration transforms now receive ONE normalized view (engine fields + nested bag) regardless of stored era, and stray flat-habit keys refuse with the fix in the message. - THE REOPEN-COLLIDER CONFORMANCE CASE (required before any RC counts as gates-green): all ten collider names + plumbing names written as user fields, verified verbatim + queryable across live reads, flush+reopen, a forced epoch rebuild, and asOf time travel; relation mirror; forgery refusals; legacy flat-record compat. 8/8 green. Gates: unit 1901/1901 (exit 0) · integration 758 (exit 0) · conformance 27/27 (exit 0) · consumer test sweep migrated (10 files).
This commit is contained in:
parent
48a6130a50
commit
24bf6cdbc5
32 changed files with 1355 additions and 1905 deletions
|
|
@ -42,7 +42,8 @@ describe('find({ where, orderBy }) bounds the sort to the page (CTX-BR-FIND-ORDE
|
|||
return real(f, ob, o, topK)
|
||||
}
|
||||
|
||||
const results = await brain.find({ where: { bucket: 'x' }, orderBy: 'createdAt', order: 'desc', limit: 5 })
|
||||
// system.createdAt — entity age, not a user metadata field named 'createdAt'.
|
||||
const results = await brain.find({ where: { bucket: 'x' }, orderBy: 'system.createdAt', order: 'desc', limit: 5 })
|
||||
|
||||
expect(results).toHaveLength(5)
|
||||
// Page-bounded: ~ limit (5) + a small hidden-tier over-fetch — NOT all 50 matches.
|
||||
|
|
|
|||
|
|
@ -1,251 +0,0 @@
|
|||
/**
|
||||
* @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<typeof vi.spyOn>
|
||||
|
||||
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<typeof vi.spyOn>
|
||||
|
||||
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<string, unknown>)?.createdBy).toBeUndefined()
|
||||
expect((entity?.metadata as Record<string, unknown>)?.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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
/**
|
||||
* @module tests/unit/brainy/update-reserved-metadata-remap
|
||||
* @description Regression tests for the reserved-field metadata-bag trap,
|
||||
* ported from the 7.x fix and extended to the full 8.0 contract.
|
||||
*
|
||||
* History: `add({metadata: {confidence}})` lifted reserved fields to their
|
||||
* canonical top-level location, but `update({metadata: {confidence}})`
|
||||
* silently dropped the same shape — the patch value survived the merge and
|
||||
* was then clobbered by the preserve-existing spread. A production
|
||||
* consumer's confidence-evolution writes no-oped for weeks before being
|
||||
* caught by reading values back.
|
||||
*
|
||||
* These tests pin the LEGACY REMAP behavior, which in 8.0 is opt-in via
|
||||
* `reservedFieldPolicy: 'remap'` (the default is `'throw'` — see the policy
|
||||
* matrix in tests/unit/brainy/reserved-field-policy.test.ts). The brain in
|
||||
* every test below is constructed with `reservedFieldPolicy: 'remap'` so these
|
||||
* deep correctness assertions about the remap path stay exercised.
|
||||
*
|
||||
* Remap contract under test (every write path, entities AND relationships):
|
||||
* - user-mutable reserved fields (`confidence`, `weight`, `subtype` — plus
|
||||
* `service`/`createdBy` at add()/relate() time) remap from the metadata
|
||||
* bag to their dedicated top-level param, with top-level winning when both
|
||||
* are present;
|
||||
* - system-managed reserved fields (`createdAt`, `_rev`, `noun`/`verb`,
|
||||
* `data`, …) are dropped from the bag;
|
||||
* - the same normalization applies to `transact()` operations and `with()`
|
||||
* speculative views;
|
||||
* - reads NEVER echo a reserved field inside `metadata`.
|
||||
*
|
||||
* TypeScript callers can't write these shapes at all (compile-time guard on
|
||||
* the metadata param types — see tests/unit/types/reserved-metadata-keys.test-d.ts);
|
||||
* these tests simulate untyped (JavaScript) callers, hence the `as object`
|
||||
* widenings on the metadata literals.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { Brainy } from '../../../src/index.js'
|
||||
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
|
||||
import { createTestConfig } from '../../helpers/test-factory.js'
|
||||
|
||||
describe('reserved-field metadata remap (8.0 legacy remap path)', () => {
|
||||
let brain: Brainy
|
||||
|
||||
beforeEach(async () => {
|
||||
// The remap path is opt-in in 8.0 (default policy is 'throw').
|
||||
brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
describe('update() — the ported 7.x regression', () => {
|
||||
it('remaps metadata.confidence to the top-level field (the production repro)', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'x',
|
||||
metadata: { confidence: 0.8 } as object
|
||||
})
|
||||
|
||||
// Top-level write works (always did)
|
||||
await brain.update({ id, confidence: 0.42 })
|
||||
let entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.42)
|
||||
|
||||
// Metadata-patch write — silently dropped pre-fix, remapped now
|
||||
await brain.update({ id, metadata: { confidence: 0.33 } as object })
|
||||
entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.33)
|
||||
// The reserved key must not linger inside the metadata bag
|
||||
expect((entity?.metadata as Record<string, unknown>)?.confidence).toBeUndefined()
|
||||
})
|
||||
|
||||
it('remaps metadata.weight and metadata.subtype the same way', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'y',
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
await brain.update({ id, metadata: { weight: 0.7, subtype: 'specialized' } as object })
|
||||
const entity = await brain.get(id)
|
||||
expect(entity?.weight).toBe(0.7)
|
||||
expect(entity?.subtype).toBe('specialized')
|
||||
expect((entity?.metadata as Record<string, unknown>)?.weight).toBeUndefined()
|
||||
expect((entity?.metadata as Record<string, unknown>)?.subtype).toBeUndefined()
|
||||
})
|
||||
|
||||
it('top-level param wins when both top-level and metadata-patch carry the field', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'z',
|
||||
metadata: { confidence: 0.5 } as object
|
||||
})
|
||||
|
||||
await brain.update({ id, confidence: 0.9, metadata: { confidence: 0.1 } as object })
|
||||
const entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.9)
|
||||
})
|
||||
|
||||
it('drops system-managed fields from patches without corrupting the entity', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'w',
|
||||
metadata: { keep: 'me' }
|
||||
})
|
||||
const before = await brain.get(id)
|
||||
|
||||
await brain.update({
|
||||
id,
|
||||
metadata: { createdAt: 1, _rev: 999, noun: 'organization', other: 'applied' } as object
|
||||
})
|
||||
const after = await brain.get(id)
|
||||
|
||||
expect(after?.createdAt).toBe(before?.createdAt) // immutable
|
||||
expect(after?.type).toBe('concept') // noun patch ignored
|
||||
expect(after?._rev).toBe((before?._rev ?? 1) + 1) // _rev patch ignored; normal bump applied
|
||||
expect((after?.metadata as Record<string, unknown>)?.other).toBe('applied') // custom fields still merge
|
||||
expect((after?.metadata as Record<string, unknown>)?.keep).toBe('me')
|
||||
expect((after?.metadata as Record<string, unknown>)?._rev).toBeUndefined()
|
||||
expect((after?.metadata as Record<string, unknown>)?.createdAt).toBeUndefined()
|
||||
expect((after?.metadata as Record<string, unknown>)?.noun).toBeUndefined()
|
||||
})
|
||||
|
||||
it('custom (non-reserved) metadata patches are unaffected by the remap', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'v',
|
||||
metadata: { status: 'draft' }
|
||||
})
|
||||
|
||||
await brain.update({ id, metadata: { status: 'reviewed', rating: 4.5 } })
|
||||
const entity = await brain.get(id)
|
||||
expect((entity?.metadata as Record<string, unknown>)?.status).toBe('reviewed')
|
||||
expect((entity?.metadata as Record<string, unknown>)?.rating).toBe(4.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('add() — explicit lift, identical contract', () => {
|
||||
it('lifts confidence/weight/subtype out of the bag to top level', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Person,
|
||||
data: 'lift check',
|
||||
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' })
|
||||
})
|
||||
|
||||
it('lifts service (settable at add time) and lets the top-level param win', async () => {
|
||||
const lifted = await brain.add({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'service lift',
|
||||
metadata: { service: 'orders' } as object
|
||||
})
|
||||
expect((await brain.get(lifted))?.service).toBe('orders')
|
||||
|
||||
const topLevelWins = await brain.add({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'service precedence',
|
||||
service: 'billing',
|
||||
metadata: { service: 'orders' } as object
|
||||
})
|
||||
const entity = await brain.get(topLevelWins)
|
||||
expect(entity?.service).toBe('billing')
|
||||
expect((entity?.metadata as Record<string, unknown>)?.service).toBeUndefined()
|
||||
})
|
||||
|
||||
it('a remapped subtype satisfies subtype enforcement like a top-level one', async () => {
|
||||
brain.requireSubtype(NounType.Document)
|
||||
|
||||
// Top-level missing, but the bag carries it — must not throw.
|
||||
const id = await brain.add({
|
||||
type: NounType.Document,
|
||||
data: 'enforcement via remap',
|
||||
metadata: { subtype: 'invoice' } as object
|
||||
})
|
||||
expect((await brain.get(id))?.subtype).toBe('invoice')
|
||||
|
||||
// Neither place carries it — must throw.
|
||||
await expect(
|
||||
brain.add({ type: NounType.Document, data: 'no subtype anywhere' })
|
||||
).rejects.toThrow(/subtype/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('transact() — same remap on add and update ops', () => {
|
||||
it('normalizes reserved fields in transact add + update ops', async () => {
|
||||
const db1 = await brain.transact([
|
||||
{
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'tx',
|
||||
metadata: { confidence: 0.7, custom: 'a' } as object
|
||||
}
|
||||
])
|
||||
const id = db1.receipt!.ids[0]
|
||||
|
||||
let entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.7)
|
||||
expect(entity?.metadata).toEqual({ custom: 'a' })
|
||||
|
||||
await brain.transact([
|
||||
{ op: 'update', id, metadata: { confidence: 0.25, custom: 'b' } as object }
|
||||
])
|
||||
entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.25)
|
||||
expect(entity?.metadata).toEqual({ custom: 'b' })
|
||||
expect((entity?.metadata as Record<string, unknown>)?.confidence).toBeUndefined()
|
||||
})
|
||||
|
||||
it('historical asOf() reads surface reserved fields ONLY top-level', async () => {
|
||||
const db1 = await brain.transact([
|
||||
{
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'historical',
|
||||
metadata: { confidence: 0.9, custom: 'past' } as object
|
||||
}
|
||||
])
|
||||
const id = db1.receipt!.ids[0]
|
||||
|
||||
// Move the world forward so generation db1 is historical.
|
||||
await brain.transact([{ op: 'update', id, confidence: 0.1, metadata: { custom: 'now' } }])
|
||||
|
||||
const past = await brain.asOf(db1.generation)
|
||||
const historical = await past.get(id)
|
||||
expect(historical?.confidence).toBe(0.9)
|
||||
expect(historical?.metadata).toEqual({ custom: 'past' })
|
||||
await past.release()
|
||||
})
|
||||
|
||||
it('with() speculative views apply the same normalization', async () => {
|
||||
const base = await brain.now()
|
||||
const speculative = await base.with([
|
||||
{
|
||||
op: 'add',
|
||||
id: 'spec-entity',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'spec',
|
||||
metadata: { confidence: 0.65, custom: 'spec' } as object
|
||||
}
|
||||
])
|
||||
|
||||
const entity = await speculative.get('spec-entity')
|
||||
expect(entity?.confidence).toBe(0.65)
|
||||
// 8.0 id normalization: a natural-key id is mapped to a stable UUID and
|
||||
// the caller's original string is preserved under _originalId — surfaced
|
||||
// here exactly as the durable transact()/add() paths do.
|
||||
expect(entity?.metadata).toEqual({ custom: 'spec', _originalId: 'spec-entity' })
|
||||
await speculative.release()
|
||||
await base.release()
|
||||
})
|
||||
})
|
||||
|
||||
describe('read paths never echo reserved fields inside metadata', () => {
|
||||
it('find() (storage pagination path) returns custom-only metadata with reserved fields top-level', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'pagination echo check',
|
||||
confidence: 0.8,
|
||||
weight: 0.6,
|
||||
metadata: { dept: 'eng' }
|
||||
})
|
||||
|
||||
// No query/filter → served by the direct storage pagination path
|
||||
// (getNounsWithPagination), which historically echoed the full flat
|
||||
// record (noun/subtype/createdAt/… inside metadata).
|
||||
const results = await brain.find({ limit: 50 })
|
||||
const result = results.find((r) => r.id === id)
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.entity.metadata).toEqual({ dept: 'eng' })
|
||||
expect(result?.entity.type).toBe(NounType.Person)
|
||||
expect(result?.entity.subtype).toBe('employee')
|
||||
expect(result?.entity.confidence).toBe(0.8)
|
||||
expect(result?.entity.weight).toBe(0.6)
|
||||
expect(typeof result?.entity.createdAt).toBe('number')
|
||||
expect(result?.entity._rev).toBe(1)
|
||||
})
|
||||
|
||||
it('related() by target surfaces reserved fields top-level, custom-only metadata', async () => {
|
||||
const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'src' })
|
||||
const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'tgt' })
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
confidence: 0.9,
|
||||
weight: 0.5,
|
||||
service: 'orders',
|
||||
metadata: { note: 'target path' }
|
||||
})
|
||||
|
||||
const relations = await brain.related({ to: b })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel).toBeDefined()
|
||||
expect(rel?.metadata).toEqual({ note: 'target path' })
|
||||
expect(rel?.subtype).toBe('direct')
|
||||
expect(rel?.confidence).toBe(0.9)
|
||||
expect(rel?.weight).toBe(0.5)
|
||||
expect(rel?.service).toBe('orders')
|
||||
expect(typeof rel?.createdAt).toBe('number')
|
||||
})
|
||||
})
|
||||
|
||||
describe('relationships — relate() / updateRelation() mirror', () => {
|
||||
let a: string
|
||||
let b: string
|
||||
|
||||
beforeEach(async () => {
|
||||
a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
|
||||
b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
|
||||
})
|
||||
|
||||
it('relate() persists the top-level confidence and service params', async () => {
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
confidence: 0.77,
|
||||
service: 'orders'
|
||||
})
|
||||
|
||||
const relations = await brain.related({ from: a })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel?.confidence).toBe(0.77)
|
||||
expect(rel?.service).toBe('orders')
|
||||
})
|
||||
|
||||
it('relate() remaps reserved fields out of the metadata bag', async () => {
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.RelatedTo,
|
||||
subtype: 'colleague',
|
||||
metadata: { confidence: 0.4, weight: 0.3, role: 'peer' } as object
|
||||
})
|
||||
|
||||
const relations = await brain.related({ from: a })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel?.confidence).toBe(0.4)
|
||||
expect(rel?.weight).toBe(0.3)
|
||||
expect(rel?.metadata).toEqual({ role: 'peer' })
|
||||
})
|
||||
|
||||
it('relation.metadata never echoes the verb type key', async () => {
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.RelatedTo,
|
||||
subtype: 'colleague',
|
||||
metadata: { note: 'no echo' }
|
||||
})
|
||||
|
||||
const relations = await brain.related({ from: a })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel?.type).toBe(VerbType.RelatedTo)
|
||||
expect((rel?.metadata as Record<string, unknown>)?.verb).toBeUndefined()
|
||||
expect(rel?.metadata).toEqual({ note: 'no echo' })
|
||||
})
|
||||
|
||||
it('updateRelation() remaps the user-mutable trio and preserves service', async () => {
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
service: 'orders',
|
||||
metadata: { keep: 'me' }
|
||||
})
|
||||
|
||||
await brain.updateRelation({
|
||||
id: relId,
|
||||
metadata: { confidence: 0.55, subtype: 'dotted-line', extra: 'applied' } as object
|
||||
})
|
||||
|
||||
const relations = await brain.related({ from: a })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel?.confidence).toBe(0.55)
|
||||
expect(rel?.subtype).toBe('dotted-line')
|
||||
expect(rel?.service).toBe('orders') // fixed at relate() time, never erased by updates
|
||||
expect(rel?.metadata).toEqual({ keep: 'me', extra: 'applied' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -198,60 +198,47 @@ describe('visibility (8.0 reserved field)', () => {
|
|||
expect(entity?.visibility).toBeUndefined()
|
||||
})
|
||||
|
||||
it('an untyped caller passing visibility inside metadata is normalized under reservedFieldPolicy:"remap" (lifted to top-level)', async () => {
|
||||
// Simulate a JavaScript caller smuggling the reserved key past the compile-time guard.
|
||||
// The legacy remap behavior is now opt-in (8.0 default is 'throw').
|
||||
const remapBrain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
|
||||
await remapBrain.init()
|
||||
try {
|
||||
const id = await remapBrain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'y',
|
||||
metadata: { visibility: 'internal', tag: 't' } as object
|
||||
})
|
||||
const entity = await remapBrain.get(id)
|
||||
// Lifted to the top-level field…
|
||||
expect(entity?.visibility).toBe('internal')
|
||||
// …and stripped from the metadata bag.
|
||||
expect((entity?.metadata as Record<string, unknown>)?.visibility).toBeUndefined()
|
||||
expect((entity?.metadata as Record<string, unknown>)?.tag).toBe('t')
|
||||
// It is excluded from the default count, exactly like a top-level internal write.
|
||||
expect(await remapBrain.getNounCount()).toBe(0)
|
||||
} finally {
|
||||
await remapBrain.close()
|
||||
}
|
||||
it('metadata.visibility is the USER’s field (field-addressing law) — stored verbatim, never lifted to the engine tier', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'y',
|
||||
metadata: { visibility: 'internal', tag: 't' } as object
|
||||
})
|
||||
const entity = await brain.get(id)
|
||||
// The user's field lives in the bag, verbatim…
|
||||
expect((entity?.metadata as Record<string, unknown>)?.visibility).toBe('internal')
|
||||
expect((entity?.metadata as Record<string, unknown>)?.tag).toBe('t')
|
||||
// …and the ENGINE tier is untouched: absent === public, so the entity
|
||||
// stays visible on default reads (the engine tier is set only via the
|
||||
// dedicated visibility param and reads at system.visibility).
|
||||
expect(entity?.visibility).toBeUndefined()
|
||||
const visible = await brain.find({ type: NounType.Concept, limit: 20 })
|
||||
expect(visible.map((r) => r.id)).toContain(id)
|
||||
})
|
||||
|
||||
it('a "system" value smuggled through metadata is dropped under reservedFieldPolicy:"remap", not honored', async () => {
|
||||
// 'system' is Brainy-only; an untyped caller must not be able to set it.
|
||||
const remapBrain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
|
||||
await remapBrain.init()
|
||||
try {
|
||||
const id = await remapBrain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'z',
|
||||
metadata: { visibility: 'system' } as object
|
||||
})
|
||||
const entity = await remapBrain.get(id)
|
||||
// The smuggled 'system' was dropped → entity stays public (counted, visible).
|
||||
expect(entity?.visibility).toBeUndefined()
|
||||
expect(await remapBrain.getNounCount()).toBe(1)
|
||||
const found = await remapBrain.find({ type: NounType.Concept, limit: 10 })
|
||||
expect(found.map((r) => r.id)).toContain(id)
|
||||
} finally {
|
||||
await remapBrain.close()
|
||||
}
|
||||
it('a user field valued "system" cannot smuggle the Brainy-only tier — it is just user data', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'z',
|
||||
metadata: { visibility: 'system' } as object
|
||||
})
|
||||
const entity = await brain.get(id)
|
||||
// Engine tier unaffected → entity stays public (counted, visible);
|
||||
// the string 'system' is ordinary user data in the bag.
|
||||
expect(entity?.visibility).toBeUndefined()
|
||||
expect((entity?.metadata as Record<string, unknown>)?.visibility).toBe('system')
|
||||
const found = await brain.find({ type: NounType.Concept, limit: 10 })
|
||||
expect(found.map((r) => r.id)).toContain(id)
|
||||
})
|
||||
|
||||
it('an untyped caller passing visibility inside metadata throws under the default policy', async () => {
|
||||
// 8.0 default: no silent remap — a reserved key in the bag is a loud error.
|
||||
it('a forged system.visibility key in metadata refuses loudly at the write door', async () => {
|
||||
await expect(
|
||||
brain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'throws',
|
||||
metadata: { visibility: 'internal', tag: 't' } as object
|
||||
metadata: { 'system.visibility': 'internal' } as object
|
||||
})
|
||||
).rejects.toThrow(/visibility.*reserved field/)
|
||||
).rejects.toThrow(/system\./)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function entity(overrides: Partial<Entity> = {}): Entity {
|
|||
}
|
||||
|
||||
describe('db/whereMatcher — resolveEntityField', () => {
|
||||
it('resolves standard top-level fields', () => {
|
||||
it('system.<field> resolves the entity scalar; bare/metadata. reads the metadata bag only (sealed 2026-08-03)', () => {
|
||||
const e = entity({
|
||||
subtype: 'invoice',
|
||||
service: 'billing',
|
||||
|
|
@ -41,17 +41,32 @@ describe('db/whereMatcher — resolveEntityField', () => {
|
|||
_rev: 3,
|
||||
data: 'payload'
|
||||
})
|
||||
expect(resolveEntityField(e, 'id')).toBe('e-1')
|
||||
expect(resolveEntityField(e, 'type')).toBe(NounType.Document)
|
||||
expect(resolveEntityField(e, 'noun')).toBe(NounType.Document) // alias
|
||||
expect(resolveEntityField(e, 'subtype')).toBe('invoice')
|
||||
expect(resolveEntityField(e, 'service')).toBe('billing')
|
||||
expect(resolveEntityField(e, 'confidence')).toBe(0.9)
|
||||
expect(resolveEntityField(e, 'weight')).toBe(0.5)
|
||||
expect(resolveEntityField(e, '_rev')).toBe(3)
|
||||
expect(resolveEntityField(e, 'createdAt')).toBe(1000)
|
||||
expect(resolveEntityField(e, 'updatedAt')).toBe(2000)
|
||||
expect(resolveEntityField(e, 'data')).toBe('payload')
|
||||
|
||||
// system.<field> is the ONLY spelling that reaches an entity scalar.
|
||||
expect(resolveEntityField(e, 'system.id')).toBe('e-1')
|
||||
expect(resolveEntityField(e, 'system.type')).toBe(NounType.Document)
|
||||
expect(resolveEntityField(e, 'system.subtype')).toBe('invoice')
|
||||
expect(resolveEntityField(e, 'system.service')).toBe('billing')
|
||||
expect(resolveEntityField(e, 'system.confidence')).toBe(0.9)
|
||||
expect(resolveEntityField(e, 'system.weight')).toBe(0.5)
|
||||
expect(resolveEntityField(e, 'system.createdAt')).toBe(1000)
|
||||
expect(resolveEntityField(e, 'system.updatedAt')).toBe(2000)
|
||||
|
||||
// Plumbing (_rev, data) is invisible even via system. — not in the
|
||||
// ten-scalar map, so this internal resolver reads it as absent (the typed
|
||||
// refusal for these lives one layer up, at the query-surface parser).
|
||||
expect(resolveEntityField(e, 'system._rev')).toBeUndefined()
|
||||
expect(resolveEntityField(e, 'system.data')).toBeUndefined()
|
||||
|
||||
// Bare names are ALWAYS the user's metadata field — even when they share
|
||||
// a spelling with an engine scalar, or with the now-dead 'noun' alias.
|
||||
// This entity's metadata bag is empty, so every bare name below reads
|
||||
// absent rather than silently falling back to the entity scalar.
|
||||
expect(resolveEntityField(e, 'id')).toBeUndefined()
|
||||
expect(resolveEntityField(e, 'type')).toBeUndefined()
|
||||
expect(resolveEntityField(e, 'noun')).toBeUndefined() // legacy alias is dead
|
||||
expect(resolveEntityField(e, 'subtype')).toBeUndefined()
|
||||
expect(resolveEntityField(e, 'createdAt')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('resolves custom fields from the metadata bag', () => {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ function allTestFiles(dir: string, out: string[] = []): string[] {
|
|||
* conscious decision — a NEW orphan not listed here fails the guard below.
|
||||
*/
|
||||
const MANUAL_ONLY = new Set<string>([
|
||||
// Conformance suites run as an explicit gate stage (both engines run them
|
||||
// by direct invocation), never swept into the unit/integration configs.
|
||||
'tests/conformance/collider-fidelity.test.ts',
|
||||
'tests/api/performance-benchmarks.test.ts',
|
||||
'tests/critical-neural-validation.test.ts',
|
||||
'tests/critical-performance-benchmark.test.ts',
|
||||
|
|
@ -38,7 +41,15 @@ const MANUAL_ONLY = new Set<string>([
|
|||
'tests/package-size-limit.test.ts',
|
||||
'tests/performance/graph-scale-performance.test.ts',
|
||||
'tests/performance/triple-intelligence-scale.test.ts',
|
||||
'tests/performance/typeAware.bench.test.ts'
|
||||
'tests/performance/typeAware.bench.test.ts',
|
||||
// Cross-engine field-addressing conformance suite: pinned bit-for-bit against
|
||||
// the native accelerator's implementation of the SAME contract, and invoked
|
||||
// directly (`npx vitest run tests/conformance/namespace-law.test.ts`), never
|
||||
// swept into the unit/integration gates — a run against a branch where the
|
||||
// resolver hasn't landed yet must SKIP loudly (see the file's own SELF-SKIP
|
||||
// doc), not silently pass/fail as a side effect of which gate happened to
|
||||
// pick it up.
|
||||
'tests/conformance/namespace-law.test.ts'
|
||||
])
|
||||
|
||||
function inGate(rel: string): boolean {
|
||||
|
|
|
|||
127
tests/unit/types/nestedBagRecord.test.ts
Normal file
127
tests/unit/types/nestedBagRecord.test.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* @module tests/unit/types/nestedBagRecord
|
||||
* @description Unit pins for the v2 (nested-bag) stored-record layer — the
|
||||
* storage half of the field-addressing law. The write door accepts ANY user
|
||||
* metadata name; what makes that lossless on disk is the record shape:
|
||||
* engine fields top-level, the user bag NESTED verbatim, discriminated by
|
||||
* the engine-written format stamp (never by names — names are the user's).
|
||||
* These pins hold the builders, the discriminator, and the shape-aware
|
||||
* split that every read path (live, batch, historical) routes through.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
buildNounMetadataRecord,
|
||||
buildVerbMetadataRecord,
|
||||
splitNounMetadataRecord,
|
||||
splitVerbMetadataRecord,
|
||||
isNestedBagRecord,
|
||||
METADATA_RECORD_FORMAT_KEY,
|
||||
NESTED_BAG_FORMAT
|
||||
} from '../../../src/types/reservedFields.js'
|
||||
|
||||
const COLLIDER_BAG = {
|
||||
confidence: 'user-confidence',
|
||||
weight: 'user-weight',
|
||||
subtype: 'user-subtype',
|
||||
createdAt: 'user-createdAt',
|
||||
service: 'user-service',
|
||||
data: 'user-data',
|
||||
noun: 'user-noun',
|
||||
_rev: 'user-rev',
|
||||
level: 7,
|
||||
plain: 'control'
|
||||
}
|
||||
|
||||
describe('v2 nested-bag stored records — build / discriminate / split', () => {
|
||||
it('build → split round-trips a fully colliding user bag VERBATIM', () => {
|
||||
const record = buildNounMetadataRecord(
|
||||
{ noun: 'document', confidence: 0.25, createdAt: 111, updatedAt: 222, _rev: 1 },
|
||||
{ ...COLLIDER_BAG }
|
||||
)
|
||||
expect(isNestedBagRecord(record)).toBe(true)
|
||||
expect(record[METADATA_RECORD_FORMAT_KEY]).toBe(NESTED_BAG_FORMAT)
|
||||
|
||||
const { reserved, custom } = splitNounMetadataRecord(record)
|
||||
// The engine half is exactly what the engine wrote…
|
||||
expect(reserved.noun).toBe('document')
|
||||
expect(reserved.confidence).toBe(0.25)
|
||||
expect(reserved._rev).toBe(1)
|
||||
// …and the user bag comes back byte-for-byte, colliders included.
|
||||
expect(custom).toEqual(COLLIDER_BAG)
|
||||
})
|
||||
|
||||
it('the verb mirror round-trips an edge collider bag verbatim', () => {
|
||||
const record = buildVerbMetadataRecord(
|
||||
{ verb: 'relatedTo', weight: 1.0, confidence: 0.5, createdAt: 333 },
|
||||
{ verb: 'user-verb', confidence: 'user-c', tag: 't' }
|
||||
)
|
||||
expect(isNestedBagRecord(record)).toBe(true)
|
||||
const { reserved, custom } = splitVerbMetadataRecord(record)
|
||||
expect(reserved.verb).toBe('relatedTo')
|
||||
expect(reserved.confidence).toBe(0.5)
|
||||
expect(custom).toEqual({ verb: 'user-verb', confidence: 'user-c', tag: 't' })
|
||||
})
|
||||
|
||||
it('a LEGACY flat record (no stamp) splits BY NAME — sound because the pre-law door refused colliders', () => {
|
||||
const legacy = {
|
||||
noun: 'document',
|
||||
confidence: 0.75,
|
||||
createdAt: 111,
|
||||
_rev: 2,
|
||||
legacyField: 'legacy-value'
|
||||
}
|
||||
expect(isNestedBagRecord(legacy)).toBe(false)
|
||||
const { reserved, custom } = splitNounMetadataRecord(legacy)
|
||||
expect(reserved.confidence).toBe(0.75)
|
||||
expect(reserved._rev).toBe(2)
|
||||
expect(custom).toEqual({ legacyField: 'legacy-value' })
|
||||
})
|
||||
|
||||
it('the stamp is the discriminator, never the name: a legacy user OBJECT field named `metadata` does not fake a v2 record', () => {
|
||||
// Pre-law, 'metadata' was never a reserved name — a flat record could
|
||||
// legally carry a user object field spelled exactly 'metadata'. Without
|
||||
// the engine-written stamp it must split as legacy, with that object
|
||||
// preserved as an ordinary user field.
|
||||
const legacyWithMetadataField = {
|
||||
noun: 'document',
|
||||
confidence: 0.5,
|
||||
metadata: { nested: 'user-object' }
|
||||
}
|
||||
expect(isNestedBagRecord(legacyWithMetadataField)).toBe(false)
|
||||
const { reserved, custom } = splitNounMetadataRecord(legacyWithMetadataField)
|
||||
expect(reserved.confidence).toBe(0.5)
|
||||
expect(custom).toEqual({ metadata: { nested: 'user-object' } })
|
||||
})
|
||||
|
||||
it('a malformed stamp (right key, wrong value / non-object bag) never discriminates as v2', () => {
|
||||
expect(
|
||||
isNestedBagRecord({ [METADATA_RECORD_FORMAT_KEY]: 999, metadata: {} })
|
||||
).toBe(false)
|
||||
expect(
|
||||
isNestedBagRecord({ [METADATA_RECORD_FORMAT_KEY]: NESTED_BAG_FORMAT, metadata: 'not-a-bag' })
|
||||
).toBe(false)
|
||||
expect(
|
||||
isNestedBagRecord({ [METADATA_RECORD_FORMAT_KEY]: NESTED_BAG_FORMAT, metadata: [1, 2] })
|
||||
).toBe(false)
|
||||
expect(isNestedBagRecord(null)).toBe(false)
|
||||
expect(isNestedBagRecord(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('the v2 split never surfaces the stamp or the bag container as fields', () => {
|
||||
const record = buildNounMetadataRecord({ noun: 'document', _rev: 1 }, { a: 1 })
|
||||
const { reserved, custom } = splitNounMetadataRecord(record)
|
||||
expect(METADATA_RECORD_FORMAT_KEY in reserved).toBe(false)
|
||||
expect(METADATA_RECORD_FORMAT_KEY in custom).toBe(false)
|
||||
expect('metadata' in reserved).toBe(false)
|
||||
expect(custom).toEqual({ a: 1 })
|
||||
})
|
||||
|
||||
it('builders copy the bag (no aliasing): later caller mutation cannot reach the record', () => {
|
||||
const bag: Record<string, unknown> = { a: 1 }
|
||||
const record = buildNounMetadataRecord({ noun: 'document' }, bag)
|
||||
bag.a = 999
|
||||
bag.b = 'sneaky'
|
||||
expect((record.metadata as Record<string, unknown>).a).toBe(1)
|
||||
expect('b' in (record.metadata as Record<string, unknown>)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
/**
|
||||
* @module tests/unit/types/reserved-metadata-keys.test-d
|
||||
* @description Compile-time tests for the reserved-field contract (layer 1 of
|
||||
* three — see src/types/reservedFields.ts): a literal reserved key inside any
|
||||
* `metadata` param is a TypeScript error, while the generic `T` ergonomics
|
||||
* stay intact (typed bags, untyped brains, index-signature shapes, and the
|
||||
* documented exemption for consumers who explicitly declare a reserved key in
|
||||
* their own metadata type).
|
||||
*
|
||||
* Runs under vitest typecheck mode (`test.typecheck` in
|
||||
* tests/configs/vitest.unit.config.ts) — these assertions are validated by
|
||||
* `tsc`, never executed. The runtime half of the contract (the write-path
|
||||
* remap for untyped callers) is pinned by
|
||||
* tests/unit/brainy/update-reserved-metadata-remap.test.ts.
|
||||
*/
|
||||
|
||||
import { describe, it, assertType } from 'vitest'
|
||||
import type {
|
||||
AddParams,
|
||||
UpdateParams,
|
||||
RelateParams,
|
||||
UpdateRelationParams,
|
||||
TxOperation
|
||||
} from '../../../src/index.js'
|
||||
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
|
||||
|
||||
describe('reserved entity keys in metadata are compile errors', () => {
|
||||
it('AddParams (untyped brain) rejects every reserved key but stays open for custom fields', () => {
|
||||
// Custom fields of any shape remain legal — exactly the pre-8.0 latitude.
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
metadata: { dept: 'eng', level: 3, tags: ['a', 'b'], nested: { ok: true } }
|
||||
})
|
||||
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'noun' is reserved (the entity type travels via the top-level 'type' param)
|
||||
metadata: { noun: 'organization' }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'subtype' is reserved (use the top-level 'subtype' param)
|
||||
metadata: { subtype: 'contractor' }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'createdAt' is reserved (system-managed)
|
||||
metadata: { createdAt: Date.now() }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'updatedAt' is reserved (system-managed)
|
||||
metadata: { updatedAt: Date.now() }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'confidence' is reserved (use the top-level 'confidence' param)
|
||||
metadata: { confidence: 0.8 }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'weight' is reserved (use the top-level 'weight' param)
|
||||
metadata: { weight: 0.5 }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'service' is reserved (use the top-level 'service' param)
|
||||
metadata: { service: 'orders' }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'data' is reserved (use the top-level 'data' param)
|
||||
metadata: { data: 'content' }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'createdBy' is reserved (use the top-level 'createdBy' param)
|
||||
metadata: { createdBy: { augmentation: 'importer', version: '1.0' } }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — '_rev' is reserved (system-managed revision counter)
|
||||
metadata: { _rev: 7 }
|
||||
})
|
||||
})
|
||||
|
||||
it('AddParams<T> (typed brain) rejects reserved keys alongside the declared shape', () => {
|
||||
interface EmployeeMeta {
|
||||
dept: string
|
||||
level: number
|
||||
}
|
||||
|
||||
assertType<AddParams<EmployeeMeta>>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
metadata: { dept: 'eng', level: 3 }
|
||||
})
|
||||
|
||||
assertType<AddParams<EmployeeMeta>>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'confidence' is reserved even when T declares other fields
|
||||
metadata: { dept: 'eng', level: 3, confidence: 0.8 }
|
||||
})
|
||||
})
|
||||
|
||||
it('documented exemptions: T-declared reserved keys and index-signature shapes stay assignable', () => {
|
||||
// A consumer who *explicitly* types a reserved key into their metadata
|
||||
// shape keeps a working (if unwise) type — the guard exempts keyof T.
|
||||
interface LegacyMeta {
|
||||
confidence: number
|
||||
note: string
|
||||
}
|
||||
assertType<AddParams<LegacyMeta>>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
metadata: { confidence: 0.8, note: 'declared by the consumer type' }
|
||||
})
|
||||
|
||||
// Index-signature metadata types (keyof T = string) remain fully open.
|
||||
assertType<AddParams<Record<string, unknown>>>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
metadata: { anything: 'goes', confidence: 0.8 }
|
||||
})
|
||||
})
|
||||
|
||||
it('UpdateParams patch rejects reserved keys but accepts partial custom patches', () => {
|
||||
interface EmployeeMeta {
|
||||
dept: string
|
||||
level: number
|
||||
}
|
||||
|
||||
// Partial patch of the declared shape is legal.
|
||||
assertType<UpdateParams<EmployeeMeta>>({ id: 'e1', metadata: { dept: 'sales' } })
|
||||
// Untyped patch with custom fields is legal.
|
||||
assertType<UpdateParams>({ id: 'e1', metadata: { status: 'reviewed', rating: 4.5 } })
|
||||
|
||||
// @ts-expect-error — 'confidence' is reserved (use the top-level 'confidence' param)
|
||||
assertType<UpdateParams>({ id: 'e1', metadata: { confidence: 0.33 } })
|
||||
// @ts-expect-error — 'subtype' is reserved (use the top-level 'subtype' param)
|
||||
assertType<UpdateParams>({ id: 'e1', metadata: { subtype: 'specialized' } })
|
||||
// @ts-expect-error — '_rev' is reserved (pass 'ifRev' for optimistic concurrency)
|
||||
assertType<UpdateParams>({ id: 'e1', metadata: { _rev: 3 } })
|
||||
// @ts-expect-error — 'confidence' is reserved even when T declares other fields
|
||||
assertType<UpdateParams<EmployeeMeta>>({ id: 'e1', metadata: { confidence: 0.1 } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('reserved relationship keys in metadata are compile errors', () => {
|
||||
it('RelateParams rejects reserved keys but stays open for custom edge fields', () => {
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
metadata: { role: 'peer', since: 2024 }
|
||||
})
|
||||
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
// @ts-expect-error — 'verb' is reserved (the relationship type travels via the top-level 'type' param)
|
||||
metadata: { verb: 'relatedTo' }
|
||||
})
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
// @ts-expect-error — 'confidence' is reserved (use the top-level 'confidence' param)
|
||||
metadata: { confidence: 0.9 }
|
||||
})
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
// @ts-expect-error — 'weight' is reserved (use the top-level 'weight' param)
|
||||
metadata: { weight: 0.4 }
|
||||
})
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
// @ts-expect-error — 'service' is reserved (use the top-level 'service' param)
|
||||
metadata: { service: 'orders' }
|
||||
})
|
||||
})
|
||||
|
||||
it('UpdateRelationParams patch rejects reserved keys', () => {
|
||||
assertType<UpdateRelationParams>({ id: 'r1', metadata: { note: 'fine' } })
|
||||
|
||||
// @ts-expect-error — 'confidence' is reserved (use the top-level 'confidence' param)
|
||||
assertType<UpdateRelationParams>({ id: 'r1', metadata: { confidence: 0.5 } })
|
||||
// @ts-expect-error — 'subtype' is reserved (use the top-level 'subtype' param)
|
||||
assertType<UpdateRelationParams>({ id: 'r1', metadata: { subtype: 'dotted-line' } })
|
||||
// @ts-expect-error — 'createdAt' is reserved (system-managed)
|
||||
assertType<UpdateRelationParams>({ id: 'r1', metadata: { createdAt: 1 } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('transact() operations inherit the same guard', () => {
|
||||
it('TxOperation add/update/relate metadata rejects reserved keys', () => {
|
||||
assertType<TxOperation>({
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'tx',
|
||||
metadata: { custom: 'a' }
|
||||
})
|
||||
assertType<TxOperation>({
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'tx',
|
||||
// @ts-expect-error — 'confidence' is reserved on transact add ops too
|
||||
metadata: { confidence: 0.7 }
|
||||
})
|
||||
assertType<TxOperation>({
|
||||
op: 'update',
|
||||
id: 'e1',
|
||||
// @ts-expect-error — 'weight' is reserved on transact update ops too
|
||||
metadata: { weight: 0.2 }
|
||||
})
|
||||
assertType<TxOperation>({
|
||||
op: 'relate',
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.RelatedTo,
|
||||
subtype: 'colleague',
|
||||
// @ts-expect-error — 'verb' is reserved on transact relate ops too
|
||||
metadata: { verb: 'contains' }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -56,11 +56,15 @@ describe('Zero-Config Parameter Validation', () => {
|
|||
})).toThrow('cannot specify both query and vector')
|
||||
})
|
||||
|
||||
it('should reject both cursor and offset', () => {
|
||||
it('should refuse cursor outright — even paired with offset — as an unimplemented option', () => {
|
||||
// cursor is now a typed, unconditional refusal (UnsupportedFindOptionError):
|
||||
// it used to be accepted-and-ignored, only conflicting when offset was also
|
||||
// given. Accepted-and-ignored died as a class — cursor refuses on its own,
|
||||
// so pairing it with offset refuses too, but with the SAME message.
|
||||
expect(() => validateFindParams({
|
||||
cursor: 'abc123',
|
||||
offset: 10
|
||||
})).toThrow('cannot use both cursor and offset pagination')
|
||||
})).toThrow("find() option 'cursor' is not implemented")
|
||||
})
|
||||
|
||||
it('should validate vector dimensions', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue