UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

380 lines (314 loc) 11.7 kB
/** * Implicit Batch Processing Tests * * Tests the automatic batch detection when using .map() without explicit await. * Provider and model come from execution context, not code. * * @example * ```ts * // Configure globally or via environment * configure({ provider: 'openai', model: 'gpt-4o', batchMode: 'auto' }) * * // Use naturally - batch is automatic * const titles = await list`10 blog post titles` * const posts = titles.map(title => write`blog post: # ${title}`) * console.log(await posts) // Batched automatically! * ``` */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { configure, resetContext, withContext, getProvider, getModel, getBatchMode, shouldUseBatchAPI, getExecutionTier, getFlexThreshold, getBatchThreshold, isFlexAvailable, createBatchMap, BatchMapPromise, } from '../src/index.js' import { captureOperation } from '../src/batch-map.js' // Memory adapter for testing - simulates batch processing locally // Import from .ts file for proper vite resolution import { configureMemoryAdapter, clearBatches } from '../src/batch/memory.ts' // ============================================================================ // Tests // ============================================================================ describe('Implicit Batch Processing', () => { beforeEach(() => { resetContext() clearBatches() configureMemoryAdapter({}) }) afterEach(() => { resetContext() clearBatches() }) describe('Execution Context', () => { it('uses global configuration', () => { configure({ provider: 'anthropic', model: 'claude-sonnet-4-20250514', batchMode: 'auto', }) expect(getProvider()).toBe('anthropic') expect(getModel()).toBe('claude-sonnet-4-20250514') expect(getBatchMode()).toBe('auto') }) it('supports withContext for scoped configuration', async () => { configure({ provider: 'openai', model: 'gpt-4o' }) await withContext({ provider: 'anthropic', model: 'claude-opus-4-20250514' }, async () => { expect(getProvider()).toBe('anthropic') expect(getModel()).toBe('claude-opus-4-20250514') }) // Back to global after context exits expect(getProvider()).toBe('openai') }) }) describe('Batch Detection', () => { it('shouldUseBatchAPI returns true for large batches', () => { configure({ batchMode: 'auto', batchThreshold: 5 }) expect(shouldUseBatchAPI(3)).toBe(false) expect(shouldUseBatchAPI(5)).toBe(true) expect(shouldUseBatchAPI(10)).toBe(true) }) it('batchMode: deferred always uses batch API', () => { configure({ batchMode: 'deferred' }) expect(shouldUseBatchAPI(1)).toBe(true) expect(shouldUseBatchAPI(100)).toBe(true) }) it('batchMode: immediate never uses batch API', () => { configure({ batchMode: 'immediate' }) expect(shouldUseBatchAPI(1)).toBe(false) expect(shouldUseBatchAPI(100)).toBe(false) }) }) describe('Three-Tier Execution (immediate -> flex -> batch)', () => { it('getExecutionTier returns immediate for < flexThreshold items', () => { configure({ batchMode: 'auto', flexThreshold: 5, batchThreshold: 500 }) expect(getExecutionTier(1)).toBe('immediate') expect(getExecutionTier(3)).toBe('immediate') expect(getExecutionTier(4)).toBe('immediate') }) it('getExecutionTier returns flex for flexThreshold to < batchThreshold items', () => { configure({ batchMode: 'auto', flexThreshold: 5, batchThreshold: 500 }) expect(getExecutionTier(5)).toBe('flex') expect(getExecutionTier(10)).toBe('flex') expect(getExecutionTier(100)).toBe('flex') expect(getExecutionTier(499)).toBe('flex') }) it('getExecutionTier returns batch for >= batchThreshold items', () => { configure({ batchMode: 'auto', flexThreshold: 5, batchThreshold: 500 }) expect(getExecutionTier(500)).toBe('batch') expect(getExecutionTier(1000)).toBe('batch') expect(getExecutionTier(50000)).toBe('batch') }) it('respects custom thresholds', () => { configure({ batchMode: 'auto', flexThreshold: 10, batchThreshold: 100 }) // immediate: < 10 expect(getExecutionTier(5)).toBe('immediate') expect(getExecutionTier(9)).toBe('immediate') // flex: 10-99 expect(getExecutionTier(10)).toBe('flex') expect(getExecutionTier(50)).toBe('flex') expect(getExecutionTier(99)).toBe('flex') // batch: 100+ expect(getExecutionTier(100)).toBe('batch') expect(getExecutionTier(200)).toBe('batch') }) it('batchMode: flex always returns flex tier', () => { configure({ batchMode: 'flex' }) expect(getExecutionTier(1)).toBe('flex') expect(getExecutionTier(10)).toBe('flex') expect(getExecutionTier(1000)).toBe('flex') }) it('batchMode: immediate always returns immediate tier', () => { configure({ batchMode: 'immediate' }) expect(getExecutionTier(1)).toBe('immediate') expect(getExecutionTier(100)).toBe('immediate') expect(getExecutionTier(1000)).toBe('immediate') }) it('batchMode: deferred always returns batch tier', () => { configure({ batchMode: 'deferred' }) expect(getExecutionTier(1)).toBe('batch') expect(getExecutionTier(100)).toBe('batch') expect(getExecutionTier(1000)).toBe('batch') }) it('getFlexThreshold returns configured value or default', () => { // Default resetContext() expect(getFlexThreshold()).toBe(5) // Custom configure({ flexThreshold: 10 }) expect(getFlexThreshold()).toBe(10) }) it('getBatchThreshold returns configured value or default', () => { // Default resetContext() expect(getBatchThreshold()).toBe(500) // Custom configure({ batchThreshold: 1000 }) expect(getBatchThreshold()).toBe(1000) }) }) describe('Flex Availability', () => { it('isFlexAvailable returns true for openai', () => { configure({ provider: 'openai' }) expect(isFlexAvailable()).toBe(true) }) it('isFlexAvailable returns true for bedrock', () => { configure({ provider: 'bedrock' }) expect(isFlexAvailable()).toBe(true) }) it('isFlexAvailable returns false for anthropic (no native flex)', () => { configure({ provider: 'anthropic' }) expect(isFlexAvailable()).toBe(false) }) it('isFlexAvailable returns true for google', () => { configure({ provider: 'google' }) expect(isFlexAvailable()).toBe(true) }) it('isFlexAvailable returns false for cloudflare', () => { configure({ provider: 'cloudflare' }) expect(isFlexAvailable()).toBe(false) }) }) describe('Operation Recording', () => { it('captures operations during createBatchMap', () => { const items = ['Topic A', 'Topic B', 'Topic C'] let recordedCount = 0 // Create batch map - this enters recording mode for each item const batchMap = createBatchMap(items, (item) => { // Capture operation for each item captureOperation(`Write about: ${item}`, 'text', undefined, undefined) recordedCount++ return `result_${item}` }) expect(batchMap.size).toBe(3) expect(recordedCount).toBe(3) }) }) describe('BatchMapPromise', () => { it('resolves with immediate execution for small batches', async () => { configure({ batchMode: 'immediate' }) const items = ['A', 'B', 'C'] const batchMap = new BatchMapPromise<string>( items, items.map((item) => [ { id: `op_${item}`, prompt: `Write about: ${item}`, itemPlaceholder: item, type: 'text' as const, }, ]), { immediate: true } ) const results = await batchMap expect(results).toHaveLength(3) // Results should contain generated text results.forEach((result) => { expect(typeof result).toBe('string') }) }) it('supports iteration over results', async () => { configure({ batchMode: 'immediate' }) const items = ['X', 'Y'] const batchMap = new BatchMapPromise<string>( items, items.map((item) => [ { id: `op_${item}`, prompt: `Generate: ${item}`, itemPlaceholder: item, type: 'text' as const, }, ]), { immediate: true } ) const collected: string[] = [] const results = await batchMap for (const result of results) { collected.push(result) } expect(collected).toHaveLength(2) }) }) describe('Full Workflow', () => { it('list -> map -> batch flow works end-to-end', async () => { // Configure for immediate execution (for testing) configure({ batchMode: 'immediate', provider: 'openai', model: 'gpt-4o' }) // Step 1: Simulate getting titles (in production this would be AI-generated) const titles = ['Title 1', 'Title 2', 'Title 3', 'Title 4', 'Title 5'] // Step 2: Map to blog posts // In the real implementation, this would capture operations const batchMap = createBatchMap(titles, (title: string) => { // Capture the write operation captureOperation(`Write a blog post about: ${title}`, 'text') return title // Return value not used, operations are captured }) // Step 3: Await resolves the batch expect(batchMap.size).toBe(5) }) it('supports complex map with multiple operations per item', async () => { configure({ batchMode: 'immediate' }) const ideas = ['AI Assistant', 'Remote Tools', 'Dev Platform'] const batchMap = createBatchMap(ideas, (idea) => { // Multiple operations per item captureOperation(`Analyze: ${idea}`, 'object') captureOperation(`Is ${idea} viable?`, 'boolean') captureOperation(`Market for: ${idea}`, 'text') return { idea } }) expect(batchMap.size).toBe(3) // Each item should have 3 operations }) }) describe('Provider Integration', () => { it('falls back to immediate when adapter not available', async () => { // Configure for a provider without adapter registered configure({ batchMode: 'deferred', provider: 'google' }) const items = ['Test'] const batchMap = new BatchMapPromise<string>( items, [ [ { id: 'op_1', prompt: 'Test prompt', itemPlaceholder: 'Test', type: 'text' as const, }, ], ], { deferred: true } ) // Should not throw, falls back to immediate const results = await batchMap expect(results).toHaveLength(1) }) }) }) describe('API Design', () => { it('demonstrates the clean API', async () => { // This is how users will write code: // // const titles = await list`10 blog post titles about building startups in 2026` // const posts = titles.map(title => write`blog post targeting founders starting with "# ${title}"`) // console.log(await posts) // Batched automatically based on context! // // No need to specify provider, model, or batch configuration in the code. // Everything comes from environment variables or configure(): // // AI_PROVIDER=anthropic // AI_MODEL=claude-sonnet-4-20250514 // AI_BATCH_MODE=auto // AI_BATCH_THRESHOLD=5 // For this test, we just verify the types work expect(true).toBe(true) }) })