UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

478 lines (406 loc) 13.4 kB
/** * 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) }) })