Core Changes: - Completely rewrote CustomResolver reducer with dependency-ordered processing - Enhanced plugin initialization with proper dependency injection - Improved delta processing and property value tracking - Added robust error handling for duplicate property IDs Resolver Improvements: - Updated to use new accumulator structure - Implemented execution order processing for plugins - Enhanced debug logging and error reporting - Simplified TimestampResolver by removing unused initializer Configuration Updates: - Added TypeScript path aliases for test helpers - Improved module resolution paths Key Benefits: - More robust plugin dependency management - More efficient state updates - Enhanced type safety - Better error messages and debugging - More consistent plugin initialization This refactoring focuses on improving the robustness of the resolver, especially around plugin lifecycle management and dependency handling. The changes ensure better separation of concerns and more predictable behavior when dealing with complex plugin dependencies.
507 lines
18 KiB
TypeScript
507 lines
18 KiB
TypeScript
import Debug from 'debug';
|
|
import { createDelta } from '@src/core/delta-builder';
|
|
import { NegationHelper } from '@src/features';
|
|
import { RhizomeNode } from '@src/node';
|
|
import { Lossless } from '@src/views';
|
|
|
|
const debug = Debug('rz:negation:test');
|
|
|
|
describe('Negation System', () => {
|
|
let node: RhizomeNode;
|
|
let lossless: Lossless;
|
|
|
|
beforeEach(() => {
|
|
node = new RhizomeNode();
|
|
lossless = new Lossless(node);
|
|
});
|
|
|
|
describe('Negation Helper', () => {
|
|
test('should create negation deltas correctly', () => {
|
|
const originalDelta = createDelta('user1', 'host1')
|
|
.setProperty('entity1', 'name', 'Alice')
|
|
.buildV1();
|
|
|
|
const negationDelta = createDelta('moderator', 'host1')
|
|
.negate(originalDelta.id)
|
|
.buildV1();
|
|
|
|
expect(negationDelta.creator).toBe('moderator');
|
|
expect(negationDelta.pointers).toHaveLength(1);
|
|
expect(negationDelta.pointers[0]).toEqual({
|
|
localContext: '_negates',
|
|
target: originalDelta.id,
|
|
targetContext: 'negated_by'
|
|
});
|
|
expect(NegationHelper.isNegationDelta(negationDelta)).toBe(true);
|
|
});
|
|
|
|
test('should identify negation deltas', () => {
|
|
const regularDelta = createDelta('user1', 'host1')
|
|
.setProperty('entity1', 'name', 'Entity 1')
|
|
.buildV1();
|
|
|
|
const negationDelta = createDelta('moderator', 'host1')
|
|
.negate('delta-to-negate')
|
|
.buildV1();
|
|
|
|
expect(NegationHelper.isNegationDelta(regularDelta)).toBe(false);
|
|
expect(NegationHelper.isNegationDelta(negationDelta)).toBe(true);
|
|
});
|
|
|
|
test('should extract negated delta ID', () => {
|
|
const targetDeltaId = 'target-delta-123';
|
|
const negationDelta = createDelta('moderator', 'host1')
|
|
.negate(targetDeltaId)
|
|
.buildV1();
|
|
|
|
const extractedId = NegationHelper.getNegatedDeltaId(negationDelta);
|
|
expect(extractedId).toBe(targetDeltaId);
|
|
|
|
const regularDelta = createDelta('user1', 'host1')
|
|
.setProperty('entity1', 'name', 'Entity 1')
|
|
.buildV1();
|
|
|
|
expect(NegationHelper.getNegatedDeltaId(regularDelta)).toBeNull();
|
|
});
|
|
|
|
test('should find negations for specific deltas', () => {
|
|
const delta1 = createDelta('user1', 'host1')
|
|
.setProperty('entity1', 'name', 'Entity 1')
|
|
.buildV1();
|
|
|
|
const delta2 = createDelta('user2', 'host1')
|
|
.setProperty('entity1', 'age', 25)
|
|
.buildV1();
|
|
|
|
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
|
|
const negation2 = createDelta('mod2', 'host1').negate(delta1.id).buildV1();
|
|
const negation3 = createDelta('mod1', 'host1').negate(delta2.id).buildV1();
|
|
|
|
const allDeltas = [delta1, delta2, negation1, negation2, negation3];
|
|
|
|
const negationsForDelta1 = NegationHelper.findNegationsFor(delta1.id, allDeltas);
|
|
expect(negationsForDelta1).toHaveLength(2);
|
|
expect(negationsForDelta1.map(d => d.id)).toContain(negation1.id);
|
|
expect(negationsForDelta1.map(d => d.id)).toContain(negation2.id);
|
|
|
|
const negationsForDelta2 = NegationHelper.findNegationsFor(delta2.id, allDeltas);
|
|
expect(negationsForDelta2).toHaveLength(1);
|
|
expect(negationsForDelta2[0].id).toBe(negation3.id);
|
|
});
|
|
|
|
test('should check if deltas are negated', () => {
|
|
const delta1 = createDelta('user1', 'host1')
|
|
.setProperty('entity1', 'name', 'Entity 1')
|
|
.buildV1();
|
|
|
|
const delta2 = createDelta('user2', 'host1')
|
|
.setProperty('entity1', 'age', 25)
|
|
.buildV1();
|
|
|
|
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
|
|
const allDeltas = [delta1, delta2, negation1];
|
|
|
|
expect(NegationHelper.isDeltaNegated(delta1.id, allDeltas)).toBe(true);
|
|
expect(NegationHelper.isDeltaNegated(delta2.id, allDeltas)).toBe(false);
|
|
});
|
|
|
|
test('should filter out negated deltas', () => {
|
|
const delta1 = createDelta('user1', 'host1')
|
|
.setProperty('entity1', 'name', 'Entity 1')
|
|
.buildV1();
|
|
|
|
const delta2 = createDelta('user2', 'host1')
|
|
.setProperty('entity1', 'age', 25)
|
|
.buildV1();
|
|
|
|
const delta3 = createDelta('user3', 'host1')
|
|
.setProperty('entity1', 'email', 'entity1@example.com')
|
|
.buildV1();
|
|
|
|
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
|
|
const negation2 = createDelta('mod2', 'host1').negate(delta2.id).buildV1();
|
|
|
|
const allDeltas = [delta1, delta2, delta3, negation1, negation2];
|
|
const filtered = NegationHelper.filterNegatedDeltas(allDeltas);
|
|
|
|
// Should only include delta3 (delta1 and delta2 are negated, negations themselves are filtered)
|
|
expect(filtered).toHaveLength(1);
|
|
expect(filtered[0].id).toBe(delta3.id);
|
|
});
|
|
|
|
test('should provide negation statistics', () => {
|
|
const delta1 = createDelta('user1', 'host1')
|
|
.setProperty('entity1', 'name', 'Entity 1')
|
|
.buildV1();
|
|
|
|
const delta2 = createDelta('user2', 'host1')
|
|
.setProperty('entity1', 'age', 25)
|
|
.buildV1();
|
|
|
|
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
|
|
const allDeltas = [delta1, delta2, negation1];
|
|
|
|
const stats = NegationHelper.getNegationStats(allDeltas);
|
|
|
|
expect(stats.totalDeltas).toBe(3);
|
|
expect(stats.negationDeltas).toBe(1);
|
|
expect(stats.negatedDeltas).toBe(1);
|
|
expect(stats.effectiveDeltas).toBe(1); // only delta2 is effective
|
|
expect(stats.negatedDeltaIds).toContain(delta1.id);
|
|
expect(stats.negationMap.get(delta1.id)).toContain(negation1.id);
|
|
});
|
|
|
|
test('should apply negations chronologically', () => {
|
|
const baseTime = Date.now();
|
|
|
|
// Create deltas with specific timestamps
|
|
const delta1 = createDelta('user1', 'host1')
|
|
.withTimestamp(baseTime)
|
|
.setProperty('entity1', 'status', 'active')
|
|
.buildV1();
|
|
|
|
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
|
|
negation1.timeCreated = baseTime + 1000; // 1 second later
|
|
|
|
const delta2 = createDelta('user1', 'host1')
|
|
.withTimestamp(baseTime + 2000)
|
|
.setProperty('entity1', 'status', 'inactive')
|
|
.buildV1();
|
|
|
|
const negation2 = createDelta('mod1', 'host1').negate(delta2.id).buildV1();
|
|
negation2.timeCreated = baseTime + 3000; // 3 seconds later
|
|
|
|
const allDeltas = [delta1, negation1, delta2, negation2];
|
|
const filtered = NegationHelper.applyNegationsChronologically(allDeltas);
|
|
|
|
// Both deltas should be negated
|
|
expect(filtered).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('Lossless View Integration', () => {
|
|
test('should filter negated deltas in lossless views', () => {
|
|
// Create original delta
|
|
const originalDelta = createDelta('user1', 'host1')
|
|
.setProperty('user123', 'name', 'Alice')
|
|
.buildV1();
|
|
|
|
// Create negation delta
|
|
const negationDelta = createDelta('moderator', 'host1')
|
|
.negate(originalDelta.id)
|
|
.buildV1();
|
|
|
|
|
|
// Create another non-negated delta
|
|
const nonNegatedDelta = createDelta('user2', 'host1')
|
|
.setProperty('user123', 'age', 25)
|
|
.buildV1();
|
|
|
|
// Ingest all deltas
|
|
lossless.ingestDelta(originalDelta);
|
|
lossless.ingestDelta(negationDelta);
|
|
lossless.ingestDelta(nonNegatedDelta);
|
|
|
|
// Get view - should only show non-negated delta
|
|
const view = lossless.compose(['user123']);
|
|
|
|
expect(view.user123).toBeDefined();
|
|
|
|
// Should only have age property (name was negated)
|
|
expect(view.user123.propertyDeltas.age).toHaveLength(1);
|
|
expect(view.user123.propertyDeltas.name).toBeUndefined();
|
|
});
|
|
|
|
test('should handle multiple negations of the same delta', () => {
|
|
const originalDelta = createDelta('user1', 'host1')
|
|
.setProperty('post1', 'content', 'Original content')
|
|
.buildV1();
|
|
|
|
const negation1 = createDelta('mod1', 'host1').negate(originalDelta.id).buildV1();
|
|
const negation2 = createDelta('mod2', 'host1').negate(originalDelta.id).buildV1();
|
|
|
|
lossless.ingestDelta(originalDelta);
|
|
lossless.ingestDelta(negation1);
|
|
lossless.ingestDelta(negation2);
|
|
|
|
const view = lossless.compose(['post1']);
|
|
|
|
// Original delta should be negated (not visible)
|
|
expect(view.post1).toBeUndefined();
|
|
});
|
|
|
|
test('should provide negation statistics for entities', () => {
|
|
const delta1 = createDelta('user1', 'host1')
|
|
.setProperty('article1', 'title', 'Original Title')
|
|
.buildV1();
|
|
|
|
const delta2 = createDelta('user2', 'host1')
|
|
.setProperty('article1', 'content', 'Article content')
|
|
.buildV1();
|
|
|
|
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
|
|
|
|
lossless.ingestDelta(delta1);
|
|
lossless.ingestDelta(delta2);
|
|
lossless.ingestDelta(negation1);
|
|
|
|
const stats = lossless.getNegationStats('article1');
|
|
|
|
expect(stats.totalDeltas).toBe(3);
|
|
expect(stats.negationDeltas).toBe(1);
|
|
expect(stats.negatedDeltas).toBe(1);
|
|
expect(stats.effectiveDeltas).toBe(1);
|
|
expect(stats.negationsByProperty.title.negated).toBe(1);
|
|
expect(stats.negationsByProperty.content.negated).toBe(0);
|
|
});
|
|
|
|
test('should retrieve negation deltas for entities', () => {
|
|
const originalDelta = createDelta('user1', 'host1')
|
|
.setProperty('task1', 'status', 'pending')
|
|
.buildV1();
|
|
|
|
const negationDelta = createDelta('admin', 'host1').negate(originalDelta.id).buildV1();
|
|
|
|
lossless.ingestDelta(originalDelta);
|
|
lossless.ingestDelta(negationDelta);
|
|
|
|
const negations = lossless.getNegationDeltas('task1');
|
|
expect(negations).toHaveLength(1);
|
|
expect(negations[0].id).toBe(negationDelta.id);
|
|
expect(negations[0].creator).toBe('admin');
|
|
});
|
|
|
|
test('should handle negation in transactions', () => {
|
|
const transactionId = 'tx-negation';
|
|
|
|
// Create transaction declaration
|
|
lossless.ingestDelta(createDelta('system', 'host1')
|
|
.declareTransaction(transactionId, 2)
|
|
.buildV1()
|
|
);
|
|
|
|
// Create original delta in transaction
|
|
const originalDelta = createDelta('user1', 'host1')
|
|
.declareTransaction(transactionId, 2)
|
|
.setProperty('post1', 'comments', 'Inappropriate comment')
|
|
.buildV1();
|
|
|
|
// Create negation delta in same transaction
|
|
const negationDelta = createDelta('moderator', 'host1').negate(originalDelta.id).buildV1();
|
|
negationDelta.pointers.unshift({
|
|
localContext: '_transaction',
|
|
target: transactionId,
|
|
targetContext: 'deltas'
|
|
});
|
|
|
|
lossless.ingestDelta(originalDelta);
|
|
lossless.ingestDelta(negationDelta);
|
|
|
|
// Transaction should complete, but original delta should be negated
|
|
const view = lossless.compose(['post1']);
|
|
expect(view.post1).toBeUndefined(); // No visible deltas
|
|
});
|
|
|
|
test('should handle chronological negation scenarios', () => {
|
|
const baseTime = Date.now();
|
|
|
|
// User posts content
|
|
const postDelta = createDelta('user1', 'host1')
|
|
.withTimestamp(baseTime)
|
|
.setProperty('post1', 'content', 'Original post')
|
|
.buildV1();
|
|
|
|
// Moderator negates it
|
|
const negationDelta = createDelta('moderator', 'host1').negate(postDelta.id).buildV1();
|
|
negationDelta.timeCreated = baseTime + 1000;
|
|
|
|
// User edits content (after negation)
|
|
const editDelta = createDelta('user1', 'host1')
|
|
.withTimestamp(baseTime + 2000)
|
|
.setProperty('post1', 'content', 'Edited post')
|
|
.buildV1();
|
|
|
|
lossless.ingestDelta(postDelta);
|
|
lossless.ingestDelta(negationDelta);
|
|
lossless.ingestDelta(editDelta);
|
|
|
|
const view = lossless.compose(['post1']);
|
|
|
|
// Should show edited content (edit happened after negation)
|
|
expect(view.post1).toBeDefined();
|
|
expect(view.post1.propertyDeltas.content).toHaveLength(1);
|
|
|
|
// The visible delta should be the edit delta
|
|
const visibleDelta = view.post1.propertyDeltas.content[0];
|
|
expect(visibleDelta.id).toBe(editDelta.id);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
test('should handle negation of non-existent deltas', () => {
|
|
const negationDelta = createDelta('moderator', 'host1').negate('non-existent-delta-id').buildV1();
|
|
|
|
lossless.ingestDelta(negationDelta);
|
|
|
|
// Should not crash and stats should reflect the orphaned negation
|
|
const stats = lossless.getNegationStats('entity1');
|
|
expect(stats.negationDeltas).toBe(0); // No negations for this entity
|
|
});
|
|
|
|
test('should handle self-referential entities in negations', () => {
|
|
// Create a delta that references itself
|
|
const selfRefDelta = createDelta('user1', 'host1')
|
|
.setProperty('node1', 'parent', 'node1')
|
|
.setProperty('node1', 'child', 'node1') // Self-reference
|
|
.buildV1();
|
|
|
|
const negationDelta = createDelta('admin', 'host1').negate(selfRefDelta.id).buildV1();
|
|
|
|
lossless.ingestDelta(selfRefDelta);
|
|
lossless.ingestDelta(negationDelta);
|
|
|
|
const view = lossless.compose(['node1']);
|
|
expect(view.node1).toBeUndefined(); // Should be negated
|
|
});
|
|
|
|
test('should handle multiple direct negations of the same delta', () => {
|
|
const testNode = new RhizomeNode();
|
|
const testLossless = new Lossless(testNode);
|
|
|
|
// Create the original delta
|
|
const originalDelta = createDelta('user1', 'host1')
|
|
.setProperty('entity2', 'title', 'Draft')
|
|
.buildV1();
|
|
|
|
// Create two negations of the same delta
|
|
const negation1 = createDelta('user2', 'host1').negate(originalDelta.id).buildV1();
|
|
const negation2 = createDelta('user3', 'host1').negate(originalDelta.id).buildV1();
|
|
|
|
// Process all deltas
|
|
testLossless.ingestDelta(originalDelta);
|
|
testLossless.ingestDelta(negation1);
|
|
testLossless.ingestDelta(negation2);
|
|
|
|
// Get the view after processing all deltas
|
|
const view = testLossless.compose(['entity2']);
|
|
|
|
// The original delta should be negated (not in view) because it has two direct negations
|
|
expect(view.entity2).toBeUndefined();
|
|
|
|
// Verify the stats
|
|
const stats = testLossless.getNegationStats('entity2');
|
|
expect(stats.negationDeltas).toBe(2);
|
|
expect(stats.negatedDeltas).toBe(1);
|
|
expect(stats.effectiveDeltas).toBe(0);
|
|
});
|
|
|
|
test('should handle complex negation chains', () => {
|
|
const testNode = new RhizomeNode();
|
|
const testLossless = new Lossless(testNode);
|
|
|
|
// Create the original delta
|
|
const deltaA = createDelta('user1', 'host1')
|
|
.setProperty('entity3', 'content', 'Hello World')
|
|
.buildV1();
|
|
|
|
// Create a chain of negations: B negates A, C negates B, D negates C
|
|
const deltaB = createDelta('user2', 'host1').negate(deltaA.id).buildV1();
|
|
const deltaC = createDelta('user3', 'host1').negate(deltaB.id).buildV1();
|
|
const deltaD = createDelta('user4', 'host1').negate(deltaC.id).buildV1();
|
|
|
|
debug('Delta A (original): %s', deltaA.id);
|
|
debug('Delta B (negates A): %s', deltaB.id);
|
|
debug('Delta C (negates B): %s', deltaC.id);
|
|
debug('Delta D (negates C): %s', deltaD.id);
|
|
|
|
// Process all deltas in order
|
|
testLossless.ingestDelta(deltaA);
|
|
testLossless.ingestDelta(deltaB);
|
|
testLossless.ingestDelta(deltaC);
|
|
testLossless.ingestDelta(deltaD);
|
|
|
|
// Get the view after processing all deltas
|
|
const view = testLossless.compose(['entity3']);
|
|
|
|
// The original delta should be negated because:
|
|
// - B negates A
|
|
// - C negates B (so A is no longer negated)
|
|
// - D negates C (so B is no longer negated, and A is negated again by B)
|
|
expect(view.entity3).toBeUndefined();
|
|
|
|
// Get all deltas for the entity
|
|
const allDeltas = [deltaA, deltaB, deltaC, deltaD];
|
|
|
|
// Get the stats
|
|
const stats = testLossless.getNegationStats('entity3');
|
|
const isANegated = NegationHelper.isDeltaNegated(deltaA.id, allDeltas);
|
|
const isBNegated = NegationHelper.isDeltaNegated(deltaB.id, allDeltas);
|
|
const isCNegated = NegationHelper.isDeltaNegated(deltaC.id, allDeltas);
|
|
const isDNegated = NegationHelper.isDeltaNegated(deltaD.id, allDeltas);
|
|
|
|
debug('Delta statuses:');
|
|
debug('- A (%s): %s', deltaA.id, isANegated ? 'NEGATED' : 'ACTIVE');
|
|
debug('- B (%s): %s, negates: %s', deltaB.id, isBNegated ? 'NEGATED' : 'ACTIVE', NegationHelper.getNegatedDeltaId(deltaB));
|
|
debug('- C (%s): %s, negates: %s', deltaC.id, isCNegated ? 'NEGATED' : 'ACTIVE', NegationHelper.getNegatedDeltaId(deltaC));
|
|
debug('- D (%s): %s, negates: %s', deltaD.id, isDNegated ? 'NEGATED' : 'ACTIVE', NegationHelper.getNegatedDeltaId(deltaD));
|
|
|
|
debug('Negation stats: %O', {
|
|
totalDeltas: stats.totalDeltas,
|
|
negationDeltas: stats.negationDeltas,
|
|
negatedDeltas: stats.negatedDeltas,
|
|
effectiveDeltas: stats.effectiveDeltas,
|
|
negationsByProperty: stats.negationsByProperty
|
|
});
|
|
|
|
// B, C, D are negation deltas
|
|
expect(stats.negationDeltas).toBe(3);
|
|
|
|
// A and C are effectively negated
|
|
expect(isANegated).toBe(true);
|
|
expect(isCNegated).toBe(true);
|
|
|
|
// B and D are not negated (they are negation deltas that are not themselves negated)
|
|
expect(isBNegated).toBe(false);
|
|
expect(isDNegated).toBe(false);
|
|
|
|
// No deltas remain unnegated
|
|
expect(stats.effectiveDeltas).toBe(0);
|
|
});
|
|
|
|
test('should handle multiple independent negations', () => {
|
|
const testNode = new RhizomeNode();
|
|
const testLossless = new Lossless(testNode);
|
|
|
|
// Create two independent deltas
|
|
const delta1 = createDelta('user1', 'host1')
|
|
.setProperty('entity4', 'item', 'Item 1')
|
|
.buildV1();
|
|
|
|
const delta2 = createDelta('user2', 'host1')
|
|
.setProperty('entity4', 'item', 'Item 2')
|
|
.buildV1();
|
|
|
|
// Create negations for both deltas
|
|
const negation1 = createDelta('user3', 'host1').negate(delta1.id).buildV1();
|
|
const negation2 = createDelta('user4', 'host1').negate(delta2.id).buildV1();
|
|
|
|
// Process all deltas
|
|
testLossless.ingestDelta(delta1);
|
|
testLossless.ingestDelta(delta2);
|
|
testLossless.ingestDelta(negation1);
|
|
testLossless.ingestDelta(negation2);
|
|
|
|
// Get the view after processing all deltas
|
|
const view = testLossless.compose(['entity4']);
|
|
|
|
// Both deltas should be negated
|
|
expect(view.entity4).toBeUndefined();
|
|
|
|
// Verify the stats
|
|
const stats = testLossless.getNegationStats('entity4');
|
|
expect(stats.negationDeltas).toBe(2);
|
|
expect(stats.negatedDeltas).toBe(2);
|
|
expect(stats.effectiveDeltas).toBe(0);
|
|
});
|
|
});
|
|
}); |