/** * Duplicate Relationship Check Optimization Tests * * Tests for v5.8.0 optimization that uses GraphAdjacencyIndex * for O(log n) duplicate detection instead of O(n) storage scan. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Brainy } from '../../../src/brainy.js' import { NounType, VerbType } from '../../../src/types/graphTypes.js' describe('Duplicate Check Optimization', () => { let brain: Brainy beforeEach(async () => { brain = new Brainy({ requireSubtype: false }) await brain.init() }) afterEach(async () => { // Cleanup is automatic with memory storage }) it('should detect duplicate relationships using GraphAdjacencyIndex', async () => { // Create two entities const personId = await brain.add({ data: { name: 'Alice' }, type: NounType.Person }) const orgId = await brain.add({ data: { name: 'Acme Corp' }, type: NounType.Organization }) // Create first relationship const relationId1 = await brain.relate({ from: personId, to: orgId, type: VerbType.ParticipatesIn }) // Attempt to create duplicate relationship const relationId2 = await brain.relate({ from: personId, to: orgId, type: VerbType.ParticipatesIn }) // Should return the same ID (duplicate detected) expect(relationId2).toBe(relationId1) // Verify only one relationship exists const relations = await brain.related({ from: personId }) expect(relations).toHaveLength(1) expect(relations[0].id).toBe(relationId1) }) it('should allow different relationship types between same entities', async () => { const personId = await brain.add({ data: { name: 'Bob' }, type: NounType.Person }) const projectId = await brain.add({ data: { name: 'Project X' }, type: NounType.Thing }) // Create first relationship const relationId1 = await brain.relate({ from: personId, to: projectId, type: VerbType.Creates }) // Create second relationship with different type (not a duplicate) const relationId2 = await brain.relate({ from: personId, to: projectId, type: VerbType.Modifies }) // Should be different IDs (different verb types) expect(relationId2).not.toBe(relationId1) // Verify both relationships exist const relations = await brain.related({ from: personId }) expect(relations).toHaveLength(2) // Both relations should exist with different IDs const relationIds = relations.map(r => r.id) expect(relationIds).toContain(relationId1) expect(relationIds).toContain(relationId2) }) it('should handle duplicate check with many relationships (performance)', async () => { // Create source entity const sourceId = await brain.add({ data: { name: 'Hub Entity' }, type: NounType.Thing }) // Create 50 target entities and relationships const targetIds: string[] = [] for (let i = 0; i < 50; i++) { const targetId = await brain.add({ data: { name: `Target ${i}` }, type: NounType.Thing }) targetIds.push(targetId) await brain.relate({ from: sourceId, to: targetId, type: VerbType.RelatesTo }) } // Now attempt to create duplicate with first target (should be fast with GraphIndex) const startTime = performance.now() const duplicateId = await brain.relate({ from: sourceId, to: targetIds[0], type: VerbType.RelatesTo }) const elapsed = performance.now() - startTime // Should be fast with O(log n) GraphIndex lookup (< 10ms even with 50 relationships) expect(elapsed).toBeLessThan(10) // Verify duplicate was detected const relations = await brain.related({ from: sourceId }) expect(relations).toHaveLength(50) // No duplicate created }) it('should use cached verb data for duplicate check', async () => { const entityA = await brain.add({ data: { name: 'Entity A' }, type: NounType.Thing }) const entityB = await brain.add({ data: { name: 'Entity B' }, type: NounType.Thing }) // Create relationship const relationId1 = await brain.relate({ from: entityA, to: entityB, type: VerbType.RelatesTo }) // Access GraphIndex to ensure verb is cached (8.0 BigInt boundary: // UUID → entity int in, verb ints out, resolved back via verbIntsToIds) const idMapper = (brain as any).metadataIndex.getIdMapper() const sourceInt = BigInt(idMapper.getInt(entityA)!) const verbInts: bigint[] = await (brain as any).graphIndex.getVerbIdsBySource(sourceInt) const verbIds = await (brain as any).graphIndex.verbIntsToIds(verbInts) expect(verbIds).toContain(relationId1) // Attempt duplicate (should use cached verb) const startTime = performance.now() const relationId2 = await brain.relate({ from: entityA, to: entityB, type: VerbType.RelatesTo }) const elapsed = performance.now() - startTime // Should be very fast with cached verb (< 5ms) expect(elapsed).toBeLessThan(5) expect(relationId2).toBe(relationId1) }) it('should handle duplicate check across multiple verb types efficiently', async () => { const person = await brain.add({ data: { name: 'Charlie' }, type: NounType.Person }) const org = await brain.add({ data: { name: 'BigCorp' }, type: NounType.Organization }) // Create different relationship types const rel1 = await brain.relate({ from: person, to: org, type: VerbType.Affects }) const rel2 = await brain.relate({ from: person, to: org, type: VerbType.Owns }) // Verify both relationships exist let relations = await brain.related({ from: person }) expect(relations).toHaveLength(2) const relationIds = relations.map(r => r.id) expect(relationIds).toContain(rel1) expect(relationIds).toContain(rel2) // Attempt duplicate of first relationship type const startTime = performance.now() const duplicate = await brain.relate({ from: person, to: org, type: VerbType.Affects }) const elapsed = performance.now() - startTime // Should detect duplicate efficiently (< 20ms) expect(elapsed).toBeLessThan(20) expect(duplicate).toBe(rel1) // Verify still same number of relationships (no duplicate added) const finalRelations = await brain.related({ from: person }) expect(finalRelations.length).toBe(relations.length) }) })