rhizome-node/__tests__/compose-decompose.ts
Lentil Hoffman 60ad920b30
refactor: update test files to use DeltaBuilder fluent API
- Refactored delta creation in test files to use createDelta() pattern
- Replaced direct Delta instantiations with fluent builder API
- Updated relationship deltas to use setProperty with proper entity context
- Ensured all tests pass with the new delta creation approach

This is part of the ongoing effort to standardize on the DeltaBuilder
API across the codebase for better consistency and maintainability.
2025-06-20 21:40:51 -05:00

206 lines
7.6 KiB
TypeScript

/**
* Tests for lossless view compose() and decompose() bidirectional conversion
* Ensures that deltas can be composed into lossless views and decomposed back
* to the original deltas with all pointer relationships preserved.
*/
import { RhizomeNode } from '../src/node';
import { createDelta } from '../src/core/delta-builder';
describe('Lossless View Compose/Decompose', () => {
let node: RhizomeNode;
beforeEach(() => {
node = new RhizomeNode();
});
describe('Bidirectional Conversion', () => {
it('should compose and decompose simple entity deltas correctly', () => {
// Create simple entity deltas
const nameDeltas = [
createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'name')
.addPointer('name', 'Alice Smith')
.buildV1(),
createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'email')
.addPointer('email', 'alice@example.com')
.buildV1()
];
// Ingest the deltas
nameDeltas.forEach(delta => node.lossless.ingestDelta(delta));
// Compose lossless view
const composed = node.lossless.compose(['alice']);
const aliceView = composed['alice'];
expect(aliceView).toBeDefined();
expect(aliceView.id).toBe('alice');
expect(aliceView.propertyDeltas.name).toHaveLength(1);
expect(aliceView.propertyDeltas.email).toHaveLength(1);
// Decompose back to deltas
const decomposed = node.lossless.decompose(aliceView);
expect(decomposed).toHaveLength(2);
// Check that original deltas are preserved
const originalIds = nameDeltas.map(d => d.id).sort();
const decomposedIds = decomposed.map(d => d.id).sort();
expect(decomposedIds).toEqual(originalIds);
// Verify pointer structure is preserved
const nameDataDelta = decomposed.find(d =>
d.pointers.some(p => p.localContext === 'name' && p.target === 'Alice Smith')
);
expect(nameDataDelta).toBeDefined();
expect(nameDataDelta?.pointers).toHaveLength(2);
const upPointer = nameDataDelta?.pointers.find(p => p.targetContext === 'name');
expect(upPointer).toBeDefined();
expect(upPointer?.target).toBe('alice');
expect(upPointer?.localContext).toBe('users');
});
it('should handle multi-pointer relationship deltas correctly', () => {
// Create a complex relationship delta
const relationshipDelta = createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'relationships')
.addPointer('partner', 'bob')
.addPointer('type', 'friendship')
.addPointer('since', '2020-01-15')
.addPointer('intensity', 8)
.buildV1();
node.lossless.ingestDelta(relationshipDelta);
// Compose and decompose
const composed = node.lossless.compose(['alice']);
const aliceView = composed['alice'];
const decomposed = node.lossless.decompose(aliceView);
expect(decomposed).toHaveLength(1);
const reconstituted = decomposed[0];
// Should have all 5 pointers
expect(reconstituted.pointers).toHaveLength(5);
// Check that all pointer types are preserved
const contexts = reconstituted.pointers.map(p => p.localContext).sort();
expect(contexts).toEqual(['users', 'partner', 'type', 'since', 'intensity'].sort());
// Check that the "up" pointer to alice is correctly reconstructed
const upPointer = reconstituted.pointers.find(p => p.targetContext === 'relationships');
expect(upPointer).toBeDefined();
expect(upPointer?.target).toBe('alice');
expect(upPointer?.localContext).toBe('users');
// Check scalar values are preserved
const intensityPointer = reconstituted.pointers.find(p => p.localContext === 'intensity');
expect(intensityPointer?.target).toBe(8);
});
it('should handle reference relationships correctly', () => {
// Create entities first
const aliceDelta = createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'name')
.addPointer('name', 'Alice')
.buildV1();
const bobDelta = createDelta('test-creator', 'test-host')
.addPointer('users', 'bob', 'name')
.addPointer('name', 'Bob')
.buildV1();
// Create friendship relationship
const friendshipDelta = createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'friends')
.addPointer('friend', 'bob', 'friends')
.buildV1();
[aliceDelta, bobDelta, friendshipDelta].forEach(d => node.lossless.ingestDelta(d));
// Compose Alice's view
const composed = node.lossless.compose(['alice']);
const aliceView = composed['alice'];
expect(aliceView.propertyDeltas.friends).toHaveLength(1);
// Decompose and verify the friendship delta is correctly reconstructed
const decomposed = node.lossless.decompose(aliceView);
const friendshipReconstituted = decomposed.find(d =>
d.pointers.some(p => p.localContext === 'friend')
);
expect(friendshipReconstituted).toBeDefined();
expect(friendshipReconstituted?.pointers).toHaveLength(2);
// Check both reference pointers are preserved
const alicePointer = friendshipReconstituted?.pointers.find(p => p.target === 'alice');
const bobPointer = friendshipReconstituted?.pointers.find(p => p.target === 'bob');
expect(alicePointer).toBeDefined();
expect(alicePointer?.targetContext).toBe('friends');
expect(bobPointer).toBeDefined();
expect(bobPointer?.targetContext).toBe('friends');
});
it('should preserve delta metadata correctly', () => {
const originalDelta = createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'name')
.addPointer('name', 'Alice')
.buildV1();
node.lossless.ingestDelta(originalDelta);
const composed = node.lossless.compose(['alice']);
const decomposed = node.lossless.decompose(composed['alice']);
expect(decomposed).toHaveLength(1);
const reconstituted = decomposed[0];
// Check metadata preservation
expect(reconstituted.id).toBe(originalDelta.id);
expect(reconstituted.creator).toBe(originalDelta.creator);
expect(reconstituted.host).toBe(originalDelta.host);
expect(reconstituted.timeCreated).toBe(originalDelta.timeCreated);
});
it('should handle multiple deltas for the same property', () => {
// Create multiple name changes for alice
const nameDeltas = [
createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'name')
.addPointer('name', 'Alice')
.buildV1(),
createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'name')
.addPointer('name', 'Alice Smith')
.buildV1(),
createDelta('test-creator', 'test-host')
.addPointer('users', 'alice', 'name')
.addPointer('name', 'Alice Johnson')
.buildV1()
];
nameDeltas.forEach(d => node.lossless.ingestDelta(d));
const composed = node.lossless.compose(['alice']);
const aliceView = composed['alice'];
// Should have 3 deltas for the name property
expect(aliceView.propertyDeltas.name).toHaveLength(3);
const decomposed = node.lossless.decompose(aliceView);
// Should decompose back to 3 separate deltas
expect(decomposed).toHaveLength(3);
// All original delta IDs should be preserved
const originalIds = nameDeltas.map(d => d.id).sort();
const decomposedIds = decomposed.map(d => d.id).sort();
expect(decomposedIds).toEqual(originalIds);
});
});
});