/** * Streaming Pipeline Tests * Tests for the new streaming data pipeline system */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Pipeline, createPipeline } from '../src/streaming/pipeline.js' import { Brainy } from '../src/brainy.js' import { NounType } from '../src/types/graphTypes.js' describe('Streaming Pipeline', () => { let brain: Brainy beforeEach(async () => { brain = new Brainy({ storage: { type: 'memory' }, augmentations: {}, warmup: false }) await brain.init() }) afterEach(async () => { await brain.close() }) describe('Core Operations', () => { it('should process data through pipeline', async () => { const results: number[] = [] await new Pipeline() .source(async function* () { for (let i = 1; i <= 5; i++) { yield i } }) .map(x => x * 2) .filter(x => x > 4) .sink(x => { results.push(x) }) .run() expect(results).toEqual([6, 8, 10]) }) it('should handle async transformations', async () => { const results: string[] = [] await new Pipeline() .source(async function* () { yield 'hello' yield 'world' }) .map(async (text) => { await new Promise(resolve => setTimeout(resolve, 10)) return text.toUpperCase() }) .sink(x => { results.push(x) }) .run() expect(results).toEqual(['HELLO', 'WORLD']) }) it('should batch items', async () => { const batches: number[][] = [] await new Pipeline() .source(async function* () { for (let i = 1; i <= 10; i++) { yield i } }) .batch(3) .sink(batch => { batches.push(batch) }) .run() expect(batches).toEqual([ [1, 2, 3], [4, 5, 6], [7, 8, 9], [10] ]) }) it('should collect all results', async () => { const pipeline = new Pipeline() .source(async function* () { for (let i = 1; i <= 5; i++) { yield i } }) .map(x => x * x) const results = await pipeline.collect() expect(results).toEqual([1, 4, 9, 16, 25]) }) }) describe('Window Operations', () => { it('should support tumbling windows', async () => { const windows: number[][] = [] await new Pipeline() .source(async function* () { for (let i = 1; i <= 10; i++) { yield i } }) .window(3, 'tumbling') .sink(window => { windows.push(window) }) .run() expect(windows).toEqual([ [1, 2, 3], [4, 5, 6], [7, 8, 9], [10] ]) }) it('should support sliding windows', async () => { const windows: number[][] = [] await new Pipeline() .source(async function* () { for (let i = 1; i <= 5; i++) { yield i } }) .window(3, 'sliding') .sink(window => { windows.push([...window]) }) .run() expect(windows).toEqual([ [1, 2, 3], [2, 3, 4], [3, 4, 5] ]) }) }) describe('Reduce Operations', () => { it('should reduce values', async () => { let result = 0 await new Pipeline() .source(async function* () { for (let i = 1; i <= 5; i++) { yield i } }) .reduce((acc, val) => acc + val, 0) .sink(sum => { result = sum }) .run() expect(result).toBe(15) }) it('should work with complex reducers', async () => { interface Stats { count: number sum: number max: number } let stats: Stats = { count: 0, sum: 0, max: 0 } await new Pipeline() .source(async function* () { for (let i = 1; i <= 10; i++) { yield i } }) .reduce((acc, val) => ({ count: acc.count + 1, sum: acc.sum + val, max: Math.max(acc.max, val) }), { count: 0, sum: 0, max: 0 }) .sink(s => { stats = s }) .run() expect(stats).toEqual({ count: 10, sum: 55, max: 10 }) }) }) describe('Brainy Integration', () => { it('should sink data to Brainy', async () => { const pipeline = new Pipeline(brain) .source(async function* () { yield { content: 'Document 1' } yield { content: 'Document 2' } yield { content: 'Document 3' } }) .toBrainy({ type: NounType.Document, metadata: { source: 'pipeline' }, batchSize: 2 }) await pipeline.run() // Verify data was added const results = await brain.find({ where: { 'metadata.source': 'pipeline' } }) expect(results.length).toBe(3) }) it('should process and transform before storing', async () => { const pipeline = new Pipeline(brain) .source(async function* () { yield 'hello world' yield 'goodbye world' }) .map(text => ({ content: text, processed: text.toUpperCase() })) .toBrainy({ type: NounType.Document, metadata: { pipeline: true } }) await pipeline.run() const results = await brain.find({ where: { 'metadata.pipeline': true } }) expect(results.length).toBe(2) }) }) describe('Error Handling', () => { it('should handle errors with handler', async () => { const errors: Error[] = [] await new Pipeline() .source(async function* () { yield 1 yield 2 yield 3 }) .map(x => { if (x === 2) throw new Error('Test error') return x }) .sink(() => {}) .run({ errorHandler: (error) => errors.push(error) }) expect(errors.length).toBe(1) expect(errors[0].message).toBe('Test error') }) it('should stop on abort signal', async () => { let count = 0 const pipeline = new Pipeline() .source(async function* () { for (let i = 1; i <= 100; i++) { yield i } }) .sink(() => { count++ }) // Start pipeline const runPromise = pipeline.run() // Stop after a short delay setTimeout(() => pipeline.stop(), 50) await runPromise // Should have processed some but not all items expect(count).toBeGreaterThan(0) expect(count).toBeLessThan(100) }) }) describe('Performance Features', () => { it('should throttle sink operations', async () => { const times: number[] = [] const startTime = Date.now() await new Pipeline() .source(async function* () { for (let i = 1; i <= 3; i++) { yield i } }) .throttledSink( () => { times.push(Date.now() - startTime) }, 10 // 10 ops/sec max ) .run() // Check that operations were throttled expect(times.length).toBe(3) expect(times[2] - times[0]).toBeGreaterThanOrEqual(200) // At least 200ms for 3 items at 10/sec }) it('should run monitoring', async () => { const logs: string[] = [] const originalLog = console.log console.log = (msg: string) => logs.push(msg) try { await new Pipeline() .source(async function* () { for (let i = 1; i <= 10; i++) { yield i } }) .sink(() => {}) .run({ monitoring: true }) // Should have logged completion metrics expect(logs.some(log => log.includes('Pipeline completed'))).toBe(true) expect(logs.some(log => log.includes('Throughput'))).toBe(true) } finally { console.log = originalLog } }) }) })