ai-functions
Version:
Core AI primitives for building intelligent applications
294 lines (293 loc) • 15.4 kB
JavaScript
/**
* Batch Blog Post Generation Test
*
* Tests the batch processing workflow where:
* 1. list`10 blog post titles` executes immediately
* 2. The mapped write operations are deferred to a batch
* 3. The batch is submitted to the provider (OpenAI/Anthropic)
*
* @example
* ```ts
* const titles = await list`10 blog post titles about building startups in 2026`
* const posts = titles.map(title => batch.add(write`blog post about ${title}`))
* const job = await batch.submit()
* const results = await batch.wait()
* ```
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { createBatch, withBatch, } from '../src/batch-queue.js';
// Import memory adapter to register it
import '../src/batch/memory.js';
import { configureMemoryAdapter, clearBatches } from '../src/batch/memory.js';
// ============================================================================
// Mock Setup
// ============================================================================
// Mock the generate functions
vi.mock('../src/generate.js', () => ({
generateObject: vi.fn().mockImplementation(async ({ prompt, schema }) => {
// Simulate list generation
if (schema?.items) {
return {
object: {
items: [
'How AI is Revolutionizing Startup Fundraising in 2026',
'The Rise of Solo Founders: Building $10M ARR Companies Alone',
'Why Remote-First is Non-Negotiable for 2026 Startups',
'Sustainable Growth vs Hypergrowth: The 2026 Paradigm Shift',
'Building in Public: How Transparency Became a Competitive Advantage',
'The API-First Startup: Lessons from 2026 Unicorns',
'From Side Project to Series A: The 2026 Playbook',
'Climate Tech Startups: The Hottest Sector of 2026',
'The Death of Traditional MVPs: Ship Faster, Learn Faster',
'Community-Led Growth: The New GTM Strategy for 2026',
],
},
};
}
// Simulate blog post generation
if (prompt.includes('blog post about')) {
const titleMatch = prompt.match(/blog post about (.+)/);
const title = titleMatch?.[1] || 'Unknown Topic';
return {
object: {
text: `# ${title}\n\nThis is a comprehensive blog post about ${title}.\n\n## Introduction\n\nIn 2026, the startup landscape continues to evolve...\n\n## Key Takeaways\n\n1. Innovation is key\n2. Focus on customer value\n3. Build sustainable businesses\n\n## Conclusion\n\nThe future of startups is bright for those who adapt.`,
},
};
}
return { object: { result: 'Generated content' } };
}),
generateText: vi.fn().mockImplementation(async ({ prompt }) => {
// Simulate blog post text generation
if (prompt.includes('blog post about')) {
const titleMatch = prompt.match(/blog post about (.+)/);
const title = titleMatch?.[1] || 'Unknown Topic';
return {
text: `# ${title}\n\nThis is a comprehensive blog post about ${title}.\n\n## Introduction\n\nIn 2026, the startup landscape continues to evolve rapidly. Entrepreneurs are finding new ways to build, scale, and succeed.\n\n## The State of Startups in 2026\n\nThe ecosystem has matured significantly. AI tools have become indispensable, funding patterns have shifted, and remote work is now the default.\n\n## Key Strategies for Success\n\n1. **Leverage AI Wisely** - Use AI as a multiplier, not a replacement\n2. **Build Community First** - Your early adopters are your growth engine\n3. **Focus on Unit Economics** - Hypergrowth without sustainability is dead\n4. **Embrace Transparency** - Building in public creates trust and accountability\n\n## Practical Steps\n\n- Start with a problem you deeply understand\n- Validate with paying customers, not surveys\n- Build the smallest thing that delivers value\n- Iterate based on real usage data\n\n## Conclusion\n\nBuilding a startup in 2026 requires a blend of traditional business fundamentals and modern tools. The founders who succeed will be those who can navigate this balance effectively.`,
};
}
return { text: 'Generated text content' };
}),
}));
// ============================================================================
// Test Helpers
// ============================================================================
/**
* Simulate the list template function
*/
async function mockList(prompt) {
const { generateObject } = await import('../src/generate.js');
const result = await generateObject({
model: 'sonnet',
schema: { items: ['List items'] },
prompt,
});
return result.object.items;
}
// ============================================================================
// Tests
// ============================================================================
describe('Batch Blog Post Generation', () => {
beforeEach(() => {
vi.clearAllMocks();
clearBatches();
// Use default handler that calls the mock
configureMemoryAdapter({});
});
afterEach(() => {
clearBatches();
});
describe('list` immediate execution', () => {
it('list` executes immediately and returns titles', async () => {
const titles = await mockList('10 blog post titles about building startups in 2026');
expect(titles).toHaveLength(10);
expect(titles[0]).toBe('How AI is Revolutionizing Startup Fundraising in 2026');
expect(titles[9]).toBe('Community-Led Growth: The New GTM Strategy for 2026');
});
});
describe('batch processing workflow', () => {
it('creates batch queue and adds items', async () => {
const batch = createBatch({ provider: 'openai', model: 'gpt-4o' });
const titles = await mockList('10 blog post titles about building startups in 2026');
// Add each title to the batch
const items = titles.map((title) => batch.add(`Write a comprehensive blog post about: ${title}`, {
customId: title.slice(0, 50).replace(/\s+/g, '-').toLowerCase(),
}));
expect(batch.size).toBe(10);
expect(items).toHaveLength(10);
expect(items[0].status).toBe('pending');
});
it('submits batch and returns job info', async () => {
const batch = createBatch({ provider: 'openai', model: 'gpt-4o' });
const titles = await mockList('10 blog post titles about building startups in 2026');
titles.forEach((title) => batch.add(`Write a comprehensive blog post about: ${title}`));
const { job, completion } = await batch.submit();
expect(job.id).toMatch(/^batch_memory_/);
expect(job.provider).toBe('openai');
expect(job.totalItems).toBe(10);
expect(job.status).toBe('pending');
// Wait for completion
const results = await completion;
expect(results).toHaveLength(10);
});
it('waits for batch completion and returns results', async () => {
const batch = createBatch({ provider: 'openai', model: 'gpt-4o' });
const titles = await mockList('10 blog post titles about building startups in 2026');
titles.forEach((title) => batch.add(`Write a comprehensive blog post about: ${title}`));
await batch.submit();
const results = await batch.wait();
expect(results).toHaveLength(10);
expect(results.every((r) => r.status === 'completed')).toBe(true);
expect(results[0].result).toBeDefined();
});
it('processes items in order', async () => {
const batch = createBatch({ provider: 'openai' });
const titles = ['First', 'Second', 'Third'];
const items = titles.map((title, i) => batch.add(`Write about: ${title}`, { customId: `item_${i}` }));
await batch.submit();
const results = await batch.wait();
expect(results[0].id).toBe('item_0');
expect(results[1].id).toBe('item_1');
expect(results[2].id).toBe('item_2');
});
});
describe('withBatchQueue helper', () => {
it('provides convenient batch execution', async () => {
const titles = await mockList('10 blog post titles about building startups in 2026');
const results = await withBatch((batch) => titles.map((title) => batch.add(`Write a blog post about: ${title}`)), { provider: 'openai', model: 'gpt-4o' });
expect(results).toHaveLength(10);
expect(results.every((r) => r.status === 'completed')).toBe(true);
});
});
describe('batch status tracking', () => {
it('tracks completion progress', async () => {
const batch = createBatch({ provider: 'openai' });
batch.add('Write post 1');
batch.add('Write post 2');
batch.add('Write post 3');
const { job } = await batch.submit();
expect(job.completedItems).toBe(0);
// Wait for completion
await batch.wait();
const finalStatus = await batch.getStatus();
expect(finalStatus.status).toBe('completed');
expect(finalStatus.completedItems).toBe(3);
});
});
describe('error handling', () => {
it('handles partial failures', async () => {
// Configure adapter to fail 30% of requests
configureMemoryAdapter({ failureRate: 0.3 });
const batch = createBatch({ provider: 'openai' });
for (let i = 0; i < 10; i++) {
batch.add(`Write post ${i}`);
}
await batch.submit();
const results = await batch.wait();
// Some should fail, some should succeed
const succeeded = results.filter((r) => r.status === 'completed').length;
const failed = results.filter((r) => r.status === 'failed').length;
expect(succeeded + failed).toBe(10);
// With 30% failure rate, expect roughly 3 failures (with some variance)
expect(failed).toBeGreaterThanOrEqual(0);
expect(failed).toBeLessThanOrEqual(10);
});
it('prevents adding items after submission', async () => {
const batch = createBatch({ provider: 'openai' });
batch.add('Write post 1');
await batch.submit();
expect(() => batch.add('Write post 2')).toThrow('Cannot add items to a submitted batch');
});
it('prevents double submission', async () => {
const batch = createBatch({ provider: 'openai' });
batch.add('Write post 1');
await batch.submit();
await expect(batch.submit()).rejects.toThrow('Batch has already been submitted');
});
it('prevents empty batch submission', async () => {
const batch = createBatch({ provider: 'openai' });
await expect(batch.submit()).rejects.toThrow('Cannot submit empty batch');
});
});
describe('batch with custom handler', () => {
it('uses custom handler for processing', async () => {
const customHandler = vi.fn().mockImplementation(async (item) => {
return `Custom result for: ${item.prompt}`;
});
configureMemoryAdapter({ handler: customHandler });
const batch = createBatch({ provider: 'openai' });
batch.add('Topic 1');
batch.add('Topic 2');
await batch.submit();
const results = await batch.wait();
expect(customHandler).toHaveBeenCalledTimes(2);
expect(results[0].result).toBe('Custom result for: Topic 1');
expect(results[1].result).toBe('Custom result for: Topic 2');
});
});
describe('full workflow: list → map → batch', () => {
it('executes the complete blog post generation workflow', async () => {
// Step 1: Get titles (executes immediately)
const titles = await mockList('10 blog post titles about building startups in 2026');
expect(titles).toHaveLength(10);
// Step 2: Create batch for blog posts (deferred)
const batch = createBatch({
provider: 'openai',
model: 'gpt-4o',
metadata: { task: 'blog-generation', topic: 'startups-2026' },
});
// Step 3: Map titles to batch items
const blogItems = titles.map((title, index) => batch.add(`Write a comprehensive blog post about: ${title}`, {
customId: `blog-${index}`,
metadata: { title },
}));
expect(batch.size).toBe(10);
expect(blogItems.every((item) => item.status === 'pending')).toBe(true);
// Step 4: Submit the batch
const { job, completion } = await batch.submit();
expect(job.id).toBeDefined();
expect(job.totalItems).toBe(10);
expect(batch.isSubmitted).toBe(true);
// Step 5: Wait for results
const results = await completion;
expect(results).toHaveLength(10);
expect(results.every((r) => r.status === 'completed')).toBe(true);
// Verify results have blog post content
for (const result of results) {
expect(result.result).toBeDefined();
expect(typeof result.result).toBe('string');
// Blog posts should have some content
expect(result.result.length).toBeGreaterThan(100);
}
// Verify items are updated after completion
expect(blogItems.every((item) => item.status === 'completed')).toBe(true);
});
});
});
describe('Provider-specific batch behavior', () => {
beforeEach(() => {
clearBatches();
configureMemoryAdapter({});
});
it('uses specified provider', async () => {
const openAIBatch = createBatch({ provider: 'openai' });
const anthropicBatch = createBatch({ provider: 'anthropic' });
openAIBatch.add('Test prompt');
anthropicBatch.add('Test prompt');
const { job: oaiJob } = await openAIBatch.submit();
const { job: antJob } = await anthropicBatch.submit();
// Memory adapter simulates OpenAI for all providers
expect(oaiJob.provider).toBe('openai');
expect(antJob.provider).toBe('openai');
});
it('respects model configuration', async () => {
const customHandler = vi.fn().mockResolvedValue('Result');
configureMemoryAdapter({ handler: customHandler });
const batch = createBatch({ provider: 'openai', model: 'gpt-4o-mini' });
batch.add('Test prompt');
await batch.submit();
await batch.wait();
// The model should be passed to the handler via batch options
// (memory adapter doesn't use it, but real adapters would)
expect(customHandler).toHaveBeenCalled();
});
});