brainy/tests/integration/vfs-versioning.test.ts

308 lines
11 KiB
TypeScript
Raw Normal View History

/**
* 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 })
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1) Brainy 8.0 makes subtype required by default on every public write path (`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`, import). Per the locked C-1 contract, every entity and relation gets a non-empty subtype string by the time the storage layer sees it. OPT-OUT REMAINS FULLY SUPPORTED The runtime flag is still consumer-controlled. Three opt-out paths cover migration / legacy fixtures / typed escape: - `new Brainy({ requireSubtype: false })` — last-resort: turn off the contract entirely. Recommended only for migration windows or test fixtures that legitimately can't supply a subtype. - `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` — per-type allowlist: strict everywhere except the listed types. - `brain.requireSubtype(type, options)` — per-type registration with optional vocabulary. Composes with the brain-wide flag. Default is now `true`. Opt-out is explicit and documented; nothing silently degrades. TEST SWEEP Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call site across 120 test files. Three sed patterns covered the shapes: - `new Brainy({` → `new Brainy({ requireSubtype: false,` - `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,` - `new Brainy()` → `new Brainy({ requireSubtype: false })` tests/helpers/test-factory.ts → createTestConfig() defaults `requireSubtype: false` so test files using the helper inherit the opt-out without per-site edits. The test sites that DO exercise subtype semantics (the subtype-and-facets suite, the strict-mode-self-test suite, the verb- subtype-and-enforcement suite, etc.) already pass real subtypes — they were the 7.30.x acceptance tests for this contract. Those tests continue to pass unchanged. CHANGES src/brainy.ts - normalizeConfig() — `requireSubtype` default `false` → `true`. Comment refreshed to document the three opt-out paths. tests/* (120 files) - Bulk-edited brain construction sites. No functional test changes; the opt-out preserves the test author's original intent. tests/helpers/test-factory.ts - createTestConfig() base config gains `requireSubtype: false`. NO-OP for consumers who were already passing subtype on every write. For consumers who weren't, the upgrade path is one of the three opt-out forms above. Migration recipe documented in 8.0 release notes (next commit). VERIFICATION - npx tsc --noEmit: clean - npm test: 1408 / 1409 (same pre-existing race-condition outstanding; no other regressions from the flip)
2026-06-09 14:58:25 -07:00
brain = new Brainy({ requireSubtype: false,
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)
})
})
})