/** * @module tests/integration/aggregate-reserved-fields * @description One field-resolution law across the whole aggregation + query * surface (SELF-AGGREGATE-DELETE-DRIFT). Laws: * (1) aggregates grouped by a RESERVED field (subtype) decrement on delete — * the delete-side entity view carries every reserved field, so the * decrement finds its group (counts must never drift from ground truth); * (2) same for update: moving an entity between reserved-field groups * decrements the old group and increments the new one (no double-count); * (3) aggregation source.where on a reserved field (subtype) FILTERS instead * of silently matching nothing; * (4) removeMany refuses empty/invalid selectors loudly (bare array, empty * object, ids: []) instead of resolving as a silent no-op; * (5) find() accepts both where spellings: flattened (entry.title) and * storage-shaped (metadata.entry.title) resolve to the same rows. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Brainy } from '../../src/brainy.js' import { NounType } from '../../src/types/graphTypes.js' const stubEmbedding = async (text: string): Promise => { const hash = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) return new Array(384).fill(0).map((_, i) => Math.sin(hash + i)) } describe('aggregation + query field-resolution law', () => { let brain: Brainy beforeEach(async () => { brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' as const }, embeddingFunction: stubEmbedding }) await brain.init() }) afterEach(async () => { await brain.close() }) it('reserved-field groupBy decrements on delete (the drift bug)', async () => { brain.defineAggregate({ name: 'by_subtype', source: { type: NounType.Document }, groupBy: ['subtype'], metrics: { count: { op: 'count' } } }) const ids: string[] = [] for (let i = 0; i < 5; i++) { ids.push( await brain.add({ data: `doc-${i}`, type: NounType.Document, subtype: 'note', metadata: { team: 'alpha' } }) ) } let groups = await brain.queryAggregate('by_subtype') expect(groups).toHaveLength(1) expect(groups[0].groupKey).toEqual({ subtype: 'note' }) expect(groups[0].metrics.count).toBe(5) await brain.remove(ids[0]) await brain.flush() groups = await brain.queryAggregate('by_subtype') expect(groups[0].metrics.count).toBe(4) const live = await brain.find({ type: NounType.Document, limit: 100 }) expect(groups[0].metrics.count).toBe(live.length) }) it('reserved-field groupBy moves between groups on update (no double-count)', async () => { brain.defineAggregate({ name: 'by_subtype', source: { type: NounType.Document }, groupBy: ['subtype'], metrics: { count: { op: 'count' } } }) const id = await brain.add({ data: 'doc-move', type: NounType.Document, subtype: 'draft' }) await brain.update({ id, subtype: 'published' }) const groups = await brain.queryAggregate('by_subtype') const byKey = Object.fromEntries( groups.map((g) => [String(g.groupKey.subtype), g.metrics.count]) ) expect(byKey['published']).toBe(1) // The old group must be gone or zero — never still counting the entity. expect(byKey['draft'] ?? 0).toBe(0) }) it('source.where on a reserved field filters instead of matching nothing', async () => { brain.defineAggregate({ name: 'notes_only', source: { type: NounType.Document, where: { subtype: 'note' } }, groupBy: ['team'], metrics: { count: { op: 'count' } } }) await brain.add({ data: 'n1', type: NounType.Document, subtype: 'note', metadata: { team: 'alpha' } }) await brain.add({ data: 'd1', type: NounType.Document, subtype: 'draft', metadata: { team: 'alpha' } }) const groups = await brain.queryAggregate('notes_only') expect(groups).toHaveLength(1) expect(groups[0].metrics.count).toBe(1) // the note, never the draft }) it('removeMany refuses empty/invalid selectors loudly', async () => { const id = await brain.add({ data: 'keep-me', type: NounType.Document }) // Bare array passed positionally — the classic silent no-op. await expect( brain.removeMany([id] as unknown as Parameters[0]) ).rejects.toThrow(/bare array/) // Empty selector object. await expect( brain.removeMany({} as Parameters[0]) ).rejects.toThrow(/requires a selector/) // Explicit empty id list. await expect(brain.removeMany({ ids: [] })).rejects.toThrow(/ids: \[\]/) // Nothing was deleted by any of the refused calls. expect(await brain.get(id)).toBeTruthy() }) it('find() accepts both flattened and metadata.-prefixed where spellings', async () => { await brain.add({ data: 'nested-doc', type: NounType.Document, metadata: { entry: { title: 'T1' }, classifier: { contextHints: { vfsPath: '/n/a.md' } } } }) await brain.flush() const flat = await brain.find({ type: NounType.Document, where: { 'entry.title': 'T1' }, limit: 10 }) const prefixed = await brain.find({ type: NounType.Document, where: { 'metadata.entry.title': 'T1' }, limit: 10 }) const deepPrefixed = await brain.find({ type: NounType.Document, where: { 'metadata.classifier.contextHints.vfsPath': '/n/a.md' }, limit: 10 }) expect(flat).toHaveLength(1) expect(prefixed).toHaveLength(1) expect(prefixed[0].id).toBe(flat[0].id) expect(deepPrefixed).toHaveLength(1) }) })