2025-11-04 11:19:02 -08:00
/ * *
* VersionStorage Unit Tests ( v5 . 3.0 )
*
* Tests content - addressable storage :
* - SHA - 256 content hashing
* - Content deduplication
* - Storage adapter integration
* - Save / load / delete operations
* /
import { describe , it , expect , beforeEach , vi } from 'vitest'
import { VersionStorage } from '../../../src/versioning/VersionStorage.js'
import type { NounMetadata } from '../../../src/coreTypes.js'
import type { EntityVersion } from '../../../src/versioning/VersionManager.js'
describe ( 'VersionStorage' , ( ) = > {
let storage : VersionStorage
let mockBrain : any
let mockFiles : Map < string , string >
beforeEach ( ( ) = > {
mockFiles = new Map ( )
mockBrain = {
storageAdapter : {
2025-12-09 09:35:45 -08:00
// v6.3.0: VersionStorage now uses saveMetadata/getMetadata instead of writeFile/readFile
saveMetadata : vi.fn ( async ( key : string , data : any ) = > {
mockFiles . set ( key , JSON . stringify ( data ) )
} ) ,
getMetadata : vi.fn ( async ( key : string ) = > {
const data = mockFiles . get ( key )
if ( ! data ) return null
return JSON . parse ( data )
} ) ,
// Legacy methods for tests that still use them
2025-11-04 11:19:02 -08:00
exists : vi.fn ( async ( path : string ) = > mockFiles . has ( path ) ) ,
writeFile : vi.fn ( async ( path : string , data : string ) = > {
mockFiles . set ( path , data )
} ) ,
readFile : vi.fn ( async ( path : string ) = > {
const data = mockFiles . get ( path )
if ( ! data ) throw new Error ( 'File not found' )
return data
} ) ,
deleteFile : vi.fn ( async ( path : string ) = > {
mockFiles . delete ( path )
} )
}
}
storage = new VersionStorage ( mockBrain )
} )
describe ( 'hashEntity()' , ( ) = > {
it ( 'should generate consistent SHA-256 hashes' , ( ) = > {
const entity : NounMetadata = {
id : 'user-123' ,
type : 'user' ,
name : 'Alice' ,
metadata : { email : 'alice@example.com' }
}
const hash1 = storage . hashEntity ( entity )
const hash2 = storage . hashEntity ( entity )
expect ( hash1 ) . toBe ( hash2 )
expect ( hash1 ) . toHaveLength ( 64 ) // SHA-256 = 64 hex chars
} )
it ( 'should generate different hashes for different content' , ( ) = > {
const entity1 : NounMetadata = {
id : 'user-1' ,
type : 'user' ,
name : 'Alice' ,
metadata : { }
}
const entity2 : NounMetadata = {
id : 'user-1' ,
type : 'user' ,
name : 'Bob' ,
metadata : { }
}
const hash1 = storage . hashEntity ( entity1 )
const hash2 = storage . hashEntity ( entity2 )
expect ( hash1 ) . not . toBe ( hash2 )
} )
it ( 'should ignore property order (stable JSON)' , ( ) = > {
const entity1 : NounMetadata = {
id : 'user-1' ,
type : 'user' ,
name : 'Alice' ,
metadata : { a : 1 , b : 2 , c : 3 }
}
const entity2 : NounMetadata = {
type : 'user' ,
metadata : { c : 3 , b : 2 , a : 1 } ,
name : 'Alice' ,
id : 'user-1'
}
const hash1 = storage . hashEntity ( entity1 )
const hash2 = storage . hashEntity ( entity2 )
expect ( hash1 ) . toBe ( hash2 )
} )
it ( 'should handle nested objects' , ( ) = > {
const entity : NounMetadata = {
id : 'test-1' ,
type : 'thing' ,
name : 'Test' ,
metadata : {
nested : {
deep : {
value : 'hello'
}
}
}
}
const hash = storage . hashEntity ( entity )
expect ( hash ) . toHaveLength ( 64 )
} )
it ( 'should handle arrays' , ( ) = > {
const entity : NounMetadata = {
id : 'test-1' ,
type : 'thing' ,
name : 'Test' ,
metadata : {
tags : [ 'a' , 'b' , 'c' ]
}
}
const hash = storage . hashEntity ( entity )
expect ( hash ) . toHaveLength ( 64 )
} )
it ( 'should handle null and undefined' , ( ) = > {
const entity1 : NounMetadata = {
id : 'test-1' ,
type : 'thing' ,
name : 'Test' ,
metadata : { value : null }
}
const entity2 : NounMetadata = {
id : 'test-1' ,
type : 'thing' ,
name : 'Test' ,
metadata : { value : undefined }
}
const hash1 = storage . hashEntity ( entity1 )
const hash2 = storage . hashEntity ( entity2 )
expect ( hash1 ) . not . toBe ( hash2 )
} )
} )
describe ( 'saveVersion()' , ( ) = > {
it ( 'should save version content to storage' , async ( ) = > {
const entity : NounMetadata = {
id : 'user-123' ,
type : 'user' ,
name : 'Alice' ,
metadata : { email : 'alice@example.com' }
}
const version : EntityVersion = {
version : 1 ,
entityId : 'user-123' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity )
}
await storage . saveVersion ( version , entity )
2025-12-09 09:35:45 -08:00
// v6.3.0: Uses __system_version_ prefix for saveMetadata keys
const expectedKey = ` __system_version_user-123_ ${ version . contentHash } `
expect ( mockFiles . has ( expectedKey ) ) . toBe ( true )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
const savedData = mockFiles . get ( expectedKey )
2025-11-04 11:19:02 -08:00
expect ( savedData ) . toBeDefined ( )
expect ( JSON . parse ( savedData ! ) ) . toEqual ( entity )
} )
it ( 'should deduplicate identical content' , async ( ) = > {
const entity : NounMetadata = {
id : 'doc-1' ,
type : 'document' ,
name : 'Doc' ,
metadata : { content : 'Same content' }
}
const contentHash = storage . hashEntity ( entity )
const version1 : EntityVersion = {
version : 1 ,
entityId : 'doc-1' ,
branch : 'main' ,
commitHash : 'commit-1' ,
timestamp : Date.now ( ) ,
contentHash
}
const version2 : EntityVersion = {
version : 2 ,
entityId : 'doc-1' ,
branch : 'main' ,
commitHash : 'commit-2' ,
timestamp : Date.now ( ) + 1000 ,
contentHash // Same hash!
}
await storage . saveVersion ( version1 , entity )
const writeCallCount1 = mockBrain . storageAdapter . writeFile . mock . calls . length
await storage . saveVersion ( version2 , entity )
const writeCallCount2 = mockBrain . storageAdapter . writeFile . mock . calls . length
// Should not write again (deduplication)
expect ( writeCallCount2 ) . toBe ( writeCallCount1 )
} )
it ( 'should store different content separately' , async ( ) = > {
const entity1 : NounMetadata = {
id : 'doc-1' ,
type : 'document' ,
name : 'Doc' ,
metadata : { content : 'Version 1' }
}
const entity2 : NounMetadata = {
id : 'doc-1' ,
type : 'document' ,
name : 'Doc' ,
metadata : { content : 'Version 2' }
}
const version1 : EntityVersion = {
version : 1 ,
entityId : 'doc-1' ,
branch : 'main' ,
commitHash : 'commit-1' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity1 )
}
const version2 : EntityVersion = {
version : 2 ,
entityId : 'doc-1' ,
branch : 'main' ,
commitHash : 'commit-2' ,
timestamp : Date.now ( ) + 1000 ,
contentHash : storage.hashEntity ( entity2 )
}
await storage . saveVersion ( version1 , entity1 )
await storage . saveVersion ( version2 , entity2 )
2025-12-09 09:35:45 -08:00
// v6.3.0: Uses __system_version_ prefix for saveMetadata keys
const key1 = ` __system_version_doc-1_ ${ version1 . contentHash } `
const key2 = ` __system_version_doc-1_ ${ version2 . contentHash } `
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
expect ( mockFiles . has ( key1 ) ) . toBe ( true )
expect ( mockFiles . has ( key2 ) ) . toBe ( true )
expect ( key1 ) . not . toBe ( key2 )
2025-11-04 11:19:02 -08:00
} )
} )
describe ( 'loadVersion()' , ( ) = > {
it ( 'should load version content from storage' , async ( ) = > {
const entity : NounMetadata = {
id : 'user-123' ,
type : 'user' ,
name : 'Alice' ,
metadata : { email : 'alice@example.com' }
}
const version : EntityVersion = {
version : 1 ,
entityId : 'user-123' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity )
}
// Save first
await storage . saveVersion ( version , entity )
// Load
const loaded = await storage . loadVersion ( version )
expect ( loaded ) . toEqual ( entity )
} )
it ( 'should return null if version not found' , async ( ) = > {
const version : EntityVersion = {
version : 1 ,
entityId : 'nonexistent' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : 'fake-hash'
}
const loaded = await storage . loadVersion ( version )
expect ( loaded ) . toBeNull ( )
} )
it ( 'should handle complex nested data' , async ( ) = > {
const entity : NounMetadata = {
id : 'complex-1' ,
type : 'thing' ,
name : 'Complex' ,
metadata : {
nested : {
array : [ 1 , 2 , 3 ] ,
object : { key : 'value' }
} ,
tags : [ 'a' , 'b' , 'c' ]
}
}
const version : EntityVersion = {
version : 1 ,
entityId : 'complex-1' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity )
}
await storage . saveVersion ( version , entity )
const loaded = await storage . loadVersion ( version )
expect ( loaded ) . toEqual ( entity )
} )
} )
describe ( 'deleteVersion()' , ( ) = > {
2025-12-09 09:35:45 -08:00
it ( 'should handle version deletion (content-addressed, no-op)' , async ( ) = > {
// v6.3.0: Version content is content-addressed and may be shared by multiple versions.
// The deleteVersion method is a no-op - it doesn't actually delete the content.
// Version INDEX is deleted via VersionIndex.removeVersion().
// A GC process could clean up unreferenced content in the future.
2025-11-04 11:19:02 -08:00
const entity : NounMetadata = {
id : 'user-123' ,
type : 'user' ,
name : 'Alice' ,
metadata : { }
}
const version : EntityVersion = {
version : 1 ,
entityId : 'user-123' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity )
}
// Save
await storage . saveVersion ( version , entity )
2025-12-09 09:35:45 -08:00
const key = ` __system_version_user-123_ ${ version . contentHash } `
expect ( mockFiles . has ( key ) ) . toBe ( true )
2025-11-04 11:19:02 -08:00
2025-12-09 09:35:45 -08:00
// Delete is a no-op for content (content-addressed, may be shared)
2025-11-04 11:19:02 -08:00
await storage . deleteVersion ( version )
2025-12-09 09:35:45 -08:00
// Content is NOT deleted to avoid breaking other versions with same content hash
expect ( mockFiles . has ( key ) ) . toBe ( true ) // v6.3.0: Content preserved
2025-11-04 11:19:02 -08:00
} )
it ( 'should handle deleting non-existent version gracefully' , async ( ) = > {
const version : EntityVersion = {
version : 1 ,
entityId : 'nonexistent' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : 'fake-hash'
}
// Should not throw
await storage . deleteVersion ( version )
} )
} )
describe ( 'Storage Adapter Integration' , ( ) = > {
it ( 'should work with memory storage adapter' , async ( ) = > {
const entity : NounMetadata = {
id : 'test-1' ,
type : 'thing' ,
name : 'Test' ,
metadata : { value : 123 }
}
const version : EntityVersion = {
version : 1 ,
entityId : 'test-1' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity )
}
await storage . saveVersion ( version , entity )
const loaded = await storage . loadVersion ( version )
expect ( loaded ) . toEqual ( entity )
} )
2025-12-09 09:35:45 -08:00
it ( 'should use adapter.saveMetadata API' , async ( ) = > {
// v6.3.0: VersionStorage now uses saveMetadata/getMetadata exclusively
const testStorage = new Map < string , string > ( )
2025-11-04 11:19:02 -08:00
mockBrain . storageAdapter = {
2025-12-09 09:35:45 -08:00
saveMetadata : vi.fn ( async ( key : string , data : any ) = > {
testStorage . set ( key , JSON . stringify ( data ) )
2025-11-04 11:19:02 -08:00
} ) ,
2025-12-09 09:35:45 -08:00
getMetadata : vi.fn ( async ( key : string ) = > {
const data = testStorage . get ( key )
if ( ! data ) return null
return JSON . parse ( data )
2025-11-04 11:19:02 -08:00
} )
}
storage = new VersionStorage ( mockBrain )
const entity : NounMetadata = {
id : 'test-1' ,
type : 'thing' ,
name : 'Test' ,
metadata : { }
}
const version : EntityVersion = {
version : 1 ,
entityId : 'test-1' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity )
}
await storage . saveVersion ( version , entity )
2025-12-09 09:35:45 -08:00
expect ( mockBrain . storageAdapter . saveMetadata ) . toHaveBeenCalled ( )
2025-11-04 11:19:02 -08:00
const loaded = await storage . loadVersion ( version )
2025-12-09 09:35:45 -08:00
expect ( mockBrain . storageAdapter . getMetadata ) . toHaveBeenCalled ( )
2025-11-04 11:19:02 -08:00
expect ( loaded ) . toEqual ( entity )
} )
it ( 'should throw if storage adapter missing' , async ( ) = > {
mockBrain . storageAdapter = null
storage = new VersionStorage ( mockBrain )
const entity : NounMetadata = {
id : 'test-1' ,
type : 'thing' ,
name : 'Test' ,
metadata : { }
}
const version : EntityVersion = {
version : 1 ,
entityId : 'test-1' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity )
}
await expect (
storage . saveVersion ( version , entity )
) . rejects . toThrow ( 'Storage adapter not available' )
} )
it ( 'should throw if adapter does not support required operations' , async ( ) = > {
2025-12-09 09:35:45 -08:00
mockBrain . storageAdapter = { } // No methods (no saveMetadata)
2025-11-04 11:19:02 -08:00
storage = new VersionStorage ( mockBrain )
const entity : NounMetadata = {
id : 'test-1' ,
type : 'thing' ,
name : 'Test' ,
metadata : { }
}
const version : EntityVersion = {
version : 1 ,
entityId : 'test-1' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash : storage.hashEntity ( entity )
}
2025-12-09 09:35:45 -08:00
// v6.3.0: Error from calling undefined saveMetadata
2025-11-04 11:19:02 -08:00
await expect (
storage . saveVersion ( version , entity )
2025-12-09 09:35:45 -08:00
) . rejects . toThrow ( ) // TypeError: adapter.saveMetadata is not a function
2025-11-04 11:19:02 -08:00
} )
} )
2025-12-09 09:35:45 -08:00
describe ( 'Key Generation' , ( ) = > {
it ( 'should generate correct storage keys' , async ( ) = > {
2025-11-04 11:19:02 -08:00
const entity : NounMetadata = {
id : 'user-123' ,
type : 'user' ,
name : 'Alice' ,
metadata : { }
}
const contentHash = storage . hashEntity ( entity )
const version : EntityVersion = {
version : 1 ,
entityId : 'user-123' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash
}
await storage . saveVersion ( version , entity )
2025-12-09 09:35:45 -08:00
// v6.3.0: Uses __system_version_ prefix for saveMetadata keys
const expectedKey = ` __system_version_user-123_ ${ contentHash } `
expect ( mockFiles . has ( expectedKey ) ) . toBe ( true )
2025-11-04 11:19:02 -08:00
} )
it ( 'should handle entity IDs with special characters' , async ( ) = > {
const entity : NounMetadata = {
id : 'user:123:profile' ,
type : 'user' ,
name : 'Alice' ,
metadata : { }
}
const contentHash = storage . hashEntity ( entity )
const version : EntityVersion = {
version : 1 ,
entityId : 'user:123:profile' ,
branch : 'main' ,
commitHash : 'commit-123' ,
timestamp : Date.now ( ) ,
contentHash
}
await storage . saveVersion ( version , entity )
2025-12-09 09:35:45 -08:00
// v6.3.0: Uses __system_version_ prefix for saveMetadata keys
const expectedKey = ` __system_version_user:123:profile_ ${ contentHash } `
expect ( mockFiles . has ( expectedKey ) ) . toBe ( true )
2025-11-04 11:19:02 -08:00
} )
} )
} )