/** * @module tests/integration/adopt-drift-cure * @description THE DRIFT-CURING BACKFILL — the actual completion of the * default-flip ruling: existing brains whose canonical wrappers carry * pre-hydration-law drift (denormalized fields disagreeing with their own * metadata leg — the real depot-brain shape, uuid-v7 rows from the 9.0 era) * must ADOPT AUTOMATICALLY: the backfill rewrites canonical in the law * shape (metadata leg = the authority; floats preserved), the oracle then * verifies the rewrite before flipping. Same safety, zero operator chores. * Log-ahead divergences still refuse as before. */ import { describe, it, expect, afterEach } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Brainy } from '../../src/index.js' import { NounType } from '../../src/types/graphTypes.js' const dirs: string[] = [] const brains: Brainy[] = [] afterEach(async () => { for (const b of brains.splice(0)) await b.close().catch(() => {}) for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) }) type RawBox = { storage: { readNounRaw(id: string): Promise<{ metadata: unknown; vector: unknown }> writeNounRaw(id: string, r: { metadata: unknown; vector: unknown }): Promise } } describe('adoption cures hydration-law drift automatically', () => { it('a drifted wrapper (stale denormalized fields) adopts green with floats preserved', async () => { const dir = mkdtempSync(join(tmpdir(), 'brainy-drift-cure-')) dirs.push(dir) const brain = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false, logAuthority: 'defer' }) await brain.init() brains.push(brain) const id = await brain.add({ data: 'early-era row with drift', type: NounType.Document, metadata: { k: 1 } }) await brain.flush() const before = await brain.get(id, { includeVectors: true }) const floats = [...(before!.vector as number[])] expect(floats.length).toBeGreaterThan(0) // Manufacture the depot shape: the stored wrapper's denormalized fields // disagree with the metadata leg (pre-hydration-law drift) — an as-is // identity re-commit preserves this forever; the law-shape rewrite cures it. const storage = (brain as unknown as RawBox).storage const raw = await storage.readNounRaw(id) const wrapper = raw.vector as Record await storage.writeNounRaw(id, { metadata: raw.metadata, vector: { ...wrapper, noun: 'thing', // stale denormalized type (metadata leg says document) legacyField: 'pre-law residue', createdAt: '1999-01-01T00:00:00.000Z' } }) // Confirm the drift is oracle-visible before the cure. expect((await brain.verifyLogAuthority()).verdict, 'drift detected').toBe('red') // THE PIN: adoption cures it without any operator step. const report = await brain.adoptLogAuthority() expect(report.verdict).toBe('green') expect(brain.logAuthority().authority).toBe('log') // Nothing degraded: floats byte-identical, metadata intact, row serves. const after = await brain.get(id, { includeVectors: true }) expect(after!.vector as number[], 'floats preserved through the cure').toEqual(floats) expect((after!.metadata as { k: number }).k).toBe(1) expect((await brain.find({ where: { k: 1 }, limit: 5 })).map((r) => r.id)).toContain(id) }, 120000) it('log-ahead divergences still refuse — the backfill never papers over a log the witness denies', async () => { const dir = mkdtempSync(join(tmpdir(), 'brainy-logahead-')) dirs.push(dir) const brain = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false, logAuthority: 'defer' }) await brain.init() brains.push(brain) const id = await brain.add({ data: 'row', type: NounType.Document, metadata: { k: 1 } }) await brain.flush() // Log-ahead shape: canonical loses the record while the log still // claims it live (log-live-canonical-absent — NOT curable by baseline). const storage = (brain as unknown as RawBox).storage await storage.writeNounRaw(id, { metadata: null, vector: null }) await expect(brain.adoptLogAuthority()).rejects.toThrow( /log-ahead|witness denies|log claims/i ) expect(brain.logAuthority().authority).toBe('tree') }, 120000) })