ai-functions
Version:
Core AI primitives for building intelligent applications
231 lines (198 loc) • 8.59 kB
text/typescript
/**
* Tests for the core generate() primitive
*
* generate(type, prompt, opts?) is the foundation that all other functions use.
* Tests require actual AI calls via the Cloudflare AI Gateway.
*/
import { describe, it, expect } from 'vitest'
import { generateObject, generateText } from '../src/generate.js'
import { z } from 'zod'
// Skip tests if no gateway configured
const hasGateway = !!process.env.AI_GATEWAY_URL
// ============================================================================
// generate(type, prompt, opts) signature tests
// ============================================================================
describe.skipIf(!hasGateway)('generate(type, prompt, opts)', () => {
describe('type: json', () => {
it('generates JSON without explicit schema (AI infers structure)', async () => {
const result = await generateObject({
model: 'haiku',
schema: z.object({
competitors: z.array(z.string()).describe('List of competitors'),
marketSize: z.number().describe('Estimated market size'),
}),
prompt:
'Provide a simple competitive analysis of the cloud computing market. List 2 competitors and an estimated market size in billions.',
})
expect(result.object).toHaveProperty('competitors')
expect(result.object).toHaveProperty('marketSize')
expect(Array.isArray(result.object.competitors)).toBe(true)
expect(typeof result.object.marketSize).toBe('number')
})
it('generates JSON with schema (typed, validated)', async () => {
const result = await generateObject({
model: 'haiku',
schema: z.object({
name: z.string().describe('Recipe name'),
servings: z.number().describe('Number of servings'),
ingredients: z.array(z.string()).describe('List of ingredients'),
steps: z.array(z.string()).describe('Cooking steps'),
}),
prompt: 'Generate a simple 3-ingredient recipe with 2 steps.',
})
expect(result.object).toHaveProperty('name')
expect(result.object).toHaveProperty('servings')
expect(typeof result.object.name).toBe('string')
expect(typeof result.object.servings).toBe('number')
expect(Array.isArray(result.object.ingredients)).toBe(true)
expect(Array.isArray(result.object.steps)).toBe(true)
})
})
describe('type: text', () => {
it('generates plain text', async () => {
const result = await generateText({
model: 'haiku',
prompt: 'Write one sentence about AI.',
})
expect(typeof result.text).toBe('string')
expect(result.text.length).toBeGreaterThan(10)
})
})
describe('type: code', () => {
it('generates code with language specified in prompt', async () => {
const result = await generateText({
model: 'haiku',
system:
'You are a code generator. Output only valid TypeScript code, no explanations or markdown.',
prompt:
'Write a TypeScript function called validateEmail that takes a string and returns boolean.',
})
expect(typeof result.text).toBe('string')
expect(result.text).toContain('function')
expect(result.text).toMatch(/validateEmail|email/i)
})
it('generates code in different languages', async () => {
const result = await generateText({
model: 'haiku',
system:
'You are a code generator. Output only valid Python code, no explanations or markdown.',
prompt:
'Write a Python function called validate_email that takes a string and returns a boolean.',
})
expect(typeof result.text).toBe('string')
expect(result.text).toContain('def')
})
})
describe('type: markdown', () => {
it('generates markdown content', async () => {
const result = await generateText({
model: 'haiku',
system: 'You write in markdown format.',
prompt: 'Write a very short README with a heading and 2 bullet points.',
})
expect(typeof result.text).toBe('string')
expect(result.text).toContain('#')
})
})
describe('type: yaml', () => {
it('generates YAML content', async () => {
const result = await generateText({
model: 'haiku',
system: 'You output only valid YAML, no explanations or markdown fences.',
prompt: 'Generate a simple YAML config with name: "test-app" and port: 3000.',
})
expect(typeof result.text).toBe('string')
expect(result.text.toLowerCase()).toContain('name')
})
})
describe('type: list', () => {
it('generates a list of items', async () => {
const result = await generateObject({
model: 'haiku',
schema: z.object({
items: z.array(z.string()).describe('List of startup ideas'),
}),
prompt: 'List exactly 3 startup ideas.',
})
expect(Array.isArray(result.object.items)).toBe(true)
expect(result.object.items.length).toBe(3)
})
})
describe('type: diagram', () => {
it('generates diagram code', async () => {
const result = await generateText({
model: 'haiku',
system:
'You generate Mermaid diagram code. Output only the diagram code, no explanations or markdown fences.',
prompt: 'Create a simple flowchart: Start -> Login -> Dashboard.',
})
expect(typeof result.text).toBe('string')
// Mermaid diagrams typically contain --> or -> for connections
expect(result.text).toMatch(/(flowchart|graph|-->|->)/i)
})
})
})
// ============================================================================
// Options parameter tests
// ============================================================================
describe.skipIf(!hasGateway)('generate options', () => {
it('respects temperature option (low temperature = more deterministic)', async () => {
// Low temperature should give consistent results
const result1 = await generateText({
model: 'haiku',
prompt: 'Say exactly "hello" and nothing else.',
temperature: 0,
})
const result2 = await generateText({
model: 'haiku',
prompt: 'Say exactly "hello" and nothing else.',
temperature: 0,
})
// With temperature 0, responses should be very similar
expect(result1.text.toLowerCase()).toContain('hello')
expect(result2.text.toLowerCase()).toContain('hello')
})
it('accepts maxTokens option without error', async () => {
// This test verifies the maxTokens option is passed through without error
// The actual truncation behavior is provider-dependent
const result = await generateText({
model: 'haiku',
prompt: 'Say "hello" and nothing else.',
maxTokens: 50,
})
// Just verify we got a response - maxTokens behavior varies by provider/gateway
expect(result.text).toBeDefined()
expect(typeof result.text).toBe('string')
})
it('passes system prompt correctly', async () => {
const result = await generateText({
model: 'haiku',
system: 'You always respond with exactly one word.',
prompt: 'What is your favorite color?',
})
// With the system prompt, response should be short (ideally one word)
const wordCount = result.text.trim().split(/\s+/).length
expect(wordCount).toBeLessThanOrEqual(3) // Allow some flexibility
})
})
// ============================================================================
// All convenience functions use generate
// ============================================================================
describe('convenience functions documentation', () => {
it('documents the mapping', () => {
// This test documents the expected mappings
const mappings = {
'ai(prompt)': 'generateText({ model, prompt })',
'write(prompt)': 'generateText({ model, prompt })',
'code(prompt)': "generateText({ model, system: 'code generator', prompt })",
'list(prompt)': 'generateObject({ model, schema: { items: [...] }, prompt })',
'lists(prompt)':
'generateObject({ model, schema: { listName1: [...], listName2: [...] }, prompt })',
'extract(prompt)': 'generateObject({ model, schema: { extracted: [...] }, prompt })',
'summarize(prompt)': "generateText({ model, system: 'summarizer', prompt })",
'diagram(prompt)': "generateText({ model, system: 'mermaid generator', prompt })",
'is(prompt)': 'generateObject({ model, schema: { result: boolean }, prompt })',
}
expect(Object.keys(mappings)).toHaveLength(9)
})
})