import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Brainy } from '../../src/brainy' import { NounType } from '../../src/types/graphTypes' import type { AggregateDefinition } from '../../src/types/brainy.types' describe('Aggregation Engine Integration', () => { let brain: Brainy beforeEach(async () => { brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true }) await brain.init() }) afterEach(async () => { await brain.close() }) // ============= Core: Define + Add + Query ============= describe('define → add → query lifecycle', () => { it('should accumulate metrics as entities are added', async () => { brain.defineAggregate({ name: 'spending', source: { type: NounType.Event, where: { domain: 'financial' } }, groupBy: ['category'], metrics: { total: { op: 'sum', field: 'amount' }, count: { op: 'count' }, average: { op: 'avg', field: 'amount' }, highest: { op: 'max', field: 'amount' }, lowest: { op: 'min', field: 'amount' } } }) // Add transactions await brain.add({ data: 'Coffee', type: NounType.Event, metadata: { domain: 'financial', category: 'food', amount: 5 } }) await brain.add({ data: 'Lunch', type: NounType.Event, metadata: { domain: 'financial', category: 'food', amount: 15 } }) await brain.add({ data: 'Uber', type: NounType.Event, metadata: { domain: 'financial', category: 'transport', amount: 25 } }) // Query all groups const results = await brain.find({ aggregate: 'spending' }) expect(results).toHaveLength(2) // Check food group const food = results.find(r => r.metadata?.category === 'food')! expect(food).toBeDefined() expect(food.metadata.total).toBe(20) expect(food.metadata.count).toBe(2) expect(food.metadata.average).toBe(10) expect(food.metadata.highest).toBe(15) expect(food.metadata.lowest).toBe(5) // Check transport group const transport = results.find(r => r.metadata?.category === 'transport')! expect(transport).toBeDefined() expect(transport.metadata.total).toBe(25) expect(transport.metadata.count).toBe(1) }) it('should filter aggregate results with where clause', async () => { brain.defineAggregate({ name: 'by_cat', source: { type: NounType.Event }, groupBy: ['category'], metrics: { total: { op: 'sum', field: 'amount' } } }) await brain.add({ data: 'Coffee', type: NounType.Event, metadata: { category: 'food', amount: 5 } }) await brain.add({ data: 'Uber', type: NounType.Event, metadata: { category: 'transport', amount: 25 } }) const results = await brain.find({ aggregate: 'by_cat', where: { category: 'food' } }) expect(results).toHaveLength(1) expect(results[0].metadata.category).toBe('food') }) it('should support sorting and pagination on aggregates', async () => { brain.defineAggregate({ name: 'by_cat', source: { type: NounType.Event }, groupBy: ['category'], metrics: { total: { op: 'sum', field: 'amount' } } }) const categories = ['food', 'transport', 'housing', 'entertainment'] for (let i = 0; i < categories.length; i++) { await brain.add({ data: `Expense ${i}`, type: NounType.Event, metadata: { category: categories[i], amount: (i + 1) * 100 } }) } // Sort descending, limit 2 const results = await brain.find({ aggregate: { name: 'by_cat', orderBy: 'total', order: 'desc', limit: 2 } }) expect(results).toHaveLength(2) expect(results[0].metadata.total).toBe(400) expect(results[1].metadata.total).toBe(300) }) }) // ============= Incremental Updates ============= describe('incremental updates', () => { it('should update aggregates when entities are modified', async () => { brain.defineAggregate({ name: 'totals', source: { type: NounType.Event }, groupBy: ['category'], metrics: { total: { op: 'sum', field: 'amount' }, count: { op: 'count' } } }) const id = await brain.add({ data: 'Coffee', type: NounType.Event, metadata: { category: 'food', amount: 5 } }) // Verify initial state let results = await brain.find({ aggregate: 'totals' }) expect(results[0].metadata.total).toBe(5) // Update amount await brain.update({ id, metadata: { category: 'food', amount: 15 } }) results = await brain.find({ aggregate: 'totals' }) expect(results[0].metadata.total).toBe(15) expect(results[0].metadata.count).toBe(1) }) it('should update aggregates when entities are deleted', async () => { brain.defineAggregate({ name: 'totals', source: { type: NounType.Event }, groupBy: ['category'], metrics: { total: { op: 'sum', field: 'amount' }, count: { op: 'count' } } }) const id1 = await brain.add({ data: 'Coffee', type: NounType.Event, metadata: { category: 'food', amount: 5 } }) await brain.add({ data: 'Lunch', type: NounType.Event, metadata: { category: 'food', amount: 15 } }) await brain.delete(id1) const results = await brain.find({ aggregate: 'totals' }) expect(results).toHaveLength(1) expect(results[0].metadata.total).toBe(15) expect(results[0].metadata.count).toBe(1) }) }) // ============= Time Windows ============= describe('time-windowed aggregates', () => { it('should group by month using time window', async () => { brain.defineAggregate({ name: 'monthly', source: { type: NounType.Event }, groupBy: [ 'category', { field: 'date', window: 'month' } ], metrics: { total: { op: 'sum', field: 'amount' }, count: { op: 'count' } } }) const jan = Date.UTC(2024, 0, 15) const feb = Date.UTC(2024, 1, 15) await brain.add({ data: 'Coffee Jan', type: NounType.Event, metadata: { category: 'food', date: jan, amount: 100 } }) await brain.add({ data: 'Coffee Feb', type: NounType.Event, metadata: { category: 'food', date: feb, amount: 200 } }) await brain.add({ data: 'Lunch Jan', type: NounType.Event, metadata: { category: 'food', date: jan, amount: 50 } }) const results = await brain.find({ aggregate: 'monthly' }) expect(results).toHaveLength(2) const janGroup = results.find(r => r.metadata.date === '2024-01')! const febGroup = results.find(r => r.metadata.date === '2024-02')! expect(janGroup.metadata.total).toBe(150) expect(janGroup.metadata.count).toBe(2) expect(febGroup.metadata.total).toBe(200) expect(febGroup.metadata.count).toBe(1) }) }) // ============= Non-matching entities ============= describe('source filtering', () => { it('should only aggregate entities matching the source filter', async () => { brain.defineAggregate({ name: 'financial_only', source: { type: NounType.Event, where: { domain: 'financial' } }, groupBy: ['category'], metrics: { count: { op: 'count' } } }) // Matching entity await brain.add({ data: 'Purchase', type: NounType.Event, metadata: { domain: 'financial', category: 'food' } }) // Non-matching: wrong type await brain.add({ data: 'Person', type: NounType.Person, metadata: { domain: 'financial', category: 'food' } }) // Non-matching: wrong domain await brain.add({ data: 'Game event', type: NounType.Event, metadata: { domain: 'gaming', category: 'food' } }) const results = await brain.find({ aggregate: 'financial_only' }) expect(results).toHaveLength(1) expect(results[0].metadata.count).toBe(1) }) }) // ============= Multiple Aggregates ============= describe('multiple overlapping aggregates', () => { it('should maintain independent aggregates on the same entities', async () => { brain.defineAggregate({ name: 'by_category', source: { type: NounType.Event }, groupBy: ['category'], metrics: { total: { op: 'sum', field: 'amount' } } }) brain.defineAggregate({ name: 'by_merchant', source: { type: NounType.Event }, groupBy: ['merchant'], metrics: { total: { op: 'sum', field: 'amount' } } }) await brain.add({ data: 'Coffee', type: NounType.Event, metadata: { category: 'food', merchant: 'Starbucks', amount: 5 } }) await brain.add({ data: 'Tea', type: NounType.Event, metadata: { category: 'food', merchant: 'Teavana', amount: 4 } }) const catResults = await brain.find({ aggregate: 'by_category' }) const merchResults = await brain.find({ aggregate: 'by_merchant' }) expect(catResults).toHaveLength(1) expect(catResults[0].metadata.total).toBe(9) expect(merchResults).toHaveLength(2) const starbucks = merchResults.find(r => r.metadata.merchant === 'Starbucks')! const teavana = merchResults.find(r => r.metadata.merchant === 'Teavana')! expect(starbucks.metadata.total).toBe(5) expect(teavana.metadata.total).toBe(4) }) }) // ============= Error Handling ============= describe('error handling', () => { it('should throw when querying an undefined aggregate', async () => { await expect(brain.find({ aggregate: 'nonexistent' })) .rejects.toThrow() }) it('should throw when defining aggregate without groupBy', () => { expect(() => brain.defineAggregate({ name: 'bad', source: { type: NounType.Event }, groupBy: [], metrics: { count: { op: 'count' } } })).toThrow() }) }) // ============= removeAggregate ============= describe('removeAggregate', () => { it('should remove aggregate and reject subsequent queries', async () => { brain.defineAggregate({ name: 'temp', source: { type: NounType.Event }, groupBy: ['category'], metrics: { count: { op: 'count' } } }) await brain.add({ data: 'Test', type: NounType.Event, metadata: { category: 'food' } }) // Aggregate exists const results = await brain.find({ aggregate: 'temp' }) expect(results).toHaveLength(1) // Remove it brain.removeAggregate('temp') // Should throw on query await expect(brain.find({ aggregate: 'temp' })).rejects.toThrow() }) }) // ============= Result format ============= describe('result format', () => { it('should return Result[] with Measurement type', async () => { brain.defineAggregate({ name: 'test', source: { type: NounType.Event }, groupBy: ['category'], metrics: { count: { op: 'count' } } }) await brain.add({ data: 'Test', type: NounType.Event, metadata: { category: 'food' } }) const results = await brain.find({ aggregate: 'test' }) expect(results).toHaveLength(1) const result = results[0] expect(result.id).toBeDefined() expect(result.score).toBe(1.0) expect(result.type).toBe(NounType.Measurement) expect(result.entity).toBeDefined() expect(result.entity.type).toBe(NounType.Measurement) expect(result.entity.service).toBe('brainy:aggregation') expect(result.metadata.__aggregate).toBe('test') }) }) // ============= Scale test ============= describe('scale', () => { it('should handle 100 entities with 3 aggregates', async () => { // Note: Full scale testing (10K+ entities) is in unit tests which skip // embedding overhead. This integration test verifies end-to-end wiring // with real Brainy operations including embeddings. brain.defineAggregate({ name: 'by_category', source: { type: NounType.Event }, groupBy: ['category'], metrics: { total: { op: 'sum', field: 'amount' }, count: { op: 'count' } } }) brain.defineAggregate({ name: 'by_merchant', source: { type: NounType.Event }, groupBy: ['merchant'], metrics: { total: { op: 'sum', field: 'amount' } } }) brain.defineAggregate({ name: 'monthly', source: { type: NounType.Event }, groupBy: [{ field: 'date', window: 'month' }], metrics: { total: { op: 'sum', field: 'amount' }, count: { op: 'count' } } }) const categories = ['food', 'transport', 'housing', 'entertainment', 'utilities'] const merchants = ['Starbucks', 'Uber', 'Amazon', 'Netflix', 'Con Edison'] const baseDate = Date.UTC(2024, 0, 1) for (let i = 0; i < 100; i++) { await brain.add({ data: `Transaction ${i}`, type: NounType.Event, metadata: { category: categories[i % 5], merchant: merchants[i % 5], amount: Math.round(Math.random() * 10000) / 100, date: baseDate + (i % 90) * 86400_000 } }) } // Verify category aggregate const catResults = await brain.find({ aggregate: 'by_category' }) expect(catResults).toHaveLength(5) const totalCount = catResults.reduce((sum, r) => sum + (r.metadata.count as number), 0) expect(totalCount).toBe(100) // Verify merchant aggregate const merchResults = await brain.find({ aggregate: 'by_merchant' }) expect(merchResults).toHaveLength(5) // Verify monthly aggregate const monthlyResults = await brain.find({ aggregate: 'monthly' }) expect(monthlyResults.length).toBeGreaterThan(0) const monthlyTotalCount = monthlyResults.reduce( (sum, r) => sum + (r.metadata.count as number), 0 ) expect(monthlyTotalCount).toBe(100) }, 60_000) }) })