rhizome-node/__tests__/compose-decompose.ts

247 lines
8.7 KiB
TypeScript

import * as _RhizomeImports from "../src";
/**
* 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 { Delta } from '../src/core';
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 = [
new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'name' },
{ localContext: 'name', target: 'Alice Smith' }
]
}),
new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'email' },
{ localContext: 'email', target: 'alice@example.com' }
]
})
];
// 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 = new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'relationships' },
{ localContext: 'partner', target: 'bob' },
{ localContext: 'type', target: 'friendship' },
{ localContext: 'since', target: '2020-01-15' },
{ localContext: 'intensity', target: 8 }
]
});
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 = new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'name' },
{ localContext: 'name', target: 'Alice' }
]
});
const bobDelta = new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'bob', targetContext: 'name' },
{ localContext: 'name', target: 'Bob' }
]
});
// Create friendship relationship
const friendshipDelta = new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'friends' },
{ localContext: 'friend', target: 'bob', targetContext: 'friends' }
]
});
[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 = new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'name' },
{ localContext: 'name', target: 'Alice' }
]
});
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 = [
new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'name' },
{ localContext: 'name', target: 'Alice' }
]
}),
new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'name' },
{ localContext: 'name', target: 'Alice Smith' }
]
}),
new Delta({
creator: 'test-creator',
host: 'test-host',
pointers: [
{ localContext: 'users', target: 'alice', targetContext: 'name' },
{ localContext: 'name', target: 'Alice Johnson' }
]
})
];
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);
});
});
});