ai-functions
Version:
Core AI primitives for building intelligent applications
478 lines (406 loc) • 13.4 kB
text/typescript
/**
* Tests for define and function registry
*
* These tests use real AI calls via the Cloudflare AI Gateway.
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { define, defineFunction, functions, createFunctionRegistry, resetGlobalRegistry } from '../src/index.js'
// Skip tests if no gateway configured
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
describe('functions registry', () => {
beforeEach(() => {
resetGlobalRegistry()
})
it('starts empty', () => {
expect(functions.list()).toEqual([])
})
it('tracks defined functions', () => {
const fn = defineFunction({
type: 'generative',
name: 'testFunc',
args: { input: 'Input text' },
output: 'string',
})
functions.set('testFunc', fn)
expect(functions.has('testFunc')).toBe(true)
expect(functions.list()).toContain('testFunc')
})
it('retrieves defined functions', () => {
const fn = defineFunction({
type: 'generative',
name: 'testFunc',
args: { input: 'Input text' },
output: 'string',
})
functions.set('testFunc', fn)
const retrieved = functions.get('testFunc')
expect(retrieved).toBeDefined()
expect(retrieved?.definition.name).toBe('testFunc')
})
it('deletes functions', () => {
const fn = defineFunction({
type: 'generative',
name: 'testFunc',
args: { input: 'Input text' },
output: 'string',
})
functions.set('testFunc', fn)
expect(functions.delete('testFunc')).toBe(true)
expect(functions.has('testFunc')).toBe(false)
})
it('clears all functions', () => {
const fn1 = defineFunction({
type: 'generative',
name: 'func1',
args: {},
output: 'string',
})
const fn2 = defineFunction({
type: 'generative',
name: 'func2',
args: {},
output: 'string',
})
functions.set('func1', fn1)
functions.set('func2', fn2)
resetGlobalRegistry()
expect(functions.list()).toEqual([])
})
})
describe('createFunctionRegistry', () => {
beforeEach(() => {
resetGlobalRegistry()
})
it('creates an isolated registry instance', () => {
const registry = createFunctionRegistry()
expect(registry.list()).toEqual([])
})
it('registry operations do not affect global registry', () => {
// Add function to global registry
const globalFn = defineFunction({
type: 'generative',
name: 'globalFunc',
args: { input: 'Test input' },
output: 'string',
})
functions.set('globalFunc', globalFn)
// Create isolated registry and add different function
const registry = createFunctionRegistry()
const isolatedFn = defineFunction({
type: 'generative',
name: 'isolatedFunc',
args: { data: 'Test data' },
output: 'string',
})
registry.set('isolatedFunc', isolatedFn)
// Verify isolation
expect(functions.has('globalFunc')).toBe(true)
expect(functions.has('isolatedFunc')).toBe(false)
expect(registry.has('isolatedFunc')).toBe(true)
expect(registry.has('globalFunc')).toBe(false)
})
it('multiple registries are independent of each other', () => {
const registry1 = createFunctionRegistry()
const registry2 = createFunctionRegistry()
const fn1 = defineFunction({
type: 'generative',
name: 'func1',
args: {},
output: 'string',
})
const fn2 = defineFunction({
type: 'generative',
name: 'func2',
args: {},
output: 'string',
})
registry1.set('func1', fn1)
registry2.set('func2', fn2)
expect(registry1.has('func1')).toBe(true)
expect(registry1.has('func2')).toBe(false)
expect(registry2.has('func1')).toBe(false)
expect(registry2.has('func2')).toBe(true)
})
it('isolated registry supports all operations', () => {
const registry = createFunctionRegistry()
const fn = defineFunction({
type: 'generative',
name: 'testFunc',
args: { x: 'number' },
output: 'string',
})
// set and get
registry.set('testFunc', fn)
expect(registry.get('testFunc')).toBe(fn)
// has
expect(registry.has('testFunc')).toBe(true)
// list
expect(registry.list()).toEqual(['testFunc'])
// delete
expect(registry.delete('testFunc')).toBe(true)
expect(registry.has('testFunc')).toBe(false)
// clear
registry.set('a', fn)
registry.set('b', fn)
registry.clear()
expect(registry.list()).toEqual([])
})
})
describe('resetGlobalRegistry', () => {
it('clears all functions from global registry', () => {
const fn = defineFunction({
type: 'generative',
name: 'testFunc',
args: {},
output: 'string',
})
functions.set('testFunc', fn)
expect(functions.has('testFunc')).toBe(true)
resetGlobalRegistry()
expect(functions.has('testFunc')).toBe(false)
expect(functions.list()).toEqual([])
})
it('does not affect isolated registries', () => {
const isolatedRegistry = createFunctionRegistry()
const fn = defineFunction({
type: 'generative',
name: 'isolatedFunc',
args: {},
output: 'string',
})
isolatedRegistry.set('isolatedFunc', fn)
// Add to global and reset
functions.set('globalFunc', fn)
resetGlobalRegistry()
// Global should be empty, isolated should still have function
expect(functions.list()).toEqual([])
expect(isolatedRegistry.has('isolatedFunc')).toBe(true)
})
it('can be called multiple times safely', () => {
resetGlobalRegistry()
resetGlobalRegistry()
resetGlobalRegistry()
expect(functions.list()).toEqual([])
})
})
describe('defineFunction', () => {
beforeEach(() => {
resetGlobalRegistry()
})
it('creates a generative function definition', () => {
const fn = defineFunction({
type: 'generative',
name: 'summarize',
args: { text: 'Text to summarize' },
output: 'string',
system: 'You are a summarizer.',
})
expect(fn.definition.type).toBe('generative')
expect(fn.definition.name).toBe('summarize')
expect(typeof fn.call).toBe('function')
expect(typeof fn.asTool).toBe('function')
})
it('creates an agentic function definition', () => {
const fn = defineFunction({
type: 'agentic',
name: 'research',
args: { topic: 'Research topic' },
instructions: 'Research the topic thoroughly.',
maxIterations: 5,
})
expect(fn.definition.type).toBe('agentic')
expect(fn.definition.name).toBe('research')
})
it('creates a human function definition', () => {
const fn = defineFunction({
type: 'human',
name: 'approve',
args: { amount: 'Amount (number)' },
channel: 'workspace',
instructions: 'Review and approve.',
})
expect(fn.definition.type).toBe('human')
expect((fn.definition as { channel: string }).channel).toBe('workspace')
})
it('creates a code function definition', () => {
const fn = defineFunction({
type: 'code',
name: 'implement',
args: { spec: 'Function specification' },
language: 'typescript',
handler: () => 'ok',
})
expect(fn.definition.type).toBe('code')
expect((fn.definition as { language: string }).language).toBe('typescript')
})
it('generates asTool with correct parameters', () => {
const fn = defineFunction({
type: 'generative',
name: 'translate',
description: 'Translate text to another language',
args: {
text: 'Text to translate',
targetLang: 'Target language',
},
output: 'string',
})
const tool = fn.asTool()
expect(tool.name).toBe('translate')
expect(tool.description).toBe('Translate text to another language')
expect(tool.parameters.type).toBe('object')
expect(tool.parameters.properties).toHaveProperty('text')
expect(tool.parameters.properties).toHaveProperty('targetLang')
expect(tool.parameters.required).toContain('text')
expect(tool.parameters.required).toContain('targetLang')
})
})
describe('define helpers', () => {
beforeEach(() => {
resetGlobalRegistry()
})
it('define.generative registers function', () => {
const fn = define.generative({
name: 'greet',
args: { name: 'Name to greet' },
output: 'string',
})
expect(functions.has('greet')).toBe(true)
expect(fn.definition.type).toBe('generative')
})
it('define.agentic registers function', () => {
const fn = define.agentic({
name: 'analyze',
args: { data: 'Data to analyze' },
instructions: 'Analyze the data.',
})
expect(functions.has('analyze')).toBe(true)
expect(fn.definition.type).toBe('agentic')
})
it('define.human registers function', () => {
const fn = define.human({
name: 'review',
args: { content: 'Content to review' },
channel: 'web',
instructions: 'Review the content.',
})
expect(functions.has('review')).toBe(true)
expect(fn.definition.type).toBe('human')
})
it('define.code registers function', () => {
const fn = define.code({
name: 'generate',
args: { prompt: 'Code generation prompt' },
language: 'typescript',
handler: () => 'ok',
})
expect(functions.has('generate')).toBe(true)
expect(fn.definition.type).toBe('code')
})
})
// Code is the DETERMINISTIC kind — no gateway required. These tests assert the
// post-split contract: `type: 'code'` runs a handler (or inline body), never a
// model, at call time. (Code-authoring moved to generateCode().)
describe('code function execution (deterministic)', () => {
beforeEach(() => {
resetGlobalRegistry()
})
it('runs the supplied handler with no LLM call', async () => {
const calculateTax = defineFunction<number, { amount: number; rate: number }>({
type: 'code',
name: 'calculateTax',
args: { amount: 'Amount (number)', rate: 'Rate (number)' },
handler: ({ amount, rate }) => amount * rate,
})
const result = await calculateTax.call({ amount: 100, rate: 0.2 })
expect(result).toBe(20)
// Determinism: same input → same output, every time
expect(await calculateTax.call({ amount: 100, rate: 0.2 })).toBe(20)
})
it('awaits an async handler', async () => {
const fn = defineFunction<string, { name: string }>({
type: 'code',
name: 'greetAsync',
args: { name: 'Name' },
handler: async ({ name }) => `hi ${name}`,
})
expect(await fn.call({ name: 'world' })).toBe('hi world')
})
it('evaluates an inline code body deterministically', async () => {
const fn = defineFunction<number, { items: number[] }>({
type: 'code',
name: 'sum',
args: { items: ['Numbers'] },
language: 'typescript',
code: 'return args.items.reduce((a, b) => a + b, 0)',
})
expect(await fn.call({ items: [1, 2, 3, 4] })).toBe(10)
})
it('throws (does not call a model) when no handler or code is provided', async () => {
const fn = defineFunction({
type: 'code',
name: 'noImpl',
args: { spec: 'spec' },
language: 'typescript',
})
await expect(fn.call({ spec: 'x' })).rejects.toThrow(/no handler or inline code/i)
})
it('define.code runs the handler', async () => {
const fn = define.code<number, { a: number; b: number }>({
name: 'add',
args: { a: 'a (number)', b: 'b (number)' },
handler: ({ a, b }) => a + b,
})
expect(await fn.call({ a: 2, b: 3 })).toBe(5)
})
})
describe.skipIf(!hasGateway)('generative function execution', () => {
beforeEach(() => {
resetGlobalRegistry()
})
it('executes a generative string function', async () => {
const greet = define.generative({
name: 'greet',
args: { name: 'Name to greet' },
output: 'string',
promptTemplate: 'Say hello to {{name}}',
})
const result = await greet.call({ name: 'World' })
expect(typeof result).toBe('string')
expect((result as string).toLowerCase()).toContain('hello')
})
it('executes a generative object function', async () => {
const analyze = define.generative({
name: 'analyze',
args: { text: 'Text to analyze' },
output: 'object',
returnType: {
sentiment: 'positive | negative | neutral',
confidence: 'Confidence 0-1 (number)',
},
promptTemplate: 'Analyze the sentiment of: {{text}}',
})
const result = await analyze.call({ text: 'I love this!' }) as { sentiment: string; confidence: number }
expect(result).toBeDefined()
expect(['positive', 'negative', 'neutral']).toContain(result.sentiment)
expect(typeof result.confidence).toBe('number')
})
})
describe.skipIf(!hasGateway)('auto-define', () => {
beforeEach(() => {
resetGlobalRegistry()
})
it('auto-defines a function from name and args', async () => {
const fn = await define('translateText', {
text: 'Hello',
targetLanguage: 'French',
})
expect(fn).toBeDefined()
expect(fn.definition.name).toBe('translateText')
expect(functions.has('translateText')).toBe(true)
})
it('returns cached function on second call', async () => {
const fn1 = await define('greetUser', { name: 'Alice' })
const fn2 = await define('greetUser', { name: 'Bob' })
expect(fn1).toBe(fn2)
})
})