ai-functions
Version:
Core AI primitives for building intelligent applications
483 lines (375 loc) • 14.4 kB
text/typescript
/**
* Tests for batch and background processing modes
*
* Batch mode: fn.batch([inputs]) - Process many inputs at ~50% discount
* Background mode: fn(..., { mode: 'background' }) - Returns job ID immediately
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
// ============================================================================
// Mock implementations
// ============================================================================
const mockBatchProcess = vi.fn()
const mockBackgroundProcess = vi.fn()
/**
* Create a mock function with batch support
*/
function createMockFunctionWithBatch<T>(
defaultHandler: (prompt: string) => Promise<T>
) {
const fn = async (prompt: string, options?: Record<string, unknown>) => {
if (options?.mode === 'background') {
return mockBackgroundProcess(prompt, options)
}
return defaultHandler(prompt)
}
// Add batch method
fn.batch = async (inputs: Array<string | Record<string, unknown>>) => {
return mockBatchProcess(inputs)
}
return fn
}
/**
* Create a tagged template function with batch support
*/
function createMockTemplateFunctionWithBatch<T>(
defaultHandler: (prompt: string) => Promise<T>
) {
function fn(promptOrStrings: string | TemplateStringsArray, ...args: unknown[]) {
let prompt: string
if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
prompt = (promptOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
return acc + str + (args[i] ?? '')
}, '')
} else {
prompt = promptOrStrings as string
}
return defaultHandler(prompt)
}
// Add batch method
fn.batch = async (inputs: Array<string | Record<string, unknown>>) => {
return mockBatchProcess(inputs)
}
return fn
}
// ============================================================================
// Batch mode tests
// ============================================================================
describe('batch mode', () => {
beforeEach(() => {
mockBatchProcess.mockReset()
})
describe('write.batch()', () => {
it('processes multiple prompts in batch', async () => {
const write = createMockFunctionWithBatch(async () => 'Generated content')
const prompts = [
'blog post about TypeScript',
'blog post about React',
'blog post about Next.js',
]
mockBatchProcess.mockResolvedValue([
'TypeScript content...',
'React content...',
'Next.js content...',
])
const results = await write.batch(prompts)
expect(mockBatchProcess).toHaveBeenCalledWith(prompts)
expect(results).toHaveLength(3)
})
it('processes object inputs with context', async () => {
const write = createMockFunctionWithBatch(async () => 'Generated content')
const brand = { voice: 'professional', audience: 'developers' }
const titles = ['Getting Started', 'Advanced Patterns', 'Best Practices']
const inputs = titles.map(title => ({
title,
brand,
tone: 'technical',
}))
mockBatchProcess.mockResolvedValue([
'Getting Started content...',
'Advanced Patterns content...',
'Best Practices content...',
])
const results = await write.batch(inputs)
expect(mockBatchProcess).toHaveBeenCalledWith(inputs)
expect(results).toHaveLength(3)
})
it('returns results in same order as inputs', async () => {
const write = createMockFunctionWithBatch(async () => 'content')
const inputs = ['first', 'second', 'third']
mockBatchProcess.mockResolvedValue([
'Result for first',
'Result for second',
'Result for third',
])
const results = await write.batch(inputs)
expect(results[0]).toContain('first')
expect(results[1]).toContain('second')
expect(results[2]).toContain('third')
})
})
describe('list.batch()', () => {
it('generates multiple lists in batch', async () => {
const list = createMockFunctionWithBatch(async () => ['item'])
mockBatchProcess.mockResolvedValue([
['TypeScript tip 1', 'TypeScript tip 2'],
['React tip 1', 'React tip 2'],
['Next.js tip 1', 'Next.js tip 2'],
])
const results = await list.batch([
'3 TypeScript tips',
'3 React tips',
'3 Next.js tips',
])
expect(results).toHaveLength(3)
expect(results[0]).toEqual(['TypeScript tip 1', 'TypeScript tip 2'])
})
})
describe('code.batch()', () => {
it('generates multiple code snippets in batch', async () => {
const code = createMockFunctionWithBatch(async () => 'code')
mockBatchProcess.mockResolvedValue([
'function validateEmail(email) { ... }',
'function validatePhone(phone) { ... }',
'function validateUrl(url) { ... }',
])
const results = await code.batch([
{ description: 'email validator', language: 'typescript' },
{ description: 'phone validator', language: 'typescript' },
{ description: 'url validator', language: 'typescript' },
])
expect(results).toHaveLength(3)
expect(results[0]).toContain('validateEmail')
})
})
describe('batch with options', () => {
it('accepts batch-level options', async () => {
const write = createMockFunctionWithBatch(async () => 'content')
// Simulating batch with options
const mockBatchWithOptions = vi.fn().mockResolvedValue(['r1', 'r2'])
const inputs = ['prompt1', 'prompt2']
const options = { model: 'claude-opus-4-5' }
await mockBatchWithOptions(inputs, options)
expect(mockBatchWithOptions).toHaveBeenCalledWith(inputs, options)
})
it('supports priority option for urgent batches', async () => {
const mockBatchWithPriority = vi.fn().mockResolvedValue(['result'])
await mockBatchWithPriority(['prompt'], { priority: 'high' })
expect(mockBatchWithPriority).toHaveBeenCalledWith(
['prompt'],
expect.objectContaining({ priority: 'high' })
)
})
})
})
// ============================================================================
// Background mode tests
// ============================================================================
describe('background mode', () => {
beforeEach(() => {
mockBackgroundProcess.mockReset()
})
it('returns job ID immediately', async () => {
const write = createMockFunctionWithBatch(async () => 'content')
mockBackgroundProcess.mockResolvedValue({
jobId: 'job_abc123',
status: 'pending',
})
const job = await write('long form article', { mode: 'background' })
expect(mockBackgroundProcess).toHaveBeenCalledWith(
'long form article',
expect.objectContaining({ mode: 'background' })
)
expect(job).toHaveProperty('jobId')
expect(job.status).toBe('pending')
})
it('can check job status', async () => {
const mockGetJobStatus = vi.fn()
// Simulating job status check
mockGetJobStatus.mockResolvedValueOnce({ status: 'processing' })
mockGetJobStatus.mockResolvedValueOnce({ status: 'completed', result: 'Generated content' })
const status1 = await mockGetJobStatus('job_abc123')
expect(status1.status).toBe('processing')
const status2 = await mockGetJobStatus('job_abc123')
expect(status2.status).toBe('completed')
expect(status2.result).toBe('Generated content')
})
it('supports webhook callback', async () => {
const write = createMockFunctionWithBatch(async () => 'content')
mockBackgroundProcess.mockResolvedValue({
jobId: 'job_xyz789',
status: 'pending',
})
await write('content', {
mode: 'background',
webhook: 'https://myapp.com/webhooks/ai-complete',
})
expect(mockBackgroundProcess).toHaveBeenCalledWith(
'content',
expect.objectContaining({
mode: 'background',
webhook: 'https://myapp.com/webhooks/ai-complete',
})
)
})
it('supports polling for result', async () => {
// Simulating a poll function
const mockPollForResult = vi.fn()
mockPollForResult.mockImplementation(async (jobId: string) => {
// Simulate polling - would normally check periodically
return { status: 'completed', result: 'Final result' }
})
const result = await mockPollForResult('job_abc123')
expect(result.status).toBe('completed')
expect(result.result).toBe('Final result')
})
})
// ============================================================================
// Combined batch and background
// ============================================================================
describe('batch + background mode', () => {
it('can run batch in background', async () => {
const mockBatchBackground = vi.fn()
mockBatchBackground.mockResolvedValue({
jobId: 'batch_job_123',
status: 'pending',
inputCount: 100,
})
// Large batch job in background
const job = await mockBatchBackground(
Array(100).fill('Generate content'),
{ mode: 'background' }
)
expect(job.jobId).toBe('batch_job_123')
expect(job.inputCount).toBe(100)
})
it('tracks progress of background batch', async () => {
const mockBatchProgress = vi.fn()
mockBatchProgress
.mockResolvedValueOnce({ status: 'processing', completed: 10, total: 100 })
.mockResolvedValueOnce({ status: 'processing', completed: 50, total: 100 })
.mockResolvedValueOnce({ status: 'completed', completed: 100, total: 100 })
const p1 = await mockBatchProgress('batch_job_123')
expect(p1.completed).toBe(10)
const p2 = await mockBatchProgress('batch_job_123')
expect(p2.completed).toBe(50)
const p3 = await mockBatchProgress('batch_job_123')
expect(p3.status).toBe('completed')
})
})
// ============================================================================
// Batch pricing and limits
// ============================================================================
describe('batch characteristics', () => {
it('batch provides cost savings (documentation test)', () => {
// This documents expected behavior
const batchInfo = {
discount: '50%',
turnaround: '24 hours max',
minBatchSize: 1,
maxBatchSize: 10000,
}
expect(batchInfo.discount).toBe('50%')
expect(batchInfo.turnaround).toBe('24 hours max')
})
it('batch handles errors gracefully', async () => {
const mockBatchWithErrors = vi.fn()
// Some items succeed, some fail
mockBatchWithErrors.mockResolvedValue({
results: [
{ index: 0, status: 'success', result: 'Content 1' },
{ index: 1, status: 'error', error: 'Content policy violation' },
{ index: 2, status: 'success', result: 'Content 3' },
],
summary: { succeeded: 2, failed: 1 },
})
const response = await mockBatchWithErrors(['p1', 'p2', 'p3'])
expect(response.summary.succeeded).toBe(2)
expect(response.summary.failed).toBe(1)
expect(response.results[1].status).toBe('error')
})
})
// ============================================================================
// Use cases from README
// ============================================================================
describe('batch use cases', () => {
beforeEach(() => {
mockBatchProcess.mockReset()
})
it('content generation at scale', async () => {
const write = createMockFunctionWithBatch(async () => 'content')
// Generate blog posts for 100 topics
const topics = Array(100)
.fill(null)
.map((_, i) => `Topic ${i + 1}`)
const inputs = topics.map(topic => ({
topic,
length: 'medium',
style: 'informative',
}))
mockBatchProcess.mockResolvedValue(
topics.map(t => `Blog post about ${t}...`)
)
const posts = await write.batch(inputs)
expect(posts).toHaveLength(100)
})
it('product description generation', async () => {
const write = createMockFunctionWithBatch(async () => 'description')
const products = [
{ name: 'Widget Pro', category: 'tools', features: ['durable', 'lightweight'] },
{ name: 'Gadget Plus', category: 'electronics', features: ['wireless', 'rechargeable'] },
]
mockBatchProcess.mockResolvedValue([
'Widget Pro is a durable, lightweight tool...',
'Gadget Plus is a wireless, rechargeable electronic...',
])
const descriptions = await write.batch(
products.map(p => ({
prompt: `product description for ${p.name}`,
product: p,
}))
)
expect(descriptions).toHaveLength(2)
expect(descriptions[0]).toContain('Widget Pro')
})
it('code generation for multiple functions', async () => {
const code = createMockFunctionWithBatch(async () => 'code')
const functions = [
{ name: 'validateEmail', description: 'Validate email format' },
{ name: 'validatePhone', description: 'Validate phone number' },
{ name: 'validateUrl', description: 'Validate URL format' },
]
mockBatchProcess.mockResolvedValue(
functions.map(f => `function ${f.name}(value) { ... }`)
)
const implementations = await code.batch(
functions.map(f => ({
description: f.description,
functionName: f.name,
language: 'typescript',
}))
)
expect(implementations).toHaveLength(3)
functions.forEach((f, i) => {
expect(implementations[i]).toContain(f.name)
})
})
})
// ============================================================================
// Error handling
// ============================================================================
describe('batch error handling', () => {
beforeEach(() => {
mockBatchProcess.mockReset()
})
it('handles empty input array', async () => {
const write = createMockFunctionWithBatch(async () => 'content')
mockBatchProcess.mockResolvedValue([])
const results = await write.batch([])
expect(results).toEqual([])
})
it('propagates batch-level errors', async () => {
const write = createMockFunctionWithBatch(async () => 'content')
mockBatchProcess.mockRejectedValue(new Error('Batch quota exceeded'))
await expect(write.batch(['prompt'])).rejects.toThrow('Batch quota exceeded')
})
})