/** * Domain and Time Clustering Tests * * Tests for clusterByDomain() and clusterByTime() methods * that were previously stub implementations. */ import { describe, it, expect, beforeEach } from 'vitest' import { Brainy } from '../../../src/brainy' import { NounType } from '../../../src/types/graphTypes' import { createAddParams } from '../../helpers/test-factory' describe('Domain and Time Clustering', () => { let brain: Brainy beforeEach(async () => { brain = new Brainy({ enableCache: false, storage: { type: 'memory' } // Use memory storage for tests }) await brain.init() }) describe('clusterByDomain() - Field-based clustering', () => { it.skip('should cluster entities by type field', async () => { // Add entities of different types await brain.add(createAddParams({ data: 'John Smith is a person', type: NounType.Person })) await brain.add(createAddParams({ data: 'Jane Doe is also a person', type: NounType.Person })) await brain.add(createAddParams({ data: 'Technical document about AI', type: NounType.Document })) await brain.add(createAddParams({ data: 'Research paper on machine learning', type: NounType.Document })) await brain.add(createAddParams({ data: 'Microsoft Corporation', type: NounType.Organization })) // Cluster by type field const clusters = await brain.neural().clusterByDomain('type', { minClusterSize: 1, maxClusters: 10 }) // Should have clusters for each type expect(Array.isArray(clusters)).toBe(true) expect(clusters.length).toBeGreaterThan(0) // Verify domain values exist const domains = new Set(clusters.map(c => c.domain)) expect(domains.has(NounType.Person) || domains.has('person')).toBe(true) expect(domains.has(NounType.Document) || domains.has('document')).toBe(true) }) it('should cluster entities by metadata field', async () => { // Add entities with category metadata await brain.add(createAddParams({ data: 'JavaScript programming guide', type: NounType.Document, metadata: { category: 'programming' } })) await brain.add(createAddParams({ data: 'Python tutorial', type: NounType.Document, metadata: { category: 'programming' } })) await brain.add(createAddParams({ data: 'Chocolate cake recipe', type: NounType.Document, metadata: { category: 'cooking' } })) await brain.add(createAddParams({ data: 'Pasta preparation', type: NounType.Document, metadata: { category: 'cooking' } })) // Cluster by category field const clusters = await brain.neural().clusterByDomain('category', { minClusterSize: 1, maxClusters: 5 }) expect(Array.isArray(clusters)).toBe(true) expect(clusters.length).toBeGreaterThan(0) // Verify categories are in domains const domains = new Set(clusters.map(c => c.domain)) expect(domains.has('programming')).toBe(true) expect(domains.has('cooking')).toBe(true) }) it.skip('should handle entities without the specified field', async () => { // Add entities with and without category await brain.add(createAddParams({ data: 'Has category', metadata: { category: 'tech' } })) await brain.add(createAddParams({ data: 'No category' })) const clusters = await brain.neural().clusterByDomain('category', { minClusterSize: 1 }) expect(Array.isArray(clusters)).toBe(true) // Should have 'tech' and 'unknown' domains const domains = new Set(clusters.map(c => c.domain)) expect(domains.has('tech')).toBe(true) expect(domains.has('unknown')).toBe(true) }) }) describe('clusterByTime() - Temporal clustering', () => { it('should cluster entities by time windows', async () => { const now = new Date() const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000) const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000) // Add entities with different timestamps await brain.add(createAddParams({ data: 'Recent item 1', metadata: { publishedAt: now.toISOString() } })) await brain.add(createAddParams({ data: 'Recent item 2', metadata: { publishedAt: oneDayAgo.toISOString() } })) await brain.add(createAddParams({ data: 'Old item 1', metadata: { publishedAt: oneWeekAgo.toISOString() } })) await brain.add(createAddParams({ data: 'Very old item', metadata: { publishedAt: oneMonthAgo.toISOString() } })) // Define time windows const timeWindows = [ { start: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000), // Last 2 days end: now, label: 'Recent' }, { start: new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000), // 2-14 days ago end: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000), label: 'This Week' }, { start: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000), // 14-60 days ago end: new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000), label: 'Older' } ] // Cluster by time const clusters = await brain.neural().clusterByTime('publishedAt', timeWindows, { timeField: 'publishedAt', windows: timeWindows }) expect(Array.isArray(clusters)).toBe(true) expect(clusters.length).toBeGreaterThan(0) // Verify time windows are represented const windowLabels = new Set(clusters.map(c => c.timeWindow?.label)) expect(windowLabels.size).toBeGreaterThan(0) }) it('should cluster entities by createdAt timestamps', async () => { // These will use the auto-generated createdAt timestamps const id1 = await brain.add(createAddParams({ data: 'First item' })) // Wait a bit to ensure different timestamps await new Promise(resolve => setTimeout(resolve, 10)) const id2 = await brain.add(createAddParams({ data: 'Second item' })) const now = new Date() const timeWindows = [ { start: new Date(now.getTime() - 60 * 60 * 1000), // Last hour end: new Date(now.getTime() + 60 * 60 * 1000), // Next hour (to include all) label: 'Now' } ] const clusters = await brain.neural().clusterByTime('createdAt', timeWindows, { timeField: 'createdAt', windows: timeWindows }) expect(Array.isArray(clusters)).toBe(true) // Both items should be in the 'Now' time window const nowCluster = clusters.find(c => c.timeWindow?.label === 'Now') expect(nowCluster).toBeDefined() if (nowCluster) { expect(nowCluster.members.length).toBeGreaterThanOrEqual(2) } }) it('should handle empty time windows gracefully', async () => { const futureStart = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year from now const futureEnd = new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000) // 2 years from now const timeWindows = [ { start: futureStart, end: futureEnd, label: 'Future' } ] const clusters = await brain.neural().clusterByTime('createdAt', timeWindows, { timeField: 'createdAt', windows: timeWindows }) // Should return empty array or array with empty clusters expect(Array.isArray(clusters)).toBe(true) }) }) describe('Cross-domain functionality', () => { it('should find cross-domain clusters when enabled', async () => { // Add entities from different domains with similar content await brain.add(createAddParams({ data: 'Machine learning and artificial intelligence', type: NounType.Document, metadata: { category: 'tech' } })) await brain.add(createAddParams({ data: 'AI and neural networks', type: NounType.Concept, metadata: { category: 'science' } })) const clusters = await brain.neural().clusterByDomain('category', { minClusterSize: 1, preserveDomainBoundaries: false, // Enable cross-domain clustering crossDomainThreshold: 0.5 }) expect(Array.isArray(clusters)).toBe(true) expect(clusters.length).toBeGreaterThan(0) }) }) })