brainy/tests/unit/brainy/visibility.test.ts
David Snelling 54c7c39669 feat(8.0): reserved-field enforcement — reservedFieldPolicy defaults to throw
An untyped (JS) caller that smuggles a Brainy-reserved field (confidence,
weight, subtype, visibility, service, createdBy, noun/verb, data, createdAt,
updatedAt, _rev) inside a write-path metadata bag previously got a silent
remap-or-drop — a class of bug where confidence-evolution writes no-oped for
weeks before being caught on read-back. 8.0 closes this with no silent failures.

- New BrainyConfig.reservedFieldPolicy: 'throw' | 'warn' | 'remap' (default 'throw').
  'throw' rejects the write naming every offending key + its correct write path;
  'warn' remaps with a one-shot per-key warning; 'remap' is the legacy silent path.
- Central enforceReservedPolicy gate wired into all four remap methods (add,
  update, relate, updateRelation) so live calls AND their transact()/with()
  mirrors honor it. Single-source reservedWritePath guidance shared by throw+warn.
- 'warn' now warns for EVERY reserved key (closes the gap where only
  system-managed fields warned). Dead warnDropped* helpers removed.
- Import pipeline migrated to route reserved values (confidence/weight/subtype)
  through dedicated params and strip reserved keys from extractor/customMetadata
  bags via the canonical split*MetadataRecord helpers — imports no longer trip
  the default throw.
- Tests: new reservedFieldPolicy matrix (throw/warn/remap across every write
  path + transact); remap-correctness suite reframed as opt-in 'remap'; shared
  test-factory no longer emits reserved keys in custom metadata.
2026-06-20 15:37:21 -07:00

257 lines
11 KiB
TypeScript

/**
* @module tests/unit/brainy/visibility
* @description Tests for the 8.0 reserved `visibility` field — the three-tier
* (`public` | `internal` | `system`) gate that controls whether an entity or
* relationship surfaces on default user-facing reads.
*
* Contract under test:
* - Absent === `'public'`: counted and returned everywhere.
* - `'internal'`: hidden from default `find()` / `related()` / counts / `stats()`,
* but retrievable with `includeInternal: true`.
* - `'system'`: Brainy plumbing (the VFS root); hidden everywhere by default,
* surfaced only with `includeSystem: true`. Not settable through the public
* `add()` / `relate()` params (the param type narrows to `'public' | 'internal'`).
* - `visibility` is a reserved top-level field: surfaced top-level on reads, never
* inside `metadata`, and an untyped caller smuggling it through `metadata` is
* normalized (a `'public'`/`'internal'` value is lifted; `'system'` is dropped).
*
* THE key regression: a fresh brain reports `getNounCount() === 0` even though the
* VFS root entity exists — because the root is `visibility: 'system'` and excluded.
*
* Runs under the deterministic embedder (the unit setup sets `BRAINY_UNIT_TEST`).
*/
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('visibility (8.0 reserved field)', () => {
let brain: Brainy
beforeEach(async () => {
brain = new Brainy(createTestConfig())
await brain.init()
})
afterEach(async () => {
await brain.close()
})
describe('counts exclude system + internal', () => {
it('a fresh brain reports getNounCount() === 0 (the VFS root is system → excluded)', async () => {
// KEY REGRESSION: the VFS root entity exists after init() but is
// visibility:'system', so it must not show up in the user-facing count.
expect(await brain.getNounCount()).toBe(0)
})
it('add() with no visibility is counted (public default)', async () => {
await brain.add({ type: NounType.Concept, data: 'public thing' })
expect(await brain.getNounCount()).toBe(1)
})
it('add({ visibility: "internal" }) is NOT counted', async () => {
await brain.add({ type: NounType.Concept, data: 'public one' })
await brain.add({ type: NounType.Concept, data: 'app-internal', visibility: 'internal' })
// Only the public entity counts.
expect(await brain.getNounCount()).toBe(1)
})
it('flipping visibility via update() moves the entity in/out of the count', async () => {
const id = await brain.add({ type: NounType.Concept, data: 'flip me' })
expect(await brain.getNounCount()).toBe(1)
await brain.update({ id, visibility: 'internal' })
expect(await brain.getNounCount()).toBe(0)
await brain.update({ id, visibility: 'public' })
expect(await brain.getNounCount()).toBe(1)
})
it('stats().entityCount reports only public entities', async () => {
await brain.add({ type: NounType.Concept, data: 'a' })
await brain.add({ type: NounType.Concept, data: 'b', visibility: 'internal' })
const stats = await brain.stats()
expect(stats.entityCount).toBe(1)
})
})
describe('find() default-excludes internal + system', () => {
it('public entities are returned; internal are hidden by default', async () => {
const pubId = await brain.add({ type: NounType.Concept, data: 'visible' })
await brain.add({ type: NounType.Concept, data: 'hidden', visibility: 'internal' })
const def = await brain.find({ type: NounType.Concept, limit: 100 })
const ids = def.map((r) => r.id)
expect(ids).toContain(pubId)
expect(def.length).toBe(1)
})
it('find({ includeInternal: true }) also returns internal entities', async () => {
const pubId = await brain.add({ type: NounType.Concept, data: 'visible' })
const intId = await brain.add({ type: NounType.Concept, data: 'hidden', visibility: 'internal' })
const withInternal = await brain.find({ type: NounType.Concept, includeInternal: true, limit: 100 })
const ids = withInternal.map((r) => r.id)
expect(ids).toContain(pubId)
expect(ids).toContain(intId)
expect(withInternal.length).toBe(2)
})
it('empty-query find() (no filter) also excludes internal by default', async () => {
const pubId = await brain.add({ type: NounType.Concept, data: 'visible' })
await brain.add({ type: NounType.Concept, data: 'hidden', visibility: 'internal' })
const all = await brain.find({ limit: 100 })
const ids = all.map((r) => r.id)
expect(ids).toContain(pubId)
// Only the one public user entity (VFS root is system → excluded too).
expect(all.length).toBe(1)
})
it('limit stays exact when internal entities are interleaved (hard candidate filter)', async () => {
// Two public + two internal; a default find(limit:2) must return exactly the
// two public ones, not get short-changed by the hidden ones.
await brain.add({ type: NounType.Concept, data: 'pub-1' })
await brain.add({ type: NounType.Concept, data: 'int-1', visibility: 'internal' })
await brain.add({ type: NounType.Concept, data: 'pub-2' })
await brain.add({ type: NounType.Concept, data: 'int-2', visibility: 'internal' })
const page = await brain.find({ type: NounType.Concept, limit: 2 })
expect(page.length).toBe(2)
for (const r of page) {
expect(r.visibility === undefined || r.visibility === 'public').toBe(true)
}
})
})
describe('the VFS root (system) entity', () => {
it('never appears in find(), even with includeInternal', async () => {
const rootId = '00000000-0000-0000-0000-000000000000'
// Sanity: the root really exists (get() is an explicit by-id read, not a default surface).
expect(await brain.get(rootId)).not.toBeNull()
const def = await brain.find({ limit: 1000 })
expect(def.map((r) => r.id)).not.toContain(rootId)
const withInternal = await brain.find({ includeInternal: true, limit: 1000 })
expect(withInternal.map((r) => r.id)).not.toContain(rootId)
})
it('appears only with includeSystem: true', async () => {
const rootId = '00000000-0000-0000-0000-000000000000'
const withSystem = await brain.find({ includeSystem: true, limit: 1000 })
expect(withSystem.map((r) => r.id)).toContain(rootId)
})
it('the root carries visibility "system" when surfaced via get()', async () => {
const rootId = '00000000-0000-0000-0000-000000000000'
const root = await brain.get(rootId)
expect(root?.visibility).toBe('system')
})
})
describe('verbs (relationships) — symmetric to nouns', () => {
it('relate({ visibility: "internal" }) is excluded from getVerbCount() + related() by default, included with includeInternal', async () => {
const a = await brain.add({ type: NounType.Person, data: 'a' })
const b = await brain.add({ type: NounType.Person, data: 'b' })
const c = await brain.add({ type: NounType.Person, data: 'c' })
// One public edge, one internal edge from the same source.
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
await brain.relate({ from: a, to: c, type: VerbType.RelatedTo, visibility: 'internal' })
// Counts: only the public edge.
expect(await brain.getVerbCount()).toBe(1)
// related() default: only the public edge.
const def = await brain.related({ from: a })
expect(def.length).toBe(1)
expect(def[0].to).toBe(b)
// related({ includeInternal }): both edges.
const withInternal = await brain.related({ from: a, includeInternal: true })
expect(withInternal.length).toBe(2)
expect(withInternal.map((r) => r.to).sort()).toEqual([b, c].sort())
})
})
describe('visibility is a reserved top-level field', () => {
it('is surfaced top-level on get(), never inside metadata', async () => {
const id = await brain.add({
type: NounType.Concept,
data: 'x',
visibility: 'internal',
metadata: { kind: 'note' }
})
const entity = await brain.get(id)
expect(entity?.visibility).toBe('internal')
// metadata holds ONLY custom fields, never the reserved visibility key.
expect(entity?.metadata).toEqual({ kind: 'note' })
expect((entity?.metadata as Record<string, unknown>)?.visibility).toBeUndefined()
})
it('a public-default entity stores no visibility (absent === public)', async () => {
const id = await brain.add({ type: NounType.Concept, data: 'plain' })
const entity = await brain.get(id)
// Absent — not the literal string 'public'. Kept lean on the common path.
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('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('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.
await expect(
brain.add({
type: NounType.Concept,
data: 'throws',
metadata: { visibility: 'internal', tag: 't' } as object
})
).rejects.toThrow(/visibility.*reserved field/)
})
})
})