/** * @module tests/integration/temporal-vfs * @description VFS content joins the Model-B immutability model. Previously * the temporal model had a hole exactly where files were concerned: entity * records were versioned per write (before-images), but the content BYTES * lived under an eager refCount GC — `rm` could physically delete bytes that * in-window history still referenced, and overwrite never released the old * hash at all (an unbounded silent leak that only accidentally preserved * history). Now blob bytes are retention-protected: a content blob referenced * by any generation inside the retention window (or live) survives, and the * ONE reclamation point is history compaction. * * Pins: * - `readFile(path, { asOf })` returns each version's exact bytes. * - `history(path)` lists versions ascending with generation/hash/size. * - overwrite releases the superseded live reference (leak fixed) while * history protects the bytes; rm keeps bytes readable via asOf. * - compaction past the last referencing generation physically reclaims * bytes (zero live + zero history), and never reclaims in-window content — * including the cross-file dedup case where an old file's history and a * newer file's history share one hash. * - overwrite refreshes the entity's `data` (embedding text) — the stale * `.data` defect. * - the backfill/scrub recounts exactly from the generation records. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { Brainy } from '../../src/brainy.js' describe('temporal VFS — blob immutability under Model B', () => { let dir: string let brain: any beforeEach(async () => { process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true' dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-temporal-vfs-')) brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir }, dimensions: 384, silent: true }) await brain.init() }) afterEach(async () => { await brain.close() fs.rmSync(dir, { recursive: true, force: true }) }) /** The file's blob metadata via the storage layer (test introspection). */ async function blobMeta(hash: string) { return brain['storage'].blobStorage.getMetadata(hash) } it('readFile(path, {asOf}) returns each version‘s exact bytes; history(path) lists them', async () => { const p = '/pages/home.json' await brain.vfs.writeFile(p, 'v1 — original') const g1 = brain.generationStore.generation() await brain.vfs.writeFile(p, 'v2 — edited') await brain.vfs.writeFile(p, 'v3 — final') // Live read = newest. expect((await brain.vfs.readFile(p)).toString()).toBe('v3 — final') // History lists the file's write-generations ascending, distinct hashes. const versions = await brain.vfs.history(p) expect(versions.length).toBeGreaterThanOrEqual(3) const gens = versions.map((v: any) => v.generation) expect([...gens].sort((a: number, b: number) => a - b)).toEqual(gens) // Every listed version's exact bytes are readable. const texts: string[] = [] for (const v of versions) { texts.push((await brain.vfs.readFile(p, { asOf: v.generation })).toString()) } expect(texts).toContain('v1 — original') expect(texts).toContain('v2 — edited') expect(texts).toContain('v3 — final') // Direct generation read too (the incident shape: recover the past bytes). expect((await brain.vfs.readFile(p, { asOf: g1 })).toString()).toBe('v1 — original') }) it('overwrite releases the superseded LIVE reference (leak fixed) while history protects the bytes', async () => { const p = '/notes/leak.txt' await brain.vfs.writeFile(p, 'old content A') const oldHash = (await brain.vfs.history(p))[0].hash const g1 = brain.generationStore.generation() await brain.vfs.writeFile(p, 'new content B') // The old hash's LIVE reference is released (previously it leaked forever)… expect((await blobMeta(oldHash))?.refCount).toBe(0) // …but the bytes survive (history-protected) and asOf still reads them. expect((await brain.vfs.readFile(p, { asOf: g1 })).toString()).toBe('old content A') }) it('rm keeps the bytes readable via asOf inside the window (no premature deletion)', async () => { const p = '/docs/doomed.txt' await brain.vfs.writeFile(p, 'still recoverable after rm') const g = brain.generationStore.generation() const hash = (await brain.vfs.history(p))[0].hash await brain.vfs.unlink(p) // Live path is gone… await expect(brain.vfs.readFile(p)).rejects.toThrow() // …the bytes are not: zero live refs, history-protected. expect((await blobMeta(hash))?.refCount).toBe(0) expect((await brain['storage'].blobStorage.read(hash)).toString()).toBe( 'still recoverable after rm' ) // (Path-level asOf resolution of a DELETED path is future work — the // bytes + entity history are intact; recovery reads go through the hash // or a pre-delete generation view.) void g }) it('compaction is the ONE reclamation point: past-window bytes are reclaimed, in-window preserved', async () => { const p = '/pages/compact.json' await brain.vfs.writeFile(p, 'version ONE') const v1Hash = (await brain.vfs.history(p))[0].hash await brain.vfs.writeFile(p, 'version TWO') await brain.vfs.writeFile(p, 'version THREE') // Sanity: v1's bytes exist (history-protected, live-released). expect(await blobMeta(v1Hash)).toBeDefined() expect((await blobMeta(v1Hash))?.refCount).toBe(0) // Compact everything reclaimable (retain nothing beyond the live state). await brain.compactHistory({ maxGenerations: 0 }) // v1's bytes are physically GONE — zero live, zero history. expect(await blobMeta(v1Hash)).toBeUndefined() // The live version is untouched. expect((await brain.vfs.readFile(p)).toString()).toBe('version THREE') }) it('cross-file dedup: a shared hash survives while ANY in-window generation references it', async () => { const shared = 'identical bytes shared across files and time' const pA = '/a.txt' const pB = '/b.txt' // A holds the shared content, then moves off it (A's history references it). await brain.vfs.writeFile(pA, shared) await brain.vfs.writeFile(pA, 'A moved on') // B now holds the shared content live, then is removed (B's remove-gen // before-image references it too). await brain.vfs.writeFile(pB, shared) const gBLive = brain.generationStore.generation() const sharedHash = (await brain.vfs.history(pA))[0].hash await brain.vfs.unlink(pB) // Live refs are zero (A moved off; B removed) — bytes survive on history. expect((await blobMeta(sharedHash))?.refCount).toBe(0) expect((await brain['storage'].blobStorage.read(sharedHash)).toString()).toBe(shared) // B's content at its live generation is still exactly readable… via asOf // on A's early version too (same bytes, same blob). const versionsA = await brain.vfs.history(pA) const aV1 = versionsA[0] expect((await brain.vfs.readFile(pA, { asOf: aV1.generation })).toString()).toBe(shared) void gBLive // Full compaction drops the last history references → bytes reclaimed. await brain.compactHistory({ maxGenerations: 0 }) expect(await blobMeta(sharedHash)).toBeUndefined() }) it('overwrite refreshes the entity data/embedding text (the stale-.data defect)', async () => { const p = '/pages/data-fresh.txt' await brain.vfs.writeFile(p, 'first words') await brain.vfs.writeFile(p, 'second words entirely') const entityId = await brain.vfs['pathResolver'].resolve(p) const entity = await brain.get(entityId) expect(entity.data).toBe('second words entirely') }) it('the scrub recounts history references exactly from the generation records', async () => { const p = '/pages/scrubbed.md' await brain.vfs.writeFile(p, 'scrub v1') await brain.vfs.writeFile(p, 'scrub v2') const v1Hash = (await brain.vfs.history(p))[0].hash await brain.flush() const storage = brain['storage'] const before = (await blobMeta(v1Hash))?.historyRefCount ?? 0 expect(before).toBeGreaterThan(0) // Corrupt the counter (simulates a legacy store / crash drift), then scrub. await storage.blobStorage.setHistoryRefCount(v1Hash, 0) const result = await storage.scrubBlobHistoryRefCounts() expect(result.records).toBeGreaterThan(0) expect((await blobMeta(v1Hash))?.historyRefCount).toBe(before) }) })