ai-functions
Version:
Core AI primitives for building intelligent applications
380 lines (314 loc) • 11.7 kB
text/typescript
/**
* 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)
})
})