/** * @module tests/integration/update-write-granularity * @description Write-granularity law for update() (SELF-ENGINE-RESTART-GRIND, * 2026-07-29): a metadata-only update must NEVER rewrite the noun record — * the record carries the full vector, so an unconditional save turns every * metadata touch into a whole-vector rewrite + fsync. Under a read-heavy * consumer sweep bumping per-entity stats this amplified into disk saturation * on a production deployment. Laws: * (1) metadata-only update() → zero saveNoun calls (metadata leg only); * (2) data/vector/type-changing update() → saveNoun runs (the vector leg and * HNSW reindex still happen when the vector side actually changed); * (3) the metadata-only path still lands: merged metadata readable, _rev * bumped, find() by the new field sees the entity. */ import { describe, it, expect, beforeEach, afterEach, vi } 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('update() write granularity', () => { 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('metadata-only update never rewrites the noun record (no vector rewrite)', async () => { const id = await brain.add({ data: 'granularity law subject', type: NounType.Concept, metadata: { touched: 0 } }) const storage = (brain as any).storage const saveNounSpy = vi.spyOn(storage, 'saveNoun') await brain.update({ id, metadata: { touched: 1 } }) expect(saveNounSpy).not.toHaveBeenCalled() saveNounSpy.mockRestore() // The metadata leg still landed with full semantics. const after = await brain.get(id, { includeVectors: true }) expect(after?.metadata?.touched).toBe(1) expect(after?._rev).toBe(2) expect(Array.isArray(after?.vector) && after!.vector!.length).toBe(384) const found = await brain.find({ where: { touched: 1 } }) expect(found.some((r: any) => r.id === id)).toBe(true) }) it('confidence/weight/subtype-only updates also skip the noun record', async () => { const id = await brain.add({ data: 'reserved-field touch subject', type: NounType.Concept, metadata: {} }) const storage = (brain as any).storage const saveNounSpy = vi.spyOn(storage, 'saveNoun') await brain.update({ id, confidence: 0.5, weight: 2, subtype: 'note' }) expect(saveNounSpy).not.toHaveBeenCalled() saveNounSpy.mockRestore() const after = await brain.get(id) expect(after?.confidence).toBe(0.5) expect(after?.subtype).toBe('note') }) it('data-changing update still writes the noun record and reindexes', async () => { const id = await brain.add({ data: 'original embedded text', type: NounType.Concept, metadata: {} }) const before = await brain.get(id, { includeVectors: true }) const storage = (brain as any).storage const saveNounSpy = vi.spyOn(storage, 'saveNoun') await brain.update({ id, data: 'completely different embedded text' }) expect(saveNounSpy).toHaveBeenCalled() saveNounSpy.mockRestore() const after = await brain.get(id, { includeVectors: true }) expect(after?.data).toBe('completely different embedded text') expect(after?.vector).not.toEqual(before?.vector) }) it('explicit-vector update still writes the noun record', async () => { const id = await brain.add({ data: 'vector swap subject', type: NounType.Concept, metadata: {} }) const storage = (brain as any).storage const saveNounSpy = vi.spyOn(storage, 'saveNoun') const newVector = new Array(384).fill(0).map((_, i) => Math.cos(i)) await brain.update({ id, vector: newVector }) expect(saveNounSpy).toHaveBeenCalled() saveNounSpy.mockRestore() const after = await brain.get(id, { includeVectors: true }) expect(after?.vector?.[0]).toBeCloseTo(1) // cos(0) }) })