rhizome-node/__tests__/unit/views/multi-pointer-resolution.test.ts
Lentil Hoffman d7c4fda93e
refactor(resolver): overhaul plugin system and dependency handling
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.
2025-06-25 06:10:34 -05:00

295 lines
12 KiB
TypeScript

/**
* Tests for nested object resolution with deltas containing 3+ pointers
* This tests the complex case where a single delta establishes relationships
* between multiple entities and includes scalar values.
*/
import { RhizomeNode } from '@src/node';
import { createDelta } from '@src/core/delta-builder';
import { DefaultSchemaRegistry } from '@src/schema';
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '@src/schema';
import { TypedCollectionImpl } from '@src/collections';
describe('Multi-Pointer Delta Resolution', () => {
let node: RhizomeNode;
let schemaRegistry: DefaultSchemaRegistry;
beforeEach(() => {
node = new RhizomeNode();
schemaRegistry = new DefaultSchemaRegistry();
});
describe('Three-Entity Relationship Deltas', () => {
test('should handle movie casting deltas with actor, movie, role, and scalars', async () => {
// Create schemas for a movie casting scenario
const actorSchema = SchemaBuilder
.create('actor')
.name('Actor')
.property('name', PrimitiveSchemas.requiredString())
.property('filmography', ReferenceSchemas.to('casting-summary', 3))
.required('name')
.build();
const movieSchema = SchemaBuilder
.create('movie')
.name('Movie')
.property('title', PrimitiveSchemas.requiredString())
.property('cast', ReferenceSchemas.to('casting-summary', 3))
.required('title')
.build();
const roleSchema = SchemaBuilder
.create('role')
.name('Role')
.property('name', PrimitiveSchemas.requiredString())
.property('portrayals', ReferenceSchemas.to('casting-summary', 3))
.required('name')
.build();
const castingSummarySchema = SchemaBuilder
.create('casting-summary')
.name('Casting Summary')
.property('name', PrimitiveSchemas.string())
.property('title', PrimitiveSchemas.string())
.property('salary', PrimitiveSchemas.number())
.property('contract_date', PrimitiveSchemas.string())
.additionalProperties(false)
.build();
schemaRegistry.register(actorSchema);
schemaRegistry.register(movieSchema);
schemaRegistry.register(roleSchema);
schemaRegistry.register(castingSummarySchema);
// Create collections
const actorCollection = new TypedCollectionImpl<{ name: string }>('actors', actorSchema, schemaRegistry);
const movieCollection = new TypedCollectionImpl<{ title: string }>('movies', movieSchema, schemaRegistry);
const roleCollection = new TypedCollectionImpl<{ name: string }>('roles', roleSchema, schemaRegistry);
actorCollection.rhizomeConnect(node);
movieCollection.rhizomeConnect(node);
roleCollection.rhizomeConnect(node);
// Create entities
await actorCollection.put('keanu', { name: 'Keanu Reeves' });
await movieCollection.put('matrix', { title: 'The Matrix' });
await roleCollection.put('neo', { name: 'Neo' });
// Create a complex casting delta with multiple entity references and scalar values
const castingDelta = createDelta(node.config.creator, node.config.peerId)
.addPointer('actors', 'keanu', 'filmography')
.addPointer('movies', 'matrix', 'cast')
.addPointer('roles', 'neo', 'portrayals')
.addPointer('salary', 15000000)
.addPointer('contract_date', '1999-03-31')
.buildV1();
node.lossless.ingestDelta(castingDelta);
// Test from Keanu's perspective
const keanuViews = node.lossless.compose(['keanu']);
const keanuView = keanuViews['keanu'];
expect(keanuView.propertyDeltas.filmography).toBeDefined();
expect(keanuView.propertyDeltas.filmography.length).toBe(1);
const nestedKeanuView = schemaRegistry.applySchemaWithNesting(
keanuView,
'actor',
node.lossless,
{ maxDepth: 2 }
);
expect(nestedKeanuView.id).toBe('keanu');
// Should resolve references to matrix and neo, but not keanu (self)
expect(nestedKeanuView.nestedObjects.filmography).toBeDefined();
if (nestedKeanuView.nestedObjects.filmography) {
const castingEntry = nestedKeanuView.nestedObjects.filmography[0];
expect(castingEntry).toBeDefined();
// The casting entry should be resolved with casting-summary schema
expect(castingEntry.schemaId).toBe('casting-summary');
// Should not contain a reference to keanu (the parent)
expect(castingEntry.id).not.toBe('keanu');
}
// Test from Matrix's perspective
const matrixViews = node.lossless.compose(['matrix']);
const matrixView = matrixViews['matrix'];
const nestedMatrixView = schemaRegistry.applySchemaWithNesting(
matrixView,
'movie',
node.lossless,
{ maxDepth: 2 }
);
expect(nestedMatrixView.id).toBe('matrix');
expect(nestedMatrixView.nestedObjects.cast).toBeDefined();
});
test('should handle deltas with mixed scalar and reference values correctly', async () => {
// Create a simpler schema for testing mixed values
const personSchema = SchemaBuilder
.create('person')
.name('Person')
.property('name', PrimitiveSchemas.requiredString())
.property('relationships', ReferenceSchemas.to('relationship-summary', 3))
.required('name')
.build();
const relationshipSummarySchema = SchemaBuilder
.create('relationship-summary')
.name('Relationship Summary')
.property('partner_name', PrimitiveSchemas.string())
.property('type', PrimitiveSchemas.string())
.property('since', PrimitiveSchemas.string())
.property('intensity', PrimitiveSchemas.number())
.additionalProperties(false)
.build();
schemaRegistry.register(personSchema);
schemaRegistry.register(relationshipSummarySchema);
const personCollection = new TypedCollectionImpl<{ name: string }>('people', personSchema, schemaRegistry);
personCollection.rhizomeConnect(node);
// Create people
await personCollection.put('alice', { name: 'Alice' });
await personCollection.put('bob', { name: 'Bob' });
// Create a relationship delta with one entity reference and multiple scalars
const relationshipDelta = createDelta(node.config.creator, node.config.peerId)
.addPointer('people', 'alice', 'relationships')
.addPointer('partner', 'bob')
.addPointer('type', 'friendship')
.addPointer('since', '2020-01-15')
.addPointer('intensity', 8)
.buildV1();
node.lossless.ingestDelta(relationshipDelta);
// Test from Alice's perspective
const aliceViews = node.lossless.compose(['alice']);
const aliceView = aliceViews['alice'];
const nestedAliceView = schemaRegistry.applySchemaWithNesting(
aliceView,
'person',
node.lossless,
{ maxDepth: 2 }
);
expect(nestedAliceView.id).toBe('alice');
expect(nestedAliceView.nestedObjects.relationships).toBeDefined();
if (nestedAliceView.nestedObjects.relationships) {
expect(nestedAliceView.nestedObjects.relationships.length).toBe(1);
const relationshipEntry = nestedAliceView.nestedObjects.relationships[0];
// Should be resolved with relationship-summary schema
expect(relationshipEntry.schemaId).toBe('relationship-summary');
// Should contain scalar values and reference to bob but not alice
expect(relationshipEntry.id).not.toBe('alice');
// The relationship should contain the scalar values from the delta
// Note: The exact structure depends on how the resolution logic handles mixed values
}
});
test('should correctly identify multiple entity references within a single delta', async () => {
// Test a scenario with multiple entity references that should all be resolved
const projectSchema = SchemaBuilder
.create('project')
.name('Project')
.property('name', PrimitiveSchemas.requiredString())
.property('collaborations', ReferenceSchemas.to('collaboration-summary', 3))
.required('name')
.build();
const collaborationSummarySchema = SchemaBuilder
.create('collaboration-summary')
.name('Collaboration Summary')
.property('project_name', PrimitiveSchemas.string())
.property('developer_name', PrimitiveSchemas.string())
.property('designer_name', PrimitiveSchemas.string())
.property('budget', PrimitiveSchemas.number())
.additionalProperties(false)
.build();
schemaRegistry.register(projectSchema);
schemaRegistry.register(collaborationSummarySchema);
const projectCollection = new TypedCollectionImpl<{ name: string }>('projects', projectSchema, schemaRegistry);
const developerCollection = new TypedCollectionImpl<{ name: string }>('developers', projectSchema, schemaRegistry);
const designerCollection = new TypedCollectionImpl<{ name: string }>('designers', projectSchema, schemaRegistry);
projectCollection.rhizomeConnect(node);
developerCollection.rhizomeConnect(node);
designerCollection.rhizomeConnect(node);
// Create entities
await projectCollection.put('website', { name: 'Company Website' });
await developerCollection.put('alice', { name: 'Alice Developer' });
await designerCollection.put('bob', { name: 'Bob Designer' });
// Create a collaboration delta with multiple entity references
const collaborationDelta = createDelta(node.config.creator, node.config.peerId)
.addPointer('projects', 'website', 'collaborations')
.addPointer('developer', 'alice')
.addPointer('designer', 'bob')
.addPointer('budget', 50000)
.addPointer('deadline', '2024-06-01')
.buildV1();
node.lossless.ingestDelta(collaborationDelta);
// Test from project's perspective
const projectViews = node.lossless.compose(['website']);
const projectView = projectViews['website'];
const nestedProjectView = schemaRegistry.applySchemaWithNesting(
projectView,
'project',
node.lossless,
{ maxDepth: 2 }
);
expect(nestedProjectView.id).toBe('website');
expect(nestedProjectView.nestedObjects.collaborations).toBeDefined();
if (nestedProjectView.nestedObjects.collaborations) {
// Verify we get exactly 1 composite object (not 2 separate objects)
expect(nestedProjectView.nestedObjects.collaborations.length).toBe(1);
const collaboration = nestedProjectView.nestedObjects.collaborations[0];
expect(collaboration.schemaId).toBe('collaboration-summary');
expect(collaboration.id).toMatch(/^composite-/); // Should be a synthetic composite ID
// Verify the composite object contains scalar properties
expect(collaboration.properties.budget).toBe(50000);
expect(collaboration.properties.deadline).toBe('2024-06-01');
// Verify the composite object contains nested entity references
expect(collaboration.nestedObjects.developer).toBeDefined();
expect(collaboration.nestedObjects.designer).toBeDefined();
// The nested entities should be resolved as arrays with single objects
const developers = collaboration.nestedObjects.developer as SchemaAppliedViewWithNesting[];
const designers = collaboration.nestedObjects.designer as SchemaAppliedViewWithNesting[];
// Each should have exactly one entity
expect(developers.length).toBe(1);
expect(designers.length).toBe(1);
// Verify each entity reference resolves to the correct entity
expect(developers[0].id).toBe('alice');
expect(developers[0].schemaId).toBe('collaboration-summary');
expect(designers[0].id).toBe('bob');
expect(designers[0].schemaId).toBe('collaboration-summary');
}
});
});
});