brainy/tests/integration/vfs-versioning.test.ts
David Snelling 3e0f235f8b fix(versioning): VFS file versions now capture actual blob content
VFS files store content in BlobStorage, but versioning was capturing
stale embedding text from entity.data instead of actual file content.

Changes:
- VersionManager.save() now reads fresh content via vfs.readFile()
- VersionManager.restore() writes content back via vfs.writeFile()
- Text files stored as UTF-8, binary as base64 with encoding flag
- Added comprehensive VFS versioning test suite (10 tests)
- Updated API docs with VFS file versioning example

Fixes: Workshop team bug report where all VFS file versions had
identical data despite different metadata.size values.
2025-12-09 16:13:45 -08:00

307 lines
11 KiB
TypeScript

/**
* VFS File Versioning Integration Tests (v6.3.2)
*
* Tests the fix for the VFS versioning bug where file content was stale.
*
* Bug: VFS files store content in BlobStorage, but versioning captured stale
* embedding text from entity.data instead of actual file content.
*
* Fix: VersionManager.save() now reads fresh content from BlobStorage for VFS files,
* and restore() writes content back to BlobStorage.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import * as fs from 'fs'
import * as path from 'path'
describe('VFS File Versioning (v6.3.2 Fix)', () => {
const testDir = path.join(process.cwd(), 'test-vfs-versioning-fix')
let brain: Brainy
beforeAll(async () => {
// Clean up from any previous run
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true })
}
fs.mkdirSync(testDir, { recursive: true })
brain = new Brainy({
storage: {
type: 'filesystem',
options: { path: testDir }
},
silent: true
})
await brain.init()
})
afterAll(() => {
// Clean up test directory
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true })
}
})
describe('Core VFS Versioning', () => {
it('should version actual file content, not stale embedding text', async () => {
// Create a text file
await brain.vfs.writeFile('/test/file.md', 'Initial content - version 1')
// Get the file entity
const stat = await brain.vfs.stat('/test/file.md')
const entityId = stat.entityId
// Save version 1
await brain.versions.save(entityId, { description: 'Version 1' })
// Modify the file with different content
await brain.vfs.writeFile('/test/file.md', 'Modified content - version 2 with more text')
// Save version 2
await brain.versions.save(entityId, { description: 'Version 2' })
// Retrieve both versions
const v1Content = await brain.versions.getContent(entityId, 1)
const v2Content = await brain.versions.getContent(entityId, 2)
// CRITICAL TEST: The data should be DIFFERENT between versions
// This was the bug - both had the same stale content
expect(v1Content.data).not.toBe(v2Content.data)
// Verify actual content
expect(v1Content.data).toBe('Initial content - version 1')
expect(v2Content.data).toBe('Modified content - version 2 with more text')
})
it('should restore VFS file content correctly', async () => {
// Create initial file
await brain.vfs.writeFile('/docs/readme.md', 'README version 1')
const stat = await brain.vfs.stat('/docs/readme.md')
const entityId = stat.entityId
// Save version 1
await brain.versions.save(entityId, { tag: 'v1' })
// Modify file
await brain.vfs.writeFile('/docs/readme.md', 'README version 2 - updated')
// Save version 2
await brain.versions.save(entityId, { tag: 'v2' })
// Verify current content is v2
let currentContent = await brain.vfs.readFile('/docs/readme.md')
expect(currentContent.toString()).toBe('README version 2 - updated')
// Restore to v1
await brain.versions.restore(entityId, 1)
// Verify content is now v1
currentContent = await brain.vfs.readFile('/docs/readme.md')
expect(currentContent.toString()).toBe('README version 1')
})
it('should handle multiple file edits and versions', async () => {
await brain.vfs.writeFile('/project/config.json', '{"version": 1}')
const stat = await brain.vfs.stat('/project/config.json')
const entityId = stat.entityId
// Create 5 versions
for (let i = 1; i <= 5; i++) {
await brain.vfs.writeFile('/project/config.json', `{"version": ${i}}`)
await brain.versions.save(entityId, { tag: `v${i}` })
}
// Verify all 5 versions exist
const versions = await brain.versions.list(entityId)
expect(versions).toHaveLength(5)
// Verify each version has correct content
for (let i = 1; i <= 5; i++) {
const content = await brain.versions.getContent(entityId, i)
expect(content.data).toBe(`{"version": ${i}}`)
}
})
it('should restore to any version in history', async () => {
await brain.vfs.writeFile('/notes/todo.txt', 'Task 1')
const stat = await brain.vfs.stat('/notes/todo.txt')
const entityId = stat.entityId
await brain.versions.save(entityId, { tag: 'initial' })
await brain.vfs.writeFile('/notes/todo.txt', 'Task 1\nTask 2')
await brain.versions.save(entityId, { tag: 'two-tasks' })
await brain.vfs.writeFile('/notes/todo.txt', 'Task 1\nTask 2\nTask 3')
await brain.versions.save(entityId, { tag: 'three-tasks' })
// Restore to middle version
await brain.versions.restore(entityId, 'two-tasks')
let content = await brain.vfs.readFile('/notes/todo.txt')
expect(content.toString()).toBe('Task 1\nTask 2')
// Restore to initial version
await brain.versions.restore(entityId, 'initial')
content = await brain.vfs.readFile('/notes/todo.txt')
expect(content.toString()).toBe('Task 1')
// Restore to latest version
await brain.versions.restore(entityId, 3)
content = await brain.vfs.readFile('/notes/todo.txt')
expect(content.toString()).toBe('Task 1\nTask 2\nTask 3')
})
})
describe('Binary File Versioning', () => {
it('should version binary files with base64 encoding', async () => {
// Create a simple binary file (simulated image header)
const binaryContent1 = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x01])
const binaryContent2 = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x02])
await brain.vfs.writeFile('/images/test.png', binaryContent1)
const stat = await brain.vfs.stat('/images/test.png')
const entityId = stat.entityId
// Save version 1
await brain.versions.save(entityId, { tag: 'v1' })
// Modify binary content
await brain.vfs.writeFile('/images/test.png', binaryContent2)
// Save version 2
await brain.versions.save(entityId, { tag: 'v2' })
// Retrieve and verify both versions are different
const v1Content = await brain.versions.getContent(entityId, 1)
const v2Content = await brain.versions.getContent(entityId, 2)
expect(v1Content.data).not.toBe(v2Content.data)
// Verify binary content can be restored
await brain.versions.restore(entityId, 1)
const restored = await brain.vfs.readFile('/images/test.png')
expect(Buffer.compare(restored, binaryContent1)).toBe(0)
})
})
describe('Version Deduplication', () => {
it('should deduplicate identical VFS file content', async () => {
await brain.vfs.writeFile('/cache/data.txt', 'Cached data')
const stat = await brain.vfs.stat('/cache/data.txt')
const entityId = stat.entityId
// Save version 1
const v1 = await brain.versions.save(entityId, { tag: 'v1' })
// Save again without changes - should dedupe
const v2 = await brain.versions.save(entityId, { tag: 'v2' })
// Should return same version (content hash match)
expect(v2.version).toBe(v1.version)
expect(v2.contentHash).toBe(v1.contentHash)
// Only 1 version should exist
const versions = await brain.versions.list(entityId)
expect(versions).toHaveLength(1)
})
})
describe('Mixed Entity Versioning', () => {
it('should handle VFS and non-VFS entities in same brain', async () => {
// Create a VFS file
await brain.vfs.writeFile('/files/doc.txt', 'File content v1')
const fileStat = await brain.vfs.stat('/files/doc.txt')
const fileId = fileStat.entityId
// Create a regular entity
const regularId = await brain.add({
data: 'Regular entity',
type: 'document',
metadata: { title: 'Regular Doc' }
})
// Version both
await brain.versions.save(fileId, { tag: 'file-v1' })
await brain.versions.save(regularId, { tag: 'regular-v1' })
// Update both
await brain.vfs.writeFile('/files/doc.txt', 'File content v2')
await brain.update({ id: regularId, metadata: { title: 'Updated Regular Doc' } })
// Version both again
await brain.versions.save(fileId, { tag: 'file-v2' })
await brain.versions.save(regularId, { tag: 'regular-v2' })
// Verify VFS file versions have different content
const fileV1 = await brain.versions.getContent(fileId, 1)
const fileV2 = await brain.versions.getContent(fileId, 2)
expect(fileV1.data).toBe('File content v1')
expect(fileV2.data).toBe('File content v2')
// Verify regular entity versions work too
const regV1 = await brain.versions.getContent(regularId, 1)
const regV2 = await brain.versions.getContent(regularId, 2)
expect(regV1.title).toBe('Regular Doc')
expect(regV2.title).toBe('Updated Regular Doc')
})
})
describe('Edge Cases', () => {
it('should handle minimal content files', async () => {
// VFS requires non-empty data for embedding, so we use minimal content
await brain.vfs.writeFile('/minimal/file.txt', ' ') // Single space
const stat = await brain.vfs.stat('/minimal/file.txt')
const entityId = stat.entityId
await brain.versions.save(entityId, { tag: 'minimal' })
await brain.vfs.writeFile('/minimal/file.txt', 'Now has content')
await brain.versions.save(entityId, { tag: 'filled' })
// Restore to minimal
await brain.versions.restore(entityId, 'minimal')
const content = await brain.vfs.readFile('/minimal/file.txt')
expect(content.toString()).toBe(' ')
})
it('should handle large text files', async () => {
const largeContent1 = 'A'.repeat(100000) + ' version 1'
const largeContent2 = 'B'.repeat(100000) + ' version 2'
await brain.vfs.writeFile('/large/file.txt', largeContent1)
const stat = await brain.vfs.stat('/large/file.txt')
const entityId = stat.entityId
await brain.versions.save(entityId, { tag: 'v1' })
await brain.vfs.writeFile('/large/file.txt', largeContent2)
await brain.versions.save(entityId, { tag: 'v2' })
// Verify versions are different
const v1 = await brain.versions.getContent(entityId, 1)
const v2 = await brain.versions.getContent(entityId, 2)
expect(v1.data).toBe(largeContent1)
expect(v2.data).toBe(largeContent2)
})
it('should handle special characters in content', async () => {
const specialContent = 'Unicode: \u{1F600} \u{1F914} \u{1F4A1}\nNewlines\nAnd\ttabs'
await brain.vfs.writeFile('/special.txt', specialContent)
const stat = await brain.vfs.stat('/special.txt')
const entityId = stat.entityId
await brain.versions.save(entityId, { tag: 'special' })
await brain.vfs.writeFile('/special.txt', 'Plain text now')
await brain.versions.save(entityId, { tag: 'plain' })
// Restore special content
await brain.versions.restore(entityId, 'special')
const content = await brain.vfs.readFile('/special.txt')
expect(content.toString()).toBe(specialContent)
})
})
})