2025-11-04 11:19:02 -08:00
/ * *
2025-12-09 09:35:45 -08:00
* Entity Versioning Integration Tests ( v5 . 3.0 , v6 . 3.0 )
2025-11-04 11:19:02 -08:00
*
* Tests the complete versioning workflow :
* - Save versions
* - List versions
* - Restore versions
* - Compare versions
* - Prune versions
2025-12-09 09:35:45 -08:00
* - Branch isolation
* - Index pollution prevention
*
* v6.3.0 : Pure key - value storage , no index pollution , restore ( ) updates all indexes
2025-11-04 11:19:02 -08:00
* /
import { describe , it , expect , beforeEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import type { EntityVersion } from '../../src/versioning/VersionManager.js'
2025-12-09 09:35:45 -08:00
import { randomUUID } from 'crypto'
// Helper to generate valid UUIDs for tests
const uuid = ( ) = > randomUUID ( ) . replace ( /-/g , '' )
2025-11-04 11:19:02 -08:00
describe ( 'Entity Versioning (v5.3.0)' , ( ) = > {
let brain : Brainy
beforeEach ( async ( ) = > {
brain = new Brainy ( {
storage : { type : 'memory' } ,
silent : true
} )
await brain . init ( )
} )
describe ( 'Core Versioning API' , ( ) = > {
it ( 'should save and retrieve versions' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
// Add initial entity
await brain . add ( {
data : 'Alice' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
type : 'person' , // v6.3.0: Use valid NounType
2025-11-04 11:19:02 -08:00
metadata : {
name : 'Alice' ,
email : 'alice@example.com'
}
} )
// Save version 1
2025-12-09 09:35:45 -08:00
const v1 = await brain . versions . save ( entityId , {
2025-11-04 11:19:02 -08:00
tag : 'v1.0' ,
description : 'Initial version'
} )
expect ( v1 . version ) . toBe ( 1 )
2025-12-09 09:35:45 -08:00
expect ( v1 . entityId ) . toBe ( entityId )
2025-11-04 11:19:02 -08:00
expect ( v1 . tag ) . toBe ( 'v1.0' )
expect ( v1 . contentHash ) . toBeDefined ( )
// Update entity
2025-12-09 09:35:45 -08:00
await brain . update ( { id : entityId , metadata : { name : 'Alice Smith' } } )
2025-11-04 11:19:02 -08:00
// Save version 2
2025-12-09 09:35:45 -08:00
const v2 = await brain . versions . save ( entityId , {
2025-11-04 11:19:02 -08:00
tag : 'v2.0' ,
description : 'Updated name'
} )
expect ( v2 . version ) . toBe ( 2 )
2025-12-09 09:35:45 -08:00
expect ( v2 . entityId ) . toBe ( entityId )
2025-11-04 11:19:02 -08:00
// List versions
2025-12-09 09:35:45 -08:00
const versions = await brain . versions . list ( entityId )
2025-11-04 11:19:02 -08:00
expect ( versions ) . toHaveLength ( 2 )
expect ( versions [ 0 ] . version ) . toBe ( 2 ) // Newest first
expect ( versions [ 1 ] . version ) . toBe ( 1 )
} )
it ( 'should deduplicate identical content' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
await brain . add ( {
data : 'Doc' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
2025-11-04 11:19:02 -08:00
type : 'document' ,
metadata : {
name : 'Doc' ,
content : 'Hello'
}
} )
// Save version 1
2025-12-09 09:35:45 -08:00
const v1 = await brain . versions . save ( entityId , { tag : 'v1' } )
2025-11-04 11:19:02 -08:00
// Save again without changes
2025-12-09 09:35:45 -08:00
const v2 = await brain . versions . save ( entityId , { tag : 'v2' } )
2025-11-04 11:19:02 -08:00
// Should return existing version (same content hash)
expect ( v2 . version ) . toBe ( v1 . version )
expect ( v2 . contentHash ) . toBe ( v1 . contentHash )
// Only one version should exist
2025-12-09 09:35:45 -08:00
const versions = await brain . versions . list ( entityId )
2025-11-04 11:19:02 -08:00
expect ( versions ) . toHaveLength ( 1 )
} )
it ( 'should restore to previous version' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
await brain . add ( {
data : 'Config' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
2025-11-04 11:19:02 -08:00
type : 'thing' ,
metadata : {
name : 'Config' ,
settings : { theme : 'light' }
}
} )
// Save v1
2025-12-09 09:35:45 -08:00
await brain . versions . save ( entityId , { tag : 'v1' } )
2025-11-04 11:19:02 -08:00
// Update
2025-12-09 09:35:45 -08:00
await brain . update ( { id : entityId , metadata : { settings : { theme : 'dark' } } } )
2025-11-04 11:19:02 -08:00
// Save v2
2025-12-09 09:35:45 -08:00
await brain . versions . save ( entityId , { tag : 'v2' } )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Verify current state (getNounMetadata returns flat NounMetadata)
let current = await brain . getNounMetadata ( entityId )
expect ( current ? . settings ? . theme ) . toBe ( 'dark' )
2025-11-04 11:19:02 -08:00
// Restore to v1
2025-12-09 09:35:45 -08:00
await brain . versions . restore ( entityId , 1 )
2025-11-04 11:19:02 -08:00
// Verify restored state
2025-12-09 09:35:45 -08:00
current = await brain . getNounMetadata ( entityId )
expect ( current ? . settings ? . theme ) . toBe ( 'light' )
2025-11-04 11:19:02 -08:00
} )
it ( 'should compare versions' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
await brain . add ( {
data : 'Bob' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
type : 'person' , // v6.3.0: Use valid NounType
2025-11-04 11:19:02 -08:00
metadata : {
name : 'Bob' ,
email : 'bob@example.com' ,
age : 30
}
} )
2025-12-09 09:35:45 -08:00
await brain . versions . save ( entityId , { tag : 'v1' } )
2025-11-04 11:19:02 -08:00
// Update
2025-12-09 09:35:45 -08:00
await brain . update ( {
id : entityId ,
metadata : {
name : 'Robert' ,
email : 'robert@example.com' ,
city : 'NYC'
}
2025-11-04 11:19:02 -08:00
} )
2025-12-09 09:35:45 -08:00
await brain . versions . save ( entityId , { tag : 'v2' } )
2025-11-04 11:19:02 -08:00
// Compare versions
2025-12-09 09:35:45 -08:00
const diff = await brain . versions . compare ( entityId , 1 , 2 )
2025-11-04 11:19:02 -08:00
expect ( diff . totalChanges ) . toBeGreaterThan ( 0 )
expect ( diff . modified . length ) . toBeGreaterThan ( 0 )
expect ( diff . added . length ) . toBeGreaterThan ( 0 )
// Check specific changes
const nameChange = diff . modified . find ( c = > c . path . includes ( 'name' ) )
expect ( nameChange ) . toBeDefined ( )
expect ( nameChange ? . oldValue ) . toBe ( 'Bob' )
expect ( nameChange ? . newValue ) . toBe ( 'Robert' )
const cityAdd = diff . added . find ( c = > c . path . includes ( 'city' ) )
expect ( cityAdd ) . toBeDefined ( )
expect ( cityAdd ? . newValue ) . toBe ( 'NYC' )
} )
it ( 'should get version content without restoring' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
await brain . add ( {
data : 'Note' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
2025-11-04 11:19:02 -08:00
type : 'document' ,
metadata : {
name : 'Note' ,
content : 'Version 1'
}
} )
2025-12-09 09:35:45 -08:00
await brain . versions . save ( entityId , { tag : 'v1' } )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
await brain . update ( { id : entityId , metadata : { content : 'Version 2' } } )
await brain . versions . save ( entityId , { tag : 'v2' } )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Get v1 content without restoring (returns flat NounMetadata)
const v1Content = await brain . versions . getContent ( entityId , 1 )
expect ( v1Content . content ) . toBe ( 'Version 1' )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Current should still be v2 (getNounMetadata returns flat NounMetadata)
const current = await brain . getNounMetadata ( entityId )
expect ( current ? . content ) . toBe ( 'Version 2' )
2025-11-04 11:19:02 -08:00
} )
it ( 'should prune old versions' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
await brain . add ( {
data : 'Log' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
2025-11-04 11:19:02 -08:00
type : 'document' ,
metadata : {
name : 'Log'
}
} )
// Create 10 versions
for ( let i = 1 ; i <= 10 ; i ++ ) {
2025-12-09 09:35:45 -08:00
await brain . update ( { id : entityId , metadata : { content : ` Entry ${ i } ` } } )
await brain . versions . save ( entityId , { tag : ` v ${ i } ` } )
2025-11-04 11:19:02 -08:00
}
// Verify all 10 exist
2025-12-09 09:35:45 -08:00
let versions = await brain . versions . list ( entityId )
2025-11-04 11:19:02 -08:00
expect ( versions . length ) . toBeGreaterThanOrEqual ( 10 )
// Prune to keep only 5 most recent
2025-12-09 09:35:45 -08:00
const result = await brain . versions . prune ( entityId , {
2025-11-04 11:19:02 -08:00
keepRecent : 5 ,
keepTagged : false
} )
expect ( result . deleted ) . toBeGreaterThan ( 0 )
expect ( result . kept ) . toBe ( 5 )
// Verify only 5 remain
2025-12-09 09:35:45 -08:00
versions = await brain . versions . list ( entityId )
2025-11-04 11:19:02 -08:00
expect ( versions ) . toHaveLength ( 5 )
} )
it ( 'should support version tags' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
await brain . add ( {
data : 'App' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
2025-11-04 11:19:02 -08:00
type : 'thing' ,
metadata : {
name : 'App'
}
} )
2025-12-09 09:35:45 -08:00
await brain . versions . save ( entityId , { tag : 'alpha' } )
await brain . update ( { id : entityId , metadata : { version : '0.2' } } )
await brain . versions . save ( entityId , { tag : 'beta' } )
await brain . update ( { id : entityId , metadata : { version : '1.0' } } )
await brain . versions . save ( entityId , { tag : 'release' } )
2025-11-04 11:19:02 -08:00
// Get by tag
2025-12-09 09:35:45 -08:00
const beta = await brain . versions . getVersionByTag ( entityId , 'beta' )
2025-11-04 11:19:02 -08:00
expect ( beta ) . toBeDefined ( )
expect ( beta ? . tag ) . toBe ( 'beta' )
// Restore by tag
2025-12-09 09:35:45 -08:00
await brain . versions . restore ( entityId , 'beta' )
const current = await brain . getNounMetadata ( entityId )
expect ( current ? . version ) . toBe ( '0.2' )
2025-11-04 11:19:02 -08:00
} )
it ( 'should support undo/revert' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
await brain . add ( {
data : 'Data' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
2025-11-04 11:19:02 -08:00
type : 'thing' ,
metadata : {
name : 'Data' ,
value : 100
}
} )
2025-12-09 09:35:45 -08:00
// Save v1 (value=100)
await brain . versions . save ( entityId , { tag : 'v1' } )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Update and save v2 (value=200)
await brain . update ( { id : entityId , metadata : { value : 200 } } )
await brain . versions . save ( entityId , { tag : 'v2' } )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// undo() restores to SECOND-MOST-RECENT version and creates a snapshot
// Note: undo() internally calls restore() with createSnapshot: true
const undone = await brain . versions . undo ( entityId )
expect ( undone ) . not . toBeNull ( )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// After undo, entity should have v1's value
const currentVal = await brain . getNounMetadata ( entityId )
expect ( currentVal ? . value ) . toBe ( 100 ) // v1's value
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Now we have: [before-undo, v2, v1] - undo created a snapshot
const versionsAfterUndo = await brain . versions . list ( entityId )
expect ( versionsAfterUndo . length ) . toBeGreaterThanOrEqual ( 2 )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Update and save v3 (value=300)
await brain . update ( { id : entityId , metadata : { value : 300 } } )
await brain . versions . save ( entityId , { tag : 'v3' } )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// revert() is alias for undo()
const reverted = await brain . versions . revert ( entityId )
expect ( reverted ) . not . toBeNull ( )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// The entity should be restored to second-most-recent version
const revertedVal = await brain . getNounMetadata ( entityId )
// After revert from v3, we go to the previous version
expect ( revertedVal ? . value ) . toBeDefined ( )
2025-11-04 11:19:02 -08:00
} )
} )
describe ( 'Branch Isolation' , ( ) = > {
it ( 'should isolate versions by branch' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
2025-11-04 11:19:02 -08:00
await brain . add ( {
data : 'Test' ,
2025-12-09 09:35:45 -08:00
id : entityId ,
2025-11-04 11:19:02 -08:00
type : 'thing' ,
metadata : {
name : 'Test'
}
} )
2025-12-09 09:35:45 -08:00
// Save versions on main
await brain . versions . save ( entityId , { tag : 'main-v1' } )
await brain . update ( { id : entityId , metadata : { name : 'Main Update' } } )
await brain . versions . save ( entityId , { tag : 'main-v2' } )
// Main should have versions
const mainVersionsBefore = await brain . versions . list ( entityId )
expect ( mainVersionsBefore . length ) . toBeGreaterThanOrEqual ( 2 )
// Main versions should have branch='main'
expect ( mainVersionsBefore . every ( v = > v . branch === 'main' ) ) . toBe ( true )
2025-11-04 11:19:02 -08:00
// Switch to feature branch
2025-12-09 09:35:45 -08:00
// Note: fork() creates the branch and returns a NEW instance
// We need to checkout() to switch the current instance to the new branch
2025-11-04 11:19:02 -08:00
await brain . fork ( 'feature' )
2025-12-09 09:35:45 -08:00
await brain . checkout ( 'feature' )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Save version on feature with unique tag
await brain . update ( { id : entityId , metadata : { name : 'Feature Update' } } )
await brain . versions . save ( entityId , { tag : 'feature-only-v1' } )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Feature versions list - may include inherited versions from COW
const featureVersions = await brain . versions . list ( entityId )
2025-11-04 11:19:02 -08:00
expect ( featureVersions . length ) . toBeGreaterThan ( 0 )
2025-12-09 09:35:45 -08:00
// Feature should have our unique tag (this is the NEW version on feature)
const featureOnlyVersion = featureVersions . find ( v = > v . tag === 'feature-only-v1' )
expect ( featureOnlyVersion ) . toBeDefined ( )
// The version we just saved on feature should have branch='feature'
expect ( featureOnlyVersion ! . branch ) . toBe ( 'feature' )
2025-11-04 11:19:02 -08:00
// Switch back to main
await brain . checkout ( 'main' )
2025-12-09 09:35:45 -08:00
// Main versions should not have feature's unique tag
const mainVersionsAfter = await brain . versions . list ( entityId )
const featureTagOnMain = mainVersionsAfter . find ( v = > v . tag === 'feature-only-v1' )
expect ( featureTagOnMain ) . toBeUndefined ( ) // Feature-only version not on main
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Main should still have its original versions
expect ( mainVersionsAfter . some ( v = > v . tag === 'main-v1' ) ) . toBe ( true )
expect ( mainVersionsAfter . some ( v = > v . tag === 'main-v2' ) ) . toBe ( true )
2025-11-04 11:19:02 -08:00
} )
} )
describe ( 'Edge Cases' , ( ) = > {
it ( 'should handle non-existent entities gracefully' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const nonExistentId = uuid ( )
2025-11-04 11:19:02 -08:00
await expect (
2025-12-09 09:35:45 -08:00
brain . versions . save ( nonExistentId , { tag : 'v1' } )
) . rejects . toThrow ( ` Entity ${ nonExistentId } not found ` )
2025-11-04 11:19:02 -08:00
} )
it ( 'should handle empty version history' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
await brain . add ( { data : 'New' , id : entityId , type : 'thing' , metadata : { name : 'New' } } )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
expect ( await brain . versions . count ( entityId ) ) . toBe ( 0 )
expect ( await brain . versions . hasVersions ( entityId ) ) . toBe ( false )
expect ( await brain . versions . getLatest ( entityId ) ) . toBeNull ( )
2025-11-04 11:19:02 -08:00
} )
it ( 'should handle version not found' , async ( ) = > {
2025-12-09 09:35:45 -08:00
const entityId = uuid ( )
await brain . add ( { data : 'Test' , id : entityId , type : 'thing' , metadata : { name : 'Test' } } )
2025-11-04 11:19:02 -08:00
await expect (
2025-12-09 09:35:45 -08:00
brain . versions . restore ( entityId , 999 )
2025-11-04 11:19:02 -08:00
) . rejects . toThrow ( 'Version 999 not found' )
} )
} )
2025-12-09 09:35:45 -08:00
describe ( 'Index Pollution Prevention (v6.3.0)' , ( ) = > {
it ( 'should NOT pollute find() results with versions' , async ( ) = > {
const entityId = uuid ( )
// Add entity
await brain . add ( {
data : 'Test entity' ,
id : entityId ,
type : 'document' ,
metadata : { name : 'Test' , category : 'pollution-test' }
} )
// Save multiple versions
await brain . versions . save ( entityId , { tag : 'v1' } )
await brain . update ( { id : entityId , metadata : { name : 'Updated' } } )
await brain . versions . save ( entityId , { tag : 'v2' } )
await brain . update ( { id : entityId , metadata : { name : 'Updated Again' } } )
await brain . versions . save ( entityId , { tag : 'v3' } )
// Verify 3 versions exist
const versions = await brain . versions . list ( entityId )
expect ( versions ) . toHaveLength ( 3 )
// find() should return ONLY the real entity, not version entries
const results = await brain . find ( { where : { category : 'pollution-test' } } )
expect ( results ) . toHaveLength ( 1 )
expect ( results [ 0 ] . id ) . toBe ( entityId )
// Verify no version entities pollute the index
// (This was the bug - _isVersion entities appeared in find())
const allDocs = await brain . find ( { where : { type : 'document' } , limit : 100 } )
const versionEntities = allDocs . filter ( ( e : any ) = > e . metadata ? . _isVersion )
expect ( versionEntities ) . toHaveLength ( 0 )
} )
it ( 'should keep current entity fully indexed after versioning' , async ( ) = > {
const entityId = uuid ( )
// Add entity
await brain . add ( {
data : 'Searchable content about machine learning' ,
id : entityId ,
type : 'document' ,
metadata : { name : 'ML Doc' , topic : 'ai' }
} )
// Save versions
await brain . versions . save ( entityId , { tag : 'v1' } )
await brain . update ( { id : entityId , metadata : { name : 'Updated ML Doc' } } )
await brain . versions . save ( entityId , { tag : 'v2' } )
// Entity should still be searchable by metadata
const byMetadata = await brain . find ( { where : { topic : 'ai' } } )
expect ( byMetadata ) . toHaveLength ( 1 )
expect ( byMetadata [ 0 ] . id ) . toBe ( entityId )
// Entity should still be searchable by vector similarity
const bySimilarity = await brain . find ( { query : 'machine learning' , limit : 5 } )
const found = bySimilarity . find ( ( e : any ) = > e . id === entityId )
expect ( found ) . toBeDefined ( )
} )
it ( 'should update indexes when restoring a version' , async ( ) = > {
const entityId = uuid ( )
// Add entity with initial state
await brain . add ( {
data : 'Initial content' ,
id : entityId ,
type : 'document' ,
metadata : { name : 'Doc' , status : 'draft' }
} )
await brain . versions . save ( entityId , { tag : 'draft' } )
// Update to published state
await brain . update ( { id : entityId , metadata : { status : 'published' } } )
await brain . versions . save ( entityId , { tag : 'published' } )
// Verify current state
let published = await brain . find ( { where : { status : 'published' } } )
expect ( published ) . toHaveLength ( 1 )
let drafts = await brain . find ( { where : { status : 'draft' } } )
expect ( drafts ) . toHaveLength ( 0 )
// Restore to draft version
await brain . versions . restore ( entityId , 'draft' )
// Indexes should update - now entity is draft again
published = await brain . find ( { where : { status : 'published' } } )
expect ( published ) . toHaveLength ( 0 )
drafts = await brain . find ( { where : { status : 'draft' } } )
expect ( drafts ) . toHaveLength ( 1 )
expect ( drafts [ 0 ] . id ) . toBe ( entityId )
} )
} )
2025-11-04 11:19:02 -08:00
} )