ai-functions
Version:
Core AI primitives for building intelligent applications
241 lines (240 loc) • 9.78 kB
JavaScript
/**
* Tests for tagged template literal support
*
* Every function should support:
* 1. Tagged template syntax: fn`prompt ${variable}`
* 2. Objects/arrays auto-convert to YAML in templates
* 3. Options chaining: fn`prompt`({ model: '...' })
*/
import { describe, it, expect } from 'vitest';
import { stringify } from 'yaml';
// ============================================================================
// Template Parsing Tests (Unit Tests - No AI calls)
// ============================================================================
/**
* Parse a tagged template literal into a prompt string
* Objects and arrays are converted to YAML for readability
*/
function parseTemplate(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i];
if (value === undefined) {
return result + str;
}
// Convert objects/arrays to YAML
if (typeof value === 'object' && value !== null) {
const yaml = stringify(value).trim();
return result + str + '\n' + yaml;
}
// Primitives use toString()
return result + str + String(value);
}, '');
}
/**
* Create a template function that supports both call styles
*/
function createTemplateFunction(handler) {
function templateFn(promptOrStrings, ...args) {
// Tagged template literal
if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
const prompt = parseTemplate(promptOrStrings, ...args);
// Return a thenable that also accepts options
const promise = handler(prompt);
// Allow chaining with options: fn`prompt`({ model: '...' })
// Create a function that also implements then/catch for Promise interface
const chainable = (options) => handler(prompt, options);
chainable.then = promise.then.bind(promise);
chainable.catch = promise.catch.bind(promise);
return chainable;
}
// Regular function call
return handler(promptOrStrings, args[0]);
}
return templateFn;
}
describe('template parsing', () => {
it('parses simple string interpolation', () => {
const topic = 'TypeScript';
const result = parseTemplate `Write about ${topic}`;
expect(result).toBe('Write about TypeScript');
});
it('parses multiple interpolations', () => {
const topic = 'TypeScript';
const audience = 'beginners';
const result = parseTemplate `Write about ${topic} for ${audience}`;
expect(result).toBe('Write about TypeScript for beginners');
});
it('converts objects to YAML', () => {
const context = { topic: 'TypeScript', level: 'beginner' };
const result = parseTemplate `Write a post about ${{ context }}`;
expect(result).toContain('context:');
expect(result).toContain('topic: TypeScript');
expect(result).toContain('level: beginner');
});
it('converts arrays to YAML lists', () => {
const topics = ['React', 'Vue', 'Angular'];
const result = parseTemplate `Compare ${topics}`;
expect(result).toContain('- React');
expect(result).toContain('- Vue');
expect(result).toContain('- Angular');
});
it('handles nested objects', () => {
const brand = {
hero: 'developers',
problem: {
internal: 'complexity',
external: 'time constraints',
},
};
const result = parseTemplate `Create a story for ${{ brand }}`;
expect(result).toContain('brand:');
expect(result).toContain('hero: developers');
expect(result).toContain('problem:');
expect(result).toContain('internal: complexity');
expect(result).toContain('external: time constraints');
});
it('handles numbers and booleans', () => {
const count = 10;
const active = true;
const result = parseTemplate `Generate ${count} items, active: ${active}`;
expect(result).toBe('Generate 10 items, active: true');
});
it('handles null and undefined gracefully', () => {
const value = null;
const result = parseTemplate `Value is ${value}`;
expect(result).toContain('Value is');
});
it('preserves template structure with objects inline', () => {
const requirements = {
pages: ['home', 'about', 'contact'],
features: ['dark mode', 'responsive'],
};
const result = parseTemplate `marketing site${{ requirements }}`;
expect(result).toContain('marketing site');
expect(result).toContain('requirements:');
expect(result).toContain('pages:');
expect(result).toContain('- home');
expect(result).toContain('features:');
expect(result).toContain('- dark mode');
});
});
describe('template function creation', () => {
it('creates a function that handles tagged templates', async () => {
const mockHandler = async (prompt) => `Processed: ${prompt}`;
const fn = createTemplateFunction(mockHandler);
const result = await fn `Hello ${'world'}`;
expect(result).toBe('Processed: Hello world');
});
it('creates a function that handles regular calls', async () => {
const mockHandler = async (prompt) => `Processed: ${prompt}`;
const fn = createTemplateFunction(mockHandler);
const result = await fn('Hello world');
expect(result).toBe('Processed: Hello world');
});
it('supports options chaining on tagged templates', async () => {
let capturedOptions;
const mockHandler = async (prompt, options) => {
capturedOptions = options;
return `Processed: ${prompt}`;
};
const fn = createTemplateFunction(mockHandler);
const result = await fn `Hello world`({ model: 'claude-opus-4-5' });
expect(result).toBe('Processed: Hello world');
expect(capturedOptions).toEqual({ model: 'claude-opus-4-5' });
});
it('passes options in regular calls', async () => {
let capturedOptions;
const mockHandler = async (prompt, options) => {
capturedOptions = options;
return `Processed: ${prompt}`;
};
const fn = createTemplateFunction(mockHandler);
await fn('Hello world', { model: 'gpt-5-1', temperature: 0.7 });
expect(capturedOptions).toEqual({ model: 'gpt-5-1', temperature: 0.7 });
});
it('converts complex objects to YAML in templates', async () => {
let capturedPrompt = '';
const mockHandler = async (prompt) => {
capturedPrompt = prompt;
return 'ok';
};
const fn = createTemplateFunction(mockHandler);
const brand = { hero: 'developers', guide: 'ai-functions' };
await fn `Create story for ${{ brand }}`;
expect(capturedPrompt).toContain('brand:');
expect(capturedPrompt).toContain('hero: developers');
expect(capturedPrompt).toContain('guide: ai-functions');
});
});
describe('options parameter', () => {
it('supports model option', async () => {
let capturedOptions;
const mockHandler = async (_prompt, options) => {
capturedOptions = options;
return 'ok';
};
const fn = createTemplateFunction(mockHandler);
await fn `test`({ model: 'claude-opus-4-5' });
expect(capturedOptions?.model).toBe('claude-opus-4-5');
});
it('supports thinking option', async () => {
let capturedOptions;
const mockHandler = async (_prompt, options) => {
capturedOptions = options;
return 'ok';
};
const fn = createTemplateFunction(mockHandler);
await fn `complex analysis`({ thinking: 'high' });
expect(capturedOptions?.thinking).toBe('high');
});
it('supports thinking as token budget number', async () => {
let capturedOptions;
const mockHandler = async (_prompt, options) => {
capturedOptions = options;
return 'ok';
};
const fn = createTemplateFunction(mockHandler);
await fn `complex analysis`({ thinking: 10000 });
expect(capturedOptions?.thinking).toBe(10000);
});
it('supports temperature option', async () => {
let capturedOptions;
const mockHandler = async (_prompt, options) => {
capturedOptions = options;
return 'ok';
};
const fn = createTemplateFunction(mockHandler);
await fn `creative writing`({ temperature: 0.9 });
expect(capturedOptions?.temperature).toBe(0.9);
});
it('supports maxTokens option', async () => {
let capturedOptions;
const mockHandler = async (_prompt, options) => {
capturedOptions = options;
return 'ok';
};
const fn = createTemplateFunction(mockHandler);
await fn `long article`({ maxTokens: 4000 });
expect(capturedOptions?.maxTokens).toBe(4000);
});
it('supports multiple options together', async () => {
let capturedOptions;
const mockHandler = async (_prompt, options) => {
capturedOptions = options;
return 'ok';
};
const fn = createTemplateFunction(mockHandler);
await fn `complex task`({
model: 'claude-opus-4-5',
thinking: 'high',
temperature: 0.7,
maxTokens: 8000,
});
expect(capturedOptions).toEqual({
model: 'claude-opus-4-5',
thinking: 'high',
temperature: 0.7,
maxTokens: 8000,
});
});
});